mirror of
https://github.com/githubhjs/CLIProxyAPIPlus.git
synced 2026-07-12 01:25:13 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot"
|
||||||
geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
||||||
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||||
kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro"
|
kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro"
|
||||||
@@ -1707,7 +1708,7 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
|
|||||||
// Create token storage
|
// Create token storage
|
||||||
tokenStorage := qwenAuth.CreateTokenStorage(tokenData)
|
tokenStorage := qwenAuth.CreateTokenStorage(tokenData)
|
||||||
|
|
||||||
tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli())
|
tokenStorage.Email = fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||||
record := &coreauth.Auth{
|
record := &coreauth.Auth{
|
||||||
ID: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
|
ID: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
|
||||||
Provider: "qwen",
|
Provider: "qwen",
|
||||||
@@ -1812,7 +1813,7 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
|||||||
tokenStorage := authSvc.CreateTokenStorage(tokenData)
|
tokenStorage := authSvc.CreateTokenStorage(tokenData)
|
||||||
identifier := strings.TrimSpace(tokenStorage.Email)
|
identifier := strings.TrimSpace(tokenStorage.Email)
|
||||||
if identifier == "" {
|
if identifier == "" {
|
||||||
identifier = fmt.Sprintf("iflow-%d", time.Now().UnixMilli())
|
identifier = fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||||
tokenStorage.Email = identifier
|
tokenStorage.Email = identifier
|
||||||
}
|
}
|
||||||
record := &coreauth.Auth{
|
record := &coreauth.Auth{
|
||||||
@@ -1843,6 +1844,89 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "url": authURL, "state": state})
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "url": authURL, "state": state})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RequestGitHubToken(c *gin.Context) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
fmt.Println("Initializing GitHub Copilot authentication...")
|
||||||
|
|
||||||
|
state := fmt.Sprintf("gh-%d", time.Now().UnixNano())
|
||||||
|
|
||||||
|
// Initialize Copilot auth service
|
||||||
|
// We need to import "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot" first if not present
|
||||||
|
// Assuming copilot package is imported as "copilot"
|
||||||
|
deviceClient := copilot.NewDeviceFlowClient(h.cfg)
|
||||||
|
|
||||||
|
// Initiate device flow
|
||||||
|
deviceCode, err := deviceClient.RequestDeviceCode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to initiate device flow: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initiate device flow"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authURL := deviceCode.VerificationURI
|
||||||
|
userCode := deviceCode.UserCode
|
||||||
|
|
||||||
|
RegisterOAuthSession(state, "github")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fmt.Printf("Please visit %s and enter code: %s\n", authURL, userCode)
|
||||||
|
|
||||||
|
tokenData, errPoll := deviceClient.PollForToken(ctx, deviceCode)
|
||||||
|
if errPoll != nil {
|
||||||
|
SetOAuthSessionError(state, "Authentication failed")
|
||||||
|
fmt.Printf("Authentication failed: %v\n", errPoll)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username, errUser := deviceClient.FetchUserInfo(ctx, tokenData.AccessToken)
|
||||||
|
if errUser != nil {
|
||||||
|
log.Warnf("Failed to fetch user info: %v", errUser)
|
||||||
|
username = "github-user"
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStorage := &copilot.CopilotTokenStorage{
|
||||||
|
AccessToken: tokenData.AccessToken,
|
||||||
|
TokenType: tokenData.TokenType,
|
||||||
|
Scope: tokenData.Scope,
|
||||||
|
Username: username,
|
||||||
|
Type: "github-copilot",
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf("github-%s.json", username)
|
||||||
|
record := &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
|
Provider: "github",
|
||||||
|
FileName: fileName,
|
||||||
|
Storage: tokenStorage,
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"email": username,
|
||||||
|
"username": username,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
savedPath, errSave := h.saveTokenRecord(ctx, record)
|
||||||
|
if errSave != nil {
|
||||||
|
log.Errorf("Failed to save authentication tokens: %v", errSave)
|
||||||
|
SetOAuthSessionError(state, "Failed to save authentication tokens")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
||||||
|
fmt.Println("You can now use GitHub Copilot services through this CLI")
|
||||||
|
CompleteOAuthSession(state)
|
||||||
|
CompleteOAuthSessionsByProvider("github")
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "ok",
|
||||||
|
"url": authURL,
|
||||||
|
"state": state,
|
||||||
|
"user_code": userCode,
|
||||||
|
"verification_uri": authURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
|
func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -1897,15 +1981,17 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
|
|||||||
fileName := iflowauth.SanitizeIFlowFileName(email)
|
fileName := iflowauth.SanitizeIFlowFileName(email)
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
fileName = fmt.Sprintf("iflow-%d", time.Now().UnixMilli())
|
fileName = fmt.Sprintf("iflow-%d", time.Now().UnixMilli())
|
||||||
|
} else {
|
||||||
|
fileName = fmt.Sprintf("iflow-%s", fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenStorage.Email = email
|
tokenStorage.Email = email
|
||||||
timestamp := time.Now().Unix()
|
timestamp := time.Now().Unix()
|
||||||
|
|
||||||
record := &coreauth.Auth{
|
record := &coreauth.Auth{
|
||||||
ID: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp),
|
ID: fmt.Sprintf("%s-%d.json", fileName, timestamp),
|
||||||
Provider: "iflow",
|
Provider: "iflow",
|
||||||
FileName: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp),
|
FileName: fmt.Sprintf("%s-%d.json", fileName, timestamp),
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
Metadata: map[string]any{
|
Metadata: map[string]any{
|
||||||
"email": email,
|
"email": email,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -360,16 +360,7 @@ func (h *Handler) logDirectory() string {
|
|||||||
if h.logDir != "" {
|
if h.logDir != "" {
|
||||||
return h.logDir
|
return h.logDir
|
||||||
}
|
}
|
||||||
if base := util.WritablePath(); base != "" {
|
return logging.ResolveLogDirectory(h.cfg)
|
||||||
return filepath.Join(base, "logs")
|
|
||||||
}
|
|
||||||
if h.configFilePath != "" {
|
|
||||||
dir := filepath.Dir(h.configFilePath)
|
|
||||||
if dir != "" && dir != "." {
|
|
||||||
return filepath.Join(dir, "logs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "logs"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) collectLogFiles(dir string) ([]string, error) {
|
func (h *Handler) collectLogFiles(dir string) ([]string, error) {
|
||||||
|
|||||||
@@ -238,6 +238,8 @@ func NormalizeOAuthProvider(provider string) (string, error) {
|
|||||||
return "qwen", nil
|
return "qwen", nil
|
||||||
case "kiro":
|
case "kiro":
|
||||||
return "kiro", nil
|
return "kiro", nil
|
||||||
|
case "github":
|
||||||
|
return "github", nil
|
||||||
default:
|
default:
|
||||||
return "", errUnsupportedOAuthFlow
|
return "", errUnsupportedOAuthFlow
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,10 +262,7 @@ 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)
|
||||||
}
|
}
|
||||||
logDir := filepath.Join(s.currentPath, "logs")
|
logDir := logging.ResolveLogDirectory(cfg)
|
||||||
if base := util.WritablePath(); base != "" {
|
|
||||||
logDir = filepath.Join(base, "logs")
|
|
||||||
}
|
|
||||||
s.mgmt.SetLogDirectory(logDir)
|
s.mgmt.SetLogDirectory(logDir)
|
||||||
s.localPassword = optionState.localPassword
|
s.localPassword = optionState.localPassword
|
||||||
|
|
||||||
@@ -649,6 +646,7 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
|
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
|
||||||
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
|
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
|
||||||
mgmt.GET("/kiro-auth-url", s.mgmt.RequestKiroToken)
|
mgmt.GET("/kiro-auth-url", s.mgmt.RequestKiroToken)
|
||||||
|
mgmt.GET("/github-auth-url", s.mgmt.RequestGitHubToken)
|
||||||
mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback)
|
mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback)
|
||||||
mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)
|
mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ var (
|
|||||||
type LogFormatter struct{}
|
type LogFormatter struct{}
|
||||||
|
|
||||||
// logFieldOrder defines the display order for common log fields.
|
// logFieldOrder defines the display order for common log fields.
|
||||||
var logFieldOrder = []string{"provider", "model", "mode", "budget", "level", "original_value", "min", "max", "clamped_to", "error"}
|
var logFieldOrder = []string{"provider", "model", "mode", "budget", "level", "original_mode", "original_value", "min", "max", "clamped_to", "error"}
|
||||||
|
|
||||||
// Format renders a single log entry with custom formatting.
|
// Format renders a single log entry with custom formatting.
|
||||||
func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
|
func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
|
||||||
@@ -122,6 +122,24 @@ func isDirWritable(dir string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveLogDirectory determines the directory used for application logs.
|
||||||
|
func ResolveLogDirectory(cfg *config.Config) string {
|
||||||
|
logDir := "logs"
|
||||||
|
if base := util.WritablePath(); base != "" {
|
||||||
|
return filepath.Join(base, "logs")
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
return logDir
|
||||||
|
}
|
||||||
|
if !isDirWritable(logDir) {
|
||||||
|
authDir := strings.TrimSpace(cfg.AuthDir)
|
||||||
|
if authDir != "" {
|
||||||
|
logDir = filepath.Join(authDir, "logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logDir
|
||||||
|
}
|
||||||
|
|
||||||
// ConfigureLogOutput switches the global log destination between rotating files and stdout.
|
// ConfigureLogOutput switches the global log destination between rotating files and stdout.
|
||||||
// When logsMaxTotalSizeMB > 0, a background cleaner removes the oldest log files in the logs directory
|
// When logsMaxTotalSizeMB > 0, a background cleaner removes the oldest log files in the logs directory
|
||||||
// until the total size is within the limit.
|
// until the total size is within the limit.
|
||||||
@@ -131,12 +149,7 @@ func ConfigureLogOutput(cfg *config.Config) error {
|
|||||||
writerMu.Lock()
|
writerMu.Lock()
|
||||||
defer writerMu.Unlock()
|
defer writerMu.Unlock()
|
||||||
|
|
||||||
logDir := "logs"
|
logDir := ResolveLogDirectory(cfg)
|
||||||
if base := util.WritablePath(); base != "" {
|
|
||||||
logDir = filepath.Join(base, "logs")
|
|
||||||
} else if !isDirWritable(logDir) {
|
|
||||||
logDir = filepath.Join(cfg.AuthDir, "logs")
|
|
||||||
}
|
|
||||||
|
|
||||||
protectedPath := ""
|
protectedPath := ""
|
||||||
if cfg.LoggingToFile {
|
if cfg.LoggingToFile {
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
|
|||||||
}
|
}
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream)
|
||||||
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
|
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
|
||||||
payload, err := thinking.ApplyThinking(payload, req.Model, "gemini")
|
payload, err := thinking.ApplyThinking(payload, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, translatedPayload{}, err
|
return nil, translatedPayload{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, "antigravity")
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,7 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, "antigravity")
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -622,7 +622,7 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, "antigravity")
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -802,7 +802,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
|
|||||||
// Prepare payload once (doesn't depend on baseURL)
|
// Prepare payload once (doesn't depend on baseURL)
|
||||||
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
payload, err := thinking.ApplyThinking(payload, req.Model, "antigravity")
|
payload, err := thinking.ApplyThinking(payload, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "claude")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "claude")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
body = sdktranslator.TranslateRequest(from, to, baseModel, body, false)
|
body = sdktranslator.TranslateRequest(from, to, baseModel, body, false)
|
||||||
body = misc.StripCodexUserAgent(body)
|
body = misc.StripCodexUserAgent(body)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "codex")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -208,7 +208,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
body = sdktranslator.TranslateRequest(from, to, baseModel, body, true)
|
body = sdktranslator.TranslateRequest(from, to, baseModel, body, true)
|
||||||
body = misc.StripCodexUserAgent(body)
|
body = misc.StripCodexUserAgent(body)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "codex")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -316,7 +316,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
body = sdktranslator.TranslateRequest(from, to, baseModel, body, false)
|
body = sdktranslator.TranslateRequest(from, to, baseModel, body, false)
|
||||||
body = misc.StripCodexUserAgent(body)
|
body = misc.StripCodexUserAgent(body)
|
||||||
|
|
||||||
body, err := thinking.ApplyThinking(body, req.Model, "codex")
|
body, err := thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
basePayload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
basePayload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
basePayload, err = thinking.ApplyThinking(basePayload, req.Model, "gemini-cli")
|
basePayload, err = thinking.ApplyThinking(basePayload, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
basePayload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
basePayload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
basePayload, err = thinking.ApplyThinking(basePayload, req.Model, "gemini-cli")
|
basePayload, err = thinking.ApplyThinking(basePayload, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -479,7 +479,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
|
|||||||
for range models {
|
for range models {
|
||||||
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
payload, err = thinking.ApplyThinking(payload, req.Model, "gemini-cli")
|
payload, err = thinking.ApplyThinking(payload, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "gemini")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "gemini")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -338,7 +338,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, "gemini")
|
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "gemini")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "gemini")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -375,7 +375,7 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "gemini")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -494,7 +494,7 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "gemini")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -605,7 +605,7 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
|
|||||||
|
|
||||||
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, "gemini")
|
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
@@ -689,7 +689,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
|
|||||||
|
|
||||||
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, "gemini")
|
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "iflow")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), "iflow")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "iflow")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), "iflow")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), opts.Stream)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), opts.Stream)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated)
|
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, "openai")
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated)
|
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, "openai")
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau
|
|||||||
|
|
||||||
modelForCounting := baseModel
|
modelForCounting := baseModel
|
||||||
|
|
||||||
translated, err := thinking.ApplyThinking(translated, req.Model, "openai")
|
translated, err := thinking.ApplyThinking(translated, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "openai")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, "openai")
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-28
@@ -2,6 +2,8 @@
|
|||||||
package thinking
|
package thinking
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -59,7 +61,8 @@ func IsUserDefinedModel(modelInfo *registry.ModelInfo) bool {
|
|||||||
// Parameters:
|
// Parameters:
|
||||||
// - body: Original request body JSON
|
// - body: Original request body JSON
|
||||||
// - model: Model name, optionally with thinking suffix (e.g., "claude-sonnet-4-5(16384)")
|
// - model: Model name, optionally with thinking suffix (e.g., "claude-sonnet-4-5(16384)")
|
||||||
// - provider: Provider name (gemini, gemini-cli, antigravity, claude, openai, codex, iflow)
|
// - fromFormat: Source request format (e.g., openai, codex, gemini)
|
||||||
|
// - toFormat: Target provider format for the request body (gemini, gemini-cli, antigravity, claude, openai, codex, iflow)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - Modified request body JSON with thinking configuration applied
|
// - Modified request body JSON with thinking configuration applied
|
||||||
@@ -76,16 +79,21 @@ func IsUserDefinedModel(modelInfo *registry.ModelInfo) bool {
|
|||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// // With suffix - suffix config takes priority
|
// // With suffix - suffix config takes priority
|
||||||
// result, err := thinking.ApplyThinking(body, "gemini-2.5-pro(8192)", "gemini")
|
// result, err := thinking.ApplyThinking(body, "gemini-2.5-pro(8192)", "gemini", "gemini")
|
||||||
//
|
//
|
||||||
// // Without suffix - uses body config
|
// // Without suffix - uses body config
|
||||||
// result, err := thinking.ApplyThinking(body, "gemini-2.5-pro", "gemini")
|
// result, err := thinking.ApplyThinking(body, "gemini-2.5-pro", "gemini", "gemini")
|
||||||
func ApplyThinking(body []byte, model string, provider string) ([]byte, error) {
|
func ApplyThinking(body []byte, model string, fromFormat string, toFormat string) ([]byte, error) {
|
||||||
|
providerFormat := strings.ToLower(strings.TrimSpace(toFormat))
|
||||||
|
fromFormat = strings.ToLower(strings.TrimSpace(fromFormat))
|
||||||
|
if fromFormat == "" {
|
||||||
|
fromFormat = providerFormat
|
||||||
|
}
|
||||||
// 1. Route check: Get provider applier
|
// 1. Route check: Get provider applier
|
||||||
applier := GetProviderApplier(provider)
|
applier := GetProviderApplier(providerFormat)
|
||||||
if applier == nil {
|
if applier == nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
"model": model,
|
"model": model,
|
||||||
}).Debug("thinking: unknown provider, passthrough |")
|
}).Debug("thinking: unknown provider, passthrough |")
|
||||||
return body, nil
|
return body, nil
|
||||||
@@ -100,19 +108,19 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) {
|
|||||||
// Unknown models are treated as user-defined so thinking config can still be applied.
|
// Unknown models are treated as user-defined so thinking config can still be applied.
|
||||||
// The upstream service is responsible for validating the configuration.
|
// The upstream service is responsible for validating the configuration.
|
||||||
if IsUserDefinedModel(modelInfo) {
|
if IsUserDefinedModel(modelInfo) {
|
||||||
return applyUserDefinedModel(body, modelInfo, provider, suffixResult)
|
return applyUserDefinedModel(body, modelInfo, fromFormat, providerFormat, suffixResult)
|
||||||
}
|
}
|
||||||
if modelInfo.Thinking == nil {
|
if modelInfo.Thinking == nil {
|
||||||
config := extractThinkingConfig(body, provider)
|
config := extractThinkingConfig(body, providerFormat)
|
||||||
if hasThinkingConfig(config) {
|
if hasThinkingConfig(config) {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"model": baseModel,
|
"model": baseModel,
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
}).Debug("thinking: model does not support thinking, stripping config |")
|
}).Debug("thinking: model does not support thinking, stripping config |")
|
||||||
return StripThinkingConfig(body, provider), nil
|
return StripThinkingConfig(body, providerFormat), nil
|
||||||
}
|
}
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
"model": baseModel,
|
"model": baseModel,
|
||||||
}).Debug("thinking: model does not support thinking, passthrough |")
|
}).Debug("thinking: model does not support thinking, passthrough |")
|
||||||
return body, nil
|
return body, nil
|
||||||
@@ -121,19 +129,19 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) {
|
|||||||
// 4. Get config: suffix priority over body
|
// 4. Get config: suffix priority over body
|
||||||
var config ThinkingConfig
|
var config ThinkingConfig
|
||||||
if suffixResult.HasSuffix {
|
if suffixResult.HasSuffix {
|
||||||
config = parseSuffixToConfig(suffixResult.RawSuffix, provider, model)
|
config = parseSuffixToConfig(suffixResult.RawSuffix, providerFormat, model)
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
"model": model,
|
"model": model,
|
||||||
"mode": config.Mode,
|
"mode": config.Mode,
|
||||||
"budget": config.Budget,
|
"budget": config.Budget,
|
||||||
"level": config.Level,
|
"level": config.Level,
|
||||||
}).Debug("thinking: config from model suffix |")
|
}).Debug("thinking: config from model suffix |")
|
||||||
} else {
|
} else {
|
||||||
config = extractThinkingConfig(body, provider)
|
config = extractThinkingConfig(body, providerFormat)
|
||||||
if hasThinkingConfig(config) {
|
if hasThinkingConfig(config) {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
"model": modelInfo.ID,
|
"model": modelInfo.ID,
|
||||||
"mode": config.Mode,
|
"mode": config.Mode,
|
||||||
"budget": config.Budget,
|
"budget": config.Budget,
|
||||||
@@ -144,17 +152,17 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) {
|
|||||||
|
|
||||||
if !hasThinkingConfig(config) {
|
if !hasThinkingConfig(config) {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
"model": modelInfo.ID,
|
"model": modelInfo.ID,
|
||||||
}).Debug("thinking: no config found, passthrough |")
|
}).Debug("thinking: no config found, passthrough |")
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Validate and normalize configuration
|
// 5. Validate and normalize configuration
|
||||||
validated, err := ValidateConfig(config, modelInfo, provider)
|
validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat, suffixResult.HasSuffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
"model": modelInfo.ID,
|
"model": modelInfo.ID,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
}).Warn("thinking: validation failed |")
|
}).Warn("thinking: validation failed |")
|
||||||
@@ -167,14 +175,14 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) {
|
|||||||
// Defensive check: ValidateConfig should never return (nil, nil)
|
// Defensive check: ValidateConfig should never return (nil, nil)
|
||||||
if validated == nil {
|
if validated == nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
"model": modelInfo.ID,
|
"model": modelInfo.ID,
|
||||||
}).Warn("thinking: ValidateConfig returned nil config without error, passthrough |")
|
}).Warn("thinking: ValidateConfig returned nil config without error, passthrough |")
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": providerFormat,
|
||||||
"model": modelInfo.ID,
|
"model": modelInfo.ID,
|
||||||
"mode": validated.Mode,
|
"mode": validated.Mode,
|
||||||
"budget": validated.Budget,
|
"budget": validated.Budget,
|
||||||
@@ -228,7 +236,7 @@ func parseSuffixToConfig(rawSuffix, provider, model string) ThinkingConfig {
|
|||||||
|
|
||||||
// applyUserDefinedModel applies thinking configuration for user-defined models
|
// applyUserDefinedModel applies thinking configuration for user-defined models
|
||||||
// without ThinkingSupport validation.
|
// without ThinkingSupport validation.
|
||||||
func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, provider string, suffixResult SuffixResult) ([]byte, error) {
|
func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, fromFormat, toFormat string, suffixResult SuffixResult) ([]byte, error) {
|
||||||
// Get model ID for logging
|
// Get model ID for logging
|
||||||
modelID := ""
|
modelID := ""
|
||||||
if modelInfo != nil {
|
if modelInfo != nil {
|
||||||
@@ -240,39 +248,57 @@ func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, provider
|
|||||||
// Get config: suffix priority over body
|
// Get config: suffix priority over body
|
||||||
var config ThinkingConfig
|
var config ThinkingConfig
|
||||||
if suffixResult.HasSuffix {
|
if suffixResult.HasSuffix {
|
||||||
config = parseSuffixToConfig(suffixResult.RawSuffix, provider, modelID)
|
config = parseSuffixToConfig(suffixResult.RawSuffix, toFormat, modelID)
|
||||||
} else {
|
} else {
|
||||||
config = extractThinkingConfig(body, provider)
|
config = extractThinkingConfig(body, toFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasThinkingConfig(config) {
|
if !hasThinkingConfig(config) {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"model": modelID,
|
"model": modelID,
|
||||||
"provider": provider,
|
"provider": toFormat,
|
||||||
}).Debug("thinking: user-defined model, passthrough (no config) |")
|
}).Debug("thinking: user-defined model, passthrough (no config) |")
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
applier := GetProviderApplier(provider)
|
applier := GetProviderApplier(toFormat)
|
||||||
if applier == nil {
|
if applier == nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"model": modelID,
|
"model": modelID,
|
||||||
"provider": provider,
|
"provider": toFormat,
|
||||||
}).Debug("thinking: user-defined model, passthrough (unknown provider) |")
|
}).Debug("thinking: user-defined model, passthrough (unknown provider) |")
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": toFormat,
|
||||||
"model": modelID,
|
"model": modelID,
|
||||||
"mode": config.Mode,
|
"mode": config.Mode,
|
||||||
"budget": config.Budget,
|
"budget": config.Budget,
|
||||||
"level": config.Level,
|
"level": config.Level,
|
||||||
}).Debug("thinking: applying config for user-defined model (skip validation)")
|
}).Debug("thinking: applying config for user-defined model (skip validation)")
|
||||||
|
|
||||||
|
config = normalizeUserDefinedConfig(config, fromFormat, toFormat)
|
||||||
return applier.Apply(body, config, modelInfo)
|
return applier.Apply(body, config, modelInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeUserDefinedConfig(config ThinkingConfig, fromFormat, toFormat string) ThinkingConfig {
|
||||||
|
if config.Mode != ModeLevel {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
if !isBudgetBasedProvider(toFormat) || !isLevelBasedProvider(fromFormat) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
budget, ok := ConvertLevelToBudget(string(config.Level))
|
||||||
|
if !ok {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
config.Mode = ModeBudget
|
||||||
|
config.Budget = budget
|
||||||
|
config.Level = ""
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
// extractThinkingConfig extracts provider-specific thinking config from request body.
|
// extractThinkingConfig extracts provider-specific thinking config from request body.
|
||||||
func extractThinkingConfig(body []byte, provider string) ThinkingConfig {
|
func extractThinkingConfig(body []byte, provider string) ThinkingConfig {
|
||||||
if len(body) == 0 || !gjson.ValidBytes(body) {
|
if len(body) == 0 || !gjson.ValidBytes(body) {
|
||||||
@@ -289,7 +315,11 @@ func extractThinkingConfig(body []byte, provider string) ThinkingConfig {
|
|||||||
case "codex":
|
case "codex":
|
||||||
return extractCodexConfig(body)
|
return extractCodexConfig(body)
|
||||||
case "iflow":
|
case "iflow":
|
||||||
return extractIFlowConfig(body)
|
config := extractIFlowConfig(body)
|
||||||
|
if hasThinkingConfig(config) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
return extractOpenAIConfig(body)
|
||||||
default:
|
default:
|
||||||
return ThinkingConfig{}
|
return ThinkingConfig{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ const (
|
|||||||
// Example: using level with a budget-only model
|
// Example: using level with a budget-only model
|
||||||
ErrLevelNotSupported ErrorCode = "LEVEL_NOT_SUPPORTED"
|
ErrLevelNotSupported ErrorCode = "LEVEL_NOT_SUPPORTED"
|
||||||
|
|
||||||
|
// ErrBudgetOutOfRange indicates the budget value is outside model range.
|
||||||
|
// Example: budget 64000 exceeds max 20000
|
||||||
|
ErrBudgetOutOfRange ErrorCode = "BUDGET_OUT_OF_RANGE"
|
||||||
|
|
||||||
// ErrProviderMismatch indicates the provider does not match the model.
|
// ErrProviderMismatch indicates the provider does not match the model.
|
||||||
// Example: applying Claude format to a Gemini model
|
// Example: applying Claude format to a Gemini model
|
||||||
ErrProviderMismatch ErrorCode = "PROVIDER_MISMATCH"
|
ErrProviderMismatch ErrorCode = "PROVIDER_MISMATCH"
|
||||||
|
|||||||
+18
-14
@@ -27,28 +27,32 @@ func StripThinkingConfig(body []byte, provider string) []byte {
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var paths []string
|
||||||
switch provider {
|
switch provider {
|
||||||
case "claude":
|
case "claude":
|
||||||
result, _ := sjson.DeleteBytes(body, "thinking")
|
paths = []string{"thinking"}
|
||||||
return result
|
|
||||||
case "gemini":
|
case "gemini":
|
||||||
result, _ := sjson.DeleteBytes(body, "generationConfig.thinkingConfig")
|
paths = []string{"generationConfig.thinkingConfig"}
|
||||||
return result
|
|
||||||
case "gemini-cli", "antigravity":
|
case "gemini-cli", "antigravity":
|
||||||
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig")
|
paths = []string{"request.generationConfig.thinkingConfig"}
|
||||||
return result
|
|
||||||
case "openai":
|
case "openai":
|
||||||
result, _ := sjson.DeleteBytes(body, "reasoning_effort")
|
paths = []string{"reasoning_effort"}
|
||||||
return result
|
|
||||||
case "codex":
|
case "codex":
|
||||||
result, _ := sjson.DeleteBytes(body, "reasoning.effort")
|
paths = []string{"reasoning.effort"}
|
||||||
return result
|
|
||||||
case "iflow":
|
case "iflow":
|
||||||
result, _ := sjson.DeleteBytes(body, "chat_template_kwargs.enable_thinking")
|
paths = []string{
|
||||||
result, _ = sjson.DeleteBytes(result, "chat_template_kwargs.clear_thinking")
|
"chat_template_kwargs.enable_thinking",
|
||||||
result, _ = sjson.DeleteBytes(result, "reasoning_split")
|
"chat_template_kwargs.clear_thinking",
|
||||||
return result
|
"reasoning_split",
|
||||||
|
"reasoning_effort",
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := body
|
||||||
|
for _, path := range paths {
|
||||||
|
result, _ = sjson.DeleteBytes(result, path)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
+238
-119
@@ -9,64 +9,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClampBudget clamps a budget value to the model's supported range.
|
|
||||||
//
|
|
||||||
// Logging:
|
|
||||||
// - Warn when value=0 but ZeroAllowed=false
|
|
||||||
// - Debug when value is clamped to min/max
|
|
||||||
//
|
|
||||||
// Fields: provider, model, original_value, clamped_to, min, max
|
|
||||||
func ClampBudget(value int, modelInfo *registry.ModelInfo, provider string) int {
|
|
||||||
model := "unknown"
|
|
||||||
support := (*registry.ThinkingSupport)(nil)
|
|
||||||
if modelInfo != nil {
|
|
||||||
if modelInfo.ID != "" {
|
|
||||||
model = modelInfo.ID
|
|
||||||
}
|
|
||||||
support = modelInfo.Thinking
|
|
||||||
}
|
|
||||||
if support == nil {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto value (-1) passes through without clamping.
|
|
||||||
if value == -1 {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
min := support.Min
|
|
||||||
max := support.Max
|
|
||||||
if value == 0 && !support.ZeroAllowed {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"provider": provider,
|
|
||||||
"model": model,
|
|
||||||
"original_value": value,
|
|
||||||
"clamped_to": min,
|
|
||||||
"min": min,
|
|
||||||
"max": max,
|
|
||||||
}).Warn("thinking: budget zero not allowed |")
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some models are level-only and do not define numeric budget ranges.
|
|
||||||
if min == 0 && max == 0 {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
if value < min {
|
|
||||||
if value == 0 && support.ZeroAllowed {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
logClamp(provider, model, value, min, min, max)
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
if value > max {
|
|
||||||
logClamp(provider, model, value, max, min, max)
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateConfig validates a thinking configuration against model capabilities.
|
// ValidateConfig validates a thinking configuration against model capabilities.
|
||||||
//
|
//
|
||||||
// This function performs comprehensive validation:
|
// This function performs comprehensive validation:
|
||||||
@@ -74,10 +16,16 @@ func ClampBudget(value int, modelInfo *registry.ModelInfo, provider string) int
|
|||||||
// - Auto-converts between Budget and Level formats based on model capability
|
// - Auto-converts between Budget and Level formats based on model capability
|
||||||
// - Validates that requested level is in the model's supported levels list
|
// - Validates that requested level is in the model's supported levels list
|
||||||
// - Clamps budget values to model's allowed range
|
// - Clamps budget values to model's allowed range
|
||||||
|
// - When converting Budget -> Level for level-only models, clamps the derived standard level to the nearest supported level
|
||||||
|
// (special values none/auto are preserved)
|
||||||
|
// - When config comes from a model suffix, strict budget validation is disabled (we clamp instead of error)
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - config: The thinking configuration to validate
|
// - config: The thinking configuration to validate
|
||||||
// - support: Model's ThinkingSupport properties (nil means no thinking support)
|
// - support: Model's ThinkingSupport properties (nil means no thinking support)
|
||||||
|
// - fromFormat: Source provider format (used to determine strict validation rules)
|
||||||
|
// - toFormat: Target provider format
|
||||||
|
// - fromSuffix: Whether config was sourced from model suffix
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - Normalized ThinkingConfig with clamped values
|
// - Normalized ThinkingConfig with clamped values
|
||||||
@@ -87,9 +35,8 @@ func ClampBudget(value int, modelInfo *registry.ModelInfo, provider string) int
|
|||||||
// - Budget-only model + Level config → Level converted to Budget
|
// - Budget-only model + Level config → Level converted to Budget
|
||||||
// - Level-only model + Budget config → Budget converted to Level
|
// - Level-only model + Budget config → Budget converted to Level
|
||||||
// - Hybrid model → preserve original format
|
// - Hybrid model → preserve original format
|
||||||
func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, provider string) (*ThinkingConfig, error) {
|
func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string, fromSuffix bool) (*ThinkingConfig, error) {
|
||||||
normalized := config
|
fromFormat, toFormat = strings.ToLower(strings.TrimSpace(fromFormat)), strings.ToLower(strings.TrimSpace(toFormat))
|
||||||
|
|
||||||
model := "unknown"
|
model := "unknown"
|
||||||
support := (*registry.ThinkingSupport)(nil)
|
support := (*registry.ThinkingSupport)(nil)
|
||||||
if modelInfo != nil {
|
if modelInfo != nil {
|
||||||
@@ -103,101 +50,108 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, provid
|
|||||||
if config.Mode != ModeNone {
|
if config.Mode != ModeNone {
|
||||||
return nil, NewThinkingErrorWithModel(ErrThinkingNotSupported, "thinking not supported for this model", model)
|
return nil, NewThinkingErrorWithModel(ErrThinkingNotSupported, "thinking not supported for this model", model)
|
||||||
}
|
}
|
||||||
return &normalized, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowClampUnsupported := isBudgetBasedProvider(fromFormat) && isLevelBasedProvider(toFormat)
|
||||||
|
strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat)
|
||||||
|
budgetDerivedFromLevel := false
|
||||||
|
|
||||||
capability := detectModelCapability(modelInfo)
|
capability := detectModelCapability(modelInfo)
|
||||||
switch capability {
|
switch capability {
|
||||||
case CapabilityBudgetOnly:
|
case CapabilityBudgetOnly:
|
||||||
if normalized.Mode == ModeLevel {
|
if config.Mode == ModeLevel {
|
||||||
if normalized.Level == LevelAuto {
|
if config.Level == LevelAuto {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
budget, ok := ConvertLevelToBudget(string(normalized.Level))
|
budget, ok := ConvertLevelToBudget(string(config.Level))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("unknown level: %s", normalized.Level))
|
return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("unknown level: %s", config.Level))
|
||||||
}
|
}
|
||||||
normalized.Mode = ModeBudget
|
config.Mode = ModeBudget
|
||||||
normalized.Budget = budget
|
config.Budget = budget
|
||||||
normalized.Level = ""
|
config.Level = ""
|
||||||
|
budgetDerivedFromLevel = true
|
||||||
}
|
}
|
||||||
case CapabilityLevelOnly:
|
case CapabilityLevelOnly:
|
||||||
if normalized.Mode == ModeBudget {
|
if config.Mode == ModeBudget {
|
||||||
level, ok := ConvertBudgetToLevel(normalized.Budget)
|
level, ok := ConvertBudgetToLevel(config.Budget)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("budget %d cannot be converted to a valid level", normalized.Budget))
|
return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("budget %d cannot be converted to a valid level", config.Budget))
|
||||||
}
|
}
|
||||||
normalized.Mode = ModeLevel
|
// When converting Budget -> Level for level-only models, clamp the derived standard level
|
||||||
normalized.Level = ThinkingLevel(level)
|
// to the nearest supported level. Special values (none/auto) are preserved.
|
||||||
normalized.Budget = 0
|
config.Mode = ModeLevel
|
||||||
|
config.Level = clampLevel(ThinkingLevel(level), modelInfo, toFormat)
|
||||||
|
config.Budget = 0
|
||||||
}
|
}
|
||||||
case CapabilityHybrid:
|
case CapabilityHybrid:
|
||||||
}
|
}
|
||||||
|
|
||||||
if normalized.Mode == ModeLevel && normalized.Level == LevelNone {
|
if config.Mode == ModeLevel && config.Level == LevelNone {
|
||||||
normalized.Mode = ModeNone
|
config.Mode = ModeNone
|
||||||
normalized.Budget = 0
|
config.Budget = 0
|
||||||
normalized.Level = ""
|
config.Level = ""
|
||||||
}
|
}
|
||||||
if normalized.Mode == ModeLevel && normalized.Level == LevelAuto {
|
if config.Mode == ModeLevel && config.Level == LevelAuto {
|
||||||
normalized.Mode = ModeAuto
|
config.Mode = ModeAuto
|
||||||
normalized.Budget = -1
|
config.Budget = -1
|
||||||
normalized.Level = ""
|
config.Level = ""
|
||||||
}
|
}
|
||||||
if normalized.Mode == ModeBudget && normalized.Budget == 0 {
|
if config.Mode == ModeBudget && config.Budget == 0 {
|
||||||
normalized.Mode = ModeNone
|
config.Mode = ModeNone
|
||||||
normalized.Level = ""
|
config.Level = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(support.Levels) > 0 && normalized.Mode == ModeLevel {
|
if len(support.Levels) > 0 && config.Mode == ModeLevel {
|
||||||
if !isLevelSupported(string(normalized.Level), support.Levels) {
|
if !isLevelSupported(string(config.Level), support.Levels) {
|
||||||
validLevels := normalizeLevels(support.Levels)
|
if allowClampUnsupported {
|
||||||
message := fmt.Sprintf("level %q not supported, valid levels: %s", strings.ToLower(string(normalized.Level)), strings.Join(validLevels, ", "))
|
config.Level = clampLevel(config.Level, modelInfo, toFormat)
|
||||||
return nil, NewThinkingError(ErrLevelNotSupported, message)
|
}
|
||||||
|
if !isLevelSupported(string(config.Level), support.Levels) {
|
||||||
|
// User explicitly specified an unsupported level - return error
|
||||||
|
// (budget-derived levels may be clamped based on source format)
|
||||||
|
validLevels := normalizeLevels(support.Levels)
|
||||||
|
message := fmt.Sprintf("level %q not supported, valid levels: %s", strings.ToLower(string(config.Level)), strings.Join(validLevels, ", "))
|
||||||
|
return nil, NewThinkingError(ErrLevelNotSupported, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strictBudget && config.Mode == ModeBudget && !budgetDerivedFromLevel {
|
||||||
|
min, max := support.Min, support.Max
|
||||||
|
if min != 0 || max != 0 {
|
||||||
|
if config.Budget < min || config.Budget > max || (config.Budget == 0 && !support.ZeroAllowed) {
|
||||||
|
message := fmt.Sprintf("budget %d out of range [%d,%d]", config.Budget, min, max)
|
||||||
|
return nil, NewThinkingError(ErrBudgetOutOfRange, message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert ModeAuto to mid-range if dynamic not allowed
|
// Convert ModeAuto to mid-range if dynamic not allowed
|
||||||
if normalized.Mode == ModeAuto && !support.DynamicAllowed {
|
if config.Mode == ModeAuto && !support.DynamicAllowed {
|
||||||
normalized = convertAutoToMidRange(normalized, support, provider, model)
|
config = convertAutoToMidRange(config, support, toFormat, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
if normalized.Mode == ModeNone && provider == "claude" {
|
if config.Mode == ModeNone && toFormat == "claude" {
|
||||||
// Claude supports explicit disable via thinking.type="disabled".
|
// Claude supports explicit disable via thinking.type="disabled".
|
||||||
// Keep Budget=0 so applier can omit budget_tokens.
|
// Keep Budget=0 so applier can omit budget_tokens.
|
||||||
normalized.Budget = 0
|
config.Budget = 0
|
||||||
normalized.Level = ""
|
config.Level = ""
|
||||||
} else {
|
} else {
|
||||||
switch normalized.Mode {
|
switch config.Mode {
|
||||||
case ModeBudget, ModeAuto, ModeNone:
|
case ModeBudget, ModeAuto, ModeNone:
|
||||||
normalized.Budget = ClampBudget(normalized.Budget, modelInfo, provider)
|
config.Budget = clampBudget(config.Budget, modelInfo, toFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModeNone with clamped Budget > 0: set Level to lowest for Level-only/Hybrid models
|
// ModeNone with clamped Budget > 0: set Level to lowest for Level-only/Hybrid models
|
||||||
// This ensures Apply layer doesn't need to access support.Levels
|
// This ensures Apply layer doesn't need to access support.Levels
|
||||||
if normalized.Mode == ModeNone && normalized.Budget > 0 && len(support.Levels) > 0 {
|
if config.Mode == ModeNone && config.Budget > 0 && len(support.Levels) > 0 {
|
||||||
normalized.Level = ThinkingLevel(support.Levels[0])
|
config.Level = ThinkingLevel(support.Levels[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &normalized, nil
|
return &config, nil
|
||||||
}
|
|
||||||
|
|
||||||
func isLevelSupported(level string, supported []string) bool {
|
|
||||||
for _, candidate := range supported {
|
|
||||||
if strings.EqualFold(level, strings.TrimSpace(candidate)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeLevels(levels []string) []string {
|
|
||||||
normalized := make([]string, 0, len(levels))
|
|
||||||
for _, level := range levels {
|
|
||||||
normalized = append(normalized, strings.ToLower(strings.TrimSpace(level)))
|
|
||||||
}
|
|
||||||
return normalized
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertAutoToMidRange converts ModeAuto to a mid-range value when dynamic is not allowed.
|
// convertAutoToMidRange converts ModeAuto to a mid-range value when dynamic is not allowed.
|
||||||
@@ -246,7 +200,172 @@ func convertAutoToMidRange(config ThinkingConfig, support *registry.ThinkingSupp
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// logClamp logs a debug message when budget clamping occurs.
|
// standardLevelOrder defines the canonical ordering of thinking levels from lowest to highest.
|
||||||
|
var standardLevelOrder = []ThinkingLevel{LevelMinimal, LevelLow, LevelMedium, LevelHigh, LevelXHigh}
|
||||||
|
|
||||||
|
// clampLevel clamps the given level to the nearest supported level.
|
||||||
|
// On tie, prefers the lower level.
|
||||||
|
func clampLevel(level ThinkingLevel, modelInfo *registry.ModelInfo, provider string) ThinkingLevel {
|
||||||
|
model := "unknown"
|
||||||
|
var supported []string
|
||||||
|
if modelInfo != nil {
|
||||||
|
if modelInfo.ID != "" {
|
||||||
|
model = modelInfo.ID
|
||||||
|
}
|
||||||
|
if modelInfo.Thinking != nil {
|
||||||
|
supported = modelInfo.Thinking.Levels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(supported) == 0 || isLevelSupported(string(level), supported) {
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
|
||||||
|
pos := levelIndex(string(level))
|
||||||
|
if pos == -1 {
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
bestIdx, bestDist := -1, len(standardLevelOrder)+1
|
||||||
|
|
||||||
|
for _, s := range supported {
|
||||||
|
if idx := levelIndex(strings.TrimSpace(s)); idx != -1 {
|
||||||
|
if dist := abs(pos - idx); dist < bestDist || (dist == bestDist && idx < bestIdx) {
|
||||||
|
bestIdx, bestDist = idx, dist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestIdx >= 0 {
|
||||||
|
clamped := standardLevelOrder[bestIdx]
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"provider": provider,
|
||||||
|
"model": model,
|
||||||
|
"original_value": string(level),
|
||||||
|
"clamped_to": string(clamped),
|
||||||
|
}).Debug("thinking: level clamped |")
|
||||||
|
return clamped
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
|
||||||
|
// clampBudget clamps a budget value to the model's supported range.
|
||||||
|
func clampBudget(value int, modelInfo *registry.ModelInfo, provider string) int {
|
||||||
|
model := "unknown"
|
||||||
|
support := (*registry.ThinkingSupport)(nil)
|
||||||
|
if modelInfo != nil {
|
||||||
|
if modelInfo.ID != "" {
|
||||||
|
model = modelInfo.ID
|
||||||
|
}
|
||||||
|
support = modelInfo.Thinking
|
||||||
|
}
|
||||||
|
if support == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto value (-1) passes through without clamping.
|
||||||
|
if value == -1 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
min, max := support.Min, support.Max
|
||||||
|
if value == 0 && !support.ZeroAllowed {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"provider": provider,
|
||||||
|
"model": model,
|
||||||
|
"original_value": value,
|
||||||
|
"clamped_to": min,
|
||||||
|
"min": min,
|
||||||
|
"max": max,
|
||||||
|
}).Warn("thinking: budget zero not allowed |")
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some models are level-only and do not define numeric budget ranges.
|
||||||
|
if min == 0 && max == 0 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if value < min {
|
||||||
|
if value == 0 && support.ZeroAllowed {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
logClamp(provider, model, value, min, min, max)
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
if value > max {
|
||||||
|
logClamp(provider, model, value, max, min, max)
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLevelSupported(level string, supported []string) bool {
|
||||||
|
for _, s := range supported {
|
||||||
|
if strings.EqualFold(level, strings.TrimSpace(s)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func levelIndex(level string) int {
|
||||||
|
for i, l := range standardLevelOrder {
|
||||||
|
if strings.EqualFold(level, string(l)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeLevels(levels []string) []string {
|
||||||
|
out := make([]string, len(levels))
|
||||||
|
for i, l := range levels {
|
||||||
|
out[i] = strings.ToLower(strings.TrimSpace(l))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBudgetBasedProvider(provider string) bool {
|
||||||
|
switch provider {
|
||||||
|
case "gemini", "gemini-cli", "antigravity", "claude":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLevelBasedProvider(provider string) bool {
|
||||||
|
switch provider {
|
||||||
|
case "openai", "openai-response", "codex":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGeminiFamily(provider string) bool {
|
||||||
|
switch provider {
|
||||||
|
case "gemini", "gemini-cli", "antigravity":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSameProviderFamily(from, to string) bool {
|
||||||
|
if from == to {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isGeminiFamily(from) && isGeminiFamily(to)
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(x int) int {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
func logClamp(provider, model string, original, clampedTo, min, max int) {
|
func logClamp(provider, model string, original, clampedTo, min, max int) {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
@@ -388,14 +387,11 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
|
|
||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
||||||
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
||||||
modelInfo := registry.LookupModelInfo(modelName)
|
if t.Get("type").String() == "enabled" {
|
||||||
if modelInfo != nil && modelInfo.Thinking != nil {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
if t.Get("type").String() == "enabled" {
|
budget := int(b.Int())
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
budget := int(b.Int())
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.include_thoughts", true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,8 +343,8 @@ func TestConvertClaudeRequestToAntigravity_ThinkingConfig(t *testing.T) {
|
|||||||
if thinkingConfig.Get("thinkingBudget").Int() != 8000 {
|
if thinkingConfig.Get("thinkingBudget").Int() != 8000 {
|
||||||
t.Errorf("Expected thinkingBudget 8000, got %d", thinkingConfig.Get("thinkingBudget").Int())
|
t.Errorf("Expected thinkingBudget 8000, got %d", thinkingConfig.Get("thinkingBudget").Int())
|
||||||
}
|
}
|
||||||
if !thinkingConfig.Get("include_thoughts").Bool() {
|
if !thinkingConfig.Get("includeThoughts").Bool() {
|
||||||
t.Error("include_thoughts should be true")
|
t.Error("includeThoughts should be true")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.Log("thinkingConfig not present - model may not be registered in test registry")
|
t.Log("thinkingConfig not present - model may not be registered in test registry")
|
||||||
|
|||||||
+15
-5
@@ -132,6 +132,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
systemPartIndex := 0
|
||||||
for i := 0; i < len(arr); i++ {
|
for i := 0; i < len(arr); i++ {
|
||||||
m := arr[i]
|
m := arr[i]
|
||||||
role := m.Get("role").String()
|
role := m.Get("role").String()
|
||||||
@@ -141,16 +142,19 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
// system -> request.systemInstruction as a user message style
|
// system -> request.systemInstruction as a user message style
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.0.text", content.String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", systemPartIndex), content.String())
|
||||||
|
systemPartIndex++
|
||||||
} else if content.IsObject() && content.Get("type").String() == "text" {
|
} else if content.IsObject() && content.Get("type").String() == "text" {
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.0.text", content.Get("text").String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", systemPartIndex), content.Get("text").String())
|
||||||
|
systemPartIndex++
|
||||||
} else if content.IsArray() {
|
} else if content.IsArray() {
|
||||||
contents := content.Array()
|
contents := content.Array()
|
||||||
if len(contents) > 0 {
|
if len(contents) > 0 {
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
||||||
for j := 0; j < len(contents); j++ {
|
for j := 0; j < len(contents); j++ {
|
||||||
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", j), contents[j].Get("text").String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", systemPartIndex), contents[j].Get("text").String())
|
||||||
|
systemPartIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +169,10 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
switch item.Get("type").String() {
|
switch item.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", item.Get("text").String())
|
text := item.Get("text").String()
|
||||||
|
if text != "" {
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", text)
|
||||||
|
}
|
||||||
p++
|
p++
|
||||||
case "image_url":
|
case "image_url":
|
||||||
imageURL := item.Get("image_url.url").String()
|
imageURL := item.Get("image_url.url").String()
|
||||||
@@ -209,7 +216,10 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
for _, item := range content.Array() {
|
for _, item := range content.Array() {
|
||||||
switch item.Get("type").String() {
|
switch item.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", item.Get("text").String())
|
text := item.Get("text").String()
|
||||||
|
if text != "" {
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", text)
|
||||||
|
}
|
||||||
p++
|
p++
|
||||||
case "image_url":
|
case "image_url":
|
||||||
// If the assistant returned an inline data URL, preserve it for history fidelity.
|
// If the assistant returned an inline data URL, preserve it for history fidelity.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -115,18 +115,41 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Include thoughts configuration for reasoning process visibility
|
// Include thoughts configuration for reasoning process visibility
|
||||||
// Only apply for models that support thinking and use numeric budgets, not discrete levels.
|
// Translator only does format conversion, ApplyThinking handles model capability validation.
|
||||||
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||||
modelInfo := registry.LookupModelInfo(modelName)
|
if thinkingLevel := thinkingConfig.Get("thinkingLevel"); thinkingLevel.Exists() {
|
||||||
if modelInfo != nil && modelInfo.Thinking != nil && len(modelInfo.Thinking.Levels) == 0 {
|
level := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||||
// Check for thinkingBudget first - if present, enable thinking with budget
|
switch level {
|
||||||
if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() && thinkingBudget.Int() > 0 {
|
case "":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
case "none":
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", thinkingBudget.Int())
|
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
||||||
} else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
// Fallback to include_thoughts if no budget specified
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
|
default:
|
||||||
|
if budget, ok := thinking.ConvertLevelToBudget(level); ok {
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
||||||
|
budget := int(thinkingBudget.Int())
|
||||||
|
switch budget {
|
||||||
|
case 0:
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
||||||
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
|
case -1:
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
|
default:
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
||||||
|
}
|
||||||
|
} else if includeThoughts := thinkingConfig.Get("includeThoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
} else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -66,23 +65,21 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
|
// Convert OpenAI reasoning_effort to Claude thinking config.
|
||||||
if v := root.Get("reasoning_effort"); v.Exists() {
|
if v := root.Get("reasoning_effort"); v.Exists() {
|
||||||
modelInfo := registry.LookupModelInfo(modelName)
|
effort := strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
if modelInfo != nil && modelInfo.Thinking != nil && len(modelInfo.Thinking.Levels) == 0 {
|
if effort != "" {
|
||||||
effort := strings.ToLower(strings.TrimSpace(v.String()))
|
budget, ok := thinking.ConvertLevelToBudget(effort)
|
||||||
if effort != "" {
|
if ok {
|
||||||
budget, ok := thinking.ConvertLevelToBudget(effort)
|
switch budget {
|
||||||
if ok {
|
case 0:
|
||||||
switch budget {
|
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
||||||
case 0:
|
case -1:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
case -1:
|
default:
|
||||||
|
if budget > 0 {
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
default:
|
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
||||||
if budget > 0 {
|
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,17 +138,35 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
|
|
||||||
// Process messages and transform them to Claude Code format
|
// Process messages and transform them to Claude Code format
|
||||||
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
|
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
|
||||||
|
messageIndex := 0
|
||||||
|
systemMessageIndex := -1
|
||||||
messages.ForEach(func(_, message gjson.Result) bool {
|
messages.ForEach(func(_, message gjson.Result) bool {
|
||||||
role := message.Get("role").String()
|
role := message.Get("role").String()
|
||||||
contentResult := message.Get("content")
|
contentResult := message.Get("content")
|
||||||
|
|
||||||
switch role {
|
switch role {
|
||||||
case "system", "user", "assistant":
|
case "system":
|
||||||
// Create Claude Code message with appropriate role mapping
|
if systemMessageIndex == -1 {
|
||||||
if role == "system" {
|
systemMsg := `{"role":"user","content":[]}`
|
||||||
role = "user"
|
out, _ = sjson.SetRaw(out, "messages.-1", systemMsg)
|
||||||
|
systemMessageIndex = messageIndex
|
||||||
|
messageIndex++
|
||||||
}
|
}
|
||||||
|
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
||||||
|
textPart := `{"type":"text","text":""}`
|
||||||
|
textPart, _ = sjson.Set(textPart, "text", contentResult.String())
|
||||||
|
out, _ = sjson.SetRaw(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
|
||||||
|
} else if contentResult.Exists() && contentResult.IsArray() {
|
||||||
|
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||||
|
if part.Get("type").String() == "text" {
|
||||||
|
textPart := `{"type":"text","text":""}`
|
||||||
|
textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
|
||||||
|
out, _ = sjson.SetRaw(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "user", "assistant":
|
||||||
msg := `{"role":"","content":[]}`
|
msg := `{"role":"","content":[]}`
|
||||||
msg, _ = sjson.Set(msg, "role", role)
|
msg, _ = sjson.Set(msg, "role", role)
|
||||||
|
|
||||||
@@ -230,6 +245,7 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||||
|
messageIndex++
|
||||||
|
|
||||||
case "tool":
|
case "tool":
|
||||||
// Handle tool result messages conversion
|
// Handle tool result messages conversion
|
||||||
@@ -240,6 +256,7 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
|
msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
|
||||||
msg, _ = sjson.Set(msg, "content.0.content", content)
|
msg, _ = sjson.Set(msg, "content.0.content", content)
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||||
|
messageIndex++
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -54,23 +53,21 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
|
// Convert OpenAI Responses reasoning.effort to Claude thinking config.
|
||||||
if v := root.Get("reasoning.effort"); v.Exists() {
|
if v := root.Get("reasoning.effort"); v.Exists() {
|
||||||
modelInfo := registry.LookupModelInfo(modelName)
|
effort := strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
if modelInfo != nil && modelInfo.Thinking != nil && len(modelInfo.Thinking.Levels) == 0 {
|
if effort != "" {
|
||||||
effort := strings.ToLower(strings.TrimSpace(v.String()))
|
budget, ok := thinking.ConvertLevelToBudget(effort)
|
||||||
if effort != "" {
|
if ok {
|
||||||
budget, ok := thinking.ConvertLevelToBudget(effort)
|
switch budget {
|
||||||
if ok {
|
case 0:
|
||||||
switch budget {
|
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
||||||
case 0:
|
case -1:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
case -1:
|
default:
|
||||||
|
if budget > 0 {
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
default:
|
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
||||||
if budget > 0 {
|
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -218,18 +217,15 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
// Add additional configuration parameters for the Codex API.
|
// Add additional configuration parameters for the Codex API.
|
||||||
template, _ = sjson.Set(template, "parallel_tool_calls", true)
|
template, _ = sjson.Set(template, "parallel_tool_calls", true)
|
||||||
|
|
||||||
// Convert thinking.budget_tokens to reasoning.effort for level-based models
|
// Convert thinking.budget_tokens to reasoning.effort.
|
||||||
reasoningEffort := "medium" // default
|
reasoningEffort := "medium"
|
||||||
if thinkingConfig := rootResult.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
if thinkingConfig := rootResult.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||||
modelInfo := registry.LookupModelInfo(modelName)
|
|
||||||
switch thinkingConfig.Get("type").String() {
|
switch thinkingConfig.Get("type").String() {
|
||||||
case "enabled":
|
case "enabled":
|
||||||
if modelInfo != nil && modelInfo.Thinking != nil && len(modelInfo.Thinking.Levels) > 0 {
|
if budgetTokens := thinkingConfig.Get("budget_tokens"); budgetTokens.Exists() {
|
||||||
if budgetTokens := thinkingConfig.Get("budget_tokens"); budgetTokens.Exists() {
|
budget := int(budgetTokens.Int())
|
||||||
budget := int(budgetTokens.Int())
|
if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" {
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" {
|
reasoningEffort = effort
|
||||||
reasoningEffort = effort
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "disabled":
|
case "disabled":
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -249,22 +248,28 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
// Fixed flags aligning with Codex expectations
|
// Fixed flags aligning with Codex expectations
|
||||||
out, _ = sjson.Set(out, "parallel_tool_calls", true)
|
out, _ = sjson.Set(out, "parallel_tool_calls", true)
|
||||||
|
|
||||||
// Convert thinkingBudget to reasoning.effort for level-based models
|
// Convert Gemini thinkingConfig to Codex reasoning.effort.
|
||||||
reasoningEffort := "medium" // default
|
effortSet := false
|
||||||
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
||||||
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||||
modelInfo := registry.LookupModelInfo(modelName)
|
if thinkingLevel := thinkingConfig.Get("thinkingLevel"); thinkingLevel.Exists() {
|
||||||
if modelInfo != nil && modelInfo.Thinking != nil && len(modelInfo.Thinking.Levels) > 0 {
|
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||||
if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
if effort != "" {
|
||||||
budget := int(thinkingBudget.Int())
|
out, _ = sjson.Set(out, "reasoning.effort", effort)
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" {
|
effortSet = true
|
||||||
reasoningEffort = effort
|
}
|
||||||
}
|
} else if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
||||||
|
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
||||||
|
out, _ = sjson.Set(out, "reasoning.effort", effort)
|
||||||
|
effortSet = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out, _ = sjson.Set(out, "reasoning.effort", reasoningEffort)
|
if !effortSet {
|
||||||
|
// No thinking config, set default effort
|
||||||
|
out, _ = sjson.Set(out, "reasoning.effort", "medium")
|
||||||
|
}
|
||||||
out, _ = sjson.Set(out, "reasoning.summary", "auto")
|
out, _ = sjson.Set(out, "reasoning.summary", "auto")
|
||||||
out, _ = sjson.Set(out, "stream", true)
|
out, _ = sjson.Set(out, "stream", true)
|
||||||
out, _ = sjson.Set(out, "store", false)
|
out, _ = sjson.Set(out, "store", false)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -161,14 +160,11 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
|
|
||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
||||||
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
||||||
modelInfo := registry.LookupModelInfo(modelName)
|
if t.Get("type").String() == "enabled" {
|
||||||
if modelInfo != nil && modelInfo.Thinking != nil {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
if t.Get("type").String() == "enabled" {
|
budget := int(b.Int())
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
budget := int(b.Int())
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.include_thoughts", true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
systemPartIndex := 0
|
||||||
for i := 0; i < len(arr); i++ {
|
for i := 0; i < len(arr); i++ {
|
||||||
m := arr[i]
|
m := arr[i]
|
||||||
role := m.Get("role").String()
|
role := m.Get("role").String()
|
||||||
@@ -138,16 +139,19 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
// system -> request.systemInstruction as a user message style
|
// system -> request.systemInstruction as a user message style
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.0.text", content.String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", systemPartIndex), content.String())
|
||||||
|
systemPartIndex++
|
||||||
} else if content.IsObject() && content.Get("type").String() == "text" {
|
} else if content.IsObject() && content.Get("type").String() == "text" {
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.0.text", content.Get("text").String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", systemPartIndex), content.Get("text").String())
|
||||||
|
systemPartIndex++
|
||||||
} else if content.IsArray() {
|
} else if content.IsArray() {
|
||||||
contents := content.Array()
|
contents := content.Array()
|
||||||
if len(contents) > 0 {
|
if len(contents) > 0 {
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
||||||
for j := 0; j < len(contents); j++ {
|
for j := 0; j < len(contents); j++ {
|
||||||
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", j), contents[j].Get("text").String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", systemPartIndex), contents[j].Get("text").String())
|
||||||
|
systemPartIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -153,16 +152,13 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
|
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
|
||||||
// Only apply for models that use numeric budgets, not discrete levels.
|
// Translator only does format conversion, ApplyThinking handles model capability validation.
|
||||||
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
||||||
modelInfo := registry.LookupModelInfo(modelName)
|
if t.Get("type").String() == "enabled" {
|
||||||
if modelInfo != nil && modelInfo.Thinking != nil && len(modelInfo.Thinking.Levels) == 0 {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
if t.Get("type").String() == "enabled" {
|
budget := int(b.Int())
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
budget := int(b.Int())
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.include_thoughts", true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
systemPartIndex := 0
|
||||||
for i := 0; i < len(arr); i++ {
|
for i := 0; i < len(arr); i++ {
|
||||||
m := arr[i]
|
m := arr[i]
|
||||||
role := m.Get("role").String()
|
role := m.Get("role").String()
|
||||||
@@ -138,16 +139,19 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
// system -> system_instruction as a user message style
|
// system -> system_instruction as a user message style
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
out, _ = sjson.SetBytes(out, "system_instruction.role", "user")
|
out, _ = sjson.SetBytes(out, "system_instruction.role", "user")
|
||||||
out, _ = sjson.SetBytes(out, "system_instruction.parts.0.text", content.String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("system_instruction.parts.%d.text", systemPartIndex), content.String())
|
||||||
|
systemPartIndex++
|
||||||
} else if content.IsObject() && content.Get("type").String() == "text" {
|
} else if content.IsObject() && content.Get("type").String() == "text" {
|
||||||
out, _ = sjson.SetBytes(out, "system_instruction.role", "user")
|
out, _ = sjson.SetBytes(out, "system_instruction.role", "user")
|
||||||
out, _ = sjson.SetBytes(out, "system_instruction.parts.0.text", content.Get("text").String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("system_instruction.parts.%d.text", systemPartIndex), content.Get("text").String())
|
||||||
|
systemPartIndex++
|
||||||
} else if content.IsArray() {
|
} else if content.IsArray() {
|
||||||
contents := content.Array()
|
contents := content.Array()
|
||||||
if len(contents) > 0 {
|
if len(contents) > 0 {
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
|
out, _ = sjson.SetBytes(out, "system_instruction.role", "user")
|
||||||
for j := 0; j < len(contents); j++ {
|
for j := 0; j < len(contents); j++ {
|
||||||
out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", j), contents[j].Get("text").String())
|
out, _ = sjson.SetBytes(out, fmt.Sprintf("system_instruction.parts.%d.text", systemPartIndex), contents[j].Get("text").String())
|
||||||
|
systemPartIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +166,10 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
switch item.Get("type").String() {
|
switch item.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", item.Get("text").String())
|
text := item.Get("text").String()
|
||||||
|
if text != "" {
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", text)
|
||||||
|
}
|
||||||
p++
|
p++
|
||||||
case "image_url":
|
case "image_url":
|
||||||
imageURL := item.Get("image_url.url").String()
|
imageURL := item.Get("image_url.url").String()
|
||||||
@@ -207,6 +214,10 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
for _, item := range content.Array() {
|
for _, item := range content.Array() {
|
||||||
switch item.Get("type").String() {
|
switch item.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
|
text := item.Get("text").String()
|
||||||
|
if text != "" {
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", text)
|
||||||
|
}
|
||||||
p++
|
p++
|
||||||
case "image_url":
|
case "image_url":
|
||||||
// If the assistant returned an inline data URL, preserve it for history fidelity.
|
// If the assistant returned an inline data URL, preserve it for history fidelity.
|
||||||
|
|||||||
@@ -77,12 +77,15 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert thinkingBudget to reasoning_effort
|
// Map Gemini thinkingConfig to OpenAI reasoning_effort.
|
||||||
// Always perform conversion to support allowCompat models that may not be in registry
|
|
||||||
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||||
if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
if thinkingLevel := thinkingConfig.Get("thinkingLevel"); thinkingLevel.Exists() {
|
||||||
budget := int(thinkingBudget.Int())
|
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" {
|
if effort != "" {
|
||||||
|
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||||
|
}
|
||||||
|
} else if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
||||||
|
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import (
|
|||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type oaiToResponsesStateReasoning struct {
|
||||||
|
ReasoningID string
|
||||||
|
ReasoningData string
|
||||||
|
}
|
||||||
type oaiToResponsesState struct {
|
type oaiToResponsesState struct {
|
||||||
Seq int
|
Seq int
|
||||||
ResponseID string
|
ResponseID string
|
||||||
@@ -23,6 +27,7 @@ type oaiToResponsesState struct {
|
|||||||
// Per-output message text buffers by index
|
// Per-output message text buffers by index
|
||||||
MsgTextBuf map[int]*strings.Builder
|
MsgTextBuf map[int]*strings.Builder
|
||||||
ReasoningBuf strings.Builder
|
ReasoningBuf strings.Builder
|
||||||
|
Reasonings []oaiToResponsesStateReasoning
|
||||||
FuncArgsBuf map[int]*strings.Builder // index -> args
|
FuncArgsBuf map[int]*strings.Builder // index -> args
|
||||||
FuncNames map[int]string // index -> name
|
FuncNames map[int]string // index -> name
|
||||||
FuncCallIDs map[int]string // index -> call_id
|
FuncCallIDs map[int]string // index -> call_id
|
||||||
@@ -63,6 +68,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
MsgItemDone: make(map[int]bool),
|
MsgItemDone: make(map[int]bool),
|
||||||
FuncArgsDone: make(map[int]bool),
|
FuncArgsDone: make(map[int]bool),
|
||||||
FuncItemDone: make(map[int]bool),
|
FuncItemDone: make(map[int]bool),
|
||||||
|
Reasonings: make([]oaiToResponsesStateReasoning, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
st := (*param).(*oaiToResponsesState)
|
st := (*param).(*oaiToResponsesState)
|
||||||
@@ -157,6 +163,31 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
st.Started = true
|
st.Started = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopReasoning := func(text string) {
|
||||||
|
// Emit reasoning done events
|
||||||
|
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
|
||||||
|
textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq())
|
||||||
|
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningID)
|
||||||
|
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
|
||||||
|
textDone, _ = sjson.Set(textDone, "text", text)
|
||||||
|
out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone))
|
||||||
|
partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`
|
||||||
|
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
|
||||||
|
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningID)
|
||||||
|
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
|
||||||
|
partDone, _ = sjson.Set(partDone, "part.text", text)
|
||||||
|
out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone))
|
||||||
|
outputItemDone := `{"type":"response.output_item.done","item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]},"output_index":0,"sequence_number":0}`
|
||||||
|
outputItemDone, _ = sjson.Set(outputItemDone, "sequence_number", nextSeq())
|
||||||
|
outputItemDone, _ = sjson.Set(outputItemDone, "item.id", st.ReasoningID)
|
||||||
|
outputItemDone, _ = sjson.Set(outputItemDone, "output_index", st.ReasoningIndex)
|
||||||
|
outputItemDone, _ = sjson.Set(outputItemDone, "item.summary.text", text)
|
||||||
|
out = append(out, emitRespEvent("response.output_item.done", outputItemDone))
|
||||||
|
|
||||||
|
st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text})
|
||||||
|
st.ReasoningID = ""
|
||||||
|
}
|
||||||
|
|
||||||
// choices[].delta content / tool_calls / reasoning_content
|
// choices[].delta content / tool_calls / reasoning_content
|
||||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
||||||
choices.ForEach(func(_, choice gjson.Result) bool {
|
choices.ForEach(func(_, choice gjson.Result) bool {
|
||||||
@@ -165,6 +196,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
if delta.Exists() {
|
if delta.Exists() {
|
||||||
if c := delta.Get("content"); c.Exists() && c.String() != "" {
|
if c := delta.Get("content"); c.Exists() && c.String() != "" {
|
||||||
// Ensure the message item and its first content part are announced before any text deltas
|
// Ensure the message item and its first content part are announced before any text deltas
|
||||||
|
if st.ReasoningID != "" {
|
||||||
|
stopReasoning(st.ReasoningBuf.String())
|
||||||
|
st.ReasoningBuf.Reset()
|
||||||
|
}
|
||||||
if !st.MsgItemAdded[idx] {
|
if !st.MsgItemAdded[idx] {
|
||||||
item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`
|
item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`
|
||||||
item, _ = sjson.Set(item, "sequence_number", nextSeq())
|
item, _ = sjson.Set(item, "sequence_number", nextSeq())
|
||||||
@@ -226,6 +261,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
|
|
||||||
// tool calls
|
// tool calls
|
||||||
if tcs := delta.Get("tool_calls"); tcs.Exists() && tcs.IsArray() {
|
if tcs := delta.Get("tool_calls"); tcs.Exists() && tcs.IsArray() {
|
||||||
|
if st.ReasoningID != "" {
|
||||||
|
stopReasoning(st.ReasoningBuf.String())
|
||||||
|
st.ReasoningBuf.Reset()
|
||||||
|
}
|
||||||
// Before emitting any function events, if a message is open for this index,
|
// Before emitting any function events, if a message is open for this index,
|
||||||
// close its text/content to match Codex expected ordering.
|
// close its text/content to match Codex expected ordering.
|
||||||
if st.MsgItemAdded[idx] && !st.MsgItemDone[idx] {
|
if st.MsgItemAdded[idx] && !st.MsgItemDone[idx] {
|
||||||
@@ -361,17 +400,8 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if st.ReasoningID != "" {
|
if st.ReasoningID != "" {
|
||||||
// Emit reasoning done events
|
stopReasoning(st.ReasoningBuf.String())
|
||||||
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
|
st.ReasoningBuf.Reset()
|
||||||
textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq())
|
|
||||||
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningID)
|
|
||||||
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
|
|
||||||
out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone))
|
|
||||||
partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`
|
|
||||||
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
|
|
||||||
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningID)
|
|
||||||
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
|
|
||||||
out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit function call done events for any active function calls
|
// Emit function call done events for any active function calls
|
||||||
@@ -485,11 +515,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
}
|
}
|
||||||
// Build response.output using aggregated buffers
|
// Build response.output using aggregated buffers
|
||||||
outputsWrapper := `{"arr":[]}`
|
outputsWrapper := `{"arr":[]}`
|
||||||
if st.ReasoningBuf.Len() > 0 {
|
if len(st.Reasonings) > 0 {
|
||||||
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
for _, r := range st.Reasonings {
|
||||||
item, _ = sjson.Set(item, "id", st.ReasoningID)
|
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
||||||
item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
|
item, _ = sjson.Set(item, "id", r.ReasoningID)
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
item, _ = sjson.Set(item, "summary.0.text", r.ReasoningData)
|
||||||
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Append message items in ascending index order
|
// Append message items in ascending index order
|
||||||
if len(st.MsgItemAdded) > 0 {
|
if len(st.MsgItemAdded) > 0 {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func CleanJSONSchemaForAntigravity(jsonStr string) string {
|
|||||||
// Phase 1: Convert and add hints
|
// Phase 1: Convert and add hints
|
||||||
jsonStr = convertRefsToHints(jsonStr)
|
jsonStr = convertRefsToHints(jsonStr)
|
||||||
jsonStr = convertConstToEnum(jsonStr)
|
jsonStr = convertConstToEnum(jsonStr)
|
||||||
|
jsonStr = convertEnumValuesToStrings(jsonStr)
|
||||||
jsonStr = addEnumHints(jsonStr)
|
jsonStr = addEnumHints(jsonStr)
|
||||||
jsonStr = addAdditionalPropertiesHints(jsonStr)
|
jsonStr = addAdditionalPropertiesHints(jsonStr)
|
||||||
jsonStr = moveConstraintsToDescription(jsonStr)
|
jsonStr = moveConstraintsToDescription(jsonStr)
|
||||||
@@ -77,6 +78,33 @@ func convertConstToEnum(jsonStr string) string {
|
|||||||
return jsonStr
|
return jsonStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertEnumValuesToStrings ensures all enum values are strings.
|
||||||
|
// Gemini API requires enum values to be of type string, not numbers or booleans.
|
||||||
|
func convertEnumValuesToStrings(jsonStr string) string {
|
||||||
|
for _, p := range findPaths(jsonStr, "enum") {
|
||||||
|
arr := gjson.Get(jsonStr, p)
|
||||||
|
if !arr.IsArray() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringVals []string
|
||||||
|
needsConversion := false
|
||||||
|
for _, item := range arr.Array() {
|
||||||
|
// Check if any value is not a string
|
||||||
|
if item.Type != gjson.String {
|
||||||
|
needsConversion = true
|
||||||
|
}
|
||||||
|
stringVals = append(stringVals, item.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if we found non-string values
|
||||||
|
if needsConversion {
|
||||||
|
jsonStr, _ = sjson.Set(jsonStr, p, stringVals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonStr
|
||||||
|
}
|
||||||
|
|
||||||
func addEnumHints(jsonStr string) string {
|
func addEnumHints(jsonStr string) string {
|
||||||
for _, p := range findPaths(jsonStr, "enum") {
|
for _, p := range findPaths(jsonStr, "enum") {
|
||||||
arr := gjson.Get(jsonStr, p)
|
arr := gjson.Get(jsonStr, p)
|
||||||
|
|||||||
@@ -818,3 +818,54 @@ func TestCleanJSONSchemaForAntigravity_MultipleFormats(t *testing.T) {
|
|||||||
t.Errorf("date-time format hint should be added, got: %s", result)
|
t.Errorf("date-time format hint should be added, got: %s", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCleanJSONSchemaForAntigravity_NumericEnumToString(t *testing.T) {
|
||||||
|
// Gemini API requires enum values to be strings, not numbers
|
||||||
|
input := `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"priority": {"type": "integer", "enum": [0, 1, 2]},
|
||||||
|
"level": {"type": "number", "enum": [1.5, 2.5, 3.5]},
|
||||||
|
"status": {"type": "string", "enum": ["active", "inactive"]}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
result := CleanJSONSchemaForAntigravity(input)
|
||||||
|
|
||||||
|
// Numeric enum values should be converted to strings
|
||||||
|
if strings.Contains(result, `"enum":[0,1,2]`) {
|
||||||
|
t.Errorf("Integer enum values should be converted to strings, got: %s", result)
|
||||||
|
}
|
||||||
|
if strings.Contains(result, `"enum":[1.5,2.5,3.5]`) {
|
||||||
|
t.Errorf("Float enum values should be converted to strings, got: %s", result)
|
||||||
|
}
|
||||||
|
// Should contain string versions
|
||||||
|
if !strings.Contains(result, `"0"`) || !strings.Contains(result, `"1"`) || !strings.Contains(result, `"2"`) {
|
||||||
|
t.Errorf("Integer enum values should be converted to string format, got: %s", result)
|
||||||
|
}
|
||||||
|
// String enum values should remain unchanged
|
||||||
|
if !strings.Contains(result, `"active"`) || !strings.Contains(result, `"inactive"`) {
|
||||||
|
t.Errorf("String enum values should remain unchanged, got: %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanJSONSchemaForAntigravity_BooleanEnumToString(t *testing.T) {
|
||||||
|
// Boolean enum values should also be converted to strings
|
||||||
|
input := `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": {"type": "boolean", "enum": [true, false]}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
result := CleanJSONSchemaForAntigravity(input)
|
||||||
|
|
||||||
|
// Boolean enum values should be converted to strings
|
||||||
|
if strings.Contains(result, `"enum":[true,false]`) {
|
||||||
|
t.Errorf("Boolean enum values should be converted to strings, got: %s", result)
|
||||||
|
}
|
||||||
|
// Should contain string versions "true" and "false"
|
||||||
|
if !strings.Contains(result, `"true"`) || !strings.Contains(result, `"false"`) {
|
||||||
|
t.Errorf("Boolean enum values should be converted to string format, got: %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -227,6 +227,18 @@ func (a *Auth) AccountInfo() (string, string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For GitHub provider, return username
|
||||||
|
if strings.ToLower(a.Provider) == "github" {
|
||||||
|
if a.Metadata != nil {
|
||||||
|
if username, ok := a.Metadata["username"].(string); ok {
|
||||||
|
username = strings.TrimSpace(username)
|
||||||
|
if username != "" {
|
||||||
|
return "oauth", username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check metadata for email first (OAuth-style auth)
|
// Check metadata for email first (OAuth-style auth)
|
||||||
if a.Metadata != nil {
|
if a.Metadata != nil {
|
||||||
if v, ok := a.Metadata["email"].(string); ok {
|
if v, ok := a.Metadata["email"].(string); ok {
|
||||||
|
|||||||
+2627
-894
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user