import type { StreamFn } from "@mariozechner/pi-agent-core"; import type { Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai"; import { describe, expect, it, vi } from "vitest"; import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js"; import { buildOpenAIProvider } from "./openai-provider.js"; const refreshOpenAICodexTokenMock = vi.hoisted(() => vi.fn()); vi.mock("./openai-codex-provider.runtime.js", () => ({ refreshOpenAICodexToken: refreshOpenAICodexTokenMock, })); function runWrappedPayloadCase(params: { wrap: NonNullable["wrapStreamFn"]>; provider: string; modelId: string; model: | Model<"openai-responses"> | Model<"openai-codex-responses"> | Model<"azure-openai-responses">; extraParams?: Record; cfg?: Record; payload?: Record; }) { const payload = params.payload ?? { store: false }; let capturedOptions: (SimpleStreamOptions & { openaiWsWarmup?: boolean }) | undefined; const baseStreamFn: StreamFn = (model, _context, options) => { capturedOptions = options as (SimpleStreamOptions & { openaiWsWarmup?: boolean }) | undefined; options?.onPayload?.(payload, model); return {} as ReturnType; }; const streamFn = params.wrap({ provider: params.provider, modelId: params.modelId, extraParams: params.extraParams, config: params.cfg as never, agentDir: "/tmp/openai-provider-test", streamFn: baseStreamFn, } as never); const context: Context = { messages: [] }; void streamFn?.(params.model, context, {}); return { payload, options: capturedOptions, }; } describe("buildOpenAIProvider", () => { it("resolves gpt-5.4 mini and nano from GPT-5 small-model templates", () => { const provider = buildOpenAIProvider(); const registry = { find(providerId: string, id: string) { if (providerId !== "openai") { return null; } if (id === "gpt-5-mini") { return { id, name: "GPT-5 mini", provider: "openai", api: "openai-responses", baseUrl: "https://api.openai.com/v1", reasoning: true, input: ["text", "image"], cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 }, contextWindow: 400_000, maxTokens: 128_000, }; } if (id === "gpt-5-nano") { return { id, name: "GPT-5 nano", provider: "openai", api: "openai-responses", baseUrl: "https://api.openai.com/v1", reasoning: true, input: ["text", "image"], cost: { input: 0.5, output: 1, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200_000, maxTokens: 64_000, }; } return null; }, }; const mini = provider.resolveDynamicModel?.({ provider: "openai", modelId: "gpt-5.4-mini", modelRegistry: registry as never, }); const nano = provider.resolveDynamicModel?.({ provider: "openai", modelId: "gpt-5.4-nano", modelRegistry: registry as never, }); expect(mini).toMatchObject({ provider: "openai", id: "gpt-5.4-mini", api: "openai-responses", baseUrl: "https://api.openai.com/v1", contextWindow: 400_000, maxTokens: 128_000, }); expect(nano).toMatchObject({ provider: "openai", id: "gpt-5.4-nano", api: "openai-responses", baseUrl: "https://api.openai.com/v1", contextWindow: 400_000, maxTokens: 128_000, }); }); it("surfaces gpt-5.4 mini and nano in xhigh and augmented catalog metadata", () => { const provider = buildOpenAIProvider(); expect( provider.supportsXHighThinking?.({ provider: "openai", modelId: "gpt-5.4-mini", } as never), ).toBe(true); expect( provider.supportsXHighThinking?.({ provider: "openai", modelId: "gpt-5.4-nano", } as never), ).toBe(true); const entries = provider.augmentModelCatalog?.({ env: process.env, entries: [ { provider: "openai", id: "gpt-5-mini", name: "GPT-5 mini" }, { provider: "openai", id: "gpt-5-nano", name: "GPT-5 nano" }, ], } as never); expect(entries).toContainEqual( expect.objectContaining({ provider: "openai", id: "gpt-5.4-mini", name: "gpt-5.4-mini", reasoning: true, input: ["text", "image"], contextWindow: 400_000, }), ); expect(entries).toContainEqual( expect.objectContaining({ provider: "openai", id: "gpt-5.4-nano", name: "gpt-5.4-nano", reasoning: true, input: ["text", "image"], contextWindow: 400_000, }), ); }); it("owns native reasoning output mode for OpenAI and Azure OpenAI responses", () => { const provider = buildOpenAIProvider(); expect( provider.resolveReasoningOutputMode?.({ provider: "openai", modelApi: "openai-responses", modelId: "gpt-5.4", } as never), ).toBe("native"); expect( provider.resolveReasoningOutputMode?.({ provider: "azure-openai-responses", modelApi: "azure-openai-responses", modelId: "gpt-5.4", } as never), ).toBe("native"); }); it("keeps GPT-5.4 family metadata aligned with native OpenAI docs", () => { const provider = buildOpenAIProvider(); const codexProvider = buildOpenAICodexProviderPlugin(); const openaiModel = provider.resolveDynamicModel?.({ provider: "openai", modelId: "gpt-5.4", modelRegistry: { find: () => null }, } as never); const codexModel = codexProvider.resolveDynamicModel?.({ provider: "openai-codex", modelId: "gpt-5.4", modelRegistry: { find: () => null }, } as never); expect(openaiModel).toMatchObject({ provider: "openai", id: "gpt-5.4", api: "openai-responses", baseUrl: "https://api.openai.com/v1", contextWindow: 1_050_000, maxTokens: 128_000, }); expect(codexModel).toMatchObject({ provider: "openai-codex", id: "gpt-5.4", api: "openai-codex-responses", baseUrl: "https://chatgpt.com/backend-api", contextWindow: 1_050_000, maxTokens: 128_000, }); }); it("keeps modern live selection on OpenAI 5.2+ and Codex 5.2+", () => { const provider = buildOpenAIProvider(); const codexProvider = buildOpenAICodexProviderPlugin(); expect( provider.isModernModelRef?.({ provider: "openai", modelId: "gpt-5.0", } as never), ).toBe(false); expect( provider.isModernModelRef?.({ provider: "openai", modelId: "gpt-5.2", } as never), ).toBe(true); expect( provider.isModernModelRef?.({ provider: "openai", modelId: "gpt-5.4", } as never), ).toBe(true); expect( codexProvider.isModernModelRef?.({ provider: "openai-codex", modelId: "gpt-5.1-codex", } as never), ).toBe(false); expect( codexProvider.isModernModelRef?.({ provider: "openai-codex", modelId: "gpt-5.1-codex-max", } as never), ).toBe(false); expect( codexProvider.isModernModelRef?.({ provider: "openai-codex", modelId: "gpt-5.2-codex", } as never), ).toBe(true); expect( codexProvider.isModernModelRef?.({ provider: "openai-codex", modelId: "gpt-5.4", } as never), ).toBe(true); }); it("owns replay policy for OpenAI and Codex transports", () => { const provider = buildOpenAIProvider(); const codexProvider = buildOpenAICodexProviderPlugin(); expect( provider.buildReplayPolicy?.({ provider: "openai", modelApi: "openai", modelId: "gpt-5.4", } as never), ).toEqual({ sanitizeMode: "images-only", applyAssistantFirstOrderingFix: false, sanitizeToolCallIds: false, validateGeminiTurns: false, validateAnthropicTurns: false, }); expect( provider.buildReplayPolicy?.({ provider: "openai", modelApi: "openai-completions", modelId: "gpt-5.4", } as never), ).toEqual({ sanitizeMode: "images-only", applyAssistantFirstOrderingFix: false, sanitizeToolCallIds: true, toolCallIdMode: "strict", validateGeminiTurns: false, validateAnthropicTurns: false, }); expect( codexProvider.buildReplayPolicy?.({ provider: "openai-codex", modelApi: "openai-codex-responses", modelId: "gpt-5.4", } as never), ).toEqual({ sanitizeMode: "images-only", applyAssistantFirstOrderingFix: false, sanitizeToolCallIds: false, validateGeminiTurns: false, validateAnthropicTurns: false, }); }); it("owns direct OpenAI wrapper composition for responses payloads", () => { const provider = buildOpenAIProvider(); const wrap = provider.wrapStreamFn; expect(wrap).toBeTypeOf("function"); if (!wrap) { throw new Error("expected OpenAI wrapper"); } const extraParams = provider.prepareExtraParams?.({ provider: "openai", modelId: "gpt-5.4", extraParams: { fastMode: true, serviceTier: "priority", textVerbosity: "low", }, } as never); const result = runWrappedPayloadCase({ wrap, provider: "openai", modelId: "gpt-5.4", extraParams: extraParams ?? undefined, model: { api: "openai-responses", provider: "openai", id: "gpt-5.4", baseUrl: "https://api.openai.com/v1", } as Model<"openai-responses">, payload: { reasoning: { effort: "none" }, }, }); expect(extraParams).toMatchObject({ transport: "auto", openaiWsWarmup: true, }); expect(result.payload.service_tier).toBe("priority"); expect(result.payload.text).toEqual({ verbosity: "low" }); expect(result.payload.reasoning).toEqual({ effort: "none" }); }); it("owns Azure OpenAI reasoning compatibility without forcing OpenAI transport defaults", () => { const provider = buildOpenAIProvider(); const wrap = provider.wrapStreamFn; expect(wrap).toBeTypeOf("function"); if (!wrap) { throw new Error("expected Azure OpenAI wrapper"); } const result = runWrappedPayloadCase({ wrap, provider: "azure-openai-responses", modelId: "gpt-5.4", model: { api: "azure-openai-responses", provider: "azure-openai-responses", id: "gpt-5.4", baseUrl: "https://example.openai.azure.com/openai/v1", } as Model<"azure-openai-responses">, payload: { reasoning: { effort: "none" }, }, }); expect(result.options?.transport).toBeUndefined(); expect(result.options?.openaiWsWarmup).toBeUndefined(); expect(result.payload.reasoning).toEqual({ effort: "none" }); }); it("owns Codex wrapper composition for responses payloads", () => { const provider = buildOpenAICodexProviderPlugin(); const wrap = provider.wrapStreamFn; expect(wrap).toBeTypeOf("function"); if (!wrap) { throw new Error("expected Codex wrapper"); } const result = runWrappedPayloadCase({ wrap, provider: "openai-codex", modelId: "gpt-5.4", extraParams: { fastMode: true, serviceTier: "priority", text_verbosity: "high", }, cfg: { auth: { profiles: { "openai-codex:default": { provider: "openai-codex", mode: "oauth", }, }, }, tools: { web: { search: { enabled: true, openaiCodex: { enabled: true, mode: "live", allowedDomains: ["example.com"], }, }, }, }, }, model: { api: "openai-codex-responses", provider: "openai-codex", id: "gpt-5.4", baseUrl: "https://chatgpt.com/backend-api", } as Model<"openai-codex-responses">, payload: { store: false, text: { verbosity: "medium" }, tools: [{ type: "function", name: "read" }], }, }); expect(result.payload.store).toBe(false); expect(result.payload.service_tier).toBe("priority"); expect(result.payload.text).toEqual({ verbosity: "high" }); expect(result.payload.tools).toEqual([ { type: "function", name: "read" }, { type: "web_search", external_web_access: true, filters: { allowed_domains: ["example.com"] }, }, ]); }); it("falls back to cached codex oauth credentials on accountId extraction failures", async () => { const provider = buildOpenAICodexProviderPlugin(); const credential = { type: "oauth" as const, provider: "openai-codex", access: "cached-access-token", refresh: "refresh-token", expires: Date.now() - 60_000, }; refreshOpenAICodexTokenMock.mockReset(); refreshOpenAICodexTokenMock.mockRejectedValueOnce( new Error("Failed to extract accountId from token"), ); await expect(provider.refreshOAuth?.(credential)).resolves.toEqual(credential); }); });