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;
  }
}