{"record":{"id":"bed5a03bdb797da5","repo":"musistudio/claude-code-router","slug":"openrouter-model-id-must-be-author-slug-model","errorCode":null,"errorMessage":"OpenRouter model id must be author/slug: ${model}","messagePattern":"OpenRouter model id must be author/slug: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/plugins/built-ins/openrouter-discount-provider-router.ts","lineNumber":323,"sourceCode":"  }\n  const failure = endpointFailureCache.get(cacheKey);\n  if (failure) {\n    const cooldownAge = now - failure.failedAt;\n    if (cooldownAge < endpointFailureCooldownMs) {\n      const remainingMs = endpointFailureCooldownMs - cooldownAge;\n      throw new Error(`OpenRouter endpoints request is cooling down after failure (${Math.ceil(remainingMs / 1000)}s left): ${failure.message}`);\n    }\n    endpointFailureCache.delete(cacheKey);\n  }\n  const inflight = endpointInflight.get(cacheKey);\n  if (inflight) {\n    return inflight;\n  }\n\n  const [author, ...slugParts] = model.split(\"/\");\n  const slug = slugParts.join(\"/\");\n  if (!author || !slug) {\n    throw new Error(`OpenRouter model id must be author/slug: ${model}`);\n  }\n\n  const url = `${trimRight(apiRoot, \"/\")}/api/v1/models/${encodeURIComponent(author)}/${encodeURIComponent(slug)}/endpoints`;\n  const promise = fetchModelEndpoints(url).then((endpoints) => {\n    endpointFailureCache.delete(cacheKey);\n    if (endpoints.length > 0) {\n      endpointCache.set(cacheKey, { endpoints, fetchedAt: Date.now() });\n      pruneEndpointCache();\n    }\n    return endpoints;\n  }).catch((error) => {\n    endpointFailureCache.set(cacheKey, {\n      failedAt: Date.now(),\n      message: formatError(error)\n    });\n    pruneEndpointFailureCache();\n    throw error;\n  }).finally(() => {","sourceCodeStart":305,"sourceCodeEnd":341,"githubUrl":"https://github.com/musistudio/claude-code-router/blob/99f24806c6a2c660b16e53e95211c517448a6c90/packages/core/src/plugins/built-ins/openrouter-discount-provider-router.ts#L305-L341","documentation":"OpenRouter model ids must have the form author/slug (exactly one slash separating vendor and model). loadModelEndpoints splits on \"/\" and requires both parts; anything else (no slash, leading slash, empty slug) is rejected before any HTTP call.","triggerScenarios":"Passing a bare model name like \"gpt-4o\", \"openai/\" (empty slug), or a multi-segment path where the slug ends up empty.","commonSituations":"Users pasting OpenAI-style model names, or ids constructed by joining config values where one part is empty.","solutions":["Use the full author/slug id from OpenRouter's model listing","Validate the id format before calling","Trim whitespace/slashes from config-provided model ids"],"exampleFix":"// before\nloadModelEndpoints(\"gpt-4o\");\n// after\nloadModelEndpoints(\"openai/gpt-4o\");","handlingStrategy":"validation","validationCode":"const parts = model.split(\"/\");\nif (parts.length < 2 || !parts[0] || parts.slice(1).join(\"/\") === \"\") throw new TypeError(`bad model id: ${model}`);","typeGuard":"const isOpenRouterModelId = (m: string): boolean => { const [a, ...rest] = m.split(\"/\"); return Boolean(a && rest.join(\"/\")); };","tryCatchPattern":"try { await loadModelEndpoints(model); } catch (e) { if (e instanceof Error && e.message.includes(\"must be author/slug\")) return badRequest(); throw e; }","preventionTips":["Pull ids verbatim from OpenRouter's models endpoint","Validate ids at config load time"],"tags":["openrouter","model-id","validation"],"backgroundTag":"invalid-model-id","analyzedSha":"99f24806c6a2c660b16e53e95211c517448a6c90","analyzedAt":"2026-08-27T04:11:01.184Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}