-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
4e0509b
feat: add native OpenAI provider support for Codex Mini model
737d70d
feat: add Codex Mini support using existing GPT-5 infrastructure
daniel-lxs ee9d7c6
refactor: remove special Codex Mini handling - use same GPT-5 infrast…
daniel-lxs c8ad976
fix: remove unreachable code and ensure Codex Mini gets reasoning eff…
daniel-lxs 7c3fd1f
docs: update Codex Mini description with accurate capabilities
daniel-lxs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1514,4 +1514,243 @@ 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.done","response":{"usage":{"prompt_tokens":50,"completion_tokens":10}}}\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 data from API | ||
| const usageChunks = chunks.filter((c) => c.type === "usage") | ||
| expect(usageChunks).toHaveLength(1) | ||
| expect(usageChunks[0]).toMatchObject({ | ||
| type: "usage", | ||
| inputTokens: 50, | ||
| outputTokens: 10, | ||
| totalCost: expect.any(Number), // Codex Mini has pricing: $1.5/M input, $6/M output | ||
| }) | ||
|
|
||
| // Verify cost is calculated correctly based on API usage data | ||
| const expectedCost = (50 / 1_000_000) * 1.5 + (10 / 1_000_000) * 6 | ||
| expect(usageChunks[0].totalCost).toBeCloseTo(expectedCost, 10) | ||
|
|
||
| // 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", | ||
| input: "Developer: You are a helpful coding assistant.\n\nUser: Write a hello world function", | ||
| stream: true, | ||
| }) | ||
|
|
||
| // Clean up | ||
| delete (global as any).fetch | ||
| }) | ||
|
|
||
| it("should handle codex-mini-latest non-streaming completion", async () => { | ||
| handler = new OpenAiNativeHandler({ | ||
| ...mockOptions, | ||
| apiModelId: "codex-mini-latest", | ||
| }) | ||
|
|
||
| // Codex Mini now uses the same Responses API as GPT-5, which doesn't support non-streaming | ||
| await expect(handler.completePrompt("Write a hello world function in Python")).rejects.toThrow( | ||
| "completePrompt is not supported for codex-mini-latest. Use createMessage (Responses API) instead.", | ||
| ) | ||
| }) | ||
|
|
||
| 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 (using the same error format as GPT-5) | ||
| await expect(async () => { | ||
| for await (const chunk of stream) { | ||
| // consume stream | ||
| } | ||
| }).rejects.toThrow("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 includes full conversation like GPT-5 | ||
| const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) | ||
| expect(requestBody.input).toContain("Developer: You are a helpful assistant") | ||
| expect(requestBody.input).toContain("User: First question") | ||
| expect(requestBody.input).toContain("Assistant: First answer") | ||
| expect(requestBody.input).toContain("User: Second question") | ||
|
|
||
| // 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', | ||
| ), | ||
| ) | ||
| // The error handler will throw, but we still need to close the stream | ||
| 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("Responses API error: Model overloaded") | ||
|
|
||
| // Clean up | ||
| delete (global as any).fetch | ||
| }) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.