openai_routing_hint.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. package service
  2. import (
  3. "context"
  4. "net/http"
  5. "strings"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  7. "github.com/tidwall/gjson"
  8. "go.uber.org/zap"
  9. "golang.org/x/net/http/httpguts"
  10. )
  11. const openAICodexRoutingHintHeader = "x-codex-routing-hint"
  12. // setOpenAICodexRoutingHint mirrors the Codex backend routing-hint contract for
  13. // OpenAI OAuth requests. The request model must already be the final upstream
  14. // slug and serviceTier must already reflect any local policy rewrite/filter.
  15. func setOpenAICodexRoutingHint(headers http.Header, account *Account, model string, serviceTier string) {
  16. if headers == nil {
  17. return
  18. }
  19. // The routing hint is gateway-owned. Strip every spelling before deciding
  20. // whether to synthesize it so API-key/provider credential paths cannot pass
  21. // through a caller- or account-override-supplied hint. http.Header.Del only
  22. // removes the canonical map key; inbound maps can contain raw lowercase keys.
  23. deleteOpenAIHeaderEqualFold(headers, openAICodexRoutingHintHeader)
  24. if account == nil || !account.IsOpenAIOAuthLike() {
  25. return
  26. }
  27. model = strings.TrimSpace(model)
  28. if model == "" || strings.ContainsAny(model, ";=") {
  29. return
  30. }
  31. // Codex treats "default" as an explicit standard-routing sentinel, not as a
  32. // service tier sent to the backend. Fast follows the gateway's existing
  33. // canonicalization and therefore becomes "priority"; flex and ultrafast
  34. // stay as themselves.
  35. canonicalTier := normalizedOpenAIServiceTierValue(serviceTier)
  36. // Keep the hint to the effective tiers Codex itself selects. default,
  37. // missing, auto, and scale remain model-only rather than expanding the
  38. // ChatGPT routing protocol here.
  39. switch canonicalTier {
  40. case OpenAIFastTierPriority, OpenAIFastTierFlex, OpenAIFastTierUltrafast:
  41. default:
  42. canonicalTier = ""
  43. }
  44. hint := "model=" + model
  45. if canonicalTier != "" {
  46. hint += ";tier=" + canonicalTier
  47. }
  48. if !httpguts.ValidHeaderFieldValue(hint) {
  49. return
  50. }
  51. headers.Set(openAICodexRoutingHintHeader, hint)
  52. }
  53. func deleteOpenAIHeaderEqualFold(headers http.Header, name string) {
  54. if headers == nil {
  55. return
  56. }
  57. name = strings.TrimSpace(name)
  58. for key := range headers {
  59. if strings.EqualFold(strings.TrimSpace(key), name) {
  60. delete(headers, key)
  61. }
  62. }
  63. }
  64. func setOpenAICodexRoutingHintFromBody(headers http.Header, account *Account, body []byte) {
  65. fields := gjson.GetManyBytes(body, "model", "service_tier")
  66. setOpenAICodexRoutingHint(headers, account, fields[0].String(), fields[1].String())
  67. }
  68. // logOpenAIRoutingDiagnostics records only gateway-derived routing state. In
  69. // particular, it deliberately does not include any header values, tokens, or
  70. // credentials because these diagnostics run on authentication-bearing paths.
  71. func logOpenAIRoutingDiagnostics(
  72. ctx context.Context,
  73. account *Account,
  74. transport string,
  75. model string,
  76. serviceTier string,
  77. hintGenerated bool,
  78. wsAffinityDecision string,
  79. ) {
  80. if ctx == nil {
  81. ctx = context.Background()
  82. }
  83. accountID := int64(0)
  84. if account != nil {
  85. accountID = account.ID
  86. }
  87. logger.FromContext(ctx).Debug("openai routing decision",
  88. zap.String("component", "service.openai_routing"),
  89. zap.String("transport", strings.TrimSpace(transport)),
  90. zap.Int64("account_id", accountID),
  91. zap.String("final_model", strings.TrimSpace(model)),
  92. zap.String("final_service_tier", normalizedOpenAIServiceTierValue(serviceTier)),
  93. zap.Bool("routing_hint_generated", hintGenerated),
  94. zap.String("ws_affinity_decision", strings.TrimSpace(wsAffinityDecision)),
  95. )
  96. }
  97. func logOpenAIRoutingDiagnosticsFromBody(
  98. ctx context.Context,
  99. account *Account,
  100. transport string,
  101. headers http.Header,
  102. body []byte,
  103. wsAffinityDecision string,
  104. ) {
  105. fields := gjson.GetManyBytes(body, "model", "service_tier")
  106. logOpenAIRoutingDiagnostics(
  107. ctx,
  108. account,
  109. transport,
  110. fields[0].String(),
  111. fields[1].String(),
  112. strings.TrimSpace(headers.Get(openAICodexRoutingHintHeader)) != "",
  113. wsAffinityDecision,
  114. )
  115. }