Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
43 kB · 1206 lines
Go
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207package executor
import ( "bytes" "context" "fmt" "io" "mime" "mime/multipart" "net/http" "net/http/httptest" "net/textproto" "strings" "testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson")
func TestOpenAICompatExecutorCompactPassthrough(t *testing.T) { var gotPath string var gotBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path body, _ := io.ReadAll(r.Body) gotBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"resp_1","object":"response.compaction","usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`)) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", SupportPromptCacheKey: true, }}, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", "compat_name": "compat", "provider_key": "compat", }, } payload := []byte(`{"model":"gpt-5.1-codex-max","input":[{"role":"user","content":"hi"}]}`) resp, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.1-codex-max", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai-response"), Alt: "responses/compact", Stream: false, }) if err != nil { t.Fatalf("Execute error: %v", err) } if gotPath != "/v1/responses/compact" { t.Fatalf("path = %q, want %q", gotPath, "/v1/responses/compact") } if !gjson.GetBytes(gotBody, "input").Exists() { t.Fatalf("expected input in body") } if gjson.GetBytes(gotBody, "messages").Exists() { t.Fatalf("unexpected messages in body") } if gjson.GetBytes(gotBody, "prompt_cache_key").Exists() { t.Fatalf("unexpected prompt_cache_key in responses compact body: %s", string(gotBody)) } if string(resp.Payload) != `{"id":"resp_1","object":"response.compaction","usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}` { t.Fatalf("payload = %s", string(resp.Payload)) }}
func TestOpenAICompatExecutorPayloadOverrideWinsOverThinkingSuffix(t *testing.T) { var gotBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) gotBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"chatcmpl_1","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`)) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ Payload: config.PayloadConfig{ Override: []config.PayloadRule{ { Models: []config.PayloadModelRule{ {Name: "custom-openai", Protocol: "openai"}, }, Params: map[string]any{ "reasoning_effort": "low", }, }, }, }, }) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} payload := []byte(`{"model":"custom-openai(high)","messages":[{"role":"user","content":"hi"}]}`) _, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "custom-openai(high)", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: false, }) if err != nil { t.Fatalf("Execute error: %v", err) } if got := gjson.GetBytes(gotBody, "reasoning_effort").String(); got != "low" { t.Fatalf("reasoning_effort = %q, want %q; body=%s", got, "low", string(gotBody)) }}
func TestOpenAICompatExecutorApplyPromptCacheKey(t *testing.T) { tests := []struct { name string support bool from string payload string metadata map[string]any wantKey string wantPresent bool }{ { name: "disabled", support: false, from: "claude", payload: `{"model":"gpt-5.6","metadata":{"user_id":"{\"session_id\":\"cache-session\"}"}}`, wantPresent: false, }, { name: "derived", support: true, from: "claude", payload: `{"model":"gpt-5.6","metadata":{"user_id":"{\"session_id\":\"cache-session\"}"}}`, wantPresent: true, }, { name: "explicit caller key wins", support: true, from: "claude", payload: `{"model":"gpt-5.6","prompt_cache_key":"caller-key","metadata":{"user_id":"{\"session_id\":\"cache-session\"}"}}`, wantKey: "caller-key", }, { name: "non Claude source without identity", support: true, from: "openai", payload: `{"model":"gpt-5.6","messages":[{"role":"user","content":"hello"}]}`, wantPresent: false, }, { name: "OpenAI", support: true, from: "openai", payload: `{"model":"gpt-5.6","messages":[{"role":"user","content":"hello"}]}`, metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:openai"}, wantPresent: true, }, { name: "OpenAI responses", support: true, from: "openai-response", payload: `{"model":"gpt-5.6","input":"hello"}`, metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:responses"}, wantPresent: true, }, { name: "Gemini", support: true, from: "gemini", payload: `{"model":"gemini-3","contents":[{"role":"user","parts":[{"text":"hello"}]}]}`, metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:gemini"}, wantPresent: true, }, { name: "Interactions", support: true, from: "interactions", payload: `{"model":"gpt-5.6","input":"hello"}`, metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:interactions"}, wantPresent: true, }, { name: "Codex", support: true, from: "codex", payload: `{"model":"gpt-5.6","input":"hello"}`, metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:codex"}, wantPresent: true, }, { name: "Antigravity", support: true, from: "antigravity", payload: `{"model":"gpt-5.6","input":"hello"}`, metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:antigravity"}, wantPresent: true, }, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", SupportPromptCacheKey: test.support, }}, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "compat_name": "compat", "provider_key": "compat", }, } translated, errApply := executor.applyPromptCacheKey( context.Background(), auth, sdktranslator.FromString(test.from), "gpt-5.6", cliproxyexecutor.Request{Model: "gpt-5.6", Payload: []byte(test.payload)}, cliproxyexecutor.Options{Metadata: test.metadata}, []byte(`{"model":"gpt-5.6","messages":[]}`), ) if errApply != nil { t.Fatalf("applyPromptCacheKey error: %v", errApply) } gotKey := gjson.GetBytes(translated, "prompt_cache_key").String() if test.wantKey != "" { if gotKey != test.wantKey { t.Fatalf("prompt_cache_key = %q, want %q", gotKey, test.wantKey) } return } if present := gotKey != ""; present != test.wantPresent { t.Fatalf("prompt_cache_key present = %t, want %t; body=%s", present, test.wantPresent, string(translated)) } }) }}
func TestOpenAICompatExecutorPromptCacheKeyCallerValueWinsPayloadOverride(t *testing.T) { executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", SupportPromptCacheKey: true, }}, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "compat_name": "compat", "provider_key": "compat", }, } for _, test := range []struct { name string payload []byte originalRequest []byte want string }{ { name: "request payload", payload: []byte(`{"model":"gpt-5.6","prompt_cache_key":"caller-key"}`), want: "caller-key", }, { name: "original request", originalRequest: []byte(`{"model":"gpt-5.6","prompt_cache_key":"caller-key"}`), want: "caller-key", }, } { t.Run(test.name, func(t *testing.T) { translated, errApply := executor.applyPromptCacheKey( context.Background(), auth, sdktranslator.FromString("openai"), "gpt-5.6", cliproxyexecutor.Request{Model: "gpt-5.6", Payload: test.payload}, cliproxyexecutor.Options{OriginalRequest: test.originalRequest}, []byte(`{"model":"gpt-5.6","prompt_cache_key":"payload-override"}`), ) if errApply != nil { t.Fatalf("applyPromptCacheKey error: %v", errApply) } if got := gjson.GetBytes(translated, "prompt_cache_key").String(); got != test.want { t.Fatalf("prompt_cache_key = %q, want %q", got, test.want) } }) }}
func TestOpenAICompatExecutorPromptCacheKeyIsModelAndProtocolScoped(t *testing.T) { executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", SupportPromptCacheKey: true, }}, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "compat_name": "compat", "provider_key": "compat", }, } metadata := map[string]any{cliproxyexecutor.ExecutionSessionMetadataKey: "execution-session"} derive := func(t *testing.T, model string, from string) string { t.Helper() translated, errApply := executor.applyPromptCacheKey( context.Background(), auth, sdktranslator.FromString(from), model, cliproxyexecutor.Request{Model: model, Payload: []byte(`{"messages":[{"role":"user","content":"hello"}]}`)}, cliproxyexecutor.Options{Metadata: metadata}, []byte(`{"model":"`+model+`","messages":[]}`), ) if errApply != nil { t.Fatalf("applyPromptCacheKey error: %v", errApply) } return gjson.GetBytes(translated, "prompt_cache_key").String() }
baseKey := derive(t, "gpt-5.6", "openai") if baseKey == "" { t.Fatal("base prompt_cache_key is empty") } if modelKey := derive(t, "gpt-5.5", "openai"); modelKey == baseKey { t.Fatalf("different model reused prompt_cache_key %q", baseKey) } if protocolKey := derive(t, "gpt-5.6", "openai-response"); protocolKey == baseKey { t.Fatalf("different protocol reused prompt_cache_key %q", baseKey) }}
func TestOpenAICompatExecutorPromptCacheKeyUsesConfigIndex(t *testing.T) { executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{ {Name: "duplicate", SupportPromptCacheKey: false}, {Name: "duplicate", SupportPromptCacheKey: true}, }, }) payload := []byte(`{"model":"gpt-5.6","metadata":{"user_id":"{\"session_id\":\"cache-session\"}"}}`) for _, test := range []struct { name string configIndex string wantPresent bool }{ {name: "first config", configIndex: "0", wantPresent: false}, {name: "second config", configIndex: "1", wantPresent: true}, } { t.Run(test.name, func(t *testing.T) { auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "compat_name": "duplicate", "provider_key": "duplicate", "config_index": test.configIndex, "source": "config:duplicate[0]", }, } translated, errApply := executor.applyPromptCacheKey( context.Background(), auth, sdktranslator.FromString("claude"), "gpt-5.6", cliproxyexecutor.Request{Model: "gpt-5.6", Payload: payload}, cliproxyexecutor.Options{}, []byte(`{"model":"gpt-5.6","messages":[]}`), ) if errApply != nil { t.Fatalf("applyPromptCacheKey error: %v", errApply) } gotPresent := gjson.GetBytes(translated, "prompt_cache_key").String() != "" if gotPresent != test.wantPresent { t.Fatalf("prompt_cache_key present = %t, want %t; body=%s", gotPresent, test.wantPresent, string(translated)) } }) }}
func TestOpenAICompatExecutorPromptCacheKeyIgnoresConfigIndexForNonConfigAuth(t *testing.T) { executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{ {Name: "duplicate", SupportPromptCacheKey: false}, {Name: "duplicate", SupportPromptCacheKey: true}, }, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "compat_name": "duplicate", "provider_key": "duplicate", "config_index": "1", }, } translated, errApply := executor.applyPromptCacheKey( context.Background(), auth, sdktranslator.FromString("openai"), "gpt-5.6", cliproxyexecutor.Request{Model: "gpt-5.6", Payload: []byte(`{"messages":[{"role":"user","content":"hello"}]}`)}, cliproxyexecutor.Options{Metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:non-config"}}, []byte(`{"model":"gpt-5.6","messages":[]}`), ) if errApply != nil { t.Fatalf("applyPromptCacheKey error: %v", errApply) } if gjson.GetBytes(translated, "prompt_cache_key").Exists() { t.Fatalf("unexpected prompt_cache_key for non-config auth: %s", string(translated)) }}
func TestOpenAICompatExecutorPromptCacheKeyExecute(t *testing.T) { var gotBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"chatcmpl_1","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]}`)) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", SupportPromptCacheKey: true, }}, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", "compat_name": "compat", "provider_key": "compat", }, } _, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.6", Payload: []byte(`{"model":"gpt-5.6","messages":[{"role":"user","content":"hello"}]}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:openai"}, }) if errExecute != nil { t.Fatalf("Execute error: %v", errExecute) } if gotKey := gjson.GetBytes(gotBody, "prompt_cache_key").String(); gotKey == "" { t.Fatalf("prompt_cache_key is missing from upstream body: %s", string(gotBody)) }}
func TestOpenAICompatExecutorPromptCacheKeyExecuteStream(t *testing.T) { var gotBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(`data: {"id":"chatcmpl_1","object":"chat.completion.chunk","choices":[]}` + "\n\n")) _, _ = w.Write([]byte("data: [DONE]\n\n")) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", SupportPromptCacheKey: true, }}, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", "compat_name": "compat", "provider_key": "compat", }, } result, errExecute := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.6", Payload: []byte(`{"model":"gpt-5.6","messages":[{"role":"user","content":"hello"}],"stream":true}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: true, Metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:openai-stream"}, }) if errExecute != nil { t.Fatalf("ExecuteStream error: %v", errExecute) } for chunk := range result.Chunks { if chunk.Err != nil { t.Fatalf("stream chunk error: %v", chunk.Err) } } if gotKey := gjson.GetBytes(gotBody, "prompt_cache_key").String(); gotKey == "" { t.Fatalf("prompt_cache_key is missing from upstream stream body: %s", string(gotBody)) }}
func TestOpenAICompatExecutorPromptCacheKeyStreamCompactSkipped(t *testing.T) { var gotBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(`data: {"id":"chatcmpl_1","object":"chat.completion.chunk","choices":[]}` + "\n\n")) _, _ = w.Write([]byte("data: [DONE]\n\n")) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", SupportPromptCacheKey: true, }}, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", "compat_name": "compat", "provider_key": "compat", }, } result, errExecute := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.6", Payload: []byte(`{"model":"gpt-5.6","messages":[{"role":"user","content":"hello"}],"stream":true}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Alt: "responses/compact", Stream: true, Metadata: map[string]any{cliproxyexecutor.DerivedSessionIDMetadataKey: "ctx:v1:compact-stream"}, }) if errExecute != nil { t.Fatalf("ExecuteStream error: %v", errExecute) } for chunk := range result.Chunks { if chunk.Err != nil { t.Fatalf("stream chunk error: %v", chunk.Err) } } if gjson.GetBytes(gotBody, "prompt_cache_key").Exists() { t.Fatalf("unexpected prompt_cache_key in streaming compact body: %s", string(gotBody)) }}
func TestOpenAICompatExecutorImagesGenerationsPassthrough(t *testing.T) { var gotPath string var gotBody []byte var gotContentType string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotContentType = r.Header.Get("Content-Type") body, _ := io.ReadAll(r.Body) gotBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"created":123,"data":[{"b64_json":"AA=="}],"usage":{"total_tokens":1}}`)) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", SupportPromptCacheKey: true, }}, }) auth := &cliproxyauth.Auth{ Provider: "openai-compatibility", Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", "compat_name": "compat", "provider_key": "compat", }, } resp, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "upstream-image", Payload: []byte(`{"model":"compat-image","prompt":"draw"}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai-image"), Stream: false, Headers: http.Header{ "Content-Type": []string{"application/json"}, }, Metadata: map[string]any{ cliproxyexecutor.RequestPathMetadataKey: "/v1/images/generations", }, }) if err != nil { t.Fatalf("Execute error: %v", err) } if gotPath != "/v1/images/generations" { t.Fatalf("path = %q, want %q", gotPath, "/v1/images/generations") } if gotContentType != "application/json" { t.Fatalf("content type = %q, want application/json", gotContentType) } if got := gjson.GetBytes(gotBody, "model").String(); got != "upstream-image" { t.Fatalf("model = %q, want upstream-image; body=%s", got, string(gotBody)) } if gjson.GetBytes(gotBody, "prompt_cache_key").Exists() { t.Fatalf("unexpected prompt_cache_key in image body: %s", string(gotBody)) } if got := gjson.GetBytes(resp.Payload, "data.0.b64_json").String(); got != "AA==" { t.Fatalf("response payload = %s", string(resp.Payload)) }}
func TestOpenAICompatExecutorImagesGenerationsStreamsUpstream(t *testing.T) { var gotPath string var gotBody []byte var gotAccept string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotAccept = r.Header.Get("Accept") body, _ := io.ReadAll(r.Body) gotBody = body w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("event: image_generation.partial\ndata: {\"type\":\"image_generation.partial\"}\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } _, _ = w.Write([]byte("data: [DONE]\n\n")) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} streamResult, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "upstream-image", Payload: []byte(`{"model":"compat-image","prompt":"draw","stream":true}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai-image"), Stream: true, Headers: http.Header{ "Content-Type": []string{"application/json"}, }, Metadata: map[string]any{ cliproxyexecutor.RequestPathMetadataKey: "/v1/images/generations", }, }) if err != nil { t.Fatalf("ExecuteStream error: %v", err) } var streamed bytes.Buffer for chunk := range streamResult.Chunks { if chunk.Err != nil { t.Fatalf("stream chunk error: %v", chunk.Err) } streamed.Write(chunk.Payload) } if gotPath != "/v1/images/generations" { t.Fatalf("path = %q, want %q", gotPath, "/v1/images/generations") } if gotAccept != "text/event-stream" { t.Fatalf("accept = %q, want text/event-stream", gotAccept) } if got := gjson.GetBytes(gotBody, "model").String(); got != "upstream-image" { t.Fatalf("model = %q, want upstream-image; body=%s", got, string(gotBody)) } if !gjson.GetBytes(gotBody, "stream").Bool() { t.Fatalf("stream flag missing from upstream body: %s", string(gotBody)) } if !strings.Contains(streamed.String(), "event: image_generation.partial") || !strings.Contains(streamed.String(), "data: [DONE]") { t.Fatalf("streamed body = %q", streamed.String()) }}
func TestOpenAICompatExecutorImagesEditsMultipartRewritesModel(t *testing.T) { var body bytes.Buffer writer := multipart.NewWriter(&body) if errWrite := writer.WriteField("model", "compat-image"); errWrite != nil { t.Fatalf("write model field: %v", errWrite) } if errWrite := writer.WriteField("prompt", "edit"); errWrite != nil { t.Fatalf("write prompt field: %v", errWrite) } header := make(textproto.MIMEHeader) header.Set("Content-Disposition", multipart.FileContentDisposition("image", "image.png")) header.Set("Content-Type", "image/png") part, errCreate := writer.CreatePart(header) if errCreate != nil { t.Fatalf("create image field: %v", errCreate) } if _, errWrite := part.Write([]byte("png-data")); errWrite != nil { t.Fatalf("write image field: %v", errWrite) } if errClose := writer.Close(); errClose != nil { t.Fatalf("close multipart writer: %v", errClose) } contentType := writer.FormDataContentType()
var gotPath string var gotModel string var gotPrompt string var gotFile string var gotFileContentType string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path if errParse := r.ParseMultipartForm(32 << 20); errParse != nil { t.Fatalf("parse multipart form: %v", errParse) } gotModel = r.FormValue("model") gotPrompt = r.FormValue("prompt") file, fileHeader, errFile := r.FormFile("image") if errFile != nil { t.Fatalf("read image file: %v", errFile) } gotFileContentType = fileHeader.Header.Get("Content-Type") data, errRead := io.ReadAll(file) if errClose := file.Close(); errClose != nil { t.Fatalf("close image file: %v", errClose) } if errRead != nil { t.Fatalf("read image file: %v", errRead) } gotFile = string(data) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"created":123,"data":[{"b64_json":"AA=="}]}`)) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} _, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "upstream-image", Payload: body.Bytes(), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai-image"), Stream: false, Headers: http.Header{ "Content-Type": []string{contentType}, }, Metadata: map[string]any{ cliproxyexecutor.RequestPathMetadataKey: "/v1/images/edits", }, }) if err != nil { t.Fatalf("Execute error: %v", err) } if gotPath != "/v1/images/edits" { t.Fatalf("path = %q, want %q", gotPath, "/v1/images/edits") } if gotModel != "upstream-image" { t.Fatalf("model = %q, want upstream-image", gotModel) } if gotPrompt != "edit" { t.Fatalf("prompt = %q, want edit", gotPrompt) } if gotFile != "png-data" { t.Fatalf("file = %q, want png-data", gotFile) } if gotFileContentType != "image/png" { t.Fatalf("file content type = %q, want image/png", gotFileContentType) }}
func TestRewriteOpenAICompatImagesMultipartPayloadPreservesStreamAndFileContentType(t *testing.T) { var body bytes.Buffer writer := multipart.NewWriter(&body) if errWrite := writer.WriteField("model", "compat-image"); errWrite != nil { t.Fatalf("write model field: %v", errWrite) } if errWrite := writer.WriteField("stream", "false"); errWrite != nil { t.Fatalf("write stream field: %v", errWrite) } header := make(textproto.MIMEHeader) header.Set("Content-Disposition", multipart.FileContentDisposition("image", "image.webp")) header.Set("Content-Type", "image/webp") part, errCreate := writer.CreatePart(header) if errCreate != nil { t.Fatalf("create image field: %v", errCreate) } if _, errWrite := part.Write([]byte("webp-data")); errWrite != nil { t.Fatalf("write image field: %v", errWrite) } if errClose := writer.Close(); errClose != nil { t.Fatalf("close multipart writer: %v", errClose) }
out, contentType, err := prepareOpenAICompatImagesPayload(body.Bytes(), "upstream-image", writer.FormDataContentType(), true) if err != nil { t.Fatalf("prepareOpenAICompatImagesPayload error: %v", err) } mediaType, params, errParse := mime.ParseMediaType(contentType) if errParse != nil { t.Fatalf("parse content type: %v", errParse) } if mediaType != "multipart/form-data" { t.Fatalf("media type = %q, want multipart/form-data", mediaType) } reader := multipart.NewReader(bytes.NewReader(out), params["boundary"]) form, errRead := reader.ReadForm(32 << 20) if errRead != nil { t.Fatalf("read rewritten form: %v", errRead) } defer func() { if errRemove := form.RemoveAll(); errRemove != nil { t.Fatalf("remove form files: %v", errRemove) } }() if got := form.Value["model"]; len(got) != 1 || got[0] != "upstream-image" { t.Fatalf("model values = %#v, want upstream-image", got) } if got := form.Value["stream"]; len(got) != 1 || got[0] != "true" { t.Fatalf("stream values = %#v, want true", got) } if got := form.File["image"]; len(got) != 1 || got[0].Header.Get("Content-Type") != "image/webp" { t.Fatalf("image headers = %#v, want image/webp", got) }}
func TestOpenAICompatExecutorStreamRejectsPlainJSONAfterBlankLines(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("\n\n: openrouter processing\n\nevent: error\n")) _, _ = w.Write([]byte(`{"error":{"message":"upstream failed","type":"server_error"}}` + "\n")) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "openrouter-model", Payload: []byte(`{"model":"openrouter-model","messages":[{"role":"user","content":"hi"}],"stream":true}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: true, }) if err != nil { t.Fatalf("ExecuteStream error: %v", err) }
var gotErr error for chunk := range result.Chunks { if chunk.Err != nil { gotErr = chunk.Err break } } if gotErr == nil { t.Fatalf("expected plain JSON stream error") } if status, ok := gotErr.(interface{ StatusCode() int }); !ok || status.StatusCode() != http.StatusBadGateway { t.Fatalf("stream error status = %v, want %d", gotErr, http.StatusBadGateway) } if !strings.Contains(gotErr.Error(), "upstream failed") { t.Fatalf("stream error = %v", gotErr) }}
func TestOpenAICompatExecutorStreamSkipsKeepAliveUntilDataLine(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("\n\n: openrouter processing\n\nevent: ping\nid: 1\nretry: 1000\n")) _, _ = w.Write([]byte(`data: {"id":"chatcmpl_1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"hello"},"finish_reason":null}]}` + "\n")) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "openrouter-model", Payload: []byte(`{"model":"openrouter-model","messages":[{"role":"user","content":"hi"}],"stream":true}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: true, }) if err != nil { t.Fatalf("ExecuteStream error: %v", err) }
var got strings.Builder for chunk := range result.Chunks { if chunk.Err != nil { t.Fatalf("unexpected stream error: %v", chunk.Err) } got.Write(chunk.Payload) } if gjson.Get(got.String(), "choices.0.delta.content").String() != "hello" { t.Fatalf("stream payload = %s", got.String()) }}
func TestOpenAICompatExecutorResponsesStreamFailsOnEOFWithoutDone(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(`data: {"id":"chatcmpl_1","object":"chat.completion.chunk","created":1773896263,"model":"deepseek-v4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":"partial"},"finish_reason":null}]}` + "\n\n")) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} request := []byte(`{"model":"deepseek-v4-flash","input":"hi","stream":true}`) result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "deepseek-v4-flash", Payload: request, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, OriginalRequest: request, Stream: true, }) if err != nil { t.Fatalf("ExecuteStream error: %v", err) }
var streamed strings.Builder var streamErr error for chunk := range result.Chunks { streamed.Write(chunk.Payload) if chunk.Err != nil { streamErr = chunk.Err } } if !strings.Contains(streamed.String(), "response.output_text.delta") { t.Fatalf("stream did not forward partial assistant output: %q", streamed.String()) } if strings.Contains(streamed.String(), "response.completed") { t.Fatalf("clean EOF without [DONE] was finalized as response.completed: %q", streamed.String()) } if streamErr == nil { t.Fatal("clean EOF without [DONE] did not produce a terminal stream error") } statusErr, ok := streamErr.(interface{ StatusCode() int }) if !ok || statusErr.StatusCode() != http.StatusBadGateway { t.Fatalf("stream error status = %v, want %d", streamErr, http.StatusBadGateway) } if !strings.Contains(streamErr.Error(), "closed before [DONE]") { t.Fatalf("stream error does not explain the missing terminal marker: %v", streamErr) }}
func TestOpenAICompatExecutorResponsesStreamPreservesUpstreamDataError(t *testing.T) { for _, withDone := range []bool{false, true} { t.Run(fmt.Sprintf("with_done=%t", withDone), func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(`data: {"id":"chatcmpl_1","object":"chat.completion.chunk","created":1773896263,"model":"deepseek-v4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":"partial"},"finish_reason":null}]}` + "\n\n")) _, _ = w.Write([]byte(`data: {"error":{"type":"server_error","code":"upstream_failed","message":"upstream failed"}}` + "\n\n")) if withDone { _, _ = w.Write([]byte("data: [DONE]\n\n")) } })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} request := []byte(`{"model":"deepseek-v4-flash","input":"hi","stream":true}`) result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "deepseek-v4-flash", Payload: request, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, OriginalRequest: request, Stream: true, }) if err != nil { t.Fatalf("ExecuteStream error: %v", err) }
var streamed strings.Builder var streamErr error for chunk := range result.Chunks { streamed.Write(chunk.Payload) if chunk.Err != nil { streamErr = chunk.Err } } if strings.Contains(streamed.String(), "response.completed") { t.Fatalf("upstream data error was finalized as response.completed: %q", streamed.String()) } if streamErr == nil || !strings.Contains(streamErr.Error(), "upstream failed") { t.Fatalf("terminal stream error = %v, want original upstream failure", streamErr) } }) }}
func TestOpenAICompatExecutorResponsesStreamPreservesNamedErrorEvent(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(`data: {"id":"chatcmpl_1","object":"chat.completion.chunk","created":1773896263,"model":"deepseek-v4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":"partial"},"finish_reason":null}]}` + "\n\n")) _, _ = w.Write([]byte("event: error\n")) _, _ = w.Write([]byte(`data: {"code":"upstream_failed",` + "\n")) _, _ = w.Write([]byte(`data: "message":"upstream failed"}` + "\n\n")) _, _ = w.Write([]byte("data: [DONE]\n\n")) })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} request := []byte(`{"model":"deepseek-v4-flash","input":"hi","stream":true}`) result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "deepseek-v4-flash", Payload: request, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, OriginalRequest: request, Stream: true, }) if err != nil { t.Fatalf("ExecuteStream error: %v", err) }
var streamed strings.Builder var streamErr error for chunk := range result.Chunks { streamed.Write(chunk.Payload) if chunk.Err != nil { streamErr = chunk.Err } } if strings.Contains(streamed.String(), "response.completed") { t.Fatalf("named upstream error event was finalized as response.completed: %q", streamed.String()) } if streamErr == nil || !strings.Contains(streamErr.Error(), "upstream failed") { t.Fatalf("terminal stream error = %v, want named upstream failure", streamErr) }}
func TestOpenAICompatExecutorResponsesStreamHandlesAdditionalErrorShapes(t *testing.T) { tests := []struct { name string lines []string wantErr string }{ { name: "response failed payload", lines: []string{ `data: {"type":"response.failed","response":{"error":{"type":"server_error","code":"upstream_failed","message":"response failed upstream"}}}` + "\n\n", "data: [DONE]\n\n", }, wantErr: "response failed upstream", }, { name: "data before named error", lines: []string{ `data: {"detail":"data before event failure"}` + "\n", "event: error\n\n", "data: [DONE]\n\n", }, wantErr: "data before event failure", }, { name: "done after incomplete error data", lines: []string{ "event: error\n", `data: {"message":"incomplete upstream failure"` + "\n", "data: [DONE]\n\n", }, wantErr: "incomplete data before [DONE]", }, { name: "done immediately after error event", lines: []string{ "event: error\n", "data: [DONE]\n\n", }, wantErr: "error event ended before [DONE]", }, { name: "incomplete data cannot cross frame boundary", lines: []string{ "data: {\n\n", `data: "id":"chatcmpl_2","object":"chat.completion.chunk","choices":[]}` + "\n\n", "data: [DONE]\n\n", }, wantErr: "incomplete SSE data frame", }, }
for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(`data: {"id":"chatcmpl_1","object":"chat.completion.chunk","created":1773896263,"model":"deepseek-v4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":"partial"},"finish_reason":null}]}` + "\n\n")) for _, line := range tc.lines { _, _ = w.Write([]byte(line)) } })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{"base_url": server.URL + "/v1", "api_key": "test"}} request := []byte(`{"model":"deepseek-v4-flash","input":"hi","stream":true}`) result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{Model: "deepseek-v4-flash", Payload: request}, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, OriginalRequest: request, Stream: true, }) if err != nil { t.Fatalf("ExecuteStream error: %v", err) }
var streamed strings.Builder var streamErr error for chunk := range result.Chunks { streamed.Write(chunk.Payload) if chunk.Err != nil { streamErr = chunk.Err } } if strings.Contains(streamed.String(), "response.completed") { t.Fatalf("upstream error was finalized as response.completed: %q", streamed.String()) } if streamErr == nil || !strings.Contains(streamErr.Error(), tc.wantErr) { t.Fatalf("terminal stream error = %v, want %q", streamErr, tc.wantErr) } }) }}
func TestOpenAICompatExecutorStreamDropsChunksAfterDone(t *testing.T) { // Some OpenAI-compatible upstreams (e.g. OpenCode zen) append non-spec // metadata after data: [DONE]. Those trailing events must not be forwarded, // otherwise clients that treat every pre-[DONE] data line as a chat chunk // fail to deserialize (e.g. missing required "id"). server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") flusher, _ := w.(http.Flusher) _, _ = w.Write([]byte(`data: {"id":"c1a4ba22","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":null}]}` + "\n\n")) _, _ = w.Write([]byte(`data: {"id":"c1a4ba22","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}` + "\n\n")) _, _ = w.Write([]byte("data: [DONE]\n\n")) _, _ = w.Write([]byte(`data: {"choices":[],"cost":"0"}` + "\n\n")) if flusher != nil { flusher.Flush() } })) defer server.Close()
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "base_url": server.URL + "/v1", "api_key": "test", }} result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "deepseek-v4-flash-free", Payload: []byte(`{"model":"deepseek-v4-flash-free","messages":[{"role":"user","content":"hi"}],"stream":true}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: true, }) if err != nil { t.Fatalf("ExecuteStream error: %v", err) }
var payloads []string for chunk := range result.Chunks { if chunk.Err != nil { t.Fatalf("unexpected stream error: %v", chunk.Err) } if len(chunk.Payload) == 0 { continue } payloads = append(payloads, string(chunk.Payload)) } if len(payloads) != 2 { t.Fatalf("got %d payloads %v, want 2 (content + finish; no post-DONE cost chunk)", len(payloads), payloads) } for _, p := range payloads { if strings.Contains(p, `"cost"`) { t.Fatalf("post-DONE cost chunk was forwarded: %s", p) } if !gjson.Get(p, "id").Exists() { t.Fatalf("chunk missing id: %s", p) } } if gjson.Get(payloads[0], "choices.0.delta.content").String() != "hi" { t.Fatalf("first chunk = %s", payloads[0]) } if gjson.Get(payloads[1], "choices.0.finish_reason").String() != "stop" { t.Fatalf("second chunk = %s", payloads[1]) }}