Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ async function handleResponsesInner(
responseEffects,
sendBudgetState,
);
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(
Expand Down
3 changes: 3 additions & 0 deletions src/server/responses/sidecar-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export async function executeResponsesSidecars(
|| (message as { toolCallId: string }).toolCallId.length === 0),
);
if (unpaired) {
// Local validation has no running sidecar body to own this lease.
releaseSearchProbeLease();
// Never interpolate the tool output: this message reaches the client and the logs.
return formatErrorResponse(
400,
Expand Down Expand Up @@ -341,6 +343,7 @@ export async function executeResponsesSidecars(
// the bridge entirely so enabling the feature doesn't break ordinary non-streaming traffic.
if (!parsed.stream) {
if (imgPlan) {
releaseSearchProbeLease();
return formatErrorResponse(400, "invalid_request_error", "image bridge requires stream=true");
}
// Video-only: skip bridge for non-streaming requests
Expand Down
4 changes: 2 additions & 2 deletions structure/transports/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ 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.

`src/server/responses/sidecar-execution.ts` owns search probe settlement: local validation releases immediately, bodyless responses release before returning, and upstream bodies retain the lease through completion, error or cancellation regardless of HTTP status. The core dispatcher does not infer body completion from a non-success status.

### Semantic progress ownership

The Responses proxy does not treat transcript growth as repository progress. It can observe request
Expand All @@ -72,9 +74,7 @@ retention limits, and the stall watchdog is a silence limit. None is a cumulativ
semantic no-progress budget.

> Decision record: [ADR-0031](../decisions/ADR-0031-responses-http-sse.md)

> Decision record: [ADR-0032](../decisions/ADR-0032-responses-http-sse.md)

> Decision record: [ADR-0033](../decisions/ADR-0033-responses-http-sse.md)

> Decision record: [ADR-0034](../decisions/ADR-0034-responses-http-sse.md)
Expand Down
48 changes: 48 additions & 0 deletions tests/responses/responses-run-turn-web-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,19 @@ mock.module("../../src/server/responses/request-sidecar-auth", () => ({ ...sidec
} as Awaited<ReturnType<typeof sidecarAuth.prepareResponsesSidecarAuth>>;
},
}));
let fixtureSidecarResponse: Response | undefined;
const webSearchModule = await import("../../src/web-search");
const executeWebSearch = webSearchModule.runWithWebSearch;
mock.module("../../src/web-search", () => ({ ...webSearchModule,
runWithWebSearch: async (...args: Parameters<typeof executeWebSearch>) =>
fixtureSidecarResponse ?? executeWebSearch(...args),
}));
const { handleResponses } = await import("../../src/server/responses");
const originalHome = process.env.OPENCODEX_HOME;
let home = "";
let release: (() => void) | undefined;
beforeEach(async () => {
fixtureSidecarResponse = undefined;
home = mkdtempSync(join(tmpdir(), "ocx-runturn-search-"));
process.env.OPENCODEX_HOME = home;
release = acquireOwnedSpendHome();
Expand Down Expand Up @@ -319,3 +327,43 @@ test("superseded 429 attempt cannot restore stale route state", async () => {
expect(owner(attempts[1])).not.toBe(owner(attempts[0]));
expect(owner(attempts[1])).not.toBe("stale-superseded");
});

for (const settlement of ["complete", "cancel", "error"] as const) {
test(`a non-success sidecar body retains its probe until ${settlement}`, async () => {
releasedFixtureProbe = false;
let controller!: ReadableStreamDefaultController<Uint8Array>;
fixtureSidecarResponse = new Response(new ReadableStream<Uint8Array>({
start(value) { controller = value; },
}), { status: 503, headers: { "content-type": "text/event-stream" } });
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;
let response: Response | undefined;
try {
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(503);
expect(releasedFixtureProbe).toBe(false);
if (settlement === "complete") {
controller.enqueue(new TextEncoder().encode("data: fixture-error\n\n"));
controller.close();
expect(await response.text()).toContain("fixture-error");
} else if (settlement === "cancel") {
await response.body!.cancel();
} else {
controller.error(new Error("fixture body failure"));
await expect(response.text()).rejects.toThrow("fixture body failure");
}
expect(releasedFixtureProbe).toBe(true);
} finally {
if (response?.body && !response.bodyUsed) await response.body.cancel();
fixtureSidecarResponse = undefined;
}
});
}
Loading