diff --git a/internal/runtime/executor/antigravity_executor_execute.go b/internal/runtime/executor/antigravity_executor_execute.go index fb9c84d69..4fd9c6dbc 100644 --- a/internal/runtime/executor/antigravity_executor_execute.go +++ b/internal/runtime/executor/antigravity_executor_execute.go @@ -66,8 +66,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au auth = updatedAuth reporter.UpdateAccessTokenFingerprint(auth) } - originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, false) - translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false) + originalTranslated, translated := helps.TranslateRequestPairWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, req.Payload, false) translated, err = helps.ApplyThinkingWithSourcePayload(translated, req.Payload, originalPayloadSource, req.Model, from.String(), to.String(), e.Identifier()) if err != nil { @@ -290,8 +289,7 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth * auth = updatedAuth reporter.UpdateAccessTokenFingerprint(auth) } - originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true) - translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true) + originalTranslated, translated := helps.TranslateRequestPairWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, req.Payload, true) translated, err = helps.ApplyThinkingWithSourcePayload(translated, req.Payload, originalPayloadSource, req.Model, from.String(), to.String(), e.Identifier()) if err != nil { diff --git a/internal/runtime/executor/antigravity_executor_stream.go b/internal/runtime/executor/antigravity_executor_stream.go index 876a757d1..30b6d4a6b 100644 --- a/internal/runtime/executor/antigravity_executor_stream.go +++ b/internal/runtime/executor/antigravity_executor_stream.go @@ -61,8 +61,7 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya reporter.UpdateAccessTokenFingerprint(auth) } - originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true) - translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true) + originalTranslated, translated := helps.TranslateRequestPairWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, req.Payload, true) translated, err = helps.ApplyThinkingWithSourcePayload(translated, req.Payload, originalPayloadSource, req.Model, from.String(), to.String(), e.Identifier()) if err != nil { diff --git a/internal/runtime/executor/helps/codex_multi_agent_v2.go b/internal/runtime/executor/helps/codex_multi_agent_v2.go index fe4d5a328..4436ca102 100644 --- a/internal/runtime/executor/helps/codex_multi_agent_v2.go +++ b/internal/runtime/executor/helps/codex_multi_agent_v2.go @@ -35,6 +35,35 @@ func TranslateRequestWithCodexMultiAgentV2(ctx context.Context, headers http.Hea return multiagentv2.TranslateRequestWithCodexMultiAgentV2(ctx, headers, cfg, from, to, model, payload, stream) } +// TranslateRequestPairWithCodexMultiAgentV2 translates the untouched baseline +// payload and the working payload that later stages mutate in place. Executors +// normally assign the original payload to the request before translating, so both +// translations would rescan the same bytes and produce the same result. Request +// translation is deterministic and never aliases its input, so that case is +// translated once and duplicated, which removes a full extra pass over payloads +// that can reach tens of megabytes. +func TranslateRequestPairWithCodexMultiAgentV2(ctx context.Context, headers http.Header, cfg *config.Config, from, to sdktranslator.Format, model string, originalPayload, requestPayload []byte, stream bool) (original, working []byte) { + original = TranslateRequestWithCodexMultiAgentV2(ctx, headers, cfg, from, to, model, originalPayload, stream) + if sameByteSlice(originalPayload, requestPayload) { + // The caller mutates the working copy, so it must not share the baseline array. + return original, append([]byte(nil), original...) + } + return original, TranslateRequestWithCodexMultiAgentV2(ctx, headers, cfg, from, to, model, requestPayload, stream) +} + +// sameByteSlice reports whether both slices describe the same bytes of the same +// backing array. It compares identity rather than content so the check stays +// constant time on large payloads. +func sameByteSlice(a, b []byte) bool { + if len(a) != len(b) { + return false + } + if len(a) == 0 { + return true + } + return &a[0] == &b[0] +} + // TranslateRequestWithAPIKeyModelCompatibility applies compatibility-aware // request translators when a configured API-key model enables compatibility mode. func TranslateRequestWithAPIKeyModelCompatibility(ctx context.Context, headers http.Header, cfg *config.Config, from, to sdktranslator.Format, model string, payload []byte, stream, isCompat bool) []byte { diff --git a/internal/runtime/executor/helps/codex_multi_agent_v2_test.go b/internal/runtime/executor/helps/codex_multi_agent_v2_test.go new file mode 100644 index 000000000..321927337 --- /dev/null +++ b/internal/runtime/executor/helps/codex_multi_agent_v2_test.go @@ -0,0 +1,125 @@ +package helps + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" +) + +func geminiToolHistoryPayload(turns int) []byte { + contents := []string{`{"role":"user","parts":[{"text":"start"}]}`} + for i := 0; i < turns; i++ { + contents = append(contents, + fmt.Sprintf(`{"role":"user","parts":[{"text":"ask %d"}]}`, i), + fmt.Sprintf(`{"role":"model","parts":[{"text":"think %d"},{"thoughtSignature":"sig-%d","functionCall":{"id":"c%d","name":"read_file","args":{"path":"a%d.go"}}}]}`, i, i, i, i), + fmt.Sprintf(`{"role":"user","parts":[{"functionResponse":{"id":"c%d","name":"read_file","response":{"content":"data %d"}}}]}`, i, i), + fmt.Sprintf(`{"role":"model","parts":[{"text":"answer %d"}]}`, i)) + } + return []byte(fmt.Sprintf( + `{"contents":[%s],"tools":[{"functionDeclarations":[{"name":"read_file","description":"read a file","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}]}],"generationConfig":{"temperature":1}}`, + strings.Join(contents, ","))) +} + +// TestTranslateRequestPairMatchesSeparateTranslations pins the reuse fast path to +// the behavior of translating both payloads independently. +func TestTranslateRequestPairMatchesSeparateTranslations(t *testing.T) { + from := sdktranslator.FormatGemini + to := sdktranslator.FromString("antigravity") + cfg := &config.Config{} + const model = "gemini-3.6-flash-high" + + for _, turns := range []int{0, 1, 5, 20} { + payload := geminiToolHistoryPayload(turns) + // Same bytes in a different backing array forces the translate-twice branch. + detached := append([]byte(nil), payload...) + + want := TranslateRequestWithCodexMultiAgentV2(context.Background(), http.Header{}, cfg, from, to, model, payload, true) + + reuseBase, reuseWork := TranslateRequestPairWithCodexMultiAgentV2( + context.Background(), http.Header{}, cfg, from, to, model, payload, payload, true) + twiceBase, twiceWork := TranslateRequestPairWithCodexMultiAgentV2( + context.Background(), http.Header{}, cfg, from, to, model, payload, detached, true) + + for name, got := range map[string][]byte{ + "reuse baseline": reuseBase, + "reuse working": reuseWork, + "twice baseline": twiceBase, + "twice working": twiceWork, + } { + if !bytes.Equal(want, got) { + t.Fatalf("turns=%d: %s translation differs from a standalone translation", turns, name) + } + } + + if len(reuseBase) > 0 && &reuseBase[0] == &reuseWork[0] { + t.Fatalf("turns=%d: working copy aliases the baseline; later in-place edits would corrupt it", turns) + } + + // The caller mutates the working copy, so the baseline must stay intact. + baselineBefore := append([]byte(nil), reuseBase...) + reuseWork[0] = 'X' + if !bytes.Equal(baselineBefore, reuseBase) { + t.Fatalf("turns=%d: mutating the working copy changed the baseline", turns) + } + } +} + +// TestTranslateRequestPairTranslatesDistinctPayloads guards the case where the +// executor really does hand over two different requests. +func TestTranslateRequestPairTranslatesDistinctPayloads(t *testing.T) { + from := sdktranslator.FormatGemini + to := sdktranslator.FromString("antigravity") + cfg := &config.Config{} + const model = "gemini-3.6-flash-high" + + original := geminiToolHistoryPayload(2) + request := geminiToolHistoryPayload(4) + + base, work := TranslateRequestPairWithCodexMultiAgentV2( + context.Background(), http.Header{}, cfg, from, to, model, original, request, true) + + wantBase := TranslateRequestWithCodexMultiAgentV2(context.Background(), http.Header{}, cfg, from, to, model, original, true) + wantWork := TranslateRequestWithCodexMultiAgentV2(context.Background(), http.Header{}, cfg, from, to, model, request, true) + + if !bytes.Equal(wantBase, base) { + t.Fatal("baseline translation differs for distinct payloads") + } + if !bytes.Equal(wantWork, work) { + t.Fatal("working translation differs for distinct payloads") + } + if bytes.Equal(base, work) { + t.Fatal("distinct payloads produced identical translations; the reuse path was taken by mistake") + } +} + +func TestSameByteSlice(t *testing.T) { + buf := []byte("payload") + cases := []struct { + name string + a, b []byte + want bool + }{ + {"identical slice", buf, buf, true}, + {"same array same length", buf[:3], buf[:3], true}, + {"equal bytes different array", buf, append([]byte(nil), buf...), false}, + {"different length", buf, buf[:3], false}, + {"both nil", nil, nil, true}, + {"nil and empty", nil, []byte{}, true}, + {"nil and non-empty", nil, buf, false}, + {"offset alias", buf, buf[1:], false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := sameByteSlice(tc.a, tc.b); got != tc.want { + t.Fatalf("sameByteSlice() = %v, want %v", got, tc.want) + } + }) + } +}