| package executor |
|
|
| import ( |
| "fmt" |
| "net/http" |
| "strings" |
|
|
| kimiauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi" |
| cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" |
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| |
| type KimiProvider struct{} |
|
|
| func (p *KimiProvider) GetIdentifier() string { |
| return "kimi" |
| } |
|
|
| func (p *KimiProvider) GetCredentials(auth *cliproxyauth.Auth) (apiKey, baseURL string) { |
| if auth == nil { |
| return "", kimiauth.KimiAPIBaseURL |
| } |
|
|
| |
| if auth.Metadata != nil { |
| if v, ok := auth.Metadata["access_token"].(string); ok && strings.TrimSpace(v) != "" { |
| return v, kimiauth.KimiAPIBaseURL |
| } |
| } |
|
|
| |
| if auth.Attributes != nil { |
| if v := auth.Attributes["access_token"]; v != "" { |
| return v, kimiauth.KimiAPIBaseURL |
| } |
| if v := auth.Attributes["api_key"]; v != "" { |
| return v, kimiauth.KimiAPIBaseURL |
| } |
| } |
|
|
| return "", kimiauth.KimiAPIBaseURL |
| } |
|
|
| func (p *KimiProvider) GetEndpoint(baseURL, model, action string, stream bool) string { |
| return baseURL + "/v1/chat/completions" |
| } |
|
|
| func (p *KimiProvider) ApplyHeaders(req *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool) { |
| applyKimiHeadersWithAuth(req, apiKey, stream, auth) |
| } |
|
|
| func (p *KimiProvider) GetTranslatorFormat() string { |
| return "openai" |
| } |
|
|
| func (p *KimiProvider) TransformRequestBody(body []byte, model string, stream bool) ([]byte, error) { |
| |
| upstreamModel := stripKimiPrefix(model) |
| body, err := sjson.SetBytes(body, "model", upstreamModel) |
| if err != nil { |
| return body, fmt.Errorf("kimi executor: failed to set model in payload: %w", err) |
| } |
|
|
| |
| body, err = normalizeKimiToolMessageLinks(body) |
| if err != nil { |
| return body, err |
| } |
|
|
| |
| if stream { |
| body, err = sjson.SetBytes(body, "stream_options.include_usage", true) |
| if err != nil { |
| return body, fmt.Errorf("kimi executor: failed to set stream_options in payload: %w", err) |
| } |
| } |
|
|
| return body, nil |
| } |
|
|
| func (p *KimiProvider) TransformResponseBody(body []byte) []byte { |
| |
| return body |
| } |
|
|
| func (p *KimiProvider) ParseUsage(data []byte, stream bool) usageDetail { |
| if stream { |
| if detail, ok := parseOpenAIStreamUsage(data); ok { |
| return detail |
| } |
| return usageDetail{} |
| } |
| return parseOpenAIUsage(data) |
| } |
|
|
| |
| func stripKimiPrefix(model string) string { |
| model = strings.TrimSpace(model) |
| if strings.HasPrefix(strings.ToLower(model), "kimi-") { |
| return model[5:] |
| } |
| return model |
| } |
|
|
| func normalizeKimiToolMessageLinks(body []byte) ([]byte, error) { |
| if len(body) == 0 || !gjson.ValidBytes(body) { |
| return body, nil |
| } |
|
|
| messages := gjson.GetBytes(body, "messages") |
| if !messages.Exists() || !messages.IsArray() { |
| return body, nil |
| } |
|
|
| out := body |
| pending := make([]string, 0) |
| patched := 0 |
| patchedReasoning := 0 |
| ambiguous := 0 |
| latestReasoning := "" |
| hasLatestReasoning := false |
|
|
| removePending := func(id string) { |
| for idx := range pending { |
| if pending[idx] != id { |
| continue |
| } |
| pending = append(pending[:idx], pending[idx+1:]...) |
| return |
| } |
| } |
|
|
| msgs := messages.Array() |
| for msgIdx := range msgs { |
| msg := msgs[msgIdx] |
| role := strings.TrimSpace(msg.Get("role").String()) |
| switch role { |
| case "assistant": |
| reasoning := msg.Get("reasoning_content") |
| if reasoning.Exists() { |
| reasoningText := reasoning.String() |
| if strings.TrimSpace(reasoningText) != "" { |
| latestReasoning = reasoningText |
| hasLatestReasoning = true |
| } |
| } |
|
|
| toolCalls := msg.Get("tool_calls") |
| if !toolCalls.Exists() || !toolCalls.IsArray() || len(toolCalls.Array()) == 0 { |
| continue |
| } |
|
|
| if !reasoning.Exists() || strings.TrimSpace(reasoning.String()) == "" { |
| reasoningText := fallbackAssistantReasoning(msg, hasLatestReasoning, latestReasoning) |
| path := fmt.Sprintf("messages.%d.reasoning_content", msgIdx) |
| next, err := sjson.SetBytes(out, path, reasoningText) |
| if err != nil { |
| return body, fmt.Errorf("kimi executor: failed to set assistant reasoning_content: %w", err) |
| } |
| out = next |
| patchedReasoning++ |
| } |
|
|
| for _, tc := range toolCalls.Array() { |
| id := strings.TrimSpace(tc.Get("id").String()) |
| if id == "" { |
| continue |
| } |
| pending = append(pending, id) |
| } |
| case "tool": |
| toolCallID := strings.TrimSpace(msg.Get("tool_call_id").String()) |
| if toolCallID == "" { |
| toolCallID = strings.TrimSpace(msg.Get("call_id").String()) |
| if toolCallID != "" { |
| path := fmt.Sprintf("messages.%d.tool_call_id", msgIdx) |
| next, err := sjson.SetBytes(out, path, toolCallID) |
| if err != nil { |
| return body, fmt.Errorf("kimi executor: failed to set tool_call_id from call_id: %w", err) |
| } |
| out = next |
| patched++ |
| } |
| } |
| if toolCallID == "" { |
| if len(pending) == 1 { |
| toolCallID = pending[0] |
| path := fmt.Sprintf("messages.%d.tool_call_id", msgIdx) |
| next, err := sjson.SetBytes(out, path, toolCallID) |
| if err != nil { |
| return body, fmt.Errorf("kimi executor: failed to infer tool_call_id: %w", err) |
| } |
| out = next |
| patched++ |
| } else if len(pending) > 1 { |
| ambiguous++ |
| } |
| } |
| if toolCallID != "" { |
| removePending(toolCallID) |
| } |
| } |
| } |
|
|
| return out, nil |
| } |
|
|
| func fallbackAssistantReasoning(msg gjson.Result, hasLatest bool, latest string) string { |
| if hasLatest && strings.TrimSpace(latest) != "" { |
| return latest |
| } |
|
|
| content := msg.Get("content") |
| if content.Type == gjson.String { |
| if text := strings.TrimSpace(content.String()); text != "" { |
| return text |
| } |
| } |
| if content.IsArray() { |
| parts := make([]string, 0, len(content.Array())) |
| for _, item := range content.Array() { |
| text := strings.TrimSpace(item.Get("text").String()) |
| if text == "" { |
| continue |
| } |
| parts = append(parts, text) |
| } |
| if len(parts) > 0 { |
| return strings.Join(parts, "\n") |
| } |
| } |
|
|
| return "[reasoning unavailable]" |
| } |
|
|