feat(pi): native mode — self-spawned per-session proxy via in-process fetch interception (#519) - #524
Conversation
[bot] 🏷 Reviewing now. Plan: check out |
[bot] 🏷 Review complete — LGTM from my side. Every pre-flight claim in the body verified or independently reproduced on Reproduced
Smoke-tested beyond the suite
Code cross-checks
Minor (non-blocking): README.zh-CN.md wasn't updated — recent feature PRs (dsh launcher, dsh native Open questions (non-blocking, for your awareness): interactive multi-session pi behavior (does Merge order stands as stated: #523 first, then this. Merging itself is yours — per repo rules I don't merge PRs. |
|
Review vs master 3cd85b6: Good properties (verified):
Issues:
Recommendation: hold for the #513/#519 family decision; rework on top of a reworked #523; single shared interception core across pi (#519) and dsh (#521). |
… fetch interception (#519) Installs a 'thorough native plugin' for pi: the user runs plain 'pi' (no launcher, no env vars) and context compression works transparently. - src/agent/intercept.ts: installFetchInterceptor wraps globalThis.fetch and rewrites only http(s) requests whose target origin matches the active model's upstream to <proxyOrigin>/bili/<original-url> (zero-config prefix format); everything else passes through untouched; SSE responses streamed back as-is; reinstall replaces (never stacks); uninstall restores the original. - src/agent/pi.ts: marker-gated native path in the existing extension factory. With <stateDir>/billion-context/native.json {"pi":true} and no launcher proxy detected, session_start spawns 'node <pkgroot>/dist/index.js daemon --parent-pid <pi-pid>' (self-located from the plugin file position — no PATH lookup), parses the single-line JSON handshake, and installs the interceptor for ctx.model.baseUrl. Launcher-provided proxies always win; no marker -> byte-identical inert behavior; spawn failure -> warn + self-disable (never breaks the client). Host ctx data is snapshotted before the first await (pi's ctx getters throw once the session is replaced). Interceptor uninstalled + daemon SIGTERM'd on session_shutdown; the parent-pid watcher is the fallback reaper. - bili plugin install pi --native / remove: writes/clears the marker (per-agent keys, other agents preserved); status rows show '(native)'; --native rejected for non-capable agents (only pi is validated). - Tests: intercept unit matrix (inputs/duplex/exclusions/reinstall/uninstall) and native-pi gating tests (inert without marker, kill-switch over marker, launcher beats native, missing entry warns). Real-pi e2e validated against a mock upstream: round-1 request goes direct (daemon still starting), the SDK retry is intercepted and forwarded through the per-session proxy, PONG streams back, daemon reaps itself when pi exits.
e515720 to
51547cc
Compare
2ba40bf to
20f2538
Compare
|
Rebased onto the rebased #523 (head 20f2538 = 51547cc + this PR's own delta, cherry-picked 2ba40bf — same stacked structure, now conflict-free). Pre-flight on 20f2538: typecheck ✓ · 1261/1264 + 1 intentional skip (native-pi.test.ts:170 skips real-daemon spawn when dist/ present) · 2 known env fails in plugin-agent.test.ts, unrelated · build ✓. Merge #523 first (this branch now includes its rebased head). |
[bot] 🏷 Noted — verifying the rebased head |
[bot] 🏷 Rebased head Stacked structure
Pre-flight on
Discrepancy (minor): you reported 2 env fails in Verdict unchanged: LGTM, merge #523 first. |
|
Superseded by #706 (spawn-in-plugin form, chosen by the owner in #513): #706 is self-contained on master, needs neither this PR nor #523, and covers all of #519 (fetch interception + plugin glue) with a smaller surface (8 files, no marker/install-flag machinery). Closing to avoid two overlapping implementations of the same feature reaching master. My independent e2e validation of the shared fetch-patch mechanics remains valid evidence for the approach. |
What
The second half of issue #513 ("探索如何发布为native模式"): a thorough native plugin for pi where the user runs plain
pi— no launcher, no env vars — and context compression works transparently. Builds directly on #523 (daemon subcommand); merge #523 first (this PR is based on its branch).src/agent/intercept.ts(new):installFetchInterceptor({proxyOrigin, upstreamOrigins})wrapsglobalThis.fetchand rewrites only http(s) requests whose target origin matches the active model's upstream to<proxyOrigin>/bili/<original-url>(the existing zero-config prefix format). Everything else passes through untouched; responses stream back as-is (SSE-safe); reinstall replaces rather than stacks; uninstall restores the original fetch.src/agent/pi.ts: marker-gated native path inside the existing extension factory. With<stateDir>/billion-context/native.json{"pi": true}and no launcher proxy detected,session_startspawnsnode <pkgroot>/dist/index.js daemon --parent-pid <pi-pid>(package root self-located from the plugin file position — no PATH lookup), parses the single-line JSON handshake, and installs the interceptor for the current model's upstream origin. Launcher-provided proxies always win (byte-identical legacy behavior); no marker → inert; spawn failure → warn + self-disable (never breaks the client). Host ctx data is snapshotted before the first await because pi's ctx getters throw once the session is replaced (ExtensionRunner.assertActive). Interceptor uninstall + daemon SIGTERM onsession_shutdown; the parent-pid watcher (Windows 上 launcher 子代理从无优雅退出:stopProxy 直接 TerminateProcess,SIGBREAK-flush 全史 0 次触发,防抖窗状态+进行中轮被拦腰 #414) is the fallback reaper.bili plugin install pi --native/remove: writes/clears the per-agent marker key (other agents preserved,.bili-bakbackup);plugin listshows(native);--nativerejected for non-capable agents — pi is the only validated host so far.Why this shape
before_provider_requestresult = payload only;before_provider_headersreturn ignored — verified against earendil-works/pi source), so runtime egress interception is the only way to route an already-running process through a proxy started at session time. Spawn-time launcher wiring (MITM env, overlay homes,-e) by definition cannot apply to a live process.Known behavior (documented in README)
Round 1 of each session can outrun daemon startup (~1s) and goes direct to the upstream unproxied; from request 2 on everything flows through the proxy, and since agents resend full history, context tracking self-heals within one turn. Same failure class as the existing launcher-mode manifest race.
Pre-flight
resolveClientCommand: codex/claude resolve to themselves(verified failing identically on pristine master)pi -pturn spawns the per-session daemon (dynamic port), round-1 completion goes direct, the SDK retry is intercepted and forwarded through the proxy (forward POST → …/v1/chat/completions, SSE PONG streamed back through it), pi exits 0, daemon reaps itself ≤3s after exit via the parent-pid watcher. Validated against this branch's combined build (feat: bili daemon subcommand — per-session dynamic-port proxy (#518) #523's daemon + this PR's plugin).Follow-ups (not here): omp native (needs its own fetch-path validation, #520), dsh into PLUGIN_AGENTS (#521), gated e2e harness for the pi-native path.