{"record":{"id":"90715d9797b52145","repo":"google-gemini/gemini-cli","slug":"model-policy-chain-must-only-have-one-islastresor","errorCode":null,"errorMessage":"Model policy chain must only have one `isLastResort`.","messagePattern":"Model policy chain must only have one `isLastResort`\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/core/src/availability/policyCatalog.ts","lineNumber":161,"sourceCode":" * Provides a default policy scaffold for models not present in the catalog.\n */\nexport function createDefaultPolicy(\n  model: string,\n  options?: { isLastResort?: boolean },\n): ModelPolicy {\n  return definePolicy({ model, isLastResort: options?.isLastResort });\n}\n\nexport function validateModelPolicyChain(chain: ModelPolicyChain): void {\n  if (chain.length === 0) {\n    throw new Error('Model policy chain must include at least one model.');\n  }\n  const lastResortCount = chain.filter((policy) => policy.isLastResort).length;\n  if (lastResortCount === 0) {\n    throw new Error('Model policy chain must include an `isLastResort` model.');\n  }\n  if (lastResortCount > 1) {\n    throw new Error('Model policy chain must only have one `isLastResort`.');\n  }\n}\n\n/**\n * Helper to define a ModelPolicy with default actions and state transitions.\n * Ensures every policy is a fresh instance to avoid shared state.\n */\nfunction definePolicy(config: PolicyConfig): ModelPolicy {\n  return {\n    model: config.model,\n    isLastResort: config.isLastResort,\n    maxAttempts: config.maxAttempts,\n    actions: { ...DEFAULT_ACTIONS, ...(config.actions ?? {}) },\n    stateTransitions: {\n      ...DEFAULT_STATE,\n      ...(config.stateTransitions ?? {}),\n    },\n  };","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/5024443c7217464a66e98f80d73172a26440bd8f/packages/core/src/availability/policyCatalog.ts#L143-L179","documentation":"Thrown by validateModelPolicyChain() when more than one entry in the chain has isLastResort set to true. The system expects exactly one terminal fallback so that recovery logic is deterministic; multiple last-resort models create ambiguity about which to use when all primary models fail. This is a structural invariant of the policy chain.","triggerScenarios":"Calling validateModelPolicyChain(chain) where two or more entries have isLastResort: true. For example, [{ model: 'a', isLastResort: true }, { model: 'b', isLastResort: true }].","commonSituations":"Concatenating two chains (e.g., custom + default) where both already contain a last-resort entry; marking every model as isLastResort defensively; merging chains from multiple config sources without deduplication; copying a policy object and accidentally duplicating the flag.","solutions":["Ensure exactly one entry has isLastResort: true; clear the flag on all others.","When merging chains, strip isLastResort from the first chain before appending a chain that already has one.","Use createDefaultPolicy(model) (without isLastResort) for primary models and reserve the flag for the single fallback.","After building programmatically, run: const lr = chain.filter(p => p.isLastResort); if (lr.length > 1) lr.slice(1).forEach(p => p.isLastResort = false)."],"exampleFix":"// before — two last-resort entries\nconst chain = [\n  definePolicy({ model: 'gemini-pro', isLastResort: true }),\n  definePolicy({ model: 'gemini-flash', isLastResort: true }),\n];\nvalidateModelPolicyChain(chain); // throws\n\n// after — only the true fallback is marked\nconst chain = [\n  definePolicy({ model: 'gemini-pro' }),\n  definePolicy({ model: 'gemini-flash', isLastResort: true }),\n];","handlingStrategy":"validation","validationCode":"// Ensure exactly one last-resort entry\nconst lastResortIndices = chain\n  .map((p, i) => (p.isLastResort ? i : -1))\n  .filter((i) => i >= 0);\nif (lastResortIndices.length > 1) {\n  // Keep only the first, clear the rest\n  for (let i = 1; i < lastResortIndices.length; i++) {\n    chain[lastResortIndices[i]].isLastResort = false;\n  }\n}\nvalidateModelPolicyChain(chain);","typeGuard":"function chainHasSingleLastResort(chain: ModelPolicyChain): boolean {\n  return chain.filter((p) => p.isLastResort).length === 1;\n}","tryCatchPattern":"try {\n  validateModelPolicyChain(chain);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('only have one')) {\n    // Deduplicate: keep the last last-resort entry, clear others\n    const lrs = chain.filter((p) => p.isLastResort);\n    lrs.slice(0, -1).forEach((p) => (p.isLastResort = false));\n    validateModelPolicyChain(chain);\n  } else throw e;\n}","preventionTips":["When merging chains, strip isLastResort from all but one.","Use a helper function that enforces the single-last-resort invariant on construction.","Add a unit test asserting validateModelPolicyChain passes for all shipped chains.","Avoid marking primary models as isLastResort defensively."],"tags":["model-policy","availability","fallback","validation"],"backgroundTag":null,"analyzedSha":"5024443c7217464a66e98f80d73172a26440bd8f","analyzedAt":"2026-08-12T06:01:53.711Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}