Merge PR #7667: fix(web-search): keep GPT-5.6 hosted tools top-level (@roboomp)
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user