Skip to content

feat: runtime-info protocol — plugins report real model config to the proxy (#955) - #956

Merged
ranxianglei merged 4 commits into
masterfrom
2026-09-18_runtime-info
Sep 19, 2026
Merged

ranxianglei merged 4 commits into
masterfrom
2026-09-18_runtime-info

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #955

What

Native plugins live INSIDE the client process, so they can read the client's real model configuration. This PR defines and implements the runtime-info protocol so the proxy stops guessing (models.dev snapshot / built-in table / first-request learning) and gets the truth pushed upstream:

  • Per-request headers (gated on x-bili-plugin): x-bili-plugin-context-window (existed) + new x-bili-plugin-max-output, x-bili-plugin-model
  • Bootstrap endpoint POST /__bili/plugin/runtime-info (loopback): {agent, model, contextWindow, maxOutput, baseURL, source} — sent at plugin bootstrap and on model switch, per-agent LRU table
  • Chain: window resolution becomes anthropic-beta > header report > runtime-info table (agent+model must match — a stale post-switch entry can never size a different model) > launcher env > route config > registry > table
  • maxOutput feeds the output-headroom reservation when the request body carries no budget of its own (Codex Responses 不发送 max_output_tokens 时 output headroom 不生效,是否可 fallback 到模型 output 配置? #924 fallback order: runtime-info > configured > registry)

Plugin implementations

  • dsh-native (was the big gap — stamped nothing): reads ctx.agentDefaultModel.currentSelection() + ctx.llm.resolveModelInfo(provider, model) via dynamic ctx.inject (missing services degrade gracefully), caches, stamps all three headers, reports at bootstrap/switch
  • opencode v1: new extractV1Outputs (limit.output), stamps model + max-output alongside the existing window
  • opencode v2: catalog limit.output map, same stamps
  • pi: ctx.model.id + optional maxTokens, same stamps + report with baseURL

Observability

/__bili/plugin/status now returns model, windowSource, runtimeInfo; the [window] log line has a runtime-info source.

Docs

README (en/zh) gained a protocol table — this is the contract other client integrations (see sibling issues) build against.

Pre-flight

  • npm run typecheck ✅
  • npm test 1893 pass / 0 fail ✅
  • npm run build ✅

… proxy (#955)

- New per-request headers x-bili-plugin-max-output / x-bili-plugin-model
  (gated on x-bili-plugin, like the existing window header)
- New POST /__bili/plugin/runtime-info endpoint: bootstrap + model-switch
  report of {agent, model, contextWindow, maxOutput, baseURL, source},
  per-agent LRU table, consulted in the native-window chain directly under
  the per-request header report (agent+model must match)
- Reported maxOutput stands in for output-headroom reservation when the
  request body carries no budget of its own (#924 fallback order)
- dsh-native: reads ctx.llm.resolveModelInfo + ctx.agentDefaultModel
  (dynamic ctx.inject) and stamps model/window/max-output headers + report
- opencode v1 (extractV1Outputs), opencode v2 (catalog limit.output),
  pi: add model/max-output stamps + change-detected runtime report
- /__bili/plugin/status now exposes model, windowSource, runtimeInfo
- Tests: header parsing gates, endpoint validation/LRU/model guard,
  change-detection with failure rollback, dsh stamp, 3 e2e chain cases
- README (en/zh): runtime-info protocol table for future client ports
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-18_runtime-info (3f3e5e9)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-956

Each push to this PR publishes a new version under the pr-956 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr956.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review started on PR #956 (branch 2026-09-18_runtime-info, head 431cae3, base = current master def3910 — no rebase needed).

Initial state:

  • CI: 4 checks green so far (version-guard, build-artifact, e2e zero-token preflight, codex MCP overlay); the 4× test matrix (ubuntu/windows × node 22/24) is still in progress.
  • Scope: 11 files, +676/−14 — new protocol surface (headers + bootstrap endpoint), so this is a feat that stays human-merge per §7.4; my job is to verify the implementation matches the described contract.

I'm now walking the diff file by file against the protocol table in the PR description. Will report findings (or a mergeable verdict once CI is green) shortly.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 PR #956 review — complete ✅

Verification (ran locally against branch head 431cae3; base master@def3910 is current — no rebase needed)

  • npm run typecheck clean · npm test → 1893 pass / 0 fail / 2 skipped · npm run build ok — matches the PR's pre-flight claims exactly (independently re-run, not taken on trust).
  • E2E pipeline regression (§3 mandates this since src/server.ts changed): ACP_TEST_E2E_FAKE=1 tests/e2e/e2e-codex-fake.test.ts → 2/2 pass (real codex 0.147.0 through the built proxy: overflow compression phase + under-window control). The full 4-phase ACP_TEST_E2E needs an upstream unreachable from here, but CI's fake-upstream job exercises the same path and is green.
  • CI at head: all 8 checks green (test matrix ubuntu/windows × node 22/24, build-artifact, version-guard, fake-upstream e2e, CODEX_HOME overlay).
  • Diff hygiene: single linear commit, 11 files, every line scoped to the feature; package.json / package-lock.json untouched (version guard respected).

Code review

  • Endpoint perimeter: POST /__bili/plugin/runtime-info sits under the existing /__bili/* admin gate (tunnel-marker rejection + loopback-only + trusted origin) — same perimeter as /compact. The runtime-info table is consulted only when the request carries x-bili-plugin, so plain clients are unaffected; the agent+model exact-match requirement means a stale post-switch entry can never size a different model. ✅
  • New header parsing mirrors the existing pluginContextWindowHeader style (gated, parseInt + >0); status-endpoint additions (model / windowSource / runtimeInfo) are loopback-gated. ✅
  • Agent side: report-once-per-(agent, model) with rollback-on-failure so a failed bootstrap POST retries on the next stamp; dsh-native degrades gracefully when llm / agentDefaultModel services are missing. ✅
  • Docs en/zh in sync. ✅

One minor finding (non-blocking): dsh-native's refreshModelInfo is async — if the model selection changes mid-resolve, the in-flight stale result overwrites the cache and its finally reports the old model, so headersFor can briefly stamp model A's window on a request whose body model is B until the next refresh self-heals (one-turn transient mis-sizing). Relatedly, the proxy trusts per-request window/max-output headers without cross-checking x-bili-plugin-model against the body model (pre-existing trust model — the table path does require a match). Optional hardening: ignore window/max-output headers whenever the model header ≠ body model. Happy to pick that up in a follow-up if you want it.

Verdict: code matches the description, all claims independently verified, CI fully green. Per §7.4 this is a feat: (new protocol surface) → human-merge.

中文摘要:审查了 runtime-info 协议(插件上报真实模型配置、窗口解析链新增 runtime-info 档、maxOutput 输出预留兜底)——本地独立复验 typecheck / 1893 项测试 / 构建 / 真 codex E2E 全部通过,CI 8 项检查全绿,diff 干净且未动版本号;仅发现一个 dsh-native 异步解析的瞬时竞态(非阻塞,可选后续加固),可以合并。

Acceptance criterion #1 ("dsh session: /acp before first request shows
model + window from client config") was not met: the runtime-info table
was only consulted once a session existed, so the latest-session probe a
client without a stable id sends (conversationId=<agent>&fallback=latest)
404'd until the first model request landed.

- handlePluginStatus now answers that probe from the agent-keyed runtime
  table (phase: "pre-first-request") when no session resolves; original
  404 messages preserved otherwise
- dsh statusOutcome renders the reported model/window/maxOutput/source
  instead of the generic "no model request yet" notice
- tests: e2e pre-first-request status (report->200, empty->404, real
  session takes over after traffic), dsh /acp render of the probe response;
  harness now isolates XDG_STATE_HOME + resets the in-memory session map so
  "no session yet" preconditions are deterministic
- README en/zh: one line documenting the pre-first-request behavior
…eck on plugin headers

Review findings on PR #956:
- dsh-native refreshModelInfo: an in-flight resolve whose model selection
  changed mid-flight must not overwrite the cache or report the OLD model
  (one-turn transient mis-sizing); commit + report are now gated on the
  live selection still matching what was resolved.
- server: per-request x-bili-plugin-context-window / -max-output headers are
  now ignored when x-bili-plugin-model differs from the request body's model
  (exact or last path segment — openai bodies use provider/model composites).
  A missing model header keeps the pre-#956 trust.
@ranxianglei

Copy link
Copy Markdown
Owner Author

Pushed bbef749 addressing both review findings:

  1. dsh mid-switch race — refreshModelInfo now gates cache-commit AND the report on the live selection still matching what was resolved (selectionStillCurrent); a mid-flight model switch discards the stale result instead of overwriting, and the next headersFor refresh re-resolves the new model.
  2. header/model cross-check — new pluginHeadersMatchModel: per-request x-bili-plugin-context-window / -max-output are ignored when x-bili-plugin-model ≠ body model (exact match or last path segment, for provider/model composite bodies). Missing model header keeps pre-feat: runtime-info protocol — plugins report real model config to the proxy (#955) #956 trust. Applied at both the window chain and the headroom fallback.

New tests: cross-check semantics (4 cases) + existing suites re-run. Local: typecheck clean, 1896 pass / 0 fail, build ok.

… review)

The selectionStillCurrent gate in refreshModelInfo had no dedicated test:
drive apply() with a gated async resolve of model A, switch the live
selection to B mid-resolve, assert A's numbers never land in the cache or
headers, then assert the next refresh self-heals by re-resolving B.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Round-2 review complete ✅ — reviewed both new commits (c1bf0e4 pre-first-request /acp, bbef749 hardening), re-ran everything locally on the final head, and pushed one test-only fix-up (3f3e5e9).

Both fixes verified correct

  1. Header/model cross-check — pluginHeadersMatchModel (src/plugin.ts:133): missing model header keeps pre-feat: runtime-info protocol — plugins report real model config to the proxy (#955) #956 trust; body model absent → trust; otherwise exact or last-path-segment match. Confirmed it gates both consumption sites: the window chain (src/server.ts:1015, where model is the request-body model from line 987) and the headroom fallback (~line 1486, fbModel0). The table path already required exact agent+model match, so all three trust layers now agree. The last-segment rule means two providers sharing a bare model name would cross-match — documented in-code as intentional (the guard targets mid-switch races, not multi-provider collisions). Fine.
  2. dsh mid-switch race — selectionStillCurrent re-reads the live selection after resolve and gates both the cache commit (success and failure paths) and the bootstrap report (the finally also requires the cached entry to be the one just resolved). I traced every interleaving: mid-switch success/failure discards stale A; A→B→A re-commits correctly; a throwing/absent currentSelection fails conservative (discard, no report); the refreshing flag still serializes resolves so no double-flight. Self-heal works: next headersFor sees cache ≠ live selection and re-resolves B. Correct and minimal.

c1bf0e4 (pre-first-request /acp) — the status endpoint's no-session branch now answers from the agent-keyed runtime table (phase: "pre-first-request") before falling through to the original 404 messages (both variants preserved); dsh renders model/window/maxOut/source from the entry. Endpoint stays loopback-gated; response only echoes config the probing client itself reported. Tests cover report→200, empty→404, and hand-off to the real session once traffic lands; harness now isolates XDG_STATE_HOME for deterministic "no session yet" preconditions. Docs en/zh synced. OK.

One gap I fixed directly (per repo review rules): the race guard had no regression test. Pushed 3f3e5e9 — single-file, test-only (+46 lines): drives apply() with a gated async resolve of model A, switches the live selection to B mid-resolve, asserts A's numbers never land in the cache or headers, then asserts the next refresh self-heals by stamping B's window/max-output.

Verification on final head 3f3e5e9

  • Local: typecheck clean · npm test → 1899 pass / 0 fail / 2 skipped (my 431cae3 baseline 1893 + 4 from c1bf0e4 + 1 from bbef749 + 1 mine — deltas fully accounted) · build ok · E2E fake-upstream (real codex through built proxy) 2/2 pass.
  • CI: all 8 checks green (4× test matrix, build-artifact, version-guard, fake-upstream e2e, CODEX_HOME overlay).

Verdict: findings addressed completely, no new issues found, green across the board. Per §7.4 this remains a feat: → human-merge.

中文摘要:二轮审查确认两处加固(dsh 中途切模型的竞态守卫、plugin 头与请求体 model 的交叉校验)实现正确且覆盖全部路径,另审了新增的 /acp 首请求前应答逻辑无问题;竞态守卫原本缺回归测试,我已直接在 PR 分支补上(3f3e5e9,纯测试提交),本地 typecheck/1899 项测试/构建/真 codex E2E 全部通过、CI 8 项全绿,可以合并。

@ranxianglei
ranxianglei merged commit b6c2ef3 into master Sep 19, 2026
8 checks passed
antclt pushed a commit to antclt/billion-context that referenced this pull request Oct 1, 2026
…r strips x-bili-plugin-context-window for the process lifetime

Root cause of the ranxianglei#1812 mis-sizing handshake gap: dsh's catalog always
knows a window (user-set models[].contextWindow or the built-in default),
and headersFor stamps x-bili-plugin-context-window from the
ModelInfoCache — but refreshModelInfo cached {provider, model} (no
window) whenever resolveModelInfo failed once, and the early-return only
compared provider+model, so a single boot-time race (catalog still
loading) latched the process headerless. Every request of the session
then sized against bili's unconfigured 200k fallback.

Observed live on the same dsh install against the same model: two proxy
runs sent x-bili-plugin-model without any context-window header (proxy
warned ranxianglei#1569 and only survived via a lucky registry-peek match), a third
run sent the header on every request — intermittency = the boot race.

Fix: a matching cache entry is final only when it carries a window; a
failure-shaped entry retries after a 30s cooldown (test hook:
BILI_MODEL_INFO_RETRY_MS). Successful resolves with a window clear the
retry state; selection-change semantics from ranxianglei#956 are untouched.

Test: new dsh-native unit test — resolve fails twice, model id still
stamped, window absent; after the cooldown the next stamp carries
x-bili-plugin-context-window: 262144 (attempts >= 3).

Part of ranxianglei#1812 (handshake root cause; the error-text extraction rework
stays in ranxianglei#1818).
@ranxianglei
ranxianglei deleted the 2026-09-18_runtime-info branch October 15, 2026 05:32
RodrigoHamuy pushed a commit to RodrigoHamuy/billion-context that referenced this pull request Oct 15, 2026
…ranxianglei#3261)

The hermes native plugin stamped model/max-output but never the context
window, so custom-alias models (absent from every registry) sized sessions
at the 200K default even when the host knew the real window. Now the
plugin reads HERMES_HOME/context_length_cache.yaml (the host's own
per-model window cache, keyed <model>@<base_url>) best-effort and reports
it on both ranxianglei#955/ranxianglei#956 channels: x-bili-plugin-context-window header in the
llm_request middleware and contextWindow in the runtime-info body (dedup
key extended so a changed window re-reports). Base URL resolution:
hook/request-supplied fields first, then the provider entry in
config.yaml; matching is exact-URL then origin-granularity (cache keeps
/v1, configs often omit it); conflicting per-upstream windows are never
guessed (fail-open to the proxy's normal chain, like every other miss).

Adjacent fix in the same call path: on_pre_api_request read 'table'
unconditionally while binding it only inside the max_tokens-present
branch — a hook fire without max_tokens raised NameError into the
swallowing except and silently skipped the whole runtime-info report.
Hoisted the binding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[native] runtime-info protocol: plugins report real model config (model / contextWindow / maxOutput) to the proxy

1 participant