model_not_found_error.go 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. package service
  2. import (
  3. "net/http"
  4. "strings"
  5. )
  6. var upstreamModelNotFoundKeywords = []string{"model not found", "unknown model", "not found"}
  7. func isUpstreamModelNotFoundError(statusCode int, body []byte) bool {
  8. if statusCode != http.StatusNotFound {
  9. return false
  10. }
  11. normalized := normalizeModelNotFoundBody(body)
  12. if normalized == "" || !strings.Contains(normalized, "model") {
  13. return false
  14. }
  15. return containsModelNotFoundKeyword(normalized)
  16. }
  17. func isModelNotFoundError(statusCode int, body []byte) bool {
  18. return isUpstreamModelNotFoundError(statusCode, body) || statusCode == http.StatusNotFound
  19. }
  20. // openAICodexPlanGatedModelPhrase matches the deterministic Codex 400 returned
  21. // when a ChatGPT OAuth account's plan cannot serve the requested model, e.g.
  22. // {"detail":"The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account."}
  23. // The phrase is compared against the normalized body (lowercased, "_"/"-"
  24. // folded to spaces), so it also matches the same message embedded in
  25. // error.message-style payloads.
  26. const openAICodexPlanGatedModelPhrase = "model is not supported when using codex"
  27. // isOpenAICodexPlanGatedModelError reports whether the upstream response is the
  28. // deterministic Codex rejection of a plan-gated model on a ChatGPT account.
  29. // Unlike transient failures, retrying the same account cannot succeed until the
  30. // account's plan changes, so callers should treat it like model-not-found and
  31. // cool the (account, model) pair down instead of re-selecting the account.
  32. func isOpenAICodexPlanGatedModelError(statusCode int, body []byte) bool {
  33. if statusCode != http.StatusBadRequest {
  34. return false
  35. }
  36. normalized := normalizeModelNotFoundBody(body)
  37. if normalized == "" {
  38. return false
  39. }
  40. return strings.Contains(normalized, openAICodexPlanGatedModelPhrase)
  41. }
  42. func containsModelNotFoundKeyword(normalizedBody string) bool {
  43. if normalizedBody == "" {
  44. return false
  45. }
  46. for _, keyword := range upstreamModelNotFoundKeywords {
  47. if strings.Contains(normalizedBody, keyword) {
  48. return true
  49. }
  50. }
  51. return false
  52. }
  53. func normalizeModelNotFoundBody(body []byte) string {
  54. if len(body) == 0 {
  55. return ""
  56. }
  57. normalized := strings.ToLower(string(body))
  58. normalized = strings.NewReplacer("_", " ", "-", " ", "\n", " ", "\r", " ", "\t", " ").Replace(normalized)
  59. return strings.Join(strings.Fields(normalized), " ")
  60. }