{"record":{"id":"3a53ad3c0eca2f75","repo":"decolua/9router","slug":"model-modelstr-failed-no-fallback","errorCode":null,"errorMessage":"Model ${modelStr} failed (no fallback)","messagePattern":"Model (.+?) failed \\(no fallback\\)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/services/combo.js","lineNumber":338,"sourceCode":"      } catch {\n        // Ignore JSON parse errors\n      }\n\n      // Track earliest retryAfter across all combo models\n      if (retryAfter && (!earliestRetryAfter || new Date(retryAfter) < new Date(earliestRetryAfter))) {\n        earliestRetryAfter = retryAfter;\n      }\n\n      // Normalize error text to string (Worker-safe)\n      if (typeof errorText !== \"string\") {\n        try { errorText = JSON.stringify(errorText); } catch { errorText = String(errorText); }\n      }\n\n      // Check if should fallback to next model\n      const { shouldFallback, cooldownMs } = checkFallbackError(result.status, errorText);\n\n      if (!shouldFallback) {\n        log.warn(\"COMBO\", `Model ${modelStr} failed (no fallback)`, { status: result.status });\n        return result;\n      }\n\n      // For transient errors (503/502/504), wait for cooldown before falling through\n      // so a briefly-overloaded provider gets a chance to recover rather than being\n      // skipped immediately (fixes: combo falls through on transient 503)\n      if (cooldownMs && cooldownMs > 0 && cooldownMs <= 5000 &&\n          (result.status === 503 || result.status === 502 || result.status === 504)) {\n        log.info(\"COMBO\", `Model ${modelStr} transient ${result.status}, waiting ${cooldownMs}ms before next`);\n        await new Promise(r => setTimeout(r, cooldownMs));\n      }\n\n      // Fallback to next model\n      lastError = errorText || String(result.status);\n      if (!lastStatus) lastStatus = result.status;\n      log.warn(\"COMBO\", `Model ${modelStr} failed, trying next`, { status: result.status });\n    } catch (error) {\n      // Catch unexpected exceptions to ensure fallback continues","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/services/combo.js#L320-L356","documentation":"In handleComboChat, a combo model returned a non-2xx response whose status/error text failed checkFallbackError — i.e. the error is classified as non-fallbackable (e.g. 400/401/403/404, invalid request). Rather than burning the remaining combo models, the combo short-circuits: the failing upstream response is returned to the client as-is and 'Model <model> failed (no fallback)' is logged.","triggerScenarios":"A combo member rejects the request itself: invalid API key (401/403), malformed body or unsupported parameters for that provider (400), unknown model id (404), content-policy refusal. Any request whose error is deterministic — retrying on other models would fail the same way.","commonSituations":"Combo mixing providers with different request schemas (tools/images not supported by one member); stale API key on one combo model after key rotation; model name typo inside the combo definition; sending Claude-style fields to an OpenAI-only member.","solutions":["Inspect the returned response body/status — it is the underlying provider error; fix that cause (key, model id, parameters).","Remove or fix the failing model in the combo definition if it cannot handle your request shape.","Regenerate/rotate the API key or re-OAuth the affected account.","If the error should legitimately fall through (e.g. you want 401 to trigger fallback), adjust checkFallbackError classification in open-sse/services/combo.js.","Normalize request fields so all combo members accept them (drop unsupported params per provider)."],"exampleFix":"// before (combo def)\n{ name: 'fast', models: ['openai/gpt-4o', 'ollama/llama3'] } // llama3 can't handle tools\n// after\n{ name: 'fast', models: ['openai/gpt-4o', 'anthropic/claude-sonnet'] } // tool-capable members","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"const res = await chat({ model: comboName, ... });\nif (!res.ok) {\n  const err = await res.json().catch(() => ({}));\n  // Non-fallbackable: fix request/key for the failing member before retrying\n  console.error(`combo member failed terminally: ${res.status} ${err?.error?.message}`);\n}","preventionTips":["Keep combo members schema-compatible with your request features (tools, images, thinking).","Rotate/verify member API keys after any credential change.","Test each combo model individually with your exact payload shape.","Avoid mixing providers with divergent parameter support in one combo."],"tags":["combo","fallback","upstream-error","non-retryable"],"backgroundTag":"non-retryable-upstream-error","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}