@axlsdk/axl 0.23.2 → 0.23.3
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 +25 -2
- package/dist/index.cjs +897 -254
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -65
- package/dist/index.d.ts +163 -65
- package/dist/index.js +896 -254
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -219,6 +219,7 @@ __export(index_exports, {
|
|
|
219
219
|
resolveRuntime: () => resolveRuntime,
|
|
220
220
|
resolveThinkingOptions: () => resolveThinkingOptions,
|
|
221
221
|
stringStreamFromEvents: () => stringStreamFromEvents,
|
|
222
|
+
summarizeModelInput: () => summarizeModelInput,
|
|
222
223
|
tool: () => tool,
|
|
223
224
|
workflow: () => workflow,
|
|
224
225
|
zodToJsonSchema: () => zodToJsonSchema
|
|
@@ -1416,38 +1417,112 @@ function unsafeTransportError(surface, remediation, exposedData) {
|
|
|
1416
1417
|
);
|
|
1417
1418
|
}
|
|
1418
1419
|
|
|
1420
|
+
// src/providers/rich-input.ts
|
|
1421
|
+
function richInputParts(input) {
|
|
1422
|
+
return typeof input === "string" ? [] : input.filter((part) => part.type !== "text");
|
|
1423
|
+
}
|
|
1424
|
+
function firstRichPart(input, history, modality) {
|
|
1425
|
+
const find = (value) => richInputParts(value).find((part) => modality === void 0 || part.type === modality);
|
|
1426
|
+
return find(input) ?? history.map((message) => find(message.content)).find((part) => part !== void 0);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/providers/audio-format.ts
|
|
1430
|
+
var OPENAI_CHAT_AUDIO_FORMATS = Object.freeze({
|
|
1431
|
+
"audio/wav": "wav",
|
|
1432
|
+
"audio/x-wav": "wav",
|
|
1433
|
+
"audio/wave": "wav",
|
|
1434
|
+
"audio/mpeg": "mp3",
|
|
1435
|
+
"audio/mp3": "mp3"
|
|
1436
|
+
});
|
|
1437
|
+
var OPENROUTER_AUDIO_FORMATS = Object.freeze({
|
|
1438
|
+
...OPENAI_CHAT_AUDIO_FORMATS,
|
|
1439
|
+
"audio/aiff": "aiff",
|
|
1440
|
+
"audio/x-aiff": "aiff",
|
|
1441
|
+
"audio/aac": "aac",
|
|
1442
|
+
"audio/ogg": "ogg",
|
|
1443
|
+
"audio/flac": "flac",
|
|
1444
|
+
"audio/x-flac": "flac",
|
|
1445
|
+
"audio/m4a": "m4a",
|
|
1446
|
+
"audio/x-m4a": "m4a",
|
|
1447
|
+
"audio/mp4": "m4a",
|
|
1448
|
+
"audio/l16": "pcm16"
|
|
1449
|
+
});
|
|
1450
|
+
function resolveAudioFormat(formats, mediaType2) {
|
|
1451
|
+
if (typeof mediaType2 !== "string") return void 0;
|
|
1452
|
+
const essence = mediaType2.split(";", 1)[0].trim().toLowerCase();
|
|
1453
|
+
return Object.prototype.hasOwnProperty.call(formats, essence) ? formats[essence] : void 0;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1419
1456
|
// src/providers/openai-compatible.ts
|
|
1420
|
-
function
|
|
1457
|
+
function unmappableMediaTypeFeature(mediaType2) {
|
|
1458
|
+
return mediaType2 === void 0 ? "audio media type (missing)" : `audio media type '${mediaType2}'`;
|
|
1459
|
+
}
|
|
1460
|
+
function compatibleInlineBase64(source) {
|
|
1421
1461
|
return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
|
|
1422
1462
|
"base64"
|
|
1423
1463
|
);
|
|
1424
1464
|
}
|
|
1425
|
-
function
|
|
1465
|
+
function compatibleRichParts(parts, model, modalities, provider) {
|
|
1466
|
+
const reject = (modality, source, feature) => new UnsupportedModelInputError({
|
|
1467
|
+
provider,
|
|
1468
|
+
model,
|
|
1469
|
+
modality,
|
|
1470
|
+
source,
|
|
1471
|
+
...feature ? { feature } : {}
|
|
1472
|
+
});
|
|
1426
1473
|
const content = [];
|
|
1427
1474
|
for (const part of parts) {
|
|
1428
1475
|
if (part.type === "text") {
|
|
1429
1476
|
content.push({ type: "text", text: part.text });
|
|
1430
1477
|
continue;
|
|
1431
1478
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1479
|
+
if (part.type === "audio") {
|
|
1480
|
+
const { source: source2 } = part;
|
|
1481
|
+
const audio = modalities.audio;
|
|
1482
|
+
if (!audio) throw reject("audio", source2.type, "audio input");
|
|
1483
|
+
if (source2.type === "provider-file" || !audio.sources.includes(source2.type)) {
|
|
1484
|
+
throw reject("audio", source2.type);
|
|
1485
|
+
}
|
|
1486
|
+
const format = resolveAudioFormat(audio.formats, source2.mediaType);
|
|
1487
|
+
if (format === void 0) {
|
|
1488
|
+
throw reject("audio", source2.type, unmappableMediaTypeFeature(source2.mediaType));
|
|
1489
|
+
}
|
|
1490
|
+
content.push({
|
|
1491
|
+
type: "input_audio",
|
|
1492
|
+
// Caller base64 passes through verbatim — never decoded and re-encoded.
|
|
1493
|
+
input_audio: { data: compatibleInlineBase64(source2), format }
|
|
1494
|
+
});
|
|
1495
|
+
if (part.label) content.push({ type: "text", text: `[Audio: ${part.label}]` });
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
if (part.type !== "image") {
|
|
1499
|
+
const unmapped = part;
|
|
1434
1500
|
throw new UnsupportedModelInputError({
|
|
1435
|
-
provider
|
|
1501
|
+
provider,
|
|
1436
1502
|
model,
|
|
1437
|
-
modality:
|
|
1438
|
-
|
|
1503
|
+
modality: unmapped.type,
|
|
1504
|
+
feature: "this input modality"
|
|
1439
1505
|
});
|
|
1440
1506
|
}
|
|
1507
|
+
const { source } = part;
|
|
1508
|
+
const image = modalities.image;
|
|
1509
|
+
if (!image) throw reject("image", source.type, "image input for this model");
|
|
1510
|
+
if (source.type === "provider-file" || !image.sources.includes(source.type)) {
|
|
1511
|
+
throw reject("image", source.type);
|
|
1512
|
+
}
|
|
1441
1513
|
content.push({
|
|
1442
1514
|
type: "image_url",
|
|
1443
1515
|
image_url: {
|
|
1444
|
-
url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${
|
|
1516
|
+
url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${compatibleInlineBase64(source)}`
|
|
1445
1517
|
}
|
|
1446
1518
|
});
|
|
1447
1519
|
if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
|
|
1448
1520
|
}
|
|
1449
1521
|
return content;
|
|
1450
1522
|
}
|
|
1523
|
+
function requestAudioParts(input, history) {
|
|
1524
|
+
return [input, ...history.map((message) => message.content)].flatMap((content) => richInputParts(content)).filter((part) => part.type === "audio");
|
|
1525
|
+
}
|
|
1451
1526
|
function resolvePerModel(value, model, fallback) {
|
|
1452
1527
|
if (value === void 0) return fallback;
|
|
1453
1528
|
return typeof value === "function" ? value(model) : value;
|
|
@@ -1461,6 +1536,9 @@ function sortedKeys(table) {
|
|
|
1461
1536
|
}
|
|
1462
1537
|
return keys;
|
|
1463
1538
|
}
|
|
1539
|
+
function reportedTokenCount(value) {
|
|
1540
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
1541
|
+
}
|
|
1464
1542
|
function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens, match = "prefix") {
|
|
1465
1543
|
const cached = cachedTokens ?? 0;
|
|
1466
1544
|
if (!Number.isSafeInteger(promptTokens) || !Number.isSafeInteger(completionTokens) || !Number.isSafeInteger(cached) || promptTokens < 0 || completionTokens < 0 || cached < 0 || cached > promptTokens) {
|
|
@@ -1564,26 +1642,47 @@ function normalizeMessageContent(content) {
|
|
|
1564
1642
|
var OpenAICompatibleProvider = class {
|
|
1565
1643
|
name;
|
|
1566
1644
|
reportsRequestLifecycle = true;
|
|
1645
|
+
/**
|
|
1646
|
+
* Modalities the effective profile declares for this model. A blank model
|
|
1647
|
+
* declares nothing, so a rich request against one fails validation exactly
|
|
1648
|
+
* as it did before any modality existed.
|
|
1649
|
+
*/
|
|
1650
|
+
inputModalities(model) {
|
|
1651
|
+
return model.trim().length > 0 ? this.profile.capabilities?.inputModalities ?? {} : {};
|
|
1652
|
+
}
|
|
1567
1653
|
inputCapabilities(model) {
|
|
1568
|
-
|
|
1654
|
+
const { image, audio } = this.inputModalities(model);
|
|
1655
|
+
return {
|
|
1656
|
+
...image ? { image: { sources: image.sources } } : {},
|
|
1657
|
+
// `formats` is engine-internal wire detail; the runtime contract is the
|
|
1658
|
+
// accepted source kinds only.
|
|
1659
|
+
...audio ? { audio: { sources: audio.sources } } : {}
|
|
1660
|
+
};
|
|
1569
1661
|
}
|
|
1570
1662
|
validateInput(request) {
|
|
1571
1663
|
const modelOverride = request.providerOptions?.model;
|
|
1572
1664
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
1573
|
-
const
|
|
1665
|
+
const failWith = (modality, source, feature) => {
|
|
1574
1666
|
throw new UnsupportedModelInputError({
|
|
1575
1667
|
provider: this.name,
|
|
1576
1668
|
model: effectiveModel || request.model,
|
|
1577
|
-
modality
|
|
1669
|
+
modality,
|
|
1578
1670
|
...source ? { source } : {},
|
|
1579
1671
|
...feature ? { feature } : {}
|
|
1580
1672
|
});
|
|
1581
1673
|
};
|
|
1674
|
+
const fail = (source, feature) => failWith(firstRichPart(request.input, request.history)?.type ?? "image", source, feature);
|
|
1675
|
+
const modalities = this.inputModalities(effectiveModel);
|
|
1676
|
+
const audioPart = firstRichPart(request.input, request.history, "audio");
|
|
1677
|
+
if (audioPart && !modalities.audio) {
|
|
1678
|
+
failWith("audio", audioPart.source.type, "audio input");
|
|
1679
|
+
}
|
|
1582
1680
|
if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
|
|
1583
1681
|
fail(void 0, "invalid model providerOptions");
|
|
1584
1682
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1683
|
+
const imagePart = firstRichPart(request.input, request.history, "image");
|
|
1684
|
+
if (!modalities.image && (!audioPart || imagePart)) {
|
|
1685
|
+
failWith("image", void 0, "image input for this model");
|
|
1587
1686
|
}
|
|
1588
1687
|
if (request.providerOptions && "messages" in request.providerOptions) {
|
|
1589
1688
|
fail(void 0, "raw messages providerOptions");
|
|
@@ -1594,11 +1693,21 @@ var OpenAICompatibleProvider = class {
|
|
|
1594
1693
|
if (message.content.some(
|
|
1595
1694
|
(part) => part.type === "image" && part.source.type === "provider-file"
|
|
1596
1695
|
)) {
|
|
1597
|
-
|
|
1696
|
+
failWith("image", "provider-file");
|
|
1598
1697
|
}
|
|
1599
1698
|
}
|
|
1600
1699
|
if (Array.isArray(request.input) && request.input.some((part) => part.type === "image" && part.source.type === "provider-file")) {
|
|
1601
|
-
|
|
1700
|
+
failWith("image", "provider-file");
|
|
1701
|
+
}
|
|
1702
|
+
const audio = modalities.audio;
|
|
1703
|
+
if (audio) {
|
|
1704
|
+
for (const part of requestAudioParts(request.input, request.history)) {
|
|
1705
|
+
const source = part.source;
|
|
1706
|
+
if (!audio.sources.includes(source.type)) failWith("audio", source.type);
|
|
1707
|
+
if (resolveAudioFormat(audio.formats, source.mediaType) === void 0) {
|
|
1708
|
+
failWith("audio", source.type, unmappableMediaTypeFeature(source.mediaType));
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1602
1711
|
}
|
|
1603
1712
|
return { effectiveModel };
|
|
1604
1713
|
}
|
|
@@ -1783,9 +1892,15 @@ var OpenAICompatibleProvider = class {
|
|
|
1783
1892
|
return body;
|
|
1784
1893
|
}
|
|
1785
1894
|
formatMessage(msg, model) {
|
|
1895
|
+
const modalities = this.inputModalities(model);
|
|
1786
1896
|
const out = {
|
|
1787
1897
|
role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
|
|
1788
|
-
|
|
1898
|
+
// History user turns go through the same builder as the caller's input,
|
|
1899
|
+
// so a rich part is reproduced identically in every continuation request.
|
|
1900
|
+
// Array content is routed UNCONDITIONALLY: a profile that declares no
|
|
1901
|
+
// modality has the builder reject every rich part (and pass text through)
|
|
1902
|
+
// rather than emitting raw `InputContentPart` objects on the wire.
|
|
1903
|
+
content: Array.isArray(msg.content) ? compatibleRichParts(msg.content, model, modalities, this.name) : msg.content
|
|
1789
1904
|
};
|
|
1790
1905
|
if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
|
|
1791
1906
|
if (msg.tool_calls) out.tool_calls = msg.tool_calls;
|
|
@@ -1816,6 +1931,8 @@ var OpenAICompatibleProvider = class {
|
|
|
1816
1931
|
}
|
|
1817
1932
|
toUsage(raw) {
|
|
1818
1933
|
if (!raw) return void 0;
|
|
1934
|
+
const audioInput = reportedTokenCount(raw.prompt_tokens_details?.audio_tokens);
|
|
1935
|
+
const audioOutput = reportedTokenCount(raw.completion_tokens_details?.audio_tokens);
|
|
1819
1936
|
return {
|
|
1820
1937
|
prompt_tokens: raw.prompt_tokens,
|
|
1821
1938
|
completion_tokens: raw.completion_tokens,
|
|
@@ -1825,7 +1942,9 @@ var OpenAICompatibleProvider = class {
|
|
|
1825
1942
|
// prompt_tokens_details. Preserve the normalized hit count for the
|
|
1826
1943
|
// generic table estimator; its split is validated before pricing.
|
|
1827
1944
|
cached_tokens: raw.prompt_cache_hit_tokens ?? raw.prompt_tokens_details?.cached_tokens,
|
|
1828
|
-
cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens
|
|
1945
|
+
cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens,
|
|
1946
|
+
...audioInput !== void 0 ? { audio_input_tokens: audioInput } : {},
|
|
1947
|
+
...audioOutput !== void 0 ? { audio_output_tokens: audioOutput } : {}
|
|
1829
1948
|
};
|
|
1830
1949
|
}
|
|
1831
1950
|
reportedCost(raw) {
|
|
@@ -1851,6 +1970,9 @@ var OpenAICompatibleProvider = class {
|
|
|
1851
1970
|
case "from-response":
|
|
1852
1971
|
return typeof reportedCost === "number" && Number.isFinite(reportedCost) && reportedCost >= 0 ? reportedCost : void 0;
|
|
1853
1972
|
case "table": {
|
|
1973
|
+
if (typeof usage.audio_input_tokens === "number" && usage.audio_input_tokens > 0 || typeof usage.audio_output_tokens === "number" && usage.audio_output_tokens > 0) {
|
|
1974
|
+
return void 0;
|
|
1975
|
+
}
|
|
1854
1976
|
if (!isBuiltinTablePricingEligible(this.profile, {
|
|
1855
1977
|
baseUrl: this.baseUrl,
|
|
1856
1978
|
request: context?.request,
|
|
@@ -2198,6 +2320,26 @@ var DIRECT_OPENAI_CATALOG = [
|
|
|
2198
2320
|
}),
|
|
2199
2321
|
contextBoundary: LONG_CONTEXT_BOUNDARY
|
|
2200
2322
|
},
|
|
2323
|
+
// Audio-capable models. Audio rates reviewed 2026-09-08 against
|
|
2324
|
+
// https://developers.openai.com/api/docs/pricing (no version stamp on the
|
|
2325
|
+
// page): `gpt-audio-1.5` and `gpt-audio` both list Text $2.50 in / $10.00
|
|
2326
|
+
// out and Audio $32.00 in / $64.00 out per 1M tokens, with no cached-input
|
|
2327
|
+
// row — hence no `cachedInput` here, which makes a cache-hit report unpriced
|
|
2328
|
+
// rather than silently billed at the full input rate. Only these exact ids
|
|
2329
|
+
// are listed: no snapshot id is published for either, and inventing one
|
|
2330
|
+
// would price an unknown model.
|
|
2331
|
+
{
|
|
2332
|
+
aliases: ["gpt-audio-1.5"],
|
|
2333
|
+
snapshotBase: "gpt-audio-1.5",
|
|
2334
|
+
short: { input: 2.5 / M, output: 10 / M },
|
|
2335
|
+
audio: { input: 32 / M, output: 64 / M }
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
aliases: ["gpt-audio"],
|
|
2339
|
+
snapshotBase: "gpt-audio",
|
|
2340
|
+
short: { input: 2.5 / M, output: 10 / M },
|
|
2341
|
+
audio: { input: 32 / M, output: 64 / M }
|
|
2342
|
+
},
|
|
2201
2343
|
// Existing direct models retain literal current Standard rows. These flat
|
|
2202
2344
|
// rows keep native pricing aligned with the compatibility wrapper while the
|
|
2203
2345
|
// private estimator still validates categories and billing mode strictly.
|
|
@@ -2348,32 +2490,45 @@ var CANONICAL_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
|
2348
2490
|
function isTextContentPart(value, type) {
|
|
2349
2491
|
return value !== null && typeof value === "object" && value.type === type && typeof value.text === "string";
|
|
2350
2492
|
}
|
|
2351
|
-
function
|
|
2493
|
+
function isChatAudioContentPart(value) {
|
|
2494
|
+
if (value === null || typeof value !== "object") return false;
|
|
2495
|
+
const part = value;
|
|
2496
|
+
return part.type === "input_audio" && part.input_audio !== null && typeof part.input_audio === "object";
|
|
2497
|
+
}
|
|
2498
|
+
function classifyDirectOpenAIContent(request) {
|
|
2499
|
+
if (!request) return "text";
|
|
2352
2500
|
if ("messages" in request) {
|
|
2353
|
-
if (!Array.isArray(request.messages)) return
|
|
2501
|
+
if (!Array.isArray(request.messages)) return "unmodeled";
|
|
2502
|
+
let carriesAudio = false;
|
|
2354
2503
|
for (const message of request.messages) {
|
|
2355
|
-
if (message === null || typeof message !== "object") return
|
|
2504
|
+
if (message === null || typeof message !== "object") return "unmodeled";
|
|
2356
2505
|
const content = message.content;
|
|
2357
2506
|
if (content === void 0 || content === null || typeof content === "string") continue;
|
|
2358
|
-
if (!Array.isArray(content)
|
|
2359
|
-
|
|
2507
|
+
if (!Array.isArray(content)) return "unmodeled";
|
|
2508
|
+
for (const part of content) {
|
|
2509
|
+
if (isTextContentPart(part, "text")) continue;
|
|
2510
|
+
if (isChatAudioContentPart(part)) {
|
|
2511
|
+
carriesAudio = true;
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
return "unmodeled";
|
|
2360
2515
|
}
|
|
2361
2516
|
}
|
|
2362
|
-
return
|
|
2517
|
+
return carriesAudio ? "audio" : "text";
|
|
2363
2518
|
}
|
|
2364
2519
|
if ("input" in request) {
|
|
2365
2520
|
const input = request.input;
|
|
2366
|
-
if (typeof input === "string") return
|
|
2367
|
-
if (!Array.isArray(input)) return
|
|
2521
|
+
if (typeof input === "string") return "text";
|
|
2522
|
+
if (!Array.isArray(input)) return "unmodeled";
|
|
2368
2523
|
for (const item of input) {
|
|
2369
|
-
if (item === null || typeof item !== "object") return
|
|
2524
|
+
if (item === null || typeof item !== "object") return "unmodeled";
|
|
2370
2525
|
const typed = item;
|
|
2371
2526
|
switch (typed.type) {
|
|
2372
2527
|
case "message": {
|
|
2373
2528
|
const content = typed.content;
|
|
2374
2529
|
if (typeof content === "string") break;
|
|
2375
2530
|
if (!Array.isArray(content) || content.some((part) => !isTextContentPart(part, "input_text"))) {
|
|
2376
|
-
return
|
|
2531
|
+
return "unmodeled";
|
|
2377
2532
|
}
|
|
2378
2533
|
break;
|
|
2379
2534
|
}
|
|
@@ -2383,31 +2538,30 @@ function hasUnmodeledDirectOpenAIContent(request) {
|
|
|
2383
2538
|
const output = typed.output;
|
|
2384
2539
|
if (typeof output === "string") break;
|
|
2385
2540
|
if (!Array.isArray(output) || output.some((part) => !isTextContentPart(part, "input_text"))) {
|
|
2386
|
-
return
|
|
2541
|
+
return "unmodeled";
|
|
2387
2542
|
}
|
|
2388
2543
|
break;
|
|
2389
2544
|
}
|
|
2390
2545
|
case "custom_tool_call":
|
|
2391
|
-
if (typeof typed.input !== "string") return
|
|
2546
|
+
if (typeof typed.input !== "string") return "unmodeled";
|
|
2392
2547
|
break;
|
|
2393
2548
|
case "custom_tool_call_output":
|
|
2394
|
-
if (typeof typed.output !== "string") return
|
|
2549
|
+
if (typeof typed.output !== "string") return "unmodeled";
|
|
2395
2550
|
break;
|
|
2396
2551
|
case "reasoning":
|
|
2397
2552
|
break;
|
|
2398
2553
|
default:
|
|
2399
|
-
return
|
|
2554
|
+
return "unmodeled";
|
|
2400
2555
|
}
|
|
2401
2556
|
}
|
|
2402
|
-
return
|
|
2557
|
+
return "text";
|
|
2403
2558
|
}
|
|
2404
|
-
return
|
|
2559
|
+
return "text";
|
|
2405
2560
|
}
|
|
2406
|
-
function isEligibleDirectOpenAIContext(context) {
|
|
2561
|
+
function isEligibleDirectOpenAIContext(context, entry) {
|
|
2407
2562
|
const request = context?.request;
|
|
2408
2563
|
const response = context?.response;
|
|
2409
2564
|
if (context?.baseUrl !== void 0 && context.baseUrl !== CANONICAL_OPENAI_BASE_URL) return false;
|
|
2410
|
-
if (request && hasUnmodeledDirectOpenAIContent(request)) return false;
|
|
2411
2565
|
const tier = response?.service_tier ?? response?.serviceTier ?? request?.service_tier ?? request?.serviceTier;
|
|
2412
2566
|
if (tier !== void 0 && (typeof tier !== "string" || !["standard", "default"].includes(tier.toLowerCase()))) {
|
|
2413
2567
|
return false;
|
|
@@ -2422,7 +2576,14 @@ function isEligibleDirectOpenAIContext(context) {
|
|
|
2422
2576
|
if (reasoning !== null && typeof reasoning === "object" && "mode" in reasoning && reasoning.mode !== void 0) {
|
|
2423
2577
|
return false;
|
|
2424
2578
|
}
|
|
2425
|
-
|
|
2579
|
+
const audioOutputRated = entry.audio?.output !== void 0;
|
|
2580
|
+
if (request?.modalities !== void 0 && (!Array.isArray(request.modalities) || request.modalities.length === 0 || request.modalities.some(
|
|
2581
|
+
(modality) => modality !== "text" && !(modality === "audio" && audioOutputRated)
|
|
2582
|
+
))) {
|
|
2583
|
+
return false;
|
|
2584
|
+
}
|
|
2585
|
+
if (request?.audio !== void 0 && !audioOutputRated) return false;
|
|
2586
|
+
if (["image_generation", "web_search_options"].some((key) => request?.[key] !== void 0)) {
|
|
2426
2587
|
return false;
|
|
2427
2588
|
}
|
|
2428
2589
|
if (Array.isArray(request?.tools) && request.tools.some(
|
|
@@ -2433,23 +2594,42 @@ function isEligibleDirectOpenAIContext(context) {
|
|
|
2433
2594
|
return true;
|
|
2434
2595
|
}
|
|
2435
2596
|
function estimateDirectOpenAICost(model, usage, context) {
|
|
2436
|
-
if (!isEligibleDirectOpenAIContext(context)) return void 0;
|
|
2437
2597
|
const entry = directOpenAIModel(model);
|
|
2438
2598
|
if (!entry) return void 0;
|
|
2439
|
-
const
|
|
2599
|
+
const content = classifyDirectOpenAIContent(context?.request);
|
|
2600
|
+
if (content === "unmodeled") return void 0;
|
|
2601
|
+
if (!isEligibleDirectOpenAIContext(context, entry)) return void 0;
|
|
2602
|
+
const {
|
|
2603
|
+
prompt_tokens,
|
|
2604
|
+
completion_tokens,
|
|
2605
|
+
cached_tokens,
|
|
2606
|
+
cache_write_tokens,
|
|
2607
|
+
audio_input_tokens,
|
|
2608
|
+
audio_output_tokens
|
|
2609
|
+
} = usage;
|
|
2610
|
+
if (content === "audio" && audio_input_tokens === void 0) return void 0;
|
|
2440
2611
|
const cached = cached_tokens ?? 0;
|
|
2441
2612
|
const cacheWrite = cache_write_tokens ?? 0;
|
|
2442
|
-
|
|
2613
|
+
const audioInput = audio_input_tokens ?? 0;
|
|
2614
|
+
const audioOutput = audio_output_tokens ?? 0;
|
|
2615
|
+
if (![prompt_tokens, completion_tokens, cached, cacheWrite, audioInput, audioOutput].every(
|
|
2443
2616
|
(count) => Number.isSafeInteger(count) && count >= 0
|
|
2444
|
-
) || cached + cacheWrite > prompt_tokens) {
|
|
2617
|
+
) || cached + cacheWrite + audioInput > prompt_tokens || audioOutput > completion_tokens) {
|
|
2445
2618
|
return void 0;
|
|
2446
2619
|
}
|
|
2447
|
-
|
|
2620
|
+
if (audioInput > 0 && entry.audio === void 0 || audioOutput > 0 && entry.audio?.output === void 0) {
|
|
2621
|
+
return void 0;
|
|
2622
|
+
}
|
|
2623
|
+
if (audioInput > 0 && cached + cacheWrite > 0) return void 0;
|
|
2624
|
+
const crossesLongContext = entry.long !== void 0 && entry.contextBoundary !== void 0 && prompt_tokens > entry.contextBoundary;
|
|
2625
|
+
if (crossesLongContext && (audioInput > 0 || audioOutput > 0)) return void 0;
|
|
2626
|
+
const rates = crossesLongContext ? entry.long : entry.short;
|
|
2448
2627
|
if (cached > 0 && rates.cachedInput === void 0 || cacheWrite > 0 && rates.cacheWrite === void 0) {
|
|
2449
2628
|
return void 0;
|
|
2450
2629
|
}
|
|
2451
|
-
const ordinary = prompt_tokens - cached - cacheWrite;
|
|
2452
|
-
|
|
2630
|
+
const ordinary = prompt_tokens - cached - cacheWrite - audioInput;
|
|
2631
|
+
const textOutput = completion_tokens - audioOutput;
|
|
2632
|
+
return ordinary * rates.input + cached * (rates.cachedInput ?? 0) + cacheWrite * (rates.cacheWrite ?? 0) + audioInput * (entry.audio?.input ?? 0) + textOutput * rates.output + audioOutput * (entry.audio?.output ?? 0);
|
|
2453
2633
|
}
|
|
2454
2634
|
function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
|
|
2455
2635
|
return estimateDirectOpenAICost(model, {
|
|
@@ -2539,6 +2719,14 @@ var OPENAI_PROFILE = {
|
|
|
2539
2719
|
envBaseUrl: "OPENAI_BASE_URL",
|
|
2540
2720
|
pricing: { kind: "table", table: OPENAI_PRICING, match: "exact" },
|
|
2541
2721
|
reasoning: { emit: openaiReasoningEmit, capture: "none" },
|
|
2722
|
+
capabilities: {
|
|
2723
|
+
// Chat Completions carries recorded audio via `input_audio` (wav/mp3 only).
|
|
2724
|
+
// Images are deliberately NOT declared: the Responses API serves images, and
|
|
2725
|
+
// Chat-Completions image pricing is unmodeled here (fork F4).
|
|
2726
|
+
inputModalities: {
|
|
2727
|
+
audio: { sources: ["bytes", "base64"], formats: OPENAI_CHAT_AUDIO_FORMATS }
|
|
2728
|
+
}
|
|
2729
|
+
},
|
|
2542
2730
|
roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
|
|
2543
2731
|
maxTokensField: "max_completion_tokens",
|
|
2544
2732
|
parallelToolCalls: (model) => !isOSeriesModel(model)
|
|
@@ -2567,19 +2755,37 @@ function base64FromSource(source) {
|
|
|
2567
2755
|
"base64"
|
|
2568
2756
|
);
|
|
2569
2757
|
}
|
|
2570
|
-
function responseImageParts(parts) {
|
|
2758
|
+
function responseImageParts(parts, model) {
|
|
2571
2759
|
const mapped = [];
|
|
2572
2760
|
for (const part of parts) {
|
|
2573
2761
|
if (part.type === "text") {
|
|
2574
2762
|
mapped.push({ type: "input_text", text: part.text });
|
|
2575
2763
|
continue;
|
|
2576
2764
|
}
|
|
2765
|
+
if (part.type === "audio") {
|
|
2766
|
+
throw new UnsupportedModelInputError({
|
|
2767
|
+
provider: "openai-responses",
|
|
2768
|
+
model,
|
|
2769
|
+
modality: "audio",
|
|
2770
|
+
source: part.source.type,
|
|
2771
|
+
feature: "audio input"
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
if (part.type !== "image") {
|
|
2775
|
+
const unmapped = part;
|
|
2776
|
+
throw new UnsupportedModelInputError({
|
|
2777
|
+
provider: "openai-responses",
|
|
2778
|
+
model,
|
|
2779
|
+
modality: unmapped.type,
|
|
2780
|
+
feature: "this input modality"
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2577
2783
|
const { source } = part;
|
|
2578
2784
|
if (source.type === "provider-file") {
|
|
2579
2785
|
if (source.provider !== "openai-responses") {
|
|
2580
2786
|
throw new UnsupportedModelInputError({
|
|
2581
2787
|
provider: "openai-responses",
|
|
2582
|
-
model
|
|
2788
|
+
model,
|
|
2583
2789
|
modality: "image",
|
|
2584
2790
|
source: "provider-file"
|
|
2585
2791
|
});
|
|
@@ -2597,6 +2803,21 @@ function responseImageParts(parts) {
|
|
|
2597
2803
|
}
|
|
2598
2804
|
return mapped;
|
|
2599
2805
|
}
|
|
2806
|
+
function toResponsesUsage(raw) {
|
|
2807
|
+
if (!raw) return void 0;
|
|
2808
|
+
const audioInput = reportedTokenCount(raw.input_tokens_details?.audio_tokens);
|
|
2809
|
+
const audioOutput = reportedTokenCount(raw.output_tokens_details?.audio_tokens);
|
|
2810
|
+
return {
|
|
2811
|
+
prompt_tokens: raw.input_tokens,
|
|
2812
|
+
completion_tokens: raw.output_tokens,
|
|
2813
|
+
total_tokens: raw.total_tokens,
|
|
2814
|
+
reasoning_tokens: raw.output_tokens_details?.reasoning_tokens,
|
|
2815
|
+
cached_tokens: raw.input_tokens_details?.cached_tokens,
|
|
2816
|
+
cache_write_tokens: raw.input_tokens_details?.cache_write_tokens,
|
|
2817
|
+
...audioInput !== void 0 ? { audio_input_tokens: audioInput } : {},
|
|
2818
|
+
...audioOutput !== void 0 ? { audio_output_tokens: audioOutput } : {}
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2600
2821
|
var OpenAIResponsesProvider = class {
|
|
2601
2822
|
name = "openai-responses";
|
|
2602
2823
|
reportsRequestLifecycle = true;
|
|
@@ -2612,15 +2833,18 @@ var OpenAIResponsesProvider = class {
|
|
|
2612
2833
|
validateInput(request) {
|
|
2613
2834
|
const modelOverride = request.providerOptions?.model;
|
|
2614
2835
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
2615
|
-
const
|
|
2836
|
+
const failWith = (modality, source, feature) => {
|
|
2616
2837
|
throw new UnsupportedModelInputError({
|
|
2617
2838
|
provider: this.name,
|
|
2618
2839
|
model: effectiveModel || request.model,
|
|
2619
|
-
modality
|
|
2840
|
+
modality,
|
|
2620
2841
|
...source ? { source } : {},
|
|
2621
2842
|
...feature ? { feature } : {}
|
|
2622
2843
|
});
|
|
2623
2844
|
};
|
|
2845
|
+
const fail = (source, feature) => failWith("image", source, feature);
|
|
2846
|
+
const audioPart = firstRichPart(request.input, request.history, "audio");
|
|
2847
|
+
if (audioPart) failWith("audio", audioPart.source.type, "audio input");
|
|
2624
2848
|
if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
|
|
2625
2849
|
fail(void 0, "invalid model providerOptions");
|
|
2626
2850
|
}
|
|
@@ -2767,7 +2991,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2767
2991
|
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
2768
2992
|
const body = {
|
|
2769
2993
|
model: effectiveModel,
|
|
2770
|
-
input: this.buildInput(nonSystemMessages),
|
|
2994
|
+
input: this.buildInput(nonSystemMessages, effectiveModel),
|
|
2771
2995
|
store: false,
|
|
2772
2996
|
stream
|
|
2773
2997
|
};
|
|
@@ -2819,7 +3043,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2819
3043
|
// ---------------------------------------------------------------------------
|
|
2820
3044
|
// Internal: message → input mapping
|
|
2821
3045
|
// ---------------------------------------------------------------------------
|
|
2822
|
-
buildInput(messages) {
|
|
3046
|
+
buildInput(messages, model) {
|
|
2823
3047
|
const input = [];
|
|
2824
3048
|
for (const msg of messages) {
|
|
2825
3049
|
const text = typeof msg.content === "string" ? msg.content : "";
|
|
@@ -2857,7 +3081,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2857
3081
|
input.push({
|
|
2858
3082
|
type: "message",
|
|
2859
3083
|
role: msg.role,
|
|
2860
|
-
content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content)
|
|
3084
|
+
content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content, model)
|
|
2861
3085
|
});
|
|
2862
3086
|
}
|
|
2863
3087
|
}
|
|
@@ -2916,14 +3140,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2916
3140
|
}
|
|
2917
3141
|
}
|
|
2918
3142
|
}
|
|
2919
|
-
const usage = json.usage
|
|
2920
|
-
prompt_tokens: json.usage.input_tokens,
|
|
2921
|
-
completion_tokens: json.usage.output_tokens,
|
|
2922
|
-
total_tokens: json.usage.total_tokens,
|
|
2923
|
-
reasoning_tokens: json.usage.output_tokens_details?.reasoning_tokens,
|
|
2924
|
-
cached_tokens: json.usage.input_tokens_details?.cached_tokens,
|
|
2925
|
-
cache_write_tokens: json.usage.input_tokens_details?.cache_write_tokens
|
|
2926
|
-
} : void 0;
|
|
3143
|
+
const usage = toResponsesUsage(json.usage);
|
|
2927
3144
|
const cost = usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(json.model ?? model, usage, {
|
|
2928
3145
|
baseUrl: this.baseUrl,
|
|
2929
3146
|
request,
|
|
@@ -3030,14 +3247,7 @@ var OpenAIResponsesProvider = class {
|
|
|
3030
3247
|
}
|
|
3031
3248
|
case "response.completed": {
|
|
3032
3249
|
const response = data.response;
|
|
3033
|
-
const usage = response?.usage
|
|
3034
|
-
prompt_tokens: response.usage.input_tokens,
|
|
3035
|
-
completion_tokens: response.usage.output_tokens,
|
|
3036
|
-
total_tokens: response.usage.total_tokens,
|
|
3037
|
-
reasoning_tokens: response.usage.output_tokens_details?.reasoning_tokens,
|
|
3038
|
-
cached_tokens: response.usage.input_tokens_details?.cached_tokens,
|
|
3039
|
-
cache_write_tokens: response.usage.input_tokens_details?.cache_write_tokens
|
|
3040
|
-
} : void 0;
|
|
3250
|
+
const usage = toResponsesUsage(response?.usage);
|
|
3041
3251
|
const reasoningItems = response?.output?.filter((item) => item.type === "reasoning") ?? [];
|
|
3042
3252
|
const providerMetadata = reasoningItems.length > 0 ? { openaiReasoningItems: reasoningItems } : void 0;
|
|
3043
3253
|
return {
|
|
@@ -3086,19 +3296,37 @@ function anthropicBase64(source) {
|
|
|
3086
3296
|
"base64"
|
|
3087
3297
|
);
|
|
3088
3298
|
}
|
|
3089
|
-
function anthropicImageBlocks(parts) {
|
|
3299
|
+
function anthropicImageBlocks(parts, model) {
|
|
3090
3300
|
const blocks = [];
|
|
3091
3301
|
for (const part of parts) {
|
|
3092
3302
|
if (part.type === "text") {
|
|
3093
3303
|
blocks.push({ type: "text", text: part.text });
|
|
3094
3304
|
continue;
|
|
3095
3305
|
}
|
|
3306
|
+
if (part.type === "audio") {
|
|
3307
|
+
throw new UnsupportedModelInputError({
|
|
3308
|
+
provider: "anthropic",
|
|
3309
|
+
model,
|
|
3310
|
+
modality: "audio",
|
|
3311
|
+
source: part.source.type,
|
|
3312
|
+
feature: "audio input"
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
if (part.type !== "image") {
|
|
3316
|
+
const unmapped = part;
|
|
3317
|
+
throw new UnsupportedModelInputError({
|
|
3318
|
+
provider: "anthropic",
|
|
3319
|
+
model,
|
|
3320
|
+
modality: unmapped.type,
|
|
3321
|
+
feature: "this input modality"
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3096
3324
|
const { source } = part;
|
|
3097
3325
|
if (source.type === "provider-file") {
|
|
3098
3326
|
if (source.provider !== "anthropic") {
|
|
3099
3327
|
throw new UnsupportedModelInputError({
|
|
3100
3328
|
provider: "anthropic",
|
|
3101
|
-
model
|
|
3329
|
+
model,
|
|
3102
3330
|
modality: "image",
|
|
3103
3331
|
source: "provider-file"
|
|
3104
3332
|
});
|
|
@@ -3560,15 +3788,18 @@ var AnthropicProvider = class {
|
|
|
3560
3788
|
validateInput(request) {
|
|
3561
3789
|
const modelOverride = request.providerOptions?.model;
|
|
3562
3790
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
3563
|
-
const
|
|
3791
|
+
const failWith = (modality, source, feature) => {
|
|
3564
3792
|
throw new UnsupportedModelInputError({
|
|
3565
3793
|
provider: this.name,
|
|
3566
3794
|
model: effectiveModel || request.model,
|
|
3567
|
-
modality
|
|
3795
|
+
modality,
|
|
3568
3796
|
...source ? { source } : {},
|
|
3569
3797
|
...feature ? { feature } : {}
|
|
3570
3798
|
});
|
|
3571
3799
|
};
|
|
3800
|
+
const fail = (source, feature) => failWith("image", source, feature);
|
|
3801
|
+
const audioPart = firstRichPart(request.input, request.history, "audio");
|
|
3802
|
+
if (audioPart) failWith("audio", audioPart.source.type, "audio input");
|
|
3572
3803
|
if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
|
|
3573
3804
|
fail(void 0, "invalid model providerOptions");
|
|
3574
3805
|
}
|
|
@@ -3739,7 +3970,7 @@ var AnthropicProvider = class {
|
|
|
3739
3970
|
const systemText = systemMessages.map((m) => m.content).join("\n\n");
|
|
3740
3971
|
const body = {
|
|
3741
3972
|
model: effectiveModel,
|
|
3742
|
-
messages: this.mapMessages(nonSystemMessages),
|
|
3973
|
+
messages: this.mapMessages(nonSystemMessages, effectiveModel),
|
|
3743
3974
|
max_tokens: options.maxTokens ?? 4096,
|
|
3744
3975
|
stream
|
|
3745
3976
|
};
|
|
@@ -3806,7 +4037,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3806
4037
|
* - assistant messages with tool_calls -> assistant with tool_use content blocks
|
|
3807
4038
|
* - tool messages (tool results) -> user messages with tool_result content blocks
|
|
3808
4039
|
*/
|
|
3809
|
-
mapMessages(messages) {
|
|
4040
|
+
mapMessages(messages, model) {
|
|
3810
4041
|
const result = [];
|
|
3811
4042
|
for (const msg of messages) {
|
|
3812
4043
|
const text = typeof msg.content === "string" ? msg.content : "";
|
|
@@ -3853,7 +4084,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3853
4084
|
} else if (msg.role === "user") {
|
|
3854
4085
|
result.push({
|
|
3855
4086
|
role: "user",
|
|
3856
|
-
content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content)
|
|
4087
|
+
content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content, model)
|
|
3857
4088
|
});
|
|
3858
4089
|
}
|
|
3859
4090
|
}
|
|
@@ -4157,7 +4388,7 @@ function lastFallbackIndex(blocks) {
|
|
|
4157
4388
|
function hasRichGeminiMessages(messages) {
|
|
4158
4389
|
return messages.some((message) => Array.isArray(message.content));
|
|
4159
4390
|
}
|
|
4160
|
-
function
|
|
4391
|
+
function geminiInlineBase64(source) {
|
|
4161
4392
|
return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
|
|
4162
4393
|
"base64"
|
|
4163
4394
|
);
|
|
@@ -4169,6 +4400,50 @@ function geminiInteractionContent(parts, model) {
|
|
|
4169
4400
|
content.push({ type: "text", text: part.text });
|
|
4170
4401
|
continue;
|
|
4171
4402
|
}
|
|
4403
|
+
if (part.type === "audio") {
|
|
4404
|
+
const { source: audioSource } = part;
|
|
4405
|
+
if (audioSource.type === "provider-file") {
|
|
4406
|
+
if (audioSource.provider !== "google") {
|
|
4407
|
+
throw new UnsupportedModelInputError({
|
|
4408
|
+
provider: "google",
|
|
4409
|
+
model,
|
|
4410
|
+
modality: "audio",
|
|
4411
|
+
source: "provider-file"
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
if (!audioSource.mediaType) {
|
|
4415
|
+
throw new UnsupportedModelInputError({
|
|
4416
|
+
provider: "google",
|
|
4417
|
+
model,
|
|
4418
|
+
modality: "audio",
|
|
4419
|
+
source: "provider-file",
|
|
4420
|
+
feature: "Interactions URI audio mediaType"
|
|
4421
|
+
});
|
|
4422
|
+
}
|
|
4423
|
+
content.push({
|
|
4424
|
+
type: "audio",
|
|
4425
|
+
uri: audioSource.reference,
|
|
4426
|
+
mime_type: audioSource.mediaType
|
|
4427
|
+
});
|
|
4428
|
+
} else {
|
|
4429
|
+
content.push({
|
|
4430
|
+
type: "audio",
|
|
4431
|
+
data: geminiInlineBase64(audioSource),
|
|
4432
|
+
mime_type: audioSource.mediaType
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
4435
|
+
if (part.label) content.push({ type: "text", text: `[Audio: ${part.label}]` });
|
|
4436
|
+
continue;
|
|
4437
|
+
}
|
|
4438
|
+
if (part.type !== "image") {
|
|
4439
|
+
const unmapped = part;
|
|
4440
|
+
throw new UnsupportedModelInputError({
|
|
4441
|
+
provider: "google",
|
|
4442
|
+
model,
|
|
4443
|
+
modality: unmapped.type,
|
|
4444
|
+
feature: "this input modality"
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4172
4447
|
const { source } = part;
|
|
4173
4448
|
if (source.type === "provider-file") {
|
|
4174
4449
|
if (source.provider !== "google") {
|
|
@@ -4202,7 +4477,11 @@ function geminiInteractionContent(parts, model) {
|
|
|
4202
4477
|
feature: "direct URL image input; pass bytes/base64 or a Gemini provider-file"
|
|
4203
4478
|
});
|
|
4204
4479
|
} else {
|
|
4205
|
-
content.push({
|
|
4480
|
+
content.push({
|
|
4481
|
+
type: "image",
|
|
4482
|
+
data: geminiInlineBase64(source),
|
|
4483
|
+
mime_type: source.mediaType
|
|
4484
|
+
});
|
|
4206
4485
|
}
|
|
4207
4486
|
if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
|
|
4208
4487
|
}
|
|
@@ -4259,7 +4538,7 @@ var GEMINI_PRICING = {
|
|
|
4259
4538
|
output: 1e-5,
|
|
4260
4539
|
longContext: { input: 25e-7, cached: 25e-8, output: 15e-6 }
|
|
4261
4540
|
},
|
|
4262
|
-
"gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
4541
|
+
"gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7, audioInput: 1e-6 },
|
|
4263
4542
|
"gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
|
|
4264
4543
|
"gemini-3.1-pro-preview": {
|
|
4265
4544
|
input: 2e-6,
|
|
@@ -4277,18 +4556,43 @@ var GEMINI_PRICING = {
|
|
|
4277
4556
|
"gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
|
|
4278
4557
|
"gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
4279
4558
|
"gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
|
|
4280
|
-
"gemini-3.7-flash": {
|
|
4559
|
+
"gemini-3.7-flash": {
|
|
4560
|
+
input: 75e-8,
|
|
4561
|
+
cached: 75e-9,
|
|
4562
|
+
output: 375e-8,
|
|
4563
|
+
audioInput: 75e-8
|
|
4564
|
+
},
|
|
4281
4565
|
"gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
|
|
4282
4566
|
};
|
|
4567
|
+
var CANONICAL_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
4283
4568
|
function isValidTokenCount2(value) {
|
|
4284
4569
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
4285
4570
|
}
|
|
4286
4571
|
function isStandardGeminiRequestTier(value) {
|
|
4287
|
-
return value === void 0 || value ===
|
|
4572
|
+
return value === void 0 || value === null || isDefinitiveStandardGeminiResponseTier(value);
|
|
4288
4573
|
}
|
|
4289
4574
|
function isDefinitiveStandardGeminiResponseTier(value) {
|
|
4290
4575
|
return value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD" || value === "standard";
|
|
4291
4576
|
}
|
|
4577
|
+
function recordGeminiResponseTier(evidence, value) {
|
|
4578
|
+
if (value === void 0) return;
|
|
4579
|
+
evidence.reportsTier = true;
|
|
4580
|
+
if (isDefinitiveStandardGeminiResponseTier(value)) {
|
|
4581
|
+
evidence.hasDefinitiveStandardTier = true;
|
|
4582
|
+
} else {
|
|
4583
|
+
evidence.hasInvalidTier = true;
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
function geminiResponseTierEvidence(responseHeaders, ...values) {
|
|
4587
|
+
const evidence = {
|
|
4588
|
+
reportsTier: false,
|
|
4589
|
+
hasDefinitiveStandardTier: false,
|
|
4590
|
+
hasInvalidTier: false
|
|
4591
|
+
};
|
|
4592
|
+
recordGeminiResponseTier(evidence, responseHeaders?.get("x-gemini-service-tier") ?? void 0);
|
|
4593
|
+
for (const value of values) recordGeminiResponseTier(evidence, value);
|
|
4594
|
+
return evidence;
|
|
4595
|
+
}
|
|
4292
4596
|
function geminiInteractionErrorStatus(code) {
|
|
4293
4597
|
if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) {
|
|
4294
4598
|
return code;
|
|
@@ -4347,8 +4651,19 @@ function estimateGeminiCost(model, usage) {
|
|
|
4347
4651
|
if (!pricing || !isValidTokenCount2(usage.inputTokens) || !isValidTokenCount2(usage.outputTokens) || !isValidTokenCount2(cached) || cached > usage.inputTokens) {
|
|
4348
4652
|
return void 0;
|
|
4349
4653
|
}
|
|
4350
|
-
const
|
|
4351
|
-
|
|
4654
|
+
const split = usage.modalities;
|
|
4655
|
+
const audio = split?.audioTokens ?? 0;
|
|
4656
|
+
if (split !== void 0) {
|
|
4657
|
+
if (audio > 0 && pricing.audioInput === void 0) return void 0;
|
|
4658
|
+
if (!isValidTokenCount2(audio) || !isValidTokenCount2(split.nonAudioTokens) || audio + split.nonAudioTokens !== usage.inputTokens || cached + audio > usage.inputTokens) {
|
|
4659
|
+
return void 0;
|
|
4660
|
+
}
|
|
4661
|
+
if (audio > 0 && cached > 0) return void 0;
|
|
4662
|
+
}
|
|
4663
|
+
const crossesLongContext = usage.inputTokens > 2e5 && pricing.longContext !== void 0;
|
|
4664
|
+
if (crossesLongContext && audio > 0) return void 0;
|
|
4665
|
+
const rate = crossesLongContext ? pricing.longContext : pricing;
|
|
4666
|
+
return (usage.inputTokens - cached - audio) * rate.input + cached * rate.cached + audio * (pricing.audioInput ?? 0) + usage.outputTokens * rate.output;
|
|
4352
4667
|
}
|
|
4353
4668
|
function safeTokenSum2(...values) {
|
|
4354
4669
|
const total = values.reduce((sum, value) => sum + value, 0);
|
|
@@ -4508,20 +4823,44 @@ var GeminiProvider = class {
|
|
|
4508
4823
|
name = "google";
|
|
4509
4824
|
reportsRequestLifecycle = true;
|
|
4510
4825
|
inputCapabilities(model) {
|
|
4511
|
-
return model.trim().length > 0 ? {
|
|
4826
|
+
return model.trim().length > 0 ? {
|
|
4827
|
+
image: { sources: ["bytes", "base64", "provider-file"] },
|
|
4828
|
+
audio: { sources: ["bytes", "base64", "provider-file"] }
|
|
4829
|
+
} : {};
|
|
4512
4830
|
}
|
|
4513
4831
|
validateInput(request) {
|
|
4514
4832
|
const modelOverride = request.providerOptions?.model;
|
|
4515
4833
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
4516
|
-
const
|
|
4834
|
+
const failWith = (modality, source, feature) => {
|
|
4517
4835
|
throw new UnsupportedModelInputError({
|
|
4518
4836
|
provider: this.name,
|
|
4519
4837
|
model: effectiveModel || request.model,
|
|
4520
|
-
modality
|
|
4838
|
+
modality,
|
|
4521
4839
|
...source ? { source } : {},
|
|
4522
4840
|
...feature ? { feature } : {}
|
|
4523
4841
|
});
|
|
4524
4842
|
};
|
|
4843
|
+
const fail = (source, feature) => failWith(firstRichPart(request.input, request.history)?.type ?? "image", source, feature);
|
|
4844
|
+
const checkPart = (part) => {
|
|
4845
|
+
if (part.type === "audio") {
|
|
4846
|
+
if (part.source.type === "provider-file" && part.source.provider !== this.name)
|
|
4847
|
+
failWith("audio", "provider-file");
|
|
4848
|
+
if (part.source.type === "provider-file" && !part.source.mediaType)
|
|
4849
|
+
failWith("audio", "provider-file", "Interactions URI audio mediaType");
|
|
4850
|
+
return;
|
|
4851
|
+
}
|
|
4852
|
+
if (part.type !== "image") return;
|
|
4853
|
+
if (part.source.type === "url")
|
|
4854
|
+
failWith(
|
|
4855
|
+
"image",
|
|
4856
|
+
"url",
|
|
4857
|
+
"direct URL image input; pass bytes/base64 or a Gemini provider-file"
|
|
4858
|
+
);
|
|
4859
|
+
if (part.source.type === "provider-file" && part.source.provider !== this.name)
|
|
4860
|
+
failWith("image", "provider-file");
|
|
4861
|
+
if (part.source.type === "provider-file" && !part.source.mediaType)
|
|
4862
|
+
failWith("image", "provider-file", "Interactions URI image mediaType");
|
|
4863
|
+
};
|
|
4525
4864
|
if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
|
|
4526
4865
|
fail(void 0, "invalid model providerOptions");
|
|
4527
4866
|
}
|
|
@@ -4543,26 +4882,10 @@ var GeminiProvider = class {
|
|
|
4543
4882
|
for (const message of request.history) {
|
|
4544
4883
|
if (!Array.isArray(message.content)) continue;
|
|
4545
4884
|
if (message.role !== "user") fail(void 0, "rich non-user history");
|
|
4546
|
-
for (const part of message.content)
|
|
4547
|
-
if (part.type !== "image") continue;
|
|
4548
|
-
if (part.source.type === "url")
|
|
4549
|
-
fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
|
|
4550
|
-
if (part.source.type === "provider-file" && part.source.provider !== this.name)
|
|
4551
|
-
fail("provider-file");
|
|
4552
|
-
if (part.source.type === "provider-file" && !part.source.mediaType)
|
|
4553
|
-
fail("provider-file", "Interactions URI image mediaType");
|
|
4554
|
-
}
|
|
4885
|
+
for (const part of message.content) checkPart(part);
|
|
4555
4886
|
}
|
|
4556
4887
|
if (Array.isArray(request.input)) {
|
|
4557
|
-
for (const part of request.input)
|
|
4558
|
-
if (part.type !== "image") continue;
|
|
4559
|
-
if (part.source.type === "url")
|
|
4560
|
-
fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
|
|
4561
|
-
if (part.source.type === "provider-file" && part.source.provider !== this.name)
|
|
4562
|
-
fail("provider-file");
|
|
4563
|
-
if (part.source.type === "provider-file" && !part.source.mediaType)
|
|
4564
|
-
fail("provider-file", "Interactions URI image mediaType");
|
|
4565
|
-
}
|
|
4888
|
+
for (const part of request.input) checkPart(part);
|
|
4566
4889
|
}
|
|
4567
4890
|
return { effectiveModel };
|
|
4568
4891
|
}
|
|
@@ -4698,6 +5021,7 @@ var GeminiProvider = class {
|
|
|
4698
5021
|
async chatInteraction(messages, options) {
|
|
4699
5022
|
this.assertSafeInteractionOptions(options);
|
|
4700
5023
|
const body = this.buildInteractionRequestBody(messages, options, false);
|
|
5024
|
+
const pricingContext = this.interactionPricingContext(this.requestModel(options), body);
|
|
4701
5025
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4702
5026
|
const recorder = new CallTimingRecorder(options.requestLifecycle);
|
|
4703
5027
|
const res = await fetchWithRetry(
|
|
@@ -4718,12 +5042,17 @@ var GeminiProvider = class {
|
|
|
4718
5042
|
}
|
|
4719
5043
|
return withChatTiming(
|
|
4720
5044
|
recorder,
|
|
4721
|
-
this.parseInteractionResponse(
|
|
5045
|
+
this.parseInteractionResponse(
|
|
5046
|
+
await res.json(),
|
|
5047
|
+
pricingContext,
|
|
5048
|
+
res.headers
|
|
5049
|
+
)
|
|
4722
5050
|
);
|
|
4723
5051
|
}
|
|
4724
5052
|
async *streamInteraction(messages, options) {
|
|
4725
5053
|
this.assertSafeInteractionOptions(options);
|
|
4726
5054
|
const body = this.buildInteractionRequestBody(messages, options, true);
|
|
5055
|
+
const pricingContext = this.interactionPricingContext(this.requestModel(options), body);
|
|
4727
5056
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4728
5057
|
const recorder = new CallTimingRecorder(options.requestLifecycle);
|
|
4729
5058
|
const res = await fetchWithRetry(
|
|
@@ -4745,7 +5074,7 @@ var GeminiProvider = class {
|
|
|
4745
5074
|
if (!res.body) throw new Error("Gemini Interactions stream has no body");
|
|
4746
5075
|
yield* withCallTiming(
|
|
4747
5076
|
recorder,
|
|
4748
|
-
this.parseInteractionSSEStream(res.body, res.headers, recorder)
|
|
5077
|
+
this.parseInteractionSSEStream(res.body, res.headers, recorder, pricingContext)
|
|
4749
5078
|
);
|
|
4750
5079
|
}
|
|
4751
5080
|
buildInteractionRequestBody(messages, options, stream) {
|
|
@@ -4840,6 +5169,7 @@ var GeminiProvider = class {
|
|
|
4840
5169
|
}
|
|
4841
5170
|
mapInteractionInput(messages, model) {
|
|
4842
5171
|
const steps = [];
|
|
5172
|
+
const functionNames = /* @__PURE__ */ new Map();
|
|
4843
5173
|
for (const message of messages) {
|
|
4844
5174
|
if (message.role === "user") {
|
|
4845
5175
|
steps.push({
|
|
@@ -4849,6 +5179,10 @@ var GeminiProvider = class {
|
|
|
4849
5179
|
} else if (message.role === "assistant") {
|
|
4850
5180
|
const nativeSteps = message.providerMetadata?.geminiInteractionSteps;
|
|
4851
5181
|
if (Array.isArray(nativeSteps) && nativeSteps.every(isGeminiInteractionStep)) {
|
|
5182
|
+
for (const step of nativeSteps) {
|
|
5183
|
+
if (step.type === "function_call" && typeof step.id === "string" && step.name)
|
|
5184
|
+
functionNames.set(step.id, step.name);
|
|
5185
|
+
}
|
|
4852
5186
|
steps.push(...nativeSteps);
|
|
4853
5187
|
continue;
|
|
4854
5188
|
}
|
|
@@ -4856,6 +5190,7 @@ var GeminiProvider = class {
|
|
|
4856
5190
|
steps.push({ type: "model_output", content: [{ type: "text", text: message.content }] });
|
|
4857
5191
|
}
|
|
4858
5192
|
for (const call of message.tool_calls ?? []) {
|
|
5193
|
+
functionNames.set(call.id, call.function.name);
|
|
4859
5194
|
steps.push({
|
|
4860
5195
|
type: "function_call",
|
|
4861
5196
|
id: call.id,
|
|
@@ -4864,9 +5199,17 @@ var GeminiProvider = class {
|
|
|
4864
5199
|
});
|
|
4865
5200
|
}
|
|
4866
5201
|
} else if (message.role === "tool") {
|
|
5202
|
+
const callId = message.tool_call_id ?? "";
|
|
5203
|
+
const name = functionNames.get(callId) ?? message.name;
|
|
5204
|
+
if (!name) {
|
|
5205
|
+
throw new InvalidModelInputError(
|
|
5206
|
+
`Gemini Interactions tool result for call '${callId}' has no function name: no preceding function_call carries that id and the tool message has no 'name'`
|
|
5207
|
+
);
|
|
5208
|
+
}
|
|
4867
5209
|
steps.push({
|
|
4868
5210
|
type: "function_result",
|
|
4869
|
-
|
|
5211
|
+
name,
|
|
5212
|
+
call_id: callId,
|
|
4870
5213
|
result: [
|
|
4871
5214
|
{ type: "text", text: typeof message.content === "string" ? message.content : "" }
|
|
4872
5215
|
]
|
|
@@ -4875,7 +5218,7 @@ var GeminiProvider = class {
|
|
|
4875
5218
|
}
|
|
4876
5219
|
return steps;
|
|
4877
5220
|
}
|
|
4878
|
-
parseInteractionResponse(json) {
|
|
5221
|
+
parseInteractionResponse(json, pricingContext, responseHeaders) {
|
|
4879
5222
|
let content = "";
|
|
4880
5223
|
let thinkingContent = "";
|
|
4881
5224
|
const toolCalls = [];
|
|
@@ -4891,18 +5234,63 @@ var GeminiProvider = class {
|
|
|
4891
5234
|
}
|
|
4892
5235
|
}
|
|
4893
5236
|
this.assertInteractionTerminalStatus(json.status, toolCalls.length);
|
|
4894
|
-
const
|
|
5237
|
+
const normalized = normalizeInteractionUsage(json.usage);
|
|
4895
5238
|
return {
|
|
4896
5239
|
content,
|
|
4897
5240
|
thinking_content: thinkingContent || void 0,
|
|
4898
5241
|
tool_calls: toolCalls.length ? toolCalls : void 0,
|
|
4899
|
-
usage,
|
|
4900
|
-
|
|
4901
|
-
|
|
5242
|
+
usage: normalized?.usage,
|
|
5243
|
+
cost: this.interactionCost({
|
|
5244
|
+
normalized,
|
|
5245
|
+
steps: json.steps,
|
|
5246
|
+
responseTierEvidence: geminiResponseTierEvidence(
|
|
5247
|
+
responseHeaders,
|
|
5248
|
+
json.service_tier,
|
|
5249
|
+
json.usage?.service_tier
|
|
5250
|
+
),
|
|
5251
|
+
effectiveModel: typeof json.model === "string" ? json.model : pricingContext.model,
|
|
5252
|
+
pricingContext,
|
|
5253
|
+
// The whole body is in hand: every part is on `json.steps`, so there
|
|
5254
|
+
// is nothing this path could have failed to materialize.
|
|
5255
|
+
unmodeledOutput: false
|
|
5256
|
+
}),
|
|
4902
5257
|
providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
|
|
4903
5258
|
};
|
|
4904
5259
|
}
|
|
4905
|
-
|
|
5260
|
+
/**
|
|
5261
|
+
* Cost for one Interactions call, or `undefined` when any precondition
|
|
5262
|
+
* fails. Streaming and non-streaming share this one body, so identical usage
|
|
5263
|
+
* always produces an identical number on both paths.
|
|
5264
|
+
*/
|
|
5265
|
+
interactionCost(call) {
|
|
5266
|
+
const { normalized, steps, responseTierEvidence, effectiveModel, pricingContext } = call;
|
|
5267
|
+
if (!normalized?.pricingUsage || normalized.hasUnmodeledBilledUsage) return void 0;
|
|
5268
|
+
if (call.unmodeledOutput || !this.isTextOnlyInteractionOutput(steps)) return void 0;
|
|
5269
|
+
if (!isEligibleGeminiPricing(
|
|
5270
|
+
pricingContext,
|
|
5271
|
+
!responseTierEvidence.reportsTier || responseTierEvidence.hasDefinitiveStandardTier,
|
|
5272
|
+
responseTierEvidence.hasInvalidTier
|
|
5273
|
+
)) {
|
|
5274
|
+
return void 0;
|
|
5275
|
+
}
|
|
5276
|
+
return estimateGeminiCost(effectiveModel, normalized.pricingUsage);
|
|
5277
|
+
}
|
|
5278
|
+
/**
|
|
5279
|
+
* Whether every model-produced step is text. A non-text reply part (image,
|
|
5280
|
+
* audio, inline data) is billed on an output track this adapter carries no
|
|
5281
|
+
* rate for, so the call stays unpriced. Echoed `user_input` and
|
|
5282
|
+
* Axl-authored `function_result` steps are not model output and are ignored.
|
|
5283
|
+
*/
|
|
5284
|
+
isTextOnlyInteractionOutput(steps) {
|
|
5285
|
+
return (steps ?? []).every((step) => {
|
|
5286
|
+
if (step.type !== "model_output" && step.type !== "thought") return true;
|
|
5287
|
+
const parts = [...step.content ?? [], ...step.summary ?? []];
|
|
5288
|
+
return parts.every(
|
|
5289
|
+
(part) => part !== null && typeof part === "object" && part.type === "text"
|
|
5290
|
+
);
|
|
5291
|
+
});
|
|
5292
|
+
}
|
|
5293
|
+
async *parseInteractionSSEStream(body, responseHeaders, timing, pricingContext) {
|
|
4906
5294
|
const reader = body.getReader();
|
|
4907
5295
|
const decoder = new TextDecoder();
|
|
4908
5296
|
let buffer = "";
|
|
@@ -4910,6 +5298,8 @@ var GeminiProvider = class {
|
|
|
4910
5298
|
const argumentBuffers = /* @__PURE__ */ new Map();
|
|
4911
5299
|
const stopped = /* @__PURE__ */ new Set();
|
|
4912
5300
|
let completed = false;
|
|
5301
|
+
const responseTierEvidence = geminiResponseTierEvidence(responseHeaders);
|
|
5302
|
+
let unmodeledOutput = false;
|
|
4913
5303
|
try {
|
|
4914
5304
|
while (true) {
|
|
4915
5305
|
const { done, value } = await timing.read(reader);
|
|
@@ -4927,6 +5317,8 @@ var GeminiProvider = class {
|
|
|
4927
5317
|
} catch {
|
|
4928
5318
|
continue;
|
|
4929
5319
|
}
|
|
5320
|
+
recordGeminiResponseTier(responseTierEvidence, event.interaction?.service_tier);
|
|
5321
|
+
recordGeminiResponseTier(responseTierEvidence, event.interaction?.usage?.service_tier);
|
|
4930
5322
|
if (event.event_type === "error") {
|
|
4931
5323
|
const status = geminiInteractionErrorStatus(event.error?.code);
|
|
4932
5324
|
const message = event.error?.message ?? "Gemini Interactions stream failed";
|
|
@@ -4954,6 +5346,7 @@ var GeminiProvider = class {
|
|
|
4954
5346
|
}
|
|
4955
5347
|
const step = cloneInteractionStep(event.step);
|
|
4956
5348
|
steps.set(index, step);
|
|
5349
|
+
if (!this.isTextOnlyInteractionOutput([step])) unmodeledOutput = true;
|
|
4957
5350
|
if (step.type === "model_output") {
|
|
4958
5351
|
for (const text of interactionTextChunks(step.content)) {
|
|
4959
5352
|
yield { type: "text_delta", content: text };
|
|
@@ -4976,6 +5369,12 @@ var GeminiProvider = class {
|
|
|
4976
5369
|
const step = steps.get(index);
|
|
4977
5370
|
if (!step)
|
|
4978
5371
|
throw new Error("Gemini Interactions stream delta arrived before step.start");
|
|
5372
|
+
if (step.type === "model_output" && !(delta.type === "text" && typeof delta.text === "string")) {
|
|
5373
|
+
unmodeledOutput = true;
|
|
5374
|
+
}
|
|
5375
|
+
if (step.type === "thought" && !(delta.type === "thought_summary" && !!delta.content) && !(delta.type === "thought_signature" && typeof delta.signature === "string")) {
|
|
5376
|
+
unmodeledOutput = true;
|
|
5377
|
+
}
|
|
4979
5378
|
if (delta.type === "text" && typeof delta.text === "string" && step.type === "model_output") {
|
|
4980
5379
|
appendInteractionContent(step, "content", { type: "text", text: delta.text });
|
|
4981
5380
|
yield { type: "text_delta", content: delta.text };
|
|
@@ -5023,12 +5422,20 @@ var GeminiProvider = class {
|
|
|
5023
5422
|
).length;
|
|
5024
5423
|
this.assertInteractionTerminalStatus(event.interaction?.status, toolCallCount);
|
|
5025
5424
|
completed = true;
|
|
5425
|
+
const orderedSteps = [...steps.entries()].sort(([a], [b]) => a - b).map(([, step]) => step);
|
|
5426
|
+
const normalized = normalizeInteractionUsage(event.interaction?.usage);
|
|
5026
5427
|
yield {
|
|
5027
5428
|
type: "done",
|
|
5028
|
-
usage:
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5429
|
+
usage: normalized?.usage,
|
|
5430
|
+
cost: this.interactionCost({
|
|
5431
|
+
normalized,
|
|
5432
|
+
steps: orderedSteps,
|
|
5433
|
+
responseTierEvidence,
|
|
5434
|
+
effectiveModel: typeof event.interaction?.model === "string" ? event.interaction.model : pricingContext.model,
|
|
5435
|
+
pricingContext,
|
|
5436
|
+
unmodeledOutput
|
|
5437
|
+
}),
|
|
5438
|
+
providerMetadata: steps.size ? { geminiInteractionSteps: orderedSteps } : void 0
|
|
5032
5439
|
};
|
|
5033
5440
|
return;
|
|
5034
5441
|
}
|
|
@@ -5070,6 +5477,51 @@ var GeminiProvider = class {
|
|
|
5070
5477
|
}
|
|
5071
5478
|
return `Gemini API error (${status}): ${body}`;
|
|
5072
5479
|
}
|
|
5480
|
+
/**
|
|
5481
|
+
* Pricing context for the stateless Interactions transport.
|
|
5482
|
+
*
|
|
5483
|
+
* The endpoint reports a per-modality input split, so a rich call IS
|
|
5484
|
+
* priceable — but only from the canonical endpoint, on a standard tier, with
|
|
5485
|
+
* a request whose billing is fully represented by the token totals.
|
|
5486
|
+
*/
|
|
5487
|
+
interactionPricingContext(fallbackModel, body) {
|
|
5488
|
+
return {
|
|
5489
|
+
model: typeof body.model === "string" ? body.model : fallbackModel,
|
|
5490
|
+
serviceTier: body.service_tier ?? body.serviceTier,
|
|
5491
|
+
eligibleRequest: this.baseUrl === CANONICAL_GEMINI_BASE_URL && isStandardGeminiRequestTier(body.serviceTier) && this.isModeledInteractionRequest(body)
|
|
5492
|
+
};
|
|
5493
|
+
}
|
|
5494
|
+
/**
|
|
5495
|
+
* Whether an Interactions request body's billing is fully represented by the
|
|
5496
|
+
* reported token totals. `providerOptions` merges arbitrary keys into the
|
|
5497
|
+
* body, so anything that can move billing off the local totals — a
|
|
5498
|
+
* server-side cache, a server-executed tool, a residency/region override, a
|
|
5499
|
+
* non-text response modality — makes the call unpriced.
|
|
5500
|
+
*
|
|
5501
|
+
* Input CONTENT is deliberately not inspected: `input_tokens_by_modality` is
|
|
5502
|
+
* the authority on what the input billed as, and it unprices any modality
|
|
5503
|
+
* without a billing category (video, documents, anything new).
|
|
5504
|
+
*/
|
|
5505
|
+
isModeledInteractionRequest(body) {
|
|
5506
|
+
if ([
|
|
5507
|
+
"cached_content",
|
|
5508
|
+
"cachedContent",
|
|
5509
|
+
"previous_interaction_id",
|
|
5510
|
+
"region",
|
|
5511
|
+
"inference_geo",
|
|
5512
|
+
"data_residency"
|
|
5513
|
+
].some((key) => body[key] !== void 0)) {
|
|
5514
|
+
return false;
|
|
5515
|
+
}
|
|
5516
|
+
if (body.tools !== void 0 && (!Array.isArray(body.tools) || body.tools.some(
|
|
5517
|
+
(tool2) => tool2 === null || typeof tool2 !== "object" || tool2.type !== "function"
|
|
5518
|
+
))) {
|
|
5519
|
+
return false;
|
|
5520
|
+
}
|
|
5521
|
+
const generationConfig = body.generation_config;
|
|
5522
|
+
const modalities = generationConfig !== null && typeof generationConfig === "object" ? generationConfig.response_modalities : void 0;
|
|
5523
|
+
return modalities === void 0 || modalities === "TEXT" || Array.isArray(modalities) && modalities.length === 1 && modalities[0] === "TEXT";
|
|
5524
|
+
}
|
|
5073
5525
|
pricingContext(fallbackModel, body) {
|
|
5074
5526
|
return {
|
|
5075
5527
|
// Gemini's model is normally selected in the URL. providerOptions is an
|
|
@@ -5077,7 +5529,7 @@ var GeminiProvider = class {
|
|
|
5077
5529
|
// the URL and pricing rather than pricing a different request.
|
|
5078
5530
|
model: typeof body.model === "string" ? body.model : fallbackModel,
|
|
5079
5531
|
serviceTier: body.serviceTier,
|
|
5080
|
-
eligibleRequest: this.baseUrl ===
|
|
5532
|
+
eligibleRequest: this.baseUrl === CANONICAL_GEMINI_BASE_URL && this.isTextOnlyClientRequest(body)
|
|
5081
5533
|
};
|
|
5082
5534
|
}
|
|
5083
5535
|
requestModel(options) {
|
|
@@ -5400,7 +5852,7 @@ var GeminiProvider = class {
|
|
|
5400
5852
|
}
|
|
5401
5853
|
const normalized = json.usageMetadata ? normalizeGeminiUsage(json.usageMetadata) : void 0;
|
|
5402
5854
|
const effectiveModel = typeof json.modelVersion === "string" ? json.modelVersion : pricingContext.model;
|
|
5403
|
-
const cost = normalized && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
|
|
5855
|
+
const cost = normalized?.pricingUsage && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
|
|
5404
5856
|
pricingContext,
|
|
5405
5857
|
isDefinitiveStandardGeminiResponseTier(json.usageMetadata?.serviceTier),
|
|
5406
5858
|
json.usageMetadata?.serviceTier !== void 0 && !isDefinitiveStandardGeminiResponseTier(json.usageMetadata.serviceTier)
|
|
@@ -5493,7 +5945,7 @@ var GeminiProvider = class {
|
|
|
5493
5945
|
yield {
|
|
5494
5946
|
type: "done",
|
|
5495
5947
|
usage: normalizedUsage?.usage,
|
|
5496
|
-
cost: normalizedUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
|
|
5948
|
+
cost: normalizedUsage?.pricingUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
|
|
5497
5949
|
pricingContext,
|
|
5498
5950
|
hasDefinitiveStandardResponseTier,
|
|
5499
5951
|
hasInvalidResponseTier
|
|
@@ -5539,20 +5991,73 @@ function safeJsonObject(value) {
|
|
|
5539
5991
|
return {};
|
|
5540
5992
|
}
|
|
5541
5993
|
}
|
|
5994
|
+
var KNOWN_GEMINI_MODALITIES = /* @__PURE__ */ new Set(["text", "image", "audio"]);
|
|
5995
|
+
function foldGeminiModalities(raw, totalInputTokens) {
|
|
5996
|
+
if (!Array.isArray(raw)) return void 0;
|
|
5997
|
+
let audioTokens = 0;
|
|
5998
|
+
let nonAudioTokens = 0;
|
|
5999
|
+
for (const entry of raw) {
|
|
6000
|
+
if (entry === null || typeof entry !== "object") return void 0;
|
|
6001
|
+
const { modality, tokens } = entry;
|
|
6002
|
+
if (typeof modality !== "string" || !isValidTokenCount2(tokens)) return void 0;
|
|
6003
|
+
const normalized = modality.toLowerCase();
|
|
6004
|
+
if (!KNOWN_GEMINI_MODALITIES.has(normalized)) return void 0;
|
|
6005
|
+
if (normalized === "audio") audioTokens += tokens;
|
|
6006
|
+
else nonAudioTokens += tokens;
|
|
6007
|
+
}
|
|
6008
|
+
const total = safeTokenSum2(audioTokens, nonAudioTokens);
|
|
6009
|
+
if (total === void 0 || total !== totalInputTokens) return void 0;
|
|
6010
|
+
return { audioTokens, nonAudioTokens };
|
|
6011
|
+
}
|
|
6012
|
+
function reportedGeminiAudioTokens(raw, totalInputTokens) {
|
|
6013
|
+
if (!Array.isArray(raw)) return void 0;
|
|
6014
|
+
let audioTokens;
|
|
6015
|
+
for (const entry of raw) {
|
|
6016
|
+
if (entry === null || typeof entry !== "object") return void 0;
|
|
6017
|
+
const { modality, tokens } = entry;
|
|
6018
|
+
if (typeof modality !== "string" || !isValidTokenCount2(tokens)) return void 0;
|
|
6019
|
+
if (modality.toLowerCase() === "audio") audioTokens = (audioTokens ?? 0) + tokens;
|
|
6020
|
+
}
|
|
6021
|
+
return audioTokens !== void 0 && audioTokens <= totalInputTokens ? audioTokens : void 0;
|
|
6022
|
+
}
|
|
5542
6023
|
function normalizeInteractionUsage(usage) {
|
|
5543
6024
|
if (!usage || !isValidTokenCount2(usage.total_input_tokens) || !isValidTokenCount2(usage.total_output_tokens) || !isValidTokenCount2(usage.total_tokens)) {
|
|
5544
6025
|
return void 0;
|
|
5545
6026
|
}
|
|
5546
6027
|
const cached = usage.total_cached_tokens;
|
|
5547
6028
|
const thinking = usage.total_thought_tokens;
|
|
5548
|
-
|
|
6029
|
+
const toolUse = usage.total_tool_use_tokens;
|
|
6030
|
+
if (cached !== void 0 && (!isValidTokenCount2(cached) || cached > usage.total_input_tokens) || thinking !== void 0 && !isValidTokenCount2(thinking) || toolUse !== void 0 && !isValidTokenCount2(toolUse))
|
|
5549
6031
|
return void 0;
|
|
6032
|
+
const inputTokens = usage.total_input_tokens;
|
|
6033
|
+
const audioInputTokens = reportedGeminiAudioTokens(usage.input_tokens_by_modality, inputTokens);
|
|
6034
|
+
const modalities = foldGeminiModalities(usage.input_tokens_by_modality, inputTokens);
|
|
6035
|
+
const billedOutputTokens = safeTokenSum2(usage.total_output_tokens, thinking ?? 0);
|
|
6036
|
+
const reportedTotal = safeTokenSum2(
|
|
6037
|
+
inputTokens,
|
|
6038
|
+
usage.total_output_tokens,
|
|
6039
|
+
thinking ?? 0,
|
|
6040
|
+
toolUse ?? 0
|
|
6041
|
+
);
|
|
6042
|
+
const reconciles = reportedTotal !== void 0 && usage.total_tokens === reportedTotal;
|
|
5550
6043
|
return {
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
6044
|
+
usage: {
|
|
6045
|
+
prompt_tokens: inputTokens,
|
|
6046
|
+
completion_tokens: usage.total_output_tokens,
|
|
6047
|
+
total_tokens: usage.total_tokens,
|
|
6048
|
+
...cached ? { cached_tokens: cached } : {},
|
|
6049
|
+
...thinking ? { reasoning_tokens: thinking } : {},
|
|
6050
|
+
...audioInputTokens !== void 0 ? { audio_input_tokens: audioInputTokens } : {}
|
|
6051
|
+
},
|
|
6052
|
+
...modalities !== void 0 && billedOutputTokens !== void 0 && reconciles ? {
|
|
6053
|
+
pricingUsage: {
|
|
6054
|
+
inputTokens,
|
|
6055
|
+
outputTokens: billedOutputTokens,
|
|
6056
|
+
cachedTokens: cached ?? 0,
|
|
6057
|
+
modalities
|
|
6058
|
+
}
|
|
6059
|
+
} : {},
|
|
6060
|
+
hasUnmodeledBilledUsage: toolUse !== void 0 && toolUse !== 0
|
|
5556
6061
|
};
|
|
5557
6062
|
}
|
|
5558
6063
|
|
|
@@ -5572,6 +6077,14 @@ var OPENROUTER_PROFILE = {
|
|
|
5572
6077
|
capture: "reasoning_details",
|
|
5573
6078
|
roundTrip: "on-tool-call-turns"
|
|
5574
6079
|
},
|
|
6080
|
+
capabilities: {
|
|
6081
|
+
// Images: `image_url` data URLs or remote URLs. Audio: `input_audio`, which
|
|
6082
|
+
// OpenRouter documents as base64-only — bytes are encoded by the engine.
|
|
6083
|
+
inputModalities: {
|
|
6084
|
+
image: { sources: ["url", "bytes", "base64"] },
|
|
6085
|
+
audio: { sources: ["bytes", "base64"], formats: OPENROUTER_AUDIO_FORMATS }
|
|
6086
|
+
}
|
|
6087
|
+
},
|
|
5575
6088
|
// OpenRouter normalizes to max_tokens across its backends.
|
|
5576
6089
|
maxTokensField: "max_tokens"
|
|
5577
6090
|
};
|
|
@@ -7067,87 +7580,98 @@ function validBase64(value) {
|
|
|
7067
7580
|
function decodedBase64Bytes(value) {
|
|
7068
7581
|
return Math.floor(value.length * 3 / 4) - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
|
|
7069
7582
|
}
|
|
7583
|
+
var INLINE_MEDIA_LIMIT_MESSAGE = "Inline media data must not exceed 25 MiB total; use a provider-file source, or a URL for images, where supported";
|
|
7584
|
+
var IMAGE_SOURCE_TYPES = ["url", "bytes", "base64", "provider-file"];
|
|
7585
|
+
var AUDIO_SOURCE_TYPES = ["bytes", "base64", "provider-file"];
|
|
7586
|
+
function cloneMediaSource(source, index, allowed, reserveInlineBytes) {
|
|
7587
|
+
if (!source || typeof source !== "object") invalid(`part ${index}.source must be an object`);
|
|
7588
|
+
const raw = source;
|
|
7589
|
+
const kind = raw.type;
|
|
7590
|
+
if (typeof kind !== "string" || !allowed.includes(kind))
|
|
7591
|
+
invalid(`part ${index}.source.type is unsupported`);
|
|
7592
|
+
let clone;
|
|
7593
|
+
switch (kind) {
|
|
7594
|
+
case "url": {
|
|
7595
|
+
const url = nonEmptyString2(raw.url, `part ${index}.source.url`);
|
|
7596
|
+
let parsed;
|
|
7597
|
+
try {
|
|
7598
|
+
parsed = new URL(url);
|
|
7599
|
+
} catch {
|
|
7600
|
+
invalid(`part ${index}.source.url must be an http(s) URL`);
|
|
7601
|
+
}
|
|
7602
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
7603
|
+
invalid(`part ${index}.source.url must be an http(s) URL`);
|
|
7604
|
+
clone = {
|
|
7605
|
+
type: "url",
|
|
7606
|
+
url,
|
|
7607
|
+
...raw.mediaType === void 0 ? {} : { mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`) }
|
|
7608
|
+
};
|
|
7609
|
+
break;
|
|
7610
|
+
}
|
|
7611
|
+
case "bytes": {
|
|
7612
|
+
if (!(raw.data instanceof Uint8Array) || raw.data.byteLength === 0)
|
|
7613
|
+
invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
|
|
7614
|
+
reserveInlineBytes(raw.data.byteLength);
|
|
7615
|
+
clone = {
|
|
7616
|
+
type: "bytes",
|
|
7617
|
+
data: new Uint8Array(raw.data),
|
|
7618
|
+
mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`)
|
|
7619
|
+
};
|
|
7620
|
+
break;
|
|
7621
|
+
}
|
|
7622
|
+
case "base64": {
|
|
7623
|
+
const data = nonEmptyString2(raw.data, `part ${index}.source.data`);
|
|
7624
|
+
if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3))
|
|
7625
|
+
invalid(INLINE_MEDIA_LIMIT_MESSAGE);
|
|
7626
|
+
if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
|
|
7627
|
+
reserveInlineBytes(decodedBase64Bytes(data));
|
|
7628
|
+
clone = {
|
|
7629
|
+
type: "base64",
|
|
7630
|
+
data,
|
|
7631
|
+
mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`)
|
|
7632
|
+
};
|
|
7633
|
+
break;
|
|
7634
|
+
}
|
|
7635
|
+
default:
|
|
7636
|
+
clone = {
|
|
7637
|
+
type: "provider-file",
|
|
7638
|
+
provider: nonEmptyString2(raw.provider, `part ${index}.source.provider`),
|
|
7639
|
+
reference: nonEmptyString2(raw.reference, `part ${index}.source.reference`),
|
|
7640
|
+
...raw.mediaType === void 0 ? {} : { mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`) }
|
|
7641
|
+
};
|
|
7642
|
+
}
|
|
7643
|
+
return clone;
|
|
7644
|
+
}
|
|
7070
7645
|
function normalizeModelInput(input) {
|
|
7071
7646
|
if (typeof input === "string") return input;
|
|
7072
7647
|
if (!Array.isArray(input) || input.length === 0)
|
|
7073
7648
|
invalid("ModelInput parts must be a non-empty array");
|
|
7074
7649
|
let inlineBytes = 0;
|
|
7075
7650
|
const reserveInlineBytes = (bytes) => {
|
|
7076
|
-
if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes)
|
|
7077
|
-
invalid(
|
|
7078
|
-
"Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
|
|
7079
|
-
);
|
|
7080
|
-
}
|
|
7651
|
+
if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes) invalid(INLINE_MEDIA_LIMIT_MESSAGE);
|
|
7081
7652
|
inlineBytes += bytes;
|
|
7082
7653
|
};
|
|
7083
7654
|
return input.map((part, index) => {
|
|
7084
7655
|
if (!part || typeof part !== "object") invalid(`ModelInput part ${index} must be an object`);
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
let parsed;
|
|
7095
|
-
try {
|
|
7096
|
-
parsed = new URL(url);
|
|
7097
|
-
} catch {
|
|
7098
|
-
invalid(`part ${index}.source.url must be an http(s) URL`);
|
|
7099
|
-
}
|
|
7100
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
7101
|
-
invalid(`part ${index}.source.url must be an http(s) URL`);
|
|
7102
|
-
clone = {
|
|
7103
|
-
type: "url",
|
|
7104
|
-
url,
|
|
7105
|
-
...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
|
|
7106
|
-
};
|
|
7107
|
-
break;
|
|
7108
|
-
}
|
|
7109
|
-
case "bytes":
|
|
7110
|
-
if (!(source.data instanceof Uint8Array) || source.data.byteLength === 0)
|
|
7111
|
-
invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
|
|
7112
|
-
reserveInlineBytes(source.data.byteLength);
|
|
7113
|
-
clone = {
|
|
7114
|
-
type: "bytes",
|
|
7115
|
-
data: source.data.slice(),
|
|
7116
|
-
mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
|
|
7117
|
-
};
|
|
7118
|
-
break;
|
|
7119
|
-
case "base64": {
|
|
7120
|
-
const data = nonEmptyString2(source.data, `part ${index}.source.data`);
|
|
7121
|
-
if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3)) {
|
|
7122
|
-
invalid(
|
|
7123
|
-
"Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
|
|
7124
|
-
);
|
|
7125
|
-
}
|
|
7126
|
-
if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
|
|
7127
|
-
reserveInlineBytes(decodedBase64Bytes(data));
|
|
7128
|
-
clone = {
|
|
7129
|
-
type: "base64",
|
|
7130
|
-
data,
|
|
7131
|
-
mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
|
|
7656
|
+
const mediaLabel = () => part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) };
|
|
7657
|
+
switch (part.type) {
|
|
7658
|
+
case "text":
|
|
7659
|
+
return { type: "text", text: nonEmptyString2(part.text, `part ${index}.text`) };
|
|
7660
|
+
case "image":
|
|
7661
|
+
return {
|
|
7662
|
+
type: "image",
|
|
7663
|
+
source: cloneMediaSource(part.source, index, IMAGE_SOURCE_TYPES, reserveInlineBytes),
|
|
7664
|
+
...mediaLabel()
|
|
7132
7665
|
};
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
provider: nonEmptyString2(source.provider, `part ${index}.source.provider`),
|
|
7139
|
-
reference: nonEmptyString2(source.reference, `part ${index}.source.reference`),
|
|
7140
|
-
...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
|
|
7666
|
+
case "audio":
|
|
7667
|
+
return {
|
|
7668
|
+
type: "audio",
|
|
7669
|
+
source: cloneMediaSource(part.source, index, AUDIO_SOURCE_TYPES, reserveInlineBytes),
|
|
7670
|
+
...mediaLabel()
|
|
7141
7671
|
};
|
|
7142
|
-
break;
|
|
7143
7672
|
default:
|
|
7144
|
-
invalid(`part ${index}.
|
|
7673
|
+
invalid(`part ${index}.type must be 'text', 'image', or 'audio'`);
|
|
7145
7674
|
}
|
|
7146
|
-
return {
|
|
7147
|
-
type: "image",
|
|
7148
|
-
source: clone,
|
|
7149
|
-
...part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) }
|
|
7150
|
-
};
|
|
7151
7675
|
});
|
|
7152
7676
|
}
|
|
7153
7677
|
function inputText(input) {
|
|
@@ -7156,33 +7680,54 @@ function inputText(input) {
|
|
|
7156
7680
|
function cloneModelInput(input) {
|
|
7157
7681
|
return typeof input === "string" ? input : normalizeModelInput(input);
|
|
7158
7682
|
}
|
|
7683
|
+
function unsupportedPart(part) {
|
|
7684
|
+
return invalid(
|
|
7685
|
+
`Unsupported model input part type '${String(part.type)}'`
|
|
7686
|
+
);
|
|
7687
|
+
}
|
|
7688
|
+
function describeSource(source, label) {
|
|
7689
|
+
const media = source;
|
|
7690
|
+
const locator = media.type === "url" ? media.url : media.type === "provider-file" ? media.reference : void 0;
|
|
7691
|
+
return {
|
|
7692
|
+
source: source.type,
|
|
7693
|
+
...media.mediaType ? { mediaType: media.mediaType } : {},
|
|
7694
|
+
...media.type === "bytes" ? { bytes: media.data.byteLength } : {},
|
|
7695
|
+
...media.type === "base64" ? { bytes: decodedBase64Bytes(media.data) } : {},
|
|
7696
|
+
...locator ? { locator } : {},
|
|
7697
|
+
...label ? { label } : {}
|
|
7698
|
+
};
|
|
7699
|
+
}
|
|
7159
7700
|
function describeModelInput(input) {
|
|
7160
7701
|
if (typeof input === "string") return void 0;
|
|
7161
7702
|
return {
|
|
7162
7703
|
parts: input.map((part) => {
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
...(source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0) ? {
|
|
7174
|
-
locator: source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0
|
|
7175
|
-
} : {},
|
|
7176
|
-
...part.label ? { label: part.label } : {}
|
|
7177
|
-
};
|
|
7704
|
+
switch (part.type) {
|
|
7705
|
+
case "text":
|
|
7706
|
+
return { type: "text", characters: part.text.length };
|
|
7707
|
+
case "image":
|
|
7708
|
+
return { type: "image", ...describeSource(part.source, part.label) };
|
|
7709
|
+
case "audio":
|
|
7710
|
+
return { type: "audio", ...describeSource(part.source, part.label) };
|
|
7711
|
+
default:
|
|
7712
|
+
return unsupportedPart(part);
|
|
7713
|
+
}
|
|
7178
7714
|
})
|
|
7179
7715
|
};
|
|
7180
7716
|
}
|
|
7181
7717
|
function summarizeModelInput(input) {
|
|
7182
7718
|
if (typeof input === "string") return input;
|
|
7183
|
-
return input.map(
|
|
7184
|
-
(part
|
|
7185
|
-
|
|
7719
|
+
return input.map((part) => {
|
|
7720
|
+
switch (part.type) {
|
|
7721
|
+
case "text":
|
|
7722
|
+
return part.text;
|
|
7723
|
+
case "image":
|
|
7724
|
+
return `[image ${part.source.mediaType ?? "media"}]`;
|
|
7725
|
+
case "audio":
|
|
7726
|
+
return `[audio ${part.source.mediaType ?? "media"}]`;
|
|
7727
|
+
default:
|
|
7728
|
+
return unsupportedPart(part);
|
|
7729
|
+
}
|
|
7730
|
+
}).join("\n");
|
|
7186
7731
|
}
|
|
7187
7732
|
function sanitizeModelInputForTrace(input) {
|
|
7188
7733
|
return inputText(input);
|
|
@@ -7196,15 +7741,86 @@ function normalizePersistedSessionHistory(history) {
|
|
|
7196
7741
|
);
|
|
7197
7742
|
}
|
|
7198
7743
|
const content = normalizeModelInput(message.content);
|
|
7199
|
-
if (typeof content !== "string" && content.some((part) => part.type
|
|
7744
|
+
if (typeof content !== "string" && content.some((part) => part.type !== "text" && part.source.type === "bytes")) {
|
|
7200
7745
|
throw new InvalidModelInputError(
|
|
7201
|
-
"Uint8Array
|
|
7746
|
+
"Uint8Array media input cannot be persisted in session history"
|
|
7202
7747
|
);
|
|
7203
7748
|
}
|
|
7204
7749
|
return { ...message, content };
|
|
7205
7750
|
});
|
|
7206
7751
|
}
|
|
7207
7752
|
|
|
7753
|
+
// src/session-input.ts
|
|
7754
|
+
var MODEL_INPUT_PART_TYPES = /* @__PURE__ */ new Set(["text", "image", "audio"]);
|
|
7755
|
+
var sessionInputMarkers = /* @__PURE__ */ new WeakMap();
|
|
7756
|
+
function isModelInputParts(input) {
|
|
7757
|
+
return Array.isArray(input) && input.length > 0 && input.every(
|
|
7758
|
+
(part) => typeof part === "object" && part !== null && MODEL_INPUT_PART_TYPES.has(part.type)
|
|
7759
|
+
);
|
|
7760
|
+
}
|
|
7761
|
+
function prepareSessionInput(input) {
|
|
7762
|
+
if (typeof input === "string") return { content: input, normalizedInput: input };
|
|
7763
|
+
if (isModelInputParts(input)) {
|
|
7764
|
+
const normalizedInput = normalizeModelInput(input);
|
|
7765
|
+
return { content: summarizeModelInput(normalizedInput), normalizedInput };
|
|
7766
|
+
}
|
|
7767
|
+
const content = JSON.stringify(input);
|
|
7768
|
+
return {
|
|
7769
|
+
content,
|
|
7770
|
+
...typeof content === "string" ? { normalizedInput: content } : {}
|
|
7771
|
+
};
|
|
7772
|
+
}
|
|
7773
|
+
function registerSessionInput(history, message, normalizedInput) {
|
|
7774
|
+
sessionInputMarkers.set(history, {
|
|
7775
|
+
message,
|
|
7776
|
+
content: message.content,
|
|
7777
|
+
normalizedInput: cloneModelInput(normalizedInput)
|
|
7778
|
+
});
|
|
7779
|
+
}
|
|
7780
|
+
function clearSessionInput(history) {
|
|
7781
|
+
sessionInputMarkers.delete(history);
|
|
7782
|
+
}
|
|
7783
|
+
function equalBytes(left, right) {
|
|
7784
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
7785
|
+
for (let index = 0; index < left.byteLength; index++) {
|
|
7786
|
+
if (left[index] !== right[index]) return false;
|
|
7787
|
+
}
|
|
7788
|
+
return true;
|
|
7789
|
+
}
|
|
7790
|
+
function equalSource(left, right) {
|
|
7791
|
+
if (left.type !== right.type) return false;
|
|
7792
|
+
switch (left.type) {
|
|
7793
|
+
case "url":
|
|
7794
|
+
return right.type === "url" && left.url === right.url && left.mediaType === right.mediaType;
|
|
7795
|
+
case "bytes":
|
|
7796
|
+
return right.type === "bytes" && left.mediaType === right.mediaType && equalBytes(left.data, right.data);
|
|
7797
|
+
case "base64":
|
|
7798
|
+
return right.type === "base64" && left.data === right.data && left.mediaType === right.mediaType;
|
|
7799
|
+
case "provider-file":
|
|
7800
|
+
return right.type === "provider-file" && left.provider === right.provider && left.reference === right.reference && left.mediaType === right.mediaType;
|
|
7801
|
+
}
|
|
7802
|
+
}
|
|
7803
|
+
function equalPart(left, right) {
|
|
7804
|
+
if (left.type !== right.type) return false;
|
|
7805
|
+
if (left.type === "text") return right.type === "text" && left.text === right.text;
|
|
7806
|
+
return right.type === left.type && left.label === right.label && equalSource(left.source, right.source);
|
|
7807
|
+
}
|
|
7808
|
+
function equalModelInput(left, right) {
|
|
7809
|
+
if (typeof left === "string" || typeof right === "string") return left === right;
|
|
7810
|
+
return left.length === right.length && left.every((part, index) => equalPart(part, right[index]));
|
|
7811
|
+
}
|
|
7812
|
+
function sessionHistoryForAsk(history, normalizedInput) {
|
|
7813
|
+
const snapshot = history.slice();
|
|
7814
|
+
const marker = sessionInputMarkers.get(history);
|
|
7815
|
+
if (!marker) return snapshot;
|
|
7816
|
+
const last = snapshot[snapshot.length - 1];
|
|
7817
|
+
if (last !== marker.message || last.role !== "user" || last.content !== marker.content || !equalModelInput(marker.normalizedInput, normalizedInput)) {
|
|
7818
|
+
return snapshot;
|
|
7819
|
+
}
|
|
7820
|
+
snapshot.pop();
|
|
7821
|
+
return snapshot;
|
|
7822
|
+
}
|
|
7823
|
+
|
|
7208
7824
|
// src/partial-json.ts
|
|
7209
7825
|
var MAX_DEPTH = 256;
|
|
7210
7826
|
function parsePartialJson(input) {
|
|
@@ -10003,7 +10619,7 @@ function estimateMessagesTokens(messages) {
|
|
|
10003
10619
|
for (const msg of messages) {
|
|
10004
10620
|
total += estimateTokens(inputText(msg.content));
|
|
10005
10621
|
if (typeof msg.content !== "string") {
|
|
10006
|
-
unmeasured ||= msg.content.some((part) => part.type
|
|
10622
|
+
unmeasured ||= msg.content.some((part) => part.type !== "text");
|
|
10007
10623
|
}
|
|
10008
10624
|
if (msg.tool_calls) {
|
|
10009
10625
|
for (const tc of msg.tool_calls) {
|
|
@@ -10049,10 +10665,6 @@ function normalizeSessionHistory(messages) {
|
|
|
10049
10665
|
function hasRichMessage(messages) {
|
|
10050
10666
|
return messages.some((message) => typeof message.content !== "string");
|
|
10051
10667
|
}
|
|
10052
|
-
function firstImageSource(input, history) {
|
|
10053
|
-
const find = (value) => typeof value === "string" ? void 0 : value.find((part) => part.type === "image")?.source.type;
|
|
10054
|
-
return find(input) ?? history.map((message) => find(message.content)).find(Boolean);
|
|
10055
|
-
}
|
|
10056
10668
|
function appendHandoffInstruction(input, originalText, instruction) {
|
|
10057
10669
|
if (typeof input === "string" || instruction.length === 0 || instruction === originalText)
|
|
10058
10670
|
return input;
|
|
@@ -10610,7 +11222,8 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10610
11222
|
// ── ctx.ask() ─────────────────────────────────────────────────────────
|
|
10611
11223
|
async ask(agent2, prompt, options) {
|
|
10612
11224
|
const normalizedInput = normalizeModelInput(prompt);
|
|
10613
|
-
const
|
|
11225
|
+
const sessionHistorySnapshot = sessionHistoryForAsk(this.sessionHistory, normalizedInput);
|
|
11226
|
+
const normalizedHistory = normalizeSessionHistory(sessionHistorySnapshot);
|
|
10614
11227
|
const delegateInputHolder = agent2;
|
|
10615
11228
|
if (delegateInputHolder._delegateOriginalInput !== void 0) {
|
|
10616
11229
|
delegateInputHolder._delegateOriginalInput = normalizeModelInput(
|
|
@@ -10820,20 +11433,31 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10820
11433
|
const providerOptions = options?.providerOptions ?? agent2._config.providerOptions;
|
|
10821
11434
|
const richRequest = typeof input !== "string" || hasRichMessage(sessionHistory);
|
|
10822
11435
|
if (richRequest) {
|
|
11436
|
+
const richPart = firstRichPart(input, sessionHistory);
|
|
11437
|
+
const modality = richPart?.type ?? "image";
|
|
11438
|
+
const source = richPart?.source.type;
|
|
10823
11439
|
if (providerOptions && ("messages" in providerOptions || "input" in providerOptions)) {
|
|
10824
11440
|
throw new UnsupportedModelInputError({
|
|
10825
11441
|
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
10826
11442
|
model,
|
|
10827
|
-
modality
|
|
11443
|
+
modality,
|
|
10828
11444
|
feature: "raw input-container providerOptions"
|
|
10829
11445
|
});
|
|
10830
11446
|
}
|
|
10831
|
-
const
|
|
11447
|
+
const audioPart = firstRichPart(input, sessionHistory, "audio");
|
|
11448
|
+
if (audioPart && !provider.inputCapabilities?.(model)?.audio) {
|
|
11449
|
+
throw new UnsupportedModelInputError({
|
|
11450
|
+
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
11451
|
+
model,
|
|
11452
|
+
modality: "audio",
|
|
11453
|
+
source: audioPart.source.type
|
|
11454
|
+
});
|
|
11455
|
+
}
|
|
10832
11456
|
if (!provider.validateInput) {
|
|
10833
11457
|
throw new UnsupportedModelInputError({
|
|
10834
11458
|
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
10835
11459
|
model,
|
|
10836
|
-
modality
|
|
11460
|
+
modality,
|
|
10837
11461
|
...source ? { source } : {}
|
|
10838
11462
|
});
|
|
10839
11463
|
}
|
|
@@ -10852,10 +11476,20 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10852
11476
|
throw new UnsupportedModelInputError({
|
|
10853
11477
|
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
10854
11478
|
model,
|
|
10855
|
-
modality
|
|
11479
|
+
modality,
|
|
10856
11480
|
...source ? { source } : {}
|
|
10857
11481
|
});
|
|
10858
11482
|
}
|
|
11483
|
+
if (audioPart && validation.effectiveModel !== model) {
|
|
11484
|
+
if (!provider.inputCapabilities?.(validation.effectiveModel)?.audio) {
|
|
11485
|
+
throw new UnsupportedModelInputError({
|
|
11486
|
+
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
11487
|
+
model: validation.effectiveModel,
|
|
11488
|
+
modality: "audio",
|
|
11489
|
+
source: audioPart.source.type
|
|
11490
|
+
});
|
|
11491
|
+
}
|
|
11492
|
+
}
|
|
10859
11493
|
model = validation.effectiveModel;
|
|
10860
11494
|
}
|
|
10861
11495
|
const effectiveModelUri = !richRequest ? modelUri : `${modelUri.slice(0, modelUri.indexOf(":") + 1)}${model}`;
|
|
@@ -10868,25 +11502,17 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10868
11502
|
(descriptor) => descriptor !== void 0
|
|
10869
11503
|
);
|
|
10870
11504
|
const parts = descriptors.flatMap((descriptor) => descriptor.parts);
|
|
11505
|
+
const media = parts.filter((part) => part.type !== "text");
|
|
11506
|
+
const withSource = (source) => media.filter((part) => part.source === source).length;
|
|
10871
11507
|
this.spanManager?.addEventToActiveSpan("axl.model_input", {
|
|
10872
11508
|
"axl.input.parts": parts.length,
|
|
10873
11509
|
"axl.input.images": parts.filter((part) => part.type === "image").length,
|
|
10874
|
-
"axl.input.
|
|
10875
|
-
|
|
10876
|
-
)
|
|
10877
|
-
"axl.input.source.
|
|
10878
|
-
|
|
10879
|
-
).
|
|
10880
|
-
"axl.input.source.url": parts.filter(
|
|
10881
|
-
(part) => part.type === "image" && part.source === "url"
|
|
10882
|
-
).length,
|
|
10883
|
-
"axl.input.source.provider_file": parts.filter(
|
|
10884
|
-
(part) => part.type === "image" && part.source === "provider-file"
|
|
10885
|
-
).length,
|
|
10886
|
-
"axl.input.inline_bytes": parts.reduce(
|
|
10887
|
-
(sum, part) => sum + (part.type === "image" ? part.bytes ?? 0 : 0),
|
|
10888
|
-
0
|
|
10889
|
-
)
|
|
11510
|
+
"axl.input.audio": parts.filter((part) => part.type === "audio").length,
|
|
11511
|
+
"axl.input.source.bytes": withSource("bytes"),
|
|
11512
|
+
"axl.input.source.base64": withSource("base64"),
|
|
11513
|
+
"axl.input.source.url": withSource("url"),
|
|
11514
|
+
"axl.input.source.provider_file": withSource("provider-file"),
|
|
11515
|
+
"axl.input.inline_bytes": media.reduce((sum, part) => sum + (part.bytes ?? 0), 0)
|
|
10890
11516
|
});
|
|
10891
11517
|
}
|
|
10892
11518
|
assertRichMessagesAreUserOnly(sessionHistory);
|
|
@@ -13453,6 +14079,11 @@ ${summaryResponse.content}`
|
|
|
13453
14079
|
}
|
|
13454
14080
|
});
|
|
13455
14081
|
const routerInput = options?.routerInput === "text" ? inputText(prompt) : prompt;
|
|
14082
|
+
if (routerInput === "") {
|
|
14083
|
+
throw new InvalidModelInputError(
|
|
14084
|
+
"delegate routerInput 'text' requires at least one text part; the input is media-only \u2014 route on the full input or add a text part"
|
|
14085
|
+
);
|
|
14086
|
+
}
|
|
13456
14087
|
routerAgent._delegateOriginalInput = prompt;
|
|
13457
14088
|
return this.ask(routerAgent, routerInput, {
|
|
13458
14089
|
schema: options?.schema,
|
|
@@ -14549,9 +15180,13 @@ var Session = class _Session {
|
|
|
14549
15180
|
}
|
|
14550
15181
|
async sendImpl(workflowName, input, signal, events) {
|
|
14551
15182
|
const { history, metadata } = await this.prepareHistory(input);
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
15183
|
+
try {
|
|
15184
|
+
const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
|
|
15185
|
+
await this.commitHistory(history, result);
|
|
15186
|
+
return result;
|
|
15187
|
+
} finally {
|
|
15188
|
+
clearSessionInput(history);
|
|
15189
|
+
}
|
|
14555
15190
|
}
|
|
14556
15191
|
/** Read history + summary, apply maxMessages limit (with optional
|
|
14557
15192
|
* summarization), and push the new user message. Returns the mutable
|
|
@@ -14561,6 +15196,7 @@ var Session = class _Session {
|
|
|
14561
15196
|
* `streamImpl` so future fixes (e.g., to summarization wiring) land in
|
|
14562
15197
|
* one place. */
|
|
14563
15198
|
async prepareHistory(input) {
|
|
15199
|
+
const preparedInput = prepareSessionInput(input);
|
|
14564
15200
|
const history = await this.store.getSession(this.sessionId);
|
|
14565
15201
|
let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
|
|
14566
15202
|
const maxMessages = this.options.history?.maxMessages;
|
|
@@ -14583,10 +15219,11 @@ var Session = class _Session {
|
|
|
14583
15219
|
history.length = 0;
|
|
14584
15220
|
history.push(...trimmed);
|
|
14585
15221
|
}
|
|
14586
|
-
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
15222
|
+
const userMessage = { role: "user", content: preparedInput.content };
|
|
15223
|
+
history.push(userMessage);
|
|
15224
|
+
if (this.options.deduplicateInput !== false && preparedInput.normalizedInput !== void 0) {
|
|
15225
|
+
registerSessionInput(history, userMessage, preparedInput.normalizedInput);
|
|
15226
|
+
}
|
|
14590
15227
|
const metadata = {
|
|
14591
15228
|
sessionId: this.sessionId,
|
|
14592
15229
|
sessionHistory: history,
|
|
@@ -14645,21 +15282,26 @@ var Session = class _Session {
|
|
|
14645
15282
|
events
|
|
14646
15283
|
});
|
|
14647
15284
|
} catch (err) {
|
|
15285
|
+
if (history) clearSessionInput(history);
|
|
14648
15286
|
rejectReady(err);
|
|
14649
15287
|
throw err;
|
|
14650
15288
|
}
|
|
14651
15289
|
resolveReady(axlStream);
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
this.
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
14658
|
-
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
|
|
15290
|
+
try {
|
|
15291
|
+
const completion = axlStream.promise.then(
|
|
15292
|
+
(result) => this.commitHistory(history, result).catch((err) => {
|
|
15293
|
+
this.runtime.emit("error", {
|
|
15294
|
+
type: "session_history_save_failed",
|
|
15295
|
+
sessionId: this.sessionId,
|
|
15296
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15297
|
+
});
|
|
15298
|
+
}),
|
|
15299
|
+
() => void 0
|
|
15300
|
+
);
|
|
15301
|
+
await completion;
|
|
15302
|
+
} finally {
|
|
15303
|
+
clearSessionInput(history);
|
|
15304
|
+
}
|
|
14663
15305
|
}
|
|
14664
15306
|
async history() {
|
|
14665
15307
|
return this.store.getSession(this.sessionId);
|
|
@@ -18274,6 +18916,7 @@ function cosineSimilarity2(a, b) {
|
|
|
18274
18916
|
resolveRuntime,
|
|
18275
18917
|
resolveThinkingOptions,
|
|
18276
18918
|
stringStreamFromEvents,
|
|
18919
|
+
summarizeModelInput,
|
|
18277
18920
|
tool,
|
|
18278
18921
|
workflow,
|
|
18279
18922
|
zodToJsonSchema
|