Files
roboomp e5ea31b22c fix(coding-agent): require web_search_call in codex search
GPT-5.6 Responses-Lite models receive tool_choice "auto" (the forced
hosted choice is invalid under the lite shape, #5771/#5772), so the model
may answer without invoking the hosted web_search tool. The codex search
parser accepted any non-empty answer, returning a stale completion with
zero sources as a successful search.

callCodexSearch now tracks response.web_search_call.* events (and
web_search_call output items) and throws CodexNoWebSearchError when none
occurred. The candidate chain treats that error as retryable, advancing
default lite models to a non-lite model that forces web_search, and
surfaces a clear failure when the model was explicitly configured.

Fixes #6988

(cherry picked from commit a276cd0b3df1d0d041faf0a63fabcbb884e36a91)
2026-07-29 23:08:54 +02:00

65 lines
2.3 KiB
TypeScript

import { describe, expect, it, vi } from "bun:test";
import type { AuthStorage } from "@oh-my-pi/pi-ai";
import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
import { AgentStorage } from "@oh-my-pi/pi-coding-agent/session/agent-storage";
import type { SearchParams } from "@oh-my-pi/pi-coding-agent/web/search/providers/base";
import { searchCodex } from "@oh-my-pi/pi-coding-agent/web/search/providers/codex";
function makeSseResponse(): string {
return [
`data: ${JSON.stringify({ type: "response.web_search_call.completed", item_id: "ws_test" })}`,
"",
`data: ${JSON.stringify({
type: "response.output_item.done",
item: {
type: "message",
content: [
{
type: "output_text",
text: "Broker-backed Codex answer",
annotations: [{ type: "url_citation", url: "https://example.com/broker", title: "Broker" }],
},
],
},
})}`,
"",
`data: ${JSON.stringify({
type: "response.completed",
response: { id: "resp_codex_broker", model: "gpt-5-codex-mini" },
})}`,
"",
].join("\n");
}
describe("Codex web search broker auth", () => {
it("uses AuthStorage.getOAuthAccess for token + account metadata without opening AgentStorage", async () => {
const getOAuthAccess = vi.fn(async () => ({
accessToken: "broker-refreshed-access-token",
accountId: "broker-account-id",
}));
const authStorage = { getOAuthAccess } as unknown as AuthStorage;
const openSpy = vi.spyOn(AgentStorage, "open");
let requestHeaders: Headers | undefined;
const fetchMock: FetchImpl = async (_url, init) => {
requestHeaders = new Headers(init?.headers);
return new Response(makeSseResponse(), { status: 200, headers: { "Content-Type": "text/event-stream" } });
};
const params: SearchParams = {
query: "broker codex search",
systemPrompt: "Use web search.",
authStorage,
sessionId: "codex-broker-session",
};
const result = await searchCodex({ ...params, fetch: fetchMock });
expect(result.provider).toBe("codex");
expect(getOAuthAccess).toHaveBeenCalledWith("openai-codex", "codex-broker-session", { signal: undefined });
expect(requestHeaders?.get("authorization")).toBe("Bearer broker-refreshed-access-token");
expect(requestHeaders?.get("chatgpt-account-id")).toBe("broker-account-id");
expect(openSpy).not.toHaveBeenCalled();
});
});