ag-common 0.0.908 → 0.0.910

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 CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  > Various reusable TypeScript libraries for common, API (AWS Cognito and AWS CDK), and React UI use cases.
4
4
 
5
+ The provider-neutral AI helpers are exported from `ag-common` and support text,
6
+ multimodal input, typed media output, and optional direct Codex endpoints.
7
+
8
+ ```ts
9
+ import { createAIClient } from "ag-common";
10
+
11
+ const ai = createAIClient({
12
+ endpoint: {
13
+ protocol: "codex",
14
+ url: "http://192.168.0.205:4501/v1",
15
+ token: process.env.CODEX_TOKEN!,
16
+ },
17
+ });
18
+
19
+ const result = await ai.generate({
20
+ input: [{ role: "user", content: [{ type: "text", text: "Describe this image." }] }],
21
+ output: ["text"],
22
+ model: "local-model",
23
+ });
24
+
25
+ const models = await ai.models();
26
+ const imageModels = models.filter((model) => model.outputModalities?.includes("image"));
27
+ ```
28
+
29
+ Codex URLs must be direct private LAN IPv4 addresses. The client sends the
30
+ version-one job protocol under `/v1`, polls bounded job status, and keeps one
31
+ idempotency key for retries. Omitting `endpoint` uses the configured Google AI
32
+ adapter and its API keys. `models()` returns provider-neutral metadata with the
33
+ model id and any input/output modalities, reasoning efforts, preferences, web
34
+ search, default, and readiness fields the provider exposes. Capability fields
35
+ may be omitted when a provider does not publish them.
36
+
5
37
  [![NPM Version][npm-image]][npm-url]
6
38
 
7
39
  ## Install
@@ -0,0 +1,12 @@
1
+ import type { AIClientDependencies, AIEndpoint, AIModel, AIRequest, GenerationResult } from "../types";
2
+ export type CodexAdapterConfig = {
3
+ endpoint: AIEndpoint;
4
+ dependencies?: AIClientDependencies;
5
+ timeoutMs?: number;
6
+ pollIntervalMs?: number;
7
+ maxPollIntervalMs?: number;
8
+ };
9
+ /** Validate and normalize the direct private LAN endpoint used by Codex. */
10
+ export declare const normalizeCodexEndpoint: (endpoint: AIEndpoint) => AIEndpoint;
11
+ export declare const generateCodex: (request: AIRequest, config: CodexAdapterConfig) => Promise<GenerationResult>;
12
+ export declare const listCodexModels: (endpoint: AIEndpoint, dependencies?: AIClientDependencies) => Promise<AIModel[]>;
@@ -0,0 +1,573 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listCodexModels = exports.generateCodex = exports.normalizeCodexEndpoint = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
6
+ const DEFAULT_POLL_INTERVAL_MS = 250;
7
+ const DEFAULT_MAX_POLL_INTERVAL_MS = 2000;
8
+ const CREATE_RETRY_DELAY_MS = 1000;
9
+ const CANCEL_TIMEOUT_MS = 1000;
10
+ const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
11
+ const TERMINAL_SUCCESS_STATUSES = new Set(["completed"]);
12
+ const TERMINAL_FAILURE_STATUSES = new Set(["failed", "cancelled", "unknown"]);
13
+ const PENDING_STATUSES = new Set(["queued", "running", "cancelling"]);
14
+ class CodexHTTPError extends Error {
15
+ status;
16
+ constructor(status, message) {
17
+ super(message);
18
+ this.name = "CodexHTTPError";
19
+ this.status = status;
20
+ }
21
+ }
22
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23
+ const getString = (record, key) => {
24
+ const value = record[key];
25
+ return typeof value === "string" ? value : undefined;
26
+ };
27
+ const getRecord = (record, key) => {
28
+ const value = record[key];
29
+ return isRecord(value) ? value : undefined;
30
+ };
31
+ const getErrorMessage = (body, fallback) => {
32
+ if (!isRecord(body))
33
+ return fallback;
34
+ const error = body.error;
35
+ if (typeof error === "string" && error.trim().length > 0)
36
+ return error;
37
+ if (isRecord(error)) {
38
+ const message = getString(error, "message");
39
+ if (message && message.trim().length > 0)
40
+ return message;
41
+ }
42
+ const message = getString(body, "message");
43
+ return message && message.trim().length > 0 ? message : fallback;
44
+ };
45
+ const parseIPv4 = (hostname) => {
46
+ const octets = hostname.split(".");
47
+ if (octets.length !== 4 || octets.some((octet) => !/^\d{1,3}$/.test(octet)))
48
+ return undefined;
49
+ const numbers = octets.map((octet) => Number(octet));
50
+ return numbers.every((octet) => octet >= 0 && octet <= 255) ? numbers : undefined;
51
+ };
52
+ const isPrivateIPv4 = (hostname) => {
53
+ const octets = parseIPv4(hostname);
54
+ if (!octets)
55
+ return false;
56
+ const [first, second] = octets;
57
+ return (first === 10 ||
58
+ (first === 172 && second >= 16 && second <= 31) ||
59
+ (first === 192 && second === 168));
60
+ };
61
+ /** Validate and normalize the direct private LAN endpoint used by Codex. */
62
+ const normalizeCodexEndpoint = (endpoint) => {
63
+ const candidate = endpoint;
64
+ if (!isRecord(candidate) || candidate.protocol !== "codex") {
65
+ throw new Error("Unsupported AI endpoint protocol");
66
+ }
67
+ const rawURL = candidate.url;
68
+ const rawToken = candidate.token;
69
+ if (typeof rawURL !== "string" || rawURL.trim().length === 0) {
70
+ throw new Error("Codex endpoint URL is required");
71
+ }
72
+ if (typeof rawToken !== "string" || rawToken.trim().length === 0) {
73
+ throw new Error("Codex endpoint token is required");
74
+ }
75
+ let parsed;
76
+ try {
77
+ parsed = new URL(rawURL);
78
+ }
79
+ catch {
80
+ throw new Error("Codex endpoint URL is invalid");
81
+ }
82
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
83
+ throw new Error("Codex endpoint URL must use HTTP or HTTPS");
84
+ }
85
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
86
+ throw new Error("Codex endpoint URL must not contain credentials, query, or fragment");
87
+ }
88
+ if (parsed.hostname.toLowerCase() === "gec.dev" ||
89
+ parsed.hostname.toLowerCase().endsWith(".gec.dev")) {
90
+ throw new Error("Codex endpoint must connect directly to the private LAN address");
91
+ }
92
+ if (!isPrivateIPv4(parsed.hostname)) {
93
+ throw new Error("Codex endpoint must use a private LAN IPv4 address");
94
+ }
95
+ const path = parsed.pathname.replace(/\/+$/, "");
96
+ if (path !== "" && path !== "/v1") {
97
+ throw new Error("Codex endpoint URL path must be empty or /v1");
98
+ }
99
+ parsed.pathname = "/v1";
100
+ parsed.search = "";
101
+ parsed.hash = "";
102
+ return { protocol: "codex", url: parsed.toString().replace(/\/$/, ""), token: rawToken.trim() };
103
+ };
104
+ exports.normalizeCodexEndpoint = normalizeCodexEndpoint;
105
+ const defaultSleep = (milliseconds) => new Promise((resolve) => {
106
+ setTimeout(resolve, milliseconds);
107
+ });
108
+ const getDependencies = (dependencies) => ({
109
+ fetch: dependencies?.fetch ?? globalThis.fetch,
110
+ sleep: dependencies?.sleep ?? defaultSleep,
111
+ now: dependencies?.now ?? (() => Date.now()),
112
+ createIdempotencyKey: dependencies?.createIdempotencyKey ?? node_crypto_1.randomUUID,
113
+ });
114
+ const throwIfAborted = (signal) => {
115
+ if (signal?.aborted) {
116
+ throw new Error("Codex generation aborted");
117
+ }
118
+ };
119
+ const createAbortError = () => {
120
+ const error = new Error("Codex request aborted");
121
+ error.name = "AbortError";
122
+ return error;
123
+ };
124
+ const awaitWithSignal = async (promise, signal) => {
125
+ if (signal === undefined)
126
+ return promise;
127
+ if (signal.aborted)
128
+ throw createAbortError();
129
+ const abort = new Promise((_, reject) => {
130
+ const onAbort = () => reject(createAbortError());
131
+ signal.addEventListener("abort", onAbort, { once: true });
132
+ void promise.then(() => signal.removeEventListener("abort", onAbort), () => signal.removeEventListener("abort", onAbort));
133
+ });
134
+ return Promise.race([promise, abort]);
135
+ };
136
+ const responseJSON = async (response, signal) => {
137
+ const contentLength = response.headers.get("content-length");
138
+ if (contentLength !== null && Number(contentLength) > MAX_RESPONSE_BYTES) {
139
+ throw new Error("Codex response body is too large");
140
+ }
141
+ const body = response.body;
142
+ if (body === null)
143
+ return undefined;
144
+ const reader = body.getReader();
145
+ const chunks = [];
146
+ let total = 0;
147
+ try {
148
+ for (;;) {
149
+ // oxlint-disable-next-line no-await-in-loop -- response bytes must be bounded sequentially
150
+ const chunk = await awaitWithSignal(reader.read(), signal);
151
+ if (chunk.done)
152
+ break;
153
+ total += chunk.value.byteLength;
154
+ if (total > MAX_RESPONSE_BYTES)
155
+ throw new Error("Codex response body is too large");
156
+ chunks.push(chunk.value);
157
+ }
158
+ }
159
+ finally {
160
+ reader.releaseLock();
161
+ }
162
+ const bytes = new Uint8Array(total);
163
+ let offset = 0;
164
+ for (const chunk of chunks) {
165
+ bytes.set(chunk, offset);
166
+ offset += chunk.byteLength;
167
+ }
168
+ try {
169
+ return JSON.parse(new TextDecoder().decode(bytes));
170
+ }
171
+ catch {
172
+ return undefined;
173
+ }
174
+ };
175
+ const fetchJSON = async ({ fetch, url, headers, method, body, signal, }) => {
176
+ const response = await awaitWithSignal(fetch(url, {
177
+ method,
178
+ headers,
179
+ ...(body === undefined ? {} : { body }),
180
+ redirect: "manual",
181
+ ...(signal === undefined ? {} : { signal }),
182
+ }), signal);
183
+ const payload = await responseJSON(response, signal);
184
+ if (!response.ok) {
185
+ throw new CodexHTTPError(response.status, getErrorMessage(payload, `Codex endpoint returned HTTP ${response.status}`));
186
+ }
187
+ return payload;
188
+ };
189
+ const isRetryable = (error) => {
190
+ if (error instanceof CodexHTTPError)
191
+ return error.status === 429 || error.status >= 500;
192
+ return error instanceof Error && error.name !== "AbortError";
193
+ };
194
+ const withCreateRetry = async (operation, sleep, signal) => {
195
+ try {
196
+ const result = await operation();
197
+ return result;
198
+ }
199
+ catch (error) {
200
+ if (!isRetryable(error))
201
+ throw error;
202
+ // oxlint-disable-next-line no-await-in-loop -- retry delay precedes the one allowed replay
203
+ await awaitWithSignal(sleep(CREATE_RETRY_DELAY_MS), signal);
204
+ throwIfAborted(signal);
205
+ return operation();
206
+ }
207
+ };
208
+ const isBase64 = (value) => value.length > 0 &&
209
+ value.length % 4 === 0 &&
210
+ /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
211
+ const parseOutputPart = (value) => {
212
+ if (!isRecord(value))
213
+ throw new Error("Codex result contains an invalid output part");
214
+ const type = getString(value, "type");
215
+ if (type === "text") {
216
+ const text = getString(value, "text");
217
+ if (text === undefined)
218
+ throw new Error("Codex text output is missing text");
219
+ return { type, text };
220
+ }
221
+ if (type !== "image" && type !== "audio" && type !== "video" && type !== "file") {
222
+ throw new Error("Codex result contains an unsupported output part");
223
+ }
224
+ const mimeType = getString(value, "mimeType");
225
+ const data = getString(value, "data");
226
+ if (!mimeType || data === undefined || !isBase64(data)) {
227
+ throw new Error("Codex media output is missing MIME type or data");
228
+ }
229
+ if (type !== "file" && !mimeType.toLowerCase().startsWith(`${type}/`)) {
230
+ throw new Error("Codex media output type does not match its MIME type");
231
+ }
232
+ const name = getString(value, "name");
233
+ return name === undefined ? { type, mimeType, data } : { type, mimeType, data, name };
234
+ };
235
+ const parseJob = (value) => {
236
+ if (!isRecord(value))
237
+ throw new Error("Codex endpoint returned an invalid job");
238
+ const id = getString(value, "id");
239
+ const status = getString(value, "status");
240
+ if (!id || !status)
241
+ throw new Error("Codex job is missing id or status");
242
+ return value;
243
+ };
244
+ const parseResult = (job) => {
245
+ const result = getRecord(job, "result");
246
+ if (!result)
247
+ throw new Error("Codex successful job is missing its result");
248
+ const rawOutput = result.output;
249
+ if (!Array.isArray(rawOutput))
250
+ throw new Error("Codex result is missing typed output parts");
251
+ const model = getString(result, "model");
252
+ if (!model || model.trim().length === 0)
253
+ throw new Error("Codex result is missing its actual model");
254
+ const generatedAtValue = result.generatedAt;
255
+ if (typeof generatedAtValue !== "number" ||
256
+ !Number.isSafeInteger(generatedAtValue) ||
257
+ generatedAtValue < 0) {
258
+ throw new Error("Codex result is missing a valid generatedAt timestamp");
259
+ }
260
+ const rawUsage = result.usage;
261
+ let usage;
262
+ if (rawUsage !== undefined) {
263
+ if (!isRecord(rawUsage))
264
+ throw new Error("Codex result usage is invalid");
265
+ const inputTokens = rawUsage.inputTokens;
266
+ const outputTokens = rawUsage.outputTokens;
267
+ const totalTokens = rawUsage.totalTokens;
268
+ if (typeof inputTokens !== "number" ||
269
+ !Number.isSafeInteger(inputTokens) ||
270
+ inputTokens < 0 ||
271
+ typeof outputTokens !== "number" ||
272
+ !Number.isSafeInteger(outputTokens) ||
273
+ outputTokens < 0 ||
274
+ typeof totalTokens !== "number" ||
275
+ !Number.isSafeInteger(totalTokens) ||
276
+ totalTokens < 0) {
277
+ throw new Error("Codex result usage is invalid");
278
+ }
279
+ usage = { inputTokens, outputTokens, totalTokens };
280
+ }
281
+ return {
282
+ output: rawOutput.map(parseOutputPart),
283
+ model,
284
+ generatedAt: generatedAtValue,
285
+ ...(usage === undefined ? {} : { usage }),
286
+ };
287
+ };
288
+ const getFailureMessage = (job) => {
289
+ const error = job.error;
290
+ const status = getString(job, "status");
291
+ if (status === "unknown") {
292
+ const detail = getErrorMessage(error, "execution outcome is unknown");
293
+ return `Codex generation outcome is uncertain; do not retry automatically: ${detail}`;
294
+ }
295
+ return getErrorMessage(error, "Codex generation failed");
296
+ };
297
+ const endpointHeaders = (endpoint, idempotencyKey) => ({
298
+ Accept: "application/json",
299
+ "Content-Type": "application/json",
300
+ Authorization: `Bearer ${endpoint.token}`,
301
+ ...(idempotencyKey === undefined ? {} : { "Idempotency-Key": idempotencyKey }),
302
+ });
303
+ const jobURL = (endpoint, id) => `${endpoint.url}/jobs${id === undefined ? "" : `/${encodeURIComponent(id)}`}`;
304
+ const cancelJob = async (endpoint, id, fetch) => {
305
+ const controller = new AbortController();
306
+ const timeout = setTimeout(() => controller.abort(), CANCEL_TIMEOUT_MS);
307
+ try {
308
+ const response = await fetch(`${jobURL(endpoint, id)}/cancel`, {
309
+ method: "POST",
310
+ headers: endpointHeaders(endpoint),
311
+ redirect: "manual",
312
+ signal: controller.signal,
313
+ });
314
+ if (!response.ok) {
315
+ throw new CodexHTTPError(response.status, `Codex cancellation returned HTTP ${response.status}`);
316
+ }
317
+ }
318
+ finally {
319
+ clearTimeout(timeout);
320
+ }
321
+ };
322
+ const abortWithCancellation = async (endpoint, id, fetch, reason) => {
323
+ try {
324
+ await Promise.race([
325
+ cancelJob(endpoint, id, fetch),
326
+ new Promise((resolve) => setTimeout(resolve, CANCEL_TIMEOUT_MS)),
327
+ ]);
328
+ }
329
+ catch {
330
+ // Preserve the original abort or timeout reason when best-effort cancellation fails.
331
+ }
332
+ throw reason;
333
+ };
334
+ const createOperationAbort = (request, timeoutMs) => {
335
+ const controller = new AbortController();
336
+ const timeout = setTimeout(() => {
337
+ controller.abort();
338
+ }, timeoutMs);
339
+ const onAbort = () => controller.abort();
340
+ request.signal?.addEventListener("abort", onAbort, { once: true });
341
+ return {
342
+ signal: controller.signal,
343
+ cleanup: () => {
344
+ clearTimeout(timeout);
345
+ request.signal?.removeEventListener("abort", onAbort);
346
+ },
347
+ };
348
+ };
349
+ const generateCodex = async (request, config) => {
350
+ const endpoint = (0, exports.normalizeCodexEndpoint)(config.endpoint);
351
+ const dependencies = getDependencies(config.dependencies);
352
+ const timeoutMs = request.timeoutMs ?? config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
353
+ const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
354
+ const maxPollIntervalMs = config.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS;
355
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
356
+ throw new Error("Codex timeout must be positive");
357
+ if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
358
+ throw new Error("Codex poll interval must be positive");
359
+ }
360
+ if (!Number.isFinite(maxPollIntervalMs) || maxPollIntervalMs < pollIntervalMs) {
361
+ throw new Error("Codex maximum poll interval is invalid");
362
+ }
363
+ throwIfAborted(request.signal);
364
+ const operationAbort = createOperationAbort(request, timeoutMs);
365
+ let jobId;
366
+ const deadline = dependencies.now() + timeoutMs;
367
+ const abortError = () => request.signal?.aborted
368
+ ? new Error("Codex generation aborted")
369
+ : new Error("Codex generation timed out");
370
+ try {
371
+ const idempotencyKey = dependencies.createIdempotencyKey();
372
+ if (idempotencyKey.trim().length === 0)
373
+ throw new Error("Codex idempotency key is empty");
374
+ const body = {
375
+ version: 1,
376
+ input: request.input,
377
+ output: request.output ?? ["text"],
378
+ ...(request.instructions === undefined ? {} : { instructions: request.instructions }),
379
+ ...(request.model === undefined ? {} : { model: request.model }),
380
+ ...(request.model === undefined && request.prefer !== undefined
381
+ ? { prefer: request.prefer }
382
+ : {}),
383
+ ...(request.reasoningEffort === undefined
384
+ ? {}
385
+ : { reasoningEffort: request.reasoningEffort }),
386
+ ...(request.maxOutputTokens === undefined
387
+ ? {}
388
+ : { maxOutputTokens: request.maxOutputTokens }),
389
+ ...(request.outputSchema === undefined ? {} : { outputSchema: request.outputSchema }),
390
+ ...(request.webSearch === undefined ? {} : { webSearch: request.webSearch }),
391
+ };
392
+ const created = parseJob(await withCreateRetry(() => fetchJSON({
393
+ fetch: dependencies.fetch,
394
+ url: jobURL(endpoint),
395
+ headers: endpointHeaders(endpoint, idempotencyKey),
396
+ method: "POST",
397
+ body: JSON.stringify(body),
398
+ signal: operationAbort.signal,
399
+ }), dependencies.sleep, operationAbort.signal));
400
+ jobId = getString(created, "id");
401
+ if (!jobId)
402
+ throw new Error("Codex job is missing its id");
403
+ const status = getString(created, "status");
404
+ if (!status)
405
+ throw new Error("Codex job is missing its status");
406
+ if (TERMINAL_FAILURE_STATUSES.has(status))
407
+ throw new Error(getFailureMessage(created));
408
+ if (!PENDING_STATUSES.has(status) && !TERMINAL_SUCCESS_STATUSES.has(status)) {
409
+ throw new Error(`Unknown Codex job status: ${status}`);
410
+ }
411
+ let delay = TERMINAL_SUCCESS_STATUSES.has(status) ? 0 : pollIntervalMs;
412
+ const maxPollAttempts = Math.max(1, Math.ceil(timeoutMs / pollIntervalMs)) + 2;
413
+ let pollAttempts = 0;
414
+ while (pollAttempts < maxPollAttempts) {
415
+ if (dependencies.now() >= deadline) {
416
+ return abortWithCancellation(endpoint, jobId, dependencies.fetch, new Error("Codex generation timed out"));
417
+ }
418
+ if (delay > 0) {
419
+ // oxlint-disable-next-line no-await-in-loop -- polling must remain sequential
420
+ await awaitWithSignal(dependencies.sleep(Math.min(delay, Math.max(0, deadline - dependencies.now()))), operationAbort.signal);
421
+ }
422
+ if (operationAbort.signal.aborted) {
423
+ return abortWithCancellation(endpoint, jobId, dependencies.fetch, abortError());
424
+ }
425
+ if (dependencies.now() >= deadline) {
426
+ return abortWithCancellation(endpoint, jobId, dependencies.fetch, new Error("Codex generation timed out"));
427
+ }
428
+ let current;
429
+ pollAttempts += 1;
430
+ try {
431
+ current = parseJob(
432
+ // oxlint-disable-next-line no-await-in-loop -- each poll depends on the prior status
433
+ await fetchJSON({
434
+ fetch: dependencies.fetch,
435
+ url: jobURL(endpoint, jobId),
436
+ headers: endpointHeaders(endpoint),
437
+ method: "GET",
438
+ signal: operationAbort.signal,
439
+ }));
440
+ }
441
+ catch (error) {
442
+ if (!isRetryable(error))
443
+ throw error;
444
+ const retryDelay = Math.max(pollIntervalMs, delay);
445
+ // oxlint-disable-next-line no-await-in-loop -- backoff precedes the next status check
446
+ await awaitWithSignal(dependencies.sleep(retryDelay), operationAbort.signal);
447
+ delay = Math.min(maxPollIntervalMs, retryDelay * 2);
448
+ continue;
449
+ }
450
+ const currentStatus = getString(current, "status");
451
+ if (!currentStatus)
452
+ throw new Error("Codex job is missing its status");
453
+ if (TERMINAL_SUCCESS_STATUSES.has(currentStatus)) {
454
+ return parseResult(current);
455
+ }
456
+ if (TERMINAL_FAILURE_STATUSES.has(currentStatus))
457
+ throw new Error(getFailureMessage(current));
458
+ if (!PENDING_STATUSES.has(currentStatus)) {
459
+ throw new Error(`Unknown Codex job status: ${currentStatus}`);
460
+ }
461
+ delay = Math.min(maxPollIntervalMs, Math.max(pollIntervalMs, delay * 2));
462
+ }
463
+ return abortWithCancellation(endpoint, jobId, dependencies.fetch, new Error("Codex generation timed out"));
464
+ }
465
+ catch (error) {
466
+ if (operationAbort.signal.aborted && jobId !== undefined) {
467
+ return abortWithCancellation(endpoint, jobId, dependencies.fetch, abortError());
468
+ }
469
+ if (operationAbort.signal.aborted)
470
+ throw abortError();
471
+ throw error;
472
+ }
473
+ finally {
474
+ operationAbort.cleanup();
475
+ }
476
+ };
477
+ exports.generateCodex = generateCodex;
478
+ const MODEL_MODALITIES = new Set(["text", "image", "audio", "video", "file"]);
479
+ const getModelId = (record) => {
480
+ for (const key of ["id", "name", "model", "modelId"]) {
481
+ const value = getString(record, key)?.trim();
482
+ if (value !== undefined && value.length > 0)
483
+ return value;
484
+ }
485
+ return undefined;
486
+ };
487
+ const parseStringList = (value) => {
488
+ if (!Array.isArray(value))
489
+ return undefined;
490
+ return [
491
+ ...new Set(value.flatMap((item) => {
492
+ if (typeof item === "string") {
493
+ const normalized = item.trim();
494
+ return normalized.length > 0 ? [normalized] : [];
495
+ }
496
+ if (isRecord(item)) {
497
+ const normalized = getString(item, "reasoningEffort")?.trim();
498
+ return normalized === undefined || normalized.length === 0 ? [] : [normalized];
499
+ }
500
+ return [];
501
+ })),
502
+ ];
503
+ };
504
+ const parseModalities = (value) => {
505
+ if (!Array.isArray(value))
506
+ return undefined;
507
+ return [
508
+ ...new Set(value.flatMap((item) => {
509
+ if (typeof item !== "string")
510
+ return [];
511
+ const normalized = item.trim();
512
+ return MODEL_MODALITIES.has(normalized) ? [normalized] : [];
513
+ })),
514
+ ];
515
+ };
516
+ const parsePreferences = (value) => {
517
+ if (!Array.isArray(value))
518
+ return undefined;
519
+ return [
520
+ ...new Set(value.filter((item) => item === "fast" || item === "quality")),
521
+ ];
522
+ };
523
+ const parseModel = (value) => {
524
+ if (typeof value === "string") {
525
+ const id = value.trim();
526
+ return id.length === 0 ? undefined : { id };
527
+ }
528
+ if (!isRecord(value))
529
+ return undefined;
530
+ const id = getModelId(value);
531
+ if (id === undefined)
532
+ return undefined;
533
+ const capabilities = getRecord(value, "capabilities") ?? value;
534
+ const model = { id };
535
+ const inputModalities = parseModalities(capabilities.inputModalities);
536
+ const outputModalities = parseModalities(capabilities.outputModalities);
537
+ const supportedReasoningEfforts = parseStringList(capabilities.supportedReasoningEfforts ?? capabilities.reasoningEfforts);
538
+ const defaultReasoningEffort = getString(capabilities, "defaultReasoningEffort")?.trim();
539
+ const prefer = parsePreferences(capabilities.prefer ?? capabilities.preferences);
540
+ if (inputModalities !== undefined)
541
+ model.inputModalities = inputModalities;
542
+ if (outputModalities !== undefined)
543
+ model.outputModalities = outputModalities;
544
+ if (supportedReasoningEfforts !== undefined) {
545
+ model.supportedReasoningEfforts = supportedReasoningEfforts;
546
+ }
547
+ if (defaultReasoningEffort !== undefined && defaultReasoningEffort.length > 0) {
548
+ model.defaultReasoningEffort = defaultReasoningEffort;
549
+ }
550
+ if (prefer !== undefined)
551
+ model.prefer = prefer;
552
+ for (const key of ["webSearch", "isDefault", "ready"]) {
553
+ const flag = capabilities[key];
554
+ if (typeof flag === "boolean")
555
+ model[key] = flag;
556
+ }
557
+ return model;
558
+ };
559
+ const listCodexModels = async (endpoint, dependencies) => {
560
+ const normalizedEndpoint = (0, exports.normalizeCodexEndpoint)(endpoint);
561
+ const { fetch } = getDependencies(dependencies);
562
+ const payload = await fetchJSON({
563
+ fetch,
564
+ url: `${normalizedEndpoint.url}/models`,
565
+ headers: endpointHeaders(normalizedEndpoint),
566
+ method: "GET",
567
+ });
568
+ const models = isRecord(payload) ? payload.models : payload;
569
+ if (!Array.isArray(models))
570
+ throw new Error("Codex models response is invalid");
571
+ return models.map(parseModel).filter((model) => model !== undefined);
572
+ };
573
+ exports.listCodexModels = listCodexModels;
@@ -0,0 +1,12 @@
1
+ import { GoogleGenAI } from "@google/genai";
2
+ import { resetModelBackoff } from "../quota";
3
+ import type { AIClientDependencies, AIModel, AIRequest, GenerationResult } from "../types";
4
+ export type GoogleAdapterConfig = {
5
+ dependencies?: AIClientDependencies;
6
+ createAI?: (apiKey: string) => GoogleGenAI;
7
+ };
8
+ export declare const generateGoogle: (request: AIRequest, config?: GoogleAdapterConfig) => Promise<GenerationResult>;
9
+ export declare const listGoogleModels: (dependencies?: AIClientDependencies) => Promise<AIModel[]>;
10
+ export { resetModelBackoff };
11
+ /** Test seam: reset adapter clients, model cache, and per-model rate-limit backoff. */
12
+ export declare const resetGoogleAdapterForTests: () => void;