openai_compat_model.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. package service
  2. import (
  3. "strings"
  4. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  5. )
  6. func NormalizeOpenAICompatRequestedModel(model string) string {
  7. trimmed := strings.TrimSpace(model)
  8. if trimmed == "" {
  9. return ""
  10. }
  11. normalized, _, ok := splitOpenAICompatReasoningModel(trimmed)
  12. if !ok || normalized == "" {
  13. return trimmed
  14. }
  15. return normalized
  16. }
  17. func applyOpenAICompatModelNormalization(req *apicompat.AnthropicRequest) {
  18. if req == nil {
  19. return
  20. }
  21. originalModel := strings.TrimSpace(req.Model)
  22. if originalModel == "" {
  23. return
  24. }
  25. normalizedModel, derivedEffort, hasReasoningSuffix := splitOpenAICompatReasoningModel(originalModel)
  26. if hasReasoningSuffix && normalizedModel != "" {
  27. req.Model = normalizedModel
  28. }
  29. if req.OutputConfig != nil && strings.TrimSpace(req.OutputConfig.Effort) != "" {
  30. return
  31. }
  32. claudeEffort := openAIReasoningEffortToClaudeOutputEffort(derivedEffort)
  33. if claudeEffort == "" {
  34. return
  35. }
  36. if req.OutputConfig == nil {
  37. req.OutputConfig = &apicompat.AnthropicOutputConfig{}
  38. }
  39. req.OutputConfig.Effort = claudeEffort
  40. }
  41. func splitOpenAICompatReasoningModel(model string) (normalizedModel string, reasoningEffort string, ok bool) {
  42. trimmed := strings.TrimSpace(model)
  43. if trimmed == "" {
  44. return "", "", false
  45. }
  46. modelID := trimmed
  47. if strings.Contains(modelID, "/") {
  48. parts := strings.Split(modelID, "/")
  49. modelID = parts[len(parts)-1]
  50. }
  51. modelID = strings.TrimSpace(modelID)
  52. if !strings.HasPrefix(strings.ToLower(modelID), "gpt-") {
  53. return trimmed, "", false
  54. }
  55. parts := strings.FieldsFunc(strings.ToLower(modelID), func(r rune) bool {
  56. switch r {
  57. case '-', '_', ' ':
  58. return true
  59. default:
  60. return false
  61. }
  62. })
  63. if len(parts) == 0 {
  64. return trimmed, "", false
  65. }
  66. last := strings.NewReplacer("-", "", "_", "", " ", "").Replace(parts[len(parts)-1])
  67. switch last {
  68. case "none", "minimal":
  69. case "low", "medium", "high":
  70. reasoningEffort = last
  71. case "xhigh", "extrahigh":
  72. reasoningEffort = "xhigh"
  73. default:
  74. return trimmed, "", false
  75. }
  76. return normalizeCodexModel(modelID), reasoningEffort, true
  77. }
  78. func openAIReasoningEffortToClaudeOutputEffort(effort string) string {
  79. switch strings.TrimSpace(effort) {
  80. case "low", "medium", "high":
  81. return effort
  82. case "xhigh":
  83. return "max"
  84. default:
  85. return ""
  86. }
  87. }
  88. // openAICompatAnthropicReasoningEffort resolves the effort emitted by the
  89. // Anthropic bridge after the final upstream model is known. Anthropic's max is
  90. // normally translated to OpenAI xhigh, but GPT-5.6 accepts the original max
  91. // value on Responses and Chat Completions.
  92. func openAICompatAnthropicReasoningEffort(req *apicompat.AnthropicRequest, upstreamModel, convertedEffort string) string {
  93. if req == nil || req.OutputConfig == nil || !strings.EqualFold(strings.TrimSpace(req.OutputConfig.Effort), "max") {
  94. return convertedEffort
  95. }
  96. if normalized := normalizeOpenAIReasoningEffortForModel(req.OutputConfig.Effort, upstreamModel); normalized != "" {
  97. return normalized
  98. }
  99. return convertedEffort
  100. }