-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: add native OpenAI provider support for Codex Mini model (#5386) #6931
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
4e0509b
737d70d
ee9d7c6
c8ad976
7c3fd1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -220,6 +220,16 @@ export const openAiNativeModels = { | |
| outputPrice: 0.6, | ||
| cacheReadsPrice: 0.075, | ||
| }, | ||
| "codex-mini-latest": { | ||
| maxTokens: 16_384, | ||
| contextWindow: 200_000, | ||
| supportsImages: false, | ||
| supportsPromptCache: false, | ||
| inputPrice: 1.5, | ||
| outputPrice: 6, | ||
| cacheReadsPrice: 0, | ||
| description: "Codex Mini: Optimized coding model using v1/responses endpoint", | ||
|
||
| }, | ||
| } as const satisfies Record<string, ModelInfo> | ||
|
|
||
| export const openAiModelInfoSaneDefaults: ModelInfo = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1514,4 +1514,268 @@ describe("GPT-5 streaming event coverage (additional)", () => { | |
| // @ts-ignore | ||
| delete global.fetch | ||
| }) | ||
|
|
||
| describe("Codex Mini Model", () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great test coverage! However, it would be valuable to add integration tests that mock at a higher level (e.g., testing the full flow from API handler to response). This would catch issues with the integration between different components. |
||
| let handler: OpenAiNativeHandler | ||
| const mockOptions: ApiHandlerOptions = { | ||
| openAiNativeApiKey: "test-api-key", | ||
| apiModelId: "codex-mini-latest", | ||
| } | ||
|
|
||
| it("should handle codex-mini-latest streaming response", async () => { | ||
| // Mock fetch for Codex Mini responses API | ||
| const mockFetch = vitest.fn().mockResolvedValue({ | ||
| ok: true, | ||
| body: new ReadableStream({ | ||
| start(controller) { | ||
| // Codex Mini uses the same responses API format | ||
| controller.enqueue( | ||
| new TextEncoder().encode('data: {"type":"response.output_text.delta","delta":"Hello"}\n\n'), | ||
| ) | ||
| controller.enqueue( | ||
| new TextEncoder().encode('data: {"type":"response.output_text.delta","delta":" from"}\n\n'), | ||
| ) | ||
| controller.enqueue( | ||
| new TextEncoder().encode( | ||
| 'data: {"type":"response.output_text.delta","delta":" Codex"}\n\n', | ||
| ), | ||
| ) | ||
| controller.enqueue( | ||
| new TextEncoder().encode( | ||
| 'data: {"type":"response.output_text.delta","delta":" Mini!"}\n\n', | ||
| ), | ||
| ) | ||
| controller.enqueue(new TextEncoder().encode('data: {"type":"response.completed"}\n\n')) | ||
| controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) | ||
| controller.close() | ||
| }, | ||
| }), | ||
| }) | ||
| global.fetch = mockFetch as any | ||
|
|
||
| handler = new OpenAiNativeHandler({ | ||
| ...mockOptions, | ||
| apiModelId: "codex-mini-latest", | ||
| }) | ||
|
|
||
| const systemPrompt = "You are a helpful coding assistant." | ||
| const messages: Anthropic.Messages.MessageParam[] = [ | ||
| { role: "user", content: "Write a hello world function" }, | ||
| ] | ||
|
|
||
| const stream = handler.createMessage(systemPrompt, messages) | ||
| const chunks: any[] = [] | ||
| for await (const chunk of stream) { | ||
| chunks.push(chunk) | ||
| } | ||
|
|
||
| // Verify text chunks | ||
| const textChunks = chunks.filter((c) => c.type === "text") | ||
| expect(textChunks).toHaveLength(4) | ||
| expect(textChunks.map((c) => c.text).join("")).toBe("Hello from Codex Mini!") | ||
|
|
||
| // Verify usage estimation (based on character count) | ||
| const usageChunks = chunks.filter((c) => c.type === "usage") | ||
| expect(usageChunks).toHaveLength(1) | ||
| expect(usageChunks[0]).toMatchObject({ | ||
| type: "usage", | ||
| inputTokens: expect.any(Number), | ||
| outputTokens: expect.any(Number), | ||
| totalCost: expect.any(Number), // Codex Mini has pricing: $1.5/M input, $6/M output | ||
| }) | ||
|
|
||
| // Verify cost is calculated correctly | ||
| expect(usageChunks[0].totalCost).toBeGreaterThan(0) | ||
|
|
||
| // Verify the request was made with correct parameters | ||
| expect(mockFetch).toHaveBeenCalledWith( | ||
| "https://api.openai.com/v1/responses", | ||
| expect.objectContaining({ | ||
| method: "POST", | ||
| headers: expect.objectContaining({ | ||
| "Content-Type": "application/json", | ||
| Authorization: "Bearer test-api-key", | ||
| Accept: "text/event-stream", | ||
| }), | ||
| body: expect.any(String), | ||
| }), | ||
| ) | ||
|
|
||
| const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) | ||
| expect(requestBody).toMatchObject({ | ||
| model: "codex-mini-latest", | ||
| instructions: systemPrompt, | ||
| input: "Write a hello world function", | ||
| stream: true, | ||
| }) | ||
|
|
||
| // Clean up | ||
| delete (global as any).fetch | ||
| }) | ||
|
|
||
| it("should handle codex-mini-latest non-streaming completion", async () => { | ||
| // Mock fetch for non-streaming response | ||
| const mockFetch = vitest.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: async () => ({ | ||
| output_text: "def hello_world():\n print('Hello, World!')", | ||
| }), | ||
| }) | ||
| global.fetch = mockFetch as any | ||
|
|
||
| handler = new OpenAiNativeHandler({ | ||
| ...mockOptions, | ||
| apiModelId: "codex-mini-latest", | ||
| }) | ||
|
|
||
| const result = await handler.completePrompt("Write a hello world function in Python") | ||
|
|
||
| expect(result).toBe("def hello_world():\n print('Hello, World!')") | ||
|
|
||
| // Verify the request | ||
| expect(mockFetch).toHaveBeenCalledWith( | ||
| "https://api.openai.com/v1/responses", | ||
| expect.objectContaining({ | ||
| method: "POST", | ||
| headers: expect.objectContaining({ | ||
| "Content-Type": "application/json", | ||
| Authorization: "Bearer test-api-key", | ||
| }), | ||
| body: expect.any(String), | ||
| }), | ||
| ) | ||
|
|
||
| const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) | ||
| expect(requestBody).toMatchObject({ | ||
| model: "codex-mini-latest", | ||
| instructions: "Complete the following prompt:", | ||
| input: "Write a hello world function in Python", | ||
| stream: false, | ||
| }) | ||
|
|
||
| // Clean up | ||
| delete (global as any).fetch | ||
| }) | ||
|
|
||
| it("should handle codex-mini-latest API errors", async () => { | ||
| // Mock fetch with error response | ||
| const mockFetch = vitest.fn().mockResolvedValue({ | ||
| ok: false, | ||
| status: 429, | ||
| statusText: "Too Many Requests", | ||
| text: async () => "Rate limit exceeded", | ||
| }) | ||
| global.fetch = mockFetch as any | ||
|
|
||
| handler = new OpenAiNativeHandler({ | ||
| ...mockOptions, | ||
| apiModelId: "codex-mini-latest", | ||
| }) | ||
|
|
||
| const systemPrompt = "You are a helpful assistant." | ||
| const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] | ||
|
|
||
| const stream = handler.createMessage(systemPrompt, messages) | ||
|
|
||
| // Should throw an error | ||
| await expect(async () => { | ||
| for await (const chunk of stream) { | ||
| // consume stream | ||
| } | ||
| }).rejects.toThrow("Codex Mini API request failed (429): Rate limit exceeded") | ||
|
|
||
| // Clean up | ||
| delete (global as any).fetch | ||
| }) | ||
|
|
||
| it("should handle codex-mini-latest with multiple user messages", async () => { | ||
| // Mock fetch for streaming response | ||
| const mockFetch = vitest.fn().mockResolvedValue({ | ||
| ok: true, | ||
| body: new ReadableStream({ | ||
| start(controller) { | ||
| controller.enqueue( | ||
| new TextEncoder().encode( | ||
| 'data: {"type":"response.output_text.delta","delta":"Combined response"}\n\n', | ||
| ), | ||
| ) | ||
| controller.enqueue(new TextEncoder().encode('data: {"type":"response.completed"}\n\n')) | ||
| controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) | ||
| controller.close() | ||
| }, | ||
| }), | ||
| }) | ||
| global.fetch = mockFetch as any | ||
|
|
||
| handler = new OpenAiNativeHandler({ | ||
| ...mockOptions, | ||
| apiModelId: "codex-mini-latest", | ||
| }) | ||
|
|
||
| const systemPrompt = "You are a helpful assistant." | ||
| const messages: Anthropic.Messages.MessageParam[] = [ | ||
| { role: "user", content: "First question" }, | ||
| { role: "assistant", content: "First answer" }, | ||
| { role: "user", content: "Second question" }, | ||
| ] | ||
|
|
||
| const stream = handler.createMessage(systemPrompt, messages) | ||
| const chunks: any[] = [] | ||
| for await (const chunk of stream) { | ||
| chunks.push(chunk) | ||
| } | ||
|
|
||
| // Verify the request body only includes user messages | ||
| const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) | ||
| expect(requestBody.input).toBe("First question\n\nSecond question") | ||
| expect(requestBody.input).not.toContain("First answer") | ||
|
|
||
| // Clean up | ||
| delete (global as any).fetch | ||
| }) | ||
|
|
||
| it("should handle codex-mini-latest stream error events", async () => { | ||
| // Mock fetch with error event in stream | ||
| const mockFetch = vitest.fn().mockResolvedValue({ | ||
| ok: true, | ||
| body: new ReadableStream({ | ||
| start(controller) { | ||
| controller.enqueue( | ||
| new TextEncoder().encode( | ||
| 'data: {"type":"response.output_text.delta","delta":"Partial"}\n\n', | ||
| ), | ||
| ) | ||
| controller.enqueue( | ||
| new TextEncoder().encode( | ||
| 'data: {"type":"response.error","error":{"message":"Model overloaded"}}\n\n', | ||
| ), | ||
| ) | ||
| controller.close() | ||
| }, | ||
| }), | ||
| }) | ||
| global.fetch = mockFetch as any | ||
|
|
||
| handler = new OpenAiNativeHandler({ | ||
| ...mockOptions, | ||
| apiModelId: "codex-mini-latest", | ||
| }) | ||
|
|
||
| const systemPrompt = "You are a helpful assistant." | ||
| const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] | ||
|
|
||
| const stream = handler.createMessage(systemPrompt, messages) | ||
|
|
||
| // Should throw an error when encountering error event | ||
| await expect(async () => { | ||
| const chunks = [] | ||
| for await (const chunk of stream) { | ||
| chunks.push(chunk) | ||
| } | ||
| }).rejects.toThrow("Codex Mini stream error: Model overloaded") | ||
|
|
||
| // Clean up | ||
| delete (global as any).fetch | ||
| }) | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pricing values here (1.5 and 6) appear to be correct based on issue #5386, but the PR description mentions '.5/M' and '/M' which could be confusing. Could you clarify if these are the correct values? The issue mentions these are the actual numeric values without dollar signs.