openai_gateway_ollama_cloud_max_tokens.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. package service
  2. import (
  3. "encoding/json"
  4. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  5. "github.com/tidwall/gjson"
  6. "github.com/tidwall/sjson"
  7. "go.uber.org/zap"
  8. )
  9. // OllamaCloudMaxTokensCapExtraKey 是账号 extra 中的可选配置键,表示该 Ollama Cloud
  10. // 账号输出 token 的 provider 级硬上限。用户可通过 admin 账号更新 API 的 extra 字段
  11. // 设置,覆盖默认值 ollamaCloudDefaultMaxTokensCap;0 或负数表示显式禁用 clamp。
  12. const OllamaCloudMaxTokensCapExtraKey = "ollama_max_tokens_cap"
  13. // ollamaCloudDefaultMaxTokensCap 是 Ollama Cloud 对输出 token 数的 provider 级硬上限
  14. // (约 65535),max_tokens 超过该值会被上游直接 400 拒绝;该上限与模型无关,不做模型过滤。
  15. const ollamaCloudDefaultMaxTokensCap = 65535
  16. // clampOllamaCloudUpstreamMaxTokens 是 raw CC 出站(forwardAsRawChatCompletions 与
  17. // /v1/responses 降级 forwardResponsesViaRawChatCompletions 共用)的独立 token 钩子,
  18. // 与 reasoning 钩子解耦:只看本次请求实际选用的 CC 上游 base_url(GetOpenAIBaseURL,
  19. // 与 openAIChatCompletionsTargetURL 的取值一致)是否 ollama.com,不读 usage extra。
  20. // DeepSeek 系模型(用模型映射后的真实出站 model,body 中 model 已改写)任意平台均
  21. // clamp;非 DeepSeek 模型仅保留既有 openai 平台 Ollama 账号(force_chat_completions
  22. // 判定)的 clamp,不扩展到其它平台。
  23. func clampOllamaCloudUpstreamMaxTokens(account *Account, body []byte) []byte {
  24. if account == nil || len(body) == 0 {
  25. return body
  26. }
  27. if !isOllamaCloudBaseURL(account.GetOpenAIBaseURL()) {
  28. return body
  29. }
  30. if !isDeepSeekModel(gjson.GetBytes(body, "model").String()) && !isOllamaCloudRawChatCompletionsAccount(account) {
  31. return body
  32. }
  33. return clampOllamaCloudMaxTokens(account, body)
  34. }
  35. // ollamaCloudResponsesUpstreamBaseURL 返回原生 /v1/responses 本次实际选用的上游
  36. // base_url,取值与 buildUpstreamRequest 一致:adaptive 原生 CN 账号用 api_base_urls
  37. // 的 responses 地址,其余用 GetOpenAIBaseURL。
  38. func ollamaCloudResponsesUpstreamBaseURL(account *Account) string {
  39. if account.UsesNativeCNResponses() && account.IsAdaptiveAPIProtocol() {
  40. return account.GetCNProtocolBaseURL(APIProtocolResponses)
  41. }
  42. return account.GetOpenAIBaseURL()
  43. }
  44. // ollamaCloudResponsesMaxOutputTokensClamp 是原生 /v1/responses 路径的 clamp:在
  45. // 平台字段归一化之后独立调用,不改写原有的 max_output_tokens 平台 switch。判定:
  46. // 实际 Responses 上游(ollamaCloudResponsesUpstreamBaseURL)为 ollama.com 的
  47. // APIKey 账号 + 映射后的出站模型是 DeepSeek 系;openai 平台 max_tokens 刚被归一化
  48. // patch 到 max_output_tokens 时取 max_tokens 为生效值。未命中返回 ok=false。
  49. // 实测:ollama.com/v1/responses max_output_tokens=256000 被上游以 "max_tokens
  50. // (256000) exceeds model's maximum output tokens (65536)" 400 拒绝(DeepSeek 模型)。
  51. func ollamaCloudResponsesMaxOutputTokensClamp(account *Account, upstreamModel string, body []byte) (int64, bool) {
  52. if account == nil || account.Type != AccountTypeAPIKey || !isDeepSeekModel(upstreamModel) {
  53. return 0, false
  54. }
  55. if !isOllamaCloudBaseURL(ollamaCloudResponsesUpstreamBaseURL(account)) {
  56. return 0, false
  57. }
  58. value := gjson.GetBytes(body, "max_output_tokens")
  59. if !value.Exists() && account.Platform == PlatformOpenAI {
  60. value = gjson.GetBytes(body, "max_tokens")
  61. }
  62. cap := ollamaCloudMaxTokensCap(account)
  63. if cap <= 0 || !value.Exists() || value.Type != gjson.Number || value.Int() <= cap {
  64. return 0, false
  65. }
  66. return cap, true
  67. }
  68. // ollamaCloudMaxTokensCap 返回账号配置的 max_tokens 上限。账号为 nil 或 extra 中
  69. // 无该键时返回默认值;键值为数值类型(float64/int64/int/json.Number)时返回其整数
  70. // 值(0 或负数表示显式禁用 clamp);其它类型回退默认值。
  71. func ollamaCloudMaxTokensCap(account *Account) int64 {
  72. if account == nil || account.Extra == nil {
  73. return ollamaCloudDefaultMaxTokensCap
  74. }
  75. value, ok := account.Extra[OllamaCloudMaxTokensCapExtraKey]
  76. if !ok {
  77. return ollamaCloudDefaultMaxTokensCap
  78. }
  79. switch number := value.(type) {
  80. case float64:
  81. return int64(number)
  82. case int64:
  83. return number
  84. case int:
  85. return int64(number)
  86. case json.Number:
  87. parsed, err := number.Int64()
  88. if err != nil {
  89. return ollamaCloudDefaultMaxTokensCap
  90. }
  91. return parsed
  92. default:
  93. return ollamaCloudDefaultMaxTokensCap
  94. }
  95. }
  96. // clampOllamaCloudMaxTokens 把 body 中超过 cap 的 max_tokens / max_completion_tokens
  97. // 单向压到 cap。cap <= 0 或 body 不是合法 JSON 时原样返回;sjson 出错时返回原始 body。
  98. // 有任一字段被 clamp 时记录一条 Debug 日志。
  99. func clampOllamaCloudMaxTokens(account *Account, body []byte) []byte {
  100. cap := ollamaCloudMaxTokensCap(account)
  101. if cap <= 0 || !gjson.ValidBytes(body) {
  102. return body
  103. }
  104. clamped := false
  105. out := body
  106. for _, key := range []string{"max_tokens", "max_completion_tokens"} {
  107. result := gjson.GetBytes(out, key)
  108. if !result.Exists() || result.Type != gjson.Number || result.Int() <= cap {
  109. continue
  110. }
  111. updated, err := sjson.SetBytes(out, key, cap)
  112. if err != nil {
  113. return body
  114. }
  115. out = updated
  116. clamped = true
  117. }
  118. if clamped && account != nil {
  119. logger.L().Debug("openai chat_completions raw: clamped max_tokens for ollama cloud account",
  120. zap.Int64("account_id", account.ID),
  121. zap.Int64("cap", cap),
  122. )
  123. }
  124. return out
  125. }