feat(server): gate forward_auth targets on the caller's credential - #1
Merged
Conversation
Signed-off-by: Matthew Mckenzie <matthew.m.mckenzie@gmail.com>
|
Signed-off-by: Matthew Mckenzie <matthew.m.mckenzie@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A
forward_authclient sends the caller's own provider credential upstream, but it does sounconditionally. A caller with no credential is sent to the provider anyway and gets a 401
mid-task, and 401 is not fallback-eligible, so the request does not degrade to another target.
Separately, a route containing any Anthropic-forwarding target is callable only through
/v1/messages, so adding such a target to a mixed route rejects every OpenAI-format caller of it.Together these force a subscription-login upstream into its own dedicated route. This change makes
one route able to offer a forwarding upstream to the clients that have a login and keyed upstreams
to everyone else.
What changed
Caller-credential gating.
Metadatagainscaller_wire_format, the API family the callerarrived through — distinct from the existing
wire_format, which pins the outbound backendformat.
Backend::accepts_callerdecides eligibility: a backend with its own key always serves; aforwarding backend serves only when the caller's format is in its provider family and that
family's credential header is present and non-empty. The format half is the security property —
authorizationalone does not say which provider issued a token, so an OpenAI caller's bearer mustnever reach an Anthropic upstream. Companion headers (
chatgpt-account-id,x-openai-fedramp)identify an account, not a credential, and cannot make a target eligible alone. An absent caller
format falls back to the credential check, so embedded hosts that serve no HTTP API keep working.
RoutedLlmClientgains a defaultedcaller_eligibility, so no other client implementation changes.ClientRouter::eligible_candidatesfilters once and both call sites use it:call_first_available,the single funnel for routing-time and answer calls, so the gate is generic across every algorithm;
and
serve_decision_dependency, so/v1/routing/decidepredicts the target that would actuallyanswer. Skips log at INFO with the candidate and provider — never the token.
When every candidate is gated, the request fails with a new
LlmClientError::MissingCallerCredentialmapped to401/authentication_error/missing_caller_credential, with no upstream call.Route::caller_authis narrowed from "any callable target forwards this kind" to "every onedoes". A route whose targets all forward keeps today's fast descriptive
400when called throughthe other provider's API; a mixed route skips that pre-flight check and lets the per-candidate
filter decide.
Stage-router reserve targets.
stage_router's tiers are a fixed scored pair, so making aforwarding target the capable tier would evict the existing capable model from the route.
reserve_targetsadds fall-through-only targets built into theFallThroughset between the tiers(
[capable, ...reserves, efficient]), so a turn that escalated to the capable tier reaches areserve before dropping back to the efficient one.
StageTargetsandStageClassifierareuntouched — the signals and the optional judge still score exactly two names, and a reserve only
ever serves a request the chosen tier could not. Wired through
StageTierConfigfor bothstage_routerandcomposite, includingrouting_target_names()so reserve clients reach theClientRouter.Behaviour changes
forward_auth, called without the provider's credential, nowreturns Switchyard's
401/missing_caller_credentialinstead of relaying the provider's ownunauthenticated response.
moves to a per-candidate filter there.
Tests
backend.rs— keyed backend accepts everyone; forwarding backend needs its own provider'scredential (absent, empty, and no-metadata cases); cross-provider credential rejected in both
directions; companion-headers-only rejected; unknown caller format gated on the credential alone.
run.rs— a gated candidate is skipped without being called and the next one serves; everycandidate gated returns
MissingCallerCredentialwith zero calls.stage.rs— reserves fall through between the tiers, in target-set order.server.rs— a mixedstage_routerroute (Anthropic forwarding capable target, keyed reserve,keyed efficient target) serves an anonymous OpenAI caller, an OpenAI caller carrying a Codex
bearer, and an anonymous Anthropic caller all locally with zero Anthropic calls, then serves an
Anthropic caller carrying an OAuth bearer upstream with the beta header filtered to
oauth-*; asingle-target forwarding passthrough called without a credential — and with an empty one — returns
401with zero upstream calls.Verification
Docs
docs/reference/toml_schema.md(gating rule +reserve_targetson both route types),docs/routing_algorithms/stage_router_routing.md(reserve-targets section),docs/getting_started.md,crates/switchyard-server/README.md,crates/libsy-llm-client/README.md, andCHANGELOG.mdunder Added and Changed.