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 test1893 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.ts2/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 racerefreshModelInfo 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-checkpluginHeadersMatchModel (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 raceselectionStillCurrent 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 test1899 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 项全绿,可以合并。

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