From 166d2d24d9bdb9632591f2397e75bb9851a1be90 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 Feb 2026 18:29:17 +1100 Subject: [PATCH 1/5] fix(schema): remove Gemini-incompatible tool metadata fields Sanitize tool schemas by stripping prefill, enumTitles, $id, and patternProperties to prevent Gemini INVALID_ARGUMENT 400 errors, and add unit and executor-level tests to lock in the behavior. Co-Authored-By: Claude Opus 4.6 --- .../antigravity_executor_buildrequest_test.go | 159 ++++++++++++++++++ internal/util/gemini_schema.go | 5 +- internal/util/gemini_schema_test.go | 51 ++++++ 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 internal/runtime/executor/antigravity_executor_buildrequest_test.go diff --git a/internal/runtime/executor/antigravity_executor_buildrequest_test.go b/internal/runtime/executor/antigravity_executor_buildrequest_test.go new file mode 100644 index 00000000..c5cba4ee --- /dev/null +++ b/internal/runtime/executor/antigravity_executor_buildrequest_test.go @@ -0,0 +1,159 @@ +package executor + +import ( + "context" + "encoding/json" + "io" + "testing" + + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" +) + +func TestAntigravityBuildRequest_SanitizesGeminiToolSchema(t *testing.T) { + body := buildRequestBodyFromPayload(t, "gemini-2.5-pro") + + decl := extractFirstFunctionDeclaration(t, body) + if _, ok := decl["parametersJsonSchema"]; ok { + t.Fatalf("parametersJsonSchema should be renamed to parameters") + } + + params, ok := decl["parameters"].(map[string]any) + if !ok { + t.Fatalf("parameters missing or invalid type") + } + assertSchemaSanitizedAndPropertyPreserved(t, params) +} + +func TestAntigravityBuildRequest_SanitizesAntigravityToolSchema(t *testing.T) { + body := buildRequestBodyFromPayload(t, "claude-opus-4-6") + + decl := extractFirstFunctionDeclaration(t, body) + params, ok := decl["parameters"].(map[string]any) + if !ok { + t.Fatalf("parameters missing or invalid type") + } + assertSchemaSanitizedAndPropertyPreserved(t, params) +} + +func buildRequestBodyFromPayload(t *testing.T, modelName string) map[string]any { + t.Helper() + + executor := &AntigravityExecutor{} + auth := &cliproxyauth.Auth{} + payload := []byte(`{ + "request": { + "tools": [ + { + "function_declarations": [ + { + "name": "tool_1", + "parametersJsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "root-schema", + "type": "object", + "properties": { + "$id": {"type": "string"}, + "arg": { + "type": "object", + "prefill": "hello", + "properties": { + "mode": { + "type": "string", + "enum": ["a", "b"], + "enumTitles": ["A", "B"] + } + } + } + }, + "patternProperties": { + "^x-": {"type": "string"} + } + } + } + ] + } + ] + } + }`) + + req, err := executor.buildRequest(context.Background(), auth, "token", modelName, payload, false, "", "https://example.com") + if err != nil { + t.Fatalf("buildRequest error: %v", err) + } + + raw, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("read request body error: %v", err) + } + + var body map[string]any + if err := json.Unmarshal(raw, &body); err != nil { + t.Fatalf("unmarshal request body error: %v, body=%s", err, string(raw)) + } + return body +} + +func extractFirstFunctionDeclaration(t *testing.T, body map[string]any) map[string]any { + t.Helper() + + request, ok := body["request"].(map[string]any) + if !ok { + t.Fatalf("request missing or invalid type") + } + tools, ok := request["tools"].([]any) + if !ok || len(tools) == 0 { + t.Fatalf("tools missing or empty") + } + tool, ok := tools[0].(map[string]any) + if !ok { + t.Fatalf("first tool invalid type") + } + decls, ok := tool["function_declarations"].([]any) + if !ok || len(decls) == 0 { + t.Fatalf("function_declarations missing or empty") + } + decl, ok := decls[0].(map[string]any) + if !ok { + t.Fatalf("first function declaration invalid type") + } + return decl +} + +func assertSchemaSanitizedAndPropertyPreserved(t *testing.T, params map[string]any) { + t.Helper() + + if _, ok := params["$id"]; ok { + t.Fatalf("root $id should be removed from schema") + } + if _, ok := params["patternProperties"]; ok { + t.Fatalf("patternProperties should be removed from schema") + } + + props, ok := params["properties"].(map[string]any) + if !ok { + t.Fatalf("properties missing or invalid type") + } + if _, ok := props["$id"]; !ok { + t.Fatalf("property named $id should be preserved") + } + + arg, ok := props["arg"].(map[string]any) + if !ok { + t.Fatalf("arg property missing or invalid type") + } + if _, ok := arg["prefill"]; ok { + t.Fatalf("prefill should be removed from nested schema") + } + + argProps, ok := arg["properties"].(map[string]any) + if !ok { + t.Fatalf("arg.properties missing or invalid type") + } + mode, ok := argProps["mode"].(map[string]any) + if !ok { + t.Fatalf("mode property missing or invalid type") + } + if _, ok := mode["enumTitles"]; ok { + t.Fatalf("enumTitles should be removed from nested schema") + } +} diff --git a/internal/util/gemini_schema.go b/internal/util/gemini_schema.go index e74d1271..b8d07bf4 100644 --- a/internal/util/gemini_schema.go +++ b/internal/util/gemini_schema.go @@ -428,8 +428,9 @@ func flattenTypeArrays(jsonStr string) string { func removeUnsupportedKeywords(jsonStr string) string { keywords := append(unsupportedConstraints, - "$schema", "$defs", "definitions", "const", "$ref", "additionalProperties", - "propertyNames", // Gemini doesn't support property name validation + "$schema", "$defs", "definitions", "const", "$ref", "$id", "additionalProperties", + "propertyNames", "patternProperties", // Gemini doesn't support these schema keywords + "enumTitles", "prefill", // Claude/OpenCode schema metadata fields unsupported by Gemini ) deletePaths := make([]string, 0) diff --git a/internal/util/gemini_schema_test.go b/internal/util/gemini_schema_test.go index ea63d111..bb06e956 100644 --- a/internal/util/gemini_schema_test.go +++ b/internal/util/gemini_schema_test.go @@ -870,6 +870,57 @@ func TestCleanJSONSchemaForAntigravity_BooleanEnumToString(t *testing.T) { } } +func TestCleanJSONSchemaForGemini_RemovesGeminiUnsupportedMetadataFields(t *testing.T) { + input := `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "root-schema", + "type": "object", + "properties": { + "payload": { + "type": "object", + "prefill": "hello", + "properties": { + "mode": { + "type": "string", + "enum": ["a", "b"], + "enumTitles": ["A", "B"] + } + }, + "patternProperties": { + "^x-": {"type": "string"} + } + }, + "$id": { + "type": "string", + "description": "property name should not be removed" + } + } + }` + + expected := `{ + "type": "object", + "properties": { + "payload": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["a", "b"], + "description": "Allowed: a, b" + } + } + }, + "$id": { + "type": "string", + "description": "property name should not be removed" + } + } + }` + + result := CleanJSONSchemaForGemini(input) + compareJSON(t, expected, result) +} + func TestRemoveExtensionFields(t *testing.T) { tests := []struct { name string From f361b2716da08496c478d9cdcde4ccfbd64b4f59 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:13:28 +0800 Subject: [PATCH 2/5] feat(registry): add glm-5 model to iflow --- internal/registry/model_definitions_static_data.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/registry/model_definitions_static_data.go b/internal/registry/model_definitions_static_data.go index bd7d74a4..a44bc596 100644 --- a/internal/registry/model_definitions_static_data.go +++ b/internal/registry/model_definitions_static_data.go @@ -814,6 +814,7 @@ func GetIFlowModels() []*ModelInfo { {ID: "kimi-k2-0905", DisplayName: "Kimi-K2-Instruct-0905", Description: "Moonshot Kimi K2 instruct 0905", Created: 1757030400}, {ID: "glm-4.6", DisplayName: "GLM-4.6", Description: "Zhipu GLM 4.6 general model", Created: 1759190400, Thinking: iFlowThinkingSupport}, {ID: "glm-4.7", DisplayName: "GLM-4.7", Description: "Zhipu GLM 4.7 general model", Created: 1766448000, Thinking: iFlowThinkingSupport}, + {ID: "glm-5", DisplayName: "GLM-5", Description: "Zhipu GLM 5 general model", Created: 1770768000, Thinking: iFlowThinkingSupport}, {ID: "kimi-k2", DisplayName: "Kimi-K2", Description: "Moonshot Kimi K2 general model", Created: 1752192000}, {ID: "kimi-k2-thinking", DisplayName: "Kimi-K2-Thinking", Description: "Moonshot Kimi K2 thinking model", Created: 1762387200}, {ID: "deepseek-v3.2-chat", DisplayName: "DeepSeek-V3.2", Description: "DeepSeek V3.2 Chat", Created: 1764576000}, From 575881cb59723b8fa997913600bb37b5923987ba Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Thu, 12 Feb 2026 22:43:01 +0800 Subject: [PATCH 3/5] feat(registry): add new model definition for MiniMax-M2.5 --- internal/registry/model_definitions_static_data.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/registry/model_definitions_static_data.go b/internal/registry/model_definitions_static_data.go index a44bc596..baf39412 100644 --- a/internal/registry/model_definitions_static_data.go +++ b/internal/registry/model_definitions_static_data.go @@ -829,6 +829,7 @@ func GetIFlowModels() []*ModelInfo { {ID: "qwen3-235b", DisplayName: "Qwen3-235B-A22B", Description: "Qwen3 235B A22B", Created: 1753401600}, {ID: "minimax-m2", DisplayName: "MiniMax-M2", Description: "MiniMax M2", Created: 1758672000, Thinking: iFlowThinkingSupport}, {ID: "minimax-m2.1", DisplayName: "MiniMax-M2.1", Description: "MiniMax M2.1", Created: 1766448000, Thinking: iFlowThinkingSupport}, + {ID: "minimax-m2.5", DisplayName: "MiniMax-M2.5", Description: "MiniMax M2.5", Created: 1770825600, Thinking: iFlowThinkingSupport}, {ID: "iflow-rome-30ba3b", DisplayName: "iFlow-ROME", Description: "iFlow Rome 30BA3B model", Created: 1736899200}, {ID: "kimi-k2.5", DisplayName: "Kimi-K2.5", Description: "Moonshot Kimi K2.5", Created: 1769443200, Thinking: iFlowThinkingSupport}, } From 4b2d40bd67bb9be51403c420c21adf17bdf33618 Mon Sep 17 00:00:00 2001 From: xSpaM <34112129+itsmylife44@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:15:46 +0100 Subject: [PATCH 4/5] Add CLIProxyAPI Dashboard to 'Who is with us?' section --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 214fe600..4fa495c6 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,10 @@ A Windows tray application implemented using PowerShell scripts, without relying 霖君 is a cross-platform desktop application for managing AI programming assistants, supporting macOS, Windows, and Linux systems. Unified management of Claude Code, Gemini CLI, OpenAI Codex, Qwen Code, and other AI coding tools, with local proxy for multi-account quota tracking and one-click configuration. +### [CLIProxyAPI Dashboard](https://github.com/itsmylife44/cliproxyapi-dashboard) + +A modern web-based management dashboard for CLIProxyAPI built with Next.js, React, and PostgreSQL. Features real-time log streaming, structured configuration editing, API key management, OAuth provider integration for Claude/Gemini/Codex, usage analytics, container management, and config sync with OpenCode via companion plugin - no manual YAML editing needed. + > [!NOTE] > If you developed a project based on CLIProxyAPI, please open a PR to add it to this list. From 1ff5de9a311de8c129af069e6f0433273180fd08 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 13 Feb 2026 00:40:39 +0800 Subject: [PATCH 5/5] docs(readme): add CLIProxyAPI Dashboard to project list --- README_CN.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README_CN.md b/README_CN.md index b7c45df7..5c91cbdc 100644 --- a/README_CN.md +++ b/README_CN.md @@ -145,6 +145,10 @@ Windows 托盘应用,基于 PowerShell 脚本实现,不依赖任何第三方 霖君是一款用于管理AI编程助手的跨平台桌面应用,支持macOS、Windows、Linux系统。统一管理Claude Code、Gemini CLI、OpenAI Codex、Qwen Code等AI编程工具,本地代理实现多账户配额跟踪和一键配置。 +### [CLIProxyAPI Dashboard](https://github.com/itsmylife44/cliproxyapi-dashboard) + +一个面向 CLIProxyAPI 的现代化 Web 管理仪表盘,基于 Next.js、React 和 PostgreSQL 构建。支持实时日志流、结构化配置编辑、API Key 管理、Claude/Gemini/Codex 的 OAuth 提供方集成、使用量分析、容器管理,并可通过配套插件与 OpenCode 同步配置,无需手动编辑 YAML。 + > [!NOTE] > 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。