mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e91e95287 | ||
|
|
c5dcbc1c1a | ||
|
|
4504ba5329 | ||
|
|
d16599fa1d | ||
|
|
674393ec12 | ||
|
|
9f45806106 | ||
|
|
307ae76ed4 | ||
|
|
735b21394c | ||
|
|
9cdef937af |
@@ -147,6 +147,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
writableBase := util.WritablePath()
|
||||||
if value, ok := lookupEnv("PGSTORE_DSN", "pgstore_dsn"); ok {
|
if value, ok := lookupEnv("PGSTORE_DSN", "pgstore_dsn"); ok {
|
||||||
usePostgresStore = true
|
usePostgresStore = true
|
||||||
pgStoreDSN = value
|
pgStoreDSN = value
|
||||||
@@ -158,6 +159,13 @@ func main() {
|
|||||||
if value, ok := lookupEnv("PGSTORE_LOCAL_PATH", "pgstore_local_path"); ok {
|
if value, ok := lookupEnv("PGSTORE_LOCAL_PATH", "pgstore_local_path"); ok {
|
||||||
pgStoreLocalPath = value
|
pgStoreLocalPath = value
|
||||||
}
|
}
|
||||||
|
if pgStoreLocalPath == "" {
|
||||||
|
if writableBase != "" {
|
||||||
|
pgStoreLocalPath = writableBase
|
||||||
|
} else {
|
||||||
|
pgStoreLocalPath = wd
|
||||||
|
}
|
||||||
|
}
|
||||||
useGitStore = false
|
useGitStore = false
|
||||||
}
|
}
|
||||||
if value, ok := lookupEnv("GITSTORE_GIT_URL", "gitstore_git_url"); ok {
|
if value, ok := lookupEnv("GITSTORE_GIT_URL", "gitstore_git_url"); ok {
|
||||||
@@ -229,11 +237,14 @@ func main() {
|
|||||||
log.Infof("postgres-backed token store enabled, workspace path: %s", pgStoreInst.WorkDir())
|
log.Infof("postgres-backed token store enabled, workspace path: %s", pgStoreInst.WorkDir())
|
||||||
}
|
}
|
||||||
} else if useObjectStore {
|
} else if useObjectStore {
|
||||||
objectStoreRoot := objectStoreLocalPath
|
if objectStoreLocalPath == "" {
|
||||||
if objectStoreRoot == "" {
|
if writableBase != "" {
|
||||||
objectStoreRoot = wd
|
objectStoreLocalPath = writableBase
|
||||||
|
} else {
|
||||||
|
objectStoreLocalPath = wd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
objectStoreRoot = filepath.Join(objectStoreRoot, "objectstore")
|
objectStoreRoot := filepath.Join(objectStoreLocalPath, "objectstore")
|
||||||
resolvedEndpoint := strings.TrimSpace(objectStoreEndpoint)
|
resolvedEndpoint := strings.TrimSpace(objectStoreEndpoint)
|
||||||
useSSL := true
|
useSSL := true
|
||||||
if strings.Contains(resolvedEndpoint, "://") {
|
if strings.Contains(resolvedEndpoint, "://") {
|
||||||
@@ -289,7 +300,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
} else if useGitStore {
|
} else if useGitStore {
|
||||||
if gitStoreLocalPath == "" {
|
if gitStoreLocalPath == "" {
|
||||||
gitStoreLocalPath = wd
|
if writableBase != "" {
|
||||||
|
gitStoreLocalPath = writableBase
|
||||||
|
} else {
|
||||||
|
gitStoreLocalPath = wd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
gitStoreRoot = filepath.Join(gitStoreLocalPath, "gitstore")
|
gitStoreRoot = filepath.Join(gitStoreLocalPath, "gitstore")
|
||||||
authDir := filepath.Join(gitStoreRoot, "auths")
|
authDir := filepath.Join(gitStoreRoot, "auths")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -145,6 +146,9 @@ func (h *Handler) logDirectory() string {
|
|||||||
if h.logDir != "" {
|
if h.logDir != "" {
|
||||||
return h.logDir
|
return h.logDir
|
||||||
}
|
}
|
||||||
|
if base := util.WritablePath(); base != "" {
|
||||||
|
return filepath.Join(base, "logs")
|
||||||
|
}
|
||||||
if h.configFilePath != "" {
|
if h.configFilePath != "" {
|
||||||
dir := filepath.Dir(h.configFilePath)
|
dir := filepath.Dir(h.configFilePath)
|
||||||
if dir != "" && dir != "." {
|
if dir != "" && dir != "." {
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ type serverOptionConfig struct {
|
|||||||
type ServerOption func(*serverOptionConfig)
|
type ServerOption func(*serverOptionConfig)
|
||||||
|
|
||||||
func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger {
|
func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger {
|
||||||
return logging.NewFileRequestLogger(cfg.RequestLog, "logs", filepath.Dir(configPath))
|
configDir := filepath.Dir(configPath)
|
||||||
|
if base := util.WritablePath(); base != "" {
|
||||||
|
return logging.NewFileRequestLogger(cfg.RequestLog, filepath.Join(base, "logs"), configDir)
|
||||||
|
}
|
||||||
|
return logging.NewFileRequestLogger(cfg.RequestLog, "logs", configDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMiddleware appends additional Gin middleware during server construction.
|
// WithMiddleware appends additional Gin middleware during server construction.
|
||||||
@@ -233,7 +237,11 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
|
|||||||
if optionState.localPassword != "" {
|
if optionState.localPassword != "" {
|
||||||
s.mgmt.SetLocalPassword(optionState.localPassword)
|
s.mgmt.SetLocalPassword(optionState.localPassword)
|
||||||
}
|
}
|
||||||
s.mgmt.SetLogDirectory(filepath.Join(s.currentPath, "logs"))
|
logDir := filepath.Join(s.currentPath, "logs")
|
||||||
|
if base := util.WritablePath(); base != "" {
|
||||||
|
logDir = filepath.Join(base, "logs")
|
||||||
|
}
|
||||||
|
s.mgmt.SetLogDirectory(logDir)
|
||||||
s.localPassword = optionState.localPassword
|
s.localPassword = optionState.localPassword
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
)
|
)
|
||||||
@@ -72,7 +73,10 @@ func ConfigureLogOutput(loggingToFile bool) error {
|
|||||||
defer writerMu.Unlock()
|
defer writerMu.Unlock()
|
||||||
|
|
||||||
if loggingToFile {
|
if loggingToFile {
|
||||||
const logDir = "logs"
|
logDir := "logs"
|
||||||
|
if base := util.WritablePath(); base != "" {
|
||||||
|
logDir = filepath.Join(base, "logs")
|
||||||
|
}
|
||||||
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("logging: failed to create log directory: %w", err)
|
return fmt.Errorf("logging: failed to create log directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequestLogger defines the interface for logging HTTP requests and responses.
|
// RequestLogger defines the interface for logging HTTP requests and responses.
|
||||||
@@ -485,7 +486,8 @@ func (l *FileRequestLogger) formatRequestInfo(url, method string, headers map[st
|
|||||||
content.WriteString("=== HEADERS ===\n")
|
content.WriteString("=== HEADERS ===\n")
|
||||||
for key, values := range headers {
|
for key, values := range headers {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
content.WriteString(fmt.Sprintf("%s: %s\n", key, value))
|
masked := util.MaskSensitiveHeaderValue(key, value)
|
||||||
|
content.WriteString(fmt.Sprintf("%s: %s\n", key, masked))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ func StaticDir(configFilePath string) string {
|
|||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if writable := util.WritablePath(); writable != "" {
|
||||||
|
return filepath.Join(writable, "static")
|
||||||
|
}
|
||||||
|
|
||||||
configFilePath = strings.TrimSpace(configFilePath)
|
configFilePath = strings.TrimSpace(configFilePath)
|
||||||
if configFilePath == "" {
|
if configFilePath == "" {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
@@ -214,18 +215,28 @@ func (e *IFlowExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshToken := ""
|
refreshToken := ""
|
||||||
|
oldAccessToken := ""
|
||||||
if auth.Metadata != nil {
|
if auth.Metadata != nil {
|
||||||
if v, ok := auth.Metadata["refresh_token"].(string); ok {
|
if v, ok := auth.Metadata["refresh_token"].(string); ok {
|
||||||
refreshToken = strings.TrimSpace(v)
|
refreshToken = strings.TrimSpace(v)
|
||||||
}
|
}
|
||||||
|
if v, ok := auth.Metadata["access_token"].(string); ok {
|
||||||
|
oldAccessToken = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if refreshToken == "" {
|
if refreshToken == "" {
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the old access token (masked) before refresh
|
||||||
|
if oldAccessToken != "" {
|
||||||
|
log.Debugf("iflow executor: refreshing access token, old: %s", util.HideAPIKey(oldAccessToken))
|
||||||
|
}
|
||||||
|
|
||||||
svc := iflowauth.NewIFlowAuth(e.cfg)
|
svc := iflowauth.NewIFlowAuth(e.cfg)
|
||||||
tokenData, err := svc.RefreshTokens(ctx, refreshToken)
|
tokenData, err := svc.RefreshTokens(ctx, refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf("iflow executor: token refresh failed: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +254,9 @@ func (e *IFlowExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*
|
|||||||
auth.Metadata["type"] = "iflow"
|
auth.Metadata["type"] = "iflow"
|
||||||
auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339)
|
auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339)
|
||||||
|
|
||||||
|
// Log the new access token (masked) after successful refresh
|
||||||
|
log.Debugf("iflow executor: token refresh successful, new: %s", util.HideAPIKey(tokenData.AccessToken))
|
||||||
|
|
||||||
if auth.Attributes == nil {
|
if auth.Attributes == nil {
|
||||||
auth.Attributes = make(map[string]string)
|
auth.Attributes = make(map[string]string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,7 +275,8 @@ func writeHeaders(builder *strings.Builder, headers http.Header) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
builder.WriteString(fmt.Sprintf("%s: %s\n", key, sanitizeHeaderValue(key, value)))
|
masked := util.MaskSensitiveHeaderValue(key, value)
|
||||||
|
builder.WriteString(fmt.Sprintf("%s: %s\n", key, masked))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,18 +320,3 @@ func formatAuthInfo(info upstreamRequestLog) string {
|
|||||||
|
|
||||||
return strings.Join(parts, ", ")
|
return strings.Join(parts, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeHeaderValue(key, value string) string {
|
|
||||||
trimmedValue := strings.TrimSpace(value)
|
|
||||||
lowerKey := strings.ToLower(strings.TrimSpace(key))
|
|
||||||
switch {
|
|
||||||
case strings.Contains(lowerKey, "authorization"),
|
|
||||||
strings.Contains(lowerKey, "api-key"),
|
|
||||||
strings.Contains(lowerKey, "apikey"),
|
|
||||||
strings.Contains(lowerKey, "token"),
|
|
||||||
strings.Contains(lowerKey, "secret"):
|
|
||||||
return util.HideAPIKey(trimmedValue)
|
|
||||||
default:
|
|
||||||
return trimmedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
package claude
|
package claude
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -180,56 +179,58 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Claude Code-compatible JSON response containing all message content and metadata
|
// - string: A Claude Code-compatible JSON response containing all message content and metadata
|
||||||
func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, _ *any) string {
|
func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, _ *any) string {
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(rawJSON))
|
|
||||||
buffer := make([]byte, 20_971_520)
|
|
||||||
scanner.Buffer(buffer, 20_971_520)
|
|
||||||
revNames := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
revNames := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
||||||
|
|
||||||
for scanner.Scan() {
|
rootResult := gjson.ParseBytes(rawJSON)
|
||||||
line := scanner.Bytes()
|
if rootResult.Get("type").String() != "response.completed" {
|
||||||
if !bytes.HasPrefix(line, dataTag) {
|
return ""
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
payload := bytes.TrimSpace(line[len(dataTag):])
|
|
||||||
if len(payload) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
rootResult := gjson.ParseBytes(payload)
|
responseData := rootResult.Get("response")
|
||||||
if rootResult.Get("type").String() != "response.completed" {
|
if !responseData.Exists() {
|
||||||
continue
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData := rootResult.Get("response")
|
response := map[string]interface{}{
|
||||||
if !responseData.Exists() {
|
"id": responseData.Get("id").String(),
|
||||||
continue
|
"type": "message",
|
||||||
}
|
"role": "assistant",
|
||||||
|
"model": responseData.Get("model").String(),
|
||||||
|
"content": []interface{}{},
|
||||||
|
"stop_reason": nil,
|
||||||
|
"stop_sequence": nil,
|
||||||
|
"usage": map[string]interface{}{
|
||||||
|
"input_tokens": responseData.Get("usage.input_tokens").Int(),
|
||||||
|
"output_tokens": responseData.Get("usage.output_tokens").Int(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
response := map[string]interface{}{
|
var contentBlocks []interface{}
|
||||||
"id": responseData.Get("id").String(),
|
hasToolCall := false
|
||||||
"type": "message",
|
|
||||||
"role": "assistant",
|
|
||||||
"model": responseData.Get("model").String(),
|
|
||||||
"content": []interface{}{},
|
|
||||||
"stop_reason": nil,
|
|
||||||
"stop_sequence": nil,
|
|
||||||
"usage": map[string]interface{}{
|
|
||||||
"input_tokens": responseData.Get("usage.input_tokens").Int(),
|
|
||||||
"output_tokens": responseData.Get("usage.output_tokens").Int(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentBlocks []interface{}
|
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
||||||
hasToolCall := false
|
output.ForEach(func(_, item gjson.Result) bool {
|
||||||
|
switch item.Get("type").String() {
|
||||||
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
case "reasoning":
|
||||||
output.ForEach(func(_, item gjson.Result) bool {
|
thinkingBuilder := strings.Builder{}
|
||||||
switch item.Get("type").String() {
|
if summary := item.Get("summary"); summary.Exists() {
|
||||||
case "reasoning":
|
if summary.IsArray() {
|
||||||
thinkingBuilder := strings.Builder{}
|
summary.ForEach(func(_, part gjson.Result) bool {
|
||||||
if summary := item.Get("summary"); summary.Exists() {
|
if txt := part.Get("text"); txt.Exists() {
|
||||||
if summary.IsArray() {
|
thinkingBuilder.WriteString(txt.String())
|
||||||
summary.ForEach(func(_, part gjson.Result) bool {
|
} else {
|
||||||
|
thinkingBuilder.WriteString(part.String())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
thinkingBuilder.WriteString(summary.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if thinkingBuilder.Len() == 0 {
|
||||||
|
if content := item.Get("content"); content.Exists() {
|
||||||
|
if content.IsArray() {
|
||||||
|
content.ForEach(func(_, part gjson.Result) bool {
|
||||||
if txt := part.Get("text"); txt.Exists() {
|
if txt := part.Get("text"); txt.Exists() {
|
||||||
thinkingBuilder.WriteString(txt.String())
|
thinkingBuilder.WriteString(txt.String())
|
||||||
} else {
|
} else {
|
||||||
@@ -238,114 +239,96 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
thinkingBuilder.WriteString(summary.String())
|
thinkingBuilder.WriteString(content.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if thinkingBuilder.Len() == 0 {
|
|
||||||
if content := item.Get("content"); content.Exists() {
|
|
||||||
if content.IsArray() {
|
|
||||||
content.ForEach(func(_, part gjson.Result) bool {
|
|
||||||
if txt := part.Get("text"); txt.Exists() {
|
|
||||||
thinkingBuilder.WriteString(txt.String())
|
|
||||||
} else {
|
|
||||||
thinkingBuilder.WriteString(part.String())
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
thinkingBuilder.WriteString(content.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if thinkingBuilder.Len() > 0 {
|
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
|
||||||
"type": "thinking",
|
|
||||||
"thinking": thinkingBuilder.String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
case "message":
|
|
||||||
if content := item.Get("content"); content.Exists() {
|
|
||||||
if content.IsArray() {
|
|
||||||
content.ForEach(func(_, part gjson.Result) bool {
|
|
||||||
if part.Get("type").String() == "output_text" {
|
|
||||||
text := part.Get("text").String()
|
|
||||||
if text != "" {
|
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
|
||||||
"type": "text",
|
|
||||||
"text": text,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
text := content.String()
|
|
||||||
if text != "" {
|
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
|
||||||
"type": "text",
|
|
||||||
"text": text,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "function_call":
|
|
||||||
hasToolCall = true
|
|
||||||
name := item.Get("name").String()
|
|
||||||
if original, ok := revNames[name]; ok {
|
|
||||||
name = original
|
|
||||||
}
|
|
||||||
|
|
||||||
toolBlock := map[string]interface{}{
|
|
||||||
"type": "tool_use",
|
|
||||||
"id": item.Get("call_id").String(),
|
|
||||||
"name": name,
|
|
||||||
"input": map[string]interface{}{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if argsStr := item.Get("arguments").String(); argsStr != "" {
|
|
||||||
var args interface{}
|
|
||||||
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
|
||||||
toolBlock["input"] = args
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentBlocks = append(contentBlocks, toolBlock)
|
|
||||||
}
|
}
|
||||||
return true
|
if thinkingBuilder.Len() > 0 {
|
||||||
})
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||||
}
|
"type": "thinking",
|
||||||
|
"thinking": thinkingBuilder.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "message":
|
||||||
|
if content := item.Get("content"); content.Exists() {
|
||||||
|
if content.IsArray() {
|
||||||
|
content.ForEach(func(_, part gjson.Result) bool {
|
||||||
|
if part.Get("type").String() == "output_text" {
|
||||||
|
text := part.Get("text").String()
|
||||||
|
if text != "" {
|
||||||
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||||
|
"type": "text",
|
||||||
|
"text": text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
text := content.String()
|
||||||
|
if text != "" {
|
||||||
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||||
|
"type": "text",
|
||||||
|
"text": text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "function_call":
|
||||||
|
hasToolCall = true
|
||||||
|
name := item.Get("name").String()
|
||||||
|
if original, ok := revNames[name]; ok {
|
||||||
|
name = original
|
||||||
|
}
|
||||||
|
|
||||||
if len(contentBlocks) > 0 {
|
toolBlock := map[string]interface{}{
|
||||||
response["content"] = contentBlocks
|
"type": "tool_use",
|
||||||
}
|
"id": item.Get("call_id").String(),
|
||||||
|
"name": name,
|
||||||
|
"input": map[string]interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
|
if argsStr := item.Get("arguments").String(); argsStr != "" {
|
||||||
response["stop_reason"] = stopReason.String()
|
var args interface{}
|
||||||
} else if hasToolCall {
|
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
||||||
response["stop_reason"] = "tool_use"
|
toolBlock["input"] = args
|
||||||
} else {
|
}
|
||||||
response["stop_reason"] = "end_turn"
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" {
|
contentBlocks = append(contentBlocks, toolBlock)
|
||||||
response["stop_sequence"] = stopSequence.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {
|
|
||||||
response["usage"] = map[string]interface{}{
|
|
||||||
"input_tokens": responseData.Get("usage.input_tokens").Int(),
|
|
||||||
"output_tokens": responseData.Get("usage.output_tokens").Int(),
|
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
responseJSON, err := json.Marshal(response)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(responseJSON)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
if len(contentBlocks) > 0 {
|
||||||
|
response["content"] = contentBlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
|
||||||
|
response["stop_reason"] = stopReason.String()
|
||||||
|
} else if hasToolCall {
|
||||||
|
response["stop_reason"] = "tool_use"
|
||||||
|
} else {
|
||||||
|
response["stop_reason"] = "end_turn"
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" {
|
||||||
|
response["stop_sequence"] = stopSequence.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {
|
||||||
|
response["usage"] = map[string]interface{}{
|
||||||
|
"input_tokens": responseData.Get("usage.input_tokens").Int(),
|
||||||
|
"output_tokens": responseData.Get("usage.output_tokens").Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJSON, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(responseJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
|
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -152,159 +151,146 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response containing all message content and metadata
|
// - string: A Gemini-compatible JSON response containing all message content and metadata
|
||||||
func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(rawJSON))
|
rootResult := gjson.ParseBytes(rawJSON)
|
||||||
buffer := make([]byte, 20_971_520)
|
|
||||||
scanner.Buffer(buffer, 20_971_520)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Bytes()
|
|
||||||
// log.Debug(string(line))
|
|
||||||
if !bytes.HasPrefix(line, dataTag) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
|
||||||
|
|
||||||
rootResult := gjson.ParseBytes(rawJSON)
|
// Verify this is a response.completed event
|
||||||
|
if rootResult.Get("type").String() != "response.completed" {
|
||||||
// Verify this is a response.completed event
|
return ""
|
||||||
if rootResult.Get("type").String() != "response.completed" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base Gemini response template for non-streaming
|
|
||||||
template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`
|
|
||||||
|
|
||||||
// Set model version
|
|
||||||
template, _ = sjson.Set(template, "modelVersion", modelName)
|
|
||||||
|
|
||||||
// Set response metadata from the completed response
|
|
||||||
responseData := rootResult.Get("response")
|
|
||||||
if responseData.Exists() {
|
|
||||||
// Set response ID
|
|
||||||
if responseId := responseData.Get("id"); responseId.Exists() {
|
|
||||||
template, _ = sjson.Set(template, "responseId", responseId.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set creation time
|
|
||||||
if createdAt := responseData.Get("created_at"); createdAt.Exists() {
|
|
||||||
template, _ = sjson.Set(template, "createTime", time.Unix(createdAt.Int(), 0).Format(time.RFC3339Nano))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set usage metadata
|
|
||||||
if usage := responseData.Get("usage"); usage.Exists() {
|
|
||||||
inputTokens := usage.Get("input_tokens").Int()
|
|
||||||
outputTokens := usage.Get("output_tokens").Int()
|
|
||||||
totalTokens := inputTokens + outputTokens
|
|
||||||
|
|
||||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens)
|
|
||||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
|
||||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process output content to build parts array
|
|
||||||
var parts []interface{}
|
|
||||||
hasToolCall := false
|
|
||||||
var pendingFunctionCalls []interface{}
|
|
||||||
|
|
||||||
flushPendingFunctionCalls := func() {
|
|
||||||
if len(pendingFunctionCalls) > 0 {
|
|
||||||
// Add all pending function calls as individual parts
|
|
||||||
// This maintains the original Gemini API format while ensuring consecutive calls are grouped together
|
|
||||||
for _, fc := range pendingFunctionCalls {
|
|
||||||
parts = append(parts, fc)
|
|
||||||
}
|
|
||||||
pendingFunctionCalls = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
|
||||||
output.ForEach(func(key, value gjson.Result) bool {
|
|
||||||
itemType := value.Get("type").String()
|
|
||||||
|
|
||||||
switch itemType {
|
|
||||||
case "reasoning":
|
|
||||||
// Flush any pending function calls before adding non-function content
|
|
||||||
flushPendingFunctionCalls()
|
|
||||||
|
|
||||||
// Add thinking content
|
|
||||||
if content := value.Get("content"); content.Exists() {
|
|
||||||
part := map[string]interface{}{
|
|
||||||
"thought": true,
|
|
||||||
"text": content.String(),
|
|
||||||
}
|
|
||||||
parts = append(parts, part)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "message":
|
|
||||||
// Flush any pending function calls before adding non-function content
|
|
||||||
flushPendingFunctionCalls()
|
|
||||||
|
|
||||||
// Add regular text content
|
|
||||||
if content := value.Get("content"); content.Exists() && content.IsArray() {
|
|
||||||
content.ForEach(func(_, contentItem gjson.Result) bool {
|
|
||||||
if contentItem.Get("type").String() == "output_text" {
|
|
||||||
if text := contentItem.Get("text"); text.Exists() {
|
|
||||||
part := map[string]interface{}{
|
|
||||||
"text": text.String(),
|
|
||||||
}
|
|
||||||
parts = append(parts, part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case "function_call":
|
|
||||||
// Collect function call for potential merging with consecutive ones
|
|
||||||
hasToolCall = true
|
|
||||||
functionCall := map[string]interface{}{
|
|
||||||
"functionCall": map[string]interface{}{
|
|
||||||
"name": func() string {
|
|
||||||
n := value.Get("name").String()
|
|
||||||
rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
|
|
||||||
if orig, ok := rev[n]; ok {
|
|
||||||
return orig
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}(),
|
|
||||||
"args": map[string]interface{}{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and set arguments
|
|
||||||
if argsStr := value.Get("arguments").String(); argsStr != "" {
|
|
||||||
argsResult := gjson.Parse(argsStr)
|
|
||||||
if argsResult.IsObject() {
|
|
||||||
var args map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
|
||||||
functionCall["functionCall"].(map[string]interface{})["args"] = args
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingFunctionCalls = append(pendingFunctionCalls, functionCall)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle any remaining pending function calls at the end
|
|
||||||
flushPendingFunctionCalls()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the parts array
|
|
||||||
if len(parts) > 0 {
|
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", mustMarshalJSON(parts))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set finish reason based on whether there were tool calls
|
|
||||||
if hasToolCall {
|
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
|
||||||
} else {
|
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return template
|
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
|
// Base Gemini response template for non-streaming
|
||||||
|
template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`
|
||||||
|
|
||||||
|
// Set model version
|
||||||
|
template, _ = sjson.Set(template, "modelVersion", modelName)
|
||||||
|
|
||||||
|
// Set response metadata from the completed response
|
||||||
|
responseData := rootResult.Get("response")
|
||||||
|
if responseData.Exists() {
|
||||||
|
// Set response ID
|
||||||
|
if responseId := responseData.Get("id"); responseId.Exists() {
|
||||||
|
template, _ = sjson.Set(template, "responseId", responseId.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set creation time
|
||||||
|
if createdAt := responseData.Get("created_at"); createdAt.Exists() {
|
||||||
|
template, _ = sjson.Set(template, "createTime", time.Unix(createdAt.Int(), 0).Format(time.RFC3339Nano))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set usage metadata
|
||||||
|
if usage := responseData.Get("usage"); usage.Exists() {
|
||||||
|
inputTokens := usage.Get("input_tokens").Int()
|
||||||
|
outputTokens := usage.Get("output_tokens").Int()
|
||||||
|
totalTokens := inputTokens + outputTokens
|
||||||
|
|
||||||
|
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens)
|
||||||
|
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
||||||
|
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process output content to build parts array
|
||||||
|
var parts []interface{}
|
||||||
|
hasToolCall := false
|
||||||
|
var pendingFunctionCalls []interface{}
|
||||||
|
|
||||||
|
flushPendingFunctionCalls := func() {
|
||||||
|
if len(pendingFunctionCalls) > 0 {
|
||||||
|
// Add all pending function calls as individual parts
|
||||||
|
// This maintains the original Gemini API format while ensuring consecutive calls are grouped together
|
||||||
|
for _, fc := range pendingFunctionCalls {
|
||||||
|
parts = append(parts, fc)
|
||||||
|
}
|
||||||
|
pendingFunctionCalls = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
||||||
|
output.ForEach(func(key, value gjson.Result) bool {
|
||||||
|
itemType := value.Get("type").String()
|
||||||
|
|
||||||
|
switch itemType {
|
||||||
|
case "reasoning":
|
||||||
|
// Flush any pending function calls before adding non-function content
|
||||||
|
flushPendingFunctionCalls()
|
||||||
|
|
||||||
|
// Add thinking content
|
||||||
|
if content := value.Get("content"); content.Exists() {
|
||||||
|
part := map[string]interface{}{
|
||||||
|
"thought": true,
|
||||||
|
"text": content.String(),
|
||||||
|
}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "message":
|
||||||
|
// Flush any pending function calls before adding non-function content
|
||||||
|
flushPendingFunctionCalls()
|
||||||
|
|
||||||
|
// Add regular text content
|
||||||
|
if content := value.Get("content"); content.Exists() && content.IsArray() {
|
||||||
|
content.ForEach(func(_, contentItem gjson.Result) bool {
|
||||||
|
if contentItem.Get("type").String() == "output_text" {
|
||||||
|
if text := contentItem.Get("text"); text.Exists() {
|
||||||
|
part := map[string]interface{}{
|
||||||
|
"text": text.String(),
|
||||||
|
}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case "function_call":
|
||||||
|
// Collect function call for potential merging with consecutive ones
|
||||||
|
hasToolCall = true
|
||||||
|
functionCall := map[string]interface{}{
|
||||||
|
"functionCall": map[string]interface{}{
|
||||||
|
"name": func() string {
|
||||||
|
n := value.Get("name").String()
|
||||||
|
rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
|
||||||
|
if orig, ok := rev[n]; ok {
|
||||||
|
return orig
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}(),
|
||||||
|
"args": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and set arguments
|
||||||
|
if argsStr := value.Get("arguments").String(); argsStr != "" {
|
||||||
|
argsResult := gjson.Parse(argsStr)
|
||||||
|
if argsResult.IsObject() {
|
||||||
|
var args map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
||||||
|
functionCall["functionCall"].(map[string]interface{})["args"] = args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingFunctionCalls = append(pendingFunctionCalls, functionCall)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle any remaining pending function calls at the end
|
||||||
|
flushPendingFunctionCalls()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the parts array
|
||||||
|
if len(parts) > 0 {
|
||||||
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", mustMarshalJSON(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set finish reason based on whether there were tool calls
|
||||||
|
if hasToolCall {
|
||||||
|
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||||
|
} else {
|
||||||
|
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildReverseMapFromGeminiOriginal builds a map[short]original from original Gemini request tools.
|
// buildReverseMapFromGeminiOriginal builds a map[short]original from original Gemini request tools.
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentBlocks []interface{}
|
contentBlocks := make([]interface{}, 0)
|
||||||
hasToolCall := false
|
hasToolCall := false
|
||||||
|
|
||||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
||||||
@@ -477,80 +477,90 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
}
|
}
|
||||||
|
|
||||||
if message := choice.Get("message"); message.Exists() {
|
if message := choice.Get("message"); message.Exists() {
|
||||||
if contentArray := message.Get("content"); contentArray.Exists() && contentArray.IsArray() {
|
if contentResult := message.Get("content"); contentResult.Exists() {
|
||||||
var textBuilder strings.Builder
|
if contentResult.IsArray() {
|
||||||
var thinkingBuilder strings.Builder
|
var textBuilder strings.Builder
|
||||||
|
var thinkingBuilder strings.Builder
|
||||||
|
|
||||||
flushText := func() {
|
flushText := func() {
|
||||||
if textBuilder.Len() == 0 {
|
if textBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||||
|
"type": "text",
|
||||||
|
"text": textBuilder.String(),
|
||||||
|
})
|
||||||
|
textBuilder.Reset()
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
|
||||||
"type": "text",
|
|
||||||
"text": textBuilder.String(),
|
|
||||||
})
|
|
||||||
textBuilder.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
flushThinking := func() {
|
flushThinking := func() {
|
||||||
if thinkingBuilder.Len() == 0 {
|
if thinkingBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||||
|
"type": "thinking",
|
||||||
|
"thinking": thinkingBuilder.String(),
|
||||||
|
})
|
||||||
|
thinkingBuilder.Reset()
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
|
||||||
"type": "thinking",
|
|
||||||
"thinking": thinkingBuilder.String(),
|
|
||||||
})
|
|
||||||
thinkingBuilder.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range contentArray.Array() {
|
for _, item := range contentResult.Array() {
|
||||||
typeStr := item.Get("type").String()
|
typeStr := item.Get("type").String()
|
||||||
switch typeStr {
|
switch typeStr {
|
||||||
case "text":
|
case "text":
|
||||||
flushThinking()
|
flushThinking()
|
||||||
textBuilder.WriteString(item.Get("text").String())
|
textBuilder.WriteString(item.Get("text").String())
|
||||||
case "tool_calls":
|
case "tool_calls":
|
||||||
flushThinking()
|
flushThinking()
|
||||||
flushText()
|
flushText()
|
||||||
toolCalls := item.Get("tool_calls")
|
toolCalls := item.Get("tool_calls")
|
||||||
if toolCalls.IsArray() {
|
if toolCalls.IsArray() {
|
||||||
toolCalls.ForEach(func(_, tc gjson.Result) bool {
|
toolCalls.ForEach(func(_, tc gjson.Result) bool {
|
||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
toolUse := map[string]interface{}{
|
toolUse := map[string]interface{}{
|
||||||
"type": "tool_use",
|
"type": "tool_use",
|
||||||
"id": tc.Get("id").String(),
|
"id": tc.Get("id").String(),
|
||||||
"name": tc.Get("function.name").String(),
|
"name": tc.Get("function.name").String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
||||||
if argsStr != "" {
|
if argsStr != "" {
|
||||||
var parsed interface{}
|
var parsed interface{}
|
||||||
if err := json.Unmarshal([]byte(argsStr), &parsed); err == nil {
|
if err := json.Unmarshal([]byte(argsStr), &parsed); err == nil {
|
||||||
toolUse["input"] = parsed
|
toolUse["input"] = parsed
|
||||||
|
} else {
|
||||||
|
toolUse["input"] = map[string]interface{}{}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toolUse["input"] = map[string]interface{}{}
|
toolUse["input"] = map[string]interface{}{}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
toolUse["input"] = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentBlocks = append(contentBlocks, toolUse)
|
contentBlocks = append(contentBlocks, toolUse)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
case "reasoning":
|
||||||
|
flushText()
|
||||||
|
if thinking := item.Get("text"); thinking.Exists() {
|
||||||
|
thinkingBuilder.WriteString(thinking.String())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
flushThinking()
|
||||||
|
flushText()
|
||||||
}
|
}
|
||||||
case "reasoning":
|
}
|
||||||
flushText()
|
|
||||||
if thinking := item.Get("text"); thinking.Exists() {
|
flushThinking()
|
||||||
thinkingBuilder.WriteString(thinking.String())
|
flushText()
|
||||||
}
|
} else if contentResult.Type == gjson.String {
|
||||||
default:
|
textContent := contentResult.String()
|
||||||
flushThinking()
|
if textContent != "" {
|
||||||
flushText()
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||||
|
"type": "text",
|
||||||
|
"text": textContent,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flushThinking()
|
|
||||||
flushText()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||||
)
|
)
|
||||||
@@ -141,3 +143,48 @@ func HideAPIKey(apiKey string) string {
|
|||||||
}
|
}
|
||||||
return apiKey
|
return apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maskAuthorizationHeader masks the Authorization header value while preserving the auth type prefix.
|
||||||
|
// Common formats: "Bearer <token>", "Basic <credentials>", "ApiKey <key>", etc.
|
||||||
|
// It preserves the prefix (e.g., "Bearer ") and only masks the token/credential part.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - value: The Authorization header value
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The masked Authorization value with prefix preserved
|
||||||
|
func MaskAuthorizationHeader(value string) string {
|
||||||
|
parts := strings.SplitN(strings.TrimSpace(value), " ", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return HideAPIKey(value)
|
||||||
|
}
|
||||||
|
return parts[0] + " " + HideAPIKey(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaskSensitiveHeaderValue masks sensitive header values while preserving expected formats.
|
||||||
|
//
|
||||||
|
// Behavior by header key (case-insensitive):
|
||||||
|
// - "Authorization": Preserve the auth type prefix (e.g., "Bearer ") and mask only the credential part.
|
||||||
|
// - Headers containing "api-key": Mask the entire value using HideAPIKey.
|
||||||
|
// - Others: Return the original value unchanged.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - key: The HTTP header name to inspect (case-insensitive matching).
|
||||||
|
// - value: The header value to mask when sensitive.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The masked value according to the header type; unchanged if not sensitive.
|
||||||
|
func MaskSensitiveHeaderValue(key, value string) string {
|
||||||
|
lowerKey := strings.ToLower(strings.TrimSpace(key))
|
||||||
|
switch {
|
||||||
|
case lowerKey == "authorization":
|
||||||
|
return MaskAuthorizationHeader(value)
|
||||||
|
case strings.Contains(lowerKey, "api-key"),
|
||||||
|
strings.Contains(lowerKey, "apikey"),
|
||||||
|
strings.Contains(lowerKey, "token"),
|
||||||
|
strings.Contains(lowerKey, "secret"):
|
||||||
|
return HideAPIKey(value)
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,3 +84,17 @@ func CountAuthFiles(authDir string) int {
|
|||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WritablePath returns the cleaned WRITABLE_PATH environment variable when it is set.
|
||||||
|
// It accepts both uppercase and lowercase variants for compatibility with existing conventions.
|
||||||
|
func WritablePath() string {
|
||||||
|
for _, key := range []string{"WRITABLE_PATH", "writable_path"} {
|
||||||
|
if value, ok := os.LookupEnv(key); ok {
|
||||||
|
trimmed := strings.TrimSpace(value)
|
||||||
|
if trimmed != "" {
|
||||||
|
return filepath.Clean(trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (a *IFlowAuthenticator) Provider() string { return "iflow" }
|
|||||||
|
|
||||||
// RefreshLead indicates how soon before expiry a refresh should be attempted.
|
// RefreshLead indicates how soon before expiry a refresh should be attempted.
|
||||||
func (a *IFlowAuthenticator) RefreshLead() *time.Duration {
|
func (a *IFlowAuthenticator) RefreshLead() *time.Duration {
|
||||||
d := 3 * time.Hour
|
d := 24 * time.Hour
|
||||||
return &d
|
return &d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user