{"record":{"id":"233e2ccd36a83952","repo":"santifer/career-ops","slug":"no-free-models-found-in-api-response","errorCode":null,"errorMessage":"No free models found in API response","messagePattern":"No free models found in API response","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"openrouter-runner.mjs","lineNumber":134,"sourceCode":"async function loadFreeModels() {\n  if (freeModels !== null) return freeModels;\n\n  try {\n    const resp = await fetch(OPENROUTER_MODELS_URL, {\n      headers: { 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}` }\n    });\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    const data = await resp.json();\n\n    // A model is free when its prompt and completion pricing are both \"0\"\n    const list = (data.data ?? [])\n      .filter(m => {\n        const p = m.pricing ?? {};\n        return String(p.prompt) === '0' && String(p.completion) === '0';\n      })\n      .map(m => m.id);\n\n    if (list.length === 0) throw new Error('No free models found in API response');\n\n    // Sort by provider priority; within the same provider sort alphabetically\n    function providerOf(id) { return id.split('/')[0]; }\n    function priorityOf(id) {\n      const idx = PROVIDER_PRIORITY.indexOf(providerOf(id));\n      return idx === -1 ? PROVIDER_PRIORITY.length : idx;\n    }\n\n    freeModels = list.sort((a, b) => {\n      const diff = priorityOf(a) - priorityOf(b);\n      return diff !== 0 ? diff : a.localeCompare(b);\n    });\n\n    console.log(`[models] ${freeModels.length} free models loaded from OpenRouter API.`);\n  } catch (e) {\n    const reason = e instanceof Error ? e.message : String(e);\n    const hasKey = Boolean(process.env.OPENROUTER_API_KEY);\n    throw new Error(","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/openrouter-runner.mjs#L116-L152","documentation":"Thrown by loadFreeModels() after the models API call succeeds but the response contains zero models whose prompt AND completion pricing are both \"0\" — i.e. OpenRouter returned a valid JSON payload with no free models matching the filter. The library treats an empty filtered list as fatal because the runner's whole strategy is rotating through free models.","triggerScenarios":"OpenRouter responds 200 with a models array that, after filtering `String(p.prompt) === '0' && String(p.completion) === '0'`, yields an empty list: pricing schema changed, `data.data` shape changed, or the account/plan context filters out all free models.","commonSituations":"OpenRouter changed the pricing representation (e.g. numbers instead of strings, or per-token pricing moved); a breaking API version change; scraping an unofficial endpoint URL that now serves a different payload; running against a mocked/proxied API returning `{data: []}`.","solutions":["Inspect the raw response: `curl https://openrouter.ai/api/v1/models | jq '.data[0].pricing'` and compare with the filter's expectations.","Check for OpenRouter API changelog notes about pricing field format; update the filter if '0' became 0 or pricing nested differently.","Verify OPENROUTER_MODELS_URL points at the official /api/v1/models endpoint.","If genuinely no free models exist on OpenRouter anymore, switch the runner to paid models with a key holding credits.","Add logging of `data.data.length` before filtering to distinguish 'API returned nothing' from 'filter matched nothing'."],"exampleFix":"// before\nconst list = (data.data ?? [])\n  .filter(m => {\n    const p = m.pricing ?? {};\n    return String(p.prompt) === '0' && String(p.completion) === '0';\n  })\n  .map(m => m.id);\nif (list.length === 0) throw new Error('No free models found in API response');\n// after\nconst all = data.data ?? [];\nconst list = all.filter(m => {\n  const p = m.pricing ?? {};\n  return String(p.prompt) === '0' && String(p.completion) === '0';\n}).map(m => m.id);\nif (list.length === 0) {\n  throw new Error(`No free models found (API returned ${all.length} models; pricing schema may have changed). Sample pricing: ${JSON.stringify(all[0]?.pricing ?? null)}`);\n}","handlingStrategy":"validation","validationCode":"const res = await fetch('https://openrouter.ai/api/v1/models');\nconst { data } = await res.json();\nconst free = (data ?? []).filter(m => String(m.pricing?.prompt) === '0' && String(m.pricing?.completion) === '0');\nif (free.length === 0) console.warn('No free models match current pricing schema — inspect data[0].pricing:', data?.[0]?.pricing);","typeGuard":"function isFreeModel(m) {\n  return m != null\n    && typeof m.id === 'string'\n    && m.pricing != null\n    && String(m.pricing.prompt) === '0'\n    && String(m.pricing.completion) === '0';\n}","tryCatchPattern":null,"preventionTips":["Log the raw count of models before filtering so schema drift is visible.","Subscribe to OpenRouter changelog/announcements for pricing-format changes.","Write a small contract test against the live models endpoint.","Pin a fallback paid-model path if the free filter ever yields nothing."],"tags":["openrouter","api","empty-result","schema"],"backgroundTag":"empty-result-set","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}