From ca4e6c41bb1d80ab3e8ec0a556afc7f9abb9a166 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Sat, 26 Sep 2026 09:33:53 +0000 Subject: [PATCH 1/6] fix(web-search): release probe on validation rejection --- src/server/responses/core.ts | 5 ++- .../responses-run-turn-web-search.test.ts | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 635d5e37c17..a6828f079ab 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -130,7 +130,10 @@ async function handleResponsesInner( responseEffects, sendBudgetState, ); - if (sidecarPlans instanceof Response) return sidecarPlans; + if (sidecarPlans instanceof Response) { + sidecarState.openAiSidecar?.releaseProbeLease?.(); + return sidecarPlans; + } const completionPolicy = createResponsesCompletionPolicy(requestContext, sidecarState); if (transportState.adapter.runTurn) return await executeResponsesRunTurn( requestContext, diff --git a/tests/responses/responses-run-turn-web-search.test.ts b/tests/responses/responses-run-turn-web-search.test.ts index 1477290f7ab..af449b7b169 100644 --- a/tests/responses/responses-run-turn-web-search.test.ts +++ b/tests/responses/responses-run-turn-web-search.test.ts @@ -40,6 +40,20 @@ mock.module("../../src/providers/request-pacing", () => ({ ...pacing, return originalWaitForSlot(...args); }, })); +const sidecarAuth = await import("../../src/server/responses/request-sidecar-auth"); +const prepareResponsesSidecarAuth = sidecarAuth.prepareResponsesSidecarAuth; +let releasedFixtureProbe = false; +mock.module("../../src/server/responses/request-sidecar-auth", () => ({ ...sidecarAuth, + prepareResponsesSidecarAuth: async (...args: Parameters) => { + if (args[0].req.headers.get("x-fixture-probe") !== "held") { + return prepareResponsesSidecarAuth(...args); + } + return { + routedCompaction: false, + openAiSidecar: { releaseProbeLease: () => { releasedFixtureProbe = true; } }, + } as Awaited>; + }, +})); const { handleResponses } = await import("../../src/server/responses"); const originalHome = process.env.OPENCODEX_HOME; let home = ""; @@ -131,6 +145,27 @@ test.each(["image", "video"] as const)("media-only %s bridge still injects its t expect(attempts[0].context.tools?.some(t => t.webSearch)).toBe(false); }); +test("releases a search probe when pre-dispatch validation rejects the request", async () => { + releasedFixtureProbe = false; + const config = { + port: 0, defaultProvider: "cursor", + webSearchSidecar: { backend: "exa", exaApiKey: "fixture-search-key" }, + providers: { + cursor: { adapter: "cursor", baseUrl: "https://api2.cursor.sh", authMode: "oauth", models: ["model"] }, + }, + } as OcxConfig; + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json", "x-fixture-probe": "held" }, + body: JSON.stringify({ model: "cursor/model", input: [{ + type: "function_call_output", output: "fixture result", + }], tools: [{ type: "web_search" }] }), + }), config, { model: "", provider: "" }); + + expect(response.status).toBe(400); + expect(releasedFixtureProbe).toBe(true); + expect(attempts).toHaveLength(0); +}); + // Streaming only: a first-event 429 replays the turn while the superseded // attempt is still in-flight (the buffered path awaits it before collecting // events, so the race cannot exist there). When that attempt finally returns, From 7266ca2262f65935eaf054762cb1c18dc7ad8ffe Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:29:28 +0000 Subject: [PATCH 2/6] fix(web-search): keep the probe lease on streamed sidecar responses Co-Authored-By: Epinephrine --- src/server/responses/core.ts | 4 +- .../responses-run-turn-web-search.test.ts | 52 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index a6828f079ab..bf2575fbfab 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -131,7 +131,9 @@ async function handleResponsesInner( sendBudgetState, ); if (sidecarPlans instanceof Response) { - sidecarState.openAiSidecar?.releaseProbeLease?.(); + // A streamed sidecar result (web search / media bridge) still owns the probe: the loop + // releases it when the stream settles. Only a rejection is terminal here. + if (!sidecarPlans.ok) sidecarState.openAiSidecar?.releaseProbeLease?.(); return sidecarPlans; } const completionPolicy = createResponsesCompletionPolicy(requestContext, sidecarState); diff --git a/tests/responses/responses-run-turn-web-search.test.ts b/tests/responses/responses-run-turn-web-search.test.ts index af449b7b169..f84e312d803 100644 --- a/tests/responses/responses-run-turn-web-search.test.ts +++ b/tests/responses/responses-run-turn-web-search.test.ts @@ -28,9 +28,23 @@ function fixture(provider: OcxProviderConfig): ProviderAdapter { }, }; } +function fetchFixture(provider: OcxProviderConfig): ProviderAdapter { + return { + name: "fetchonly", + buildRequest: () => ({ url: provider.baseUrl, method: "POST", headers: {}, body: "{}" }), + fetchResponse: async () => new Response("{}", { status: 200 }), + async *parseStream() { + yield { type: "text_delta", text: "search-enabled answer" } as AdapterEvent; + yield { type: "done" } as AdapterEvent; + }, + async parseResponse() { return [{ type: "done" }] as AdapterEvent[]; }, + }; +} mock.module("../../src/server/adapter-resolve", () => ({ ...resolver, resolveAdapter: (provider: OcxProviderConfig, cache?: "none" | "short" | "long") => - provider.adapter === "cursor" ? fixture(provider) : resolveAdapter(provider, cache), + provider.adapter === "cursor" ? fixture(provider) + : provider.adapter === "fetchonly" ? fetchFixture(provider) + : resolveAdapter(provider, cache), })); const pacing = await import("../../src/providers/request-pacing"); const originalWaitForSlot = pacing.waitForProviderRequestSlot; @@ -166,6 +180,42 @@ test("releases a search probe when pre-dispatch validation rejects the request", expect(attempts).toHaveLength(0); }); +test("a streamed sidecar response keeps the search probe until the stream settles", async () => { + releasedFixtureProbe = false; + const realFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(typeof input === "object" && "url" in input ? input.url : input); + if (url.includes("exa")) { + return new Response(JSON.stringify({ results: [{ title: "fixture", url: "https://fixture.test" }] }), + { status: 200, headers: { "content-type": "application/json" } }); + } + return new Response( + 'event: response.completed\ndata: {"type":"response.completed","response":{"output":[]}}\n\n', + { status: 200, headers: { "content-type": "text/event-stream" } }); + }) as typeof fetch; + try { + const config = { + port: 0, defaultProvider: "fetchonly", + webSearchSidecar: { backend: "exa", exaApiKey: "fixture-search-key" }, + providers: { + fetchonly: { adapter: "fetchonly", baseUrl: "https://fetchonly.test/v1", apiKey: "fixture-key", models: ["model"] }, + }, + } as OcxConfig; + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json", "x-fixture-probe": "held" }, + body: JSON.stringify({ model: "fetchonly/model", input: "search this", stream: true, + tools: [{ type: "web_search" }] }), + }), config, { model: "", provider: "" }); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("event-stream"); + expect(releasedFixtureProbe).toBe(false); + await response.text(); + } finally { + globalThis.fetch = realFetch; + } +}); + // Streaming only: a first-event 429 replays the turn while the superseded // attempt is still in-flight (the buffered path awaits it before collecting // events, so the race cannot exist there). When that attempt finally returns, From 746d2ccd8a38eb609ab289aba41c13d15fa3bb57 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:46:44 +0000 Subject: [PATCH 3/6] fix(web-search): release search probe when a streamed sidecar response settles A fetch-based web-search stream keeps the account's quota-recovery probe while it is live (a later in-stream sidecar outcome may still clear the cooldown), but when the routed model answers without calling web_search no outcome ever consumes it. Hand the lease back through the stream's own completion/cancel tracker and the terminal no-body return, matching the release ownership executeResponsesRunTurn already uses. Co-Authored-By: Epinephrine --- src/server/responses/sidecar-execution.ts | 17 +++++++++++++++-- .../responses-run-turn-web-search.test.ts | 3 +++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/server/responses/sidecar-execution.ts b/src/server/responses/sidecar-execution.ts index 24ffc9f126c..3bea7c078de 100644 --- a/src/server/responses/sidecar-execution.ts +++ b/src/server/responses/sidecar-execution.ts @@ -104,6 +104,17 @@ export async function executeResponsesSidecars( cancelResponseCompletion, } = responseEffects; + // Resolving the OpenAI search credential may hold the account's sole + // cooldown-recovery probe lease. A streamed sidecar result keeps it until the + // stream settles — completion or client cancel — so a later in-stream search + // outcome can still clear the cooldown; a response with no live body is + // terminal, so the lease is handed back before returning it. A recorded + // search outcome already settled the lease, making each release a + // generation-bound no-op. + const releaseSearchProbeLease = (): void => { + openAiSidecar?.releaseProbeLease?.(); + }; + // Tool results are PAIRED by call_id. parseRequest writes it into OcxToolResultMessage.toolCallId // (parser.ts:738/752) without validating it, because inputItemSchema's permissive catch-all @@ -402,11 +413,12 @@ export async function executeResponsesSidecars( if (imgResponse.body) { const imgTurnAc = new AbortController(); imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true }); - return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, undefined, options.turnAdmissionLease), { + return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, releaseSearchProbeLease, options.turnAdmissionLease), { status: imgResponse.status, headers: imgResponse.headers, }); } + releaseSearchProbeLease(); return imgResponse; } // end else (streaming bridge) } @@ -478,11 +490,12 @@ export async function executeResponsesSidecars( if (wsResponse.body) { const wsTurnAc = new AbortController(); wsTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true }); - return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, undefined, options.turnAdmissionLease), { + return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, releaseSearchProbeLease, options.turnAdmissionLease), { status: wsResponse.status, headers: wsResponse.headers, }); } + releaseSearchProbeLease(); return wsResponse; } diff --git a/tests/responses/responses-run-turn-web-search.test.ts b/tests/responses/responses-run-turn-web-search.test.ts index f84e312d803..d944b8c67b4 100644 --- a/tests/responses/responses-run-turn-web-search.test.ts +++ b/tests/responses/responses-run-turn-web-search.test.ts @@ -211,6 +211,9 @@ test("a streamed sidecar response keeps the search probe until the stream settle expect(response.headers.get("content-type")).toContain("event-stream"); expect(releasedFixtureProbe).toBe(false); await response.text(); + // The routed model answered without a web_search call, so no sidecar outcome + // settled the lease — the stream's own completion hands the probe back. + expect(releasedFixtureProbe).toBe(true); } finally { globalThis.fetch = realFetch; } From b82bb02e1fa73ac38533b5eaf75fabe0136e0b04 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:00:38 +0000 Subject: [PATCH 4/6] test(responses): cover probe release on cancel and media-bridge settle The streamed-sidecar probe test only covered web-search completion; add the cancellation path (tracked stream cancel settles the lease) and the media-bridge stream, which shares the same ownership handoff. Co-Authored-By: Epinephrine --- .../responses-run-turn-web-search.test.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/responses/responses-run-turn-web-search.test.ts b/tests/responses/responses-run-turn-web-search.test.ts index d944b8c67b4..afbf1d50c49 100644 --- a/tests/responses/responses-run-turn-web-search.test.ts +++ b/tests/responses/responses-run-turn-web-search.test.ts @@ -219,6 +219,60 @@ test("a streamed sidecar response keeps the search probe until the stream settle } }); +test("a cancelled streamed sidecar response releases the search probe", async () => { + releasedFixtureProbe = false; + const realFetch = globalThis.fetch; + globalThis.fetch = (async () => new Response( + 'event: response.completed\ndata: {"type":"response.completed","response":{"output":[]}}\n\n', + { status: 200, headers: { "content-type": "text/event-stream" } })) as typeof fetch; + try { + const config = { + port: 0, defaultProvider: "fetchonly", + webSearchSidecar: { backend: "exa", exaApiKey: "fixture-search-key" }, + providers: { + fetchonly: { adapter: "fetchonly", baseUrl: "https://fetchonly.test/v1", apiKey: "fixture-key", models: ["model"] }, + }, + } as OcxConfig; + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json", "x-fixture-probe": "held" }, + body: JSON.stringify({ model: "fetchonly/model", input: "search this", stream: true, + tools: [{ type: "web_search" }] }), + }), config, { model: "", provider: "" }); + + expect(response.status).toBe(200); + expect(releasedFixtureProbe).toBe(false); + // Client disconnect: the tracked stream's cancel path must settle the lease the same + // way a completed stream does, or the probe stays held until process exit. + await response.body!.cancel(); + expect(releasedFixtureProbe).toBe(true); + } finally { + globalThis.fetch = realFetch; + } +}); + +test("a media-bridge stream releases the search probe when it settles", async () => { + releasedFixtureProbe = false; + events = [[{ type: "text_delta", text: "media answer" }, { type: "done" }]]; + const config = { + port: 0, defaultProvider: "cursor", + images: { bridgeEnabled: true }, + providers: { + cursor: { adapter: "cursor", baseUrl: "https://api2.cursor.sh", authMode: "oauth", models: ["model"] }, + }, + } as OcxConfig; + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json", "x-fixture-probe": "held" }, + body: JSON.stringify({ model: "cursor/model", input: "draw a fixture", stream: true, + tools: [{ type: "image_generation" }] }), + }), config, { model: "", provider: "" }); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("event-stream"); + expect(releasedFixtureProbe).toBe(false); + await response.text(); + expect(releasedFixtureProbe).toBe(true); +}); + // Streaming only: a first-event 429 replays the turn while the superseded // attempt is still in-flight (the buffered path awaits it before collecting // events, so the race cannot exist there). When that attempt finally returns, From e6f432d0155eee1a983d7a52c3fb3c3b28e4af52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 04:35:53 +0000 Subject: [PATCH 5/6] fix(responses): keep sidecar probe cleanup within composition-root budget --- src/server/responses/core.ts | 8 ++------ structure/transports/responses.md | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index bf2575fbfab..aea67f586d2 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -130,12 +130,8 @@ async function handleResponsesInner( responseEffects, sendBudgetState, ); - if (sidecarPlans instanceof Response) { - // A streamed sidecar result (web search / media bridge) still owns the probe: the loop - // releases it when the stream settles. Only a rejection is terminal here. - if (!sidecarPlans.ok) sidecarState.openAiSidecar?.releaseProbeLease?.(); - return sidecarPlans; - } + if (sidecarPlans instanceof Response && !sidecarPlans.ok) sidecarState.openAiSidecar?.releaseProbeLease?.(); + if (sidecarPlans instanceof Response) return sidecarPlans; const completionPolicy = createResponsesCompletionPolicy(requestContext, sidecarState); if (transportState.adapter.runTurn) return await executeResponsesRunTurn( requestContext, diff --git a/structure/transports/responses.md b/structure/transports/responses.md index f47a747511c..2caa549a400 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -61,6 +61,13 @@ the code let a provider-scoped transport past it is what #4992 recorded, and it regression for this policy has to enter through `handleResponses` rather than through a hand-written override that cooperates by calling the executor it was handed. +### Search-probe lifetime + +`src/server/responses/core.ts` releases the search probe for a rejected sidecar response. +`src/server/responses/sidecar-execution.ts` retains it across successful streaming search +and image responses until completion or cancellation, and releases bodyless responses +before returning. Releasing an already-settled probe is generation-bound and idempotent. + ### Semantic progress ownership The Responses proxy does not treat transcript growth as repository progress. It can observe request From 1681984ab28eef5261aa16a3fcbbdfe4fa2c72d7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 04:41:35 +0000 Subject: [PATCH 6/6] docs(responses): retain existing contract without exceeding its size budget --- structure/transports/responses.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 2caa549a400..f47a747511c 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -61,13 +61,6 @@ the code let a provider-scoped transport past it is what #4992 recorded, and it regression for this policy has to enter through `handleResponses` rather than through a hand-written override that cooperates by calling the executor it was handed. -### Search-probe lifetime - -`src/server/responses/core.ts` releases the search probe for a rejected sidecar response. -`src/server/responses/sidecar-execution.ts` retains it across successful streaming search -and image responses until completion or cancellation, and releases bodyless responses -before returning. Releasing an already-settled probe is generation-bound and idempotent. - ### Semantic progress ownership The Responses proxy does not treat transcript growth as repository progress. It can observe request