feat(gateway): expose active provider in runtime footer - #95135
jimmywu0816 wants to merge 1 commit into
Conversation
Add an opt-in `provider` runtime-footer field showing the provider that actually served the turn (fallback switches update agent.provider in place, so the footer reflects the live backend, not the configured primary). - gateway/runtime_footer.py: render `provider` when listed in fields; skipped silently when the runtime value is missing. Legacy default fields (model, context_pct, cwd) and byte-stability are preserved. - gateway/run.py: capture provider from the same live agent instance as model on both result paths and pass it to build_footer_line(). - tests: field ordering, missing/empty/whitespace provider, and the public build_footer_line() path. - docs (en + zh-Hans): document the new field.
Related: #67968 already covers the runtime-provider propagation bug with a broader root-path fix. This PR is the narrower built-in runtime-footer implementation; please consider the two together. |
This PR needs a rebase — its
|
| This PR changes | Status on main @ fa75692 |
|---|---|
run.py — _resolved_provider capture next to _resolved_model |
Capture point no longer exists in run.py; it now lives in gateway/run_turn_runner.py |
run.py — the two agent_result dicts ("provider": _resolved_provider,) |
Both dicts moved out of run.py |
run.py — GatewayRunner footer call site (_bfl(..., provider=...)) |
Now in gateway/run_turn.py |
The three _resolved_model hits still in main's run.py (L2204, L3317, L3476) are unrelated legacy references — a string in a key list, a legacy_dict_property shim, and a comment. None is the capture site this PR patches, so a naive grep will make the patch look rebasable when it isn't.
The code path on current main is split across two files — both halves need the field
The important part: on main the footer is producer + consumer in different files, and only the consumer survived the split. Patching the call site alone produces a footer that silently drops the field — no error, no log, and runtime_footer config looks perfectly correct.
1. Consumer — gateway/run_turn.py (~L1461), needs the argument added:
from gateway.runtime_footer import build_footer_line as _bfl
return _bfl(
user_config=_load_gateway_config(),
platform_key=_platform_config_key(source.platform), model=agent_result.get("model"),
provider=agent_result.get("provider"), # <-- add this
context_tokens=agent_result.get("last_prompt_tokens", 0) or 0,
context_length=agent_result.get("context_length") or None,
cwd=_terminal_scope_cwd(""), turn_seconds=_turn_seconds,
)2. Producer — gateway/run_turn_runner.py, in TurnRunner.run_sync() ("model" line is at L1663 on fa75692), the run result must carry the value. This is the half that was lost in the split, and the half this PR's run.py hunks were meant to cover. Add the field to the usage dict, right after the "model" line:
usage = {
"last_prompt_tokens": getattr(comp, "last_prompt_tokens", 0) if has_comp else 0,
"input_tokens": getattr(agent, "session_prompt_tokens", 0) if has_comp else 0,
"output_tokens": getattr(agent, "session_completion_tokens", 0) if has_comp else 0,
"model": getattr(agent, "model", None) if agent else None,
"provider": getattr(agent, "provider", None) if agent else None, # <-- add this
"context_length": (getattr(comp, "context_length", 0) or 0) if has_comp else 0,
}usage is then spread into common = {..., **usage} (L1681), and common is carried by both return paths of run_sync() — so one dict update replaces the two-dict duplication the pre-split run.py code needed.
3. gateway/runtime_footer.py — this half of the PR still applies as-is. The module was not split; _DEFAULT_FIELDS, the format_runtime_footer loop, and build_footer_line are all still where this PR expects them. The only note is that if you keep provider opt-in (out of _DEFAULT_FIELDS, as this PR does), users must list it explicitly — that's fine and I'd keep it that way.
Verification that actually catches the silent-failure mode
Grepping all four anchors green is not sufficient — the failure mode here is a None that propagates into _provider_short() → empty string → the field is dropped with no error anywhere. Assert on rendered output instead:
import sys; sys.path.insert(0, ".")
from gateway.runtime_footer import build_footer_line
cfg = {"display": {"runtime_footer": {"enabled": True,
"fields": ["model", "provider", "context_pct", "latency"]}}}
line = build_footer_line(user_config=cfg, platform_key=None,
model="deepseek-v4-flash", provider="opencode-go",
context_tokens=42000, context_length=200000,
cwd="/tmp", turn_seconds=12.0)
print(line) # expect on this PR's renderer: deepseek-v4-flash · opencode-go · 21% · 12sWith both halves applied I get model · provider · context_pct · latency in the footer, and tests/gateway/test_runtime_footer.py stays green (37 passed on v0.21.1 / WSL2). Note my local build maps provider ids to short labels (opencode-go → Go) — that part is user-specific and deliberately not something I'm proposing here; this PR's raw-value rendering (L129-132) is the right default for upstream.
Happy to test a rebased push on v0.21.1 / WSL2 if that's useful — just say the word.
(Disclosure: this comment was drafted with AI assistance from a local patch that addresses the same feature; all line references were verified against main @ fa75692 before posting.)
Correction to my comment above —
|
provider value passed |
footer shows |
|---|---|
"custom" (from agent.provider) |
deepseek-flash · custom · 21% · 12s |
"custom:deepseek" (from agent.requested_provider) |
deepseek-flash · DeepSeek官方 · 21% · 12s |
(the short labels come from my local build's map — upstream's raw-value renderer will show custom:deepseek, which is still strictly more informative than custom.)
I ran into this live: with the first version of the patch applied, the footer read custom for a custom:deepseek provider, which is what prompted the trace above. Worth an explicit test in the PR — asserting on a named custom provider (not just a built-in one like openrouter) is what catches it, since built-ins don't go through the collapse path.
Sorry for the noise; the line references in the previous comment stand, only that one attribute changes.
Independent provider-only implementation (based on current
main)This PR adds an opt-in
providerfield to the built-in gateway runtime footer, showing the provider that actually served the turn — not the configured primary. It is the provider slice of #35427, implemented independently and verified against the current upstreammain(f751a8c).Problem
agent.provideris updated in place when a fallback provider activates, but the built-in footer path (gateway/runtime_footer.py/ thebuild_footer_line()call site ingateway/run.py) never received it. A footer configured with a provider field had nothing to render, and any fallback switch was invisible in the footer.Change
gateway/runtime_footer.py— new opt-inproviderfield informat_runtime_footer()/build_footer_line()(keyword-only param, skipped silently when the runtime value is missing). Legacy default fields (model,context_pct,cwd) and_DEFAULT_FIELDSare untouched — byte-stability preserved.gateway/run.py— captureproviderfrom the same live agent instance asmodelon both result paths (empty reply + normal reply) and pass it tobuild_footer_line().tests/gateway/test_runtime_footer.py— field ordering, missing/empty/whitespace provider, and the publicbuild_footer_line()path (44 passed).Verification
Real-world gateway output with
fields: ["bot", "provider", "model", "context_pct"]:Relationship to existing PRs
config.model.providerfallback for legacy call sites — the field is silent when the runtime value is absent.Happy to close this in favor of #67968 if maintainers prefer to fold the doc/test coverage into that branch — the two are designed to be non-overlapping.