Skip to content

refactor(llm): redesign error model as flat tagged union#36622

Open
rekram1-node wants to merge 1 commit into
v2from
llm-error-redesign
Open

refactor(llm): redesign error model as flat tagged union#36622
rekram1-node wants to merge 1 commit into
v2from
llm-error-redesign

Conversation

@rekram1-node

Copy link
Copy Markdown
Collaborator

Summary

Redesigns the packages/llm failure contract so streams carry output only and every failure exits through one typed error channel, classified identically across native protocol routes and the AI SDK route.

Error model

  • LLMError is now a flat tagged union (no { module, method, reason } wrapper): BadRequest | Authentication | PermissionDenied | NotFound | RateLimit | QuotaExceeded | ContentPolicy | ContextOverflow | ServerError | APIError | ConnectionError | TimeoutError | MalformedResponse | NoRoute, with fields directly on the error and an isLLMError guard.
  • Status-derived tags are objective (Stainless-style); ContextOverflow, QuotaExceeded, and ContentPolicy are the only semantic upgrades, applied by one contained classifier.
  • APIError is the fallback for any other deliberate non-2xx rejection.

provider-error event removed

  • The provider-error LLMEvent is deleted. Anthropic SSE error events, OpenAI Responses response.failed/error, and Bedrock exception frames now fail the stream with a classified LLMError.
  • New shared classifyApiFailure is used by the HTTP executor, protocol stream errors, and the AI SDK adapter. Includes OpenAI's actual in-stream codes (rate_limit_exceededRateLimit, internal_errorServerError).

Terminal contract

  • LLMClient.stream enforces, for every route including the synthetic AI SDK route: exactly one finish on success; EOF before finish fails as MalformedResponse (fixes silent truncation); output after finish fails.

Core

  • AI SDK failures are properly classified in core/aisdk.ts (previously collapsed to UnknownProvider), preserving status/headers/body/retry-after.
  • Runner: held-back overflow-event logic deleted; overflow recovery keys off LLM.ContextOverflow; retry policy = RateLimit | ServerError | ConnectionError | TimeoutError. Retry scheduling and the no-retry-after-output rule are unchanged.
  • toSessionError adds provider.context-overflow, provider.timeout, provider.not-found.

Notes

  • Two minimal compile fixes in packages/opencode (dead provider-error switch case, one test stream) — schema change broke the V1 build; no V1 feature work.
  • Bare HTTP 413 remains non-overflow (existing deliberate test).
  • A silently-skipped anthropic recorded test (cassette name drift) is wired up and replaying again.
  • TUI not touched; if it matches provider.* session error strings it may want a follow-up for the new types.
  • Known follow-ups out of scope: WebSocket bounded-queue frame dropping, shared native/AI SDK conformance matrix, spec/DESIGN doc updates.

Testing

  • packages/llm: typecheck clean, 309 pass / 0 fail
  • packages/core: typecheck clean, 1270 pass / 0 fail
  • packages/opencode, packages/server, packages/cli: typecheck clean (one pre-existing processor-effect failure reproduces identically at HEAD)

- Replace LLMError { module, method, reason } wrapper with a flat tagged
  union: BadRequest, Authentication, PermissionDenied, NotFound, RateLimit,
  QuotaExceeded, ContentPolicy, ContextOverflow, ServerError, APIError,
  ConnectionError, TimeoutError, MalformedResponse, NoRoute.
- Delete the provider-error LLMEvent: streams carry output only and every
  failure exits through the typed error channel.
- Add one shared classifyApiFailure classifier used by the HTTP executor,
  protocol stream errors, and the AI SDK adapter so all routes classify
  identically (including OpenAI in-stream rate_limit_exceeded and
  internal_error codes).
- Enforce a terminal contract in LLMClient.stream for every route: EOF
  without finish and output after finish fail as MalformedResponse.
- Classify AI SDK failures properly in core/aisdk.ts instead of collapsing
  to UnknownProvider; preserve status, headers, body, and retry-after.
- Simplify the session runner: drop held-back overflow events, key overflow
  recovery off LLM.ContextOverflow, retry RateLimit | ServerError |
  ConnectionError | TimeoutError.
- Map new tags in toSessionError (provider.context-overflow,
  provider.timeout, provider.not-found).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant