@opengeni/codex 0.2.7 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/constants.ts CHANGED
@@ -52,7 +52,14 @@ export const CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT = Math.floor(
52
52
  // 2026-07-09: 0.142.4 filtered every GPT-5.6 slug out of GET /models, while the
53
53
  // official Codex 0.144.0+ releases return all three exact slugs above. Keep
54
54
  // this pinned to the latest stable Codex release we have verified end-to-end.
55
- export const CODEX_CLIENT_VERSION = "0.144.6";
55
+ export const CODEX_CLIENT_VERSION = "0.145.0";
56
+
57
+ // Native ChatGPT/Codex subscription WebRTC call creation. Verified against
58
+ // openai/codex 0.145.0 commit bd2de422aa287b97b06ca6425a10935bcf1b3731.
59
+ export const CODEX_REALTIME_MODEL = "gpt-live-1-boulder-alpha";
60
+ export const CODEX_REALTIME_VERSION = "v3";
61
+ export const CODEX_REALTIME_DEFAULT_VOICE = "cove";
62
+ export const CODEX_REALTIME_CALL_TIMEOUT_MS = 15_000;
56
63
 
57
64
  export const CODEX_REFRESH_WINDOW_MS = 5 * 60 * 1000; // proactive refresh when within 5 min of exp (spec §1.1)
58
65
  export const CODEX_REFRESH_FALLBACK_MS = 8 * 24 * 60 * 60 * 1000; // 8 days when exp is unparseable
package/src/fetch.ts CHANGED
@@ -63,6 +63,56 @@ export function isCodexTransportError(error: unknown): boolean {
63
63
  return false;
64
64
  }
65
65
 
66
+ export type CodexEncryptedArtifactRejection = {
67
+ status: 400;
68
+ kind: "encrypted_content_rejected";
69
+ };
70
+
71
+ /**
72
+ * Classify only the provider's definitive request rejection for an opaque
73
+ * reasoning artifact that it can no longer decrypt/parse. A Codex transport
74
+ * marker plus HTTP 400 proves this request was rejected before inference; the
75
+ * semantic match prevents unrelated malformed prompts from entering recovery.
76
+ */
77
+ export function classifyCodexEncryptedArtifactRejection(
78
+ error: unknown,
79
+ ): CodexEncryptedArtifactRejection | null {
80
+ if (!isCodexTransportError(error)) return null;
81
+ let current: unknown = error;
82
+ for (let depth = 0; depth < 6 && current && typeof current === "object"; depth += 1) {
83
+ const value = current as Record<string, unknown>;
84
+ const body =
85
+ value.error && typeof value.error === "object"
86
+ ? (value.error as Record<string, unknown>)
87
+ : null;
88
+ const status = Number(value.status ?? body?.status);
89
+ const message = [
90
+ typeof value.message === "string" ? value.message : "",
91
+ typeof body?.message === "string" ? body.message : "",
92
+ typeof value.code === "string" ? value.code : "",
93
+ typeof body?.code === "string" ? body.code : "",
94
+ typeof value.type === "string" ? value.type : "",
95
+ typeof body?.type === "string" ? body.type : "",
96
+ ]
97
+ .join(" ")
98
+ .toLowerCase();
99
+ const unsupportedFieldShape =
100
+ /(?:invalid value|supported values?|unsupported|unknown (?:field|parameter|value))/.test(
101
+ message,
102
+ );
103
+ if (
104
+ status === 400 &&
105
+ !unsupportedFieldShape &&
106
+ /(?:encrypted[_ ]content|encrypted reasoning|reasoning artifact)/.test(message) &&
107
+ /(?:decrypt(?:ed|ion)?|could not be parsed|cannot be parsed|failed to parse)/.test(message)
108
+ ) {
109
+ return { status: 400, kind: "encrypted_content_rejected" };
110
+ }
111
+ current = value.cause;
112
+ }
113
+ return null;
114
+ }
115
+
66
116
  /** Parse an integer header value; null when absent or not a finite integer. */
67
117
  function parseIntHeader(value: string | null): number | null {
68
118
  if (value === null) {
@@ -284,7 +334,9 @@ async function observedResponse(
284
334
  if (externalSignal?.aborted) {
285
335
  abortFromOutside();
286
336
  } else {
287
- externalSignal?.addEventListener("abort", abortFromOutside, { once: true });
337
+ externalSignal?.addEventListener("abort", abortFromOutside, {
338
+ once: true,
339
+ });
288
340
  }
289
341
  },
290
342
  async pull(controller) {
@@ -344,7 +396,11 @@ async function observedResponse(
344
396
  });
345
397
  const headers = new Headers(res.headers);
346
398
  headers.delete("content-length");
347
- return new Response(body, { status: res.status, statusText: res.statusText, headers });
399
+ return new Response(body, {
400
+ status: res.status,
401
+ statusText: res.statusText,
402
+ headers,
403
+ });
348
404
  }
349
405
 
350
406
  function timeoutErrorResponse(info: {
@@ -418,6 +474,15 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
418
474
  }
419
475
  headers.delete("OpenAI-Beta"); // omit on SSE (spec §1.2); fallback: "responses=experimental" if backend 400s
420
476
  headers.delete("x-api-key");
477
+ // Codex CLI advertises betas via x-codex-beta-features (not OpenAI-Beta).
478
+ if (ctx.betaFeatures && ctx.betaFeatures.length > 0) {
479
+ headers.set("x-codex-beta-features", ctx.betaFeatures.join(","));
480
+ }
481
+ // Turn analytics / request_kind live in x-codex-turn-metadata — body
482
+ // metadata is stripped by normalizeCodexRequestBody and rejected upstream.
483
+ if (ctx.turnMetadata && Object.keys(ctx.turnMetadata).length > 0) {
484
+ headers.set("x-codex-turn-metadata", JSON.stringify(ctx.turnMetadata));
485
+ }
421
486
 
422
487
  // The backend is streaming-only; force stream=true on the wire but remember
423
488
  // the caller's intent so a non-streaming caller (e.g. the compaction
package/src/index.ts CHANGED
@@ -11,3 +11,5 @@ export * from "./response-timeout";
11
11
  export * from "./fetch";
12
12
  export * from "./mcp-sanitize";
13
13
  export * from "./model-output-truncation";
14
+ export * from "./realtime";
15
+ export * from "./realtime-v3";
package/src/normalize.ts CHANGED
@@ -16,9 +16,11 @@ const MINIMAL = "minimal";
16
16
  // The ChatGPT/Codex backend is a STRICT ALLOWLIST: it 400s on ANY top-level field
17
17
  // the Codex CLI itself does not send (confirmed live against the backend —
18
18
  // "Unsupported parameter: temperature / top_p / metadata / previous_response_id /
19
- // logprobs / service_tier / user / safety_identifier / truncation / max_tool_calls /
20
- // background / conversation", and "Unsupported tool type: mcp"). Our @openai/agents
21
- // stack adds several of these, so after our transforms we keep ONLY the codex
19
+ // logprobs / user / safety_identifier / truncation / max_tool_calls /
20
+ // background / conversation", and "Unsupported tool type: mcp").
21
+ // `service_tier` is allowlisted for Codex Fast mode (`priority`; config may say
22
+ // `fast` and maps to the same request value). Our @openai/agents stack adds
23
+ // several other fields, so after our transforms we keep ONLY the codex
22
24
  // Responses payload fields (CODEX-SUBSCRIPTION-SPEC §1 field table).
23
25
  const CODEX_ALLOWED_TOP_LEVEL_KEYS = new Set<string>([
24
26
  "model",
@@ -33,6 +35,7 @@ const CODEX_ALLOWED_TOP_LEVEL_KEYS = new Set<string>([
33
35
  "include",
34
36
  "prompt_cache_key",
35
37
  "text",
38
+ "service_tier",
36
39
  ]);
37
40
 
38
41
  /** Mutates a parsed Responses request body in place and returns it. Pure + synchronous + unit-testable. */
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Codex GPT-Live Frameless Bidi (V3) wire adapter.
3
+ *
4
+ * These shapes are pinned to openai/codex@fa1d4c40. V3 delegates work with
5
+ * `delegation.created`; it does not expose the ordinary Realtime V2
6
+ * `function_call` protocol.
7
+ */
8
+
9
+ export const CODEX_REALTIME_INITIAL_ITEMS_MAX_COUNT = 128;
10
+ export const CODEX_REALTIME_INITIAL_ITEMS_MAX_TOKENS = 8_192;
11
+ export const CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES = 500;
12
+ export const CODEX_REALTIME_V3_MAX_EVENT_BYTES = 1_048_576;
13
+ export const CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES = 1_024;
14
+ export const CODEX_REALTIME_V3_MAX_TEXT_BYTES = 131_072;
15
+
16
+ export type CodexRealtimeInitialItem = {
17
+ role: "user" | "developer" | "assistant";
18
+ text: string;
19
+ };
20
+
21
+ type ProviderEventIdentity = {
22
+ providerEventId: string | null;
23
+ };
24
+
25
+ export type CodexRealtimeV3Event =
26
+ | (ProviderEventIdentity & {
27
+ type: "session.started" | "session.updated";
28
+ sessionId: string;
29
+ instructions: string | null;
30
+ })
31
+ | (ProviderEventIdentity & {
32
+ type: "input_transcript.added" | "output_transcript.added";
33
+ itemId: string | null;
34
+ text: string;
35
+ })
36
+ | (ProviderEventIdentity & {
37
+ type: "output_audio.delta";
38
+ audio: string;
39
+ startMs: number | null;
40
+ endMs: number | null;
41
+ })
42
+ | (ProviderEventIdentity & {
43
+ type: "turn.done";
44
+ turnId: string;
45
+ role: "user" | "assistant";
46
+ transcript: string;
47
+ })
48
+ | (ProviderEventIdentity & {
49
+ type: "delegation.created";
50
+ delegationItemId: string;
51
+ inputTranscript: string;
52
+ offsetMs: number | null;
53
+ })
54
+ | (ProviderEventIdentity & {
55
+ type: "error";
56
+ message: string;
57
+ });
58
+
59
+ export type CodexRealtimeV3ParseFailure = {
60
+ ok: false;
61
+ reason:
62
+ | "invalid_json"
63
+ | "missing_type"
64
+ | "unsupported_type"
65
+ | "invalid_shape"
66
+ | "oversized_event"
67
+ | "oversized_field";
68
+ eventType: string | null;
69
+ };
70
+
71
+ export type CodexRealtimeV3ParseResult =
72
+ | { ok: true; event: CodexRealtimeV3Event }
73
+ | CodexRealtimeV3ParseFailure;
74
+
75
+ export type CodexRealtimeV3ContextAppendChannel = "speakable" | "commentary";
76
+
77
+ export type CodexRealtimeV3DelegationContextAppend = {
78
+ type: "delegation.context.append";
79
+ delegation_item_id: string;
80
+ channel?: CodexRealtimeV3ContextAppendChannel | undefined;
81
+ content: Array<{ type: "input_text"; text: string }>;
82
+ };
83
+
84
+ export type CodexRealtimeV3SessionContextAppend = {
85
+ type: "session.context.append";
86
+ channel?: CodexRealtimeV3ContextAppendChannel | undefined;
87
+ content: Array<{ type: "input_text"; text: string }>;
88
+ };
89
+
90
+ export function parseCodexRealtimeV3Event(payload: string): CodexRealtimeV3ParseResult {
91
+ if (utf8ByteLength(payload) > CODEX_REALTIME_V3_MAX_EVENT_BYTES) {
92
+ return failure("oversized_event", null);
93
+ }
94
+ let value: unknown;
95
+ try {
96
+ value = JSON.parse(payload);
97
+ } catch {
98
+ return failure("invalid_json", null);
99
+ }
100
+ const event = record(value);
101
+ const typeField = boundedStringField(event, "type", CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES);
102
+ if (typeField.oversized) return failure("oversized_field", null);
103
+ const type = typeField.value;
104
+ if (!type) return failure("missing_type", null);
105
+ const providerEventIdField = firstBoundedStringField(
106
+ event,
107
+ ["event_id", "id"],
108
+ CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES,
109
+ );
110
+ if (providerEventIdField.oversized) return failure("oversized_field", type);
111
+ const providerEventId = providerEventIdField.value ?? null;
112
+
113
+ if (type === "session.started" || type === "session.updated") {
114
+ const session = record(event.session);
115
+ const sessionIdField = boundedStringField(
116
+ session,
117
+ "id",
118
+ CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES,
119
+ );
120
+ const instructionsField = boundedStringField(
121
+ session,
122
+ "instructions",
123
+ CODEX_REALTIME_V3_MAX_TEXT_BYTES,
124
+ );
125
+ if (sessionIdField.oversized || instructionsField.oversized) {
126
+ return failure("oversized_field", type);
127
+ }
128
+ const sessionId = sessionIdField.value;
129
+ if (!sessionId) return failure("invalid_shape", type);
130
+ return {
131
+ ok: true,
132
+ event: {
133
+ type,
134
+ providerEventId,
135
+ sessionId,
136
+ instructions: instructionsField.value ?? null,
137
+ },
138
+ };
139
+ }
140
+
141
+ if (type === "input_transcript.added" || type === "output_transcript.added") {
142
+ const item = record(event.item);
143
+ const itemIdField = boundedStringField(item, "id", CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES);
144
+ const textField = boundedStringField(item, "text", CODEX_REALTIME_V3_MAX_TEXT_BYTES);
145
+ if (itemIdField.oversized || textField.oversized) {
146
+ return failure("oversized_field", type);
147
+ }
148
+ const text = textField.value;
149
+ if (text === undefined) return failure("invalid_shape", type);
150
+ return {
151
+ ok: true,
152
+ event: {
153
+ type,
154
+ providerEventId,
155
+ itemId: itemIdField.value ?? null,
156
+ text,
157
+ },
158
+ };
159
+ }
160
+
161
+ if (type === "output_audio.delta") {
162
+ const audio = stringField(event, "audio");
163
+ if (audio === undefined) return failure("invalid_shape", type);
164
+ return {
165
+ ok: true,
166
+ event: {
167
+ type,
168
+ providerEventId,
169
+ audio,
170
+ startMs: finiteNumberField(event, "start_ms"),
171
+ endMs: finiteNumberField(event, "end_ms"),
172
+ },
173
+ };
174
+ }
175
+
176
+ if (type === "turn.done") {
177
+ const turn = record(event.turn);
178
+ const turnIdField = boundedStringField(turn, "id", CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES);
179
+ const transcriptField = boundedStringField(
180
+ turn,
181
+ "transcript",
182
+ CODEX_REALTIME_V3_MAX_TEXT_BYTES,
183
+ );
184
+ if (turnIdField.oversized || transcriptField.oversized) {
185
+ return failure("oversized_field", type);
186
+ }
187
+ const turnId = turnIdField.value;
188
+ const role = stringField(turn, "role");
189
+ const transcript = transcriptField.value;
190
+ if (!turnId || (role !== "user" && role !== "assistant") || transcript === undefined) {
191
+ return failure("invalid_shape", type);
192
+ }
193
+ return {
194
+ ok: true,
195
+ event: { type, providerEventId, turnId, role, transcript },
196
+ };
197
+ }
198
+
199
+ if (type === "delegation.created") {
200
+ const item = record(event.item);
201
+ const delegationItemIdField = boundedStringField(
202
+ item,
203
+ "id",
204
+ CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES,
205
+ );
206
+ if (delegationItemIdField.oversized) return failure("oversized_field", type);
207
+ const delegationItemId = delegationItemIdField.value;
208
+ if (
209
+ !delegationItemId ||
210
+ stringField(item, "type") !== "delegation" ||
211
+ stringField(item, "target") !== "client" ||
212
+ !Array.isArray(item.content)
213
+ ) {
214
+ return failure("invalid_shape", type);
215
+ }
216
+ const inputParts: string[] = [];
217
+ let inputBytes = 0;
218
+ for (const rawContent of item.content) {
219
+ const content = record(rawContent);
220
+ if (stringField(content, "type") !== "input_text") continue;
221
+ const textField = boundedStringField(content, "text", CODEX_REALTIME_V3_MAX_TEXT_BYTES);
222
+ if (textField.oversized) return failure("oversized_field", type);
223
+ const text = textField.value ?? "";
224
+ inputBytes += utf8ByteLength(text);
225
+ if (inputBytes > CODEX_REALTIME_V3_MAX_TEXT_BYTES) {
226
+ return failure("oversized_field", type);
227
+ }
228
+ inputParts.push(text);
229
+ }
230
+ const inputTranscript = inputParts.join("");
231
+ return {
232
+ ok: true,
233
+ event: {
234
+ type,
235
+ providerEventId,
236
+ delegationItemId,
237
+ inputTranscript,
238
+ offsetMs: finiteNumberField(event, "offset_ms"),
239
+ },
240
+ };
241
+ }
242
+
243
+ if (type === "error") {
244
+ const nested = record(event.error);
245
+ const message =
246
+ stringField(event, "message") ??
247
+ stringField(nested, "message") ??
248
+ (event.error === undefined ? undefined : JSON.stringify(event.error));
249
+ if (message === undefined) return failure("invalid_shape", type);
250
+ if (utf8ByteLength(message) > CODEX_REALTIME_V3_MAX_TEXT_BYTES) {
251
+ return failure("oversized_field", type);
252
+ }
253
+ return { ok: true, event: { type, providerEventId, message } };
254
+ }
255
+
256
+ return failure("unsupported_type", type);
257
+ }
258
+
259
+ export function encodeCodexRealtimeV3DelegationContextAppend(input: {
260
+ delegationItemId: string;
261
+ text: string;
262
+ channel?: CodexRealtimeV3ContextAppendChannel | undefined;
263
+ }): CodexRealtimeV3DelegationContextAppend[] {
264
+ return contextAppendChunks(input.text).map((text) => ({
265
+ type: "delegation.context.append",
266
+ delegation_item_id: input.delegationItemId,
267
+ ...(input.channel ? { channel: input.channel } : {}),
268
+ content: [{ type: "input_text", text }],
269
+ }));
270
+ }
271
+
272
+ export function encodeCodexRealtimeV3SessionContextAppend(input: {
273
+ text: string;
274
+ channel?: CodexRealtimeV3ContextAppendChannel | undefined;
275
+ }): CodexRealtimeV3SessionContextAppend[] {
276
+ return contextAppendChunks(input.text).map((text) => ({
277
+ type: "session.context.append",
278
+ ...(input.channel ? { channel: input.channel } : {}),
279
+ content: [{ type: "input_text", text }],
280
+ }));
281
+ }
282
+
283
+ export function contextAppendChunks(text: string): string[] {
284
+ if (utf8ByteLength(text) <= CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES) return [text];
285
+ const chunks: string[] = [];
286
+ let chunk = "";
287
+ let bytes = 0;
288
+ for (const character of text) {
289
+ const characterBytes = utf8ByteLength(character);
290
+ if (bytes + characterBytes > CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES && chunk) {
291
+ chunks.push(chunk);
292
+ chunk = "";
293
+ bytes = 0;
294
+ }
295
+ chunk += character;
296
+ bytes += characterBytes;
297
+ }
298
+ if (chunk || text.length === 0) chunks.push(chunk);
299
+ return chunks;
300
+ }
301
+
302
+ function utf8ByteLength(value: string): number {
303
+ return new TextEncoder().encode(value).byteLength;
304
+ }
305
+
306
+ function failure(
307
+ reason: CodexRealtimeV3ParseFailure["reason"],
308
+ eventType: string | null,
309
+ ): CodexRealtimeV3ParseFailure {
310
+ return { ok: false, reason, eventType };
311
+ }
312
+
313
+ function record(value: unknown): Record<string, unknown> {
314
+ return value !== null && typeof value === "object" && !Array.isArray(value)
315
+ ? (value as Record<string, unknown>)
316
+ : {};
317
+ }
318
+
319
+ function stringField(value: Record<string, unknown>, key: string): string | undefined {
320
+ return typeof value[key] === "string" ? value[key] : undefined;
321
+ }
322
+
323
+ function boundedStringField(
324
+ value: Record<string, unknown>,
325
+ key: string,
326
+ maxBytes: number,
327
+ ): { value: string | undefined; oversized: boolean } {
328
+ const candidate = stringField(value, key);
329
+ return {
330
+ value: candidate,
331
+ oversized: candidate !== undefined && utf8ByteLength(candidate) > maxBytes,
332
+ };
333
+ }
334
+
335
+ function firstBoundedStringField(
336
+ value: Record<string, unknown>,
337
+ keys: readonly string[],
338
+ maxBytes: number,
339
+ ): { value: string | undefined; oversized: boolean } {
340
+ for (const key of keys) {
341
+ if (typeof value[key] === "string") return boundedStringField(value, key, maxBytes);
342
+ }
343
+ return { value: undefined, oversized: false };
344
+ }
345
+
346
+ function finiteNumberField(value: Record<string, unknown>, key: string): number | null {
347
+ const candidate = value[key];
348
+ return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : null;
349
+ }