Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
140d6211cc | ||
|
|
60f9a1442c | ||
|
|
cb6caf3f87 | ||
|
|
99c7abbbf1 | ||
|
|
8f511ac33c | ||
|
|
1046152119 | ||
|
|
dd6d78cb31 | ||
|
|
c8843edb81 | ||
|
|
f89feb881c | ||
|
|
dbba71028e | ||
|
|
8549a92e9a | ||
|
|
109cffc010 |
@@ -130,6 +130,10 @@ Windows-native CLIProxyAPI fork with TUI, system tray, and multi-provider OAuth
|
|||||||
|
|
||||||
VSCode extension for quick switching between Claude Code models, featuring integrated CLIProxyAPI as its backend with automatic background lifecycle management.
|
VSCode extension for quick switching between Claude Code models, featuring integrated CLIProxyAPI as its backend with automatic background lifecycle management.
|
||||||
|
|
||||||
|
### [ZeroLimit](https://github.com/0xtbug/zero-limit)
|
||||||
|
|
||||||
|
Windows desktop app built with Tauri + React for monitoring AI coding assistant quotas via CLIProxyAPI. Track usage across Gemini, Claude, OpenAI Codex, and Antigravity accounts with real-time dashboard, system tray integration, and one-click proxy control - no API keys needed.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you developed a project based on CLIProxyAPI, please open a PR to add it to this list.
|
> If you developed a project based on CLIProxyAPI, please open a PR to add it to this list.
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,10 @@ CLI 封装器,用于通过 CLIProxyAPI OAuth 即时切换多个 Claude 账户
|
|||||||
|
|
||||||
一款 VSCode 扩展,提供了在 VSCode 中快速切换 Claude Code 模型的功能,内置 CLIProxyAPI 作为其后端,支持后台自动启动和关闭。
|
一款 VSCode 扩展,提供了在 VSCode 中快速切换 Claude Code 模型的功能,内置 CLIProxyAPI 作为其后端,支持后台自动启动和关闭。
|
||||||
|
|
||||||
|
### [ZeroLimit](https://github.com/0xtbug/zero-limit)
|
||||||
|
|
||||||
|
Windows 桌面应用,基于 Tauri + React 构建,用于通过 CLIProxyAPI 监控 AI 编程助手配额。支持跨 Gemini、Claude、OpenAI Codex 和 Antigravity 账户的使用量追踪,提供实时仪表盘、系统托盘集成和一键代理控制,无需 API 密钥。
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。
|
> 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。
|
||||||
|
|
||||||
|
|||||||
@@ -1703,7 +1703,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",
|
||||||
@@ -1808,7 +1808,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{
|
||||||
@@ -1893,15 +1893,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,
|
||||||
|
|||||||
@@ -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", "original_level", "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) {
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func ApplyThinking(body []byte, model string, fromFormat string, toFormat string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Validate and normalize configuration
|
// 5. Validate and normalize configuration
|
||||||
validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat)
|
validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat, suffixResult.HasSuffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": providerFormat,
|
"provider": providerFormat,
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ import (
|
|||||||
// - 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
|
// - When converting Budget -> Level for level-only models, clamps the derived standard level to the nearest supported level
|
||||||
// (special values none/auto are preserved)
|
// (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)
|
// - fromFormat: Source provider format (used to determine strict validation rules)
|
||||||
// - toFormat: Target provider format
|
// - toFormat: Target provider format
|
||||||
|
// - fromSuffix: Whether config was sourced from model suffix
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - Normalized ThinkingConfig with clamped values
|
// - Normalized ThinkingConfig with clamped values
|
||||||
@@ -33,7 +35,7 @@ import (
|
|||||||
// - 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, fromFormat, toFormat string) (*ThinkingConfig, error) {
|
func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string, fromSuffix bool) (*ThinkingConfig, error) {
|
||||||
fromFormat, toFormat = strings.ToLower(strings.TrimSpace(fromFormat)), strings.ToLower(strings.TrimSpace(toFormat))
|
fromFormat, toFormat = strings.ToLower(strings.TrimSpace(fromFormat)), strings.ToLower(strings.TrimSpace(toFormat))
|
||||||
model := "unknown"
|
model := "unknown"
|
||||||
support := (*registry.ThinkingSupport)(nil)
|
support := (*registry.ThinkingSupport)(nil)
|
||||||
@@ -52,7 +54,7 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo
|
|||||||
}
|
}
|
||||||
|
|
||||||
allowClampUnsupported := isBudgetBasedProvider(fromFormat) && isLevelBasedProvider(toFormat)
|
allowClampUnsupported := isBudgetBasedProvider(fromFormat) && isLevelBasedProvider(toFormat)
|
||||||
strictBudget := fromFormat != "" && isSameProviderFamily(fromFormat, toFormat)
|
strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat)
|
||||||
budgetDerivedFromLevel := false
|
budgetDerivedFromLevel := false
|
||||||
|
|
||||||
capability := detectModelCapability(modelInfo)
|
capability := detectModelCapability(modelInfo)
|
||||||
@@ -238,7 +240,7 @@ func clampLevel(level ThinkingLevel, modelInfo *registry.ModelInfo, provider str
|
|||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"model": model,
|
"model": model,
|
||||||
"original_level": string(level),
|
"original_value": string(level),
|
||||||
"clamped_to": string(clamped),
|
"clamped_to": string(clamped),
|
||||||
}).Debug("thinking: level clamped |")
|
}).Debug("thinking: level clamped |")
|
||||||
return clamped
|
return clamped
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1001,15 +1001,17 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
|
|||||||
includeThoughts: "true",
|
includeThoughts: "true",
|
||||||
expectErr: false,
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 85: Gemini to Gemini, budget 64000 → exceeds Max error
|
// Case 85: Gemini to Gemini, budget 64000 → clamped to Max
|
||||||
{
|
{
|
||||||
name: "85",
|
name: "85",
|
||||||
from: "gemini",
|
from: "gemini",
|
||||||
to: "gemini",
|
to: "gemini",
|
||||||
model: "gemini-budget-model(64000)",
|
model: "gemini-budget-model(64000)",
|
||||||
inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
|
inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
|
||||||
expectField: "",
|
expectField: "generationConfig.thinkingConfig.thinkingBudget",
|
||||||
expectErr: true,
|
expectValue: "20000",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 86: Claude to Claude, budget 8192 → passthrough thinking.budget_tokens
|
// Case 86: Claude to Claude, budget 8192 → passthrough thinking.budget_tokens
|
||||||
{
|
{
|
||||||
@@ -1022,20 +1024,21 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
|
|||||||
expectValue: "8192",
|
expectValue: "8192",
|
||||||
expectErr: false,
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 87: Claude to Claude, budget 200000 → exceeds Max error
|
// Case 87: Claude to Claude, budget 200000 → clamped to Max
|
||||||
{
|
{
|
||||||
name: "87",
|
name: "87",
|
||||||
from: "claude",
|
from: "claude",
|
||||||
to: "claude",
|
to: "claude",
|
||||||
model: "claude-budget-model(200000)",
|
model: "claude-budget-model(200000)",
|
||||||
inputJSON: `{"model":"claude-budget-model(200000)","messages":[{"role":"user","content":"hi"}]}`,
|
inputJSON: `{"model":"claude-budget-model(200000)","messages":[{"role":"user","content":"hi"}]}`,
|
||||||
expectField: "",
|
expectField: "thinking.budget_tokens",
|
||||||
expectErr: true,
|
expectValue: "128000",
|
||||||
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 88: Antigravity to Antigravity, budget 8192 → passthrough thinkingBudget
|
// Case 88: Gemini-CLI to Antigravity, budget 8192 → passthrough thinkingBudget
|
||||||
{
|
{
|
||||||
name: "88",
|
name: "88",
|
||||||
from: "antigravity",
|
from: "gemini-cli",
|
||||||
to: "antigravity",
|
to: "antigravity",
|
||||||
model: "antigravity-budget-model(8192)",
|
model: "antigravity-budget-model(8192)",
|
||||||
inputJSON: `{"model":"antigravity-budget-model(8192)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
inputJSON: `{"model":"antigravity-budget-model(8192)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
||||||
@@ -1044,15 +1047,17 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
|
|||||||
includeThoughts: "true",
|
includeThoughts: "true",
|
||||||
expectErr: false,
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 89: Antigravity to Antigravity, budget 64000 → exceeds Max error
|
// Case 89: Gemini-CLI to Antigravity, budget 64000 → clamped to Max
|
||||||
{
|
{
|
||||||
name: "89",
|
name: "89",
|
||||||
from: "antigravity",
|
from: "gemini-cli",
|
||||||
to: "antigravity",
|
to: "antigravity",
|
||||||
model: "antigravity-budget-model(64000)",
|
model: "antigravity-budget-model(64000)",
|
||||||
inputJSON: `{"model":"antigravity-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
inputJSON: `{"model":"antigravity-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
||||||
expectField: "",
|
expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
|
||||||
expectErr: true,
|
expectValue: "20000",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// iflow tests: glm-test and minimax-test (Cases 90-105)
|
// iflow tests: glm-test and minimax-test (Cases 90-105)
|
||||||
@@ -1236,45 +1241,53 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
|
|||||||
// Gemini Family Cross-Channel Consistency (Cases 106-114)
|
// Gemini Family Cross-Channel Consistency (Cases 106-114)
|
||||||
// Tests that gemini/gemini-cli/antigravity as same API family should have consistent validation behavior
|
// Tests that gemini/gemini-cli/antigravity as same API family should have consistent validation behavior
|
||||||
|
|
||||||
// Case 106: Gemini to Antigravity, budget 64000 → exceeds Max error (same family strict validation)
|
// Case 106: Gemini to Antigravity, budget 64000 (suffix) → clamped to Max
|
||||||
{
|
{
|
||||||
name: "106",
|
name: "106",
|
||||||
from: "gemini",
|
from: "gemini",
|
||||||
to: "antigravity",
|
to: "antigravity",
|
||||||
model: "gemini-budget-model(64000)",
|
model: "gemini-budget-model(64000)",
|
||||||
inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
|
inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
|
||||||
expectField: "",
|
expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
|
||||||
expectErr: true,
|
expectValue: "20000",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 107: Gemini to Gemini-CLI, budget 64000 → exceeds Max error (same family strict validation)
|
// Case 107: Gemini to Gemini-CLI, budget 64000 (suffix) → clamped to Max
|
||||||
{
|
{
|
||||||
name: "107",
|
name: "107",
|
||||||
from: "gemini",
|
from: "gemini",
|
||||||
to: "gemini-cli",
|
to: "gemini-cli",
|
||||||
model: "gemini-budget-model(64000)",
|
model: "gemini-budget-model(64000)",
|
||||||
inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
|
inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`,
|
||||||
expectField: "",
|
expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
|
||||||
expectErr: true,
|
expectValue: "20000",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 108: Gemini-CLI to Antigravity, budget 64000 → exceeds Max error (same family strict validation)
|
// Case 108: Gemini-CLI to Antigravity, budget 64000 (suffix) → clamped to Max
|
||||||
{
|
{
|
||||||
name: "108",
|
name: "108",
|
||||||
from: "gemini-cli",
|
from: "gemini-cli",
|
||||||
to: "antigravity",
|
to: "antigravity",
|
||||||
model: "gemini-budget-model(64000)",
|
model: "gemini-budget-model(64000)",
|
||||||
inputJSON: `{"model":"gemini-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
inputJSON: `{"model":"gemini-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
||||||
expectField: "",
|
expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
|
||||||
expectErr: true,
|
expectValue: "20000",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 109: Gemini-CLI to Gemini, budget 64000 → exceeds Max error (same family strict validation)
|
// Case 109: Gemini-CLI to Gemini, budget 64000 (suffix) → clamped to Max
|
||||||
{
|
{
|
||||||
name: "109",
|
name: "109",
|
||||||
from: "gemini-cli",
|
from: "gemini-cli",
|
||||||
to: "gemini",
|
to: "gemini",
|
||||||
model: "gemini-budget-model(64000)",
|
model: "gemini-budget-model(64000)",
|
||||||
inputJSON: `{"model":"gemini-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
inputJSON: `{"model":"gemini-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`,
|
||||||
expectField: "",
|
expectField: "generationConfig.thinkingConfig.thinkingBudget",
|
||||||
expectErr: true,
|
expectValue: "20000",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 110: Gemini to Antigravity, budget 8192 → passthrough (normal value)
|
// Case 110: Gemini to Antigravity, budget 8192 → passthrough (normal value)
|
||||||
{
|
{
|
||||||
@@ -2301,10 +2314,10 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
|
|||||||
expectField: "",
|
expectField: "",
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
// Case 88: Antigravity to Antigravity, thinkingBudget=8192 → passthrough
|
// Case 88: Gemini-CLI to Antigravity, thinkingBudget=8192 → passthrough
|
||||||
{
|
{
|
||||||
name: "88",
|
name: "88",
|
||||||
from: "antigravity",
|
from: "gemini-cli",
|
||||||
to: "antigravity",
|
to: "antigravity",
|
||||||
model: "antigravity-budget-model",
|
model: "antigravity-budget-model",
|
||||||
inputJSON: `{"model":"antigravity-budget-model","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":8192}}}}`,
|
inputJSON: `{"model":"antigravity-budget-model","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":8192}}}}`,
|
||||||
@@ -2313,10 +2326,10 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
|
|||||||
includeThoughts: "true",
|
includeThoughts: "true",
|
||||||
expectErr: false,
|
expectErr: false,
|
||||||
},
|
},
|
||||||
// Case 89: Antigravity to Antigravity, thinkingBudget=64000 → exceeds Max error
|
// Case 89: Gemini-CLI to Antigravity, thinkingBudget=64000 → exceeds Max error
|
||||||
{
|
{
|
||||||
name: "89",
|
name: "89",
|
||||||
from: "antigravity",
|
from: "gemini-cli",
|
||||||
to: "antigravity",
|
to: "antigravity",
|
||||||
model: "antigravity-budget-model",
|
model: "antigravity-budget-model",
|
||||||
inputJSON: `{"model":"antigravity-budget-model","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":64000}}}}`,
|
inputJSON: `{"model":"antigravity-budget-model","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":64000}}}}`,
|
||||||
@@ -2744,9 +2757,9 @@ func runThinkingTests(t *testing.T, cases []thinkingTestCase) {
|
|||||||
t.Fatalf("field %s: expected %q, got %q, body=%s", tc.expectField, tc.expectValue, actualValue, string(body))
|
t.Fatalf("field %s: expected %q, got %q, body=%s", tc.expectField, tc.expectValue, actualValue, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.includeThoughts != "" && (tc.to == "gemini" || tc.to == "antigravity") {
|
if tc.includeThoughts != "" && (tc.to == "gemini" || tc.to == "gemini-cli" || tc.to == "antigravity") {
|
||||||
path := "generationConfig.thinkingConfig.includeThoughts"
|
path := "generationConfig.thinkingConfig.includeThoughts"
|
||||||
if tc.to == "antigravity" {
|
if tc.to == "gemini-cli" || tc.to == "antigravity" {
|
||||||
path = "request.generationConfig.thinkingConfig.includeThoughts"
|
path = "request.generationConfig.thinkingConfig.includeThoughts"
|
||||||
}
|
}
|
||||||
itVal := gjson.GetBytes(body, path)
|
itVal := gjson.GetBytes(body, path)
|
||||||
|
|||||||
Reference in New Issue
Block a user