39b5965569
Every fallback/dedup/skip decision asks one question — 'is this candidate the same backend as the one that failed, along the axis that failure invalidated?' — but it was re-implemented inline at six sites across four subsystems, each comparing whatever string was locally convenient. Each incident fixed one site while the others kept the bug: #22548, #70893, #59561, #72468, #62984/#54250/#57584. agent/backend_identity.py now owns the concept: BackendIdentity (provider / model / base_url axes), FailureScope (MODEL / CREDENTIAL / ENDPOINT — each failure class invalidates a different axis), and should_skip_candidate(). Unknown axes never manufacture a skip (over-skipping strands failover; a wrong try costs one RTT). Migrated sites: - chat_completion_helpers.try_activate_fallback: replaces the provider+model early-exit (the #62984 bug: ignored base_url, stranding multi-endpoint pools) AND _fallback_entry_is_same_backend_by_base_url (deleted) - auxiliary_client._try_configured_fallback_chain + _try_main_agent_model_fallback: replace label/model comparisons; auth and payment map to CREDENTIAL scope, keeping the #59561 carve-out - hermes_cli/fallback_cmd add: primary-match + duplicate checks now identity- aware (#54250/#57584): same provider+model on a different explicit base_url is a pool entry, not a duplicate _mark_provider_unhealthy stays label-keyed deliberately: its only triggers are confirmed 402s, which ARE credential-scoped. Owner-level tests pin each incident's semantics by number; sabotage-verified (removing the base_url axis fails the #62984 test).