Repository navigation
perf(core): warm the model provider connection at prompt admission - #58803
kitlangton wants to merge 2 commits into
Conversation
Opens DNS/TCP/TLS to the model API's origin when a prompt is admitted and on model switch, with an empty unauthenticated GET, so the real request reuses the pooled socket. The origin comes from the session's last primary request after model.request hooks. Skips proxies, non-https, websocket, ai-sdk and HTTP-hooked routes; at most once per origin per 4 min; OPENCODE_DISABLE_PRECONNECT=1 opts out.
There was a problem hiding this comment.
The approach is sound. Warming is cheap, best-effort, sends only the origin with no credentials, skips cases where the warm socket wouldn't be reused, and can never fail a prompt. I ran model-preconnect.test.ts (10 pass, including the local-server connection-reuse and no-redirect tests) and the session-model-request and session-prompt suites (53 pass). The package typecheck couldn't run here because this Bun (1.4.2) has no bun check. I couldn't measure the latency gain end to end, since the simulated model isn't an https route and there are no provider credentials.
Public API: adds the OPENCODE_DISABLE_PRECONNECT environment variable (backwards compatible). The server also now makes a new kind of outbound request by default, an unauthenticated GET / to provider origins, which users behind strict egress rules may notice. No SDK, HTTP API, plugin or config changes.
One non-blocking design point, inline: the warm-up state lives in module-level maps rather than an Effect service.
| readonly model?: { readonly providerID: Provider.ID } | ||
| } | ||
|
|
||
| const targets = new Map<string, URL | undefined>() |
There was a problem hiding this comment.
targets and warmed are process-wide mutable state outside any Layer. Every instance, test and server in the process shares them, so the tests only stay independent by using unique directories and origins. The env opt-out is read from process.env at call time, which is why the test has to mutate process.env in beforeAll. Holding this in a small service (with a no-op layer for the session test preload instead of the env var) would make it scoped and easier to test. It would also drop the Bun.sleep waits in tests that check something did not happen.
There was a problem hiding this comment.
Moved into a ModelPreconnect Effect service (global node, per-instance state, noopLayer for tests), gated by experimental.preconnect read from Config; the env var and process.env mutation are gone, and the negative checks no longer sleep. 2a605ef
Moves the warm-up state into a ModelPreconnect Effect service with a no-op layer for tests, reads the flag from config (off by default), and drops OPENCODE_DISABLE_PRECONNECT.
There was a problem hiding this comment.
The earlier point about process-global state is addressed: the maps now live in a ModelPreconnect service with its own layer and noopLayer, and the env var and preload change are gone. Making it opt-in under experimental.preconnect also settles the default-on outbound request. I re-ran the preconnect, session-owned, model-request and prompt tests in core (84 pass) and session-instances in server (passes). The type check couldn't run here (no bun check on this Bun). I didn't verify it end to end, because the simulated model isn't an https route and there are no provider credentials.
Public API: adds the optional experimental.preconnect config key (boolean, default false), reflected in the OpenAPI spec and the generated client types. This is backwards compatible. The OPENCODE_DISABLE_PRECONNECT env var from the earlier revision is gone, so it never shipped.
One new point, inline: the provider-keyed target isn't scoped to the directory, so it can leak across projects.
|
|
||
| // A session without a model runs its agent's model, or the project's default, so it shares a target with every | ||
| // other such session on that agent and directory. | ||
| const key = (session: Selection) => |
There was a problem hiding this comment.
Now that this is a global node, the provider:<id> key is shared by every project in the process, but experimental.preconnect and provider baseURLs are per-location config. If project A has preconnect on and uses provider openai, a session in project B with openai selected gets warmed at admission or on switchModel. That happens even when B has preconnect off, or when B's openai points at a different gateway, in which case it warms an origin B never uses. Including session.location.directory in the provider key, as the agent key already does, would keep each project's opt-in and origins separate.
What it does
Every turn's first model request pays DNS, TCP and TLS setup when the provider socket has gone idle. This opens that connection while the prompt is admitted and its context loads, so the real request reuses a warm socket. It also warms the provider you switch to.
It's opt-in and off by default. To try it:
{ "experimental": { "preconnect": true, }, }How it works
ModelPreconnectEffect service (global node) holds the remembered origins and the per-origin throttle. Tests useModelPreconnect.noopLayeror a fresh layer.model.requesthooks resolve the route, each primary request readsexperimental.preconnectfrom config. When it's on, the request records its origin under the session's agent and directory (or the session's provider when a model is selected), and under the resolved provider.Session.promptlooks that origin up and fires a backgroundGETto it.switchModeldoes the same for the new provider.GETto the origin only (no path, query, credentials or headers), withredirect: "manual"and a 5 s timeout. The body is drained so HTTP/1.1 can reuse the socket. It goes at most once per origin every 4 min, under Bun's 5 min keep-alive.HEADisn't used because Bun 1.4.4 never pools aHEADconnection, and aHEADanswered withContent-Lengthcan leave the socket broken.fetch.preconnectrejects every URL on this Bun.Risk
false, nothing is remembered and no request is sent.HTTP(S)_PROXY/ALL_PROXYset, non-https base URLs, websocket transport, ai-sdk routes, providers withhttp.request/http.responsehooks, and non-Bun runtimes. The first prompt of a fresh server isn't warmed, since no route has been resolved yet.Proof
bun test test/model-preconnect.test.tspasses, 11 tests:false: no request.model.request-rewritten origin are warmed; HTTP-hooked providers are skipped.packages/server/test/session-instances.test.ts(the CI failure) passes.oxlint,prettier,check:generated: clean.Added by the opencode bot (AI-generated).
Public API: adds the optional experimental.preconnect config key (default false; backwards compatible)