Merge PR #7667: fix(web-search): keep GPT-5.6 hosted tools top-level (@roboomp)

This commit is contained in:
can1357
2026-08-05 21:50:25 +02:00
3 changed files with 12 additions and 70 deletions
+3
View File
@@ -43,6 +43,9 @@
### Fixed
- Fixed `nerd`-preset role chips overhanging into the label and swallowing its first character (`efault` instead of `default`) by inserting a separator after the status glyph in the model browser and model hub ([#7664](https://github.com/can1357/oh-my-pi/issues/7664)).
### Fixed
- Fixed Codex web search sending GPT-5.6 models a Responses-Lite request shape that the hosted `web_search` tool ignores. ([#7666](https://github.com/can1357/oh-my-pi/issues/7666))
## [17.2.9] - 2026-08-05
@@ -13,11 +13,7 @@ import {
withAuth,
withOAuthAccess,
} from "@oh-my-pi/pi-ai";
import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
import {
createOpenAICodexCompatibilityMetadata,
resolveCodexResponsesUrl,
} from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import { resolveCodexResponsesUrl } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
import {
CODEX_BASE_URL,
@@ -447,7 +443,6 @@ async function callCodexSearch(
systemPrompt?: string;
searchContextSize?: "low" | "medium" | "high";
model: CodexModelCandidate;
sessionId?: string;
fetch?: FetchImpl;
transport: CodexSearchTransport;
},
@@ -455,7 +450,6 @@ async function callCodexSearch(
const headers = buildCodexHeaders(auth.accessToken, auth.accountId, options.transport.headers);
const requestedModel = options.model.modelId;
const usesResponsesLite = options.model.catalogModel?.useResponsesLite === true;
const body: Record<string, unknown> = {
model: requestedModel,
@@ -477,21 +471,6 @@ async function callCodexSearch(
tool_choice: { type: "web_search" },
instructions: options.systemPrompt ?? DEFAULT_INSTRUCTIONS,
};
if (usesResponsesLite) {
const metadata = createOpenAICodexCompatibilityMetadata({
sessionId: options.sessionId,
requestKind: "turn",
startNewTurn: true,
});
for (const name in metadata.headers) {
const value = metadata.headers[name];
if (value !== undefined) headers.set(name, value);
}
headers.set(OPENAI_HEADERS.RESPONSES_LITE, "true");
body.client_metadata = metadata.clientMetadata;
body.reasoning = { context: "all_turns" };
applyCodexResponsesLiteShape(body);
}
const fetchImpl = options.fetch ?? fetch;
const response = await fetchImpl(options.transport.url, {
@@ -519,9 +498,8 @@ async function callCodexSearch(
let model = requestedModel;
let requestId = "";
let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
// Evidence that the hosted web_search tool actually ran. Lite models get
// `tool_choice: "auto"` and may answer without searching (#6988); a search
// command must reject that rather than return a non-search completion.
// A search command must reject a completion that did not invoke the hosted
// tool rather than returning an answer from the model's own knowledge (#6988).
let webSearchInvoked = false;
for await (const rawEvent of readSseJson<Record<string, unknown>>(response.body, options.signal)) {
@@ -651,7 +629,6 @@ async function runCodexSearchCandidates(options: {
systemPrompt: options.params.systemPrompt,
searchContextSize: "high",
model: candidate,
sessionId: options.params.sessionId,
fetch: options.params.fetch,
transport: options.transport,
});
@@ -330,9 +330,7 @@ describe("searchCodex model selection", () => {
expect(sentUserText()).toBe('bun runtime "exact phrase" site:bun.sh -site:reddit.com after:2024-01-01');
// Tool config stays untouched: the ChatGPT backend's filter support is
// unverified, so no `filters` field is added to the web_search tool.
const input = capturedRequest?.body?.input as Array<Record<string, unknown>>;
const additionalTools = input.find(item => item.type === "additional_tools");
expect(additionalTools?.tools).toEqual([{ type: "web_search", search_context_size: "high" }]);
expect(capturedRequest?.body?.tools).toEqual([{ type: "web_search", search_context_size: "high" }]);
});
it("sends directive-free queries byte-identical", async () => {
@@ -470,67 +468,31 @@ describe("searchCodex model selection", () => {
expect(result.sources).toEqual([{ title: "Example Article", url: "https://example.com/article" }]);
});
it("encodes explicit gpt-5.6-sol as a Responses-Lite request", async () => {
it("keeps hosted web_search top-level for explicit Responses-Lite catalog models (#7666)", async () => {
process.env.PI_CODEX_WEB_SEARCH_MODEL = "gpt-5.6-sol";
const result = await searchCodex(makeSearchParams("Sol web search", mockCodexFetch("gpt-5.6-sol")));
expect(capturedRequest).not.toBeNull();
const headers = new Headers(capturedRequest?.headers);
expect(headers.get("x-openai-internal-codex-responses-lite")).toBe("true");
expect(headers.get("session-id")).toBeTruthy();
expect(headers.get("thread-id")).toBeTruthy();
expect(headers.get("x-codex-window-id")).toBeTruthy();
expect(headers.get("x-openai-internal-codex-responses-lite")).toBeNull();
expect(capturedRequest?.body).toEqual(
expect.objectContaining({
model: "gpt-5.6-sol",
tool_choice: "auto",
reasoning: { context: "all_turns" },
parallel_tool_calls: false,
tools: [{ type: "web_search", search_context_size: "high" }],
tool_choice: { type: "web_search" },
instructions: "Codex test system prompt",
input: [
{
type: "additional_tools",
role: "developer",
tools: [{ type: "web_search", search_context_size: "high" }],
},
{
type: "message",
role: "developer",
content: [{ type: "input_text", text: "Codex test system prompt" }],
},
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Sol web search" }],
},
],
client_metadata: expect.objectContaining({
session_id: headers.get("session-id"),
thread_id: headers.get("thread-id"),
"x-codex-window-id": headers.get("x-codex-window-id"),
}),
}),
);
expect(capturedRequest?.body?.tools).toBeUndefined();
expect(capturedRequest?.body?.instructions).toBeUndefined();
expect(result.model).toBe("gpt-5.6-sol");
});
it("never leaves a forced hosted tool_choice on a Responses-Lite request (#5771)", async () => {
process.env.PI_CODEX_WEB_SEARCH_MODEL = "gpt-5.6-sol";
await searchCodex(makeSearchParams("forced choice guard", mockCodexFetch("gpt-5.6-sol")));
const body = capturedRequest?.body;
expect(body).not.toBeNull();
// Lite moves tools into `additional_tools` and drops top-level `tools`;
// a forced hosted choice against absent top-level tools is rejected 400.
const additionalTools = (body?.input as Array<Record<string, unknown>>)?.[0];
expect(additionalTools?.type).toBe("additional_tools");
expect(additionalTools?.tools).toEqual([{ type: "web_search", search_context_size: "high" }]);
expect(body?.tools).toBeUndefined();
expect(body?.tool_choice).toBe("auto");
expect(body?.tool_choice).not.toEqual({ type: "web_search" });
});
it("does not retry default candidates when PI_CODEX_WEB_SEARCH_MODEL is explicitly unsupported", async () => {
process.env.PI_CODEX_WEB_SEARCH_MODEL = "gpt-5.5";
let calls = 0;