Add Codex plan/spark provider support
This commit is contained in:
172
src/services/api/codexShim.test.ts
Normal file
172
src/services/api/codexShim.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import {
|
||||
codexStreamToAnthropic,
|
||||
convertAnthropicMessagesToResponsesInput,
|
||||
convertCodexResponseToAnthropicMessage,
|
||||
} from './codexShim.js'
|
||||
import {
|
||||
resolveCodexApiCredentials,
|
||||
resolveProviderRequest,
|
||||
} from './providerConfig.js'
|
||||
|
||||
const tempDirs: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop()
|
||||
if (dir) rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function createTempAuthJson(payload: Record<string, unknown>): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'openclaude-codex-'))
|
||||
tempDirs.push(dir)
|
||||
const authPath = join(dir, 'auth.json')
|
||||
writeFileSync(authPath, JSON.stringify(payload), 'utf8')
|
||||
return authPath
|
||||
}
|
||||
|
||||
async function collectStreamEventTypes(responseText: string): Promise<string[]> {
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(responseText))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
const events: string[] = []
|
||||
for await (const event of codexStreamToAnthropic(new Response(stream), 'gpt-5.4')) {
|
||||
events.push(event.type)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
describe('Codex provider config', () => {
|
||||
test('resolves codexplan alias to Codex transport with reasoning', () => {
|
||||
const resolved = resolveProviderRequest({ model: 'codexplan' })
|
||||
expect(resolved.transport).toBe('codex_responses')
|
||||
expect(resolved.resolvedModel).toBe('gpt-5.4')
|
||||
expect(resolved.reasoning).toEqual({ effort: 'high' })
|
||||
})
|
||||
|
||||
test('loads Codex credentials from auth.json fallback', () => {
|
||||
const authPath = createTempAuthJson({
|
||||
tokens: {
|
||||
access_token: 'header.payload.signature',
|
||||
account_id: 'acct_test',
|
||||
},
|
||||
})
|
||||
|
||||
const credentials = resolveCodexApiCredentials({
|
||||
CODEX_AUTH_JSON_PATH: authPath,
|
||||
} as NodeJS.ProcessEnv)
|
||||
|
||||
expect(credentials.apiKey).toBe('header.payload.signature')
|
||||
expect(credentials.accountId).toBe('acct_test')
|
||||
expect(credentials.source).toBe('auth.json')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Codex request translation', () => {
|
||||
test('converts assistant tool use and user tool result into Responses items', () => {
|
||||
const items = convertAnthropicMessagesToResponsesInput([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'text', text: 'Working...' },
|
||||
{ type: 'tool_use', id: 'call_123', name: 'search', input: { q: 'x' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'tool_result', tool_use_id: 'call_123', content: 'done' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
expect(items).toEqual([
|
||||
{
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'output_text', text: 'Working...' }],
|
||||
},
|
||||
{
|
||||
type: 'function_call',
|
||||
id: 'fc_123',
|
||||
call_id: 'call_123',
|
||||
name: 'search',
|
||||
arguments: '{"q":"x"}',
|
||||
},
|
||||
{
|
||||
type: 'function_call_output',
|
||||
call_id: 'call_123',
|
||||
output: 'done',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('converts completed Codex tool response into Anthropic message', () => {
|
||||
const message = convertCodexResponseToAnthropicMessage(
|
||||
{
|
||||
id: 'resp_1',
|
||||
model: 'gpt-5.3-codex-spark',
|
||||
output: [
|
||||
{
|
||||
type: 'function_call',
|
||||
id: 'fc_1',
|
||||
call_id: 'call_1',
|
||||
name: 'ping',
|
||||
arguments: '{"value":"ping"}',
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 12, output_tokens: 4 },
|
||||
},
|
||||
'gpt-5.3-codex-spark',
|
||||
)
|
||||
|
||||
expect(message.stop_reason).toBe('tool_use')
|
||||
expect(message.content).toEqual([
|
||||
{
|
||||
type: 'tool_use',
|
||||
id: 'call_1',
|
||||
name: 'ping',
|
||||
input: { value: 'ping' },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('translates Codex SSE text stream into Anthropic events', async () => {
|
||||
const responseText = [
|
||||
'event: response.output_item.added',
|
||||
'data: {"type":"response.output_item.added","item":{"id":"msg_1","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":0}',
|
||||
'',
|
||||
'event: response.content_part.added',
|
||||
'data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_1","output_index":0,"part":{"type":"output_text","text":""},"sequence_number":1}',
|
||||
'',
|
||||
'event: response.output_text.delta',
|
||||
'data: {"type":"response.output_text.delta","content_index":0,"delta":"ok","item_id":"msg_1","output_index":0,"sequence_number":2}',
|
||||
'',
|
||||
'event: response.output_item.done',
|
||||
'data: {"type":"response.output_item.done","item":{"id":"msg_1","type":"message","status":"completed","content":[{"type":"output_text","text":"ok"}],"role":"assistant"},"output_index":0,"sequence_number":3}',
|
||||
'',
|
||||
'event: response.completed',
|
||||
'data: {"type":"response.completed","response":{"id":"resp_1","status":"completed","model":"gpt-5.4","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":2,"output_tokens":1}},"sequence_number":4}',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const eventTypes = await collectStreamEventTypes(responseText)
|
||||
|
||||
expect(eventTypes).toEqual([
|
||||
'message_start',
|
||||
'content_block_start',
|
||||
'content_block_delta',
|
||||
'content_block_stop',
|
||||
'message_delta',
|
||||
'message_stop',
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user