Compare commits

...

9 Commits

Author SHA1 Message Date
Luis Pater
faa483b57d Merge pull request #257 from lollipopkit/main
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
fix(claude translator): guard tool schema properties
2025-11-16 12:19:38 +08:00
Luis Pater
f0711be302 **fix(auth): prevent access to removed credentials lingering in memory**
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
Add logic to avoid exposing credentials that have been removed from disk but still persist in memory. Ensure `runtimeOnly` checks and proper handling of disabled or removed authentication states.
2025-11-16 12:12:24 +08:00
Luis Pater
1d0f0301b4 **refactor(api/config): centralize legacy OpenAI compatibility key migration**
Introduce `migrateLegacyOpenAICompatibilityKeys` to streamline and reuse the normalization of OpenAI compatibility entries. Remove redundant loops and enhance maintainability for compatibility key handling. Add cleanup for legacy `api-keys` in YAML configuration during persistence.
2025-11-16 11:39:35 +08:00
lollipopkit🏳️‍⚧️
c73b3fa43b fix(claude translator): guard tool schema properties 2025-11-15 19:14:13 +08:00
Luis Pater
772fa69515 Fixed: #254
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
**feat(registry): add Kimi-K2-Thinking model to model definitions**
2025-11-14 21:20:54 +08:00
Luis Pater
1ccb01631d **refactor(runtime): centralize reasoning effort logic for GPT models**
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
Extract reasoning effort mapping into a reusable function `setReasoningEffortByAlias` to reduce redundancy and improve maintainability. Introduce support for the "gpt-5.1-none" variant in the registry and runtime executor.
2025-11-14 17:24:40 +08:00
Luis Pater
1ede1347fa Merge pull request #249 from ben-vargas/fix-gpt5-1-reasoning
fix(runtime): remove gpt-5.1 minimal effort variant
2025-11-14 17:04:27 +08:00
Ben Vargas
cfbaed0e90 fix(runtime): remove gpt-5.1 minimal effort variant
Stop advertising and mapping the unsupported gpt-5.1-minimal variant in the model registry and Codex executor, and align bare gpt-5.1 requests to use medium reasoning effort like Codex CLI while preserving minimal for gpt-5.
2025-11-13 19:43:52 -07:00
Luis Pater
cf9b9be7ea **feat(runtime): extend executor support for GPT-5.1 Codex and variants**
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
Expand executor logic to handle GPT-5.1 Codex family and its variants, including reasoning effort configurations for minimal, low, medium, and high levels. Ensure proper mapping of models to payload parameters.
2025-11-14 08:08:25 +08:00
6 changed files with 126 additions and 143 deletions

View File

@@ -346,6 +346,10 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H {
entry["size"] = info.Size()
entry["modtime"] = info.ModTime()
} else if os.IsNotExist(err) {
// Hide credentials removed from disk but still lingering in memory.
if !runtimeOnly && (auth.Disabled || auth.Status == coreauth.StatusDisabled || strings.EqualFold(strings.TrimSpace(auth.StatusMessage), "removed via management api")) {
return nil
}
entry["source"] = "memory"
} else {
log.WithError(err).Warnf("failed to stat auth file %s", path)

View File

@@ -408,9 +408,7 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) {
}
arr = obj.Items
}
for i := range arr {
normalizeOpenAICompatibilityEntry(&arr[i])
}
arr = migrateLegacyOpenAICompatibilityKeys(arr)
// Filter out providers with empty base-url -> remove provider entirely
filtered := make([]config.OpenAICompatibility, 0, len(arr))
for i := range arr {
@@ -418,7 +416,7 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) {
filtered = append(filtered, arr[i])
}
}
h.cfg.OpenAICompatibility = filtered
h.cfg.OpenAICompatibility = migrateLegacyOpenAICompatibilityKeys(filtered)
h.cfg.SanitizeOpenAICompatibility()
h.persist(c)
}
@@ -432,6 +430,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
c.JSON(400, gin.H{"error": "invalid body"})
return
}
h.cfg.OpenAICompatibility = migrateLegacyOpenAICompatibilityKeys(h.cfg.OpenAICompatibility)
normalizeOpenAICompatibilityEntry(body.Value)
// If base-url becomes empty, delete the provider instead of updating
if strings.TrimSpace(body.Value.BaseURL) == "" {
@@ -661,6 +660,13 @@ func normalizeOpenAICompatibilityEntry(entry *config.OpenAICompatibility) {
entry.APIKeys = nil
}
func migrateLegacyOpenAICompatibilityKeys(entries []config.OpenAICompatibility) []config.OpenAICompatibility {
for i := range entries {
normalizeOpenAICompatibilityEntry(&entries[i])
}
return entries
}
func normalizedOpenAICompatibilityEntries(entries []config.OpenAICompatibility) []config.OpenAICompatibility {
if len(entries) == 0 {
return nil

View File

@@ -479,6 +479,7 @@ func SaveConfigPreserveComments(configFile string, cfg *Config) error {
// Remove deprecated auth block before merging to avoid persisting it again.
removeMapKey(original.Content[0], "auth")
removeLegacyOpenAICompatAPIKeys(original.Content[0])
// Merge generated into original in-place, preserving comments/order of existing nodes.
mergeMappingPreserve(original.Content[0], generated.Content[0])
@@ -935,6 +936,25 @@ func removeMapKey(mapNode *yaml.Node, key string) {
}
}
func removeLegacyOpenAICompatAPIKeys(root *yaml.Node) {
if root == nil || root.Kind != yaml.MappingNode {
return
}
idx := findMapKeyIndex(root, "openai-compatibility")
if idx < 0 || idx+1 >= len(root.Content) {
return
}
seq := root.Content[idx+1]
if seq == nil || seq.Kind != yaml.SequenceNode {
return
}
for i := range seq.Content {
if seq.Content[i] != nil && seq.Content[i].Kind == yaml.MappingNode {
removeMapKey(seq.Content[i], "api-keys")
}
}
}
// normalizeCollectionNodeStyles forces YAML collections to use block notation, keeping
// lists and maps readable. Empty sequences retain flow style ([]) so empty list markers
// remain compact.

View File

@@ -431,13 +431,13 @@ func GetOpenAIModels() []*ModelInfo {
SupportedParameters: []string{"tools"},
},
{
ID: "gpt-5.1-minimal",
ID: "gpt-5.1-none",
Object: "model",
Created: time.Now().Unix(),
OwnedBy: "openai",
Type: "openai",
Version: "gpt-5.1-2025-11-12",
DisplayName: "GPT 5 Minimal",
DisplayName: "GPT 5 Low",
Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.",
ContextLength: 400000,
MaxCompletionTokens: 128000,
@@ -639,6 +639,7 @@ func GetIFlowModels() []*ModelInfo {
{ID: "kimi-k2-0905", DisplayName: "Kimi-K2-Instruct-0905", Description: "Moonshot Kimi K2 instruct 0905"},
{ID: "glm-4.6", DisplayName: "GLM-4.6", Description: "Zhipu GLM 4.6 general model"},
{ID: "kimi-k2", DisplayName: "Kimi-K2", Description: "Moonshot Kimi K2 general model"},
{ID: "kimi-k2-thinking", DisplayName: "Kimi-K2-Thinking", Description: "Moonshot Kimi K2 general model"},
{ID: "deepseek-v3.2", DisplayName: "DeepSeek-V3.2-Exp", Description: "DeepSeek V3.2 experimental"},
{ID: "deepseek-v3.1", DisplayName: "DeepSeek-V3.1-Terminus", Description: "DeepSeek V3.1 Terminus"},
{ID: "deepseek-r1", DisplayName: "DeepSeek-R1", Description: "DeepSeek reasoning model R1"},

View File

@@ -53,71 +53,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
to := sdktranslator.FromString("codex")
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
if util.InArray([]string{"gpt-5", "gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5")
switch req.Model {
case "gpt-5-minimal":
body, _ = sjson.SetBytes(body, "reasoning.effort", "minimal")
case "gpt-5-low":
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
case "gpt-5-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5-codex")
switch req.Model {
case "gpt-5-codex-low":
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
case "gpt-5-codex-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-codex-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5-codex-mini", "gpt-5-codex-mini-medium", "gpt-5-codex-mini-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5-codex-mini")
switch req.Model {
case "gpt-5-codex-mini-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-codex-mini-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
default:
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
}
} else if util.InArray([]string{"gpt-5.1", "gpt-5.1-minimal", "gpt-5.1-low", "gpt-5.1-medium", "gpt-5.1-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5.1")
switch req.Model {
case "gpt-5.1-minimal":
body, _ = sjson.SetBytes(body, "reasoning.effort", "minimal")
case "gpt-5.1-low":
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
case "gpt-5.1-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5.1-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5.1-codex", "gpt-5.1-codex-low", "gpt-5.1-codex-medium", "gpt-5.1-codex-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5.1-codex")
switch req.Model {
case "gpt-5.1-codex-low":
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
case "gpt-5.1-codex-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5.1-codex-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5.1-codex-mini", "gpt-5.1-codex-mini-medium", "gpt-5.1-codex-mini-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5.1-codex-mini")
switch req.Model {
case "gpt-5.1-codex-mini-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5.1-codex-mini-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
default:
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
}
}
body = e.setReasoningEffortByAlias(req.Model, body)
body = applyPayloadConfig(e.cfg, req.Model, body)
body, _ = sjson.SetBytes(body, "stream", true)
@@ -209,37 +146,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
to := sdktranslator.FromString("codex")
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
if util.InArray([]string{"gpt-5", "gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5")
switch req.Model {
case "gpt-5-minimal":
body, _ = sjson.SetBytes(body, "reasoning.effort", "minimal")
case "gpt-5-low":
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
case "gpt-5-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5-codex")
switch req.Model {
case "gpt-5-codex-low":
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
case "gpt-5-codex-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-codex-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5-codex-mini", "gpt-5-codex-mini-medium", "gpt-5-codex-mini-high"}, req.Model) {
body, _ = sjson.SetBytes(body, "model", "gpt-5-codex-mini")
switch req.Model {
case "gpt-5-codex-mini-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-codex-mini-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
}
}
body = e.setReasoningEffortByAlias(req.Model, body)
body = applyPayloadConfig(e.cfg, req.Model, body)
body, _ = sjson.DeleteBytes(body, "previous_response_id")
@@ -335,46 +242,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
modelForCounting := req.Model
if util.InArray([]string{"gpt-5", "gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) {
modelForCounting = "gpt-5"
body, _ = sjson.SetBytes(body, "model", "gpt-5")
switch req.Model {
case "gpt-5-minimal":
body, _ = sjson.SetBytes(body, "reasoning.effort", "minimal")
case "gpt-5-low":
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
case "gpt-5-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
default:
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
}
} else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, req.Model) {
modelForCounting = "gpt-5"
body, _ = sjson.SetBytes(body, "model", "gpt-5-codex")
switch req.Model {
case "gpt-5-codex-low":
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
case "gpt-5-codex-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-codex-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
default:
body, _ = sjson.SetBytes(body, "reasoning.effort", "low")
}
} else if util.InArray([]string{"gpt-5-codex-mini", "gpt-5-codex-mini-medium", "gpt-5-codex-mini-high"}, req.Model) {
modelForCounting = "gpt-5"
body, _ = sjson.SetBytes(body, "model", "codex-mini-latest")
switch req.Model {
case "gpt-5-codex-mini-medium":
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
case "gpt-5-codex-mini-high":
body, _ = sjson.SetBytes(body, "reasoning.effort", "high")
default:
body, _ = sjson.SetBytes(body, "reasoning.effort", "medium")
}
}
body = e.setReasoningEffortByAlias(req.Model, body)
body, _ = sjson.DeleteBytes(body, "previous_response_id")
body, _ = sjson.SetBytes(body, "stream", false)
@@ -394,6 +262,71 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
}
func (e *CodexExecutor) setReasoningEffortByAlias(modelName string, payload []byte) []byte {
if util.InArray([]string{"gpt-5", "gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, modelName) {
payload, _ = sjson.SetBytes(payload, "model", "gpt-5")
switch modelName {
case "gpt-5-minimal":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "minimal")
case "gpt-5-low":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
case "gpt-5-medium":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
case "gpt-5-high":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, modelName) {
payload, _ = sjson.SetBytes(payload, "model", "gpt-5-codex")
switch modelName {
case "gpt-5-codex-low":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
case "gpt-5-codex-medium":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
case "gpt-5-codex-high":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5-codex-mini", "gpt-5-codex-mini-medium", "gpt-5-codex-mini-high"}, modelName) {
payload, _ = sjson.SetBytes(payload, "model", "gpt-5-codex-mini")
switch modelName {
case "gpt-5-codex-mini-medium":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
case "gpt-5-codex-mini-high":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5.1", "gpt-5.1-none", "gpt-5.1-low", "gpt-5.1-medium", "gpt-5.1-high"}, modelName) {
payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1")
switch modelName {
case "gpt-5.1-none":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "none")
case "gpt-5.1-low":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
case "gpt-5.1-medium":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
case "gpt-5.1-high":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5.1-codex", "gpt-5.1-codex-low", "gpt-5.1-codex-medium", "gpt-5.1-codex-high"}, modelName) {
payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1-codex")
switch modelName {
case "gpt-5.1-codex-low":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
case "gpt-5.1-codex-medium":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
case "gpt-5.1-codex-high":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5.1-codex-mini", "gpt-5.1-codex-mini-medium", "gpt-5.1-codex-mini-high"}, modelName) {
payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1-codex-mini")
switch modelName {
case "gpt-5.1-codex-mini-medium":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
case "gpt-5.1-codex-mini-high":
payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
}
}
return payload
}
func tokenizerForCodexModel(model string) (tokenizer.Codec, error) {
sanitized := strings.ToLower(strings.TrimSpace(model))
switch {

View File

@@ -204,7 +204,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
}
tool, _ = sjson.Set(tool, "name", name)
}
tool, _ = sjson.SetRaw(tool, "parameters", toolResult.Get("input_schema").Raw)
tool, _ = sjson.SetRaw(tool, "parameters", normalizeToolParameters(toolResult.Get("input_schema").Raw))
tool, _ = sjson.Delete(tool, "input_schema")
tool, _ = sjson.Delete(tool, "parameters.$schema")
tool, _ = sjson.Set(tool, "strict", false)
@@ -334,3 +334,22 @@ func buildReverseMapFromClaudeOriginalToShort(original []byte) map[string]string
}
return m
}
// normalizeToolParameters ensures object schemas contain at least an empty properties map.
func normalizeToolParameters(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" || raw == "null" || !gjson.Valid(raw) {
return `{"type":"object","properties":{}}`
}
schema := raw
result := gjson.Parse(raw)
schemaType := result.Get("type").String()
if schemaType == "" {
schema, _ = sjson.Set(schema, "type", "object")
schemaType = "object"
}
if schemaType == "object" && !result.Get("properties").Exists() {
schema, _ = sjson.SetRaw(schema, "properties", `{}`)
}
return schema
}