can1357/oh-my-pi · error · AIError.ConfigurationError
Unhandled stop reason: ${reason satisfies never}
Error message
Unhandled stop reason: ${reason satisfies never} What it means
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.
Source
Thrown at packages/ai/src/providers/google-shared.ts:414
return "length";
case "BLOCKLIST":
case "PROHIBITED_CONTENT":
case "SPII":
case "SAFETY":
case "IMAGE_SAFETY":
case "IMAGE_PROHIBITED_CONTENT":
case "IMAGE_RECITATION":
case "IMAGE_OTHER":
case "RECITATION":
case "FINISH_REASON_UNSPECIFIED":
case "OTHER":
case "LANGUAGE":
case "MALFORMED_FUNCTION_CALL":
case "UNEXPECTED_TOOL_CALL":
case "NO_IMAGE":
return "error";
default: {
throw new AIError.ConfigurationError(`Unhandled stop reason: ${reason satisfies never}`);
}
}
}
/**
* Map string finish reason to our StopReason (for raw API responses).
*/
export function mapStopReasonString(reason: string): StopReason {
switch (reason) {
case "STOP":
return "stop";
case "MAX_TOKENS":
return "length";
default:
return "error";
}
}
View on GitHub (pinned to 9690622007)
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
Example fix
// before
case "NO_IMAGE":
return "error";
default: {
throw new AIError.ConfigurationError(`Unhandled stop reason: ${reason satisfies never}`);
}
// after
case "NO_IMAGE":
return "error";
case "OTHER":
return "other";
default: {
const unknown: string = reason;
throw new AIError.ConfigurationError(`Unhandled stop reason: ${unknown}`);
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-validate known finish reasons before custom mapping paths
const KNOWN = ["STOP","MAX_TOKENS","SAFETY","RECITATION","LANGUAGE","MALFORMED_FUNCTION_CALL","UNEXPECTED_TOOL_CALL","NO_IMAGE","BLOCKLIST","PROHIBITED_CONTENT","SPII"];
function isKnownFinishReason(r) { return KNOWN.includes(r); } Type guard
function isStopReason(v: unknown): v is StopReason {
return typeof v === "string" && ["stop","length","content-filter","tool-calls","aborted","error","other"].includes(v);
} Try / catch
try {
const stop = mapStopReason(rawReason);
} catch (err) {
if (err instanceof AIError.ConfigurationError) {
logger.warn("unknown google finish reason, treating as error", { rawReason });
return "error";
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: Google ships a new finish reason in apiwalking responses; proxy/middleware rewrites finishReason; unit tests feed fake reasons directly into mapStopReason.
Related errors
- cachedContent must not be blank
- Antigravity credentials missing projectId
- No model configured
- Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL
- Cannot register custom API "${api}": built-in API names are
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/f87054d0e95b27de.
Report an issue: GitHub.