fix(agent): report model refusals as typed MODEL_REFUSED instead of MODEL_PROVIDER_FAILED

Closed
VA
14d ago
+233-8

Fixes #945.

What

A model refusal (finish reason content-filter) on any adapter model call — agent.extract included — is now reported as the typed, non-retryable MODEL_REFUSED code (category configuration, exit 2) instead of MODEL_PROVIDER_FAILED: model provider failed: No output generated. (exit 3). The message names the step, the model, and the raw finish reason, so the report reads as a refusal instead of a provider outage, and retry policies no longer burn a second call on a prompt that is refused by construction.

Also fixed on the text path (no schema), where a refusal previously surfaced as MODEL_OUTPUT_INVALID — which invocation.ask would spend a repair round retrying.

Drive-by fix in the same seam: the SDK's no-output error was renamed in AI SDK v7 (NoOutputGeneratedError), so the old NoObjectGeneratedError.isInstance check in translateModelError never matched and empty non-refusal output fell through to MODEL_PROVIDER_FAILED. It now recognizes both names and maps back to MODEL_OUTPUT_INVALID as originally intended.

Changes

  • packages/e2e/src/types.ts, packages/e2e/src/agent/error.ts: new MODEL_REFUSED code, configuration category — deliberately not test, so the runner's retry policy (which only retries test) never retries a refusal.
  • packages/e2e/src/agent/model/sdk.ts: throwIfModelRefused checks result.finishReason right after generateText, before parsing / touching result.output (whose getter throws the SDK's no-output error); both the structured and text branches covered.
  • docs/reference/errors.mdx, skills/e2e/references/debugging.md: documented (node scripts/check-error-codes.ts green).
  • .changeset/model-refused-typed-error.md: patch changeset.

Verified

  • New packages/e2e/tests/unit/model-refusal.test.ts (3 tests) against a local stub provider returning finish_reason: content_filter:
    • pre-fix: structured path → MODEL_PROVIDER_FAILED (model provider failed: No output generated. — the exact report from #945); text path → MODEL_OUTPUT_INVALID. Both fail as expected.
    • post-fix: both → MODEL_REFUSED with step/model/reason in the message; serializeError(...).retryable === false and isRetryEligible(...) === false.
  • Full unit suite: 185 files / 2737 tests pass. oxlint clean, tsc --noEmit clean apart from pre-existing @e2e-dev/web resolution errors in integration tests (package not installed in this env), node scripts/check-error-codes.ts green.