@@ -1514,4 +1514,243 @@ describe("GPT-5 streaming event coverage (additional)", () => {
15141514 // @ts -ignore
15151515 delete global . fetch
15161516 } )
1517+
1518+ describe ( "Codex Mini Model" , ( ) => {
1519+ let handler : OpenAiNativeHandler
1520+ const mockOptions : ApiHandlerOptions = {
1521+ openAiNativeApiKey : "test-api-key" ,
1522+ apiModelId : "codex-mini-latest" ,
1523+ }
1524+
1525+ it ( "should handle codex-mini-latest streaming response" , async ( ) => {
1526+ // Mock fetch for Codex Mini responses API
1527+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
1528+ ok : true ,
1529+ body : new ReadableStream ( {
1530+ start ( controller ) {
1531+ // Codex Mini uses the same responses API format
1532+ controller . enqueue (
1533+ new TextEncoder ( ) . encode ( 'data: {"type":"response.output_text.delta","delta":"Hello"}\n\n' ) ,
1534+ )
1535+ controller . enqueue (
1536+ new TextEncoder ( ) . encode ( 'data: {"type":"response.output_text.delta","delta":" from"}\n\n' ) ,
1537+ )
1538+ controller . enqueue (
1539+ new TextEncoder ( ) . encode (
1540+ 'data: {"type":"response.output_text.delta","delta":" Codex"}\n\n' ,
1541+ ) ,
1542+ )
1543+ controller . enqueue (
1544+ new TextEncoder ( ) . encode (
1545+ 'data: {"type":"response.output_text.delta","delta":" Mini!"}\n\n' ,
1546+ ) ,
1547+ )
1548+ controller . enqueue (
1549+ new TextEncoder ( ) . encode (
1550+ 'data: {"type":"response.done","response":{"usage":{"prompt_tokens":50,"completion_tokens":10}}}\n\n' ,
1551+ ) ,
1552+ )
1553+ controller . enqueue ( new TextEncoder ( ) . encode ( "data: [DONE]\n\n" ) )
1554+ controller . close ( )
1555+ } ,
1556+ } ) ,
1557+ } )
1558+ global . fetch = mockFetch as any
1559+
1560+ handler = new OpenAiNativeHandler ( {
1561+ ...mockOptions ,
1562+ apiModelId : "codex-mini-latest" ,
1563+ } )
1564+
1565+ const systemPrompt = "You are a helpful coding assistant."
1566+ const messages : Anthropic . Messages . MessageParam [ ] = [
1567+ { role : "user" , content : "Write a hello world function" } ,
1568+ ]
1569+
1570+ const stream = handler . createMessage ( systemPrompt , messages )
1571+ const chunks : any [ ] = [ ]
1572+ for await ( const chunk of stream ) {
1573+ chunks . push ( chunk )
1574+ }
1575+
1576+ // Verify text chunks
1577+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
1578+ expect ( textChunks ) . toHaveLength ( 4 )
1579+ expect ( textChunks . map ( ( c ) => c . text ) . join ( "" ) ) . toBe ( "Hello from Codex Mini!" )
1580+
1581+ // Verify usage data from API
1582+ const usageChunks = chunks . filter ( ( c ) => c . type === "usage" )
1583+ expect ( usageChunks ) . toHaveLength ( 1 )
1584+ expect ( usageChunks [ 0 ] ) . toMatchObject ( {
1585+ type : "usage" ,
1586+ inputTokens : 50 ,
1587+ outputTokens : 10 ,
1588+ totalCost : expect . any ( Number ) , // Codex Mini has pricing: $1.5/M input, $6/M output
1589+ } )
1590+
1591+ // Verify cost is calculated correctly based on API usage data
1592+ const expectedCost = ( 50 / 1_000_000 ) * 1.5 + ( 10 / 1_000_000 ) * 6
1593+ expect ( usageChunks [ 0 ] . totalCost ) . toBeCloseTo ( expectedCost , 10 )
1594+
1595+ // Verify the request was made with correct parameters
1596+ expect ( mockFetch ) . toHaveBeenCalledWith (
1597+ "https://api.openai.com/v1/responses" ,
1598+ expect . objectContaining ( {
1599+ method : "POST" ,
1600+ headers : expect . objectContaining ( {
1601+ "Content-Type" : "application/json" ,
1602+ Authorization : "Bearer test-api-key" ,
1603+ Accept : "text/event-stream" ,
1604+ } ) ,
1605+ body : expect . any ( String ) ,
1606+ } ) ,
1607+ )
1608+
1609+ const requestBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body )
1610+ expect ( requestBody ) . toMatchObject ( {
1611+ model : "codex-mini-latest" ,
1612+ input : "Developer: You are a helpful coding assistant.\n\nUser: Write a hello world function" ,
1613+ stream : true ,
1614+ } )
1615+
1616+ // Clean up
1617+ delete ( global as any ) . fetch
1618+ } )
1619+
1620+ it ( "should handle codex-mini-latest non-streaming completion" , async ( ) => {
1621+ handler = new OpenAiNativeHandler ( {
1622+ ...mockOptions ,
1623+ apiModelId : "codex-mini-latest" ,
1624+ } )
1625+
1626+ // Codex Mini now uses the same Responses API as GPT-5, which doesn't support non-streaming
1627+ await expect ( handler . completePrompt ( "Write a hello world function in Python" ) ) . rejects . toThrow (
1628+ "completePrompt is not supported for codex-mini-latest. Use createMessage (Responses API) instead." ,
1629+ )
1630+ } )
1631+
1632+ it ( "should handle codex-mini-latest API errors" , async ( ) => {
1633+ // Mock fetch with error response
1634+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
1635+ ok : false ,
1636+ status : 429 ,
1637+ statusText : "Too Many Requests" ,
1638+ text : async ( ) => "Rate limit exceeded" ,
1639+ } )
1640+ global . fetch = mockFetch as any
1641+
1642+ handler = new OpenAiNativeHandler ( {
1643+ ...mockOptions ,
1644+ apiModelId : "codex-mini-latest" ,
1645+ } )
1646+
1647+ const systemPrompt = "You are a helpful assistant."
1648+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" , content : "Hello" } ]
1649+
1650+ const stream = handler . createMessage ( systemPrompt , messages )
1651+
1652+ // Should throw an error (using the same error format as GPT-5)
1653+ await expect ( async ( ) => {
1654+ for await ( const chunk of stream ) {
1655+ // consume stream
1656+ }
1657+ } ) . rejects . toThrow ( "Rate limit exceeded" )
1658+
1659+ // Clean up
1660+ delete ( global as any ) . fetch
1661+ } )
1662+
1663+ it ( "should handle codex-mini-latest with multiple user messages" , async ( ) => {
1664+ // Mock fetch for streaming response
1665+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
1666+ ok : true ,
1667+ body : new ReadableStream ( {
1668+ start ( controller ) {
1669+ controller . enqueue (
1670+ new TextEncoder ( ) . encode (
1671+ 'data: {"type":"response.output_text.delta","delta":"Combined response"}\n\n' ,
1672+ ) ,
1673+ )
1674+ controller . enqueue ( new TextEncoder ( ) . encode ( 'data: {"type":"response.completed"}\n\n' ) )
1675+ controller . enqueue ( new TextEncoder ( ) . encode ( "data: [DONE]\n\n" ) )
1676+ controller . close ( )
1677+ } ,
1678+ } ) ,
1679+ } )
1680+ global . fetch = mockFetch as any
1681+
1682+ handler = new OpenAiNativeHandler ( {
1683+ ...mockOptions ,
1684+ apiModelId : "codex-mini-latest" ,
1685+ } )
1686+
1687+ const systemPrompt = "You are a helpful assistant."
1688+ const messages : Anthropic . Messages . MessageParam [ ] = [
1689+ { role : "user" , content : "First question" } ,
1690+ { role : "assistant" , content : "First answer" } ,
1691+ { role : "user" , content : "Second question" } ,
1692+ ]
1693+
1694+ const stream = handler . createMessage ( systemPrompt , messages )
1695+ const chunks : any [ ] = [ ]
1696+ for await ( const chunk of stream ) {
1697+ chunks . push ( chunk )
1698+ }
1699+
1700+ // Verify the request body includes full conversation like GPT-5
1701+ const requestBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body )
1702+ expect ( requestBody . input ) . toContain ( "Developer: You are a helpful assistant" )
1703+ expect ( requestBody . input ) . toContain ( "User: First question" )
1704+ expect ( requestBody . input ) . toContain ( "Assistant: First answer" )
1705+ expect ( requestBody . input ) . toContain ( "User: Second question" )
1706+
1707+ // Clean up
1708+ delete ( global as any ) . fetch
1709+ } )
1710+
1711+ it ( "should handle codex-mini-latest stream error events" , async ( ) => {
1712+ // Mock fetch with error event in stream
1713+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
1714+ ok : true ,
1715+ body : new ReadableStream ( {
1716+ start ( controller ) {
1717+ controller . enqueue (
1718+ new TextEncoder ( ) . encode (
1719+ 'data: {"type":"response.output_text.delta","delta":"Partial"}\n\n' ,
1720+ ) ,
1721+ )
1722+ controller . enqueue (
1723+ new TextEncoder ( ) . encode (
1724+ 'data: {"type":"response.error","error":{"message":"Model overloaded"}}\n\n' ,
1725+ ) ,
1726+ )
1727+ // The error handler will throw, but we still need to close the stream
1728+ controller . close ( )
1729+ } ,
1730+ } ) ,
1731+ } )
1732+ global . fetch = mockFetch as any
1733+
1734+ handler = new OpenAiNativeHandler ( {
1735+ ...mockOptions ,
1736+ apiModelId : "codex-mini-latest" ,
1737+ } )
1738+
1739+ const systemPrompt = "You are a helpful assistant."
1740+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" , content : "Hello" } ]
1741+
1742+ const stream = handler . createMessage ( systemPrompt , messages )
1743+
1744+ // Should throw an error when encountering error event
1745+ await expect ( async ( ) => {
1746+ const chunks = [ ]
1747+ for await ( const chunk of stream ) {
1748+ chunks . push ( chunk )
1749+ }
1750+ } ) . rejects . toThrow ( "Responses API error: Model overloaded" )
1751+
1752+ // Clean up
1753+ delete ( global as any ) . fetch
1754+ } )
1755+ } )
15171756} )
0 commit comments