package service import ( "strconv" "strings" "github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) // Ollama Cloud 的 OpenAI 兼容 /v1/chat/completions 把思维放在 reasoning / thinking, // 而 DeepSeek/OpenAI 客户端只认 reasoning_content。仅在 raw CC 直转路径上做 wire JSON // 双向补齐,不改 CC↔Responses / Anthropic / Grok 桥。 func isOllamaCloudRawChatCompletionsAccount(account *Account) bool { if account == nil || account.Platform != PlatformOpenAI || account.Type != AccountTypeAPIKey { return false } mode, _ := account.Extra[openai_compat.ExtraKeyResponsesMode].(string) if openai_compat.NormalizeResponsesSupportMode(mode) != openai_compat.ResponsesSupportModeForceChatCompletions { return false } if accountHasOllamaCloudUsageExtra(account) { return true } if account.Credentials == nil { return false } baseURL, _ := account.Credentials["base_url"].(string) return isOllamaCloudBaseURL(baseURL) } func accountHasOllamaCloudUsageExtra(account *Account) bool { if account == nil || account.Extra == nil { return false } for _, key := range []string{ OllamaCloudUsageSessionExtraKey, OllamaCloudUsageAutoRefreshExtraKey, OllamaCloudUsageSnapshotExtraKey, } { if _, ok := account.Extra[key]; ok { return true } } return false } // applyOllamaCloudRawChatCompletionsRequest 只做 Ollama Cloud reasoning 归一化; // max_tokens clamp 已解耦到独立钩子 clampOllamaCloudUpstreamMaxTokens,由出站方依次调用。 func applyOllamaCloudRawChatCompletionsRequest(account *Account, body []byte) []byte { if !isOllamaCloudRawChatCompletionsAccount(account) || len(body) == 0 { return body } return normalizeOllamaCloudChatCompletionsRequest(body) } func applyOllamaCloudRawChatCompletionsResponse(account *Account, body []byte) []byte { if !isOllamaCloudRawChatCompletionsAccount(account) || len(body) == 0 { return body } return normalizeOllamaCloudChatCompletionsResponseJSON(body) } func applyOllamaCloudRawChatCompletionsSSELine(account *Account, line string) string { if !isOllamaCloudRawChatCompletionsAccount(account) || line == "" { return line } return normalizeOllamaCloudChatCompletionsSSELine(line) } func normalizeOllamaCloudChatCompletionsRequest(body []byte) []byte { if !gjson.ValidBytes(body) { return body } messages := gjson.GetBytes(body, "messages") if !messages.IsArray() { return body } updated := body changed := false for i, msg := range messages.Array() { if msg.Get("role").String() != "assistant" { continue } reasoningContent, ok := jsonNonEmptyString(msg.Get("reasoning_content")) if !ok { continue } if _, has := jsonNonEmptyString(msg.Get("reasoning")); has { continue } if _, has := jsonNonEmptyString(msg.Get("thinking")); has { continue } next, err := sjson.SetBytes(updated, "messages."+strconv.Itoa(i)+".reasoning", reasoningContent) if err != nil { return body } updated = next changed = true } if !changed { return body } return updated } func normalizeOllamaCloudChatCompletionsResponseJSON(body []byte) []byte { if !gjson.ValidBytes(body) { return body } choices := gjson.GetBytes(body, "choices") if !choices.IsArray() { return body } updated := body changed := false for i, choice := range choices.Array() { for _, container := range []string{"message", "delta"} { obj := choice.Get(container) if !obj.Exists() || !obj.IsObject() { continue } if obj.Get("reasoning_content").Exists() { continue } src, ok := jsonNonEmptyString(obj.Get("reasoning")) if !ok { src, ok = jsonNonEmptyString(obj.Get("thinking")) } if !ok { continue } next, err := sjson.SetBytes(updated, "choices."+strconv.Itoa(i)+"."+container+".reasoning_content", src) if err != nil { return body } updated = next changed = true } } if !changed { return body } return updated } func normalizeOllamaCloudChatCompletionsSSELine(line string) string { payload, ok := extractOpenAISSEDataLine(line) if !ok { return line } trimmed := strings.TrimSpace(payload) if trimmed == "" || trimmed == "[DONE]" { return line } rewritten := normalizeOllamaCloudChatCompletionsResponseJSON([]byte(payload)) if string(rewritten) == payload { return line } prefixLen := len(line) - len(payload) if prefixLen < 0 { return line } return line[:prefixLen] + string(rewritten) } func jsonNonEmptyString(v gjson.Result) (string, bool) { if v.Type != gjson.String || v.Str == "" { return "", false } return v.Str, true }