From 5c817a9b429b0bf657fb397c5befa3945e8618db Mon Sep 17 00:00:00 2001 From: DragonFSKY Date: Sat, 14 Mar 2026 23:46:23 +0800 Subject: [PATCH 01/21] fix(auth): prevent stale ModelStates inheritance from disabled auth entries When an auth file is deleted and re-created with the same path/ID, the new auth could inherit stale ModelStates (cooldown/backoff) from the previously disabled entry, preventing it from being routed. Gate runtime state inheritance (ModelStates, LastRefreshedAt, NextRefreshAfter) on both existing and incoming auth being non-disabled in Manager.Update and Service.applyCoreAuthAddOrUpdate. Closes #2061 --- sdk/cliproxy/auth/conductor.go | 6 +- sdk/cliproxy/auth/conductor_update_test.go | 155 +++++++++++++++++++++ sdk/cliproxy/service.go | 10 +- 3 files changed, 165 insertions(+), 6 deletions(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index b29e04db..aab23305 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -832,8 +832,10 @@ func (m *Manager) Update(ctx context.Context, auth *Auth) (*Auth, error) { auth.Index = existing.Index auth.indexAssigned = existing.indexAssigned } - if len(auth.ModelStates) == 0 && len(existing.ModelStates) > 0 { - auth.ModelStates = existing.ModelStates + if !existing.Disabled && existing.Status != StatusDisabled && !auth.Disabled && auth.Status != StatusDisabled { + if len(auth.ModelStates) == 0 && len(existing.ModelStates) > 0 { + auth.ModelStates = existing.ModelStates + } } } auth.EnsureIndex() diff --git a/sdk/cliproxy/auth/conductor_update_test.go b/sdk/cliproxy/auth/conductor_update_test.go index f058f517..7dd44ff8 100644 --- a/sdk/cliproxy/auth/conductor_update_test.go +++ b/sdk/cliproxy/auth/conductor_update_test.go @@ -47,3 +47,158 @@ func TestManager_Update_PreservesModelStates(t *testing.T) { t.Fatalf("expected BackoffLevel to be %d, got %d", backoffLevel, state.Quota.BackoffLevel) } } + +func TestManager_Update_DisabledExistingDoesNotInheritModelStates(t *testing.T) { + m := NewManager(nil, nil, nil) + + // Register a disabled auth with existing ModelStates. + if _, err := m.Register(context.Background(), &Auth{ + ID: "auth-disabled", + Provider: "claude", + Disabled: true, + Status: StatusDisabled, + ModelStates: map[string]*ModelState{ + "stale-model": { + Quota: QuotaState{BackoffLevel: 5}, + }, + }, + }); err != nil { + t.Fatalf("register auth: %v", err) + } + + // Update with empty ModelStates — should NOT inherit stale states. + if _, err := m.Update(context.Background(), &Auth{ + ID: "auth-disabled", + Provider: "claude", + Disabled: true, + Status: StatusDisabled, + }); err != nil { + t.Fatalf("update auth: %v", err) + } + + updated, ok := m.GetByID("auth-disabled") + if !ok || updated == nil { + t.Fatalf("expected auth to be present") + } + if len(updated.ModelStates) != 0 { + t.Fatalf("expected disabled auth NOT to inherit ModelStates, got %d entries", len(updated.ModelStates)) + } +} + +func TestManager_Update_ActiveToDisabledDoesNotInheritModelStates(t *testing.T) { + m := NewManager(nil, nil, nil) + + // Register an active auth with ModelStates (simulates existing live auth). + if _, err := m.Register(context.Background(), &Auth{ + ID: "auth-a2d", + Provider: "claude", + Status: StatusActive, + ModelStates: map[string]*ModelState{ + "stale-model": { + Quota: QuotaState{BackoffLevel: 9}, + }, + }, + }); err != nil { + t.Fatalf("register auth: %v", err) + } + + // File watcher deletes config → synthesizes Disabled=true auth → Update. + // Even though existing is active, incoming auth is disabled → skip inheritance. + if _, err := m.Update(context.Background(), &Auth{ + ID: "auth-a2d", + Provider: "claude", + Disabled: true, + Status: StatusDisabled, + }); err != nil { + t.Fatalf("update auth: %v", err) + } + + updated, ok := m.GetByID("auth-a2d") + if !ok || updated == nil { + t.Fatalf("expected auth to be present") + } + if len(updated.ModelStates) != 0 { + t.Fatalf("expected active→disabled transition NOT to inherit ModelStates, got %d entries", len(updated.ModelStates)) + } +} + +func TestManager_Update_DisabledToActiveDoesNotInheritStaleModelStates(t *testing.T) { + m := NewManager(nil, nil, nil) + + // Register a disabled auth with stale ModelStates. + if _, err := m.Register(context.Background(), &Auth{ + ID: "auth-d2a", + Provider: "claude", + Disabled: true, + Status: StatusDisabled, + ModelStates: map[string]*ModelState{ + "stale-model": { + Quota: QuotaState{BackoffLevel: 4}, + }, + }, + }); err != nil { + t.Fatalf("register auth: %v", err) + } + + // Re-enable: incoming auth is active, existing is disabled → skip inheritance. + if _, err := m.Update(context.Background(), &Auth{ + ID: "auth-d2a", + Provider: "claude", + Status: StatusActive, + }); err != nil { + t.Fatalf("update auth: %v", err) + } + + updated, ok := m.GetByID("auth-d2a") + if !ok || updated == nil { + t.Fatalf("expected auth to be present") + } + if len(updated.ModelStates) != 0 { + t.Fatalf("expected disabled→active transition NOT to inherit stale ModelStates, got %d entries", len(updated.ModelStates)) + } +} + +func TestManager_Update_ActiveInheritsModelStates(t *testing.T) { + m := NewManager(nil, nil, nil) + + model := "active-model" + backoffLevel := 3 + + // Register an active auth with ModelStates. + if _, err := m.Register(context.Background(), &Auth{ + ID: "auth-active", + Provider: "claude", + Status: StatusActive, + ModelStates: map[string]*ModelState{ + model: { + Quota: QuotaState{BackoffLevel: backoffLevel}, + }, + }, + }); err != nil { + t.Fatalf("register auth: %v", err) + } + + // Update with empty ModelStates — both sides active → SHOULD inherit. + if _, err := m.Update(context.Background(), &Auth{ + ID: "auth-active", + Provider: "claude", + Status: StatusActive, + }); err != nil { + t.Fatalf("update auth: %v", err) + } + + updated, ok := m.GetByID("auth-active") + if !ok || updated == nil { + t.Fatalf("expected auth to be present") + } + if len(updated.ModelStates) == 0 { + t.Fatalf("expected active auth to inherit ModelStates") + } + state := updated.ModelStates[model] + if state == nil { + t.Fatalf("expected model state to be present") + } + if state.Quota.BackoffLevel != backoffLevel { + t.Fatalf("expected BackoffLevel to be %d, got %d", backoffLevel, state.Quota.BackoffLevel) + } +} diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index abe1deed..e5c7e76c 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -286,10 +286,12 @@ func (s *Service) applyCoreAuthAddOrUpdate(ctx context.Context, auth *coreauth.A var err error if existing, ok := s.coreManager.GetByID(auth.ID); ok { auth.CreatedAt = existing.CreatedAt - auth.LastRefreshedAt = existing.LastRefreshedAt - auth.NextRefreshAfter = existing.NextRefreshAfter - if len(auth.ModelStates) == 0 && len(existing.ModelStates) > 0 { - auth.ModelStates = existing.ModelStates + if !existing.Disabled && existing.Status != coreauth.StatusDisabled && !auth.Disabled && auth.Status != coreauth.StatusDisabled { + auth.LastRefreshedAt = existing.LastRefreshedAt + auth.NextRefreshAfter = existing.NextRefreshAfter + if len(auth.ModelStates) == 0 && len(existing.ModelStates) > 0 { + auth.ModelStates = existing.ModelStates + } } op = "update" _, err = s.coreManager.Update(ctx, auth) From 74b862d8b88dfde3dfe7d3a12f3d46eff1d927a1 Mon Sep 17 00:00:00 2001 From: DragonFSKY Date: Tue, 24 Mar 2026 00:21:04 +0800 Subject: [PATCH 02/21] test(cliproxy): cover delete re-add stale state flow --- sdk/cliproxy/service_stale_state_test.go | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 sdk/cliproxy/service_stale_state_test.go diff --git a/sdk/cliproxy/service_stale_state_test.go b/sdk/cliproxy/service_stale_state_test.go new file mode 100644 index 00000000..db5ce467 --- /dev/null +++ b/sdk/cliproxy/service_stale_state_test.go @@ -0,0 +1,85 @@ +package cliproxy + +import ( + "context" + "testing" + "time" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" +) + +func TestServiceApplyCoreAuthAddOrUpdate_DeleteReAddDoesNotInheritStaleRuntimeState(t *testing.T) { + service := &Service{ + cfg: &config.Config{}, + coreManager: coreauth.NewManager(nil, nil, nil), + } + + authID := "service-stale-state-auth" + modelID := "stale-model" + lastRefreshedAt := time.Date(2026, time.March, 1, 8, 0, 0, 0, time.UTC) + nextRefreshAfter := lastRefreshedAt.Add(30 * time.Minute) + + t.Cleanup(func() { + GlobalModelRegistry().UnregisterClient(authID) + }) + + service.applyCoreAuthAddOrUpdate(context.Background(), &coreauth.Auth{ + ID: authID, + Provider: "claude", + Status: coreauth.StatusActive, + LastRefreshedAt: lastRefreshedAt, + NextRefreshAfter: nextRefreshAfter, + ModelStates: map[string]*coreauth.ModelState{ + modelID: { + Quota: coreauth.QuotaState{BackoffLevel: 7}, + }, + }, + }) + + service.applyCoreAuthRemoval(context.Background(), authID) + + disabled, ok := service.coreManager.GetByID(authID) + if !ok || disabled == nil { + t.Fatalf("expected disabled auth after removal") + } + if !disabled.Disabled || disabled.Status != coreauth.StatusDisabled { + t.Fatalf("expected disabled auth after removal, got disabled=%v status=%v", disabled.Disabled, disabled.Status) + } + if disabled.LastRefreshedAt.IsZero() { + t.Fatalf("expected disabled auth to still carry prior LastRefreshedAt for regression setup") + } + if disabled.NextRefreshAfter.IsZero() { + t.Fatalf("expected disabled auth to still carry prior NextRefreshAfter for regression setup") + } + if len(disabled.ModelStates) == 0 { + t.Fatalf("expected disabled auth to still carry prior ModelStates for regression setup") + } + + service.applyCoreAuthAddOrUpdate(context.Background(), &coreauth.Auth{ + ID: authID, + Provider: "claude", + Status: coreauth.StatusActive, + }) + + updated, ok := service.coreManager.GetByID(authID) + if !ok || updated == nil { + t.Fatalf("expected re-added auth to be present") + } + if updated.Disabled { + t.Fatalf("expected re-added auth to be active") + } + if !updated.LastRefreshedAt.IsZero() { + t.Fatalf("expected LastRefreshedAt to reset on delete -> re-add, got %v", updated.LastRefreshedAt) + } + if !updated.NextRefreshAfter.IsZero() { + t.Fatalf("expected NextRefreshAfter to reset on delete -> re-add, got %v", updated.NextRefreshAfter) + } + if len(updated.ModelStates) != 0 { + t.Fatalf("expected ModelStates to reset on delete -> re-add, got %d entries", len(updated.ModelStates)) + } + if models := registry.GetGlobalRegistry().GetModelsForClient(authID); len(models) == 0 { + t.Fatalf("expected re-added auth to re-register models in global registry") + } +} From 7e1a543b79b21657536609b7eb14734df3197354 Mon Sep 17 00:00:00 2001 From: trph <894304504@qq.com> Date: Thu, 19 Mar 2026 15:16:05 +0800 Subject: [PATCH 03/21] fix: preserve separate streamed tool calls in Responses API --- .../openai_openai-responses_response.go | 265 +++++++++--------- .../openai_openai-responses_response_test.go | 127 +++++++++ 2 files changed, 261 insertions(+), 131 deletions(-) create mode 100644 internal/translator/openai/openai/responses/openai_openai-responses_response_test.go diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response.go b/internal/translator/openai/openai/responses/openai_openai-responses_response.go index c2ac608a..a34a6ff4 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "sort" "strings" "sync/atomic" "time" @@ -16,6 +17,7 @@ import ( type oaiToResponsesStateReasoning struct { ReasoningID string ReasoningData string + OutputIndex int } type oaiToResponsesState struct { Seq int @@ -29,16 +31,19 @@ type oaiToResponsesState struct { MsgTextBuf map[int]*strings.Builder ReasoningBuf strings.Builder Reasonings []oaiToResponsesStateReasoning - FuncArgsBuf map[int]*strings.Builder // index -> args - FuncNames map[int]string // index -> name - FuncCallIDs map[int]string // index -> call_id + FuncArgsBuf map[string]*strings.Builder + FuncNames map[string]string + FuncCallIDs map[string]string + FuncOutputIx map[string]int + MsgOutputIx map[int]int + NextOutputIx int // message item state per output index MsgItemAdded map[int]bool // whether response.output_item.added emitted for message MsgContentAdded map[int]bool // whether response.content_part.added emitted for message MsgItemDone map[int]bool // whether message done events were emitted // function item done state - FuncArgsDone map[int]bool - FuncItemDone map[int]bool + FuncArgsDone map[string]bool + FuncItemDone map[string]bool // usage aggregation PromptTokens int64 CachedTokens int64 @@ -60,15 +65,17 @@ func emitRespEvent(event string, payload []byte) []byte { func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &oaiToResponsesState{ - FuncArgsBuf: make(map[int]*strings.Builder), - FuncNames: make(map[int]string), - FuncCallIDs: make(map[int]string), + FuncArgsBuf: make(map[string]*strings.Builder), + FuncNames: make(map[string]string), + FuncCallIDs: make(map[string]string), + FuncOutputIx: make(map[string]int), + MsgOutputIx: make(map[int]int), MsgTextBuf: make(map[int]*strings.Builder), MsgItemAdded: make(map[int]bool), MsgContentAdded: make(map[int]bool), MsgItemDone: make(map[int]bool), - FuncArgsDone: make(map[int]bool), - FuncItemDone: make(map[int]bool), + FuncArgsDone: make(map[string]bool), + FuncItemDone: make(map[string]bool), Reasonings: make([]oaiToResponsesStateReasoning, 0), } } @@ -125,6 +132,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, } nextSeq := func() int { st.Seq++; return st.Seq } + allocOutputIndex := func() int { + ix := st.NextOutputIx + st.NextOutputIx++ + return ix + } + toolStateKey := func(outputIndex, toolIndex int) string { return fmt.Sprintf("%d:%d", outputIndex, toolIndex) } var out [][]byte if !st.Started { @@ -135,14 +148,17 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, st.ReasoningBuf.Reset() st.ReasoningID = "" st.ReasoningIndex = 0 - st.FuncArgsBuf = make(map[int]*strings.Builder) - st.FuncNames = make(map[int]string) - st.FuncCallIDs = make(map[int]string) + st.FuncArgsBuf = make(map[string]*strings.Builder) + st.FuncNames = make(map[string]string) + st.FuncCallIDs = make(map[string]string) + st.FuncOutputIx = make(map[string]int) + st.MsgOutputIx = make(map[int]int) + st.NextOutputIx = 0 st.MsgItemAdded = make(map[int]bool) st.MsgContentAdded = make(map[int]bool) st.MsgItemDone = make(map[int]bool) - st.FuncArgsDone = make(map[int]bool) - st.FuncItemDone = make(map[int]bool) + st.FuncArgsDone = make(map[string]bool) + st.FuncItemDone = make(map[string]bool) st.PromptTokens = 0 st.CachedTokens = 0 st.CompletionTokens = 0 @@ -185,7 +201,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, outputItemDone, _ = sjson.SetBytes(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.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text, OutputIndex: st.ReasoningIndex}) st.ReasoningID = "" } @@ -201,10 +217,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, stopReasoning(st.ReasoningBuf.String()) st.ReasoningBuf.Reset() } + if _, exists := st.MsgOutputIx[idx]; !exists { + st.MsgOutputIx[idx] = allocOutputIndex() + } + msgOutputIndex := st.MsgOutputIx[idx] if !st.MsgItemAdded[idx] { item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`) item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) - item, _ = sjson.SetBytes(item, "output_index", idx) + item, _ = sjson.SetBytes(item, "output_index", msgOutputIndex) item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) out = append(out, emitRespEvent("response.output_item.added", item)) st.MsgItemAdded[idx] = true @@ -213,7 +233,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, part := []byte(`{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) part, _ = sjson.SetBytes(part, "sequence_number", nextSeq()) part, _ = sjson.SetBytes(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - part, _ = sjson.SetBytes(part, "output_index", idx) + part, _ = sjson.SetBytes(part, "output_index", msgOutputIndex) part, _ = sjson.SetBytes(part, "content_index", 0) out = append(out, emitRespEvent("response.content_part.added", part)) st.MsgContentAdded[idx] = true @@ -222,7 +242,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, msg := []byte(`{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`) msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) msg, _ = sjson.SetBytes(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - msg, _ = sjson.SetBytes(msg, "output_index", idx) + msg, _ = sjson.SetBytes(msg, "output_index", msgOutputIndex) msg, _ = sjson.SetBytes(msg, "content_index", 0) msg, _ = sjson.SetBytes(msg, "delta", c.String()) out = append(out, emitRespEvent("response.output_text.delta", msg)) @@ -238,10 +258,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, // On first appearance, add reasoning item and part if st.ReasoningID == "" { st.ReasoningID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx) - st.ReasoningIndex = idx + st.ReasoningIndex = allocOutputIndex() item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}`) item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) - item, _ = sjson.SetBytes(item, "output_index", idx) + item, _ = sjson.SetBytes(item, "output_index", st.ReasoningIndex) item, _ = sjson.SetBytes(item, "item.id", st.ReasoningID) out = append(out, emitRespEvent("response.output_item.added", item)) part := []byte(`{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`) @@ -269,6 +289,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, // Before emitting any function events, if a message is open for this index, // close its text/content to match Codex expected ordering. if st.MsgItemAdded[idx] && !st.MsgItemDone[idx] { + msgOutputIndex := st.MsgOutputIx[idx] fullText := "" if b := st.MsgTextBuf[idx]; b != nil { fullText = b.String() @@ -276,7 +297,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`) done, _ = sjson.SetBytes(done, "sequence_number", nextSeq()) done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - done, _ = sjson.SetBytes(done, "output_index", idx) + done, _ = sjson.SetBytes(done, "output_index", msgOutputIndex) done, _ = sjson.SetBytes(done, "content_index", 0) done, _ = sjson.SetBytes(done, "text", fullText) out = append(out, emitRespEvent("response.output_text.done", done)) @@ -284,69 +305,72 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, partDone := []byte(`{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - partDone, _ = sjson.SetBytes(partDone, "output_index", idx) + partDone, _ = sjson.SetBytes(partDone, "output_index", msgOutputIndex) partDone, _ = sjson.SetBytes(partDone, "content_index", 0) partDone, _ = sjson.SetBytes(partDone, "part.text", fullText) out = append(out, emitRespEvent("response.content_part.done", partDone)) itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`) itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", msgOutputIndex) itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText) out = append(out, emitRespEvent("response.output_item.done", itemDone)) st.MsgItemDone[idx] = true } - // Only emit item.added once per tool call and preserve call_id across chunks. - newCallID := tcs.Get("0.id").String() - nameChunk := tcs.Get("0.function.name").String() - if nameChunk != "" { - st.FuncNames[idx] = nameChunk - } - existingCallID := st.FuncCallIDs[idx] - effectiveCallID := existingCallID - shouldEmitItem := false - if existingCallID == "" && newCallID != "" { - // First time seeing a valid call_id for this index - effectiveCallID = newCallID - st.FuncCallIDs[idx] = newCallID - shouldEmitItem = true - } - - if shouldEmitItem && effectiveCallID != "" { - o := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}`) - o, _ = sjson.SetBytes(o, "sequence_number", nextSeq()) - o, _ = sjson.SetBytes(o, "output_index", idx) - o, _ = sjson.SetBytes(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID)) - o, _ = sjson.SetBytes(o, "item.call_id", effectiveCallID) - name := st.FuncNames[idx] - o, _ = sjson.SetBytes(o, "item.name", name) - out = append(out, emitRespEvent("response.output_item.added", o)) - } - - // Ensure args buffer exists for this index - if st.FuncArgsBuf[idx] == nil { - st.FuncArgsBuf[idx] = &strings.Builder{} - } - - // Append arguments delta if available and we have a valid call_id to reference - if args := tcs.Get("0.function.arguments"); args.Exists() && args.String() != "" { - // Prefer an already known call_id; fall back to newCallID if first time - refCallID := st.FuncCallIDs[idx] - if refCallID == "" { - refCallID = newCallID + tcs.ForEach(func(_, tc gjson.Result) bool { + toolIndex := int(tc.Get("index").Int()) + key := toolStateKey(idx, toolIndex) + newCallID := tc.Get("id").String() + nameChunk := tc.Get("function.name").String() + if nameChunk != "" { + st.FuncNames[key] = nameChunk } - if refCallID != "" { - ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`) - ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq()) - ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", refCallID)) - ad, _ = sjson.SetBytes(ad, "output_index", idx) - ad, _ = sjson.SetBytes(ad, "delta", args.String()) - out = append(out, emitRespEvent("response.function_call_arguments.delta", ad)) + + existingCallID := st.FuncCallIDs[key] + effectiveCallID := existingCallID + shouldEmitItem := false + if existingCallID == "" && newCallID != "" { + effectiveCallID = newCallID + st.FuncCallIDs[key] = newCallID + st.FuncOutputIx[key] = allocOutputIndex() + shouldEmitItem = true } - st.FuncArgsBuf[idx].WriteString(args.String()) - } + + if shouldEmitItem && effectiveCallID != "" { + outputIndex := st.FuncOutputIx[key] + o := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}`) + o, _ = sjson.SetBytes(o, "sequence_number", nextSeq()) + o, _ = sjson.SetBytes(o, "output_index", outputIndex) + o, _ = sjson.SetBytes(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID)) + o, _ = sjson.SetBytes(o, "item.call_id", effectiveCallID) + o, _ = sjson.SetBytes(o, "item.name", st.FuncNames[key]) + out = append(out, emitRespEvent("response.output_item.added", o)) + } + + if st.FuncArgsBuf[key] == nil { + st.FuncArgsBuf[key] = &strings.Builder{} + } + + if args := tc.Get("function.arguments"); args.Exists() && args.String() != "" { + refCallID := st.FuncCallIDs[key] + if refCallID == "" { + refCallID = newCallID + } + if refCallID != "" { + outputIndex := st.FuncOutputIx[key] + ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`) + ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq()) + ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", refCallID)) + ad, _ = sjson.SetBytes(ad, "output_index", outputIndex) + ad, _ = sjson.SetBytes(ad, "delta", args.String()) + out = append(out, emitRespEvent("response.function_call_arguments.delta", ad)) + } + st.FuncArgsBuf[key].WriteString(args.String()) + } + return true + }) } } @@ -360,15 +384,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, for i := range st.MsgItemAdded { idxs = append(idxs, i) } - for i := 0; i < len(idxs); i++ { - for j := i + 1; j < len(idxs); j++ { - if idxs[j] < idxs[i] { - idxs[i], idxs[j] = idxs[j], idxs[i] - } - } - } + sort.Slice(idxs, func(i, j int) bool { return st.MsgOutputIx[idxs[i]] < st.MsgOutputIx[idxs[j]] }) for _, i := range idxs { if st.MsgItemAdded[i] && !st.MsgItemDone[i] { + msgOutputIndex := st.MsgOutputIx[i] fullText := "" if b := st.MsgTextBuf[i]; b != nil { fullText = b.String() @@ -376,7 +395,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`) done, _ = sjson.SetBytes(done, "sequence_number", nextSeq()) done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) - done, _ = sjson.SetBytes(done, "output_index", i) + done, _ = sjson.SetBytes(done, "output_index", msgOutputIndex) done, _ = sjson.SetBytes(done, "content_index", 0) done, _ = sjson.SetBytes(done, "text", fullText) out = append(out, emitRespEvent("response.output_text.done", done)) @@ -384,14 +403,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, partDone := []byte(`{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) - partDone, _ = sjson.SetBytes(partDone, "output_index", i) + partDone, _ = sjson.SetBytes(partDone, "output_index", msgOutputIndex) partDone, _ = sjson.SetBytes(partDone, "content_index", 0) partDone, _ = sjson.SetBytes(partDone, "part.text", fullText) out = append(out, emitRespEvent("response.content_part.done", partDone)) itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`) itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.SetBytes(itemDone, "output_index", i) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", msgOutputIndex) itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText) out = append(out, emitRespEvent("response.output_item.done", itemDone)) @@ -407,43 +426,42 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, // Emit function call done events for any active function calls if len(st.FuncCallIDs) > 0 { - idxs := make([]int, 0, len(st.FuncCallIDs)) - for i := range st.FuncCallIDs { - idxs = append(idxs, i) + keys := make([]string, 0, len(st.FuncCallIDs)) + for key := range st.FuncCallIDs { + keys = append(keys, key) } - for i := 0; i < len(idxs); i++ { - for j := i + 1; j < len(idxs); j++ { - if idxs[j] < idxs[i] { - idxs[i], idxs[j] = idxs[j], idxs[i] - } - } - } - for _, i := range idxs { - callID := st.FuncCallIDs[i] - if callID == "" || st.FuncItemDone[i] { + sort.Slice(keys, func(i, j int) bool { + left := st.FuncOutputIx[keys[i]] + right := st.FuncOutputIx[keys[j]] + return left < right || (left == right && keys[i] < keys[j]) + }) + for _, key := range keys { + callID := st.FuncCallIDs[key] + if callID == "" || st.FuncItemDone[key] { continue } + outputIndex := st.FuncOutputIx[key] args := "{}" - if b := st.FuncArgsBuf[i]; b != nil && b.Len() > 0 { + if b := st.FuncArgsBuf[key]; b != nil && b.Len() > 0 { args = b.String() } fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`) fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq()) fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", callID)) - fcDone, _ = sjson.SetBytes(fcDone, "output_index", i) + fcDone, _ = sjson.SetBytes(fcDone, "output_index", outputIndex) fcDone, _ = sjson.SetBytes(fcDone, "arguments", args) out = append(out, emitRespEvent("response.function_call_arguments.done", fcDone)) itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`) itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.SetBytes(itemDone, "output_index", i) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", outputIndex) itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", callID)) itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args) itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", callID) - itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[i]) + itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[key]) out = append(out, emitRespEvent("response.output_item.done", itemDone)) - st.FuncItemDone[i] = true - st.FuncArgsDone[i] = true + st.FuncItemDone[key] = true + st.FuncArgsDone[key] = true } } completed := []byte(`{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`) @@ -516,28 +534,21 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, } // Build response.output using aggregated buffers outputsWrapper := []byte(`{"arr":[]}`) + type completedOutputItem struct { + index int + raw []byte + } + outputItems := make([]completedOutputItem, 0, len(st.Reasonings)+len(st.MsgItemAdded)+len(st.FuncArgsBuf)) if len(st.Reasonings) > 0 { for _, r := range st.Reasonings { item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`) item, _ = sjson.SetBytes(item, "id", r.ReasoningID) item, _ = sjson.SetBytes(item, "summary.0.text", r.ReasoningData) - outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) + outputItems = append(outputItems, completedOutputItem{index: r.OutputIndex, raw: item}) } } - // Append message items in ascending index order if len(st.MsgItemAdded) > 0 { - midxs := make([]int, 0, len(st.MsgItemAdded)) for i := range st.MsgItemAdded { - midxs = append(midxs, i) - } - for i := 0; i < len(midxs); i++ { - for j := i + 1; j < len(midxs); j++ { - if midxs[j] < midxs[i] { - midxs[i], midxs[j] = midxs[j], midxs[i] - } - } - } - for _, i := range midxs { txt := "" if b := st.MsgTextBuf[i]; b != nil { txt = b.String() @@ -545,37 +556,29 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`) item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) item, _ = sjson.SetBytes(item, "content.0.text", txt) - outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) + outputItems = append(outputItems, completedOutputItem{index: st.MsgOutputIx[i], raw: item}) } } if len(st.FuncArgsBuf) > 0 { - idxs := make([]int, 0, len(st.FuncArgsBuf)) - for i := range st.FuncArgsBuf { - idxs = append(idxs, i) - } - // small-N sort without extra imports - for i := 0; i < len(idxs); i++ { - for j := i + 1; j < len(idxs); j++ { - if idxs[j] < idxs[i] { - idxs[i], idxs[j] = idxs[j], idxs[i] - } - } - } - for _, i := range idxs { + for key := range st.FuncArgsBuf { args := "" - if b := st.FuncArgsBuf[i]; b != nil { + if b := st.FuncArgsBuf[key]; b != nil { args = b.String() } - callID := st.FuncCallIDs[i] - name := st.FuncNames[i] + callID := st.FuncCallIDs[key] + name := st.FuncNames[key] item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID)) item, _ = sjson.SetBytes(item, "arguments", args) item, _ = sjson.SetBytes(item, "call_id", callID) item, _ = sjson.SetBytes(item, "name", name) - outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) + outputItems = append(outputItems, completedOutputItem{index: st.FuncOutputIx[key], raw: item}) } } + sort.Slice(outputItems, func(i, j int) bool { return outputItems[i].index < outputItems[j].index }) + for _, item := range outputItems { + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item.raw) + } if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 { completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw)) } diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go b/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go new file mode 100644 index 00000000..164acbca --- /dev/null +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go @@ -0,0 +1,127 @@ +package responses + +import ( + "context" + "strings" + "testing" + + "github.com/tidwall/gjson" +) + +func parseOpenAIResponsesSSEEvent(t *testing.T, chunk []byte) (string, gjson.Result) { + t.Helper() + + lines := strings.Split(string(chunk), "\n") + if len(lines) < 2 { + t.Fatalf("unexpected SSE chunk: %q", chunk) + } + + event := strings.TrimSpace(strings.TrimPrefix(lines[0], "event:")) + dataLine := strings.TrimSpace(strings.TrimPrefix(lines[1], "data:")) + if !gjson.Valid(dataLine) { + t.Fatalf("invalid SSE data JSON: %q", dataLine) + } + return event, gjson.Parse(dataLine) +} + +func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_MultipleToolCallsRemainSeparate(t *testing.T) { + in := []string{ + `data: {"id":"resp_test","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":0,"id":"call_read","type":"function","function":{"name":"read","arguments":""}}]},"finish_reason":null}]}`, + `data: {"id":"resp_test","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":[{"index":0,"function":{"arguments":"{\"filePath\":\"C:\\\\repo\",\"limit\":400,\"offset\":1}"}}]},"finish_reason":null}]}`, + `data: {"id":"resp_test","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":1,"id":"call_glob","type":"function","function":{"name":"glob","arguments":""}}]},"finish_reason":null}]}`, + `data: {"id":"resp_test","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":[{"index":1,"function":{"arguments":"{\"path\":\"C:\\\\repo\",\"pattern\":\"*.{yml,yaml}\"}"}}]},"finish_reason":null}]}`, + `data: {"id":"resp_test","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":"tool_calls"}],"usage":{"completion_tokens":10,"total_tokens":20,"prompt_tokens":10}}`, + } + + request := []byte(`{"model":"gpt-5.4","tool_choice":"auto","parallel_tool_calls":true}`) + + var param any + var out [][]byte + for _, line := range in { + out = append(out, ConvertOpenAIChatCompletionsResponseToOpenAIResponses(context.Background(), "model", request, request, []byte(line), ¶m)...) + } + + addedNames := map[string]string{} + doneArgs := map[string]string{} + doneNames := map[string]string{} + outputItems := map[string]gjson.Result{} + + for _, chunk := range out { + ev, data := parseOpenAIResponsesSSEEvent(t, chunk) + switch ev { + case "response.output_item.added": + if data.Get("item.type").String() != "function_call" { + continue + } + addedNames[data.Get("item.call_id").String()] = data.Get("item.name").String() + case "response.output_item.done": + if data.Get("item.type").String() != "function_call" { + continue + } + callID := data.Get("item.call_id").String() + doneArgs[callID] = data.Get("item.arguments").String() + doneNames[callID] = data.Get("item.name").String() + case "response.completed": + output := data.Get("response.output") + for _, item := range output.Array() { + if item.Get("type").String() == "function_call" { + outputItems[item.Get("call_id").String()] = item + } + } + } + } + + if len(addedNames) != 2 { + t.Fatalf("expected 2 function_call added events, got %d", len(addedNames)) + } + if len(doneArgs) != 2 { + t.Fatalf("expected 2 function_call done events, got %d", len(doneArgs)) + } + + if addedNames["call_read"] != "read" { + t.Fatalf("unexpected added name for call_read: %q", addedNames["call_read"]) + } + if addedNames["call_glob"] != "glob" { + t.Fatalf("unexpected added name for call_glob: %q", addedNames["call_glob"]) + } + + if !gjson.Valid(doneArgs["call_read"]) { + t.Fatalf("invalid JSON args for call_read: %q", doneArgs["call_read"]) + } + if !gjson.Valid(doneArgs["call_glob"]) { + t.Fatalf("invalid JSON args for call_glob: %q", doneArgs["call_glob"]) + } + if strings.Contains(doneArgs["call_read"], "}{") { + t.Fatalf("call_read args were concatenated: %q", doneArgs["call_read"]) + } + if strings.Contains(doneArgs["call_glob"], "}{") { + t.Fatalf("call_glob args were concatenated: %q", doneArgs["call_glob"]) + } + + if doneNames["call_read"] != "read" { + t.Fatalf("unexpected done name for call_read: %q", doneNames["call_read"]) + } + if doneNames["call_glob"] != "glob" { + t.Fatalf("unexpected done name for call_glob: %q", doneNames["call_glob"]) + } + + if got := gjson.Get(doneArgs["call_read"], "filePath").String(); got != `C:\repo` { + t.Fatalf("unexpected filePath for call_read: %q", got) + } + if got := gjson.Get(doneArgs["call_glob"], "path").String(); got != `C:\repo` { + t.Fatalf("unexpected path for call_glob: %q", got) + } + if got := gjson.Get(doneArgs["call_glob"], "pattern").String(); got != "*.{yml,yaml}" { + t.Fatalf("unexpected pattern for call_glob: %q", got) + } + + if len(outputItems) != 2 { + t.Fatalf("expected 2 function_call items in response.output, got %d", len(outputItems)) + } + if outputItems["call_read"].Get("name").String() != "read" { + t.Fatalf("unexpected response.output name for call_read: %q", outputItems["call_read"].Get("name").String()) + } + if outputItems["call_glob"].Get("name").String() != "glob" { + t.Fatalf("unexpected response.output name for call_glob: %q", outputItems["call_glob"].Get("name").String()) + } +} From fbff68b9e00467e3228c36b75c3d9050e5b4a013 Mon Sep 17 00:00:00 2001 From: trph <894304504@qq.com> Date: Thu, 19 Mar 2026 15:47:45 +0800 Subject: [PATCH 04/21] fix: preserve choice-aware output indexes for streamed tool calls --- .../openai_openai-responses_response_test.go | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go b/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go index 164acbca..81da82da 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go @@ -125,3 +125,86 @@ func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_MultipleToolCalls t.Fatalf("unexpected response.output name for call_glob: %q", outputItems["call_glob"].Get("name").String()) } } + +func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_MultiChoiceToolCallsUseDistinctOutputIndexes(t *testing.T) { + in := []string{ + `data: {"id":"resp_multi_choice","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":0,"id":"call_choice0","type":"function","function":{"name":"glob","arguments":""}}]},"finish_reason":null},{"index":1,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":0,"id":"call_choice1","type":"function","function":{"name":"read","arguments":""}}]},"finish_reason":null}]}`, + `data: {"id":"resp_multi_choice","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":[{"index":0,"function":{"arguments":"{\"path\":\"C:\\\\repo\",\"pattern\":\"*.go\"}"}}]},"finish_reason":null},{"index":1,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":[{"index":0,"function":{"arguments":"{\"filePath\":\"C:\\\\repo\\\\README.md\",\"limit\":20,\"offset\":1}"}}]},"finish_reason":null}]}`, + `data: {"id":"resp_multi_choice","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":"tool_calls"},{"index":1,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":"tool_calls"}],"usage":{"completion_tokens":10,"total_tokens":20,"prompt_tokens":10}}`, + } + + request := []byte(`{"model":"gpt-5.4","tool_choice":"auto","parallel_tool_calls":true}`) + + var param any + var out [][]byte + for _, line := range in { + out = append(out, ConvertOpenAIChatCompletionsResponseToOpenAIResponses(context.Background(), "model", request, request, []byte(line), ¶m)...) + } + + type fcEvent struct { + outputIndex int64 + name string + arguments string + } + + added := map[string]fcEvent{} + done := map[string]fcEvent{} + + for _, chunk := range out { + ev, data := parseOpenAIResponsesSSEEvent(t, chunk) + switch ev { + case "response.output_item.added": + if data.Get("item.type").String() != "function_call" { + continue + } + callID := data.Get("item.call_id").String() + added[callID] = fcEvent{ + outputIndex: data.Get("output_index").Int(), + name: data.Get("item.name").String(), + } + case "response.output_item.done": + if data.Get("item.type").String() != "function_call" { + continue + } + callID := data.Get("item.call_id").String() + done[callID] = fcEvent{ + outputIndex: data.Get("output_index").Int(), + name: data.Get("item.name").String(), + arguments: data.Get("item.arguments").String(), + } + } + } + + if len(added) != 2 { + t.Fatalf("expected 2 function_call added events, got %d", len(added)) + } + if len(done) != 2 { + t.Fatalf("expected 2 function_call done events, got %d", len(done)) + } + + if added["call_choice0"].name != "glob" { + t.Fatalf("unexpected added name for call_choice0: %q", added["call_choice0"].name) + } + if added["call_choice1"].name != "read" { + t.Fatalf("unexpected added name for call_choice1: %q", added["call_choice1"].name) + } + if added["call_choice0"].outputIndex == added["call_choice1"].outputIndex { + t.Fatalf("expected distinct output indexes for different choices, both got %d", added["call_choice0"].outputIndex) + } + + if !gjson.Valid(done["call_choice0"].arguments) { + t.Fatalf("invalid JSON args for call_choice0: %q", done["call_choice0"].arguments) + } + if !gjson.Valid(done["call_choice1"].arguments) { + t.Fatalf("invalid JSON args for call_choice1: %q", done["call_choice1"].arguments) + } + if done["call_choice0"].outputIndex == done["call_choice1"].outputIndex { + t.Fatalf("expected distinct done output indexes for different choices, both got %d", done["call_choice0"].outputIndex) + } + if done["call_choice0"].name != "glob" { + t.Fatalf("unexpected done name for call_choice0: %q", done["call_choice0"].name) + } + if done["call_choice1"].name != "read" { + t.Fatalf("unexpected done name for call_choice1: %q", done["call_choice1"].name) + } +} From cc32f5ff618132463c2795911c7eac95e6dbfe14 Mon Sep 17 00:00:00 2001 From: trph <894304504@qq.com> Date: Thu, 19 Mar 2026 16:11:19 +0800 Subject: [PATCH 05/21] fix: unify Responses output indexes for streamed items --- .../openai_openai-responses_response_test.go | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go b/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go index 81da82da..9f3ed3f4 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go @@ -208,3 +208,98 @@ func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_MultiChoiceToolCa t.Fatalf("unexpected done name for call_choice1: %q", done["call_choice1"].name) } } + +func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_MixedMessageAndToolUseDistinctOutputIndexes(t *testing.T) { + in := []string{ + `data: {"id":"resp_mixed","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":"hello","reasoning_content":null,"tool_calls":null},"finish_reason":null},{"index":1,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":0,"id":"call_choice1","type":"function","function":{"name":"read","arguments":""}}]},"finish_reason":null}]}`, + `data: {"id":"resp_mixed","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":"stop"},{"index":1,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":[{"index":0,"function":{"arguments":"{\"filePath\":\"C:\\\\repo\\\\README.md\",\"limit\":20,\"offset\":1}"}}]},"finish_reason":"tool_calls"}],"usage":{"completion_tokens":10,"total_tokens":20,"prompt_tokens":10}}`, + } + + request := []byte(`{"model":"gpt-5.4","tool_choice":"auto","parallel_tool_calls":true}`) + + var param any + var out [][]byte + for _, line := range in { + out = append(out, ConvertOpenAIChatCompletionsResponseToOpenAIResponses(context.Background(), "model", request, request, []byte(line), ¶m)...) + } + + var messageOutputIndex int64 = -1 + var toolOutputIndex int64 = -1 + + for _, chunk := range out { + ev, data := parseOpenAIResponsesSSEEvent(t, chunk) + if ev != "response.output_item.added" { + continue + } + switch data.Get("item.type").String() { + case "message": + if data.Get("item.id").String() == "msg_resp_mixed_0" { + messageOutputIndex = data.Get("output_index").Int() + } + case "function_call": + if data.Get("item.call_id").String() == "call_choice1" { + toolOutputIndex = data.Get("output_index").Int() + } + } + } + + if messageOutputIndex < 0 { + t.Fatal("did not find message output index") + } + if toolOutputIndex < 0 { + t.Fatal("did not find tool output index") + } + if messageOutputIndex == toolOutputIndex { + t.Fatalf("expected distinct output indexes for message and tool call, both got %d", messageOutputIndex) + } +} + +func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_FunctionCallDoneAndCompletedOutputStayAscending(t *testing.T) { + in := []string{ + `data: {"id":"resp_order","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":0,"id":"call_glob","type":"function","function":{"name":"glob","arguments":""}}]},"finish_reason":null}]}`, + `data: {"id":"resp_order","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":[{"index":0,"function":{"arguments":"{\"path\":\"C:\\\\repo\",\"pattern\":\"*.go\"}"}}]},"finish_reason":null}]}`, + `data: {"id":"resp_order","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":1,"id":"call_read","type":"function","function":{"name":"read","arguments":""}}]},"finish_reason":null}]}`, + `data: {"id":"resp_order","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":[{"index":1,"function":{"arguments":"{\"filePath\":\"C:\\\\repo\\\\README.md\",\"limit\":20,\"offset\":1}"}}]},"finish_reason":null}]}`, + `data: {"id":"resp_order","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":"tool_calls"}],"usage":{"completion_tokens":10,"total_tokens":20,"prompt_tokens":10}}`, + } + + request := []byte(`{"model":"gpt-5.4","tool_choice":"auto","parallel_tool_calls":true}`) + + var param any + var out [][]byte + for _, line := range in { + out = append(out, ConvertOpenAIChatCompletionsResponseToOpenAIResponses(context.Background(), "model", request, request, []byte(line), ¶m)...) + } + + var doneIndexes []int64 + var completedOrder []string + + for _, chunk := range out { + ev, data := parseOpenAIResponsesSSEEvent(t, chunk) + switch ev { + case "response.output_item.done": + if data.Get("item.type").String() == "function_call" { + doneIndexes = append(doneIndexes, data.Get("output_index").Int()) + } + case "response.completed": + for _, item := range data.Get("response.output").Array() { + if item.Get("type").String() == "function_call" { + completedOrder = append(completedOrder, item.Get("call_id").String()) + } + } + } + } + + if len(doneIndexes) != 2 { + t.Fatalf("expected 2 function_call done indexes, got %d", len(doneIndexes)) + } + if doneIndexes[0] >= doneIndexes[1] { + t.Fatalf("expected ascending done output indexes, got %v", doneIndexes) + } + if len(completedOrder) != 2 { + t.Fatalf("expected 2 function_call items in completed output, got %d", len(completedOrder)) + } + if completedOrder[0] != "call_glob" || completedOrder[1] != "call_read" { + t.Fatalf("unexpected completed function_call order: %v", completedOrder) + } +} From c89d19b300140344f2ec6727296a30a672089c45 Mon Sep 17 00:00:00 2001 From: kwz Date: Wed, 25 Mar 2026 15:33:09 +0800 Subject: [PATCH 06/21] Preserve default transport settings for proxy clients --- sdk/proxyutil/proxy.go | 32 ++++++++++++++---------- sdk/proxyutil/proxy_test.go | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/sdk/proxyutil/proxy.go b/sdk/proxyutil/proxy.go index 591ec9d9..029efeb7 100644 --- a/sdk/proxyutil/proxy.go +++ b/sdk/proxyutil/proxy.go @@ -68,14 +68,18 @@ func Parse(raw string) (Setting, error) { } } +func cloneDefaultTransport() *http.Transport { + if transport, ok := http.DefaultTransport.(*http.Transport); ok && transport != nil { + return transport.Clone() + } + return &http.Transport{} +} + // NewDirectTransport returns a transport that bypasses environment proxies. func NewDirectTransport() *http.Transport { - if transport, ok := http.DefaultTransport.(*http.Transport); ok && transport != nil { - clone := transport.Clone() - clone.Proxy = nil - return clone - } - return &http.Transport{Proxy: nil} + clone := cloneDefaultTransport() + clone.Proxy = nil + return clone } // BuildHTTPTransport constructs an HTTP transport for the provided proxy setting. @@ -102,14 +106,16 @@ func BuildHTTPTransport(raw string) (*http.Transport, Mode, error) { if errSOCKS5 != nil { return nil, setting.Mode, fmt.Errorf("create SOCKS5 dialer failed: %w", errSOCKS5) } - return &http.Transport{ - Proxy: nil, - DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { - return dialer.Dial(network, addr) - }, - }, setting.Mode, nil + transport := cloneDefaultTransport() + transport.Proxy = nil + transport.DialContext = func(_ context.Context, network, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + } + return transport, setting.Mode, nil } - return &http.Transport{Proxy: http.ProxyURL(setting.URL)}, setting.Mode, nil + transport := cloneDefaultTransport() + transport.Proxy = http.ProxyURL(setting.URL) + return transport, setting.Mode, nil default: return nil, setting.Mode, nil } diff --git a/sdk/proxyutil/proxy_test.go b/sdk/proxyutil/proxy_test.go index bea413dc..5b250117 100644 --- a/sdk/proxyutil/proxy_test.go +++ b/sdk/proxyutil/proxy_test.go @@ -5,6 +5,16 @@ import ( "testing" ) +func mustDefaultTransport(t *testing.T) *http.Transport { + t.Helper() + + transport, ok := http.DefaultTransport.(*http.Transport) + if !ok || transport == nil { + t.Fatal("http.DefaultTransport is not an *http.Transport") + } + return transport +} + func TestParse(t *testing.T) { t.Parallel() @@ -86,4 +96,44 @@ func TestBuildHTTPTransportHTTPProxy(t *testing.T) { if proxyURL == nil || proxyURL.String() != "http://proxy.example.com:8080" { t.Fatalf("proxy URL = %v, want http://proxy.example.com:8080", proxyURL) } + + defaultTransport := mustDefaultTransport(t) + if transport.ForceAttemptHTTP2 != defaultTransport.ForceAttemptHTTP2 { + t.Fatalf("ForceAttemptHTTP2 = %v, want %v", transport.ForceAttemptHTTP2, defaultTransport.ForceAttemptHTTP2) + } + if transport.IdleConnTimeout != defaultTransport.IdleConnTimeout { + t.Fatalf("IdleConnTimeout = %v, want %v", transport.IdleConnTimeout, defaultTransport.IdleConnTimeout) + } + if transport.TLSHandshakeTimeout != defaultTransport.TLSHandshakeTimeout { + t.Fatalf("TLSHandshakeTimeout = %v, want %v", transport.TLSHandshakeTimeout, defaultTransport.TLSHandshakeTimeout) + } +} + +func TestBuildHTTPTransportSOCKS5ProxyInheritsDefaultTransportSettings(t *testing.T) { + t.Parallel() + + transport, mode, errBuild := BuildHTTPTransport("socks5://proxy.example.com:1080") + if errBuild != nil { + t.Fatalf("BuildHTTPTransport returned error: %v", errBuild) + } + if mode != ModeProxy { + t.Fatalf("mode = %d, want %d", mode, ModeProxy) + } + if transport == nil { + t.Fatal("expected transport, got nil") + } + if transport.Proxy != nil { + t.Fatal("expected SOCKS5 transport to bypass http proxy function") + } + + defaultTransport := mustDefaultTransport(t) + if transport.ForceAttemptHTTP2 != defaultTransport.ForceAttemptHTTP2 { + t.Fatalf("ForceAttemptHTTP2 = %v, want %v", transport.ForceAttemptHTTP2, defaultTransport.ForceAttemptHTTP2) + } + if transport.IdleConnTimeout != defaultTransport.IdleConnTimeout { + t.Fatalf("IdleConnTimeout = %v, want %v", transport.IdleConnTimeout, defaultTransport.IdleConnTimeout) + } + if transport.TLSHandshakeTimeout != defaultTransport.TLSHandshakeTimeout { + t.Fatalf("TLSHandshakeTimeout = %v, want %v", transport.TLSHandshakeTimeout, defaultTransport.TLSHandshakeTimeout) + } } From 36973d4a6f5debc93e768a00b6741a66fadacf8d Mon Sep 17 00:00:00 2001 From: pjpj Date: Wed, 25 Mar 2026 23:25:31 +0800 Subject: [PATCH 07/21] Handle Codex capacity errors as retryable --- internal/runtime/executor/codex_executor.go | 30 +++++++++++++++++-- .../executor/codex_executor_retry_test.go | 13 ++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 7e4163b8..1c3a916a 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -685,13 +685,39 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s } func newCodexStatusErr(statusCode int, body []byte) statusErr { - err := statusErr{code: statusCode, msg: string(body)} - if retryAfter := parseCodexRetryAfter(statusCode, body, time.Now()); retryAfter != nil { + errCode := statusCode + if isCodexModelCapacityError(body) { + errCode = http.StatusTooManyRequests + } + err := statusErr{code: errCode, msg: string(body)} + if retryAfter := parseCodexRetryAfter(errCode, body, time.Now()); retryAfter != nil { err.retryAfter = retryAfter } return err } +func isCodexModelCapacityError(errorBody []byte) bool { + if len(errorBody) == 0 { + return false + } + candidates := []string{ + gjson.GetBytes(errorBody, "error.message").String(), + gjson.GetBytes(errorBody, "message").String(), + string(errorBody), + } + for _, candidate := range candidates { + lower := strings.ToLower(strings.TrimSpace(candidate)) + if lower == "" { + continue + } + if strings.Contains(lower, "selected model is at capacity") || + strings.Contains(lower, "model is at capacity. please try a different model") { + return true + } + } + return false +} + func parseCodexRetryAfter(statusCode int, errorBody []byte, now time.Time) *time.Duration { if statusCode != http.StatusTooManyRequests || len(errorBody) == 0 { return nil diff --git a/internal/runtime/executor/codex_executor_retry_test.go b/internal/runtime/executor/codex_executor_retry_test.go index 3e54ae7c..249d40d6 100644 --- a/internal/runtime/executor/codex_executor_retry_test.go +++ b/internal/runtime/executor/codex_executor_retry_test.go @@ -60,6 +60,19 @@ func TestParseCodexRetryAfter(t *testing.T) { }) } +func TestNewCodexStatusErrTreatsCapacityAsRetryableRateLimit(t *testing.T) { + body := []byte(`{"error":{"message":"Selected model is at capacity. Please try a different model."}}`) + + err := newCodexStatusErr(http.StatusBadRequest, body) + + if got := err.StatusCode(); got != http.StatusTooManyRequests { + t.Fatalf("status code = %d, want %d", got, http.StatusTooManyRequests) + } + if err.RetryAfter() != nil { + t.Fatalf("expected nil explicit retryAfter for capacity fallback, got %v", *err.RetryAfter()) + } +} + func itoa(v int64) string { return strconv.FormatInt(v, 10) } From 754f3bcbc32a92f7639837208e33fe8e5247df7c Mon Sep 17 00:00:00 2001 From: edlsh Date: Wed, 25 Mar 2026 11:58:36 -0400 Subject: [PATCH 08/21] fix(codex): strip stream_options from Responses API requests The Codex/OpenAI Responses API does not support the stream_options parameter. When clients (e.g. Amp CLI) include stream_options in their requests, CLIProxyAPI forwards it as-is, causing a 400 error: {"detail":"Unsupported parameter: stream_options"} Strip stream_options alongside the other unsupported parameters (previous_response_id, prompt_cache_retention, safety_identifier) in Execute, ExecuteStream, and CountTokens. --- internal/runtime/executor/codex_executor.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 7e4163b8..33277687 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -113,6 +113,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re body, _ = sjson.DeleteBytes(body, "previous_response_id") body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") + body, _ = sjson.DeleteBytes(body, "stream_options") if !gjson.GetBytes(body, "instructions").Exists() { body, _ = sjson.SetBytes(body, "instructions", "") } @@ -311,6 +312,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au body, _ = sjson.DeleteBytes(body, "previous_response_id") body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") + body, _ = sjson.DeleteBytes(body, "stream_options") body, _ = sjson.SetBytes(body, "model", baseModel) if !gjson.GetBytes(body, "instructions").Exists() { body, _ = sjson.SetBytes(body, "instructions", "") @@ -415,6 +417,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth body, _ = sjson.DeleteBytes(body, "previous_response_id") body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") + body, _ = sjson.DeleteBytes(body, "stream_options") body, _ = sjson.SetBytes(body, "stream", false) if !gjson.GetBytes(body, "instructions").Exists() { body, _ = sjson.SetBytes(body, "instructions", "") From d54de441d3935326a0f8cd41da4f1f07e5433d3a Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Fri, 27 Mar 2026 10:53:09 +0100 Subject: [PATCH 09/21] docs: clarify provider-specific routing for aliased models --- README.md | 8 ++++++++ README_CN.md | 8 ++++++++ README_JA.md | 8 ++++++++ config.example.yaml | 3 +++ 4 files changed, 27 insertions(+) diff --git a/README.md b/README.md index 25e0090e..ab427e6c 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,14 @@ CLIProxyAPI includes integrated support for [Amp CLI](https://ampcode.com) and A - **Model mapping** to route unavailable models to alternatives (e.g., `claude-opus-4.5` → `claude-sonnet-4`) - Security-first design with localhost-only management endpoints +When routing needs to stay deterministic, prefer the provider-specific paths over the merged OpenAI-compatible `/v1/...` endpoints: + +- Use `/api/provider/anthropic/v1/messages` to force the Anthropic executor. +- Use `/api/provider/google/v1beta/models/...` to force the Gemini/Google executor. +- Use `/api/provider/openai/v1/chat/completions` for OpenAI-compatible executors. + +This matters when `oauth-model-alias`, alias pools, or fallback mappings reuse the same client-visible model name across multiple backends. In those cases, `/v1/models` may show the merged alias view instead of the executor you intend to hit. Treat the provider-specific request path as the source of truth for backend selection. + **→ [Complete Amp CLI Integration Guide](https://help.router-for.me/agent-client/amp-cli.html)** ## SDK Docs diff --git a/README_CN.md b/README_CN.md index 6301e403..a5675499 100644 --- a/README_CN.md +++ b/README_CN.md @@ -73,6 +73,14 @@ CLIProxyAPI 已内置对 [Amp CLI](https://ampcode.com) 和 Amp IDE 扩展的支 - 智能模型回退与自动路由 - 以安全为先的设计,管理端点仅限 localhost +当你需要确定地命中某个后端执行器时,优先使用 provider-specific 路径,而不是合并后的 OpenAI 兼容 `/v1/...` 端点: + +- 使用 `/api/provider/anthropic/v1/messages` 强制走 Anthropic 执行器。 +- 使用 `/api/provider/google/v1beta/models/...` 强制走 Gemini/Google 执行器。 +- 使用 `/api/provider/openai/v1/chat/completions` 强制走 OpenAI 兼容执行器。 + +这一点在 `oauth-model-alias`、alias 池或 model fallback 让多个后端复用同一个客户端可见模型名时尤其重要。此时 `/v1/models` 可能展示的是合并后的别名视图,而不是你真正想命中的执行器。对于后端选择,请以 provider-specific 的请求路径为准。 + **→ [Amp CLI 完整集成指南](https://help.router-for.me/cn/agent-client/amp-cli.html)** ## SDK 文档 diff --git a/README_JA.md b/README_JA.md index 4dbd36bb..036fdcc2 100644 --- a/README_JA.md +++ b/README_JA.md @@ -74,6 +74,14 @@ CLIProxyAPIは[Amp CLI](https://ampcode.com)およびAmp IDE拡張機能の統 - 利用できないモデルを代替モデルにルーティングする**モデルマッピング**(例:`claude-opus-4.5` → `claude-sonnet-4`) - localhostのみの管理エンドポイントによるセキュリティファーストの設計 +どのバックエンド実行系に送るかを確定させたい場合は、統合された OpenAI 互換 `/v1/...` エンドポイントよりも provider-specific のパスを優先してください。 + +- Anthropic 実行系を強制したい場合は `/api/provider/anthropic/v1/messages` +- Gemini/Google 実行系を強制したい場合は `/api/provider/google/v1beta/models/...` +- OpenAI 互換実行系を強制したい場合は `/api/provider/openai/v1/chat/completions` + +これは `oauth-model-alias`、alias プール、model fallback によって複数のバックエンドが同じクライアント向けモデル名を共有する場合に特に重要です。その場合、`/v1/models` は実際に狙っている実行系ではなく、統合後のエイリアス表示を返すことがあります。バックエンド選択の基準としては provider-specific のリクエストパスを使ってください。 + **→ [Amp CLI統合ガイドの完全版](https://help.router-for.me/agent-client/amp-cli.html)** ## SDKドキュメント diff --git a/config.example.yaml b/config.example.yaml index 42867ecb..0493c544 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -281,6 +281,9 @@ nonstream-keepalive-interval: 0 # These aliases rename model IDs for both model listing and request routing. # Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow, kimi. # NOTE: Aliases do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode. +# NOTE: Because aliases affect the merged /v1 model list and merged request routing, overlapping +# client-visible names can become ambiguous across providers. When you need deterministic backend +# selection, prefer /api/provider/{provider}/... instead of the generic /v1/... endpoints. # You can repeat the same name with different aliases to expose multiple client model names. # oauth-model-alias: # gemini-cli: From 224f0de35348b4567c81984caf17b1851e2211ab Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Fri, 27 Mar 2026 11:11:06 +0100 Subject: [PATCH 10/21] docs: neutralize provider-specific path wording --- README.md | 8 ++++---- README_CN.md | 8 ++++---- README_JA.md | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ab427e6c..de8b4c03 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,11 @@ CLIProxyAPI includes integrated support for [Amp CLI](https://ampcode.com) and A - **Model mapping** to route unavailable models to alternatives (e.g., `claude-opus-4.5` → `claude-sonnet-4`) - Security-first design with localhost-only management endpoints -When routing needs to stay deterministic, prefer the provider-specific paths over the merged OpenAI-compatible `/v1/...` endpoints: +When routing needs to stay deterministic, prefer the provider-specific paths over the merged `/v1/...` endpoints: -- Use `/api/provider/anthropic/v1/messages` to force the Anthropic executor. -- Use `/api/provider/google/v1beta/models/...` to force the Gemini/Google executor. -- Use `/api/provider/openai/v1/chat/completions` for OpenAI-compatible executors. +- Use `/api/provider/{provider}/v1/messages` for messages-style backends. +- Use `/api/provider/{provider}/v1beta/models/...` for model-scoped generate endpoints. +- Use `/api/provider/{provider}/v1/chat/completions` for chat-completions backends. This matters when `oauth-model-alias`, alias pools, or fallback mappings reuse the same client-visible model name across multiple backends. In those cases, `/v1/models` may show the merged alias view instead of the executor you intend to hit. Treat the provider-specific request path as the source of truth for backend selection. diff --git a/README_CN.md b/README_CN.md index a5675499..463c7ad1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -73,11 +73,11 @@ CLIProxyAPI 已内置对 [Amp CLI](https://ampcode.com) 和 Amp IDE 扩展的支 - 智能模型回退与自动路由 - 以安全为先的设计,管理端点仅限 localhost -当你需要确定地命中某个后端执行器时,优先使用 provider-specific 路径,而不是合并后的 OpenAI 兼容 `/v1/...` 端点: +当你需要确定地命中某个后端执行器时,优先使用 provider-specific 路径,而不是合并后的 `/v1/...` 端点: -- 使用 `/api/provider/anthropic/v1/messages` 强制走 Anthropic 执行器。 -- 使用 `/api/provider/google/v1beta/models/...` 强制走 Gemini/Google 执行器。 -- 使用 `/api/provider/openai/v1/chat/completions` 强制走 OpenAI 兼容执行器。 +- 对于 messages 风格的后端,使用 `/api/provider/{provider}/v1/messages`。 +- 对于按模型路径暴露生成接口的后端,使用 `/api/provider/{provider}/v1beta/models/...`。 +- 对于 chat-completions 风格的后端,使用 `/api/provider/{provider}/v1/chat/completions`。 这一点在 `oauth-model-alias`、alias 池或 model fallback 让多个后端复用同一个客户端可见模型名时尤其重要。此时 `/v1/models` 可能展示的是合并后的别名视图,而不是你真正想命中的执行器。对于后端选择,请以 provider-specific 的请求路径为准。 diff --git a/README_JA.md b/README_JA.md index 036fdcc2..6380977a 100644 --- a/README_JA.md +++ b/README_JA.md @@ -74,11 +74,11 @@ CLIProxyAPIは[Amp CLI](https://ampcode.com)およびAmp IDE拡張機能の統 - 利用できないモデルを代替モデルにルーティングする**モデルマッピング**(例:`claude-opus-4.5` → `claude-sonnet-4`) - localhostのみの管理エンドポイントによるセキュリティファーストの設計 -どのバックエンド実行系に送るかを確定させたい場合は、統合された OpenAI 互換 `/v1/...` エンドポイントよりも provider-specific のパスを優先してください。 +どのバックエンド実行系に送るかを確定させたい場合は、統合された `/v1/...` エンドポイントよりも provider-specific のパスを優先してください。 -- Anthropic 実行系を強制したい場合は `/api/provider/anthropic/v1/messages` -- Gemini/Google 実行系を強制したい場合は `/api/provider/google/v1beta/models/...` -- OpenAI 互換実行系を強制したい場合は `/api/provider/openai/v1/chat/completions` +- messages 系のバックエンドには `/api/provider/{provider}/v1/messages` +- モデル単位の generate 系エンドポイントには `/api/provider/{provider}/v1beta/models/...` +- chat-completions 系のバックエンドには `/api/provider/{provider}/v1/chat/completions` これは `oauth-model-alias`、alias プール、model fallback によって複数のバックエンドが同じクライアント向けモデル名を共有する場合に特に重要です。その場合、`/v1/models` は実際に狙っている実行系ではなく、統合後のエイリアス表示を返すことがあります。バックエンド選択の基準としては provider-specific のリクエストパスを使ってください。 From 0ab977c23670cd3a280165c765f0320b31bfef14 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Fri, 27 Mar 2026 11:13:08 +0100 Subject: [PATCH 11/21] docs: clarify provider path limitations --- README.md | 4 ++-- README_CN.md | 4 ++-- README_JA.md | 4 ++-- config.example.yaml | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index de8b4c03..d1854d33 100644 --- a/README.md +++ b/README.md @@ -74,13 +74,13 @@ CLIProxyAPI includes integrated support for [Amp CLI](https://ampcode.com) and A - **Model mapping** to route unavailable models to alternatives (e.g., `claude-opus-4.5` → `claude-sonnet-4`) - Security-first design with localhost-only management endpoints -When routing needs to stay deterministic, prefer the provider-specific paths over the merged `/v1/...` endpoints: +When you need the request/response shape of a specific backend family, use the provider-specific paths instead of the merged `/v1/...` endpoints: - Use `/api/provider/{provider}/v1/messages` for messages-style backends. - Use `/api/provider/{provider}/v1beta/models/...` for model-scoped generate endpoints. - Use `/api/provider/{provider}/v1/chat/completions` for chat-completions backends. -This matters when `oauth-model-alias`, alias pools, or fallback mappings reuse the same client-visible model name across multiple backends. In those cases, `/v1/models` may show the merged alias view instead of the executor you intend to hit. Treat the provider-specific request path as the source of truth for backend selection. +These routes help you select the protocol surface, but they do not by themselves guarantee a unique inference executor when the same client-visible model name is reused across multiple backends. Inference routing is still resolved from the request model/alias. For strict backend pinning, use unique aliases, prefixes, or otherwise avoid overlapping client-visible model names. **→ [Complete Amp CLI Integration Guide](https://help.router-for.me/agent-client/amp-cli.html)** diff --git a/README_CN.md b/README_CN.md index 463c7ad1..ea65408c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -73,13 +73,13 @@ CLIProxyAPI 已内置对 [Amp CLI](https://ampcode.com) 和 Amp IDE 扩展的支 - 智能模型回退与自动路由 - 以安全为先的设计,管理端点仅限 localhost -当你需要确定地命中某个后端执行器时,优先使用 provider-specific 路径,而不是合并后的 `/v1/...` 端点: +当你需要某一类后端的请求/响应协议形态时,优先使用 provider-specific 路径,而不是合并后的 `/v1/...` 端点: - 对于 messages 风格的后端,使用 `/api/provider/{provider}/v1/messages`。 - 对于按模型路径暴露生成接口的后端,使用 `/api/provider/{provider}/v1beta/models/...`。 - 对于 chat-completions 风格的后端,使用 `/api/provider/{provider}/v1/chat/completions`。 -这一点在 `oauth-model-alias`、alias 池或 model fallback 让多个后端复用同一个客户端可见模型名时尤其重要。此时 `/v1/models` 可能展示的是合并后的别名视图,而不是你真正想命中的执行器。对于后端选择,请以 provider-specific 的请求路径为准。 +这些路径有助于选择协议表面,但当多个后端复用同一个客户端可见模型名时,它们本身并不能保证唯一的推理执行器。实际的推理路由仍然根据请求里的 model/alias 解析。若要严格钉住某个后端,请使用唯一 alias、前缀,或避免让多个后端暴露相同的客户端模型名。 **→ [Amp CLI 完整集成指南](https://help.router-for.me/cn/agent-client/amp-cli.html)** diff --git a/README_JA.md b/README_JA.md index 6380977a..895b4bbd 100644 --- a/README_JA.md +++ b/README_JA.md @@ -74,13 +74,13 @@ CLIProxyAPIは[Amp CLI](https://ampcode.com)およびAmp IDE拡張機能の統 - 利用できないモデルを代替モデルにルーティングする**モデルマッピング**(例:`claude-opus-4.5` → `claude-sonnet-4`) - localhostのみの管理エンドポイントによるセキュリティファーストの設計 -どのバックエンド実行系に送るかを確定させたい場合は、統合された `/v1/...` エンドポイントよりも provider-specific のパスを優先してください。 +特定のバックエンド系統のリクエスト/レスポンス形状が必要な場合は、統合された `/v1/...` エンドポイントよりも provider-specific のパスを優先してください。 - messages 系のバックエンドには `/api/provider/{provider}/v1/messages` - モデル単位の generate 系エンドポイントには `/api/provider/{provider}/v1beta/models/...` - chat-completions 系のバックエンドには `/api/provider/{provider}/v1/chat/completions` -これは `oauth-model-alias`、alias プール、model fallback によって複数のバックエンドが同じクライアント向けモデル名を共有する場合に特に重要です。その場合、`/v1/models` は実際に狙っている実行系ではなく、統合後のエイリアス表示を返すことがあります。バックエンド選択の基準としては provider-specific のリクエストパスを使ってください。 +これらのパスはプロトコル面の選択には役立ちますが、同じクライアント向けモデル名が複数バックエンドで再利用されている場合、それだけで推論実行系が一意に固定されるわけではありません。実際の推論ルーティングは、引き続きリクエスト内の model/alias 解決に従います。厳密にバックエンドを固定したい場合は、一意な alias や prefix を使うか、クライアント向けモデル名の重複自体を避けてください。 **→ [Amp CLI統合ガイドの完全版](https://help.router-for.me/agent-client/amp-cli.html)** diff --git a/config.example.yaml b/config.example.yaml index 0493c544..1b365d87 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -282,8 +282,9 @@ nonstream-keepalive-interval: 0 # Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow, kimi. # NOTE: Aliases do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode. # NOTE: Because aliases affect the merged /v1 model list and merged request routing, overlapping -# client-visible names can become ambiguous across providers. When you need deterministic backend -# selection, prefer /api/provider/{provider}/... instead of the generic /v1/... endpoints. +# client-visible names can become ambiguous across providers. /api/provider/{provider}/... helps +# you select the protocol surface, but inference backend selection can still follow the resolved +# model/alias. For strict backend pinning, use unique aliases/prefixes or avoid overlapping names. # You can repeat the same name with different aliases to expose multiple client model names. # oauth-model-alias: # gemini-cli: From 511b8a992e88a2ffefc806606023dc310a56b808 Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 27 Mar 2026 17:49:29 +0200 Subject: [PATCH 12/21] fix(codex): restore prompt cache continuity for Codex requests Prompt caching on Codex was not reliably reusable through the proxy because repeated chat-completions requests could reach the upstream without the same continuity envelope. In practice this showed up most clearly with OpenCode, where cache reads worked in the reference client but not through CLIProxyAPI, although the root cause is broader than OpenCode itself. The proxy was breaking continuity in several ways: executor-layer Codex request preparation stripped prompt_cache_retention, chat-completions translation did not preserve that field, continuity headers used a different shape than the working client behavior, and OpenAI-style Codex requests could be sent without a stable prompt_cache_key. When that happened, session_id fell back to a fresh random value per request, so upstream Codex treated repeated requests as unrelated turns instead of as part of the same cacheable context. This change fixes that by preserving caller-provided prompt_cache_retention on Codex execution paths, preserving prompt_cache_retention when translating OpenAI chat-completions requests to Codex, aligning Codex continuity headers to session_id, and introducing an explicit Codex continuity policy that derives a stable continuity key from the best available signal. The resolution order prefers an explicit prompt_cache_key, then execution session metadata, then an explicit idempotency key, then stable request-affinity metadata, then a stable client-principal hash, and finally a stable auth-ID hash when no better continuity signal exists. The same continuity key is applied to both prompt_cache_key in the request body and session_id in the request headers so repeated requests reuse the same upstream cache/session identity. The auth manager also keeps auth selection sticky for repeated request sequences, preventing otherwise-equivalent Codex requests from drifting across different upstream auth contexts and accidentally breaking cache reuse. To keep the implementation maintainable, the continuity resolution and diagnostics are centralized in a dedicated Codex continuity helper instead of being scattered across executor flow code. Regression coverage now verifies retention preservation, continuity-key precedence, stable auth-ID fallback, websocket parity, translator preservation, and auth-affinity behavior. Manual validation confirmed prompt cache reads now occur through CLIProxyAPI when using Codex via OpenCode, and the fix should also benefit other clients that rely on stable repeated Codex request continuity. --- internal/runtime/executor/codex_continuity.go | 153 ++++++++++++++++++ internal/runtime/executor/codex_executor.go | 35 ++-- .../executor/codex_executor_cache_test.go | 101 +++++++++++- .../executor/codex_websockets_executor.go | 10 +- .../codex_websockets_executor_test.go | 25 +++ .../chat-completions/codex_openai_request.go | 3 + .../codex_openai_request_test.go | 16 ++ sdk/api/handlers/handlers.go | 14 +- sdk/cliproxy/auth/conductor.go | 111 ++++++++++++- sdk/cliproxy/auth/conductor_affinity_test.go | 85 ++++++++++ 10 files changed, 516 insertions(+), 37 deletions(-) create mode 100644 internal/runtime/executor/codex_continuity.go create mode 100644 sdk/cliproxy/auth/conductor_affinity_test.go diff --git a/internal/runtime/executor/codex_continuity.go b/internal/runtime/executor/codex_continuity.go new file mode 100644 index 00000000..e7d4508f --- /dev/null +++ b/internal/runtime/executor/codex_continuity.go @@ -0,0 +1,153 @@ +package executor + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/google/uuid" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +const codexAuthAffinityMetadataKey = "auth_affinity_key" + +type codexContinuity struct { + Key string + Source string +} + +func resolveCodexContinuity(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) codexContinuity { + if promptCacheKey := strings.TrimSpace(gjson.GetBytes(req.Payload, "prompt_cache_key").String()); promptCacheKey != "" { + return codexContinuity{Key: promptCacheKey, Source: "prompt_cache_key"} + } + if opts.Metadata != nil { + if raw, ok := opts.Metadata[cliproxyexecutor.ExecutionSessionMetadataKey]; ok && raw != nil { + switch v := raw.(type) { + case string: + if trimmed := strings.TrimSpace(v); trimmed != "" { + return codexContinuity{Key: trimmed, Source: "execution_session"} + } + case []byte: + if trimmed := strings.TrimSpace(string(v)); trimmed != "" { + return codexContinuity{Key: trimmed, Source: "execution_session"} + } + } + } + } + if ginCtx := ginContextFrom(ctx); ginCtx != nil { + if ginCtx.Request != nil { + if v := strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key")); v != "" { + return codexContinuity{Key: v, Source: "idempotency_key"} + } + } + if v, exists := ginCtx.Get("apiKey"); exists && v != nil { + switch value := v.(type) { + case string: + if trimmed := strings.TrimSpace(value); trimmed != "" { + return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"} + } + case fmt.Stringer: + if trimmed := strings.TrimSpace(value.String()); trimmed != "" { + return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"} + } + default: + trimmed := strings.TrimSpace(fmt.Sprintf("%v", value)) + if trimmed != "" { + return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"} + } + } + } + } + if opts.Metadata != nil { + if raw, ok := opts.Metadata[codexAuthAffinityMetadataKey]; ok && raw != nil { + switch v := raw.(type) { + case string: + if trimmed := strings.TrimSpace(v); trimmed != "" { + return codexContinuity{Key: trimmed, Source: "auth_affinity"} + } + case []byte: + if trimmed := strings.TrimSpace(string(v)); trimmed != "" { + return codexContinuity{Key: trimmed, Source: "auth_affinity"} + } + } + } + } + if auth != nil { + if authID := strings.TrimSpace(auth.ID); authID != "" { + return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:auth:"+authID)).String(), Source: "auth_id"} + } + } + return codexContinuity{} +} + +func applyCodexContinuityBody(rawJSON []byte, continuity codexContinuity) []byte { + if continuity.Key == "" { + return rawJSON + } + rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", continuity.Key) + return rawJSON +} + +func applyCodexContinuityHeaders(headers http.Header, continuity codexContinuity) { + if headers == nil || continuity.Key == "" { + return + } + headers.Set("session_id", continuity.Key) +} + +func logCodexRequestDiagnostics(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, headers http.Header, body []byte, continuity codexContinuity) { + if !log.IsLevelEnabled(log.DebugLevel) { + return + } + entry := logWithRequestID(ctx) + authID := "" + authFile := "" + if auth != nil { + authID = strings.TrimSpace(auth.ID) + authFile = strings.TrimSpace(auth.FileName) + } + selectedAuthID := "" + executionSessionID := "" + if opts.Metadata != nil { + if raw, ok := opts.Metadata[cliproxyexecutor.SelectedAuthMetadataKey]; ok && raw != nil { + switch v := raw.(type) { + case string: + selectedAuthID = strings.TrimSpace(v) + case []byte: + selectedAuthID = strings.TrimSpace(string(v)) + } + } + if raw, ok := opts.Metadata[cliproxyexecutor.ExecutionSessionMetadataKey]; ok && raw != nil { + switch v := raw.(type) { + case string: + executionSessionID = strings.TrimSpace(v) + case []byte: + executionSessionID = strings.TrimSpace(string(v)) + } + } + } + entry.Debugf( + "codex request diagnostics auth_id=%s selected_auth_id=%s auth_file=%s exec_session=%s continuity_source=%s session_id=%s prompt_cache_key=%s prompt_cache_retention=%s store=%t has_instructions=%t reasoning_effort=%s reasoning_summary=%s chatgpt_account_id=%t originator=%s model=%s source_format=%s", + authID, + selectedAuthID, + authFile, + executionSessionID, + continuity.Source, + strings.TrimSpace(headers.Get("session_id")), + gjson.GetBytes(body, "prompt_cache_key").String(), + gjson.GetBytes(body, "prompt_cache_retention").String(), + gjson.GetBytes(body, "store").Bool(), + gjson.GetBytes(body, "instructions").Exists(), + gjson.GetBytes(body, "reasoning.effort").String(), + gjson.GetBytes(body, "reasoning.summary").String(), + strings.TrimSpace(headers.Get("Chatgpt-Account-Id")) != "", + strings.TrimSpace(headers.Get("Originator")), + req.Model, + opts.SourceFormat.String(), + ) +} diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 7e4163b8..766a081a 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -111,18 +111,18 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "stream", true) body, _ = sjson.DeleteBytes(body, "previous_response_id") - body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") if !gjson.GetBytes(body, "instructions").Exists() { body, _ = sjson.SetBytes(body, "instructions", "") } url := strings.TrimSuffix(baseURL, "/") + "/responses" - httpReq, err := e.cacheHelper(ctx, from, url, req, body) + httpReq, continuity, err := e.cacheHelper(ctx, auth, from, url, req, opts, body) if err != nil { return resp, err } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) + logCodexRequestDiagnostics(ctx, auth, req, opts, httpReq.Header, body, continuity) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -222,11 +222,12 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A body, _ = sjson.DeleteBytes(body, "stream") url := strings.TrimSuffix(baseURL, "/") + "/responses/compact" - httpReq, err := e.cacheHelper(ctx, from, url, req, body) + httpReq, continuity, err := e.cacheHelper(ctx, auth, from, url, req, opts, body) if err != nil { return resp, err } applyCodexHeaders(httpReq, auth, apiKey, false, e.cfg) + logCodexRequestDiagnostics(ctx, auth, req, opts, httpReq.Header, body, continuity) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -309,7 +310,6 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au requestedModel := payloadRequestedModel(opts, req.Model) body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.DeleteBytes(body, "previous_response_id") - body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") body, _ = sjson.SetBytes(body, "model", baseModel) if !gjson.GetBytes(body, "instructions").Exists() { @@ -317,11 +317,12 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } url := strings.TrimSuffix(baseURL, "/") + "/responses" - httpReq, err := e.cacheHelper(ctx, from, url, req, body) + httpReq, continuity, err := e.cacheHelper(ctx, auth, from, url, req, opts, body) if err != nil { return nil, err } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) + logCodexRequestDiagnostics(ctx, auth, req, opts, httpReq.Header, body, continuity) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -596,8 +597,9 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (* return auth, nil } -func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte) (*http.Request, error) { +func (e *CodexExecutor) cacheHelper(ctx context.Context, auth *cliproxyauth.Auth, from sdktranslator.Format, url string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, rawJSON []byte) (*http.Request, codexContinuity, error) { var cache codexCache + continuity := codexContinuity{} if from == "claude" { userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id") if userIDResult.Exists() { @@ -615,25 +617,20 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key") if promptCacheKey.Exists() { cache.ID = promptCacheKey.String() + continuity = codexContinuity{Key: cache.ID, Source: "prompt_cache_key"} } } else if from == "openai" { - if apiKey := strings.TrimSpace(apiKeyFromContext(ctx)); apiKey != "" { - cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String() - } + continuity = resolveCodexContinuity(ctx, auth, req, opts) + cache.ID = continuity.Key } - if cache.ID != "" { - rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", cache.ID) - } + rawJSON = applyCodexContinuityBody(rawJSON, continuity) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(rawJSON)) if err != nil { - return nil, err + return nil, continuity, err } - if cache.ID != "" { - httpReq.Header.Set("Conversation_id", cache.ID) - httpReq.Header.Set("Session_id", cache.ID) - } - return httpReq, nil + applyCodexContinuityHeaders(httpReq.Header, continuity) + return httpReq, continuity, nil } func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) { @@ -646,7 +643,7 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s } misc.EnsureHeader(r.Header, ginHeaders, "Version", "") - misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString()) + misc.EnsureHeader(r.Header, ginHeaders, "session_id", uuid.NewString()) misc.EnsureHeader(r.Header, ginHeaders, "X-Codex-Turn-Metadata", "") misc.EnsureHeader(r.Header, ginHeaders, "X-Client-Request-Id", "") cfgUserAgent, _ := codexHeaderDefaults(cfg, auth) diff --git a/internal/runtime/executor/codex_executor_cache_test.go b/internal/runtime/executor/codex_executor_cache_test.go index d6dca031..116b06ff 100644 --- a/internal/runtime/executor/codex_executor_cache_test.go +++ b/internal/runtime/executor/codex_executor_cache_test.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" "github.com/tidwall/gjson" @@ -27,7 +28,7 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFrom } url := "https://example.com/responses" - httpReq, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON) + httpReq, _, err := executor.cacheHelper(ctx, nil, sdktranslator.FromString("openai"), url, req, cliproxyexecutor.Options{}, rawJSON) if err != nil { t.Fatalf("cacheHelper error: %v", err) } @@ -42,14 +43,14 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFrom if gotKey != expectedKey { t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedKey) } - if gotConversation := httpReq.Header.Get("Conversation_id"); gotConversation != expectedKey { - t.Fatalf("Conversation_id = %q, want %q", gotConversation, expectedKey) + if gotSession := httpReq.Header.Get("session_id"); gotSession != expectedKey { + t.Fatalf("session_id = %q, want %q", gotSession, expectedKey) } - if gotSession := httpReq.Header.Get("Session_id"); gotSession != expectedKey { - t.Fatalf("Session_id = %q, want %q", gotSession, expectedKey) + if got := httpReq.Header.Get("Conversation_id"); got != "" { + t.Fatalf("Conversation_id = %q, want empty", got) } - httpReq2, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON) + httpReq2, _, err := executor.cacheHelper(ctx, nil, sdktranslator.FromString("openai"), url, req, cliproxyexecutor.Options{}, rawJSON) if err != nil { t.Fatalf("cacheHelper error (second call): %v", err) } @@ -62,3 +63,91 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFrom t.Fatalf("prompt_cache_key (second call) = %q, want %q", gotKey2, expectedKey) } } + +func TestCodexExecutorCacheHelper_OpenAIResponses_PreservesPromptCacheRetention(t *testing.T) { + executor := &CodexExecutor{} + url := "https://example.com/responses" + req := cliproxyexecutor.Request{ + Model: "gpt-5.3-codex", + Payload: []byte(`{"model":"gpt-5.3-codex","prompt_cache_key":"cache-key-1","prompt_cache_retention":"persistent"}`), + } + rawJSON := []byte(`{"model":"gpt-5.3-codex","stream":true,"prompt_cache_retention":"persistent"}`) + + httpReq, _, err := executor.cacheHelper(context.Background(), nil, sdktranslator.FromString("openai-response"), url, req, cliproxyexecutor.Options{}, rawJSON) + if err != nil { + t.Fatalf("cacheHelper error: %v", err) + } + + body, err := io.ReadAll(httpReq.Body) + if err != nil { + t.Fatalf("read request body: %v", err) + } + + if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != "cache-key-1" { + t.Fatalf("prompt_cache_key = %q, want %q", got, "cache-key-1") + } + if got := gjson.GetBytes(body, "prompt_cache_retention").String(); got != "persistent" { + t.Fatalf("prompt_cache_retention = %q, want %q", got, "persistent") + } + if got := httpReq.Header.Get("session_id"); got != "cache-key-1" { + t.Fatalf("session_id = %q, want %q", got, "cache-key-1") + } + if got := httpReq.Header.Get("Conversation_id"); got != "" { + t.Fatalf("Conversation_id = %q, want empty", got) + } +} + +func TestCodexExecutorCacheHelper_OpenAIChatCompletions_UsesExecutionSessionForContinuity(t *testing.T) { + executor := &CodexExecutor{} + rawJSON := []byte(`{"model":"gpt-5.4","stream":true}`) + req := cliproxyexecutor.Request{ + Model: "gpt-5.4", + Payload: []byte(`{"model":"gpt-5.4"}`), + } + opts := cliproxyexecutor.Options{Metadata: map[string]any{cliproxyexecutor.ExecutionSessionMetadataKey: "exec-session-1"}} + + httpReq, _, err := executor.cacheHelper(context.Background(), nil, sdktranslator.FromString("openai"), "https://example.com/responses", req, opts, rawJSON) + if err != nil { + t.Fatalf("cacheHelper error: %v", err) + } + + body, err := io.ReadAll(httpReq.Body) + if err != nil { + t.Fatalf("read request body: %v", err) + } + + if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != "exec-session-1" { + t.Fatalf("prompt_cache_key = %q, want %q", got, "exec-session-1") + } + if got := httpReq.Header.Get("session_id"); got != "exec-session-1" { + t.Fatalf("session_id = %q, want %q", got, "exec-session-1") + } +} + +func TestCodexExecutorCacheHelper_OpenAIChatCompletions_FallsBackToStableAuthID(t *testing.T) { + executor := &CodexExecutor{} + rawJSON := []byte(`{"model":"gpt-5.4","stream":true}`) + req := cliproxyexecutor.Request{ + Model: "gpt-5.4", + Payload: []byte(`{"model":"gpt-5.4"}`), + } + auth := &cliproxyauth.Auth{ID: "codex-auth-1", Provider: "codex"} + + httpReq, _, err := executor.cacheHelper(context.Background(), auth, sdktranslator.FromString("openai"), "https://example.com/responses", req, cliproxyexecutor.Options{}, rawJSON) + if err != nil { + t.Fatalf("cacheHelper error: %v", err) + } + + body, err := io.ReadAll(httpReq.Body) + if err != nil { + t.Fatalf("read request body: %v", err) + } + + expected := uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:auth:codex-auth-1")).String() + if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != expected { + t.Fatalf("prompt_cache_key = %q, want %q", got, expected) + } + if got := httpReq.Header.Get("session_id"); got != expected { + t.Fatalf("session_id = %q, want %q", got, expected) + } +} diff --git a/internal/runtime/executor/codex_websockets_executor.go b/internal/runtime/executor/codex_websockets_executor.go index fca82fe7..b8ae11ae 100644 --- a/internal/runtime/executor/codex_websockets_executor.go +++ b/internal/runtime/executor/codex_websockets_executor.go @@ -178,7 +178,6 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "stream", true) body, _ = sjson.DeleteBytes(body, "previous_response_id") - body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") if !gjson.GetBytes(body, "instructions").Exists() { body, _ = sjson.SetBytes(body, "instructions", "") @@ -191,6 +190,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut } body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body) + continuity := codexContinuity{Key: strings.TrimSpace(wsHeaders.Get("session_id"))} wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg) var authID, authLabel, authType, authValue string @@ -209,6 +209,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut } wsReqBody := buildCodexWebsocketRequestBody(body) + logCodexRequestDiagnostics(ctx, auth, req, opts, wsHeaders, body, continuity) recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", @@ -386,6 +387,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr } body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body) + continuity := codexContinuity{Key: strings.TrimSpace(wsHeaders.Get("session_id"))} wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg) var authID, authLabel, authType, authValue string @@ -403,6 +405,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr } wsReqBody := buildCodexWebsocketRequestBody(body) + logCodexRequestDiagnostics(ctx, auth, req, opts, wsHeaders, body, continuity) recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", @@ -790,8 +793,7 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto if cache.ID != "" { rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", cache.ID) - headers.Set("Conversation_id", cache.ID) - headers.Set("Session_id", cache.ID) + headers.Set("session_id", cache.ID) } return rawJSON, headers @@ -826,7 +828,7 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth * betaHeader = codexResponsesWebsocketBetaHeaderValue } headers.Set("OpenAI-Beta", betaHeader) - misc.EnsureHeader(headers, ginHeaders, "Session_id", uuid.NewString()) + misc.EnsureHeader(headers, ginHeaders, "session_id", uuid.NewString()) ensureHeaderWithConfigPrecedence(headers, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent) isAPIKey := false diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index d34e7c39..733318a3 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -9,7 +9,9 @@ import ( "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" "github.com/tidwall/gjson" ) @@ -32,6 +34,29 @@ func TestBuildCodexWebsocketRequestBodyPreservesPreviousResponseID(t *testing.T) } } +func TestApplyCodexPromptCacheHeaders_PreservesPromptCacheRetention(t *testing.T) { + req := cliproxyexecutor.Request{ + Model: "gpt-5-codex", + Payload: []byte(`{"prompt_cache_key":"cache-key-1","prompt_cache_retention":"persistent"}`), + } + body := []byte(`{"model":"gpt-5-codex","stream":true,"prompt_cache_retention":"persistent"}`) + + updatedBody, headers := applyCodexPromptCacheHeaders(sdktranslator.FromString("openai-response"), req, body) + + if got := gjson.GetBytes(updatedBody, "prompt_cache_key").String(); got != "cache-key-1" { + t.Fatalf("prompt_cache_key = %q, want %q", got, "cache-key-1") + } + if got := gjson.GetBytes(updatedBody, "prompt_cache_retention").String(); got != "persistent" { + t.Fatalf("prompt_cache_retention = %q, want %q", got, "persistent") + } + if got := headers.Get("session_id"); got != "cache-key-1" { + t.Fatalf("session_id = %q, want %q", got, "cache-key-1") + } + if got := headers.Get("Conversation_id"); got != "" { + t.Fatalf("Conversation_id = %q, want empty", got) + } +} + func TestApplyCodexWebsocketHeadersDefaultsToCurrentResponsesBeta(t *testing.T) { headers := applyCodexWebsocketHeaders(context.Background(), http.Header{}, nil, "", nil) diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_request.go b/internal/translator/codex/openai/chat-completions/codex_openai_request.go index 6cc701e7..7d24d60e 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_request.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request.go @@ -65,6 +65,9 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b // Model out, _ = sjson.SetBytes(out, "model", modelName) + if v := gjson.GetBytes(rawJSON, "prompt_cache_retention"); v.Exists() { + out, _ = sjson.SetBytes(out, "prompt_cache_retention", v.Value()) + } // Build tool name shortening map from original tools (if any) originalToolNameMap := map[string]string{} diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_request_test.go b/internal/translator/codex/openai/chat-completions/codex_openai_request_test.go index 84c8dad2..1202980f 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_request_test.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request_test.go @@ -633,3 +633,19 @@ func TestToolsDefinitionTranslated(t *testing.T) { t.Errorf("tool 'search' not found in output tools: %s", gjson.Get(result, "tools").Raw) } } + +func TestPromptCacheRetentionPreserved(t *testing.T) { + input := []byte(`{ + "model": "gpt-4o", + "prompt_cache_retention": "persistent", + "messages": [ + {"role": "user", "content": "Hello"} + ] + }`) + + out := ConvertOpenAIRequestToCodex("gpt-4o", input, true) + + if got := gjson.GetBytes(out, "prompt_cache_retention").String(); got != "persistent" { + t.Fatalf("prompt_cache_retention = %q, want %q", got, "persistent") + } +} diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 28ab970d..8679f1a1 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -46,6 +46,7 @@ type ErrorDetail struct { } const idempotencyKeyMetadataKey = "idempotency_key" +const authAffinityMetadataKey = "auth_affinity_key" const ( defaultStreamingKeepAliveSeconds = 0 @@ -189,9 +190,11 @@ func requestExecutionMetadata(ctx context.Context) map[string]any { // Idempotency-Key is an optional client-supplied header used to correlate retries. // It is forwarded as execution metadata; when absent we generate a UUID. key := "" + explicitIdempotencyKey := "" if ctx != nil { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { - key = strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key")) + explicitIdempotencyKey = strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key")) + key = explicitIdempotencyKey } } if key == "" { @@ -207,6 +210,15 @@ func requestExecutionMetadata(ctx context.Context) map[string]any { } if executionSessionID := executionSessionIDFromContext(ctx); executionSessionID != "" { meta[coreexecutor.ExecutionSessionMetadataKey] = executionSessionID + meta[authAffinityMetadataKey] = executionSessionID + } else if explicitIdempotencyKey != "" { + meta[authAffinityMetadataKey] = explicitIdempotencyKey + } else if ctx != nil { + if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { + if apiKey, exists := ginCtx.Get("apiKey"); exists && apiKey != nil { + meta[authAffinityMetadataKey] = fmt.Sprintf("principal:%v", apiKey) + } + } } return meta } diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 9f46c7cf..7a62f852 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -128,13 +128,15 @@ func (NoopHook) OnResult(context.Context, Result) {} // Manager orchestrates auth lifecycle, selection, execution, and persistence. type Manager struct { - store Store - executors map[string]ProviderExecutor - selector Selector - hook Hook - mu sync.RWMutex - auths map[string]*Auth - scheduler *authScheduler + store Store + executors map[string]ProviderExecutor + selector Selector + hook Hook + mu sync.RWMutex + auths map[string]*Auth + scheduler *authScheduler + affinityMu sync.RWMutex + affinity map[string]string // providerOffsets tracks per-model provider rotation state for multi-provider routing. providerOffsets map[string]int @@ -179,6 +181,7 @@ func NewManager(store Store, selector Selector, hook Hook) *Manager { selector: selector, hook: hook, auths: make(map[string]*Auth), + affinity: make(map[string]string), providerOffsets: make(map[string]int), modelPoolOffsets: make(map[string]int), refreshSemaphore: make(chan struct{}, refreshMaxConcurrency), @@ -1090,6 +1093,12 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req entry := logEntryWithRequestID(ctx) debugLogAuthSelection(entry, auth, provider, req.Model) publishSelectedAuthMetadata(opts.Metadata, auth.ID) + if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { + m.SetAuthAffinity(affinityKey, auth.ID) + if log.IsLevelEnabled(log.DebugLevel) { + entry.Debugf("auth affinity pinned key=%s auth_id=%s provider=%s model=%s", affinityKey, auth.ID, provider, req.Model) + } + } tried[auth.ID] = struct{}{} execCtx := ctx @@ -1168,6 +1177,12 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string, entry := logEntryWithRequestID(ctx) debugLogAuthSelection(entry, auth, provider, req.Model) publishSelectedAuthMetadata(opts.Metadata, auth.ID) + if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { + m.SetAuthAffinity(affinityKey, auth.ID) + if log.IsLevelEnabled(log.DebugLevel) { + entry.Debugf("auth affinity pinned key=%s auth_id=%s provider=%s model=%s", affinityKey, auth.ID, provider, req.Model) + } + } tried[auth.ID] = struct{}{} execCtx := ctx @@ -1254,6 +1269,12 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string entry := logEntryWithRequestID(ctx) debugLogAuthSelection(entry, auth, provider, req.Model) publishSelectedAuthMetadata(opts.Metadata, auth.ID) + if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { + m.SetAuthAffinity(affinityKey, auth.ID) + if log.IsLevelEnabled(log.DebugLevel) { + entry.Debugf("auth affinity pinned key=%s auth_id=%s provider=%s model=%s", affinityKey, auth.ID, provider, req.Model) + } + } tried[auth.ID] = struct{}{} execCtx := ctx @@ -2222,6 +2243,58 @@ func (m *Manager) CloseExecutionSession(sessionID string) { } } +func authAffinityKeyFromMetadata(meta map[string]any) string { + if len(meta) == 0 { + return "" + } + raw, ok := meta["auth_affinity_key"] + if !ok || raw == nil { + return "" + } + switch val := raw.(type) { + case string: + return strings.TrimSpace(val) + case []byte: + return strings.TrimSpace(string(val)) + default: + return "" + } +} + +func (m *Manager) AuthAffinity(key string) string { + key = strings.TrimSpace(key) + if m == nil || key == "" { + return "" + } + m.affinityMu.RLock() + defer m.affinityMu.RUnlock() + return strings.TrimSpace(m.affinity[key]) +} + +func (m *Manager) SetAuthAffinity(key, authID string) { + key = strings.TrimSpace(key) + authID = strings.TrimSpace(authID) + if m == nil || key == "" || authID == "" { + return + } + m.affinityMu.Lock() + if m.affinity == nil { + m.affinity = make(map[string]string) + } + m.affinity[key] = authID + m.affinityMu.Unlock() +} + +func (m *Manager) ClearAuthAffinity(key string) { + key = strings.TrimSpace(key) + if m == nil || key == "" { + return + } + m.affinityMu.Lock() + delete(m.affinity, key) + m.affinityMu.Unlock() +} + func (m *Manager) useSchedulerFastPath() bool { if m == nil || m.scheduler == nil { return false @@ -2305,6 +2378,18 @@ func (m *Manager) pickNextLegacy(ctx context.Context, provider, model string, op } func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) { + if pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata); pinnedAuthID == "" { + if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { + if affinityAuthID := m.AuthAffinity(affinityKey); affinityAuthID != "" { + meta := opts.Metadata + if meta == nil { + meta = make(map[string]any) + opts.Metadata = meta + } + meta[cliproxyexecutor.PinnedAuthMetadataKey] = affinityAuthID + } + } + } if !m.useSchedulerFastPath() { return m.pickNextLegacy(ctx, provider, model, opts, tried) } @@ -2419,6 +2504,18 @@ func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, m } func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) { + if pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata); pinnedAuthID == "" { + if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { + if affinityAuthID := m.AuthAffinity(affinityKey); affinityAuthID != "" { + meta := opts.Metadata + if meta == nil { + meta = make(map[string]any) + opts.Metadata = meta + } + meta[cliproxyexecutor.PinnedAuthMetadataKey] = affinityAuthID + } + } + } if !m.useSchedulerFastPath() { return m.pickNextMixedLegacy(ctx, providers, model, opts, tried) } diff --git a/sdk/cliproxy/auth/conductor_affinity_test.go b/sdk/cliproxy/auth/conductor_affinity_test.go new file mode 100644 index 00000000..e84f7c96 --- /dev/null +++ b/sdk/cliproxy/auth/conductor_affinity_test.go @@ -0,0 +1,85 @@ +package auth + +import ( + "context" + "net/http" + "testing" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" +) + +type affinityTestExecutor struct{ id string } + +func (e affinityTestExecutor) Identifier() string { return e.id } + +func (e affinityTestExecutor) Execute(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { + return cliproxyexecutor.Response{}, nil +} + +func (e affinityTestExecutor) ExecuteStream(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) { + ch := make(chan cliproxyexecutor.StreamChunk) + close(ch) + return &cliproxyexecutor.StreamResult{Chunks: ch}, nil +} + +func (e affinityTestExecutor) Refresh(_ context.Context, auth *Auth) (*Auth, error) { return auth, nil } + +func (e affinityTestExecutor) CountTokens(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { + return cliproxyexecutor.Response{}, nil +} + +func (e affinityTestExecutor) HttpRequest(context.Context, *Auth, *http.Request) (*http.Response, error) { + return nil, nil +} + +func TestManagerPickNextMixedUsesAuthAffinity(t *testing.T) { + t.Parallel() + + manager := NewManager(nil, &RoundRobinSelector{}, nil) + manager.executors["codex"] = affinityTestExecutor{id: "codex"} + reg := registry.GetGlobalRegistry() + reg.RegisterClient("codex-a", "codex", []*registry.ModelInfo{{ID: "gpt-5.4"}}) + reg.RegisterClient("codex-b", "codex", []*registry.ModelInfo{{ID: "gpt-5.4"}}) + t.Cleanup(func() { + reg.UnregisterClient("codex-a") + reg.UnregisterClient("codex-b") + }) + if _, errRegister := manager.Register(context.Background(), &Auth{ID: "codex-a", Provider: "codex"}); errRegister != nil { + t.Fatalf("Register(codex-a) error = %v", errRegister) + } + if _, errRegister := manager.Register(context.Background(), &Auth{ID: "codex-b", Provider: "codex"}); errRegister != nil { + t.Fatalf("Register(codex-b) error = %v", errRegister) + } + + manager.SetAuthAffinity("idem-1", "codex-b") + opts := cliproxyexecutor.Options{Metadata: map[string]any{"auth_affinity_key": "idem-1"}} + + got, _, provider, errPick := manager.pickNextMixed(context.Background(), []string{"codex"}, "gpt-5.4", opts, map[string]struct{}{}) + if errPick != nil { + t.Fatalf("pickNextMixed() error = %v", errPick) + } + if provider != "codex" { + t.Fatalf("provider = %q, want %q", provider, "codex") + } + if got == nil || got.ID != "codex-b" { + t.Fatalf("auth.ID = %v, want codex-b", got) + } + if pinned := pinnedAuthIDFromMetadata(opts.Metadata); pinned != "codex-b" { + t.Fatalf("pinned auth metadata = %q, want %q", pinned, "codex-b") + } +} + +func TestManagerAuthAffinityRoundTrip(t *testing.T) { + t.Parallel() + + manager := NewManager(nil, nil, nil) + manager.SetAuthAffinity("idem-2", "auth-1") + if got := manager.AuthAffinity("idem-2"); got != "auth-1" { + t.Fatalf("AuthAffinity = %q, want %q", got, "auth-1") + } + manager.ClearAuthAffinity("idem-2") + if got := manager.AuthAffinity("idem-2"); got != "" { + t.Fatalf("AuthAffinity after clear = %q, want empty", got) + } +} From 62b17f40a100c312bbdd32f9e5631858ff85af8c Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 27 Mar 2026 18:11:57 +0200 Subject: [PATCH 13/21] refactor(codex): align continuity helpers with review feedback Align websocket continuity resolution with the HTTP Codex path, make auth-affinity principal keys use a stable string representation, and extract small helpers that remove duplicated continuity and affinity logic without changing the validated cache-hit behavior. --- internal/runtime/executor/codex_continuity.go | 99 +++++++------------ .../executor/codex_websockets_executor.go | 23 ++--- .../codex_websockets_executor_test.go | 2 +- sdk/api/handlers/handlers.go | 20 +++- sdk/cliproxy/auth/conductor.go | 40 +++----- 5 files changed, 86 insertions(+), 98 deletions(-) diff --git a/internal/runtime/executor/codex_continuity.go b/internal/runtime/executor/codex_continuity.go index e7d4508f..3ebb721f 100644 --- a/internal/runtime/executor/codex_continuity.go +++ b/internal/runtime/executor/codex_continuity.go @@ -21,23 +21,44 @@ type codexContinuity struct { Source string } +func metadataString(meta map[string]any, key string) string { + if len(meta) == 0 { + return "" + } + raw, ok := meta[key] + if !ok || raw == nil { + return "" + } + switch v := raw.(type) { + case string: + return strings.TrimSpace(v) + case []byte: + return strings.TrimSpace(string(v)) + default: + return "" + } +} + +func principalString(raw any) string { + switch v := raw.(type) { + case string: + return strings.TrimSpace(v) + case fmt.Stringer: + return strings.TrimSpace(v.String()) + default: + return strings.TrimSpace(fmt.Sprintf("%v", raw)) + } +} + func resolveCodexContinuity(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) codexContinuity { if promptCacheKey := strings.TrimSpace(gjson.GetBytes(req.Payload, "prompt_cache_key").String()); promptCacheKey != "" { return codexContinuity{Key: promptCacheKey, Source: "prompt_cache_key"} } - if opts.Metadata != nil { - if raw, ok := opts.Metadata[cliproxyexecutor.ExecutionSessionMetadataKey]; ok && raw != nil { - switch v := raw.(type) { - case string: - if trimmed := strings.TrimSpace(v); trimmed != "" { - return codexContinuity{Key: trimmed, Source: "execution_session"} - } - case []byte: - if trimmed := strings.TrimSpace(string(v)); trimmed != "" { - return codexContinuity{Key: trimmed, Source: "execution_session"} - } - } - } + if executionSession := metadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey); executionSession != "" { + return codexContinuity{Key: executionSession, Source: "execution_session"} + } + if affinityKey := metadataString(opts.Metadata, codexAuthAffinityMetadataKey); affinityKey != "" { + return codexContinuity{Key: affinityKey, Source: "auth_affinity"} } if ginCtx := ginContextFrom(ctx); ginCtx != nil { if ginCtx.Request != nil { @@ -46,34 +67,8 @@ func resolveCodexContinuity(ctx context.Context, auth *cliproxyauth.Auth, req cl } } if v, exists := ginCtx.Get("apiKey"); exists && v != nil { - switch value := v.(type) { - case string: - if trimmed := strings.TrimSpace(value); trimmed != "" { - return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"} - } - case fmt.Stringer: - if trimmed := strings.TrimSpace(value.String()); trimmed != "" { - return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"} - } - default: - trimmed := strings.TrimSpace(fmt.Sprintf("%v", value)) - if trimmed != "" { - return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"} - } - } - } - } - if opts.Metadata != nil { - if raw, ok := opts.Metadata[codexAuthAffinityMetadataKey]; ok && raw != nil { - switch v := raw.(type) { - case string: - if trimmed := strings.TrimSpace(v); trimmed != "" { - return codexContinuity{Key: trimmed, Source: "auth_affinity"} - } - case []byte: - if trimmed := strings.TrimSpace(string(v)); trimmed != "" { - return codexContinuity{Key: trimmed, Source: "auth_affinity"} - } + if trimmed := principalString(v); trimmed != "" { + return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"} } } } @@ -111,26 +106,8 @@ func logCodexRequestDiagnostics(ctx context.Context, auth *cliproxyauth.Auth, re authID = strings.TrimSpace(auth.ID) authFile = strings.TrimSpace(auth.FileName) } - selectedAuthID := "" - executionSessionID := "" - if opts.Metadata != nil { - if raw, ok := opts.Metadata[cliproxyexecutor.SelectedAuthMetadataKey]; ok && raw != nil { - switch v := raw.(type) { - case string: - selectedAuthID = strings.TrimSpace(v) - case []byte: - selectedAuthID = strings.TrimSpace(string(v)) - } - } - if raw, ok := opts.Metadata[cliproxyexecutor.ExecutionSessionMetadataKey]; ok && raw != nil { - switch v := raw.(type) { - case string: - executionSessionID = strings.TrimSpace(v) - case []byte: - executionSessionID = strings.TrimSpace(string(v)) - } - } - } + selectedAuthID := metadataString(opts.Metadata, cliproxyexecutor.SelectedAuthMetadataKey) + executionSessionID := metadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey) entry.Debugf( "codex request diagnostics auth_id=%s selected_auth_id=%s auth_file=%s exec_session=%s continuity_source=%s session_id=%s prompt_cache_key=%s prompt_cache_retention=%s store=%t has_instructions=%t reasoning_effort=%s reasoning_summary=%s chatgpt_account_id=%t originator=%s model=%s source_format=%s", authID, diff --git a/internal/runtime/executor/codex_websockets_executor.go b/internal/runtime/executor/codex_websockets_executor.go index b8ae11ae..d0dd22c3 100644 --- a/internal/runtime/executor/codex_websockets_executor.go +++ b/internal/runtime/executor/codex_websockets_executor.go @@ -189,8 +189,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut return resp, err } - body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body) - continuity := codexContinuity{Key: strings.TrimSpace(wsHeaders.Get("session_id"))} + body, wsHeaders, continuity := applyCodexPromptCacheHeaders(ctx, auth, from, req, opts, body) wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg) var authID, authLabel, authType, authValue string @@ -386,8 +385,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr return nil, err } - body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body) - continuity := codexContinuity{Key: strings.TrimSpace(wsHeaders.Get("session_id"))} + body, wsHeaders, continuity := applyCodexPromptCacheHeaders(ctx, auth, from, req, opts, body) wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg) var authID, authLabel, authType, authValue string @@ -764,13 +762,14 @@ func buildCodexResponsesWebsocketURL(httpURL string) (string, error) { return parsed.String(), nil } -func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecutor.Request, rawJSON []byte) ([]byte, http.Header) { +func applyCodexPromptCacheHeaders(ctx context.Context, auth *cliproxyauth.Auth, from sdktranslator.Format, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, rawJSON []byte) ([]byte, http.Header, codexContinuity) { headers := http.Header{} if len(rawJSON) == 0 { - return rawJSON, headers + return rawJSON, headers, codexContinuity{} } var cache codexCache + continuity := codexContinuity{} if from == "claude" { userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id") if userIDResult.Exists() { @@ -788,15 +787,17 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto } else if from == "openai-response" { if promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key"); promptCacheKey.Exists() { cache.ID = promptCacheKey.String() + continuity = codexContinuity{Key: cache.ID, Source: "prompt_cache_key"} } + } else if from == "openai" { + continuity = resolveCodexContinuity(ctx, auth, req, opts) + cache.ID = continuity.Key } - if cache.ID != "" { - rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", cache.ID) - headers.Set("session_id", cache.ID) - } + rawJSON = applyCodexContinuityBody(rawJSON, continuity) + applyCodexContinuityHeaders(headers, continuity) - return rawJSON, headers + return rawJSON, headers, continuity } func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *cliproxyauth.Auth, token string, cfg *config.Config) http.Header { diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index 733318a3..e86036bc 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -41,7 +41,7 @@ func TestApplyCodexPromptCacheHeaders_PreservesPromptCacheRetention(t *testing.T } body := []byte(`{"model":"gpt-5-codex","stream":true,"prompt_cache_retention":"persistent"}`) - updatedBody, headers := applyCodexPromptCacheHeaders(sdktranslator.FromString("openai-response"), req, body) + updatedBody, headers, _ := applyCodexPromptCacheHeaders(context.Background(), nil, sdktranslator.FromString("openai-response"), req, cliproxyexecutor.Options{}, body) if got := gjson.GetBytes(updatedBody, "prompt_cache_key").String(); got != "cache-key-1" { t.Fatalf("prompt_cache_key = %q, want %q", got, "cache-key-1") diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 8679f1a1..5fc1154e 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -216,13 +216,31 @@ func requestExecutionMetadata(ctx context.Context) map[string]any { } else if ctx != nil { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { if apiKey, exists := ginCtx.Get("apiKey"); exists && apiKey != nil { - meta[authAffinityMetadataKey] = fmt.Sprintf("principal:%v", apiKey) + if principal := stablePrincipalMetadataKey(apiKey); principal != "" { + meta[authAffinityMetadataKey] = principal + } } } } return meta } +func stablePrincipalMetadataKey(raw any) string { + var keyStr string + switch v := raw.(type) { + case string: + keyStr = v + case fmt.Stringer: + keyStr = v.String() + default: + keyStr = fmt.Sprintf("%v", raw) + } + if trimmed := strings.TrimSpace(keyStr); trimmed != "" { + return "principal:" + trimmed + } + return "" +} + func pinnedAuthIDFromContext(ctx context.Context) string { if ctx == nil { return "" diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 7a62f852..d7736cf4 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -2271,6 +2271,20 @@ func (m *Manager) AuthAffinity(key string) string { return strings.TrimSpace(m.affinity[key]) } +func (m *Manager) applyAuthAffinity(opts *cliproxyexecutor.Options) { + if m == nil || opts == nil || pinnedAuthIDFromMetadata(opts.Metadata) != "" { + return + } + if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { + if affinityAuthID := m.AuthAffinity(affinityKey); affinityAuthID != "" { + if opts.Metadata == nil { + opts.Metadata = make(map[string]any) + } + opts.Metadata[cliproxyexecutor.PinnedAuthMetadataKey] = affinityAuthID + } + } +} + func (m *Manager) SetAuthAffinity(key, authID string) { key = strings.TrimSpace(key) authID = strings.TrimSpace(authID) @@ -2378,18 +2392,7 @@ func (m *Manager) pickNextLegacy(ctx context.Context, provider, model string, op } func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) { - if pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata); pinnedAuthID == "" { - if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - if affinityAuthID := m.AuthAffinity(affinityKey); affinityAuthID != "" { - meta := opts.Metadata - if meta == nil { - meta = make(map[string]any) - opts.Metadata = meta - } - meta[cliproxyexecutor.PinnedAuthMetadataKey] = affinityAuthID - } - } - } + m.applyAuthAffinity(&opts) if !m.useSchedulerFastPath() { return m.pickNextLegacy(ctx, provider, model, opts, tried) } @@ -2504,18 +2507,7 @@ func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, m } func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) { - if pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata); pinnedAuthID == "" { - if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - if affinityAuthID := m.AuthAffinity(affinityKey); affinityAuthID != "" { - meta := opts.Metadata - if meta == nil { - meta = make(map[string]any) - opts.Metadata = meta - } - meta[cliproxyexecutor.PinnedAuthMetadataKey] = affinityAuthID - } - } - } + m.applyAuthAffinity(&opts) if !m.useSchedulerFastPath() { return m.pickNextMixedLegacy(ctx, providers, model, opts, tried) } From 26eca8b6ba66783cff8fd5747dc630b6e4d9bb14 Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 27 Mar 2026 18:27:33 +0200 Subject: [PATCH 14/21] fix(codex): preserve continuity and safe affinity fallback Restore Claude continuity after the continuity refactor, keep auth-affinity keys out of upstream Codex session identifiers, and only persist affinity after successful execution so retries can still rotate to healthy credentials when the first auth fails. --- internal/runtime/executor/codex_continuity.go | 3 -- internal/runtime/executor/codex_executor.go | 1 + .../executor/codex_executor_cache_test.go | 42 +++++++++++++++++++ .../executor/codex_websockets_executor.go | 1 + .../codex_websockets_executor_test.go | 20 +++++++++ sdk/cliproxy/auth/conductor.go | 33 +++++++-------- 6 files changed, 79 insertions(+), 21 deletions(-) diff --git a/internal/runtime/executor/codex_continuity.go b/internal/runtime/executor/codex_continuity.go index 3ebb721f..e2fa8de0 100644 --- a/internal/runtime/executor/codex_continuity.go +++ b/internal/runtime/executor/codex_continuity.go @@ -57,9 +57,6 @@ func resolveCodexContinuity(ctx context.Context, auth *cliproxyauth.Auth, req cl if executionSession := metadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey); executionSession != "" { return codexContinuity{Key: executionSession, Source: "execution_session"} } - if affinityKey := metadataString(opts.Metadata, codexAuthAffinityMetadataKey); affinityKey != "" { - return codexContinuity{Key: affinityKey, Source: "auth_affinity"} - } if ginCtx := ginContextFrom(ctx); ginCtx != nil { if ginCtx.Request != nil { if v := strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key")); v != "" { diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 766a081a..5f06ace2 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -612,6 +612,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, auth *cliproxyauth.Auth } setCodexCache(key, cache) } + continuity = codexContinuity{Key: cache.ID, Source: "claude_user_cache"} } } else if from == "openai-response" { promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key") diff --git a/internal/runtime/executor/codex_executor_cache_test.go b/internal/runtime/executor/codex_executor_cache_test.go index 116b06ff..8c61a22e 100644 --- a/internal/runtime/executor/codex_executor_cache_test.go +++ b/internal/runtime/executor/codex_executor_cache_test.go @@ -151,3 +151,45 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_FallsBackToStableAuthID( t.Fatalf("session_id = %q, want %q", got, expected) } } + +func TestCodexExecutorCacheHelper_ClaudePreservesCacheContinuity(t *testing.T) { + executor := &CodexExecutor{} + req := cliproxyexecutor.Request{ + Model: "claude-3-7-sonnet", + Payload: []byte(`{"metadata":{"user_id":"user-1"}}`), + } + rawJSON := []byte(`{"model":"gpt-5.4","stream":true}`) + + httpReq, continuity, err := executor.cacheHelper(context.Background(), nil, sdktranslator.FromString("claude"), "https://example.com/responses", req, cliproxyexecutor.Options{}, rawJSON) + if err != nil { + t.Fatalf("cacheHelper error: %v", err) + } + if continuity.Key == "" { + t.Fatal("continuity.Key = empty, want non-empty") + } + body, err := io.ReadAll(httpReq.Body) + if err != nil { + t.Fatalf("read request body: %v", err) + } + if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != continuity.Key { + t.Fatalf("prompt_cache_key = %q, want %q", got, continuity.Key) + } + if got := httpReq.Header.Get("session_id"); got != continuity.Key { + t.Fatalf("session_id = %q, want %q", got, continuity.Key) + } +} + +func TestResolveCodexContinuity_DoesNotForwardAuthAffinityKey(t *testing.T) { + req := cliproxyexecutor.Request{Payload: []byte(`{"model":"gpt-5.4"}`)} + opts := cliproxyexecutor.Options{Metadata: map[string]any{"auth_affinity_key": "principal:raw-client-secret"}} + auth := &cliproxyauth.Auth{ID: "codex-auth-1", Provider: "codex"} + + continuity := resolveCodexContinuity(context.Background(), auth, req, opts) + + if continuity.Source != "auth_id" { + t.Fatalf("continuity.Source = %q, want %q", continuity.Source, "auth_id") + } + if continuity.Key == "principal:raw-client-secret" { + t.Fatal("continuity.Key leaked raw auth affinity key") + } +} diff --git a/internal/runtime/executor/codex_websockets_executor.go b/internal/runtime/executor/codex_websockets_executor.go index d0dd22c3..50cc736d 100644 --- a/internal/runtime/executor/codex_websockets_executor.go +++ b/internal/runtime/executor/codex_websockets_executor.go @@ -783,6 +783,7 @@ func applyCodexPromptCacheHeaders(ctx context.Context, auth *cliproxyauth.Auth, } setCodexCache(key, cache) } + continuity = codexContinuity{Key: cache.ID, Source: "claude_user_cache"} } } else if from == "openai-response" { if promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key"); promptCacheKey.Exists() { diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index e86036bc..0a06982f 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -57,6 +57,26 @@ func TestApplyCodexPromptCacheHeaders_PreservesPromptCacheRetention(t *testing.T } } +func TestApplyCodexPromptCacheHeaders_ClaudePreservesContinuity(t *testing.T) { + req := cliproxyexecutor.Request{ + Model: "claude-3-7-sonnet", + Payload: []byte(`{"metadata":{"user_id":"user-1"}}`), + } + body := []byte(`{"model":"gpt-5.4","stream":true}`) + + updatedBody, headers, continuity := applyCodexPromptCacheHeaders(context.Background(), nil, sdktranslator.FromString("claude"), req, cliproxyexecutor.Options{}, body) + + if continuity.Key == "" { + t.Fatal("continuity.Key = empty, want non-empty") + } + if got := gjson.GetBytes(updatedBody, "prompt_cache_key").String(); got != continuity.Key { + t.Fatalf("prompt_cache_key = %q, want %q", got, continuity.Key) + } + if got := headers.Get("session_id"); got != continuity.Key { + t.Fatalf("session_id = %q, want %q", got, continuity.Key) + } +} + func TestApplyCodexWebsocketHeadersDefaultsToCurrentResponsesBeta(t *testing.T) { headers := applyCodexWebsocketHeaders(context.Background(), http.Header{}, nil, "", nil) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index d7736cf4..6ef13baa 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -1093,12 +1093,6 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req entry := logEntryWithRequestID(ctx) debugLogAuthSelection(entry, auth, provider, req.Model) publishSelectedAuthMetadata(opts.Metadata, auth.ID) - if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - m.SetAuthAffinity(affinityKey, auth.ID) - if log.IsLevelEnabled(log.DebugLevel) { - entry.Debugf("auth affinity pinned key=%s auth_id=%s provider=%s model=%s", affinityKey, auth.ID, provider, req.Model) - } - } tried[auth.ID] = struct{}{} execCtx := ctx @@ -1138,6 +1132,7 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req continue } m.MarkResult(execCtx, result) + m.persistAuthAffinity(entry, opts, auth.ID, provider, req.Model) return resp, nil } if authErr != nil { @@ -1177,12 +1172,6 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string, entry := logEntryWithRequestID(ctx) debugLogAuthSelection(entry, auth, provider, req.Model) publishSelectedAuthMetadata(opts.Metadata, auth.ID) - if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - m.SetAuthAffinity(affinityKey, auth.ID) - if log.IsLevelEnabled(log.DebugLevel) { - entry.Debugf("auth affinity pinned key=%s auth_id=%s provider=%s model=%s", affinityKey, auth.ID, provider, req.Model) - } - } tried[auth.ID] = struct{}{} execCtx := ctx @@ -1222,6 +1211,7 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string, continue } m.MarkResult(execCtx, result) + m.persistAuthAffinity(entry, opts, auth.ID, provider, req.Model) return resp, nil } if authErr != nil { @@ -1269,12 +1259,6 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string entry := logEntryWithRequestID(ctx) debugLogAuthSelection(entry, auth, provider, req.Model) publishSelectedAuthMetadata(opts.Metadata, auth.ID) - if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - m.SetAuthAffinity(affinityKey, auth.ID) - if log.IsLevelEnabled(log.DebugLevel) { - entry.Debugf("auth affinity pinned key=%s auth_id=%s provider=%s model=%s", affinityKey, auth.ID, provider, req.Model) - } - } tried[auth.ID] = struct{}{} execCtx := ctx @@ -1298,6 +1282,7 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string lastErr = errStream continue } + m.persistAuthAffinity(entry, opts, auth.ID, provider, req.Model) return streamResult, nil } } @@ -2285,6 +2270,18 @@ func (m *Manager) applyAuthAffinity(opts *cliproxyexecutor.Options) { } } +func (m *Manager) persistAuthAffinity(entry *log.Entry, opts cliproxyexecutor.Options, authID, provider, model string) { + if m == nil { + return + } + if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { + m.SetAuthAffinity(affinityKey, authID) + if entry != nil && log.IsLevelEnabled(log.DebugLevel) { + entry.Debugf("auth affinity pinned key=%s auth_id=%s provider=%s model=%s", affinityKey, authID, provider, model) + } + } +} + func (m *Manager) SetAuthAffinity(key, authID string) { key = strings.TrimSpace(key) authID = strings.TrimSpace(authID) From 4c4cbd44dab25856182b8dec8c887c519465e512 Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 27 Mar 2026 18:34:51 +0200 Subject: [PATCH 15/21] fix(auth): avoid leaking or over-persisting affinity keys Stop using one-shot idempotency keys as long-lived auth-affinity identifiers and remove raw affinity-key values from debug logs so sticky routing keeps its continuity benefits without creating avoidable memory growth or credential exposure risks. --- sdk/api/handlers/handlers.go | 2 -- sdk/cliproxy/auth/conductor.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 5fc1154e..420d1fcc 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -211,8 +211,6 @@ func requestExecutionMetadata(ctx context.Context) map[string]any { if executionSessionID := executionSessionIDFromContext(ctx); executionSessionID != "" { meta[coreexecutor.ExecutionSessionMetadataKey] = executionSessionID meta[authAffinityMetadataKey] = executionSessionID - } else if explicitIdempotencyKey != "" { - meta[authAffinityMetadataKey] = explicitIdempotencyKey } else if ctx != nil { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { if apiKey, exists := ginCtx.Get("apiKey"); exists && apiKey != nil { diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 6ef13baa..147b0ece 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -2277,7 +2277,7 @@ func (m *Manager) persistAuthAffinity(entry *log.Entry, opts cliproxyexecutor.Op if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { m.SetAuthAffinity(affinityKey, authID) if entry != nil && log.IsLevelEnabled(log.DebugLevel) { - entry.Debugf("auth affinity pinned key=%s auth_id=%s provider=%s model=%s", affinityKey, authID, provider, model) + entry.Debugf("auth affinity pinned auth_id=%s provider=%s model=%s", authID, provider, model) } } } From 6962e09dd945120760b3ae3166d9e9480a1afd7a Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 27 Mar 2026 18:52:58 +0200 Subject: [PATCH 16/21] fix(auth): scope affinity by provider Keep sticky auth affinity limited to matching providers and stop persisting execution-session IDs as long-lived affinity keys so provider switching and normal streaming traffic do not create incorrect pins or stale affinity state. --- sdk/api/handlers/handlers.go | 1 - sdk/cliproxy/auth/conductor.go | 40 +++++++++++++++----- sdk/cliproxy/auth/conductor_affinity_test.go | 25 +++++++++--- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 420d1fcc..69dd6007 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -210,7 +210,6 @@ func requestExecutionMetadata(ctx context.Context) map[string]any { } if executionSessionID := executionSessionIDFromContext(ctx); executionSessionID != "" { meta[coreexecutor.ExecutionSessionMetadataKey] = executionSessionID - meta[authAffinityMetadataKey] = executionSessionID } else if ctx != nil { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { if apiKey, exists := ginCtx.Get("apiKey"); exists && apiKey != nil { diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 147b0ece..5c38654b 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -2246,8 +2246,17 @@ func authAffinityKeyFromMetadata(meta map[string]any) string { } } -func (m *Manager) AuthAffinity(key string) string { +func scopedAuthAffinityKey(provider, key string) string { + provider = strings.TrimSpace(strings.ToLower(provider)) key = strings.TrimSpace(key) + if provider == "" || key == "" { + return "" + } + return provider + "|" + key +} + +func (m *Manager) AuthAffinity(provider, key string) string { + key = scopedAuthAffinityKey(provider, key) if m == nil || key == "" { return "" } @@ -2256,12 +2265,12 @@ func (m *Manager) AuthAffinity(key string) string { return strings.TrimSpace(m.affinity[key]) } -func (m *Manager) applyAuthAffinity(opts *cliproxyexecutor.Options) { +func (m *Manager) applyAuthAffinity(provider string, opts *cliproxyexecutor.Options) { if m == nil || opts == nil || pinnedAuthIDFromMetadata(opts.Metadata) != "" { return } if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - if affinityAuthID := m.AuthAffinity(affinityKey); affinityAuthID != "" { + if affinityAuthID := m.AuthAffinity(provider, affinityKey); affinityAuthID != "" { if opts.Metadata == nil { opts.Metadata = make(map[string]any) } @@ -2275,15 +2284,15 @@ func (m *Manager) persistAuthAffinity(entry *log.Entry, opts cliproxyexecutor.Op return } if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - m.SetAuthAffinity(affinityKey, authID) + m.SetAuthAffinity(provider, affinityKey, authID) if entry != nil && log.IsLevelEnabled(log.DebugLevel) { entry.Debugf("auth affinity pinned auth_id=%s provider=%s model=%s", authID, provider, model) } } } -func (m *Manager) SetAuthAffinity(key, authID string) { - key = strings.TrimSpace(key) +func (m *Manager) SetAuthAffinity(provider, key, authID string) { + key = scopedAuthAffinityKey(provider, key) authID = strings.TrimSpace(authID) if m == nil || key == "" || authID == "" { return @@ -2296,8 +2305,8 @@ func (m *Manager) SetAuthAffinity(key, authID string) { m.affinityMu.Unlock() } -func (m *Manager) ClearAuthAffinity(key string) { - key = strings.TrimSpace(key) +func (m *Manager) ClearAuthAffinity(provider, key string) { + key = scopedAuthAffinityKey(provider, key) if m == nil || key == "" { return } @@ -2389,7 +2398,7 @@ func (m *Manager) pickNextLegacy(ctx context.Context, provider, model string, op } func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) { - m.applyAuthAffinity(&opts) + m.applyAuthAffinity(provider, &opts) if !m.useSchedulerFastPath() { return m.pickNextLegacy(ctx, provider, model, opts, tried) } @@ -2504,7 +2513,18 @@ func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, m } func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) { - m.applyAuthAffinity(&opts) + if pinnedAuthIDFromMetadata(opts.Metadata) == "" { + for _, provider := range providers { + providerKey := strings.TrimSpace(strings.ToLower(provider)) + if providerKey == "" { + continue + } + m.applyAuthAffinity(providerKey, &opts) + if pinnedAuthIDFromMetadata(opts.Metadata) != "" { + break + } + } + } if !m.useSchedulerFastPath() { return m.pickNextMixedLegacy(ctx, providers, model, opts, tried) } diff --git a/sdk/cliproxy/auth/conductor_affinity_test.go b/sdk/cliproxy/auth/conductor_affinity_test.go index e84f7c96..363e2367 100644 --- a/sdk/cliproxy/auth/conductor_affinity_test.go +++ b/sdk/cliproxy/auth/conductor_affinity_test.go @@ -52,7 +52,7 @@ func TestManagerPickNextMixedUsesAuthAffinity(t *testing.T) { t.Fatalf("Register(codex-b) error = %v", errRegister) } - manager.SetAuthAffinity("idem-1", "codex-b") + manager.SetAuthAffinity("codex", "idem-1", "codex-b") opts := cliproxyexecutor.Options{Metadata: map[string]any{"auth_affinity_key": "idem-1"}} got, _, provider, errPick := manager.pickNextMixed(context.Background(), []string{"codex"}, "gpt-5.4", opts, map[string]struct{}{}) @@ -74,12 +74,27 @@ func TestManagerAuthAffinityRoundTrip(t *testing.T) { t.Parallel() manager := NewManager(nil, nil, nil) - manager.SetAuthAffinity("idem-2", "auth-1") - if got := manager.AuthAffinity("idem-2"); got != "auth-1" { + manager.SetAuthAffinity("codex", "idem-2", "auth-1") + if got := manager.AuthAffinity("codex", "idem-2"); got != "auth-1" { t.Fatalf("AuthAffinity = %q, want %q", got, "auth-1") } - manager.ClearAuthAffinity("idem-2") - if got := manager.AuthAffinity("idem-2"); got != "" { + manager.ClearAuthAffinity("codex", "idem-2") + if got := manager.AuthAffinity("codex", "idem-2"); got != "" { t.Fatalf("AuthAffinity after clear = %q, want empty", got) } } + +func TestManagerAuthAffinityScopedByProvider(t *testing.T) { + t.Parallel() + + manager := NewManager(nil, nil, nil) + manager.SetAuthAffinity("codex", "shared-key", "codex-auth") + manager.SetAuthAffinity("gemini", "shared-key", "gemini-auth") + + if got := manager.AuthAffinity("codex", "shared-key"); got != "codex-auth" { + t.Fatalf("codex affinity = %q, want %q", got, "codex-auth") + } + if got := manager.AuthAffinity("gemini", "shared-key"); got != "gemini-auth" { + t.Fatalf("gemini affinity = %q, want %q", got, "gemini-auth") + } +} From 35f158d5261ee6cb7b511ade41bddbebdc08c4b8 Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 27 Mar 2026 19:06:34 +0200 Subject: [PATCH 17/21] refactor(pr): narrow Codex cache fix scope Remove the experimental auth-affinity routing changes from this PR so it stays focused on the validated Codex continuity fix. This keeps the prompt-cache repair while avoiding unrelated routing-policy concerns such as provider/model affinity scope, lifecycle cleanup, and hard-pin fallback semantics. --- sdk/api/handlers/handlers.go | 29 +---- sdk/cliproxy/auth/conductor.go | 120 ++----------------- sdk/cliproxy/auth/conductor_affinity_test.go | 100 ---------------- 3 files changed, 8 insertions(+), 241 deletions(-) delete mode 100644 sdk/cliproxy/auth/conductor_affinity_test.go diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 69dd6007..28ab970d 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -46,7 +46,6 @@ type ErrorDetail struct { } const idempotencyKeyMetadataKey = "idempotency_key" -const authAffinityMetadataKey = "auth_affinity_key" const ( defaultStreamingKeepAliveSeconds = 0 @@ -190,11 +189,9 @@ func requestExecutionMetadata(ctx context.Context) map[string]any { // Idempotency-Key is an optional client-supplied header used to correlate retries. // It is forwarded as execution metadata; when absent we generate a UUID. key := "" - explicitIdempotencyKey := "" if ctx != nil { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { - explicitIdempotencyKey = strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key")) - key = explicitIdempotencyKey + key = strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key")) } } if key == "" { @@ -210,34 +207,10 @@ func requestExecutionMetadata(ctx context.Context) map[string]any { } if executionSessionID := executionSessionIDFromContext(ctx); executionSessionID != "" { meta[coreexecutor.ExecutionSessionMetadataKey] = executionSessionID - } else if ctx != nil { - if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { - if apiKey, exists := ginCtx.Get("apiKey"); exists && apiKey != nil { - if principal := stablePrincipalMetadataKey(apiKey); principal != "" { - meta[authAffinityMetadataKey] = principal - } - } - } } return meta } -func stablePrincipalMetadataKey(raw any) string { - var keyStr string - switch v := raw.(type) { - case string: - keyStr = v - case fmt.Stringer: - keyStr = v.String() - default: - keyStr = fmt.Sprintf("%v", raw) - } - if trimmed := strings.TrimSpace(keyStr); trimmed != "" { - return "principal:" + trimmed - } - return "" -} - func pinnedAuthIDFromContext(ctx context.Context) string { if ctx == nil { return "" diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 5c38654b..9f46c7cf 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -128,15 +128,13 @@ func (NoopHook) OnResult(context.Context, Result) {} // Manager orchestrates auth lifecycle, selection, execution, and persistence. type Manager struct { - store Store - executors map[string]ProviderExecutor - selector Selector - hook Hook - mu sync.RWMutex - auths map[string]*Auth - scheduler *authScheduler - affinityMu sync.RWMutex - affinity map[string]string + store Store + executors map[string]ProviderExecutor + selector Selector + hook Hook + mu sync.RWMutex + auths map[string]*Auth + scheduler *authScheduler // providerOffsets tracks per-model provider rotation state for multi-provider routing. providerOffsets map[string]int @@ -181,7 +179,6 @@ func NewManager(store Store, selector Selector, hook Hook) *Manager { selector: selector, hook: hook, auths: make(map[string]*Auth), - affinity: make(map[string]string), providerOffsets: make(map[string]int), modelPoolOffsets: make(map[string]int), refreshSemaphore: make(chan struct{}, refreshMaxConcurrency), @@ -1132,7 +1129,6 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req continue } m.MarkResult(execCtx, result) - m.persistAuthAffinity(entry, opts, auth.ID, provider, req.Model) return resp, nil } if authErr != nil { @@ -1211,7 +1207,6 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string, continue } m.MarkResult(execCtx, result) - m.persistAuthAffinity(entry, opts, auth.ID, provider, req.Model) return resp, nil } if authErr != nil { @@ -1282,7 +1277,6 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string lastErr = errStream continue } - m.persistAuthAffinity(entry, opts, auth.ID, provider, req.Model) return streamResult, nil } } @@ -2228,93 +2222,6 @@ func (m *Manager) CloseExecutionSession(sessionID string) { } } -func authAffinityKeyFromMetadata(meta map[string]any) string { - if len(meta) == 0 { - return "" - } - raw, ok := meta["auth_affinity_key"] - if !ok || raw == nil { - return "" - } - switch val := raw.(type) { - case string: - return strings.TrimSpace(val) - case []byte: - return strings.TrimSpace(string(val)) - default: - return "" - } -} - -func scopedAuthAffinityKey(provider, key string) string { - provider = strings.TrimSpace(strings.ToLower(provider)) - key = strings.TrimSpace(key) - if provider == "" || key == "" { - return "" - } - return provider + "|" + key -} - -func (m *Manager) AuthAffinity(provider, key string) string { - key = scopedAuthAffinityKey(provider, key) - if m == nil || key == "" { - return "" - } - m.affinityMu.RLock() - defer m.affinityMu.RUnlock() - return strings.TrimSpace(m.affinity[key]) -} - -func (m *Manager) applyAuthAffinity(provider string, opts *cliproxyexecutor.Options) { - if m == nil || opts == nil || pinnedAuthIDFromMetadata(opts.Metadata) != "" { - return - } - if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - if affinityAuthID := m.AuthAffinity(provider, affinityKey); affinityAuthID != "" { - if opts.Metadata == nil { - opts.Metadata = make(map[string]any) - } - opts.Metadata[cliproxyexecutor.PinnedAuthMetadataKey] = affinityAuthID - } - } -} - -func (m *Manager) persistAuthAffinity(entry *log.Entry, opts cliproxyexecutor.Options, authID, provider, model string) { - if m == nil { - return - } - if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { - m.SetAuthAffinity(provider, affinityKey, authID) - if entry != nil && log.IsLevelEnabled(log.DebugLevel) { - entry.Debugf("auth affinity pinned auth_id=%s provider=%s model=%s", authID, provider, model) - } - } -} - -func (m *Manager) SetAuthAffinity(provider, key, authID string) { - key = scopedAuthAffinityKey(provider, key) - authID = strings.TrimSpace(authID) - if m == nil || key == "" || authID == "" { - return - } - m.affinityMu.Lock() - if m.affinity == nil { - m.affinity = make(map[string]string) - } - m.affinity[key] = authID - m.affinityMu.Unlock() -} - -func (m *Manager) ClearAuthAffinity(provider, key string) { - key = scopedAuthAffinityKey(provider, key) - if m == nil || key == "" { - return - } - m.affinityMu.Lock() - delete(m.affinity, key) - m.affinityMu.Unlock() -} - func (m *Manager) useSchedulerFastPath() bool { if m == nil || m.scheduler == nil { return false @@ -2398,7 +2305,6 @@ func (m *Manager) pickNextLegacy(ctx context.Context, provider, model string, op } func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) { - m.applyAuthAffinity(provider, &opts) if !m.useSchedulerFastPath() { return m.pickNextLegacy(ctx, provider, model, opts, tried) } @@ -2513,18 +2419,6 @@ func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, m } func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) { - if pinnedAuthIDFromMetadata(opts.Metadata) == "" { - for _, provider := range providers { - providerKey := strings.TrimSpace(strings.ToLower(provider)) - if providerKey == "" { - continue - } - m.applyAuthAffinity(providerKey, &opts) - if pinnedAuthIDFromMetadata(opts.Metadata) != "" { - break - } - } - } if !m.useSchedulerFastPath() { return m.pickNextMixedLegacy(ctx, providers, model, opts, tried) } diff --git a/sdk/cliproxy/auth/conductor_affinity_test.go b/sdk/cliproxy/auth/conductor_affinity_test.go deleted file mode 100644 index 363e2367..00000000 --- a/sdk/cliproxy/auth/conductor_affinity_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package auth - -import ( - "context" - "net/http" - "testing" - - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" -) - -type affinityTestExecutor struct{ id string } - -func (e affinityTestExecutor) Identifier() string { return e.id } - -func (e affinityTestExecutor) Execute(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { - return cliproxyexecutor.Response{}, nil -} - -func (e affinityTestExecutor) ExecuteStream(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) { - ch := make(chan cliproxyexecutor.StreamChunk) - close(ch) - return &cliproxyexecutor.StreamResult{Chunks: ch}, nil -} - -func (e affinityTestExecutor) Refresh(_ context.Context, auth *Auth) (*Auth, error) { return auth, nil } - -func (e affinityTestExecutor) CountTokens(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { - return cliproxyexecutor.Response{}, nil -} - -func (e affinityTestExecutor) HttpRequest(context.Context, *Auth, *http.Request) (*http.Response, error) { - return nil, nil -} - -func TestManagerPickNextMixedUsesAuthAffinity(t *testing.T) { - t.Parallel() - - manager := NewManager(nil, &RoundRobinSelector{}, nil) - manager.executors["codex"] = affinityTestExecutor{id: "codex"} - reg := registry.GetGlobalRegistry() - reg.RegisterClient("codex-a", "codex", []*registry.ModelInfo{{ID: "gpt-5.4"}}) - reg.RegisterClient("codex-b", "codex", []*registry.ModelInfo{{ID: "gpt-5.4"}}) - t.Cleanup(func() { - reg.UnregisterClient("codex-a") - reg.UnregisterClient("codex-b") - }) - if _, errRegister := manager.Register(context.Background(), &Auth{ID: "codex-a", Provider: "codex"}); errRegister != nil { - t.Fatalf("Register(codex-a) error = %v", errRegister) - } - if _, errRegister := manager.Register(context.Background(), &Auth{ID: "codex-b", Provider: "codex"}); errRegister != nil { - t.Fatalf("Register(codex-b) error = %v", errRegister) - } - - manager.SetAuthAffinity("codex", "idem-1", "codex-b") - opts := cliproxyexecutor.Options{Metadata: map[string]any{"auth_affinity_key": "idem-1"}} - - got, _, provider, errPick := manager.pickNextMixed(context.Background(), []string{"codex"}, "gpt-5.4", opts, map[string]struct{}{}) - if errPick != nil { - t.Fatalf("pickNextMixed() error = %v", errPick) - } - if provider != "codex" { - t.Fatalf("provider = %q, want %q", provider, "codex") - } - if got == nil || got.ID != "codex-b" { - t.Fatalf("auth.ID = %v, want codex-b", got) - } - if pinned := pinnedAuthIDFromMetadata(opts.Metadata); pinned != "codex-b" { - t.Fatalf("pinned auth metadata = %q, want %q", pinned, "codex-b") - } -} - -func TestManagerAuthAffinityRoundTrip(t *testing.T) { - t.Parallel() - - manager := NewManager(nil, nil, nil) - manager.SetAuthAffinity("codex", "idem-2", "auth-1") - if got := manager.AuthAffinity("codex", "idem-2"); got != "auth-1" { - t.Fatalf("AuthAffinity = %q, want %q", got, "auth-1") - } - manager.ClearAuthAffinity("codex", "idem-2") - if got := manager.AuthAffinity("codex", "idem-2"); got != "" { - t.Fatalf("AuthAffinity after clear = %q, want empty", got) - } -} - -func TestManagerAuthAffinityScopedByProvider(t *testing.T) { - t.Parallel() - - manager := NewManager(nil, nil, nil) - manager.SetAuthAffinity("codex", "shared-key", "codex-auth") - manager.SetAuthAffinity("gemini", "shared-key", "gemini-auth") - - if got := manager.AuthAffinity("codex", "shared-key"); got != "codex-auth" { - t.Fatalf("codex affinity = %q, want %q", got, "codex-auth") - } - if got := manager.AuthAffinity("gemini", "shared-key"); got != "gemini-auth" { - t.Fatalf("gemini affinity = %q, want %q", got, "gemini-auth") - } -} From 79755e76ea1d503f0a586091c851a45700d7364a Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 27 Mar 2026 19:34:13 +0200 Subject: [PATCH 18/21] refactor(pr): remove forbidden translator changes Drop the chat-completions translator edits from this PR so the branch complies with the repository policy that forbids pull-request changes under internal/translator. The remaining PR stays focused on the executor-level Codex continuity fix that was validated to restore cache reuse. --- .../chat-completions/codex_openai_request.go | 3 --- .../codex_openai_request_test.go | 16 ---------------- 2 files changed, 19 deletions(-) diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_request.go b/internal/translator/codex/openai/chat-completions/codex_openai_request.go index 7d24d60e..6cc701e7 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_request.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request.go @@ -65,9 +65,6 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b // Model out, _ = sjson.SetBytes(out, "model", modelName) - if v := gjson.GetBytes(rawJSON, "prompt_cache_retention"); v.Exists() { - out, _ = sjson.SetBytes(out, "prompt_cache_retention", v.Value()) - } // Build tool name shortening map from original tools (if any) originalToolNameMap := map[string]string{} diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_request_test.go b/internal/translator/codex/openai/chat-completions/codex_openai_request_test.go index 1202980f..84c8dad2 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_request_test.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request_test.go @@ -633,19 +633,3 @@ func TestToolsDefinitionTranslated(t *testing.T) { t.Errorf("tool 'search' not found in output tools: %s", gjson.Get(result, "tools").Raw) } } - -func TestPromptCacheRetentionPreserved(t *testing.T) { - input := []byte(`{ - "model": "gpt-4o", - "prompt_cache_retention": "persistent", - "messages": [ - {"role": "user", "content": "Hello"} - ] - }`) - - out := ConvertOpenAIRequestToCodex("gpt-4o", input, true) - - if got := gjson.GetBytes(out, "prompt_cache_retention").String(); got != "persistent" { - t.Fatalf("prompt_cache_retention = %q, want %q", got, "persistent") - } -} From e5d3541b5a70bdc9fcc65a69cf26f1986e57d35b Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 27 Mar 2026 20:40:26 +0200 Subject: [PATCH 19/21] refactor(codex): remove stale affinity cleanup leftovers Drop the last affinity-related executor artifacts so the PR stays focused on the minimal Codex continuity fix set: stable prompt cache identity, stable session_id, and the executor-only behavior that was validated to restore cache reads. --- internal/runtime/executor/codex_continuity.go | 2 -- .../runtime/executor/codex_executor_cache_test.go | 15 --------------- 2 files changed, 17 deletions(-) diff --git a/internal/runtime/executor/codex_continuity.go b/internal/runtime/executor/codex_continuity.go index e2fa8de0..9a0cd1b4 100644 --- a/internal/runtime/executor/codex_continuity.go +++ b/internal/runtime/executor/codex_continuity.go @@ -14,8 +14,6 @@ import ( "github.com/tidwall/sjson" ) -const codexAuthAffinityMetadataKey = "auth_affinity_key" - type codexContinuity struct { Key string Source string diff --git a/internal/runtime/executor/codex_executor_cache_test.go b/internal/runtime/executor/codex_executor_cache_test.go index 8c61a22e..f6def7ae 100644 --- a/internal/runtime/executor/codex_executor_cache_test.go +++ b/internal/runtime/executor/codex_executor_cache_test.go @@ -178,18 +178,3 @@ func TestCodexExecutorCacheHelper_ClaudePreservesCacheContinuity(t *testing.T) { t.Fatalf("session_id = %q, want %q", got, continuity.Key) } } - -func TestResolveCodexContinuity_DoesNotForwardAuthAffinityKey(t *testing.T) { - req := cliproxyexecutor.Request{Payload: []byte(`{"model":"gpt-5.4"}`)} - opts := cliproxyexecutor.Options{Metadata: map[string]any{"auth_affinity_key": "principal:raw-client-secret"}} - auth := &cliproxyauth.Auth{ID: "codex-auth-1", Provider: "codex"} - - continuity := resolveCodexContinuity(context.Background(), auth, req, opts) - - if continuity.Source != "auth_id" { - t.Fatalf("continuity.Source = %q, want %q", continuity.Source, "auth_id") - } - if continuity.Key == "principal:raw-client-secret" { - t.Fatal("continuity.Key leaked raw auth affinity key") - } -} From e41c22ef446544da9fdfcebdcb9b55e21b204638 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sun, 29 Mar 2026 12:23:37 +0800 Subject: [PATCH 20/21] docs(readme): add LingtrueAPI sponsorship details to all README translations --- README.md | 4 ++++ README_CN.md | 4 ++++ README_JA.md | 4 ++++ assets/lingtrue.png | Bin 0 -> 131878 bytes 4 files changed, 12 insertions(+) create mode 100644 assets/lingtrue.png diff --git a/README.md b/README.md index 867f4b28..7db25311 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB BmoPlus Huge thanks to BmoPlus for sponsoring this project! BmoPlus is a highly reliable AI account provider built strictly for heavy AI users and developers. They offer rock-solid, ready-to-use accounts and official top-up services for ChatGPT Plus / ChatGPT Pro (Full Warranty) / Claude Pro / Super Grok / Gemini Pro. By registering and ordering through BmoPlus - Premium AI Accounts & Top-ups, users can unlock the mind-blowing rate of 10% of the official GPT subscription price (90% OFF)! + +LingtrueAPI +Thanks to LingtrueAPI for its sponsorship of this project! LingtrueAPI is a global large - model API intermediary service platform that provides API calling services for various top - notch models such as Claude Code, Codex, and Gemini. It is committed to enabling users to connect to global AI capabilities at low cost and with high stability. LingtrueAPI offers special discounts to users of this software: register using this link, and enter the promo code "LingtrueAPI" when making the first recharge to enjoy a 10% discount. + diff --git a/README_CN.md b/README_CN.md index 28179194..282b85e2 100644 --- a/README_CN.md +++ b/README_CN.md @@ -34,6 +34,10 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元 BmoPlus 感谢 BmoPlus 赞助了本项目!BmoPlus 是一家专为AI订阅重度用户打造的可靠 AI 账号代充服务商,提供稳定的 ChatGPT Plus / ChatGPT Pro(全程质保) / Claude Pro / Super Grok / Gemini Pro 的官方代充&成品账号。 通过BmoPlus AI成品号专卖/代充注册下单的用户,可享GPT 官网订阅一折 的震撼价格! + +LingtrueAPI +感谢 LingtrueAPI 对本项目的赞助!LingtrueAPI 是一家全球大模型API中转服务平台,提供Claude Code、Codex、Gemini 等多种顶级模型API调用服务,致力于让用户以低成本、高稳定性链接全球AI能力。LingtrueAPI为本软件用户提供了特别优惠:使用此链接注册,并在首次充值时输入 "LingtrueAPI" 优惠码即可享受9折优惠。 + diff --git a/README_JA.md b/README_JA.md index 41a438eb..19e7d42a 100644 --- a/README_JA.md +++ b/README_JA.md @@ -34,6 +34,10 @@ GLM CODING PLANを10%割引で取得:https://z.ai/subscribe?ic=8JVLJQFSKB BmoPlus 本プロジェクトにご支援いただいた BmoPlus に感謝いたします!BmoPlusは、AIサブスクリプションのヘビーユーザー向けに特化した信頼性の高いAIアカウントサービスプロバイダーであり、安定した ChatGPT Plus / ChatGPT Pro (完全保証) / Claude Pro / Super Grok / Gemini Pro の公式代行チャージおよび即納アカウントを提供しています。こちらのBmoPlus AIアカウント専門店/代行チャージ経由でご登録・ご注文いただいたユーザー様は、GPTを 公式サイト価格の約1割(90% OFF) という驚異的な価格でご利用いただけます! + +LingtrueAPI +LingtrueAPIのスポンサーシップに感謝します!LingtrueAPIはグローバルな大規模モデルAPIリレーサービスプラットフォームで、Claude Code、Codex、GeminiなどのトップモデルAPI呼び出しサービスを提供し、ユーザーが低コストかつ高い安定性で世界中のAI能力に接続できるよう支援しています。LingtrueAPIは本ソフトウェアのユーザーに特別割引を提供しています:こちらのリンクから登録し、初回チャージ時にプロモーションコード「LingtrueAPI」を入力すると10%割引になります。 + diff --git a/assets/lingtrue.png b/assets/lingtrue.png new file mode 100644 index 0000000000000000000000000000000000000000..2ab1a40bd1a0bc1184ecba1b4948d083168690a4 GIT binary patch literal 131878 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

vzlF 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 Date: Sun, 29 Mar 2026 12:30:24 +0800 Subject: [PATCH 21/21] docs(readme): update LingtrueAPI link in all README translations --- README.md | 2 +- README_CN.md | 2 +- README_JA.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7db25311..40d8c595 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB Huge thanks to BmoPlus for sponsoring this project! BmoPlus is a highly reliable AI account provider built strictly for heavy AI users and developers. They offer rock-solid, ready-to-use accounts and official top-up services for ChatGPT Plus / ChatGPT Pro (Full Warranty) / Claude Pro / Super Grok / Gemini Pro. By registering and ordering through BmoPlus - Premium AI Accounts & Top-ups, users can unlock the mind-blowing rate of 10% of the official GPT subscription price (90% OFF)! -LingtrueAPI +LingtrueAPI Thanks to LingtrueAPI for its sponsorship of this project! LingtrueAPI is a global large - model API intermediary service platform that provides API calling services for various top - notch models such as Claude Code, Codex, and Gemini. It is committed to enabling users to connect to global AI capabilities at low cost and with high stability. LingtrueAPI offers special discounts to users of this software: register using this link, and enter the promo code "LingtrueAPI" when making the first recharge to enjoy a 10% discount. diff --git a/README_CN.md b/README_CN.md index 282b85e2..618b86dd 100644 --- a/README_CN.md +++ b/README_CN.md @@ -35,7 +35,7 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元 感谢 BmoPlus 赞助了本项目!BmoPlus 是一家专为AI订阅重度用户打造的可靠 AI 账号代充服务商,提供稳定的 ChatGPT Plus / ChatGPT Pro(全程质保) / Claude Pro / Super Grok / Gemini Pro 的官方代充&成品账号。 通过BmoPlus AI成品号专卖/代充注册下单的用户,可享GPT 官网订阅一折 的震撼价格! -LingtrueAPI +LingtrueAPI 感谢 LingtrueAPI 对本项目的赞助!LingtrueAPI 是一家全球大模型API中转服务平台,提供Claude Code、Codex、Gemini 等多种顶级模型API调用服务,致力于让用户以低成本、高稳定性链接全球AI能力。LingtrueAPI为本软件用户提供了特别优惠:使用此链接注册,并在首次充值时输入 "LingtrueAPI" 优惠码即可享受9折优惠。 diff --git a/README_JA.md b/README_JA.md index 19e7d42a..d1b64ba7 100644 --- a/README_JA.md +++ b/README_JA.md @@ -35,7 +35,7 @@ GLM CODING PLANを10%割引で取得:https://z.ai/subscribe?ic=8JVLJQFSKB 本プロジェクトにご支援いただいた BmoPlus に感謝いたします!BmoPlusは、AIサブスクリプションのヘビーユーザー向けに特化した信頼性の高いAIアカウントサービスプロバイダーであり、安定した ChatGPT Plus / ChatGPT Pro (完全保証) / Claude Pro / Super Grok / Gemini Pro の公式代行チャージおよび即納アカウントを提供しています。こちらのBmoPlus AIアカウント専門店/代行チャージ経由でご登録・ご注文いただいたユーザー様は、GPTを 公式サイト価格の約1割(90% OFF) という驚異的な価格でご利用いただけます! -LingtrueAPI +LingtrueAPI LingtrueAPIのスポンサーシップに感謝します!LingtrueAPIはグローバルな大規模モデルAPIリレーサービスプラットフォームで、Claude Code、Codex、GeminiなどのトップモデルAPI呼び出しサービスを提供し、ユーザーが低コストかつ高い安定性で世界中のAI能力に接続できるよう支援しています。LingtrueAPIは本ソフトウェアのユーザーに特別割引を提供しています:こちらのリンクから登録し、初回チャージ時にプロモーションコード「LingtrueAPI」を入力すると10%割引になります。