{"record":{"id":"684c33a47d9bbdaa","repo":"justjavac/wechat-miniapp-radar","slug":"ai-provider-rejected-model-providererror","errorCode":null,"errorMessage":"AI provider rejected ${model}: ${providerError}","messagePattern":"AI provider rejected (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/ai-client.ts","lineNumber":156,"sourceCode":"        authorization: `Bearer ${apiKey}`,\n        ...openRouterHeaders(config)\n      },\n      body: JSON.stringify({\n        model,\n        messages,\n        temperature: 0.2,\n        max_tokens: DEFAULT_AI_MAX_TOKENS,\n        stream: false,\n        ...responseFormatForModel(config, model)\n      })\n    },\n    timeoutMs\n  );\n\n  const payload = parseCompletionPayload(text);\n  if (!response.ok) {\n    const providerError = stringifyProviderError(payload?.error);\n    throw new Error(providerError ? `AI provider rejected ${model}: ${providerError}` : `AI provider rejected ${model} with HTTP ${response.status}.`);\n  }\n\n  const content = payload?.choices?.[0]?.message?.content;\n  if (typeof content !== \"string\" || content.trim().length === 0) {\n    throw new Error(`AI provider returned an empty response for ${model}.`);\n  }\n\n  return parseJsonObject<T>(content);\n}\n\nexport async function createAiJsonCompletion<T>({\n  messages,\n  timeoutMs = DEFAULT_AI_TIMEOUT_MS,\n  totalTimeoutMs = DEFAULT_AI_TOTAL_TIMEOUT_MS\n}: {\n  messages: AiPromptMessage[];\n  timeoutMs?: number;\n  totalTimeoutMs?: number;","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/justjavac/wechat-miniapp-radar/blob/02a010ecea0320b7aa975bb62a5dde271ae630c9/lib/ai-client.ts#L138-L174","documentation":"Thrown by requestChatCompletion() in lib/ai-client.ts when the provider returns a non-2xx HTTP status. It first parses the error body via parseCompletionPayload() and stringifyProviderError() (which reads message/code and metadata.reason/raw/provider_name) and formats `AI provider rejected <model>: <detail>`; if no detail is parseable it falls back to `... with HTTP <status>`. The model name interpolated is the one actually requested (primary or fallback). createAiJsonCompletion() catches this, records the message, and tries the next model before returning ok:false.","triggerScenarios":"401/403 invalid or revoked OPENAI_API_KEY; 402 insufficient credits (common with OpenRouter free quota); 404 unknown or deprecated model id in OPENAI_MODEL/OPENAI_FALLBACK_MODEL; 429 rate limit or daily free quota exhausted; 400 unsupported response_format for a model not in OPENROUTER_JSON_RESPONSE_FORMAT_MODELS; provider 5xx.","commonSituations":"OpenRouter free-tier daily limit hit; model renamed or retired upstream (e.g. a :free variant removed); wrong OPENAI_API_URL (custom endpoint returning HTML error pages); key without permission for the chosen model; burst traffic triggering 429.","solutions":["Decode the interpolated detail first: 401/403 fix OPENAI_API_KEY; 402 add credits or switch model; 404 correct OPENAI_MODEL; 429 back off or raise quota; 400 response_format remove the model or add it to OPENROUTER_JSON_RESPONSE_FORMAT_MODELS.","Let createAiJsonCompletion() try config.fallbackModel automatically; ensure OPENAI_FALLBACK_MODEL is a different, currently available model.","For 429/5xx, retry with bounded exponential backoff at the caller.","If all models fail, fall back to the app's rule-based advisor/scoring."],"exampleFix":"// before\nconst result = await createAiJsonCompletion<T>({ messages });\n// result.error may be 'AI provider rejected openai/gpt-oss-20b:free: ...'\n\n// after\nif (!result.ok) {\n  console.warn(result.error);\n  return ruleBasedFallback();\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"const result = await createAiJsonCompletion<T>({ messages });\nif (!result.ok || !result.value) {\n  // result.error lists per-model rejection reasons joined by ' | '\n  return ruleBasedFallback();\n}\nreturn result.value;","preventionTips":["Keep OPENAI_FALLBACK_MODEL distinct and currently available so createAiJsonCompletion can recover.","For OpenRouter free models, watch the daily quota and keep a funded backup model configured.","Validate that OPENAI_API_URL ends in /v1 and is the REST root, not a page URL."],"tags":["ai","network","http","provider-error"],"backgroundTag":null,"analyzedSha":"02a010ecea0320b7aa975bb62a5dde271ae630c9","analyzedAt":"2026-08-12T16:16:33.227Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}