fix(antigravity): preserve request plugin hook semantics

This commit is contained in:
sususu
2026-08-11 13:58:00 +08:00
committed by sususu98
parent cf8c27fe90
commit 5fa66293db
4 changed files with 90 additions and 5 deletions

View File

@@ -38,13 +38,14 @@ func TranslateRequestWithCodexMultiAgentV2(ctx context.Context, headers http.Hea
// 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.
// translations would rescan the same bytes and produce the same result. Built-in
// request translation is deterministic, so that case is translated once and
// duplicated when no plugin hooks are installed. Hooks retain two invocations
// because they may have request-scoped output or side effects. This 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) {
if sameByteSlice(originalPayload, requestPayload) && !sdktranslator.HasPluginHooks() {
// The caller mutates the working copy, so it must not share the baseline array.
return original, append([]byte(nil), original...)
}

View File

@@ -11,8 +11,36 @@ import (
"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"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
type pairRequestPluginHooks struct {
calls int64
}
func (h *pairRequestPluginHooks) NormalizeRequest(_ context.Context, _, _ sdktranslator.Format, _ string, body []byte, _ bool) []byte {
h.calls++
updated, _ := sjson.SetBytes(body, "plugin_call", h.calls)
return updated
}
func (*pairRequestPluginHooks) TranslateRequest(context.Context, sdktranslator.Format, sdktranslator.Format, string, []byte, bool) ([]byte, bool) {
return nil, false
}
func (*pairRequestPluginHooks) NormalizeResponseBefore(context.Context, sdktranslator.Format, sdktranslator.Format, string, []byte, []byte, []byte, bool) []byte {
return nil
}
func (*pairRequestPluginHooks) TranslateResponse(context.Context, sdktranslator.Format, sdktranslator.Format, string, []byte, []byte, []byte, bool) ([]byte, bool) {
return nil, false
}
func (*pairRequestPluginHooks) NormalizeResponseAfter(context.Context, sdktranslator.Format, sdktranslator.Format, string, []byte, []byte, []byte, bool) []byte {
return nil
}
func geminiToolHistoryPayload(turns int) []byte {
contents := []string{`{"role":"user","parts":[{"text":"start"}]}`}
for i := 0; i < turns; i++ {
@@ -99,6 +127,35 @@ func TestTranslateRequestPairTranslatesDistinctPayloads(t *testing.T) {
}
}
func TestTranslateRequestPairPreservesPluginHookInvocations(t *testing.T) {
hooks := &pairRequestPluginHooks{}
sdktranslator.SetPluginHooks(hooks)
t.Cleanup(func() { sdktranslator.SetPluginHooks(nil) })
payload := geminiToolHistoryPayload(1)
base, work := TranslateRequestPairWithCodexMultiAgentV2(
context.Background(),
http.Header{},
&config.Config{},
sdktranslator.FormatGemini,
sdktranslator.FromString("antigravity"),
"gemini-3.6-flash-high",
payload,
payload,
true,
)
if hooks.calls != 2 {
t.Fatalf("plugin hook calls = %d, want 2", hooks.calls)
}
if got := gjson.GetBytes(base, "plugin_call").Int(); got != 1 {
t.Fatalf("baseline plugin_call = %d, want 1", got)
}
if got := gjson.GetBytes(work, "plugin_call").Int(); got != 2 {
t.Fatalf("working plugin_call = %d, want 2", got)
}
}
func TestSameByteSlice(t *testing.T) {
buf := []byte("payload")
cases := []struct {

View File

@@ -52,6 +52,13 @@ func (r *Registry) SetPluginHooks(hooks PluginHooks) {
r.hooks = hooks
}
// HasPluginHooks reports whether request or response translation hooks are installed.
func (r *Registry) HasPluginHooks() bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.hooks != nil
}
// TranslateRequest converts a payload between schemas, returning the original payload
// if no translator is registered. When falling back to the original payload, the
// "model" field is still updated to match the resolved model name so that
@@ -247,6 +254,11 @@ func SetPluginHooks(hooks PluginHooks) {
defaultRegistry.SetPluginHooks(hooks)
}
// HasPluginHooks reports whether hooks are installed on the default registry.
func HasPluginHooks() bool {
return defaultRegistry.HasPluginHooks()
}
// TranslateRequest is a helper on the default registry.
func TranslateRequest(from, to Format, model string, rawJSON []byte, stream bool) []byte {
return defaultRegistry.TranslateRequest(from, to, model, rawJSON, stream)

View File

@@ -61,6 +61,21 @@ func hasCall(calls []string, want string) bool {
return false
}
func TestHasPluginHooks(t *testing.T) {
registry := NewRegistry()
if registry.HasPluginHooks() {
t.Fatal("new registry unexpectedly reports plugin hooks")
}
registry.SetPluginHooks(&fakePluginHooks{})
if !registry.HasPluginHooks() {
t.Fatal("registry did not report installed plugin hooks")
}
registry.SetPluginHooks(nil)
if registry.HasPluginHooks() {
t.Fatal("registry still reports cleared plugin hooks")
}
}
func TestTranslateRequest_FallbackNormalizesModel(t *testing.T) {
r := NewRegistry()