mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-18 13:34:46 +08:00
258 lines
11 KiB
Go
258 lines
11 KiB
Go
package executor
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
|
|
_ "github.com/router-for-me/CLIProxyAPI/v8/internal/translator"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
|
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v8/sdk/translator"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
type interactionsTranslateHooks struct {
|
|
calls int64
|
|
}
|
|
|
|
func (h *interactionsTranslateHooks) NormalizeRequest(_ context.Context, _, _ sdktranslator.Format, _ string, body []byte, _ bool) []byte {
|
|
h.calls++
|
|
updated, errSet := sjson.SetBytes(body, "plugin_call", h.calls)
|
|
if errSet != nil {
|
|
return body
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func (*interactionsTranslateHooks) TranslateRequest(context.Context, sdktranslator.Format, sdktranslator.Format, string, []byte, bool) ([]byte, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
func (*interactionsTranslateHooks) NormalizeResponseBefore(context.Context, sdktranslator.Format, sdktranslator.Format, string, []byte, []byte, []byte, bool) []byte {
|
|
return nil
|
|
}
|
|
|
|
func (*interactionsTranslateHooks) TranslateResponse(context.Context, sdktranslator.Format, sdktranslator.Format, string, []byte, []byte, []byte, bool) ([]byte, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
func (*interactionsTranslateHooks) NormalizeResponseAfter(context.Context, sdktranslator.Format, sdktranslator.Format, string, []byte, []byte, []byte, bool) []byte {
|
|
return nil
|
|
}
|
|
|
|
func TestTranslateGeminiInteractionsRequestPairReusesSameSlice(t *testing.T) {
|
|
ctx := context.Background()
|
|
cfg := &config.Config{}
|
|
const model = "gemini-3.1-flash-lite"
|
|
payloads := [][]byte{
|
|
[]byte(`{"model":"gemini-3.1-flash-lite","messages":[{"role":"user","content":"hi"}]}`),
|
|
[]byte(`{"model":"gemini-3.1-flash-lite","max_tokens":128,"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`),
|
|
}
|
|
formats := []sdktranslator.Format{sdktranslator.FormatOpenAI, sdktranslator.FormatClaude}
|
|
|
|
for i, payload := range payloads {
|
|
for _, stream := range []bool{false, true} {
|
|
for _, compat := range []bool{false, true} {
|
|
opts := cliproxyexecutor.Options{SourceFormat: formats[i]}
|
|
base, work := translateGeminiInteractionsRequestPair(ctx, cfg, model, payload, opts, stream, compat)
|
|
want := translateGeminiInteractionsRequestBody(ctx, cfg, model, payload, opts, stream, compat)
|
|
if !bytes.Equal(base, want) || !bytes.Equal(work, want) {
|
|
t.Fatalf("format=%s stream=%v compat=%v: reused translation differs", formats[i], stream, compat)
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestTranslateGeminiInteractionsRequestPairTranslatesSameSliceOnce counts translator
|
|
// calls. Byte equality stays green if the reuse branch is deleted and the same
|
|
// deterministic translator runs twice.
|
|
func TestTranslateGeminiInteractionsRequestPairTranslatesSameSliceOnce(t *testing.T) {
|
|
from := sdktranslator.Format("gemini-interactions-translate-count")
|
|
to := sdktranslator.FormatInteractions
|
|
if sdktranslator.HasRequestTransformer(from, to) {
|
|
t.Fatalf("request transformer %s -> %s is already registered", from, to)
|
|
}
|
|
|
|
const model = "gemini-interactions-count"
|
|
var calls int
|
|
var wantStream bool
|
|
sdktranslator.Register(from, to, func(gotModel string, rawJSON []byte, stream bool) []byte {
|
|
if gotModel != model {
|
|
t.Errorf("model = %q, want %q", gotModel, model)
|
|
}
|
|
if stream != wantStream {
|
|
t.Errorf("stream = %v, want %v", stream, wantStream)
|
|
}
|
|
calls++
|
|
return append([]byte(nil), rawJSON...)
|
|
}, sdktranslator.ResponseTransform{})
|
|
t.Cleanup(func() { sdktranslator.Unregister(from, to) })
|
|
|
|
ctx := context.Background()
|
|
cfg := &config.Config{}
|
|
payload := []byte(`{"model":"gemini-interactions-count","messages":[{"role":"user","content":"hi"}]}`)
|
|
for _, stream := range []bool{false, true} {
|
|
for _, compat := range []bool{false, true} {
|
|
wantStream = stream
|
|
calls = 0
|
|
opts := cliproxyexecutor.Options{SourceFormat: from}
|
|
base, work := translateGeminiInteractionsRequestPair(ctx, cfg, model, payload, opts, stream, compat)
|
|
if calls != 1 {
|
|
t.Fatalf("stream=%v compat=%v: same slice translations = %d, want 1", stream, compat, calls)
|
|
}
|
|
if !bytes.Equal(base, payload) || !bytes.Equal(work, payload) {
|
|
t.Fatalf("stream=%v compat=%v: same slice translation changed the payload", stream, compat)
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
|
|
calls = 0
|
|
opts.OriginalRequest = payload
|
|
base, work = translateGeminiInteractionsRequestPair(ctx, cfg, model, payload, opts, stream, compat)
|
|
if calls != 1 {
|
|
t.Fatalf("stream=%v compat=%v: identical backing translations = %d, want 1", stream, compat, calls)
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
|
|
calls = 0
|
|
detached := bytes.Clone(payload)
|
|
opts.OriginalRequest = detached
|
|
base, work = translateGeminiInteractionsRequestPair(ctx, cfg, model, payload, opts, stream, compat)
|
|
if calls != 2 {
|
|
t.Fatalf("stream=%v compat=%v: equal content with a different backing array translations = %d, want 2", stream, compat, calls)
|
|
}
|
|
if !bytes.Equal(base, payload) || !bytes.Equal(work, payload) {
|
|
t.Fatalf("stream=%v compat=%v: distinct backing translation changed the payload", stream, compat)
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTranslateGeminiInteractionsRequestPairTranslatesDistinctInputs(t *testing.T) {
|
|
ctx := context.Background()
|
|
cfg := &config.Config{}
|
|
const model = "gemini-3.1-flash-lite"
|
|
payload := []byte(`{"model":"gemini-3.1-flash-lite","messages":[{"role":"user","content":"working"}]}`)
|
|
originalReq := []byte(`{"model":"gemini-3.1-flash-lite","messages":[{"role":"user","content":"baseline"}]}`)
|
|
// Same bytes in another array must still be translated separately.
|
|
detached := bytes.Clone(payload)
|
|
|
|
for _, original := range [][]byte{originalReq, detached} {
|
|
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatOpenAI, OriginalRequest: original}
|
|
base, work := translateGeminiInteractionsRequestPair(ctx, cfg, model, payload, opts, true, false)
|
|
wantWork := translateGeminiInteractionsRequestBody(ctx, cfg, model, payload, opts, true, false)
|
|
wantBase := geminiInteractionsPayloadConfigSource(ctx, cfg, model, payload, opts, true, false)
|
|
if !bytes.Equal(work, wantWork) || !bytes.Equal(base, wantBase) {
|
|
t.Fatal("distinct inputs did not keep separate translations")
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
}
|
|
}
|
|
|
|
func TestTranslateGeminiInteractionsRequestPairInvokesPluginOncePerInput(t *testing.T) {
|
|
hooks := &interactionsTranslateHooks{}
|
|
sdktranslator.SetPluginHooks(hooks)
|
|
t.Cleanup(func() { sdktranslator.SetPluginHooks(nil) })
|
|
|
|
ctx := context.Background()
|
|
cfg := &config.Config{}
|
|
const model = "gemini-3.1-flash-lite"
|
|
payload := []byte(`{"model":"gemini-3.1-flash-lite","messages":[{"role":"user","content":"same"}]}`)
|
|
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatOpenAI, OriginalRequest: payload}
|
|
for _, stream := range []bool{false, true} {
|
|
for _, compat := range []bool{false, true} {
|
|
hooks.calls = 0
|
|
base, work := translateGeminiInteractionsRequestPair(ctx, cfg, model, payload, opts, stream, compat)
|
|
if hooks.calls != 1 {
|
|
t.Fatalf("stream=%v compat=%v: plugin calls = %d, want 1", stream, compat, hooks.calls)
|
|
}
|
|
if got := gjson.GetBytes(work, "plugin_call").Int(); got != 1 {
|
|
t.Fatalf("working plugin_call = %d, want 1", got)
|
|
}
|
|
if !bytes.Equal(base, work) {
|
|
t.Fatal("same input produced different plugin results")
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
}
|
|
}
|
|
|
|
detached := bytes.Clone(payload)
|
|
opts.OriginalRequest = detached
|
|
before := hooks.calls
|
|
base, work := translateGeminiInteractionsRequestPair(ctx, cfg, model, payload, opts, true, false)
|
|
if hooks.calls != before+2 {
|
|
t.Fatalf("distinct plugin hook calls = %d, want %d", hooks.calls-before, 2)
|
|
}
|
|
if got := gjson.GetBytes(work, "plugin_call").Int(); got != before+1 {
|
|
t.Fatalf("distinct working plugin_call = %d, want %d", got, before+1)
|
|
}
|
|
if got := gjson.GetBytes(base, "plugin_call").Int(); got != before+2 {
|
|
t.Fatalf("distinct baseline plugin_call = %d, want %d", got, before+2)
|
|
}
|
|
}
|
|
|
|
func TestTranslateGeminiInteractionsRequestPairNativeCopy(t *testing.T) {
|
|
ctx := context.Background()
|
|
cfg := &config.Config{}
|
|
payload := []byte(`{"model":"gemini-3.1-flash-lite","input":"hi"}`)
|
|
for _, format := range []sdktranslator.Format{"", sdktranslator.FormatInteractions} {
|
|
opts := cliproxyexecutor.Options{SourceFormat: format}
|
|
base, work := translateGeminiInteractionsRequestPair(ctx, cfg, "gemini-3.1-flash-lite", payload, opts, false, false)
|
|
if !bytes.Equal(base, payload) || !bytes.Equal(work, payload) {
|
|
t.Fatalf("format=%q: native interactions payload was translated", format)
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
|
|
originalReq := []byte(`{"model":"gemini-3.1-flash-lite","input":"original"}`)
|
|
opts.OriginalRequest = originalReq
|
|
base, work = translateGeminiInteractionsRequestPair(ctx, cfg, "gemini-3.1-flash-lite", payload, opts, true, true)
|
|
if !bytes.Equal(work, payload) || !bytes.Equal(base, originalReq) {
|
|
t.Fatalf("format=%q: distinct native inputs were not copied independently", format)
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
}
|
|
}
|
|
|
|
func TestTranslateGeminiInteractionsRequestPairNativeCopyIgnoresHooks(t *testing.T) {
|
|
hooks := &interactionsTranslateHooks{}
|
|
sdktranslator.SetPluginHooks(hooks)
|
|
t.Cleanup(func() { sdktranslator.SetPluginHooks(nil) })
|
|
|
|
payload := []byte(`{"model":"gemini-3.1-flash-lite","input":"hi"}`)
|
|
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatInteractions, OriginalRequest: payload}
|
|
base, work := translateGeminiInteractionsRequestPair(context.Background(), &config.Config{}, "gemini-3.1-flash-lite", payload, opts, true, false)
|
|
if hooks.calls != 0 {
|
|
t.Fatalf("native copy invoked plugin hooks %d times", hooks.calls)
|
|
}
|
|
if !bytes.Equal(base, payload) || !bytes.Equal(work, payload) {
|
|
t.Fatal("native interactions payload changed")
|
|
}
|
|
assertIndependentGeminiInteractionsBuffers(t, payload, base, work)
|
|
}
|
|
|
|
func assertIndependentGeminiInteractionsBuffers(t *testing.T, input, base, work []byte) {
|
|
t.Helper()
|
|
if len(base) > 0 && len(work) > 0 && &base[0] == &work[0] {
|
|
t.Fatal("working buffer aliases the baseline")
|
|
}
|
|
if len(work) > 0 && len(input) > 0 && &work[0] == &input[0] {
|
|
t.Fatal("working buffer aliases the input")
|
|
}
|
|
if len(work) == 0 {
|
|
return
|
|
}
|
|
baselineBefore := bytes.Clone(base)
|
|
inputBefore := bytes.Clone(input)
|
|
work[0] = 'X'
|
|
if !bytes.Equal(baselineBefore, base) {
|
|
t.Fatal("mutating the working buffer changed the baseline")
|
|
}
|
|
if !bytes.Equal(inputBefore, input) {
|
|
t.Fatal("mutating the working buffer changed the input")
|
|
}
|
|
}
|