{"record":{"id":"f87054d0e95b27de","repo":"can1357/oh-my-pi","slug":"unhandled-stop-reason-reason-satisfies-never","errorCode":null,"errorMessage":"Unhandled stop reason: ${reason satisfies never}","messagePattern":"Unhandled stop reason: (.+?)","errorType":"exception","errorClass":"AIError.ConfigurationError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/google-shared.ts","lineNumber":414,"sourceCode":"\t\t\treturn \"length\";\n\t\tcase \"BLOCKLIST\":\n\t\tcase \"PROHIBITED_CONTENT\":\n\t\tcase \"SPII\":\n\t\tcase \"SAFETY\":\n\t\tcase \"IMAGE_SAFETY\":\n\t\tcase \"IMAGE_PROHIBITED_CONTENT\":\n\t\tcase \"IMAGE_RECITATION\":\n\t\tcase \"IMAGE_OTHER\":\n\t\tcase \"RECITATION\":\n\t\tcase \"FINISH_REASON_UNSPECIFIED\":\n\t\tcase \"OTHER\":\n\t\tcase \"LANGUAGE\":\n\t\tcase \"MALFORMED_FUNCTION_CALL\":\n\t\tcase \"UNEXPECTED_TOOL_CALL\":\n\t\tcase \"NO_IMAGE\":\n\t\t\treturn \"error\";\n\t\tdefault: {\n\t\t\tthrow new AIError.ConfigurationError(`Unhandled stop reason: ${reason satisfies never}`);\n\t\t}\n\t}\n}\n\n/**\n * Map string finish reason to our StopReason (for raw API responses).\n */\nexport function mapStopReasonString(reason: string): StopReason {\n\tswitch (reason) {\n\t\tcase \"STOP\":\n\t\t\treturn \"stop\";\n\t\tcase \"MAX_TOKENS\":\n\t\t\treturn \"length\";\n\t\tdefault:\n\t\t\treturn \"error\";\n\t}\n}\n","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/google-shared.ts#L396-L432","documentation":"mapStopReason converts Google finish reasons (STOP, MAX_TOKENS, SAFETY, LANGUAGE, MALFORMED_FUNCTION_CALL, etc.) to the library's StopReason union. The default branch throws AIError.ConfigurationError when Google returns a finish reason the mapping table does not know — exhaustiveness means this only fires when a new upstream reason string appears or a non-standard value arrives.","triggerScenarios":"A raw Google finishReason string not present in the switch (new API value, typo in custom mapping, test fixture with an invented reason) reaches the default branch and the value is not assignable to the known union (satisfies never).","commonSituations":"Google ships a new finish reason in apiwalking responses; proxy/middleware rewrites finishReason; unit tests feed fake reasons directly into mapStopReason.","solutions":["Update google-shared.ts to add a case mapping the new finish reason to a sensible StopReason","Log the raw reason value (capture it in the error via wrapping the throw) to identify the unknown string","If behind a proxy, check whether finishReason strings are being altered","Pin/verify your google-genai API surface version and check its changelog for new finish reasons"],"exampleFix":"// before\n\t\tcase \"NO_IMAGE\":\n\t\t\treturn \"error\";\n\t\tdefault: {\n\t\t\tthrow new AIError.ConfigurationError(`Unhandled stop reason: ${reason satisfies never}`);\n\t\t}\n// after\n\t\tcase \"NO_IMAGE\":\n\t\t\treturn \"error\";\n\t\tcase \"OTHER\":\n\t\t\treturn \"other\";\n\t\tdefault: {\n\t\t\tconst unknown: string = reason;\n\t\t\tthrow new AIError.ConfigurationError(`Unhandled stop reason: ${unknown}`);\n\t\t}","handlingStrategy":"try-catch","validationCode":"// pre-validate known finish reasons before custom mapping paths\nconst KNOWN = [\"STOP\",\"MAX_TOKENS\",\"SAFETY\",\"RECITATION\",\"LANGUAGE\",\"MALFORMED_FUNCTION_CALL\",\"UNEXPECTED_TOOL_CALL\",\"NO_IMAGE\",\"BLOCKLIST\",\"PROHIBITED_CONTENT\",\"SPII\"];\nfunction isKnownFinishReason(r) { return KNOWN.includes(r); }","typeGuard":"function isStopReason(v: unknown): v is StopReason {\n  return typeof v === \"string\" && [\"stop\",\"length\",\"content-filter\",\"tool-calls\",\"aborted\",\"error\",\"other\"].includes(v);\n}","tryCatchPattern":"try {\n  const stop = mapStopReason(rawReason);\n} catch (err) {\n  if (err instanceof AIError.ConfigurationError) {\n    logger.warn(\"unknown google finish reason, treating as error\", { rawReason });\n    return \"error\";\n  }\n  throw err;\n}","preventionTips":["Keep the mapStopReason switch updated when bumping the google-genai SDK / API version","Wrap raw API responses in a sanitizer that maps unknown reasons to \"other\" before mapping","Integration-test streaming against the live API after provider updates","Never feed fabricated finish reasons into mapStopReason in tests"],"tags":["google","stop-reason","configuration","exhaustiveness"],"backgroundTag":"unhandled-stop-reason","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}