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

  1. Update google-shared.ts to add a case mapping the new finish reason to a sensible StopReason
  2. Log the raw reason value (capture it in the error via wrapping the throw) to identify the unknown string
  3. If behind a proxy, check whether finishReason strings are being altered
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/f87054d0e95b27de. Report an issue: GitHub.