ag-ai 0.2.1 → 0.3.1
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 +27 -0
- package/dist/client/adapters/cluster.d.ts +2 -0
- package/dist/client/adapters/cluster.js +39 -13
- package/dist/client/adapters/google.d.ts +9 -0
- package/dist/client/adapters/google.js +71 -14
- package/dist/client/client.d.ts +3 -2
- package/dist/client/client.js +49 -6
- package/dist/client/quota.d.ts +16 -1
- package/dist/client/quota.js +66 -3
- package/dist/client/types.d.ts +21 -0
- package/dist/internal/apikey.d.ts +2 -0
- package/dist/internal/apikey.js +8 -3
- package/dist/k8s/opencode.js +16 -1
- package/package.json +1 -1
- package/runtime/executor/opencode.mjs +25 -2
- package/runtime/protocol.mjs +14 -2
package/README.md
CHANGED
|
@@ -28,6 +28,33 @@ runtime imports only Node builtins.
|
|
|
28
28
|
- Codex is not used. The executor is OpenCode; the image adapter is separate
|
|
29
29
|
because OpenCode has no image-generation output.
|
|
30
30
|
|
|
31
|
+
## Gemini Fallback
|
|
32
|
+
|
|
33
|
+
Gemini is always the primary provider when no `endpoint` is selected. A client
|
|
34
|
+
can opt into one explicit cluster fallback after every eligible Gemini
|
|
35
|
+
model/key combination is quota- or capacity-exhausted, or is still in its
|
|
36
|
+
cooldown window:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const client = createAIClient({
|
|
40
|
+
fallback: {
|
|
41
|
+
endpoint: { protocol: 'ag-ai', url: clusterURL, token: clusterToken },
|
|
42
|
+
model: 'gpt-5.6-luna',
|
|
43
|
+
reasoningEffort: 'xhigh',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The fallback endpoint is never inferred. It is used only after aggregate
|
|
49
|
+
Gemini exhaustion, not for validation, authentication, transport, or other
|
|
50
|
+
nonquota failures. The Gemini model id and reasoning settings are not copied
|
|
51
|
+
to the cluster request; `inputs`, `outputSchema`, and other request fields are
|
|
52
|
+
preserved while the configured fallback model and reasoning effort are used.
|
|
53
|
+
`onFallback` receives an explicit exhaustion event, and `onGenerated` reports
|
|
54
|
+
the actual selected model and provider (`google` or `cluster`). Cluster
|
|
55
|
+
network operations and retry waits for this fallback are individually capped
|
|
56
|
+
at 3000 ms; model generation time is not included in that cap.
|
|
57
|
+
|
|
31
58
|
## Tooling
|
|
32
59
|
|
|
33
60
|
Formatting and linting use `e7-oxfmt` / `e7-oxlint` from
|
|
@@ -5,6 +5,8 @@ export type ClusterAdapterConfig = {
|
|
|
5
5
|
timeoutMs?: number;
|
|
6
6
|
pollIntervalMs?: number;
|
|
7
7
|
maxPollIntervalMs?: number;
|
|
8
|
+
/** Maximum time for each queue/status network operation; generation is not capped by this. */
|
|
9
|
+
networkTimeoutMs?: number;
|
|
8
10
|
};
|
|
9
11
|
/** Validate and normalize the direct private LAN endpoint used by the ag-ai cluster. */
|
|
10
12
|
export declare const normalizeClusterEndpoint: (endpoint: AIEndpoint) => AIEndpoint;
|
|
@@ -168,19 +168,43 @@ const responseJSON = async (response, signal) => {
|
|
|
168
168
|
return undefined;
|
|
169
169
|
}
|
|
170
170
|
};
|
|
171
|
-
const fetchJSON = async ({ fetch, url, headers, method, body, signal, }) => {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
const fetchJSON = async ({ fetch, url, headers, method, body, signal, networkTimeoutMs, }) => {
|
|
172
|
+
const controller = networkTimeoutMs === undefined ? undefined : new AbortController();
|
|
173
|
+
const timeout = controller === undefined ? undefined : setTimeout(() => controller.abort(), networkTimeoutMs);
|
|
174
|
+
const onAbort = controller === undefined ? undefined : () => controller.abort();
|
|
175
|
+
if (controller !== undefined && onAbort !== undefined) {
|
|
176
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
177
|
+
}
|
|
178
|
+
const requestSignal = controller?.signal ?? signal;
|
|
179
|
+
try {
|
|
180
|
+
const response = await awaitWithSignal(fetch(url, {
|
|
181
|
+
method,
|
|
182
|
+
headers,
|
|
183
|
+
...(body === undefined ? {} : { body }),
|
|
184
|
+
redirect: 'manual',
|
|
185
|
+
...(requestSignal === undefined ? {} : { signal: requestSignal }),
|
|
186
|
+
}), requestSignal);
|
|
187
|
+
const payload = await responseJSON(response, requestSignal);
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
throw new ClusterHTTPError(response.status, getErrorMessage(payload, `AI endpoint returned HTTP ${response.status}`));
|
|
190
|
+
}
|
|
191
|
+
return payload;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (signal?.aborted)
|
|
195
|
+
throw createAbortError();
|
|
196
|
+
if (controller?.signal.aborted) {
|
|
197
|
+
throw new Error(`AI endpoint network request timed out after ${networkTimeoutMs}ms`);
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
if (timeout !== undefined)
|
|
203
|
+
clearTimeout(timeout);
|
|
204
|
+
if (controller !== undefined && onAbort !== undefined) {
|
|
205
|
+
signal?.removeEventListener('abort', onAbort);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
184
208
|
};
|
|
185
209
|
const isRetryable = (error) => {
|
|
186
210
|
if (error instanceof ClusterHTTPError)
|
|
@@ -390,6 +414,7 @@ const generateCluster = async (request, config) => {
|
|
|
390
414
|
method: 'POST',
|
|
391
415
|
body: JSON.stringify(body),
|
|
392
416
|
signal: operationAbort.signal,
|
|
417
|
+
networkTimeoutMs: config.networkTimeoutMs,
|
|
393
418
|
}), dependencies.sleep, operationAbort.signal));
|
|
394
419
|
jobId = getString(created, 'id');
|
|
395
420
|
if (!jobId)
|
|
@@ -430,6 +455,7 @@ const generateCluster = async (request, config) => {
|
|
|
430
455
|
headers: endpointHeaders(endpoint),
|
|
431
456
|
method: 'GET',
|
|
432
457
|
signal: operationAbort.signal,
|
|
458
|
+
networkTimeoutMs: config.networkTimeoutMs,
|
|
433
459
|
}));
|
|
434
460
|
}
|
|
435
461
|
catch (error) {
|
|
@@ -5,6 +5,15 @@ export type GoogleAdapterConfig = {
|
|
|
5
5
|
dependencies?: AIClientDependencies;
|
|
6
6
|
createAI?: (apiKey: string) => GoogleGenAI;
|
|
7
7
|
};
|
|
8
|
+
/** Every eligible Gemini combination was exhausted by quota/capacity or cooldown. */
|
|
9
|
+
export declare class GeminiPoolExhaustedError extends Error {
|
|
10
|
+
readonly causes: readonly unknown[];
|
|
11
|
+
readonly models: readonly string[];
|
|
12
|
+
constructor(options?: {
|
|
13
|
+
causes?: readonly unknown[];
|
|
14
|
+
models?: readonly string[];
|
|
15
|
+
});
|
|
16
|
+
}
|
|
8
17
|
export declare const generateGoogle: (request: AIRequest, config?: GoogleAdapterConfig) => Promise<GenerationResult>;
|
|
9
18
|
export declare const listGoogleModels: (dependencies?: AIClientDependencies) => Promise<AIModel[]>;
|
|
10
19
|
export { resetModelBackoff };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resetGoogleAdapterForTests = exports.resetModelBackoff = exports.listGoogleModels = exports.generateGoogle = void 0;
|
|
3
|
+
exports.resetGoogleAdapterForTests = exports.resetModelBackoff = exports.listGoogleModels = exports.generateGoogle = exports.GeminiPoolExhaustedError = void 0;
|
|
4
4
|
const genai_1 = require("@google/genai");
|
|
5
5
|
const apikey_1 = require("../../internal/apikey");
|
|
6
6
|
const log_1 = require("../../internal/log");
|
|
@@ -25,6 +25,18 @@ const NON_TEXT_MODEL_CAPABILITIES = [
|
|
|
25
25
|
'robotics',
|
|
26
26
|
'tts',
|
|
27
27
|
];
|
|
28
|
+
/** Every eligible Gemini combination was exhausted by quota/capacity or cooldown. */
|
|
29
|
+
class GeminiPoolExhaustedError extends Error {
|
|
30
|
+
causes;
|
|
31
|
+
models;
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
super('Gemini model pool exhausted; all eligible combinations are unavailable');
|
|
34
|
+
this.name = 'GeminiPoolExhaustedError';
|
|
35
|
+
this.causes = options.causes ?? [];
|
|
36
|
+
this.models = options.models ?? [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.GeminiPoolExhaustedError = GeminiPoolExhaustedError;
|
|
28
40
|
let genAIs = new Map();
|
|
29
41
|
const geminiModelsCache = new node_cache_1.TypedNodeCache({ stdTTL: 86400 });
|
|
30
42
|
const geminiModelsCacheKey = 'gemini-models-v3';
|
|
@@ -212,15 +224,27 @@ const getCombinations = async (model, prefer, output, signal, config) => {
|
|
|
212
224
|
if (models.length === 0)
|
|
213
225
|
throw new Error('No text generation models are available');
|
|
214
226
|
const combinations = [];
|
|
227
|
+
let cooldownOnly = false;
|
|
228
|
+
let nonCooldownBlock = false;
|
|
215
229
|
for (const [key, ai] of clients) {
|
|
216
230
|
for (const selectedModel of models) {
|
|
217
|
-
const
|
|
231
|
+
const service = `gemini-${selectedModel}`;
|
|
232
|
+
const status = (0, quota_1.getModelKeyBackoffStatus)(selectedModel, key, config.dependencies?.now?.() ?? Date.now());
|
|
233
|
+
if (status === 'expired')
|
|
234
|
+
(0, apikey_1.clearKeyServiceBlock)(key, service);
|
|
235
|
+
const available = (0, apikey_1.getAvailableCombinations)(service, false);
|
|
218
236
|
if (available.some((entry) => entry.key === key)) {
|
|
219
237
|
combinations.push([key, ai, selectedModel]);
|
|
220
238
|
}
|
|
239
|
+
else if (status === 'active') {
|
|
240
|
+
cooldownOnly = true;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
nonCooldownBlock = true;
|
|
244
|
+
}
|
|
221
245
|
}
|
|
222
246
|
}
|
|
223
|
-
return combinations;
|
|
247
|
+
return { combinations, models, cooldownOnly: cooldownOnly && !nonCooldownBlock, nonCooldownBlock };
|
|
224
248
|
};
|
|
225
249
|
const mediaType = (mimeType) => {
|
|
226
250
|
const normalized = mimeType.toLowerCase();
|
|
@@ -379,22 +403,37 @@ const generateGoogle = async (request, config = {}) => {
|
|
|
379
403
|
setTimeout(resolve, ms);
|
|
380
404
|
}));
|
|
381
405
|
const result = await (0, retryOnError_1.retryOnError)(`generation:${request.ident ?? 'unknown'}`, async () => {
|
|
382
|
-
const
|
|
383
|
-
|
|
406
|
+
const combinationSet = await getCombinations(request.model, request.prefer, request.output, controller.signal, config);
|
|
407
|
+
const combinations = combinationSet.combinations;
|
|
408
|
+
if (combinations.length === 0) {
|
|
409
|
+
if (combinationSet.cooldownOnly) {
|
|
410
|
+
throw new GeminiPoolExhaustedError({
|
|
411
|
+
models: combinationSet.models,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
384
414
|
throw new Error('No available model for this request');
|
|
415
|
+
}
|
|
385
416
|
let lastFailure;
|
|
417
|
+
const exhaustionCauses = [];
|
|
418
|
+
const exhaustedModels = new Set();
|
|
419
|
+
let attemptedCombination = false;
|
|
420
|
+
let nonExhaustionFailure;
|
|
421
|
+
let skippedForNonCooldown = false;
|
|
386
422
|
for (const [key, ai, selectedModel] of combinations) {
|
|
387
|
-
|
|
423
|
+
const service = `gemini-${selectedModel}`;
|
|
424
|
+
const nowMs = config.dependencies?.now?.() ?? Date.now();
|
|
425
|
+
const keyBackoffStatus = (0, quota_1.getModelKeyBackoffStatus)(selectedModel, key, nowMs);
|
|
426
|
+
if (keyBackoffStatus === 'active') {
|
|
427
|
+
(0, log_1.debug)(`[${selectedModel}] skipping rate-limited model/key`, {
|
|
428
|
+
remainingMs: (0, quota_1.modelKeyBackoffRemainingMs)(selectedModel, key, nowMs),
|
|
429
|
+
});
|
|
388
430
|
continue;
|
|
389
431
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if ((0, quota_1.isModelBackedOff)(selectedModel)) {
|
|
393
|
-
(0, log_1.debug)(`[${selectedModel}] skipping rate-limited model`, {
|
|
394
|
-
remainingMs: (0, quota_1.modelBackoffRemainingMs)(selectedModel),
|
|
395
|
-
});
|
|
432
|
+
if (!(0, apikey_1.getAvailableCombinations)(service, false).some((entry) => entry.key === key)) {
|
|
433
|
+
skippedForNonCooldown = true;
|
|
396
434
|
continue;
|
|
397
435
|
}
|
|
436
|
+
attemptedCombination = true;
|
|
398
437
|
const selectedRequest = {
|
|
399
438
|
...requestConfig,
|
|
400
439
|
model: selectedModel,
|
|
@@ -423,22 +462,39 @@ const generateGoogle = async (request, config = {}) => {
|
|
|
423
462
|
const unavailableModel = failureStatus === 404 && message.includes(selectedModel);
|
|
424
463
|
// Fallback only; per-model backoff below is keyed by rate limiting.
|
|
425
464
|
if (rateLimited || overloadedModel || unavailableModel) {
|
|
426
|
-
(0, log_1.
|
|
465
|
+
(0, log_1.debug)(`[${selectedModel}] generation attempt failed; trying next available combination`, {
|
|
427
466
|
status: failureStatus,
|
|
428
467
|
});
|
|
429
468
|
(0, apikey_1.blockKeyService)(key, `gemini-${selectedModel}`);
|
|
430
469
|
if (rateLimited || overloadedModel) {
|
|
431
|
-
(0, quota_1.
|
|
470
|
+
(0, quota_1.recordModelKeyRateLimit)(selectedModel, key, {
|
|
432
471
|
cause: error,
|
|
433
472
|
nowMs: config.dependencies?.now?.(),
|
|
434
473
|
});
|
|
435
474
|
}
|
|
475
|
+
if (unavailableModel)
|
|
476
|
+
nonExhaustionFailure = error;
|
|
477
|
+
if (rateLimited || overloadedModel)
|
|
478
|
+
exhaustionCauses.push(error);
|
|
479
|
+
if (!unavailableModel)
|
|
480
|
+
exhaustedModels.add(selectedModel);
|
|
436
481
|
lastFailure = error;
|
|
437
482
|
continue;
|
|
438
483
|
}
|
|
439
484
|
throw error;
|
|
440
485
|
}
|
|
441
486
|
}
|
|
487
|
+
if (nonExhaustionFailure !== undefined)
|
|
488
|
+
throw nonExhaustionFailure;
|
|
489
|
+
if (skippedForNonCooldown || combinationSet.nonCooldownBlock) {
|
|
490
|
+
throw new Error('No available model for this request');
|
|
491
|
+
}
|
|
492
|
+
if (!attemptedCombination || exhaustionCauses.length > 0) {
|
|
493
|
+
throw new GeminiPoolExhaustedError({
|
|
494
|
+
causes: exhaustionCauses,
|
|
495
|
+
models: [...exhaustedModels],
|
|
496
|
+
});
|
|
497
|
+
}
|
|
442
498
|
throw lastFailure ?? new Error('No available model for this request');
|
|
443
499
|
}, {
|
|
444
500
|
retries: 1,
|
|
@@ -448,6 +504,7 @@ const generateGoogle = async (request, config = {}) => {
|
|
|
448
504
|
sleepFn: async (ms) => {
|
|
449
505
|
await sleep(ms);
|
|
450
506
|
},
|
|
507
|
+
errorCheck: (error) => !(error instanceof GeminiPoolExhaustedError) && (0, retryOnError_1.isRetryableApiError)(error),
|
|
451
508
|
});
|
|
452
509
|
return result;
|
|
453
510
|
}
|
package/dist/client/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AIClient, AIClientDefaults, AIClientDependencies, AIEndpoint, AIInput, AIModel, AIModelPreference, AIPart, AIRequest, BinaryMedia, GenerateTextOptions, GenerationResult, PromptDirectOptions, PromptImageOptions } from './types';
|
|
1
|
+
import type { AIClient, AIClientDefaults, AIClientDependencies, AIEndpoint, AIInput, AIModel, AIModelPreference, AIProvider, AIPart, AIRequest, BinaryMedia, GenerateTextOptions, GenerationResult, PromptDirectOptions, PromptImageOptions } from './types';
|
|
2
|
+
export { GeminiPoolExhaustedError } from './adapters/google';
|
|
2
3
|
export declare const createAIClient: (defaults?: AIClientDefaults) => AIClient;
|
|
3
4
|
export declare const generate: (request: AIRequest) => Promise<GenerationResult>;
|
|
4
5
|
export declare function generateText(prompt: string, options?: GenerateTextOptions): Promise<string>;
|
|
@@ -8,4 +9,4 @@ export declare const promptImage: (options: PromptImageOptions) => Promise<strin
|
|
|
8
9
|
export declare const models: (options?: {
|
|
9
10
|
endpoint?: AIEndpoint;
|
|
10
11
|
}) => Promise<AIModel[]>;
|
|
11
|
-
export type { AIClient, AIClientDefaults, AIClientDependencies, AIEndpoint, AIInput, AIModel, AIModelPreference, AIPart, AIRequest, BinaryMedia, GenerateTextOptions, GenerationResult, PromptDirectOptions, PromptImageOptions, };
|
|
12
|
+
export type { AIClient, AIClientDefaults, AIClientDependencies, AIEndpoint, AIInput, AIProvider, AIModel, AIModelPreference, AIPart, AIRequest, BinaryMedia, GenerateTextOptions, GenerationResult, PromptDirectOptions, PromptImageOptions, };
|
package/dist/client/client.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.models = exports.promptImage = exports.promptDirect = exports.generate = exports.createAIClient = void 0;
|
|
3
|
+
exports.models = exports.promptImage = exports.promptDirect = exports.generate = exports.createAIClient = exports.GeminiPoolExhaustedError = void 0;
|
|
4
4
|
exports.generateText = generateText;
|
|
5
5
|
const cluster_1 = require("./adapters/cluster");
|
|
6
6
|
const google_1 = require("./adapters/google");
|
|
7
|
+
var google_2 = require("./adapters/google");
|
|
8
|
+
Object.defineProperty(exports, "GeminiPoolExhaustedError", { enumerable: true, get: function () { return google_2.GeminiPoolExhaustedError; } });
|
|
7
9
|
const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024;
|
|
8
10
|
const DOWNLOAD_TIMEOUT_MS = 30 * 1000;
|
|
11
|
+
const FALLBACK_NETWORK_TIMEOUT_MS = 3000;
|
|
9
12
|
const defaultSleep = (milliseconds) => new Promise((resolve) => {
|
|
10
13
|
setTimeout(resolve, milliseconds);
|
|
11
14
|
});
|
|
@@ -112,6 +115,7 @@ const mergeRequest = (defaults, request) => ({
|
|
|
112
115
|
outputSchema: request.outputSchema ?? defaults.outputSchema,
|
|
113
116
|
webSearch: request.webSearch ?? defaults.webSearch,
|
|
114
117
|
onGenerated: request.onGenerated ?? defaults.onGenerated,
|
|
118
|
+
onFallback: request.onFallback ?? defaults.onFallback,
|
|
115
119
|
signal: request.signal ?? defaults.signal,
|
|
116
120
|
timeoutMs: request.timeoutMs ?? defaults.timeoutMs,
|
|
117
121
|
staggerMs: request.staggerMs ?? defaults.staggerMs,
|
|
@@ -133,16 +137,55 @@ const createAIClient = (defaults = {}) => {
|
|
|
133
137
|
const dependencies = getDependencies(defaults);
|
|
134
138
|
const generate = async (request) => {
|
|
135
139
|
const merged = mergeRequest(defaults, request);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
let result;
|
|
141
|
+
let provider;
|
|
142
|
+
if (merged.endpoint === undefined) {
|
|
143
|
+
try {
|
|
144
|
+
result = await (0, google_1.generateGoogle)(merged, { dependencies });
|
|
145
|
+
provider = 'google';
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (!(error instanceof google_1.GeminiPoolExhaustedError) || defaults.fallback === undefined) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
const fallback = defaults.fallback;
|
|
152
|
+
const fallbackPollIntervalMs = Math.min(defaults.pollIntervalMs ?? 250, 3000);
|
|
153
|
+
const fallbackMaxPollIntervalMs = Math.min(Math.max(fallbackPollIntervalMs, defaults.maxPollIntervalMs ?? 2000), FALLBACK_NETWORK_TIMEOUT_MS);
|
|
154
|
+
const fallbackRequest = {
|
|
155
|
+
...merged,
|
|
156
|
+
endpoint: fallback.endpoint,
|
|
157
|
+
model: fallback.model,
|
|
158
|
+
reasoningEffort: fallback.reasoningEffort,
|
|
159
|
+
};
|
|
160
|
+
const fallbackEvent = {
|
|
161
|
+
from: { provider: 'google' },
|
|
162
|
+
to: { provider: 'cluster', model: fallback.model },
|
|
163
|
+
reason: 'gemini-pool-exhausted',
|
|
164
|
+
cause: error,
|
|
165
|
+
};
|
|
166
|
+
merged.onFallback?.(fallbackEvent);
|
|
167
|
+
result = await (0, cluster_1.generateCluster)(fallbackRequest, {
|
|
168
|
+
endpoint: fallback.endpoint,
|
|
169
|
+
dependencies,
|
|
170
|
+
timeoutMs: merged.timeoutMs,
|
|
171
|
+
pollIntervalMs: fallbackPollIntervalMs,
|
|
172
|
+
maxPollIntervalMs: fallbackMaxPollIntervalMs,
|
|
173
|
+
networkTimeoutMs: FALLBACK_NETWORK_TIMEOUT_MS,
|
|
174
|
+
});
|
|
175
|
+
provider = 'cluster';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
result = await (0, cluster_1.generateCluster)(merged, {
|
|
139
180
|
endpoint: merged.endpoint,
|
|
140
181
|
dependencies,
|
|
141
|
-
timeoutMs:
|
|
182
|
+
timeoutMs: merged.timeoutMs,
|
|
142
183
|
pollIntervalMs: defaults.pollIntervalMs,
|
|
143
184
|
maxPollIntervalMs: defaults.maxPollIntervalMs,
|
|
144
185
|
});
|
|
145
|
-
|
|
186
|
+
provider = 'cluster';
|
|
187
|
+
}
|
|
188
|
+
merged.onGenerated?.({ model: result.model, generatedAt: result.generatedAt, provider });
|
|
146
189
|
return result;
|
|
147
190
|
};
|
|
148
191
|
async function generateText(promptOrOptions, options) {
|
package/dist/client/quota.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare class QuotaDeferredError extends Error {
|
|
|
16
16
|
cause?: unknown;
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
/** True when
|
|
19
|
+
/** True when a cause chain signals model quota/capacity exhaustion. */
|
|
20
20
|
export declare const isQuotaExhaustedError: (cause: unknown) => boolean;
|
|
21
21
|
/**
|
|
22
22
|
* Honor the provider's retry delay when present (observed 9-57s). Returns undefined
|
|
@@ -34,6 +34,21 @@ export declare const MODEL_RATE_LIMIT_BASE_DELAY_MS = 60000;
|
|
|
34
34
|
export declare const MODEL_RATE_LIMIT_MAX_DELAY_MS: number;
|
|
35
35
|
/** Small jitter so backed-off models do not resurface in lockstep. */
|
|
36
36
|
export declare const MODEL_RATE_LIMIT_JITTER_MS = 1000;
|
|
37
|
+
export type ModelKeyBackoffStatus = 'active' | 'expired' | 'none';
|
|
38
|
+
/** Inspect a model/key cooldown without exposing the credential in state or logs. */
|
|
39
|
+
export declare const getModelKeyBackoffStatus: (model: string, apiKey: string, nowMs?: number) => ModelKeyBackoffStatus;
|
|
40
|
+
/** Record a quota/capacity response for one model/key combination. */
|
|
41
|
+
export declare const recordModelKeyRateLimit: (model: string, apiKey: string, opt?: {
|
|
42
|
+
cause?: unknown;
|
|
43
|
+
nowMs?: number;
|
|
44
|
+
random?: () => number;
|
|
45
|
+
}) => number;
|
|
46
|
+
/** True while one model/key combination remains inside its cooldown window. */
|
|
47
|
+
export declare const isModelKeyBackedOff: (model: string, apiKey: string, nowMs?: number) => boolean;
|
|
48
|
+
/** Remaining cooldown for one model/key combination, or 0 when eligible. */
|
|
49
|
+
export declare const modelKeyBackoffRemainingMs: (model: string, apiKey: string, nowMs?: number) => number;
|
|
50
|
+
/** Mark one model/key combination eligible again. */
|
|
51
|
+
export declare const clearModelKeyBackoff: (model: string, apiKey: string) => void;
|
|
37
52
|
/**
|
|
38
53
|
* Record a rate-limit response for a model. The model is skipped until the
|
|
39
54
|
* backoff window elapses: the provider's own retry delay when present,
|
package/dist/client/quota.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resetModelBackoff = exports.clearModelBackoff = exports.modelBackoffRemainingMs = exports.isModelBackedOff = exports.recordModelRateLimit = exports.MODEL_RATE_LIMIT_JITTER_MS = exports.MODEL_RATE_LIMIT_MAX_DELAY_MS = exports.MODEL_RATE_LIMIT_BASE_DELAY_MS = exports.quotaBackoffDelayMs = exports.getQuotaRetryDelayMs = exports.isQuotaExhaustedError = exports.QuotaDeferredError = exports.AI_QUOTA_EXHAUSTED_MESSAGE = void 0;
|
|
3
|
+
exports.resetModelBackoff = exports.clearModelBackoff = exports.modelBackoffRemainingMs = exports.isModelBackedOff = exports.recordModelRateLimit = exports.clearModelKeyBackoff = exports.modelKeyBackoffRemainingMs = exports.isModelKeyBackedOff = exports.recordModelKeyRateLimit = exports.getModelKeyBackoffStatus = exports.MODEL_RATE_LIMIT_JITTER_MS = exports.MODEL_RATE_LIMIT_MAX_DELAY_MS = exports.MODEL_RATE_LIMIT_BASE_DELAY_MS = exports.quotaBackoffDelayMs = exports.getQuotaRetryDelayMs = exports.isQuotaExhaustedError = exports.QuotaDeferredError = exports.AI_QUOTA_EXHAUSTED_MESSAGE = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
5
|
const retryOnError_1 = require("../internal/retryOnError");
|
|
5
6
|
/**
|
|
6
7
|
* Reactive per-model rate-limit backoff.
|
|
@@ -26,8 +27,20 @@ class QuotaDeferredError extends Error {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
exports.QuotaDeferredError = QuotaDeferredError;
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const hasRateLimitStatus = (cause, seen = new Set()) => {
|
|
31
|
+
if (cause === null || cause === undefined || seen.has(cause))
|
|
32
|
+
return false;
|
|
33
|
+
if (typeof cause !== 'object')
|
|
34
|
+
return false;
|
|
35
|
+
seen.add(cause);
|
|
36
|
+
const record = cause;
|
|
37
|
+
const status = record.status ?? record.statusCode ?? record.code;
|
|
38
|
+
if (status === 429 || status === '429')
|
|
39
|
+
return true;
|
|
40
|
+
return [record.cause, record.error, record.response].some((nested) => hasRateLimitStatus(nested, seen));
|
|
41
|
+
};
|
|
42
|
+
/** True when a cause chain signals model quota/capacity exhaustion. */
|
|
43
|
+
const isQuotaExhaustedError = (cause) => hasRateLimitStatus(cause) || (0, retryOnError_1.isOverloadedApiKeyError)(cause);
|
|
31
44
|
exports.isQuotaExhaustedError = isQuotaExhaustedError;
|
|
32
45
|
const RETRY_DELAY_PATTERNS = [
|
|
33
46
|
/"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/i,
|
|
@@ -95,6 +108,54 @@ exports.MODEL_RATE_LIMIT_MAX_DELAY_MS = 15 * 60_000;
|
|
|
95
108
|
exports.MODEL_RATE_LIMIT_JITTER_MS = 1_000;
|
|
96
109
|
const modelBackoffUntilMs = new Map();
|
|
97
110
|
const modelBackoffHits = new Map();
|
|
111
|
+
const modelKeyBackoffUntilMs = new Map();
|
|
112
|
+
const modelKeyBackoffHits = new Map();
|
|
113
|
+
const modelKeyBackoffId = (model, apiKey) => `${model}:${(0, node_crypto_1.createHash)('sha256').update(apiKey).digest('hex').slice(0, 16)}`;
|
|
114
|
+
/** Inspect a model/key cooldown without exposing the credential in state or logs. */
|
|
115
|
+
const getModelKeyBackoffStatus = (model, apiKey, nowMs = Date.now()) => {
|
|
116
|
+
const id = modelKeyBackoffId(model, apiKey);
|
|
117
|
+
const until = modelKeyBackoffUntilMs.get(id);
|
|
118
|
+
if (until === undefined)
|
|
119
|
+
return 'none';
|
|
120
|
+
if (nowMs >= until) {
|
|
121
|
+
modelKeyBackoffUntilMs.delete(id);
|
|
122
|
+
modelKeyBackoffHits.delete(id);
|
|
123
|
+
return 'expired';
|
|
124
|
+
}
|
|
125
|
+
return 'active';
|
|
126
|
+
};
|
|
127
|
+
exports.getModelKeyBackoffStatus = getModelKeyBackoffStatus;
|
|
128
|
+
/** Record a quota/capacity response for one model/key combination. */
|
|
129
|
+
const recordModelKeyRateLimit = (model, apiKey, opt) => {
|
|
130
|
+
const now = opt?.nowMs ?? Date.now();
|
|
131
|
+
const id = modelKeyBackoffId(model, apiKey);
|
|
132
|
+
const serverDelay = opt?.cause === undefined ? undefined : (0, exports.getQuotaRetryDelayMs)(opt.cause);
|
|
133
|
+
const hits = (modelKeyBackoffHits.get(id) ?? 0) + 1;
|
|
134
|
+
modelKeyBackoffHits.set(id, hits);
|
|
135
|
+
const exponential = Math.min(exports.MODEL_RATE_LIMIT_BASE_DELAY_MS * 2 ** (hits - 1), exports.MODEL_RATE_LIMIT_MAX_DELAY_MS);
|
|
136
|
+
const jitter = Math.floor((opt?.random ?? Math.random)() * exports.MODEL_RATE_LIMIT_JITTER_MS);
|
|
137
|
+
const until = now + Math.max(serverDelay ?? 0, exponential) + jitter;
|
|
138
|
+
modelKeyBackoffUntilMs.set(id, until);
|
|
139
|
+
return until;
|
|
140
|
+
};
|
|
141
|
+
exports.recordModelKeyRateLimit = recordModelKeyRateLimit;
|
|
142
|
+
/** True while one model/key combination remains inside its cooldown window. */
|
|
143
|
+
const isModelKeyBackedOff = (model, apiKey, nowMs) => (0, exports.getModelKeyBackoffStatus)(model, apiKey, nowMs) === 'active';
|
|
144
|
+
exports.isModelKeyBackedOff = isModelKeyBackedOff;
|
|
145
|
+
/** Remaining cooldown for one model/key combination, or 0 when eligible. */
|
|
146
|
+
const modelKeyBackoffRemainingMs = (model, apiKey, nowMs = Date.now()) => {
|
|
147
|
+
if ((0, exports.getModelKeyBackoffStatus)(model, apiKey, nowMs) !== 'active')
|
|
148
|
+
return 0;
|
|
149
|
+
return Math.max(0, (modelKeyBackoffUntilMs.get(modelKeyBackoffId(model, apiKey)) ?? nowMs) - nowMs);
|
|
150
|
+
};
|
|
151
|
+
exports.modelKeyBackoffRemainingMs = modelKeyBackoffRemainingMs;
|
|
152
|
+
/** Mark one model/key combination eligible again. */
|
|
153
|
+
const clearModelKeyBackoff = (model, apiKey) => {
|
|
154
|
+
const id = modelKeyBackoffId(model, apiKey);
|
|
155
|
+
modelKeyBackoffUntilMs.delete(id);
|
|
156
|
+
modelKeyBackoffHits.delete(id);
|
|
157
|
+
};
|
|
158
|
+
exports.clearModelKeyBackoff = clearModelKeyBackoff;
|
|
98
159
|
/**
|
|
99
160
|
* Record a rate-limit response for a model. The model is skipped until the
|
|
100
161
|
* backoff window elapses: the provider's own retry delay when present,
|
|
@@ -144,5 +205,7 @@ exports.clearModelBackoff = clearModelBackoff;
|
|
|
144
205
|
const resetModelBackoff = () => {
|
|
145
206
|
modelBackoffUntilMs.clear();
|
|
146
207
|
modelBackoffHits.clear();
|
|
208
|
+
modelKeyBackoffUntilMs.clear();
|
|
209
|
+
modelKeyBackoffHits.clear();
|
|
147
210
|
};
|
|
148
211
|
exports.resetModelBackoff = resetModelBackoff;
|
package/dist/client/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type AIOutputType = 'text' | 'image' | 'audio' | 'video' | 'file';
|
|
2
2
|
export type AIModelPreference = 'fast' | 'quality';
|
|
3
|
+
export type AIProvider = 'google' | 'cluster';
|
|
3
4
|
export type AIInputRole = 'user' | 'assistant' | 'system' | 'developer';
|
|
4
5
|
export type AITextPart = {
|
|
5
6
|
type: 'text';
|
|
@@ -29,6 +30,8 @@ export type AIOutputSchema = {
|
|
|
29
30
|
export type GenerationMetadata = {
|
|
30
31
|
model: string;
|
|
31
32
|
generatedAt: number;
|
|
33
|
+
/** Present on client provenance callbacks; raw adapters may omit it. */
|
|
34
|
+
provider?: AIProvider;
|
|
32
35
|
};
|
|
33
36
|
export type GenerationUsage = {
|
|
34
37
|
inputTokens: number;
|
|
@@ -67,6 +70,7 @@ export type AIRequest = {
|
|
|
67
70
|
outputSchema?: AIOutputSchema;
|
|
68
71
|
webSearch?: boolean;
|
|
69
72
|
onGenerated?: (metadata: GenerationMetadata) => void;
|
|
73
|
+
onFallback?: (event: AIFallbackEvent) => void;
|
|
70
74
|
signal?: AbortSignal;
|
|
71
75
|
timeoutMs?: number;
|
|
72
76
|
/**
|
|
@@ -76,6 +80,22 @@ export type AIRequest = {
|
|
|
76
80
|
*/
|
|
77
81
|
staggerMs?: number;
|
|
78
82
|
};
|
|
83
|
+
export type AIFallback = {
|
|
84
|
+
endpoint: AIEndpoint;
|
|
85
|
+
model: string;
|
|
86
|
+
reasoningEffort: string;
|
|
87
|
+
};
|
|
88
|
+
export type AIFallbackEvent = {
|
|
89
|
+
from: {
|
|
90
|
+
provider: 'google';
|
|
91
|
+
};
|
|
92
|
+
to: {
|
|
93
|
+
provider: 'cluster';
|
|
94
|
+
model: string;
|
|
95
|
+
};
|
|
96
|
+
reason: 'gemini-pool-exhausted';
|
|
97
|
+
cause: unknown;
|
|
98
|
+
};
|
|
79
99
|
export type BinaryMedia = {
|
|
80
100
|
arraybuffer: ArrayBuffer;
|
|
81
101
|
type: string;
|
|
@@ -107,6 +127,7 @@ export type AIClientDependencies = {
|
|
|
107
127
|
};
|
|
108
128
|
export type AIClientDefaults = AIRequestOptions & AIClientDependencies & {
|
|
109
129
|
dependencies?: AIClientDependencies;
|
|
130
|
+
fallback?: AIFallback;
|
|
110
131
|
pollIntervalMs?: number;
|
|
111
132
|
maxPollIntervalMs?: number;
|
|
112
133
|
};
|
|
@@ -3,3 +3,5 @@ export declare const getAvailableCombinations: (service: string, resetIfExhauste
|
|
|
3
3
|
key: string;
|
|
4
4
|
}[];
|
|
5
5
|
export declare const blockKeyService: (apiKey: string, service: string) => void;
|
|
6
|
+
/** Remove only one key+service block, preserving every unrelated blocklist entry. */
|
|
7
|
+
export declare const clearKeyServiceBlock: (apiKey: string, service: string) => void;
|
package/dist/internal/apikey.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.blockKeyService = exports.getAvailableCombinations = void 0;
|
|
3
|
+
exports.clearKeyServiceBlock = exports.blockKeyService = exports.getAvailableCombinations = void 0;
|
|
4
4
|
const array_1 = require("./array");
|
|
5
5
|
const log_1 = require("./log");
|
|
6
6
|
const node_cache_1 = require("./node-cache");
|
|
@@ -33,7 +33,7 @@ const getAvailableCombinations = (service, resetIfExhausted = true) => {
|
|
|
33
33
|
}
|
|
34
34
|
// If all combinations are blocklisted, clear the blocklist and try again
|
|
35
35
|
if (combinations.length === 0 && resetIfExhausted) {
|
|
36
|
-
(0, log_1.
|
|
36
|
+
(0, log_1.debug)(`[${service}] all API key + service combinations were blocklisted; clearing blocklist and trying again.`);
|
|
37
37
|
blocklist.flushAll();
|
|
38
38
|
for (const key of keys) {
|
|
39
39
|
combinations.push({ service, key });
|
|
@@ -45,7 +45,12 @@ const getAvailableCombinations = (service, resetIfExhausted = true) => {
|
|
|
45
45
|
exports.getAvailableCombinations = getAvailableCombinations;
|
|
46
46
|
// Block a key+service combination
|
|
47
47
|
const blockKeyService = (apiKey, service) => {
|
|
48
|
-
(0, log_1.
|
|
48
|
+
(0, log_1.debug)(`[${service}] key+service blocklisted`);
|
|
49
49
|
addToBlocklist(apiKey, service);
|
|
50
50
|
};
|
|
51
51
|
exports.blockKeyService = blockKeyService;
|
|
52
|
+
/** Remove only one key+service block, preserving every unrelated blocklist entry. */
|
|
53
|
+
const clearKeyServiceBlock = (apiKey, service) => {
|
|
54
|
+
blocklist.del(getBlocklistKey(apiKey, service));
|
|
55
|
+
};
|
|
56
|
+
exports.clearKeyServiceBlock = clearKeyServiceBlock;
|
package/dist/k8s/opencode.js
CHANGED
|
@@ -40,7 +40,22 @@ const opencodeStack = (context) => {
|
|
|
40
40
|
(0, common_1.env)('XDG_CACHE_HOME', '/data/opencode/cache'),
|
|
41
41
|
// The unrestricted /provider response processes thousands of models from
|
|
42
42
|
// unconfigured providers on every request, exceeding the runner's budget.
|
|
43
|
-
|
|
43
|
+
// Each advertised model carries the reasoning effort its consumer asks
|
|
44
|
+
// for: sol at high for draft copy, 5.5 at high for draft images, and luna
|
|
45
|
+
// at max for the client's cluster fallback. OpenCode merges these entries
|
|
46
|
+
// with its model catalogue; it does not replace it.
|
|
47
|
+
(0, common_1.env)('OPENCODE_CONFIG_CONTENT', JSON.stringify({
|
|
48
|
+
enabled_providers: ['openai', 'google'],
|
|
49
|
+
provider: {
|
|
50
|
+
openai: {
|
|
51
|
+
models: {
|
|
52
|
+
'gpt-5.6-sol': { options: { reasoningEffort: 'high' } },
|
|
53
|
+
'gpt-5.6-luna': { options: { reasoningEffort: 'max' } },
|
|
54
|
+
'gpt-5.5': { options: { reasoningEffort: 'high' } },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
})),
|
|
44
59
|
],
|
|
45
60
|
startupProbe: { ...probe, failureThreshold: 30 },
|
|
46
61
|
readinessProbe: { ...(0, common_1.openCodeHealthProbe)(common_1.OPENCODE_PORT, 'models'), initialDelaySeconds: 5 },
|
package/package.json
CHANGED
|
@@ -191,6 +191,26 @@ const usageFromTokens = (tokens) => {
|
|
|
191
191
|
return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Read the reasoning efforts a model declares in the OpenCode provider catalog.
|
|
196
|
+
* OpenCode stores the effort on the model's `options`, and newer builds may also
|
|
197
|
+
* expose an explicit list. Missing metadata falls back to the executor default
|
|
198
|
+
* rather than inventing capabilities.
|
|
199
|
+
*/
|
|
200
|
+
const reasoningEffortsFromModel = (model) => {
|
|
201
|
+
const efforts = [];
|
|
202
|
+
const configured = asObject(model?.options)?.reasoningEffort;
|
|
203
|
+
if (typeof configured === 'string' && configured.length > 0) efforts.push(configured);
|
|
204
|
+
for (const source of [model?.supportedReasoningEfforts, model?.reasoningEfforts]) {
|
|
205
|
+
if (!Array.isArray(source)) continue;
|
|
206
|
+
for (const item of source) {
|
|
207
|
+
const value = typeof item === 'string' ? item : asObject(item)?.reasoningEffort;
|
|
208
|
+
if (typeof value === 'string' && value.length > 0) efforts.push(value);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return [...new Set(efforts)];
|
|
212
|
+
};
|
|
213
|
+
|
|
194
214
|
/**
|
|
195
215
|
* Create an executor backed by an OpenCode HTTP server. The server URL comes
|
|
196
216
|
* from `AG_AI_OPENCODE_URL`; the basic-auth password comes from
|
|
@@ -397,12 +417,15 @@ const createOpenCodeExecutor = async ({
|
|
|
397
417
|
const modelId = typeof model?.id === 'string' && model.id.length > 0 ? model.id : key;
|
|
398
418
|
const inputModalities = ['text'];
|
|
399
419
|
if (model?.capabilities?.input?.image === true) inputModalities.push('image');
|
|
420
|
+
const declaredEfforts = reasoningEffortsFromModel(model);
|
|
421
|
+
const supportedReasoningEfforts =
|
|
422
|
+
declaredEfforts.length > 0 ? declaredEfforts : [DEFAULT_REASONING_EFFORT];
|
|
400
423
|
models.push({
|
|
401
424
|
id: `${providerId}/${modelId}`,
|
|
402
425
|
inputModalities,
|
|
403
426
|
outputModalities: imageAdapter !== undefined ? ['text', 'image'] : ['text'],
|
|
404
|
-
supportedReasoningEfforts
|
|
405
|
-
defaultReasoningEffort:
|
|
427
|
+
supportedReasoningEfforts,
|
|
428
|
+
defaultReasoningEffort: supportedReasoningEfforts[0],
|
|
406
429
|
webSearch: false,
|
|
407
430
|
prefer: ['quality'],
|
|
408
431
|
isDefault: defaults[providerId] === modelId,
|
package/runtime/protocol.mjs
CHANGED
|
@@ -117,6 +117,16 @@ const normalizeModelCatalog = (models) => {
|
|
|
117
117
|
const unsupported = (what) =>
|
|
118
118
|
new GenerationProtocolError(422, 'unsupported', `${what} is not supported by the selected model`);
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Resolve an unqualified model id (for example `gpt-5.6-luna`) to the single
|
|
122
|
+
* catalog entry that exposes it under a provider prefix. An ambiguous or absent
|
|
123
|
+
* match stays unresolved so the caller gets an explicit unsupported error.
|
|
124
|
+
*/
|
|
125
|
+
const uniqueSuffixModel = (catalog, requestedModel) => {
|
|
126
|
+
const matches = catalog.filter((candidate) => candidate.id.endsWith(`/${requestedModel}`));
|
|
127
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
128
|
+
};
|
|
129
|
+
|
|
120
130
|
const normalizePart = (part, capabilities, total) => {
|
|
121
131
|
const object = asObject(part);
|
|
122
132
|
const type = object?.type;
|
|
@@ -262,7 +272,9 @@ const normalizeGenerationRequest = (value, models) => {
|
|
|
262
272
|
catalog.find((candidate) => candidate.prefer.includes('quality'))?.id ??
|
|
263
273
|
catalog[0].id;
|
|
264
274
|
}
|
|
265
|
-
const model =
|
|
275
|
+
const model =
|
|
276
|
+
catalog.find((candidate) => candidate.id === requestedModel) ??
|
|
277
|
+
(requestedModel.includes('/') ? undefined : uniqueSuffixModel(catalog, requestedModel));
|
|
266
278
|
if (!model) throw unsupported(`model ${requestedModel}`);
|
|
267
279
|
const capabilities = normalizeCapabilities(model);
|
|
268
280
|
const instructions =
|
|
@@ -306,7 +318,7 @@ const normalizeGenerationRequest = (value, models) => {
|
|
|
306
318
|
instructions,
|
|
307
319
|
input,
|
|
308
320
|
output,
|
|
309
|
-
model:
|
|
321
|
+
model: model.id,
|
|
310
322
|
...(prefer === undefined ? {} : { prefer }),
|
|
311
323
|
reasoningEffort,
|
|
312
324
|
...(outputSchema === undefined ? {} : { outputSchema }),
|