Skip to content

Commit 2e3197d

Browse files
authored
fix: avoid stream:false error in CodexMessageAdapter.generateResponse (#511)
* fix: avoid stream:false error in CodexMessageAdapter.generateResponse * fix: Fix lint
1 parent bafa77f commit 2e3197d

File tree

1 file changed

+150
-16
lines changed

1 file changed

+150
-16
lines changed

src/core/llm/codexMessageAdapter.ts

Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ import {
2525
ToolCall,
2626
ToolCallDelta,
2727
} from '../../types/llm/response'
28-
import {
29-
StreamSource,
30-
postJson,
31-
postStream,
32-
} from '../../utils/llm/httpTransport'
28+
import { StreamSource, postStream } from '../../utils/llm/httpTransport'
3329
import { parseJsonSseStream } from '../../utils/llm/sse'
3430

3531
type CodexAdapterConfig = {
@@ -51,24 +47,65 @@ export class CodexMessageAdapter {
5147
options?: LLMOptions,
5248
headers?: Record<string, string>,
5349
): Promise<LLMResponseNonStreaming> {
54-
const body = this.buildRequestBody({ request, stream: false })
55-
const payload = await postJson<Response>(this.endpoint, body, {
50+
// Codex Responses require stream: true; build a snapshot from the stream.
51+
const body = this.buildRequestBody({ request, stream: true })
52+
const stream = await postStream(this.endpoint, body, {
5653
headers,
5754
signal: options?.signal,
5855
fetchFn: this.fetchFn,
5956
})
60-
const content = extractResponseText(payload)
61-
const toolCalls = extractToolCalls(payload)
62-
const reasoningSummary = extractReasoningSummary(payload)
57+
58+
let summaryText = ''
59+
let responsePayload: Response | undefined
60+
for await (const chunk of parseJsonSseStream<ResponseStreamEvent>(stream)) {
61+
if (chunk.type === 'response.created') {
62+
responsePayload = chunk.response
63+
continue
64+
}
65+
66+
if (chunk.type === 'error') {
67+
throw new Error(chunk.message)
68+
}
69+
70+
if (!responsePayload) {
71+
throw new Error(
72+
`Stream event received before response.created: ${chunk.type}`,
73+
)
74+
}
75+
76+
if (chunk.type === 'response.reasoning_summary_text.delta') {
77+
summaryText += chunk.delta
78+
continue
79+
}
80+
81+
if (chunk.type === 'response.reasoning_summary_text.done') {
82+
if (!summaryText.length) {
83+
summaryText = chunk.text
84+
}
85+
continue
86+
}
87+
88+
responsePayload = accumulateResponseSnapshot(responsePayload, chunk)
89+
}
90+
91+
if (!responsePayload) {
92+
throw new Error('Stream ended without receiving a response payload')
93+
}
94+
95+
const content = extractResponseText(responsePayload)
96+
const toolCalls = extractToolCalls(responsePayload)
97+
const reasoningSummary =
98+
extractReasoningSummary(responsePayload) ??
99+
(summaryText.length ? summaryText : undefined)
63100

64101
return {
65-
id: payload.id,
66-
created: payload.created_at,
67-
model: payload.model,
102+
id: responsePayload.id,
103+
created: responsePayload.created_at,
104+
model: responsePayload.model,
68105
object: 'chat.completion',
69106
choices: [
70107
{
71-
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
108+
finish_reason: null,
72109
message: {
73110
role: 'assistant',
74111
content,
@@ -77,8 +114,8 @@ export class CodexMessageAdapter {
77114
},
78115
},
79116
],
80-
system_fingerprint: getSystemFingerprint(payload),
81-
usage: mapUsage(payload.usage),
117+
system_fingerprint: getSystemFingerprint(responsePayload),
118+
usage: mapUsage(responsePayload.usage),
82119
}
83120
}
84121

@@ -536,6 +573,103 @@ function getSystemFingerprint(payload: Response): string | undefined {
536573
return (payload as { system_fingerprint?: string }).system_fingerprint
537574
}
538575

576+
function accumulateResponseSnapshot(
577+
snapshot: Response,
578+
event: ResponseStreamEvent,
579+
): Response {
580+
switch (event.type) {
581+
case 'response.output_item.added': {
582+
snapshot.output.push(event.item)
583+
return snapshot
584+
}
585+
case 'response.content_part.added': {
586+
const output = snapshot.output[event.output_index]
587+
if (!output) {
588+
throw new Error(`missing output at index ${event.output_index}`)
589+
}
590+
const part = event.part
591+
if (output.type === 'message' && part.type !== 'reasoning_text') {
592+
output.content.push(part)
593+
} else if (
594+
output.type === 'reasoning' &&
595+
part.type === 'reasoning_text'
596+
) {
597+
if (!output.content) {
598+
output.content = []
599+
}
600+
output.content.push(part)
601+
}
602+
return snapshot
603+
}
604+
case 'response.output_text.delta': {
605+
const output = snapshot.output[event.output_index]
606+
if (!output) {
607+
throw new Error(`missing output at index ${event.output_index}`)
608+
}
609+
if (output.type === 'message') {
610+
const content = output.content[event.content_index]
611+
if (!content) {
612+
throw new Error(`missing content at index ${event.content_index}`)
613+
}
614+
if (content.type !== 'output_text') {
615+
throw new Error(
616+
`expected content to be 'output_text', got ${content.type}`,
617+
)
618+
}
619+
content.text += event.delta
620+
}
621+
return snapshot
622+
}
623+
case 'response.function_call_arguments.delta': {
624+
const output = snapshot.output[event.output_index]
625+
if (!output) {
626+
throw new Error(`missing output at index ${event.output_index}`)
627+
}
628+
if (output.type === 'function_call') {
629+
output.arguments += event.delta
630+
}
631+
return snapshot
632+
}
633+
case 'response.function_call_arguments.done': {
634+
const output = snapshot.output[event.output_index]
635+
if (!output) {
636+
throw new Error(`missing output at index ${event.output_index}`)
637+
}
638+
if (output.type === 'function_call' && !output.arguments?.length) {
639+
output.arguments = event.arguments
640+
}
641+
return snapshot
642+
}
643+
case 'response.reasoning_text.delta': {
644+
const output = snapshot.output[event.output_index]
645+
if (!output) {
646+
throw new Error(`missing output at index ${event.output_index}`)
647+
}
648+
if (output.type === 'reasoning') {
649+
const content = output.content?.[event.content_index]
650+
if (!content) {
651+
throw new Error(`missing content at index ${event.content_index}`)
652+
}
653+
if (content.type !== 'reasoning_text') {
654+
const contentType = (content as { type: string }).type
655+
throw new Error(
656+
`expected content to be 'reasoning_text', got ${contentType}`,
657+
)
658+
}
659+
content.text += event.delta
660+
}
661+
return snapshot
662+
}
663+
case 'response.completed':
664+
return event.response
665+
case 'response.incomplete':
666+
return event.response
667+
case 'error':
668+
return snapshot
669+
}
670+
return snapshot
671+
}
672+
539673
function mapUsage(usage?: OpenAIResponseUsage): ResponseUsage | undefined {
540674
if (!usage) {
541675
return undefined

0 commit comments

Comments
 (0)