import { expect, test } from "bun:test"; import { createAssistantMessageEventStream, type Model } from "@earendil-works/pi-ai"; import { convertToLlm } from "@earendil-works/pi-coding-agent"; import { createGrammarToolInputProperties } from "../src/constrained-sampling.ts"; import { buildRequestBody } from "../src/provider/request-body.ts"; import { createInitialAssistantMessage } from "../src/provider/types.ts"; import { convertResponsesMessages } from "../src/responses/shared.ts"; import { processResponsesStream } from "../src/responses/stream.ts"; import { createWebRunTool } from "../src/tools/web-run/definition.ts"; const model: Model<"openai-codex-responses"> = { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", provider: "openai-codex", api: "openai-codex-responses", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16384, }; test("request conversion keeps the local web runner as a named function tool", () => { const body = buildRequestBody(model, { systemPrompt: "You are useful.", messages: [{ role: "user", content: "Measure this." }], tools: [createWebRunTool()], } as never); expect(body.tools).toHaveLength(1); expect(body.tools?.[0]).toMatchObject({ type: "function", name: "web__run", description: "Search and inspect the web, images, finance, weather, sports, and time. Use this when current web information or direct source attribution is required.", }); }); test("request conversion emits grammar-constrained tools as native freeform tools", () => { const tools = ["exec", "apply_patch"].map((name) => ({ name, label: name, description: `${name} freeform`, parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"], additionalProperties: false, }, constrainedSampling: { type: "grammar", variants: { openai_lark: "start: /[\\s\\S]+/" }, }, })) as never; const body = buildRequestBody( model, { systemPrompt: "Prompt.", messages: [{ role: "user", content: "Work." }], tools, } as never, { grammarToolInputProperties: createGrammarToolInputProperties(tools, true), }, ); expect(body.tools).toEqual([ expect.objectContaining({ type: "custom", name: "exec", format: expect.objectContaining({ type: "grammar" }) }), expect.objectContaining({ type: "custom", name: "apply_patch", format: expect.objectContaining({ type: "grammar" }), }), ]); }); test("request conversion preserves Codex session metadata", () => { const body = buildRequestBody( model, { systemPrompt: "Prompt.", messages: [{ role: "user", content: "Work." }], tools: [], } as never, { sessionId: "session-1", }, ); expect(body.parallel_tool_calls).toBe(true); expect(body.client_metadata).toEqual({ session_id: "session-1", thread_id: "session-1", "x-codex-turn-metadata": JSON.stringify({ session_id: "session-1", thread_id: "session-1", }), }); }); test("native custom tool events become a Pi tool call", async () => { const output = createInitialAssistantMessage(model); const stream = createAssistantMessageEventStream(); await processResponsesStream( [ { type: "response.created", response: { id: "resp_1" } }, { type: "response.output_item.added", output_index: 0, item: { type: "custom_tool_call", id: "ctc_1", call_id: "call_1", name: "custom_tool", input: "" }, }, { type: "response.custom_tool_call_input.delta", output_index: 0, delta: "hello" }, { type: "response.custom_tool_call_input.done", output_index: 0, input: "hello" }, { type: "response.output_item.done", output_index: 0, item: { type: "custom_tool_call", id: "ctc_1", call_id: "call_1", name: "custom_tool", input: "hello" }, }, { type: "response.completed", response: { id: "resp_1", status: "completed", usage: { input_tokens: 1, output_tokens: 2, total_tokens: 3 } }, }, ] as never, output, stream, model, { grammarToolInputProperties: new Map([["custom_tool", "text"]]), }, ); expect(output.content).toContainEqual({ type: "toolCall", id: "call_1|ctc_1", name: "custom_tool", arguments: { text: "hello" }, }); expect(output.stopReason).toBe("toolUse"); }); test("stored native custom tool calls replay without current grammar metadata", () => { const input = convertResponsesMessages( model, { messages: [ { role: "assistant", provider: "openai-codex", api: "openai-codex-responses", model: "gpt-5.6-sol", content: [{ type: "toolCall", id: "call_1|ctc_1", name: "exec", arguments: { code: "return 1" } }], stopReason: "toolUse", timestamp: 1, }, { role: "toolResult", toolCallId: "call_1|ctc_1", toolName: "exec", content: [{ type: "text", text: "1" }], isError: false, timestamp: 2, }, ], } as never, new Set(["openai-codex"]), ); expect(input).toEqual([ { type: "custom_tool_call", id: "ctc_1", call_id: "call_1", name: "exec", input: "return 1", }, { type: "custom_tool_call_output", call_id: "call_1", output: "1" }, ]); }); test("native web search items replay as Responses input", () => { const input = convertResponsesMessages( model, { messages: [ { role: "assistant", provider: "openai-codex", api: "openai-codex-responses", model: "gpt-5.6-sol", stopReason: "stop", timestamp: 1, content: [ { type: "web_search_call", item: { type: "web_search_call", id: "ws_1", status: "completed", action: { type: "search", query: "latest" }, }, }, ], }, ], } as never, new Set(["openai-codex"]), ); expect(input).toEqual([ { type: "web_search_call", id: "ws_1", status: "completed", action: { type: "search", query: "latest" }, }, ]); }); test("a loaded skill follows its paired tool output as contextual user content", () => { const messages = convertToLlm([ { role: "assistant", provider: "openai-codex", api: "openai-codex-responses", model: "gpt-5.6-sol", stopReason: "toolUse", timestamp: 1, content: [{ type: "toolCall", id: "call_1|fc_1", name: "skill", arguments: { name: "writing-for-agents" } }], }, { role: "toolResult", toolCallId: "call_1|fc_1", toolName: "skill", content: [{ type: "text", text: 'Loaded skill "writing-for-agents".' }], isError: false, timestamp: 2, }, { role: "custom", customType: "pi-skills/loaded", content: "\nwriting-for-agents\n/skills/writing-for-agents/SKILL.md\nWrite clearly.\n", display: true, timestamp: 2, }, ] as never); const input = convertResponsesMessages(model, { messages } as never, new Set(["openai-codex"])); expect(input.map((item) => item.type ?? item.role)).toEqual(["function_call", "function_call_output", "user"]); expect(JSON.stringify(input[2])).toContain(""); }); test("serializes Code Mode audio as a Responses tool output", () => { const messages = [ { role: "assistant", provider: "openai-codex", api: "openai-codex-responses", model: "gpt-5.6-sol", stopReason: "toolUse", timestamp: 1, content: [{ type: "toolCall", id: "call_audio|fc_audio", name: "exec", arguments: { code: "audio(x)" } }], }, { role: "toolResult", toolCallId: "call_audio|fc_audio", toolName: "exec", content: [{ type: "audio", data: "YQ==", mimeType: "audio/wav" }], isError: false, timestamp: 2, }, ]; const input = convertResponsesMessages(model, { messages } as never, new Set(["openai-codex"])); expect(input[1]).toEqual({ type: "function_call_output", call_id: "call_audio", output: [{ type: "input_audio", audio_url: "data:audio/wav;base64,YQ==" }], }); }); test("ordinary Codex function tools keep optional arguments optional", () => { const parameters = { type: "object", properties: { action: { type: "string" }, name: { type: "string" } }, required: ["action"], }; const body = buildRequestBody(model, { messages: [], tools: [{ name: "notebook__control", description: "Control notebook", parameters }], }); expect(body.tools?.[0]).toMatchObject({ type: "function", strict: false, parameters }); }); test("transcript prompt and tool deltas retain their position and honor removals", () => { const nativeModel = { ...model, compat: { supportsMidConvoSystemMessages: true, supportsAdditionalTools: true } }; const search = { name: "tool_search", description: "Find tools", parameters: { type: "object" as const } }; const loaded = { name: "weather", description: "Weather", parameters: { type: "object" as const } }; const messages = [ { role: "system" as const, content: "Base instructions", toolsAdded: [search], timestamp: 0 }, { role: "user" as const, content: "Find the weather", timestamp: 1 }, { role: "system" as const, content: "Use metric units", toolsAdded: [loaded], timestamp: 2 }, ]; const body = buildRequestBody(nativeModel, { messages }); expect(body.instructions).toBe("Base instructions"); expect(body.tools).toMatchObject([{ name: "tool_search" }]); expect(body.input).toMatchObject([ { role: "user" }, { type: "additional_tools", tools: [{ name: "weather" }] }, { role: "developer", content: "Use metric units" }, ]); const removed = buildRequestBody(nativeModel, { messages: [...messages, { role: "system", content: "", toolsRemoved: [{ name: "weather" }], timestamp: 3 }], }); expect(removed.tools).toMatchObject([{ name: "tool_search" }]); expect( removed.input.some( (item) => item !== null && typeof item === "object" && "type" in item && item.type === "additional_tools", ), ).toBe(false); }); test.each(["off", "none"])("explicit reasoning %s uses the model's Off mapping", (effort) => { const options = effort === "off" ? {} : { reasoningEffort: "none" as const }; expect( buildRequestBody({ ...model, thinkingLevelMap: { off: "none" } }, { messages: [] }, options).reasoning, ).toMatchObject({ effort: "none" }); const unsupported = buildRequestBody( { ...model, thinkingLevelMap: { off: null } }, { messages: [] }, options, ).reasoning; expect(unsupported).toBeUndefined(); });