@opengeni/codex 0.2.9 → 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.
@@ -0,0 +1,86 @@
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
+ export declare const CODEX_REALTIME_INITIAL_ITEMS_MAX_COUNT = 128;
9
+ export declare const CODEX_REALTIME_INITIAL_ITEMS_MAX_TOKENS = 8192;
10
+ export declare const CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES = 500;
11
+ export declare const CODEX_REALTIME_V3_MAX_EVENT_BYTES = 1048576;
12
+ export declare const CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES = 1024;
13
+ export declare const CODEX_REALTIME_V3_MAX_TEXT_BYTES = 131072;
14
+ export type CodexRealtimeInitialItem = {
15
+ role: "user" | "developer" | "assistant";
16
+ text: string;
17
+ };
18
+ type ProviderEventIdentity = {
19
+ providerEventId: string | null;
20
+ };
21
+ export type CodexRealtimeV3Event = (ProviderEventIdentity & {
22
+ type: "session.started" | "session.updated";
23
+ sessionId: string;
24
+ instructions: string | null;
25
+ }) | (ProviderEventIdentity & {
26
+ type: "input_transcript.added" | "output_transcript.added";
27
+ itemId: string | null;
28
+ text: string;
29
+ }) | (ProviderEventIdentity & {
30
+ type: "output_audio.delta";
31
+ audio: string;
32
+ startMs: number | null;
33
+ endMs: number | null;
34
+ }) | (ProviderEventIdentity & {
35
+ type: "turn.done";
36
+ turnId: string;
37
+ role: "user" | "assistant";
38
+ transcript: string;
39
+ }) | (ProviderEventIdentity & {
40
+ type: "delegation.created";
41
+ delegationItemId: string;
42
+ inputTranscript: string;
43
+ offsetMs: number | null;
44
+ }) | (ProviderEventIdentity & {
45
+ type: "error";
46
+ message: string;
47
+ });
48
+ export type CodexRealtimeV3ParseFailure = {
49
+ ok: false;
50
+ reason: "invalid_json" | "missing_type" | "unsupported_type" | "invalid_shape" | "oversized_event" | "oversized_field";
51
+ eventType: string | null;
52
+ };
53
+ export type CodexRealtimeV3ParseResult = {
54
+ ok: true;
55
+ event: CodexRealtimeV3Event;
56
+ } | CodexRealtimeV3ParseFailure;
57
+ export type CodexRealtimeV3ContextAppendChannel = "speakable" | "commentary";
58
+ export type CodexRealtimeV3DelegationContextAppend = {
59
+ type: "delegation.context.append";
60
+ delegation_item_id: string;
61
+ channel?: CodexRealtimeV3ContextAppendChannel | undefined;
62
+ content: Array<{
63
+ type: "input_text";
64
+ text: string;
65
+ }>;
66
+ };
67
+ export type CodexRealtimeV3SessionContextAppend = {
68
+ type: "session.context.append";
69
+ channel?: CodexRealtimeV3ContextAppendChannel | undefined;
70
+ content: Array<{
71
+ type: "input_text";
72
+ text: string;
73
+ }>;
74
+ };
75
+ export declare function parseCodexRealtimeV3Event(payload: string): CodexRealtimeV3ParseResult;
76
+ export declare function encodeCodexRealtimeV3DelegationContextAppend(input: {
77
+ delegationItemId: string;
78
+ text: string;
79
+ channel?: CodexRealtimeV3ContextAppendChannel | undefined;
80
+ }): CodexRealtimeV3DelegationContextAppend[];
81
+ export declare function encodeCodexRealtimeV3SessionContextAppend(input: {
82
+ text: string;
83
+ channel?: CodexRealtimeV3ContextAppendChannel | undefined;
84
+ }): CodexRealtimeV3SessionContextAppend[];
85
+ export declare function contextAppendChunks(text: string): string[];
86
+ export {};
@@ -0,0 +1,25 @@
1
+ import {
2
+ CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES,
3
+ CODEX_REALTIME_INITIAL_ITEMS_MAX_COUNT,
4
+ CODEX_REALTIME_INITIAL_ITEMS_MAX_TOKENS,
5
+ CODEX_REALTIME_V3_MAX_EVENT_BYTES,
6
+ CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES,
7
+ CODEX_REALTIME_V3_MAX_TEXT_BYTES,
8
+ contextAppendChunks,
9
+ encodeCodexRealtimeV3DelegationContextAppend,
10
+ encodeCodexRealtimeV3SessionContextAppend,
11
+ parseCodexRealtimeV3Event
12
+ } from "./chunk-MFVGCR3N.js";
13
+ export {
14
+ CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES,
15
+ CODEX_REALTIME_INITIAL_ITEMS_MAX_COUNT,
16
+ CODEX_REALTIME_INITIAL_ITEMS_MAX_TOKENS,
17
+ CODEX_REALTIME_V3_MAX_EVENT_BYTES,
18
+ CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES,
19
+ CODEX_REALTIME_V3_MAX_TEXT_BYTES,
20
+ contextAppendChunks,
21
+ encodeCodexRealtimeV3DelegationContextAppend,
22
+ encodeCodexRealtimeV3SessionContextAppend,
23
+ parseCodexRealtimeV3Event
24
+ };
25
+ //# sourceMappingURL=realtime-v3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,48 @@
1
+ import { type CodexAuthHeaders } from "./api-client";
2
+ import { CODEX_REALTIME_MODEL, CODEX_REALTIME_VERSION } from "./constants";
3
+ import type { CodexFetch } from "./device-code";
4
+ import { type CodexRealtimeInitialItem } from "./realtime-v3";
5
+ export declare const CODEX_REALTIME_VOICES: readonly ["juniper", "maple", "spruce", "ember", "vale", "breeze", "arbor", "sol", "cove"];
6
+ export type CodexRealtimeVoice = (typeof CODEX_REALTIME_VOICES)[number];
7
+ export type CodexRealtimeCallInput = {
8
+ /** Browser-created WebRTC offer. It must negotiate an audio media section. */
9
+ sdp: string;
10
+ /** This transport intentionally supports only Codex's Frameless/V3 protocol. */
11
+ version: typeof CODEX_REALTIME_VERSION;
12
+ /** Server-owned session/thread binding; sent upstream but never returned. */
13
+ sessionId: string;
14
+ /** Server-projected ordinary-session history for Frameless V3 bootstrap. */
15
+ initialItems?: CodexRealtimeInitialItem[] | undefined;
16
+ instructions?: string | undefined;
17
+ voice?: CodexRealtimeVoice | undefined;
18
+ };
19
+ export type CodexRealtimeCallResult = {
20
+ sdp: string;
21
+ version: typeof CODEX_REALTIME_VERSION;
22
+ model: typeof CODEX_REALTIME_MODEL;
23
+ };
24
+ export type CodexRealtimeErrorCode = "invalid_request" | "incompatible" | "authentication" | "entitlement" | "rate_limited" | "provider" | "invalid_response" | "network" | "timeout" | "cancelled";
25
+ /** Safe provider failure: it contains no response body, credential, or account identity. */
26
+ export declare class CodexRealtimeError extends Error {
27
+ readonly code: CodexRealtimeErrorCode;
28
+ readonly providerStatus: number | null;
29
+ constructor(code: CodexRealtimeErrorCode, message: string, providerStatus?: number | null);
30
+ }
31
+ export type CodexRealtimeCallOptions = {
32
+ signal?: AbortSignal | undefined;
33
+ timeoutMs?: number | undefined;
34
+ };
35
+ /**
36
+ * Create one native subscription-authenticated Codex GPT-Live V3 WebRTC call.
37
+ *
38
+ * There is deliberately no API-key or WebSocket fallback and no transport
39
+ * retry. A caller may refresh the same connected subscription after one 401,
40
+ * but this adapter always performs exactly one provider request.
41
+ */
42
+ export declare function createCodexRealtimeCall(auth: CodexAuthHeaders, input: CodexRealtimeCallInput, fetchImpl?: CodexFetch, options?: CodexRealtimeCallOptions): Promise<CodexRealtimeCallResult>;
43
+ /** Shared pin→active selection for worker turns and direct realtime calls. */
44
+ export declare function selectCodexCredentialId(args: {
45
+ sessionPinnedCredentialId: string | null;
46
+ activeCredentialId: string | null;
47
+ connectedIds: ReadonlySet<string>;
48
+ }): string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/codex",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "ChatGPT/Codex subscription auth + transport: device-code login, token refresh, and the Responses-backend fetch. Pure HTTP + transforms; no database dependency.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -25,6 +25,10 @@
25
25
  "./constants": {
26
26
  "types": "./dist/constants.d.ts",
27
27
  "import": "./dist/constants.js"
28
+ },
29
+ "./realtime-v3": {
30
+ "types": "./dist/realtime-v3.d.ts",
31
+ "import": "./dist/realtime-v3.js"
28
32
  }
29
33
  },
30
34
  "publishConfig": {
package/src/api-client.ts CHANGED
@@ -28,7 +28,8 @@ export type ResetCreditFetchFailureReason =
28
28
  | "network_error"
29
29
  | "timeout";
30
30
 
31
- function subscriptionHeaders(a: CodexAuthHeaders): Record<string, string> {
31
+ /** Server-only headers shared by every ChatGPT/Codex subscription transport. */
32
+ export function codexSubscriptionHeaders(a: CodexAuthHeaders): Record<string, string> {
32
33
  return {
33
34
  Authorization: `Bearer ${a.accessToken}`,
34
35
  ...(a.chatgptAccountId ? { "ChatGPT-Account-ID": a.chatgptAccountId } : {}),
@@ -48,7 +49,7 @@ export async function fetchCodexModels(
48
49
  const fetched = await runBoundedCodexOperation(async (signal) => {
49
50
  const res = await fetchImpl(
50
51
  `${CODEX_RESPONSES_BASE}/models?client_version=${encodeURIComponent(a.clientVersion)}`,
51
- { method: "GET", headers: subscriptionHeaders(a), signal },
52
+ { method: "GET", headers: codexSubscriptionHeaders(a), signal },
52
53
  );
53
54
  if (!res.ok) {
54
55
  await res.arrayBuffer().catch(() => undefined);
@@ -72,7 +73,7 @@ export async function fetchCodexUsage(
72
73
  const fetched = await runBoundedCodexOperation(async (signal) => {
73
74
  const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/usage`, {
74
75
  method: "GET",
75
- headers: subscriptionHeaders(a),
76
+ headers: codexSubscriptionHeaders(a),
76
77
  signal,
77
78
  });
78
79
  // A 404 may carry a usage-limit body; the route layer normalizes it to a limits state (spec §1.8c).
@@ -102,19 +103,27 @@ export async function fetchCodexRateLimitResetCredits(
102
103
  const fetched = await runBoundedCodexOperation(async (signal) => {
103
104
  const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/rate-limit-reset-credits`, {
104
105
  method: "GET",
105
- headers: subscriptionHeaders(a),
106
+ headers: codexSubscriptionHeaders(a),
106
107
  signal,
107
108
  });
108
109
  if (!res.ok) {
109
110
  // Drain the body without retaining/logging it. Provider error bodies may
110
111
  // contain account-specific details and are not part of this contract.
111
112
  await res.arrayBuffer().catch(() => undefined);
112
- return { ok: false as const, status: res.status, reason: "http_error" as const };
113
+ return {
114
+ ok: false as const,
115
+ status: res.status,
116
+ reason: "http_error" as const,
117
+ };
113
118
  }
114
119
  const details = parseCodexRateLimitResetCreditsDetails(await res.json().catch(() => null));
115
120
  return details
116
121
  ? { ok: true as const, status: res.status, details }
117
- : { ok: false as const, status: res.status, reason: "invalid_response" as const };
122
+ : {
123
+ ok: false as const,
124
+ status: res.status,
125
+ reason: "invalid_response" as const,
126
+ };
118
127
  }, timeoutMs);
119
128
  return fetched.ok ? fetched.value : { ok: false, status: 0, reason: fetched.reason };
120
129
  }
@@ -146,7 +155,7 @@ export async function consumeCodexRateLimitResetCredit(
146
155
  const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/rate-limit-reset-credits/consume`, {
147
156
  method: "POST",
148
157
  headers: {
149
- ...subscriptionHeaders(a),
158
+ ...codexSubscriptionHeaders(a),
150
159
  "content-type": "application/json",
151
160
  },
152
161
  body: JSON.stringify({
@@ -157,12 +166,20 @@ export async function consumeCodexRateLimitResetCredit(
157
166
  });
158
167
  if (!res.ok) {
159
168
  await res.arrayBuffer().catch(() => undefined);
160
- return { ok: false as const, status: res.status, reason: "http_error" as const };
169
+ return {
170
+ ok: false as const,
171
+ status: res.status,
172
+ reason: "http_error" as const,
173
+ };
161
174
  }
162
175
  const result = parseCodexRateLimitResetConsumeResponse(await res.json().catch(() => null));
163
176
  return result
164
177
  ? { ok: true as const, status: res.status, result }
165
- : { ok: false as const, status: res.status, reason: "invalid_response" as const };
178
+ : {
179
+ ok: false as const,
180
+ status: res.status,
181
+ reason: "invalid_response" as const,
182
+ };
166
183
  }, timeoutMs);
167
184
  return fetched.ok ? fetched.value : { ok: false, status: 0, reason: fetched.reason };
168
185
  }
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/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";
@@ -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
+ }