| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- 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
- }
|