@opengeni/sdk 0.36.1 → 0.37.0
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/README.md +6 -1
- package/dist/chunk-DXDL7EEW.js +573 -0
- package/dist/chunk-DXDL7EEW.js.map +1 -0
- package/dist/chunk-OB5PGV6N.js +1988 -0
- package/dist/chunk-OB5PGV6N.js.map +1 -0
- package/dist/chunk-OINSI33U.js +97 -0
- package/dist/chunk-OINSI33U.js.map +1 -0
- package/dist/{chunk-B7E4FPNZ.js → chunk-VOQ235WN.js} +136 -106
- package/dist/chunk-VOQ235WN.js.map +1 -0
- package/dist/client.d.ts +35 -2
- package/dist/codex-realtime-controller.d.ts +109 -0
- package/dist/codex-realtime-controller.js +12 -0
- package/dist/codex-realtime-controller.js.map +1 -0
- package/dist/codex-realtime-lifecycle.d.ts +18 -0
- package/dist/codex-realtime-v3-wire.d.ts +86 -0
- package/dist/codex-realtime-v3.d.ts +52 -0
- package/dist/codex-realtime.d.ts +68 -0
- package/dist/core.js +6 -4
- package/dist/gateway-realtime-transport.d.ts +2 -0
- package/dist/gateway-realtime-transport.js +7 -0
- package/dist/gateway-realtime-transport.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +38 -6
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +193 -3
- package/package.json +10 -1
- package/src/client.ts +211 -17
- package/src/codex-realtime-controller.ts +1427 -0
- package/src/codex-realtime-lifecycle.ts +97 -0
- package/src/codex-realtime-v3-wire.ts +349 -0
- package/src/codex-realtime-v3.ts +575 -0
- package/src/codex-realtime.ts +411 -0
- package/src/gateway-realtime-transport.ts +650 -0
- package/src/index.ts +71 -0
- package/src/types.ts +238 -3
- package/dist/chunk-B7E4FPNZ.js.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SessionEvent, SessionRealtimeModel } from "./types";
|
|
2
|
+
|
|
3
|
+
export type SessionRealtimeLifecycleProjection =
|
|
4
|
+
| {
|
|
5
|
+
state: "active";
|
|
6
|
+
realtimeId: string;
|
|
7
|
+
operationId: string;
|
|
8
|
+
version: number;
|
|
9
|
+
connectionEpoch: number;
|
|
10
|
+
leaseExpiresAt: string;
|
|
11
|
+
model: SessionRealtimeModel;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
state: "ended";
|
|
15
|
+
realtimeId: string;
|
|
16
|
+
operationId: string;
|
|
17
|
+
version: number;
|
|
18
|
+
connectionEpoch: number;
|
|
19
|
+
reason: "user_stop" | "browser_unload" | "lease_expired";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function projectSessionRealtimeLifecycle(
|
|
23
|
+
events: ReadonlyArray<Pick<SessionEvent, "sequence" | "type" | "payload">>,
|
|
24
|
+
): SessionRealtimeLifecycleProjection | null {
|
|
25
|
+
let projected: SessionRealtimeLifecycleProjection | null = null;
|
|
26
|
+
for (const event of [...events].sort((left, right) => left.sequence - right.sequence)) {
|
|
27
|
+
const payload = recordValue(event.payload);
|
|
28
|
+
if (event.type === "session.realtime.started") {
|
|
29
|
+
const realtimeId = stringValue(payload?.realtimeId);
|
|
30
|
+
const operationId = stringValue(payload?.operationId);
|
|
31
|
+
const version = positiveInteger(payload?.version);
|
|
32
|
+
const connectionEpoch = positiveInteger(payload?.connectionEpoch);
|
|
33
|
+
const leaseExpiresAt = stringValue(payload?.leaseExpiresAt);
|
|
34
|
+
const model = realtimeModel(payload?.model);
|
|
35
|
+
if (realtimeId && operationId && version && connectionEpoch && leaseExpiresAt && model) {
|
|
36
|
+
projected = {
|
|
37
|
+
state: "active",
|
|
38
|
+
realtimeId,
|
|
39
|
+
operationId,
|
|
40
|
+
version,
|
|
41
|
+
connectionEpoch,
|
|
42
|
+
leaseExpiresAt,
|
|
43
|
+
model,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
} else if (event.type === "session.realtime.ended") {
|
|
47
|
+
const realtimeId = stringValue(payload?.realtimeId);
|
|
48
|
+
const operationId = stringValue(payload?.operationId);
|
|
49
|
+
const version = positiveInteger(payload?.version);
|
|
50
|
+
const connectionEpoch = positiveInteger(payload?.connectionEpoch);
|
|
51
|
+
const reason = endReason(payload?.reason);
|
|
52
|
+
if (realtimeId && operationId && version && connectionEpoch && reason) {
|
|
53
|
+
projected = {
|
|
54
|
+
state: "ended",
|
|
55
|
+
realtimeId,
|
|
56
|
+
operationId,
|
|
57
|
+
version,
|
|
58
|
+
connectionEpoch,
|
|
59
|
+
reason,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return projected;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function realtimeModel(value: unknown): SessionRealtimeModel | null {
|
|
68
|
+
return value === "gpt-live-1-boulder-alpha" ||
|
|
69
|
+
value === "opengeni-gateway/openai/gpt-realtime-2.1" ||
|
|
70
|
+
value === "opengeni-gateway/openai/gpt-realtime-mini" ||
|
|
71
|
+
value === "opengeni-gateway/xai/grok-voice-think-fast-2.0" ||
|
|
72
|
+
value === "workspace-gateway/openai/gpt-realtime-2.1" ||
|
|
73
|
+
value === "workspace-gateway/openai/gpt-realtime-mini" ||
|
|
74
|
+
value === "workspace-gateway/xai/grok-voice-think-fast-2.0"
|
|
75
|
+
? value
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function recordValue(value: unknown): Record<string, unknown> | null {
|
|
80
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
81
|
+
? (value as Record<string, unknown>)
|
|
82
|
+
: null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stringValue(value: unknown): string | null {
|
|
86
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function positiveInteger(value: unknown): number | null {
|
|
90
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function endReason(value: unknown): "user_stop" | "browser_unload" | "lease_expired" | null {
|
|
94
|
+
return value === "user_stop" || value === "browser_unload" || value === "lease_expired"
|
|
95
|
+
? value
|
|
96
|
+
: null;
|
|
97
|
+
}
|
|
@@ -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
|
+
}
|