From c6dd82144baf2b9a387a8908a2448e9508a79f49 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:06:07 +0800 Subject: [PATCH] refactor(kimi): use request thinking helper --- internal/runtime/executor/kimi_executor.go | 4 +- .../kimi_home_model_capabilities_test.go | 131 ++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 internal/runtime/executor/kimi_home_model_capabilities_test.go diff --git a/internal/runtime/executor/kimi_executor.go b/internal/runtime/executor/kimi_executor.go index 8bd73b9b..2232aed5 100644 --- a/internal/runtime/executor/kimi_executor.go +++ b/internal/runtime/executor/kimi_executor.go @@ -132,7 +132,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req return resp, fmt.Errorf("kimi executor: failed to set model in payload: %w", err) } - body, err = helps.ApplyThinkingWithSourcePayload(body, req.Payload, originalPayloadSource, req.Model, from.String(), "kimi", e.Identifier()) + body, err = helps.ApplyRequestThinking(body, req, opts, from.String(), "kimi", e.Identifier()) if err != nil { return resp, err } @@ -253,7 +253,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut return nil, fmt.Errorf("kimi executor: failed to set model in payload: %w", err) } - body, err = helps.ApplyThinkingWithSourcePayload(body, req.Payload, originalPayloadSource, req.Model, from.String(), "kimi", e.Identifier()) + body, err = helps.ApplyRequestThinking(body, req, opts, from.String(), "kimi", e.Identifier()) if err != nil { return nil, err } diff --git a/internal/runtime/executor/kimi_home_model_capabilities_test.go b/internal/runtime/executor/kimi_home_model_capabilities_test.go new file mode 100644 index 00000000..05af7b64 --- /dev/null +++ b/internal/runtime/executor/kimi_home_model_capabilities_test.go @@ -0,0 +1,131 @@ +package executor + +import ( + "context" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry" + 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" +) + +type kimiHomeModelCapabilityDispatcher struct{} + +func (*kimiHomeModelCapabilityDispatcher) HeartbeatOK() bool { return true } + +func (*kimiHomeModelCapabilityDispatcher) RPopAuth(context.Context, string, string, http.Header, int) ([]byte, error) { + return json.Marshal(map[string]any{ + "model": "kimi-k3", + "provider": "kimi", + "model_info": map[string]any{ + "id": "kimi-k3", + "type": "kimi", + "context_length": 1048576, + "thinking": map[string]any{ + "levels": []string{"low", "high"}, + }, + "user_defined": false, + }, + "auth": cliproxyauth.Auth{ + ID: "home-kimi-k3-auth", + Provider: "kimi", + Status: cliproxyauth.StatusActive, + Attributes: map[string]string{}, + Metadata: map[string]any{ + "access_token": "token", + "expired": time.Now().Add(time.Hour).Format(time.RFC3339), + }, + }, + }) +} + +func (*kimiHomeModelCapabilityDispatcher) AbortAmbiguousDispatch() {} + +func TestKimiHomeModelThinkingCapabilitiesOverrideLocalRegistry(t *testing.T) { + tests := []struct { + name string + stream bool + }{ + {name: "non-streaming"}, + {name: "streaming", stream: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(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 + } + if tt.stream { + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}}, + Body: io.NopCloser(strings.NewReader( + "data: {\"id\":\"chatcmpl_test\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"k3\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"},\"finish_reason\":null}]}\n\n" + + "data: [DONE]\n\n", + )), + }, nil + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader( + `{"id":"chatcmpl_test","object":"chat.completion","created":1,"model":"k3","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`, + )), + }, nil + })) + + cfg := &config.Config{RequestRetry: 1} + cfg.Home.Enabled = true + manager := cliproxyauth.NewManager(nil, nil, nil) + manager.SetConfig(cfg) + manager.RegisterExecutor(NewKimiExecutor(cfg)) + manager.PublishHomeDispatch(&kimiHomeModelCapabilityDispatcher{}, executionregistry.New(), 1) + + payload := []byte(`{"model":"kimi-k3","reasoning_effort":"max","messages":[{"role":"user","content":"hello"}]}`) + req := cliproxyexecutor.Request{Model: "kimi-k3", Payload: payload} + opts := cliproxyexecutor.Options{ + Stream: tt.stream, + SourceFormat: sdktranslator.FormatOpenAI, + ResponseFormat: sdktranslator.FormatOpenAI, + OriginalRequest: payload, + } + if tt.stream { + result, errExecute := manager.ExecuteStream(ctx, []string{"kimi"}, req, opts) + 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) + } + } + } else { + if _, errExecute := manager.Execute(ctx, []string{"kimi"}, req, opts); errExecute != nil { + t.Fatalf("Execute() error = %v", errExecute) + } + } + + if got := gjson.GetBytes(upstreamBody, "thinking.type").String(); got != "enabled" { + t.Fatalf("thinking.type = %q, want enabled; body=%s", got, upstreamBody) + } + if got := gjson.GetBytes(upstreamBody, "thinking.effort").String(); got != "high" { + t.Fatalf("thinking.effort = %q, want high from Home model capabilities; body=%s", got, upstreamBody) + } + if effort := gjson.GetBytes(upstreamBody, "reasoning_effort"); effort.Exists() { + t.Fatalf("reasoning_effort should be absent, got %s; body=%s", effort.Raw, upstreamBody) + } + }) + } +} -- 2.51.2