package executor import ( "context" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "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 TestNewKimiExecutorInitializesDelegatedClaudeConfig(t *testing.T) { cfg := &config.Config{SDKConfig: config.SDKConfig{RequestLog: true}} executor := NewKimiExecutor(cfg) if executor.cfg != cfg { t.Fatal("Kimi executor config was not initialized") } if executor.ClaudeExecutor.cfg != cfg { t.Fatal("delegated Claude executor config was not initialized") }} func TestKimiExecutorRequestToFormatMatchesWireProtocol(t *testing.T) { type requestToFormatReporter interface { RequestToFormat(cliproxyexecutor.Request, cliproxyexecutor.Options) sdktranslator.Format } executor := NewKimiExecutor(&config.Config{}) reporter, ok := any(executor).(requestToFormatReporter) if !ok { t.Fatal("Kimi executor does not report its upstream request format") } tests := []struct { name string stream bool source sdktranslator.Format want sdktranslator.Format }{ {name: "Claude non-streaming", source: sdktranslator.FormatClaude, want: sdktranslator.FormatClaude}, {name: "Claude streaming", stream: true, source: sdktranslator.FormatClaude, want: sdktranslator.FormatClaude}, {name: "OpenAI non-streaming", source: sdktranslator.FormatOpenAI, want: sdktranslator.FormatOpenAI}, {name: "OpenAI streaming", stream: true, source: sdktranslator.FormatOpenAI, want: sdktranslator.FormatOpenAI}, {name: "OpenAI Responses non-streaming", source: sdktranslator.FormatOpenAIResponse, want: sdktranslator.FormatOpenAIResponse}, {name: "OpenAI Responses streaming", stream: true, source: sdktranslator.FormatOpenAIResponse, want: sdktranslator.FormatOpenAIResponse}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := reporter.RequestToFormat(cliproxyexecutor.Request{}, cliproxyexecutor.Options{ SourceFormat: tt.source, Stream: tt.stream, }) if got != tt.want { t.Fatalf("RequestToFormat() = %q, want %q", got, tt.want) } }) }} func TestKimiExecutorResponsesPassthrough(t *testing.T) { var upstreamURL string var upstreamBody []byte var authHeader string ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { upstreamURL = req.URL.String() authHeader = req.Header.Get("Authorization") var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"resp_123","object":"response","status":"completed","model":"k3","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hello world"}]}],"usage":{"total_tokens":10,"input_tokens":6,"output_tokens":4}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{ "model":"kimi-k3", "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}] }`) resp, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if upstreamURL != "https://api.kimi.com/coding/v1/responses" { t.Fatalf("upstreamURL = %q, want %q", upstreamURL, "https://api.kimi.com/coding/v1/responses") } if authHeader != "Bearer test-kimi-key" { t.Fatalf("Authorization = %q, want Bearer test-kimi-key", authHeader) } if gotModel := gjson.GetBytes(upstreamBody, "model").String(); gotModel != "k3" { t.Fatalf("upstreamBody model = %q, want k3", gotModel) } if gotText := gjson.GetBytes(resp.Payload, "output.0.content.0.text").String(); gotText != "hello world" { t.Fatalf("response output text = %q, want hello world", gotText) }} func TestKimiExecutorResponsesStreamPassthrough(t *testing.T) { var upstreamURL string var upstreamBody []byte var streamHeader string ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { upstreamURL = req.URL.String() streamHeader = req.Header.Get("Accept") var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } sseData := "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_stream\",\"status\":\"in_progress\",\"service_tier\":\"default\",\"model\":\"k3\"}}\n\n" + "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"role\":\"assistant\",\"content\":[]}}\n\n" + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"content_index\":0,\"delta\":\"hello stream\"}\n\n" + "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_stream\",\"status\":\"completed\",\"usage\":{\"total_tokens\":12,\"input_tokens\":5,\"output_tokens\":7}}}\n\n" return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"text/event-stream"}}, Body: io.NopCloser(strings.NewReader(sseData)), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{ "model":"kimi-k3", "stream":true, "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}] }`) result, err := executor.ExecuteStream(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Stream: true, }) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } if upstreamURL != "https://api.kimi.com/coding/v1/responses" { t.Fatalf("upstreamURL = %q, want %q", upstreamURL, "https://api.kimi.com/coding/v1/responses") } if streamHeader != "text/event-stream" { t.Fatalf("Accept header = %q, want text/event-stream", streamHeader) } if gotModel := gjson.GetBytes(upstreamBody, "model").String(); gotModel != "k3" { t.Fatalf("upstreamBody model = %q, want k3", gotModel) } var chunks []string for chunk := range result.Chunks { if chunk.Err != nil { t.Fatalf("stream chunk error = %v", chunk.Err) } chunks = append(chunks, string(chunk.Payload)) } combined := strings.Join(chunks, "") if !strings.Contains(combined, "event: response.created") { t.Fatalf("stream chunks missing response.created: %s", combined) } if !strings.Contains(combined, "event: response.completed") { t.Fatalf("stream chunks missing response.completed: %s", combined) } if !strings.Contains(combined, "hello stream") { t.Fatalf("stream chunks missing expected text: %s", combined) }} func TestKimiExecutorResponsesCompactReturnsNotImplemented(t *testing.T) { executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{"model":"kimi-k3","input":[]}`) _, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Alt: "responses/compact", }) if errExecute == nil { t.Fatal("Execute(compact) expected error, got nil") } var statusCoder interface{ StatusCode() int } if !errors.As(errExecute, &statusCoder) || statusCoder.StatusCode() != http.StatusNotImplemented { t.Fatalf("Execute(compact) status = %v, want 501", errExecute) } _, errStream := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Alt: "responses/compact", }) if errStream == nil { t.Fatal("ExecuteStream(compact) expected error, got nil") } if !errors.As(errStream, &statusCoder) || statusCoder.StatusCode() != http.StatusBadRequest { t.Fatalf("ExecuteStream(compact) status = %v, want 400", errStream) }} func TestKimiExecutorResponsesAppliesSuffixThinking(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"resp_think","object":"response","status":"completed","model":"k3","output":[],"usage":{"total_tokens":2}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{"model":"kimi-k3","input":[{"type":"message","role":"user","content":"hello"}]}`) _, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3(high)", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if gotModel := gjson.GetBytes(upstreamBody, "model").String(); gotModel != "k3" { t.Fatalf("upstream model = %q, want k3", gotModel) } if gotEffort := gjson.GetBytes(upstreamBody, "reasoning.effort").String(); gotEffort != "high" { t.Fatalf("upstream reasoning.effort = %q, want high; body=%s", gotEffort, upstreamBody) }} func TestKimiExecutorResponsesSuffixOverridesBodyThinking(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"resp_think","object":"response","status":"completed","model":"k3","output":[],"usage":{"total_tokens":2}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{"model":"kimi-k3","reasoning":{"effort":"low"},"input":[{"type":"message","role":"user","content":"hello"}]}`) _, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3(max)", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if gotEffort := gjson.GetBytes(upstreamBody, "reasoning.effort").String(); gotEffort != "max" { t.Fatalf("upstream reasoning.effort = %q, want max (suffix overrides body); body=%s", gotEffort, upstreamBody) }} func TestKimiExecutorResponsesStreamAppliesSuffixThinking(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"text/event-stream"}}, Body: io.NopCloser(strings.NewReader( "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"status\":\"completed\",\"usage\":{\"total_tokens\":2}}}\n\n", )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{"model":"kimi-k3","stream":true,"input":[{"type":"message","role":"user","content":"hello"}]}`) result, err := executor.ExecuteStream(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3(low)", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Stream: true, }) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } for range result.Chunks { } if gotEffort := gjson.GetBytes(upstreamBody, "reasoning.effort").String(); gotEffort != "low" { t.Fatalf("upstream reasoning.effort = %q, want low; body=%s", gotEffort, upstreamBody) }} func TestKimiExecutorClaudeRequestPreservesInternalModelSemantics(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"msg_test","type":"message","role":"assistant","model":"k2.5","content":[{"type":"text","text":"hello"}],"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-token"}, } const model = "kimi-k2.5(max)" payload := []byte(`{"model":"kimi-k2.5(max)","max_tokens":32,"messages":[{"role":"user","content":"hello"}]}`) response, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: model, Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatClaude, OriginalRequest: payload, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if got := gjson.GetBytes(upstreamBody, "model").String(); got != "k2.5" { t.Fatalf("upstream model = %q, want k2.5", got) } if got := gjson.GetBytes(upstreamBody, "output_config.effort").String(); got != "high" { t.Fatalf("upstream output_config.effort = %q, want high", got) } if got := gjson.GetBytes(response.Payload, "model").String(); got != model { t.Fatalf("response model = %q, want %q", got, model) }} func TestKimiExecutorPreservesAssistantContentAndToolCallsFromResponsesHistory(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"resp_test","object":"response","status":"completed","model":"k3","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"done"}]}],"usage":{"total_tokens":2,"input_tokens":1,"output_tokens":1}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-token"}, } payload := []byte(`{ "model":"kimi-k3", "input":[ {"type":"reasoning","id":"rs_1","summary":[{"type":"summary_text","text":"inspect the next step"}]}, {"type":"message","role":"assistant","content":[{"type":"output_text","text":"Step 3 completed; continue to step 4."}]}, {"type":"function_call","call_id":"call_4","name":"exec_command","arguments":"{\"cmd\":\"pwd\"}"}, {"type":"function_call_output","call_id":"call_4","output":"ok"} ] }`) _, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, OriginalRequest: payload, }) if err != nil { t.Fatalf("Execute() error = %v", err) } input := gjson.GetBytes(upstreamBody, "input").Array() if got := len(input); got != 4 { t.Fatalf("upstream input count = %d, want 4; body=%s", got, upstreamBody) } assistant := input[1] if got := assistant.Get("content.0.text").String(); got != "Step 3 completed; continue to step 4." { t.Fatalf("assistant content = %q, want preserved text; body=%s", got, upstreamBody) } if got := input[0].Get("summary.0.text").String(); got != "inspect the next step" { t.Fatalf("reasoning summary text = %q, want inspect the next step; body=%s", got, upstreamBody) } if got := input[2].Get("call_id").String(); got != "call_4" { t.Fatalf("function call ID = %q, want call_4; body=%s", got, upstreamBody) } if got := input[3].Get("call_id").String(); got != "call_4" { t.Fatalf("function output call ID = %q, want call_4; body=%s", got, upstreamBody) }} func TestKimiExecutorCountTokensUsesCanonicalUpstreamModel(t *testing.T) { var upstreamRequest *http.Request var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { upstreamRequest = req.Clone(req.Context()) var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"input_tokens":42}`)), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-token"}, } payload := []byte(`{"model":"kimi-k3[1m](high)","messages":[{"role":"user","content":"hello"}]}`) _, err := executor.CountTokens(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3[1m](high)", Payload: payload, }, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude}) if err != nil { t.Fatalf("CountTokens() error = %v", err) } if upstreamRequest == nil {Lines 1–500 of 1302. Load more
package executor import ( "context" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "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 TestNewKimiExecutorInitializesDelegatedClaudeConfig(t *testing.T) { cfg := &config.Config{SDKConfig: config.SDKConfig{RequestLog: true}} executor := NewKimiExecutor(cfg) if executor.cfg != cfg { t.Fatal("Kimi executor config was not initialized") } if executor.ClaudeExecutor.cfg != cfg { t.Fatal("delegated Claude executor config was not initialized") } } func TestKimiExecutorRequestToFormatMatchesWireProtocol(t *testing.T) { type requestToFormatReporter interface { RequestToFormat(cliproxyexecutor.Request, cliproxyexecutor.Options) sdktranslator.Format } executor := NewKimiExecutor(&config.Config{}) reporter, ok := any(executor).(requestToFormatReporter) if !ok { t.Fatal("Kimi executor does not report its upstream request format") } tests := []struct { name string stream bool source sdktranslator.Format want sdktranslator.Format }{ {name: "Claude non-streaming", source: sdktranslator.FormatClaude, want: sdktranslator.FormatClaude}, {name: "Claude streaming", stream: true, source: sdktranslator.FormatClaude, want: sdktranslator.FormatClaude}, {name: "OpenAI non-streaming", source: sdktranslator.FormatOpenAI, want: sdktranslator.FormatOpenAI}, {name: "OpenAI streaming", stream: true, source: sdktranslator.FormatOpenAI, want: sdktranslator.FormatOpenAI}, {name: "OpenAI Responses non-streaming", source: sdktranslator.FormatOpenAIResponse, want: sdktranslator.FormatOpenAIResponse}, {name: "OpenAI Responses streaming", stream: true, source: sdktranslator.FormatOpenAIResponse, want: sdktranslator.FormatOpenAIResponse}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := reporter.RequestToFormat(cliproxyexecutor.Request{}, cliproxyexecutor.Options{ SourceFormat: tt.source, Stream: tt.stream, }) if got != tt.want { t.Fatalf("RequestToFormat() = %q, want %q", got, tt.want) } }) } } func TestKimiExecutorResponsesPassthrough(t *testing.T) { var upstreamURL string var upstreamBody []byte var authHeader string ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { upstreamURL = req.URL.String() authHeader = req.Header.Get("Authorization") var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"resp_123","object":"response","status":"completed","model":"k3","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hello world"}]}],"usage":{"total_tokens":10,"input_tokens":6,"output_tokens":4}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{ "model":"kimi-k3", "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}] }`) resp, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if upstreamURL != "https://api.kimi.com/coding/v1/responses" { t.Fatalf("upstreamURL = %q, want %q", upstreamURL, "https://api.kimi.com/coding/v1/responses") } if authHeader != "Bearer test-kimi-key" { t.Fatalf("Authorization = %q, want Bearer test-kimi-key", authHeader) } if gotModel := gjson.GetBytes(upstreamBody, "model").String(); gotModel != "k3" { t.Fatalf("upstreamBody model = %q, want k3", gotModel) } if gotText := gjson.GetBytes(resp.Payload, "output.0.content.0.text").String(); gotText != "hello world" { t.Fatalf("response output text = %q, want hello world", gotText) } } func TestKimiExecutorResponsesStreamPassthrough(t *testing.T) { var upstreamURL string var upstreamBody []byte var streamHeader string ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { upstreamURL = req.URL.String() streamHeader = req.Header.Get("Accept") var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } sseData := "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_stream\",\"status\":\"in_progress\",\"service_tier\":\"default\",\"model\":\"k3\"}}\n\n" + "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"role\":\"assistant\",\"content\":[]}}\n\n" + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"content_index\":0,\"delta\":\"hello stream\"}\n\n" + "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_stream\",\"status\":\"completed\",\"usage\":{\"total_tokens\":12,\"input_tokens\":5,\"output_tokens\":7}}}\n\n" return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"text/event-stream"}}, Body: io.NopCloser(strings.NewReader(sseData)), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{ "model":"kimi-k3", "stream":true, "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}] }`) result, err := executor.ExecuteStream(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Stream: true, }) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } if upstreamURL != "https://api.kimi.com/coding/v1/responses" { t.Fatalf("upstreamURL = %q, want %q", upstreamURL, "https://api.kimi.com/coding/v1/responses") } if streamHeader != "text/event-stream" { t.Fatalf("Accept header = %q, want text/event-stream", streamHeader) } if gotModel := gjson.GetBytes(upstreamBody, "model").String(); gotModel != "k3" { t.Fatalf("upstreamBody model = %q, want k3", gotModel) } var chunks []string for chunk := range result.Chunks { if chunk.Err != nil { t.Fatalf("stream chunk error = %v", chunk.Err) } chunks = append(chunks, string(chunk.Payload)) } combined := strings.Join(chunks, "") if !strings.Contains(combined, "event: response.created") { t.Fatalf("stream chunks missing response.created: %s", combined) } if !strings.Contains(combined, "event: response.completed") { t.Fatalf("stream chunks missing response.completed: %s", combined) } if !strings.Contains(combined, "hello stream") { t.Fatalf("stream chunks missing expected text: %s", combined) } } func TestKimiExecutorResponsesCompactReturnsNotImplemented(t *testing.T) { executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{"model":"kimi-k3","input":[]}`) _, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Alt: "responses/compact", }) if errExecute == nil { t.Fatal("Execute(compact) expected error, got nil") } var statusCoder interface{ StatusCode() int } if !errors.As(errExecute, &statusCoder) || statusCoder.StatusCode() != http.StatusNotImplemented { t.Fatalf("Execute(compact) status = %v, want 501", errExecute) } _, errStream := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Alt: "responses/compact", }) if errStream == nil { t.Fatal("ExecuteStream(compact) expected error, got nil") } if !errors.As(errStream, &statusCoder) || statusCoder.StatusCode() != http.StatusBadRequest { t.Fatalf("ExecuteStream(compact) status = %v, want 400", errStream) } } func TestKimiExecutorResponsesAppliesSuffixThinking(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"resp_think","object":"response","status":"completed","model":"k3","output":[],"usage":{"total_tokens":2}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{"model":"kimi-k3","input":[{"type":"message","role":"user","content":"hello"}]}`) _, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3(high)", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if gotModel := gjson.GetBytes(upstreamBody, "model").String(); gotModel != "k3" { t.Fatalf("upstream model = %q, want k3", gotModel) } if gotEffort := gjson.GetBytes(upstreamBody, "reasoning.effort").String(); gotEffort != "high" { t.Fatalf("upstream reasoning.effort = %q, want high; body=%s", gotEffort, upstreamBody) } } func TestKimiExecutorResponsesSuffixOverridesBodyThinking(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"resp_think","object":"response","status":"completed","model":"k3","output":[],"usage":{"total_tokens":2}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{"model":"kimi-k3","reasoning":{"effort":"low"},"input":[{"type":"message","role":"user","content":"hello"}]}`) _, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3(max)", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if gotEffort := gjson.GetBytes(upstreamBody, "reasoning.effort").String(); gotEffort != "max" { t.Fatalf("upstream reasoning.effort = %q, want max (suffix overrides body); body=%s", gotEffort, upstreamBody) } } func TestKimiExecutorResponsesStreamAppliesSuffixThinking(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"text/event-stream"}}, Body: io.NopCloser(strings.NewReader( "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"status\":\"completed\",\"usage\":{\"total_tokens\":2}}}\n\n", )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-kimi-key"}, } payload := []byte(`{"model":"kimi-k3","stream":true,"input":[{"type":"message","role":"user","content":"hello"}]}`) result, err := executor.ExecuteStream(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3(low)", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Stream: true, }) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } for range result.Chunks { } if gotEffort := gjson.GetBytes(upstreamBody, "reasoning.effort").String(); gotEffort != "low" { t.Fatalf("upstream reasoning.effort = %q, want low; body=%s", gotEffort, upstreamBody) } } func TestKimiExecutorClaudeRequestPreservesInternalModelSemantics(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"msg_test","type":"message","role":"assistant","model":"k2.5","content":[{"type":"text","text":"hello"}],"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-token"}, } const model = "kimi-k2.5(max)" payload := []byte(`{"model":"kimi-k2.5(max)","max_tokens":32,"messages":[{"role":"user","content":"hello"}]}`) response, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: model, Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatClaude, OriginalRequest: payload, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if got := gjson.GetBytes(upstreamBody, "model").String(); got != "k2.5" { t.Fatalf("upstream model = %q, want k2.5", got) } if got := gjson.GetBytes(upstreamBody, "output_config.effort").String(); got != "high" { t.Fatalf("upstream output_config.effort = %q, want high", got) } if got := gjson.GetBytes(response.Payload, "model").String(); got != model { t.Fatalf("response model = %q, want %q", got, model) } } func TestKimiExecutorPreservesAssistantContentAndToolCallsFromResponsesHistory(t *testing.T) { var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( `{"id":"resp_test","object":"response","status":"completed","model":"k3","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"done"}]}],"usage":{"total_tokens":2,"input_tokens":1,"output_tokens":1}}`, )), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-token"}, } payload := []byte(`{ "model":"kimi-k3", "input":[ {"type":"reasoning","id":"rs_1","summary":[{"type":"summary_text","text":"inspect the next step"}]}, {"type":"message","role":"assistant","content":[{"type":"output_text","text":"Step 3 completed; continue to step 4."}]}, {"type":"function_call","call_id":"call_4","name":"exec_command","arguments":"{\"cmd\":\"pwd\"}"}, {"type":"function_call_output","call_id":"call_4","output":"ok"} ] }`) _, err := executor.Execute(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3", Payload: payload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, OriginalRequest: payload, }) if err != nil { t.Fatalf("Execute() error = %v", err) } input := gjson.GetBytes(upstreamBody, "input").Array() if got := len(input); got != 4 { t.Fatalf("upstream input count = %d, want 4; body=%s", got, upstreamBody) } assistant := input[1] if got := assistant.Get("content.0.text").String(); got != "Step 3 completed; continue to step 4." { t.Fatalf("assistant content = %q, want preserved text; body=%s", got, upstreamBody) } if got := input[0].Get("summary.0.text").String(); got != "inspect the next step" { t.Fatalf("reasoning summary text = %q, want inspect the next step; body=%s", got, upstreamBody) } if got := input[2].Get("call_id").String(); got != "call_4" { t.Fatalf("function call ID = %q, want call_4; body=%s", got, upstreamBody) } if got := input[3].Get("call_id").String(); got != "call_4" { t.Fatalf("function output call ID = %q, want call_4; body=%s", got, upstreamBody) } } func TestKimiExecutorCountTokensUsesCanonicalUpstreamModel(t *testing.T) { var upstreamRequest *http.Request var upstreamBody []byte ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", kimiRoundTripperFunc(func(req *http.Request) (*http.Response, error) { upstreamRequest = req.Clone(req.Context()) var errRead error upstreamBody, errRead = io.ReadAll(req.Body) if errRead != nil { return nil, errRead } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"input_tokens":42}`)), }, nil })) executor := NewKimiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Attributes: map[string]string{}, Metadata: map[string]any{"access_token": "test-token"}, } payload := []byte(`{"model":"kimi-k3[1m](high)","messages":[{"role":"user","content":"hello"}]}`) _, err := executor.CountTokens(ctx, auth, cliproxyexecutor.Request{ Model: "kimi-k3[1m](high)", Payload: payload, }, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude}) if err != nil { t.Fatalf("CountTokens() error = %v", err) } if upstreamRequest == nil {