When a profile declares "fallback": ["kimi", "local"], create_llm_client
now returns a FailoverClient wrapper holding the ordered chain. For the
idempotent non-streaming path (chat_completion — conversation
compression, light subagents):
- Fail over on transient errors only: HTTP 429/5xx (now raised as
TransientLLMError by DeepSeekClient) and connection errors/timeouts
(previously swallowed into None by chat_completion's catch-all, now
re-raised). Auth/param 4xx failures keep returning None with no
failover — config problems stay loud, consistent with the H1/H2
error-early philosophy.
- Failed profiles enter a 60s in-process cooldown and are skipped
until it expires; chain exhaustion returns None per the existing
failure contract.
- Config structure errors fail at construction (non-list fallback,
unknown profile name, self-reference — H2-style). Environmental
problems (e.g. fallback profile missing its API key) skip that
fallback with a warning instead of blocking the primary.
- Unspecified attributes delegate to the primary client via
__getattr__ (name/model/api_key/cancel), so DeepSeekChat and
set_client keep working unchanged. stream_chat still delegates to
the primary directly; first-token failover is L4b.
create_llm_client refactored: single-profile construction extracted
into _construct_profile_client, shared by the main path and the
fallback chain (flat-api path untouched).
Tests: new TestL4aChatCompletionFailover (15 cases: chain build,
validation, 429/5xx/connection failover, 401 no-failover, exhaustion,
cooldown skip/expiry, missing-key skip with warning, transient raise,
4xx None regression, stream delegation); full suite 330 passed.