enhancement
major
minor
defect
critical
major
minor
major
#30128
A run without provider credentials ends as an authentication failure, and HTTP 402 is a billing failure for every provider
An AI run whose model has a provider without its key fails with an exception that is logged as an ERROR ("failed unexpectedly") instead of ending like any other provider failure, and an account out of credit (HTTP 402) is reported as a malformed request by every provider but Anthropic. Callers that want to fall back on a missing key have to check availability before every run. Split off #30083 (2026-11-03).
Problem
Observed:
- A run started on a model whose provider has no key (secret not set) throws a TopLogicException out of AgentLoop.run. The log shows "Agent run ... failed unexpectedly" with a stack trace; the result of the run has no errorKind.
- A provider answering HTTP 402 (payment required) ends the run with errorKind=INVALID_REQUEST, except for the Anthropic provider, which says BILLING.
Expected: a run without a key ends like a run whose key the provider refused: StopReason.ERROR, errorKind=AUTHENTICATION, a ProviderError event, and a message that names the secret to provide - shown in the chat in the language of the user who reads it. HTTP 402 is BILLING for every provider.
Reproduce:
- Configure an agent on a model whose provider secret (e.g. tl_anthropic_api_key) is not set.
- Start a chat with it.
- The log has an ERROR with stack trace; TL-Script callers get an exception instead of a failed result.
Cause
AgentLoop.callRouted throws new TopLogicException(unavailable) when no model of the run has a provider with its credentials; nothing in the loop catches it. ErrorClassifier maps 402 to INVALID_REQUEST; only AnthropicMessagesProvider overrode it.
Proposal
- callRouted throws a ProviderException of the kind AUTHENTICATION, so the run ends on it like on any provider failure. *decided*
- The message naming the secret travels as resource key: ProviderException#errorKey() and AgentEvent.ProviderError#messageKey(); the chat translates it when it shows the error (ChatProcessor.providerErrorMessage). Logs, transcripts and the result of the run carry its text in the language of the log. *decided* (review of !2084, 2026-11-03)
- ErrorClassifier classifies 402 as BILLING for every provider. *decided*
Done when
- TestInactiveProvider passes: a run without a key ends ERROR / AUTHENTICATION, reports one ProviderError whose key names the secret, and leaves no health mark.
- TestChatErrorMessage passes: the chat shows the key of a provider error in the user's language.
- TestErrorClassifier passes: 402 is BILLING.
Related
- #30083 - the original cut.