Spaces:
Paused
Paused
File size: 4,936 Bytes
347f81b fadda70 347f81b ab2754a 9d6278d 347f81b 7366e72 347f81b cf0807a ab2754a 347f81b ab2754a 347f81b 7366e72 347f81b 7366e72 ab2754a 1531084 ab2754a 8068f1c 1531084 8068f1c 347f81b cf0807a 347f81b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | /**
* Shared Codex SSE event data extraction layer.
*
* The three translation files (OpenAI, Anthropic, Gemini) all extract
* the same data from Codex events — this module centralizes that logic.
*/
import type { CodexApi, CodexSSEEvent } from "../proxy/codex-api.js";
import {
parseCodexEvent,
type TypedCodexEvent,
} from "../types/codex-events.js";
export interface UsageInfo {
input_tokens: number;
output_tokens: number;
cached_tokens?: number;
reasoning_tokens?: number;
}
export interface FunctionCallStart {
callId: string;
name: string;
outputIndex: number;
}
export interface FunctionCallDelta {
callId: string;
delta: string;
}
export interface FunctionCallDone {
callId: string;
name: string;
arguments: string;
}
export class EmptyResponseError extends Error {
constructor(
public readonly responseId: string | null,
public readonly usage: UsageInfo | undefined,
) {
super("Codex returned an empty response");
this.name = "EmptyResponseError";
}
}
export interface ExtractedEvent {
typed: TypedCodexEvent;
responseId?: string;
textDelta?: string;
reasoningDelta?: string;
usage?: UsageInfo;
error?: { code: string; message: string };
functionCallStart?: FunctionCallStart;
functionCallDelta?: FunctionCallDelta;
functionCallDone?: FunctionCallDone;
}
/**
* Iterate over a Codex SSE stream, parsing + extracting common fields.
* Yields ExtractedEvent with pre-extracted responseId, textDelta, and usage.
*/
export async function* iterateCodexEvents(
codexApi: CodexApi,
rawResponse: Response,
): AsyncGenerator<ExtractedEvent> {
// Map item_id → { call_id, name } for resolving delta/done events
const itemIdToCallInfo = new Map<string, { callId: string; name: string }>();
for await (const raw of codexApi.parseStream(rawResponse)) {
const typed = parseCodexEvent(raw);
const extracted: ExtractedEvent = { typed };
// Log unrecognized events to discover new Codex event types
if (typed.type === "unknown") {
console.debug(`[CodexEvents] Unknown event: ${raw.event}`, JSON.stringify(raw.data).slice(0, 300));
}
switch (typed.type) {
case "response.created":
case "response.in_progress":
if (typed.response.id) extracted.responseId = typed.response.id;
break;
case "response.output_text.delta":
extracted.textDelta = typed.delta;
break;
case "response.reasoning_summary_text.delta":
extracted.reasoningDelta = typed.delta;
break;
case "response.output_item.added":
if (typed.item.type === "function_call" && typed.item.call_id && typed.item.name) {
// Register item_id → call_id mapping
itemIdToCallInfo.set(typed.item.id, {
callId: typed.item.call_id,
name: typed.item.name,
});
extracted.functionCallStart = {
callId: typed.item.call_id,
name: typed.item.name,
outputIndex: typed.outputIndex,
};
}
break;
case "response.function_call_arguments.delta": {
// Resolve item_id to call_id if needed
const deltaInfo = itemIdToCallInfo.get(typed.call_id);
extracted.functionCallDelta = {
callId: deltaInfo?.callId ?? typed.call_id,
delta: typed.delta,
};
break;
}
case "response.function_call_arguments.done": {
// Resolve item_id to call_id + name if needed
const doneInfo = itemIdToCallInfo.get(typed.call_id);
extracted.functionCallDone = {
callId: doneInfo?.callId ?? typed.call_id,
name: typed.name || doneInfo?.name || "",
arguments: typed.arguments,
};
break;
}
case "response.output_item.done":
case "response.content_part.added":
case "response.content_part.done":
// Lifecycle markers — no data extraction needed
break;
case "response.incomplete":
// Response was truncated/incomplete
if (typed.response.id) extracted.responseId = typed.response.id;
if (typed.response.usage) extracted.usage = typed.response.usage;
break;
case "response.queued":
// Response is queued for processing
if (typed.response.id) extracted.responseId = typed.response.id;
break;
case "response.completed":
if (typed.response.id) extracted.responseId = typed.response.id;
if (typed.response.usage) extracted.usage = typed.response.usage;
break;
case "error":
extracted.error = { code: typed.error.code, message: typed.error.message };
break;
case "response.failed":
extracted.error = { code: typed.error.code, message: typed.error.message };
if (typed.response.id) extracted.responseId = typed.response.id;
break;
}
yield extracted;
}
}
|