Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,11 @@ nonstream-keepalive-interval: 0
# - name: "kimi-k2.5"
# alias: "claude-opus-4.66"

# Vertex API keys (Vertex-compatible endpoints, use API key + base URL)
# Vertex API keys (Vertex-compatible endpoints, base-url is optional)
# vertex-api-key:
# - api-key: "vk-123..." # x-goog-api-key header
# prefix: "test" # optional: require calls like "test/vertex-pro" to target this credential
# base-url: "https://example.com/api" # e.g. https://zenmux.ai/api
# base-url: "https://example.com/api" # optional, e.g. https://zenmux.ai/api; falls back to Google Vertex when omitted
# proxy-url: "socks5://proxy.example.com:1080" # optional per-key proxy override
# # proxy-url: "direct" # optional: explicit direct connect for this credential
# headers:
Expand Down
6 changes: 5 additions & 1 deletion internal/api/handlers/management/config_lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,12 @@ func (h *Handler) PutVertexCompatKeys(c *gin.Context) {
}
for i := range arr {
normalizeVertexCompatKey(&arr[i])
if arr[i].APIKey == "" {
c.JSON(400, gin.H{"error": fmt.Sprintf("vertex-api-key[%d].api-key is required", i)})
return
}
}
h.cfg.VertexCompatAPIKey = arr
h.cfg.VertexCompatAPIKey = append([]config.VertexCompatKey(nil), arr...)
h.cfg.SanitizeVertexCompatKeys()
h.persist(c)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
// Sanitize Gemini API key configuration and migrate legacy entries.
cfg.SanitizeGeminiKeys()

// Sanitize Vertex-compatible API keys: drop entries without base-url
// Sanitize Vertex-compatible API keys.
cfg.SanitizeVertexCompatKeys()

// Sanitize Codex keys: drop entries without base-url
Expand Down
8 changes: 2 additions & 6 deletions internal/config/vertex_compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ type VertexCompatKey struct {
// Prefix optionally namespaces model aliases for this credential (e.g., "teamA/vertex-pro").
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`

// BaseURL is the base URL for the Vertex-compatible API endpoint.
// BaseURL optionally overrides the Vertex-compatible API endpoint.
// The executor will append "/v1/publishers/google/models/{model}:action" to this.
// Example: "https://zenmux.ai/api" becomes "https://zenmux.ai/api/v1/publishers/google/models/..."
// When empty, requests fall back to the default Vertex API base URL.
BaseURL string `yaml:"base-url,omitempty" json:"base-url,omitempty"`

// ProxyURL optionally overrides the global proxy for this API key.
Expand Down Expand Up @@ -71,10 +71,6 @@ func (cfg *Config) SanitizeVertexCompatKeys() {
}
entry.Prefix = normalizeModelPrefix(entry.Prefix)
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
if entry.BaseURL == "" {
// BaseURL is required for Vertex API key entries
continue
}
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
entry.Headers = NormalizeHeaders(entry.Headers)
entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels)
Expand Down
4 changes: 4 additions & 0 deletions internal/runtime/executor/openai_compat_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
return nil, err
}

// Request usage data in the final streaming chunk so that token statistics
// are captured even when the upstream is an OpenAI-compatible provider.
translated, _ = sjson.SetBytes(translated, "stream_options.include_usage", true)

url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
package claude

import (
"bytes"
"strings"

"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
Expand All @@ -36,7 +36,6 @@ const geminiCLIClaudeThoughtSignature = "skip_thought_signature_validator"
// - []byte: The transformed request data in Gemini CLI API format
func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
rawJSON := inputRawJSON
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)

// Build output Gemini CLI request JSON
out := `{"model":"","request":{"contents":[]}}`
Expand Down Expand Up @@ -149,14 +148,15 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
inputSchemaResult := toolResult.Get("input_schema")
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
inputSchema := inputSchemaResult.Raw
inputSchema := util.CleanJSONSchemaForGemini(inputSchemaResult.Raw)
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
tool, _ = sjson.Delete(tool, "strict")
tool, _ = sjson.Delete(tool, "input_examples")
tool, _ = sjson.Delete(tool, "type")
tool, _ = sjson.Delete(tool, "cache_control")
tool, _ = sjson.Delete(tool, "defer_loading")
tool, _ = sjson.Delete(tool, "eager_input_streaming")
if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
if !hasTools {
out, _ = sjson.SetRaw(out, "request.tools", `[{"functionDeclarations":[]}]`)
Expand Down
17 changes: 17 additions & 0 deletions internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,23 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by
return true
})

// Filter out contents with empty parts to avoid Gemini API error:
// "required oneof field 'data' must have one initialized field"
filteredContents := "[]"
hasFiltered := false
gjson.GetBytes(rawJSON, "request.contents").ForEach(func(_, content gjson.Result) bool {
parts := content.Get("parts")
if !parts.IsArray() || len(parts.Array()) == 0 {
hasFiltered = true
return true
}
filteredContents, _ = sjson.SetRaw(filteredContents, "-1", content.Raw)
return true
})
if hasFiltered {
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", []byte(filteredContents))
}
Comment on lines +116 to +129

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For filtering the contents array, instead of creating a new JSON array string and replacing the old one, you could consider an in-place removal approach. This can be more efficient, especially if only a few items need to be removed, as it avoids reconstructing the entire array.

You can collect the indices of items to be deleted and then remove them from the JSON using sjson.DeleteBytes, iterating backwards to avoid index shifting issues.

Suggested change
filteredContents := "[]"
hasFiltered := false
gjson.GetBytes(rawJSON, "request.contents").ForEach(func(_, content gjson.Result) bool {
parts := content.Get("parts")
if !parts.IsArray() || len(parts.Array()) == 0 {
hasFiltered = true
return true
}
filteredContents, _ = sjson.SetRaw(filteredContents, "-1", content.Raw)
return true
})
if hasFiltered {
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", []byte(filteredContents))
}
indicesToDelete := []int{}
gjson.GetBytes(rawJSON, "request.contents").ForEach(func(key, content gjson.Result) bool {
parts := content.Get("parts")
if !parts.IsArray() || len(parts.Array()) == 0 {
indicesToDelete = append(indicesToDelete, int(key.Int()))
}
return true
})
if len(indicesToDelete) > 0 {
// Iterate backwards to avoid index shifting issues during deletion.
for i := len(indicesToDelete) - 1; i >= 0; i-- {
path := fmt.Sprintf("request.contents.%d", indicesToDelete[i])
rawJSON, _ = sjson.DeleteBytes(rawJSON, path)
}
}


return common.AttachDefaultSafetySettings(rawJSON, "request.safetySettings")
}

Expand Down
2 changes: 1 addition & 1 deletion sdk/cliproxy/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
models = registry.GetCodexProModels()
case "plus":
models = registry.GetCodexPlusModels()
case "team":
case "team", "business", "go":
models = registry.GetCodexTeamModels()
case "free":
models = registry.GetCodexFreeModels()
Expand Down
Loading