@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.js
CHANGED
|
@@ -1193,38 +1193,112 @@ function unsafeTransportError(surface, remediation, exposedData) {
|
|
|
1193
1193
|
);
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
1196
|
+
// src/providers/rich-input.ts
|
|
1197
|
+
function richInputParts(input) {
|
|
1198
|
+
return typeof input === "string" ? [] : input.filter((part) => part.type !== "text");
|
|
1199
|
+
}
|
|
1200
|
+
function firstRichPart(input, history, modality) {
|
|
1201
|
+
const find = (value) => richInputParts(value).find((part) => modality === void 0 || part.type === modality);
|
|
1202
|
+
return find(input) ?? history.map((message) => find(message.content)).find((part) => part !== void 0);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/providers/audio-format.ts
|
|
1206
|
+
var OPENAI_CHAT_AUDIO_FORMATS = Object.freeze({
|
|
1207
|
+
"audio/wav": "wav",
|
|
1208
|
+
"audio/x-wav": "wav",
|
|
1209
|
+
"audio/wave": "wav",
|
|
1210
|
+
"audio/mpeg": "mp3",
|
|
1211
|
+
"audio/mp3": "mp3"
|
|
1212
|
+
});
|
|
1213
|
+
var OPENROUTER_AUDIO_FORMATS = Object.freeze({
|
|
1214
|
+
...OPENAI_CHAT_AUDIO_FORMATS,
|
|
1215
|
+
"audio/aiff": "aiff",
|
|
1216
|
+
"audio/x-aiff": "aiff",
|
|
1217
|
+
"audio/aac": "aac",
|
|
1218
|
+
"audio/ogg": "ogg",
|
|
1219
|
+
"audio/flac": "flac",
|
|
1220
|
+
"audio/x-flac": "flac",
|
|
1221
|
+
"audio/m4a": "m4a",
|
|
1222
|
+
"audio/x-m4a": "m4a",
|
|
1223
|
+
"audio/mp4": "m4a",
|
|
1224
|
+
"audio/l16": "pcm16"
|
|
1225
|
+
});
|
|
1226
|
+
function resolveAudioFormat(formats, mediaType2) {
|
|
1227
|
+
if (typeof mediaType2 !== "string") return void 0;
|
|
1228
|
+
const essence = mediaType2.split(";", 1)[0].trim().toLowerCase();
|
|
1229
|
+
return Object.prototype.hasOwnProperty.call(formats, essence) ? formats[essence] : void 0;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1196
1232
|
// src/providers/openai-compatible.ts
|
|
1197
|
-
function
|
|
1233
|
+
function unmappableMediaTypeFeature(mediaType2) {
|
|
1234
|
+
return mediaType2 === void 0 ? "audio media type (missing)" : `audio media type '${mediaType2}'`;
|
|
1235
|
+
}
|
|
1236
|
+
function compatibleInlineBase64(source) {
|
|
1198
1237
|
return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
|
|
1199
1238
|
"base64"
|
|
1200
1239
|
);
|
|
1201
1240
|
}
|
|
1202
|
-
function
|
|
1241
|
+
function compatibleRichParts(parts, model, modalities, provider) {
|
|
1242
|
+
const reject = (modality, source, feature) => new UnsupportedModelInputError({
|
|
1243
|
+
provider,
|
|
1244
|
+
model,
|
|
1245
|
+
modality,
|
|
1246
|
+
source,
|
|
1247
|
+
...feature ? { feature } : {}
|
|
1248
|
+
});
|
|
1203
1249
|
const content = [];
|
|
1204
1250
|
for (const part of parts) {
|
|
1205
1251
|
if (part.type === "text") {
|
|
1206
1252
|
content.push({ type: "text", text: part.text });
|
|
1207
1253
|
continue;
|
|
1208
1254
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1255
|
+
if (part.type === "audio") {
|
|
1256
|
+
const { source: source2 } = part;
|
|
1257
|
+
const audio = modalities.audio;
|
|
1258
|
+
if (!audio) throw reject("audio", source2.type, "audio input");
|
|
1259
|
+
if (source2.type === "provider-file" || !audio.sources.includes(source2.type)) {
|
|
1260
|
+
throw reject("audio", source2.type);
|
|
1261
|
+
}
|
|
1262
|
+
const format = resolveAudioFormat(audio.formats, source2.mediaType);
|
|
1263
|
+
if (format === void 0) {
|
|
1264
|
+
throw reject("audio", source2.type, unmappableMediaTypeFeature(source2.mediaType));
|
|
1265
|
+
}
|
|
1266
|
+
content.push({
|
|
1267
|
+
type: "input_audio",
|
|
1268
|
+
// Caller base64 passes through verbatim — never decoded and re-encoded.
|
|
1269
|
+
input_audio: { data: compatibleInlineBase64(source2), format }
|
|
1270
|
+
});
|
|
1271
|
+
if (part.label) content.push({ type: "text", text: `[Audio: ${part.label}]` });
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (part.type !== "image") {
|
|
1275
|
+
const unmapped = part;
|
|
1211
1276
|
throw new UnsupportedModelInputError({
|
|
1212
|
-
provider
|
|
1277
|
+
provider,
|
|
1213
1278
|
model,
|
|
1214
|
-
modality:
|
|
1215
|
-
|
|
1279
|
+
modality: unmapped.type,
|
|
1280
|
+
feature: "this input modality"
|
|
1216
1281
|
});
|
|
1217
1282
|
}
|
|
1283
|
+
const { source } = part;
|
|
1284
|
+
const image = modalities.image;
|
|
1285
|
+
if (!image) throw reject("image", source.type, "image input for this model");
|
|
1286
|
+
if (source.type === "provider-file" || !image.sources.includes(source.type)) {
|
|
1287
|
+
throw reject("image", source.type);
|
|
1288
|
+
}
|
|
1218
1289
|
content.push({
|
|
1219
1290
|
type: "image_url",
|
|
1220
1291
|
image_url: {
|
|
1221
|
-
url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${
|
|
1292
|
+
url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${compatibleInlineBase64(source)}`
|
|
1222
1293
|
}
|
|
1223
1294
|
});
|
|
1224
1295
|
if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
|
|
1225
1296
|
}
|
|
1226
1297
|
return content;
|
|
1227
1298
|
}
|
|
1299
|
+
function requestAudioParts(input, history) {
|
|
1300
|
+
return [input, ...history.map((message) => message.content)].flatMap((content) => richInputParts(content)).filter((part) => part.type === "audio");
|
|
1301
|
+
}
|
|
1228
1302
|
function resolvePerModel(value, model, fallback) {
|
|
1229
1303
|
if (value === void 0) return fallback;
|
|
1230
1304
|
return typeof value === "function" ? value(model) : value;
|
|
@@ -1238,6 +1312,9 @@ function sortedKeys(table) {
|
|
|
1238
1312
|
}
|
|
1239
1313
|
return keys;
|
|
1240
1314
|
}
|
|
1315
|
+
function reportedTokenCount(value) {
|
|
1316
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
1317
|
+
}
|
|
1241
1318
|
function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens, match = "prefix") {
|
|
1242
1319
|
const cached = cachedTokens ?? 0;
|
|
1243
1320
|
if (!Number.isSafeInteger(promptTokens) || !Number.isSafeInteger(completionTokens) || !Number.isSafeInteger(cached) || promptTokens < 0 || completionTokens < 0 || cached < 0 || cached > promptTokens) {
|
|
@@ -1341,26 +1418,47 @@ function normalizeMessageContent(content) {
|
|
|
1341
1418
|
var OpenAICompatibleProvider = class {
|
|
1342
1419
|
name;
|
|
1343
1420
|
reportsRequestLifecycle = true;
|
|
1421
|
+
/**
|
|
1422
|
+
* Modalities the effective profile declares for this model. A blank model
|
|
1423
|
+
* declares nothing, so a rich request against one fails validation exactly
|
|
1424
|
+
* as it did before any modality existed.
|
|
1425
|
+
*/
|
|
1426
|
+
inputModalities(model) {
|
|
1427
|
+
return model.trim().length > 0 ? this.profile.capabilities?.inputModalities ?? {} : {};
|
|
1428
|
+
}
|
|
1344
1429
|
inputCapabilities(model) {
|
|
1345
|
-
|
|
1430
|
+
const { image, audio } = this.inputModalities(model);
|
|
1431
|
+
return {
|
|
1432
|
+
...image ? { image: { sources: image.sources } } : {},
|
|
1433
|
+
// `formats` is engine-internal wire detail; the runtime contract is the
|
|
1434
|
+
// accepted source kinds only.
|
|
1435
|
+
...audio ? { audio: { sources: audio.sources } } : {}
|
|
1436
|
+
};
|
|
1346
1437
|
}
|
|
1347
1438
|
validateInput(request) {
|
|
1348
1439
|
const modelOverride = request.providerOptions?.model;
|
|
1349
1440
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
1350
|
-
const
|
|
1441
|
+
const failWith = (modality, source, feature) => {
|
|
1351
1442
|
throw new UnsupportedModelInputError({
|
|
1352
1443
|
provider: this.name,
|
|
1353
1444
|
model: effectiveModel || request.model,
|
|
1354
|
-
modality
|
|
1445
|
+
modality,
|
|
1355
1446
|
...source ? { source } : {},
|
|
1356
1447
|
...feature ? { feature } : {}
|
|
1357
1448
|
});
|
|
1358
1449
|
};
|
|
1450
|
+
const fail = (source, feature) => failWith(firstRichPart(request.input, request.history)?.type ?? "image", source, feature);
|
|
1451
|
+
const modalities = this.inputModalities(effectiveModel);
|
|
1452
|
+
const audioPart = firstRichPart(request.input, request.history, "audio");
|
|
1453
|
+
if (audioPart && !modalities.audio) {
|
|
1454
|
+
failWith("audio", audioPart.source.type, "audio input");
|
|
1455
|
+
}
|
|
1359
1456
|
if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
|
|
1360
1457
|
fail(void 0, "invalid model providerOptions");
|
|
1361
1458
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1459
|
+
const imagePart = firstRichPart(request.input, request.history, "image");
|
|
1460
|
+
if (!modalities.image && (!audioPart || imagePart)) {
|
|
1461
|
+
failWith("image", void 0, "image input for this model");
|
|
1364
1462
|
}
|
|
1365
1463
|
if (request.providerOptions && "messages" in request.providerOptions) {
|
|
1366
1464
|
fail(void 0, "raw messages providerOptions");
|
|
@@ -1371,11 +1469,21 @@ var OpenAICompatibleProvider = class {
|
|
|
1371
1469
|
if (message.content.some(
|
|
1372
1470
|
(part) => part.type === "image" && part.source.type === "provider-file"
|
|
1373
1471
|
)) {
|
|
1374
|
-
|
|
1472
|
+
failWith("image", "provider-file");
|
|
1375
1473
|
}
|
|
1376
1474
|
}
|
|
1377
1475
|
if (Array.isArray(request.input) && request.input.some((part) => part.type === "image" && part.source.type === "provider-file")) {
|
|
1378
|
-
|
|
1476
|
+
failWith("image", "provider-file");
|
|
1477
|
+
}
|
|
1478
|
+
const audio = modalities.audio;
|
|
1479
|
+
if (audio) {
|
|
1480
|
+
for (const part of requestAudioParts(request.input, request.history)) {
|
|
1481
|
+
const source = part.source;
|
|
1482
|
+
if (!audio.sources.includes(source.type)) failWith("audio", source.type);
|
|
1483
|
+
if (resolveAudioFormat(audio.formats, source.mediaType) === void 0) {
|
|
1484
|
+
failWith("audio", source.type, unmappableMediaTypeFeature(source.mediaType));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1379
1487
|
}
|
|
1380
1488
|
return { effectiveModel };
|
|
1381
1489
|
}
|
|
@@ -1560,9 +1668,15 @@ var OpenAICompatibleProvider = class {
|
|
|
1560
1668
|
return body;
|
|
1561
1669
|
}
|
|
1562
1670
|
formatMessage(msg, model) {
|
|
1671
|
+
const modalities = this.inputModalities(model);
|
|
1563
1672
|
const out = {
|
|
1564
1673
|
role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
|
|
1565
|
-
|
|
1674
|
+
// History user turns go through the same builder as the caller's input,
|
|
1675
|
+
// so a rich part is reproduced identically in every continuation request.
|
|
1676
|
+
// Array content is routed UNCONDITIONALLY: a profile that declares no
|
|
1677
|
+
// modality has the builder reject every rich part (and pass text through)
|
|
1678
|
+
// rather than emitting raw `InputContentPart` objects on the wire.
|
|
1679
|
+
content: Array.isArray(msg.content) ? compatibleRichParts(msg.content, model, modalities, this.name) : msg.content
|
|
1566
1680
|
};
|
|
1567
1681
|
if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
|
|
1568
1682
|
if (msg.tool_calls) out.tool_calls = msg.tool_calls;
|
|
@@ -1593,6 +1707,8 @@ var OpenAICompatibleProvider = class {
|
|
|
1593
1707
|
}
|
|
1594
1708
|
toUsage(raw) {
|
|
1595
1709
|
if (!raw) return void 0;
|
|
1710
|
+
const audioInput = reportedTokenCount(raw.prompt_tokens_details?.audio_tokens);
|
|
1711
|
+
const audioOutput = reportedTokenCount(raw.completion_tokens_details?.audio_tokens);
|
|
1596
1712
|
return {
|
|
1597
1713
|
prompt_tokens: raw.prompt_tokens,
|
|
1598
1714
|
completion_tokens: raw.completion_tokens,
|
|
@@ -1602,7 +1718,9 @@ var OpenAICompatibleProvider = class {
|
|
|
1602
1718
|
// prompt_tokens_details. Preserve the normalized hit count for the
|
|
1603
1719
|
// generic table estimator; its split is validated before pricing.
|
|
1604
1720
|
cached_tokens: raw.prompt_cache_hit_tokens ?? raw.prompt_tokens_details?.cached_tokens,
|
|
1605
|
-
cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens
|
|
1721
|
+
cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens,
|
|
1722
|
+
...audioInput !== void 0 ? { audio_input_tokens: audioInput } : {},
|
|
1723
|
+
...audioOutput !== void 0 ? { audio_output_tokens: audioOutput } : {}
|
|
1606
1724
|
};
|
|
1607
1725
|
}
|
|
1608
1726
|
reportedCost(raw) {
|
|
@@ -1628,6 +1746,9 @@ var OpenAICompatibleProvider = class {
|
|
|
1628
1746
|
case "from-response":
|
|
1629
1747
|
return typeof reportedCost === "number" && Number.isFinite(reportedCost) && reportedCost >= 0 ? reportedCost : void 0;
|
|
1630
1748
|
case "table": {
|
|
1749
|
+
if (typeof usage.audio_input_tokens === "number" && usage.audio_input_tokens > 0 || typeof usage.audio_output_tokens === "number" && usage.audio_output_tokens > 0) {
|
|
1750
|
+
return void 0;
|
|
1751
|
+
}
|
|
1631
1752
|
if (!isBuiltinTablePricingEligible(this.profile, {
|
|
1632
1753
|
baseUrl: this.baseUrl,
|
|
1633
1754
|
request: context?.request,
|
|
@@ -1975,6 +2096,26 @@ var DIRECT_OPENAI_CATALOG = [
|
|
|
1975
2096
|
}),
|
|
1976
2097
|
contextBoundary: LONG_CONTEXT_BOUNDARY
|
|
1977
2098
|
},
|
|
2099
|
+
// Audio-capable models. Audio rates reviewed 2026-09-08 against
|
|
2100
|
+
// https://developers.openai.com/api/docs/pricing (no version stamp on the
|
|
2101
|
+
// page): `gpt-audio-1.5` and `gpt-audio` both list Text $2.50 in / $10.00
|
|
2102
|
+
// out and Audio $32.00 in / $64.00 out per 1M tokens, with no cached-input
|
|
2103
|
+
// row — hence no `cachedInput` here, which makes a cache-hit report unpriced
|
|
2104
|
+
// rather than silently billed at the full input rate. Only these exact ids
|
|
2105
|
+
// are listed: no snapshot id is published for either, and inventing one
|
|
2106
|
+
// would price an unknown model.
|
|
2107
|
+
{
|
|
2108
|
+
aliases: ["gpt-audio-1.5"],
|
|
2109
|
+
snapshotBase: "gpt-audio-1.5",
|
|
2110
|
+
short: { input: 2.5 / M, output: 10 / M },
|
|
2111
|
+
audio: { input: 32 / M, output: 64 / M }
|
|
2112
|
+
},
|
|
2113
|
+
{
|
|
2114
|
+
aliases: ["gpt-audio"],
|
|
2115
|
+
snapshotBase: "gpt-audio",
|
|
2116
|
+
short: { input: 2.5 / M, output: 10 / M },
|
|
2117
|
+
audio: { input: 32 / M, output: 64 / M }
|
|
2118
|
+
},
|
|
1978
2119
|
// Existing direct models retain literal current Standard rows. These flat
|
|
1979
2120
|
// rows keep native pricing aligned with the compatibility wrapper while the
|
|
1980
2121
|
// private estimator still validates categories and billing mode strictly.
|
|
@@ -2125,32 +2266,45 @@ var CANONICAL_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
|
2125
2266
|
function isTextContentPart(value, type) {
|
|
2126
2267
|
return value !== null && typeof value === "object" && value.type === type && typeof value.text === "string";
|
|
2127
2268
|
}
|
|
2128
|
-
function
|
|
2269
|
+
function isChatAudioContentPart(value) {
|
|
2270
|
+
if (value === null || typeof value !== "object") return false;
|
|
2271
|
+
const part = value;
|
|
2272
|
+
return part.type === "input_audio" && part.input_audio !== null && typeof part.input_audio === "object";
|
|
2273
|
+
}
|
|
2274
|
+
function classifyDirectOpenAIContent(request) {
|
|
2275
|
+
if (!request) return "text";
|
|
2129
2276
|
if ("messages" in request) {
|
|
2130
|
-
if (!Array.isArray(request.messages)) return
|
|
2277
|
+
if (!Array.isArray(request.messages)) return "unmodeled";
|
|
2278
|
+
let carriesAudio = false;
|
|
2131
2279
|
for (const message of request.messages) {
|
|
2132
|
-
if (message === null || typeof message !== "object") return
|
|
2280
|
+
if (message === null || typeof message !== "object") return "unmodeled";
|
|
2133
2281
|
const content = message.content;
|
|
2134
2282
|
if (content === void 0 || content === null || typeof content === "string") continue;
|
|
2135
|
-
if (!Array.isArray(content)
|
|
2136
|
-
|
|
2283
|
+
if (!Array.isArray(content)) return "unmodeled";
|
|
2284
|
+
for (const part of content) {
|
|
2285
|
+
if (isTextContentPart(part, "text")) continue;
|
|
2286
|
+
if (isChatAudioContentPart(part)) {
|
|
2287
|
+
carriesAudio = true;
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
return "unmodeled";
|
|
2137
2291
|
}
|
|
2138
2292
|
}
|
|
2139
|
-
return
|
|
2293
|
+
return carriesAudio ? "audio" : "text";
|
|
2140
2294
|
}
|
|
2141
2295
|
if ("input" in request) {
|
|
2142
2296
|
const input = request.input;
|
|
2143
|
-
if (typeof input === "string") return
|
|
2144
|
-
if (!Array.isArray(input)) return
|
|
2297
|
+
if (typeof input === "string") return "text";
|
|
2298
|
+
if (!Array.isArray(input)) return "unmodeled";
|
|
2145
2299
|
for (const item of input) {
|
|
2146
|
-
if (item === null || typeof item !== "object") return
|
|
2300
|
+
if (item === null || typeof item !== "object") return "unmodeled";
|
|
2147
2301
|
const typed = item;
|
|
2148
2302
|
switch (typed.type) {
|
|
2149
2303
|
case "message": {
|
|
2150
2304
|
const content = typed.content;
|
|
2151
2305
|
if (typeof content === "string") break;
|
|
2152
2306
|
if (!Array.isArray(content) || content.some((part) => !isTextContentPart(part, "input_text"))) {
|
|
2153
|
-
return
|
|
2307
|
+
return "unmodeled";
|
|
2154
2308
|
}
|
|
2155
2309
|
break;
|
|
2156
2310
|
}
|
|
@@ -2160,31 +2314,30 @@ function hasUnmodeledDirectOpenAIContent(request) {
|
|
|
2160
2314
|
const output = typed.output;
|
|
2161
2315
|
if (typeof output === "string") break;
|
|
2162
2316
|
if (!Array.isArray(output) || output.some((part) => !isTextContentPart(part, "input_text"))) {
|
|
2163
|
-
return
|
|
2317
|
+
return "unmodeled";
|
|
2164
2318
|
}
|
|
2165
2319
|
break;
|
|
2166
2320
|
}
|
|
2167
2321
|
case "custom_tool_call":
|
|
2168
|
-
if (typeof typed.input !== "string") return
|
|
2322
|
+
if (typeof typed.input !== "string") return "unmodeled";
|
|
2169
2323
|
break;
|
|
2170
2324
|
case "custom_tool_call_output":
|
|
2171
|
-
if (typeof typed.output !== "string") return
|
|
2325
|
+
if (typeof typed.output !== "string") return "unmodeled";
|
|
2172
2326
|
break;
|
|
2173
2327
|
case "reasoning":
|
|
2174
2328
|
break;
|
|
2175
2329
|
default:
|
|
2176
|
-
return
|
|
2330
|
+
return "unmodeled";
|
|
2177
2331
|
}
|
|
2178
2332
|
}
|
|
2179
|
-
return
|
|
2333
|
+
return "text";
|
|
2180
2334
|
}
|
|
2181
|
-
return
|
|
2335
|
+
return "text";
|
|
2182
2336
|
}
|
|
2183
|
-
function isEligibleDirectOpenAIContext(context) {
|
|
2337
|
+
function isEligibleDirectOpenAIContext(context, entry) {
|
|
2184
2338
|
const request = context?.request;
|
|
2185
2339
|
const response = context?.response;
|
|
2186
2340
|
if (context?.baseUrl !== void 0 && context.baseUrl !== CANONICAL_OPENAI_BASE_URL) return false;
|
|
2187
|
-
if (request && hasUnmodeledDirectOpenAIContent(request)) return false;
|
|
2188
2341
|
const tier = response?.service_tier ?? response?.serviceTier ?? request?.service_tier ?? request?.serviceTier;
|
|
2189
2342
|
if (tier !== void 0 && (typeof tier !== "string" || !["standard", "default"].includes(tier.toLowerCase()))) {
|
|
2190
2343
|
return false;
|
|
@@ -2199,7 +2352,14 @@ function isEligibleDirectOpenAIContext(context) {
|
|
|
2199
2352
|
if (reasoning !== null && typeof reasoning === "object" && "mode" in reasoning && reasoning.mode !== void 0) {
|
|
2200
2353
|
return false;
|
|
2201
2354
|
}
|
|
2202
|
-
|
|
2355
|
+
const audioOutputRated = entry.audio?.output !== void 0;
|
|
2356
|
+
if (request?.modalities !== void 0 && (!Array.isArray(request.modalities) || request.modalities.length === 0 || request.modalities.some(
|
|
2357
|
+
(modality) => modality !== "text" && !(modality === "audio" && audioOutputRated)
|
|
2358
|
+
))) {
|
|
2359
|
+
return false;
|
|
2360
|
+
}
|
|
2361
|
+
if (request?.audio !== void 0 && !audioOutputRated) return false;
|
|
2362
|
+
if (["image_generation", "web_search_options"].some((key) => request?.[key] !== void 0)) {
|
|
2203
2363
|
return false;
|
|
2204
2364
|
}
|
|
2205
2365
|
if (Array.isArray(request?.tools) && request.tools.some(
|
|
@@ -2210,23 +2370,42 @@ function isEligibleDirectOpenAIContext(context) {
|
|
|
2210
2370
|
return true;
|
|
2211
2371
|
}
|
|
2212
2372
|
function estimateDirectOpenAICost(model, usage, context) {
|
|
2213
|
-
if (!isEligibleDirectOpenAIContext(context)) return void 0;
|
|
2214
2373
|
const entry = directOpenAIModel(model);
|
|
2215
2374
|
if (!entry) return void 0;
|
|
2216
|
-
const
|
|
2375
|
+
const content = classifyDirectOpenAIContent(context?.request);
|
|
2376
|
+
if (content === "unmodeled") return void 0;
|
|
2377
|
+
if (!isEligibleDirectOpenAIContext(context, entry)) return void 0;
|
|
2378
|
+
const {
|
|
2379
|
+
prompt_tokens,
|
|
2380
|
+
completion_tokens,
|
|
2381
|
+
cached_tokens,
|
|
2382
|
+
cache_write_tokens,
|
|
2383
|
+
audio_input_tokens,
|
|
2384
|
+
audio_output_tokens
|
|
2385
|
+
} = usage;
|
|
2386
|
+
if (content === "audio" && audio_input_tokens === void 0) return void 0;
|
|
2217
2387
|
const cached = cached_tokens ?? 0;
|
|
2218
2388
|
const cacheWrite = cache_write_tokens ?? 0;
|
|
2219
|
-
|
|
2389
|
+
const audioInput = audio_input_tokens ?? 0;
|
|
2390
|
+
const audioOutput = audio_output_tokens ?? 0;
|
|
2391
|
+
if (![prompt_tokens, completion_tokens, cached, cacheWrite, audioInput, audioOutput].every(
|
|
2220
2392
|
(count) => Number.isSafeInteger(count) && count >= 0
|
|
2221
|
-
) || cached + cacheWrite > prompt_tokens) {
|
|
2393
|
+
) || cached + cacheWrite + audioInput > prompt_tokens || audioOutput > completion_tokens) {
|
|
2222
2394
|
return void 0;
|
|
2223
2395
|
}
|
|
2224
|
-
|
|
2396
|
+
if (audioInput > 0 && entry.audio === void 0 || audioOutput > 0 && entry.audio?.output === void 0) {
|
|
2397
|
+
return void 0;
|
|
2398
|
+
}
|
|
2399
|
+
if (audioInput > 0 && cached + cacheWrite > 0) return void 0;
|
|
2400
|
+
const crossesLongContext = entry.long !== void 0 && entry.contextBoundary !== void 0 && prompt_tokens > entry.contextBoundary;
|
|
2401
|
+
if (crossesLongContext && (audioInput > 0 || audioOutput > 0)) return void 0;
|
|
2402
|
+
const rates = crossesLongContext ? entry.long : entry.short;
|
|
2225
2403
|
if (cached > 0 && rates.cachedInput === void 0 || cacheWrite > 0 && rates.cacheWrite === void 0) {
|
|
2226
2404
|
return void 0;
|
|
2227
2405
|
}
|
|
2228
|
-
const ordinary = prompt_tokens - cached - cacheWrite;
|
|
2229
|
-
|
|
2406
|
+
const ordinary = prompt_tokens - cached - cacheWrite - audioInput;
|
|
2407
|
+
const textOutput = completion_tokens - audioOutput;
|
|
2408
|
+
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);
|
|
2230
2409
|
}
|
|
2231
2410
|
function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
|
|
2232
2411
|
return estimateDirectOpenAICost(model, {
|
|
@@ -2316,6 +2495,14 @@ var OPENAI_PROFILE = {
|
|
|
2316
2495
|
envBaseUrl: "OPENAI_BASE_URL",
|
|
2317
2496
|
pricing: { kind: "table", table: OPENAI_PRICING, match: "exact" },
|
|
2318
2497
|
reasoning: { emit: openaiReasoningEmit, capture: "none" },
|
|
2498
|
+
capabilities: {
|
|
2499
|
+
// Chat Completions carries recorded audio via `input_audio` (wav/mp3 only).
|
|
2500
|
+
// Images are deliberately NOT declared: the Responses API serves images, and
|
|
2501
|
+
// Chat-Completions image pricing is unmodeled here (fork F4).
|
|
2502
|
+
inputModalities: {
|
|
2503
|
+
audio: { sources: ["bytes", "base64"], formats: OPENAI_CHAT_AUDIO_FORMATS }
|
|
2504
|
+
}
|
|
2505
|
+
},
|
|
2319
2506
|
roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
|
|
2320
2507
|
maxTokensField: "max_completion_tokens",
|
|
2321
2508
|
parallelToolCalls: (model) => !isOSeriesModel(model)
|
|
@@ -2344,19 +2531,37 @@ function base64FromSource(source) {
|
|
|
2344
2531
|
"base64"
|
|
2345
2532
|
);
|
|
2346
2533
|
}
|
|
2347
|
-
function responseImageParts(parts) {
|
|
2534
|
+
function responseImageParts(parts, model) {
|
|
2348
2535
|
const mapped = [];
|
|
2349
2536
|
for (const part of parts) {
|
|
2350
2537
|
if (part.type === "text") {
|
|
2351
2538
|
mapped.push({ type: "input_text", text: part.text });
|
|
2352
2539
|
continue;
|
|
2353
2540
|
}
|
|
2541
|
+
if (part.type === "audio") {
|
|
2542
|
+
throw new UnsupportedModelInputError({
|
|
2543
|
+
provider: "openai-responses",
|
|
2544
|
+
model,
|
|
2545
|
+
modality: "audio",
|
|
2546
|
+
source: part.source.type,
|
|
2547
|
+
feature: "audio input"
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
if (part.type !== "image") {
|
|
2551
|
+
const unmapped = part;
|
|
2552
|
+
throw new UnsupportedModelInputError({
|
|
2553
|
+
provider: "openai-responses",
|
|
2554
|
+
model,
|
|
2555
|
+
modality: unmapped.type,
|
|
2556
|
+
feature: "this input modality"
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2354
2559
|
const { source } = part;
|
|
2355
2560
|
if (source.type === "provider-file") {
|
|
2356
2561
|
if (source.provider !== "openai-responses") {
|
|
2357
2562
|
throw new UnsupportedModelInputError({
|
|
2358
2563
|
provider: "openai-responses",
|
|
2359
|
-
model
|
|
2564
|
+
model,
|
|
2360
2565
|
modality: "image",
|
|
2361
2566
|
source: "provider-file"
|
|
2362
2567
|
});
|
|
@@ -2374,6 +2579,21 @@ function responseImageParts(parts) {
|
|
|
2374
2579
|
}
|
|
2375
2580
|
return mapped;
|
|
2376
2581
|
}
|
|
2582
|
+
function toResponsesUsage(raw) {
|
|
2583
|
+
if (!raw) return void 0;
|
|
2584
|
+
const audioInput = reportedTokenCount(raw.input_tokens_details?.audio_tokens);
|
|
2585
|
+
const audioOutput = reportedTokenCount(raw.output_tokens_details?.audio_tokens);
|
|
2586
|
+
return {
|
|
2587
|
+
prompt_tokens: raw.input_tokens,
|
|
2588
|
+
completion_tokens: raw.output_tokens,
|
|
2589
|
+
total_tokens: raw.total_tokens,
|
|
2590
|
+
reasoning_tokens: raw.output_tokens_details?.reasoning_tokens,
|
|
2591
|
+
cached_tokens: raw.input_tokens_details?.cached_tokens,
|
|
2592
|
+
cache_write_tokens: raw.input_tokens_details?.cache_write_tokens,
|
|
2593
|
+
...audioInput !== void 0 ? { audio_input_tokens: audioInput } : {},
|
|
2594
|
+
...audioOutput !== void 0 ? { audio_output_tokens: audioOutput } : {}
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2377
2597
|
var OpenAIResponsesProvider = class {
|
|
2378
2598
|
name = "openai-responses";
|
|
2379
2599
|
reportsRequestLifecycle = true;
|
|
@@ -2389,15 +2609,18 @@ var OpenAIResponsesProvider = class {
|
|
|
2389
2609
|
validateInput(request) {
|
|
2390
2610
|
const modelOverride = request.providerOptions?.model;
|
|
2391
2611
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
2392
|
-
const
|
|
2612
|
+
const failWith = (modality, source, feature) => {
|
|
2393
2613
|
throw new UnsupportedModelInputError({
|
|
2394
2614
|
provider: this.name,
|
|
2395
2615
|
model: effectiveModel || request.model,
|
|
2396
|
-
modality
|
|
2616
|
+
modality,
|
|
2397
2617
|
...source ? { source } : {},
|
|
2398
2618
|
...feature ? { feature } : {}
|
|
2399
2619
|
});
|
|
2400
2620
|
};
|
|
2621
|
+
const fail = (source, feature) => failWith("image", source, feature);
|
|
2622
|
+
const audioPart = firstRichPart(request.input, request.history, "audio");
|
|
2623
|
+
if (audioPart) failWith("audio", audioPart.source.type, "audio input");
|
|
2401
2624
|
if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
|
|
2402
2625
|
fail(void 0, "invalid model providerOptions");
|
|
2403
2626
|
}
|
|
@@ -2544,7 +2767,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2544
2767
|
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
2545
2768
|
const body = {
|
|
2546
2769
|
model: effectiveModel,
|
|
2547
|
-
input: this.buildInput(nonSystemMessages),
|
|
2770
|
+
input: this.buildInput(nonSystemMessages, effectiveModel),
|
|
2548
2771
|
store: false,
|
|
2549
2772
|
stream
|
|
2550
2773
|
};
|
|
@@ -2596,7 +2819,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2596
2819
|
// ---------------------------------------------------------------------------
|
|
2597
2820
|
// Internal: message → input mapping
|
|
2598
2821
|
// ---------------------------------------------------------------------------
|
|
2599
|
-
buildInput(messages) {
|
|
2822
|
+
buildInput(messages, model) {
|
|
2600
2823
|
const input = [];
|
|
2601
2824
|
for (const msg of messages) {
|
|
2602
2825
|
const text = typeof msg.content === "string" ? msg.content : "";
|
|
@@ -2634,7 +2857,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2634
2857
|
input.push({
|
|
2635
2858
|
type: "message",
|
|
2636
2859
|
role: msg.role,
|
|
2637
|
-
content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content)
|
|
2860
|
+
content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content, model)
|
|
2638
2861
|
});
|
|
2639
2862
|
}
|
|
2640
2863
|
}
|
|
@@ -2693,14 +2916,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2693
2916
|
}
|
|
2694
2917
|
}
|
|
2695
2918
|
}
|
|
2696
|
-
const usage = json.usage
|
|
2697
|
-
prompt_tokens: json.usage.input_tokens,
|
|
2698
|
-
completion_tokens: json.usage.output_tokens,
|
|
2699
|
-
total_tokens: json.usage.total_tokens,
|
|
2700
|
-
reasoning_tokens: json.usage.output_tokens_details?.reasoning_tokens,
|
|
2701
|
-
cached_tokens: json.usage.input_tokens_details?.cached_tokens,
|
|
2702
|
-
cache_write_tokens: json.usage.input_tokens_details?.cache_write_tokens
|
|
2703
|
-
} : void 0;
|
|
2919
|
+
const usage = toResponsesUsage(json.usage);
|
|
2704
2920
|
const cost = usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(json.model ?? model, usage, {
|
|
2705
2921
|
baseUrl: this.baseUrl,
|
|
2706
2922
|
request,
|
|
@@ -2807,14 +3023,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2807
3023
|
}
|
|
2808
3024
|
case "response.completed": {
|
|
2809
3025
|
const response = data.response;
|
|
2810
|
-
const usage = response?.usage
|
|
2811
|
-
prompt_tokens: response.usage.input_tokens,
|
|
2812
|
-
completion_tokens: response.usage.output_tokens,
|
|
2813
|
-
total_tokens: response.usage.total_tokens,
|
|
2814
|
-
reasoning_tokens: response.usage.output_tokens_details?.reasoning_tokens,
|
|
2815
|
-
cached_tokens: response.usage.input_tokens_details?.cached_tokens,
|
|
2816
|
-
cache_write_tokens: response.usage.input_tokens_details?.cache_write_tokens
|
|
2817
|
-
} : void 0;
|
|
3026
|
+
const usage = toResponsesUsage(response?.usage);
|
|
2818
3027
|
const reasoningItems = response?.output?.filter((item) => item.type === "reasoning") ?? [];
|
|
2819
3028
|
const providerMetadata = reasoningItems.length > 0 ? { openaiReasoningItems: reasoningItems } : void 0;
|
|
2820
3029
|
return {
|
|
@@ -2863,19 +3072,37 @@ function anthropicBase64(source) {
|
|
|
2863
3072
|
"base64"
|
|
2864
3073
|
);
|
|
2865
3074
|
}
|
|
2866
|
-
function anthropicImageBlocks(parts) {
|
|
3075
|
+
function anthropicImageBlocks(parts, model) {
|
|
2867
3076
|
const blocks = [];
|
|
2868
3077
|
for (const part of parts) {
|
|
2869
3078
|
if (part.type === "text") {
|
|
2870
3079
|
blocks.push({ type: "text", text: part.text });
|
|
2871
3080
|
continue;
|
|
2872
3081
|
}
|
|
3082
|
+
if (part.type === "audio") {
|
|
3083
|
+
throw new UnsupportedModelInputError({
|
|
3084
|
+
provider: "anthropic",
|
|
3085
|
+
model,
|
|
3086
|
+
modality: "audio",
|
|
3087
|
+
source: part.source.type,
|
|
3088
|
+
feature: "audio input"
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
if (part.type !== "image") {
|
|
3092
|
+
const unmapped = part;
|
|
3093
|
+
throw new UnsupportedModelInputError({
|
|
3094
|
+
provider: "anthropic",
|
|
3095
|
+
model,
|
|
3096
|
+
modality: unmapped.type,
|
|
3097
|
+
feature: "this input modality"
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
2873
3100
|
const { source } = part;
|
|
2874
3101
|
if (source.type === "provider-file") {
|
|
2875
3102
|
if (source.provider !== "anthropic") {
|
|
2876
3103
|
throw new UnsupportedModelInputError({
|
|
2877
3104
|
provider: "anthropic",
|
|
2878
|
-
model
|
|
3105
|
+
model,
|
|
2879
3106
|
modality: "image",
|
|
2880
3107
|
source: "provider-file"
|
|
2881
3108
|
});
|
|
@@ -3337,15 +3564,18 @@ var AnthropicProvider = class {
|
|
|
3337
3564
|
validateInput(request) {
|
|
3338
3565
|
const modelOverride = request.providerOptions?.model;
|
|
3339
3566
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
3340
|
-
const
|
|
3567
|
+
const failWith = (modality, source, feature) => {
|
|
3341
3568
|
throw new UnsupportedModelInputError({
|
|
3342
3569
|
provider: this.name,
|
|
3343
3570
|
model: effectiveModel || request.model,
|
|
3344
|
-
modality
|
|
3571
|
+
modality,
|
|
3345
3572
|
...source ? { source } : {},
|
|
3346
3573
|
...feature ? { feature } : {}
|
|
3347
3574
|
});
|
|
3348
3575
|
};
|
|
3576
|
+
const fail = (source, feature) => failWith("image", source, feature);
|
|
3577
|
+
const audioPart = firstRichPart(request.input, request.history, "audio");
|
|
3578
|
+
if (audioPart) failWith("audio", audioPart.source.type, "audio input");
|
|
3349
3579
|
if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
|
|
3350
3580
|
fail(void 0, "invalid model providerOptions");
|
|
3351
3581
|
}
|
|
@@ -3516,7 +3746,7 @@ var AnthropicProvider = class {
|
|
|
3516
3746
|
const systemText = systemMessages.map((m) => m.content).join("\n\n");
|
|
3517
3747
|
const body = {
|
|
3518
3748
|
model: effectiveModel,
|
|
3519
|
-
messages: this.mapMessages(nonSystemMessages),
|
|
3749
|
+
messages: this.mapMessages(nonSystemMessages, effectiveModel),
|
|
3520
3750
|
max_tokens: options.maxTokens ?? 4096,
|
|
3521
3751
|
stream
|
|
3522
3752
|
};
|
|
@@ -3583,7 +3813,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3583
3813
|
* - assistant messages with tool_calls -> assistant with tool_use content blocks
|
|
3584
3814
|
* - tool messages (tool results) -> user messages with tool_result content blocks
|
|
3585
3815
|
*/
|
|
3586
|
-
mapMessages(messages) {
|
|
3816
|
+
mapMessages(messages, model) {
|
|
3587
3817
|
const result = [];
|
|
3588
3818
|
for (const msg of messages) {
|
|
3589
3819
|
const text = typeof msg.content === "string" ? msg.content : "";
|
|
@@ -3630,7 +3860,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3630
3860
|
} else if (msg.role === "user") {
|
|
3631
3861
|
result.push({
|
|
3632
3862
|
role: "user",
|
|
3633
|
-
content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content)
|
|
3863
|
+
content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content, model)
|
|
3634
3864
|
});
|
|
3635
3865
|
}
|
|
3636
3866
|
}
|
|
@@ -3934,7 +4164,7 @@ function lastFallbackIndex(blocks) {
|
|
|
3934
4164
|
function hasRichGeminiMessages(messages) {
|
|
3935
4165
|
return messages.some((message) => Array.isArray(message.content));
|
|
3936
4166
|
}
|
|
3937
|
-
function
|
|
4167
|
+
function geminiInlineBase64(source) {
|
|
3938
4168
|
return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
|
|
3939
4169
|
"base64"
|
|
3940
4170
|
);
|
|
@@ -3946,6 +4176,50 @@ function geminiInteractionContent(parts, model) {
|
|
|
3946
4176
|
content.push({ type: "text", text: part.text });
|
|
3947
4177
|
continue;
|
|
3948
4178
|
}
|
|
4179
|
+
if (part.type === "audio") {
|
|
4180
|
+
const { source: audioSource } = part;
|
|
4181
|
+
if (audioSource.type === "provider-file") {
|
|
4182
|
+
if (audioSource.provider !== "google") {
|
|
4183
|
+
throw new UnsupportedModelInputError({
|
|
4184
|
+
provider: "google",
|
|
4185
|
+
model,
|
|
4186
|
+
modality: "audio",
|
|
4187
|
+
source: "provider-file"
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
if (!audioSource.mediaType) {
|
|
4191
|
+
throw new UnsupportedModelInputError({
|
|
4192
|
+
provider: "google",
|
|
4193
|
+
model,
|
|
4194
|
+
modality: "audio",
|
|
4195
|
+
source: "provider-file",
|
|
4196
|
+
feature: "Interactions URI audio mediaType"
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
content.push({
|
|
4200
|
+
type: "audio",
|
|
4201
|
+
uri: audioSource.reference,
|
|
4202
|
+
mime_type: audioSource.mediaType
|
|
4203
|
+
});
|
|
4204
|
+
} else {
|
|
4205
|
+
content.push({
|
|
4206
|
+
type: "audio",
|
|
4207
|
+
data: geminiInlineBase64(audioSource),
|
|
4208
|
+
mime_type: audioSource.mediaType
|
|
4209
|
+
});
|
|
4210
|
+
}
|
|
4211
|
+
if (part.label) content.push({ type: "text", text: `[Audio: ${part.label}]` });
|
|
4212
|
+
continue;
|
|
4213
|
+
}
|
|
4214
|
+
if (part.type !== "image") {
|
|
4215
|
+
const unmapped = part;
|
|
4216
|
+
throw new UnsupportedModelInputError({
|
|
4217
|
+
provider: "google",
|
|
4218
|
+
model,
|
|
4219
|
+
modality: unmapped.type,
|
|
4220
|
+
feature: "this input modality"
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
3949
4223
|
const { source } = part;
|
|
3950
4224
|
if (source.type === "provider-file") {
|
|
3951
4225
|
if (source.provider !== "google") {
|
|
@@ -3979,7 +4253,11 @@ function geminiInteractionContent(parts, model) {
|
|
|
3979
4253
|
feature: "direct URL image input; pass bytes/base64 or a Gemini provider-file"
|
|
3980
4254
|
});
|
|
3981
4255
|
} else {
|
|
3982
|
-
content.push({
|
|
4256
|
+
content.push({
|
|
4257
|
+
type: "image",
|
|
4258
|
+
data: geminiInlineBase64(source),
|
|
4259
|
+
mime_type: source.mediaType
|
|
4260
|
+
});
|
|
3983
4261
|
}
|
|
3984
4262
|
if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
|
|
3985
4263
|
}
|
|
@@ -4036,7 +4314,7 @@ var GEMINI_PRICING = {
|
|
|
4036
4314
|
output: 1e-5,
|
|
4037
4315
|
longContext: { input: 25e-7, cached: 25e-8, output: 15e-6 }
|
|
4038
4316
|
},
|
|
4039
|
-
"gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
4317
|
+
"gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7, audioInput: 1e-6 },
|
|
4040
4318
|
"gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
|
|
4041
4319
|
"gemini-3.1-pro-preview": {
|
|
4042
4320
|
input: 2e-6,
|
|
@@ -4054,18 +4332,43 @@ var GEMINI_PRICING = {
|
|
|
4054
4332
|
"gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
|
|
4055
4333
|
"gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
4056
4334
|
"gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
|
|
4057
|
-
"gemini-3.7-flash": {
|
|
4335
|
+
"gemini-3.7-flash": {
|
|
4336
|
+
input: 75e-8,
|
|
4337
|
+
cached: 75e-9,
|
|
4338
|
+
output: 375e-8,
|
|
4339
|
+
audioInput: 75e-8
|
|
4340
|
+
},
|
|
4058
4341
|
"gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
|
|
4059
4342
|
};
|
|
4343
|
+
var CANONICAL_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
4060
4344
|
function isValidTokenCount2(value) {
|
|
4061
4345
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
4062
4346
|
}
|
|
4063
4347
|
function isStandardGeminiRequestTier(value) {
|
|
4064
|
-
return value === void 0 || value ===
|
|
4348
|
+
return value === void 0 || value === null || isDefinitiveStandardGeminiResponseTier(value);
|
|
4065
4349
|
}
|
|
4066
4350
|
function isDefinitiveStandardGeminiResponseTier(value) {
|
|
4067
4351
|
return value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD" || value === "standard";
|
|
4068
4352
|
}
|
|
4353
|
+
function recordGeminiResponseTier(evidence, value) {
|
|
4354
|
+
if (value === void 0) return;
|
|
4355
|
+
evidence.reportsTier = true;
|
|
4356
|
+
if (isDefinitiveStandardGeminiResponseTier(value)) {
|
|
4357
|
+
evidence.hasDefinitiveStandardTier = true;
|
|
4358
|
+
} else {
|
|
4359
|
+
evidence.hasInvalidTier = true;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
function geminiResponseTierEvidence(responseHeaders, ...values) {
|
|
4363
|
+
const evidence = {
|
|
4364
|
+
reportsTier: false,
|
|
4365
|
+
hasDefinitiveStandardTier: false,
|
|
4366
|
+
hasInvalidTier: false
|
|
4367
|
+
};
|
|
4368
|
+
recordGeminiResponseTier(evidence, responseHeaders?.get("x-gemini-service-tier") ?? void 0);
|
|
4369
|
+
for (const value of values) recordGeminiResponseTier(evidence, value);
|
|
4370
|
+
return evidence;
|
|
4371
|
+
}
|
|
4069
4372
|
function geminiInteractionErrorStatus(code) {
|
|
4070
4373
|
if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) {
|
|
4071
4374
|
return code;
|
|
@@ -4124,8 +4427,19 @@ function estimateGeminiCost(model, usage) {
|
|
|
4124
4427
|
if (!pricing || !isValidTokenCount2(usage.inputTokens) || !isValidTokenCount2(usage.outputTokens) || !isValidTokenCount2(cached) || cached > usage.inputTokens) {
|
|
4125
4428
|
return void 0;
|
|
4126
4429
|
}
|
|
4127
|
-
const
|
|
4128
|
-
|
|
4430
|
+
const split = usage.modalities;
|
|
4431
|
+
const audio = split?.audioTokens ?? 0;
|
|
4432
|
+
if (split !== void 0) {
|
|
4433
|
+
if (audio > 0 && pricing.audioInput === void 0) return void 0;
|
|
4434
|
+
if (!isValidTokenCount2(audio) || !isValidTokenCount2(split.nonAudioTokens) || audio + split.nonAudioTokens !== usage.inputTokens || cached + audio > usage.inputTokens) {
|
|
4435
|
+
return void 0;
|
|
4436
|
+
}
|
|
4437
|
+
if (audio > 0 && cached > 0) return void 0;
|
|
4438
|
+
}
|
|
4439
|
+
const crossesLongContext = usage.inputTokens > 2e5 && pricing.longContext !== void 0;
|
|
4440
|
+
if (crossesLongContext && audio > 0) return void 0;
|
|
4441
|
+
const rate = crossesLongContext ? pricing.longContext : pricing;
|
|
4442
|
+
return (usage.inputTokens - cached - audio) * rate.input + cached * rate.cached + audio * (pricing.audioInput ?? 0) + usage.outputTokens * rate.output;
|
|
4129
4443
|
}
|
|
4130
4444
|
function safeTokenSum2(...values) {
|
|
4131
4445
|
const total = values.reduce((sum, value) => sum + value, 0);
|
|
@@ -4285,20 +4599,44 @@ var GeminiProvider = class {
|
|
|
4285
4599
|
name = "google";
|
|
4286
4600
|
reportsRequestLifecycle = true;
|
|
4287
4601
|
inputCapabilities(model) {
|
|
4288
|
-
return model.trim().length > 0 ? {
|
|
4602
|
+
return model.trim().length > 0 ? {
|
|
4603
|
+
image: { sources: ["bytes", "base64", "provider-file"] },
|
|
4604
|
+
audio: { sources: ["bytes", "base64", "provider-file"] }
|
|
4605
|
+
} : {};
|
|
4289
4606
|
}
|
|
4290
4607
|
validateInput(request) {
|
|
4291
4608
|
const modelOverride = request.providerOptions?.model;
|
|
4292
4609
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
4293
|
-
const
|
|
4610
|
+
const failWith = (modality, source, feature) => {
|
|
4294
4611
|
throw new UnsupportedModelInputError({
|
|
4295
4612
|
provider: this.name,
|
|
4296
4613
|
model: effectiveModel || request.model,
|
|
4297
|
-
modality
|
|
4614
|
+
modality,
|
|
4298
4615
|
...source ? { source } : {},
|
|
4299
4616
|
...feature ? { feature } : {}
|
|
4300
4617
|
});
|
|
4301
4618
|
};
|
|
4619
|
+
const fail = (source, feature) => failWith(firstRichPart(request.input, request.history)?.type ?? "image", source, feature);
|
|
4620
|
+
const checkPart = (part) => {
|
|
4621
|
+
if (part.type === "audio") {
|
|
4622
|
+
if (part.source.type === "provider-file" && part.source.provider !== this.name)
|
|
4623
|
+
failWith("audio", "provider-file");
|
|
4624
|
+
if (part.source.type === "provider-file" && !part.source.mediaType)
|
|
4625
|
+
failWith("audio", "provider-file", "Interactions URI audio mediaType");
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
4628
|
+
if (part.type !== "image") return;
|
|
4629
|
+
if (part.source.type === "url")
|
|
4630
|
+
failWith(
|
|
4631
|
+
"image",
|
|
4632
|
+
"url",
|
|
4633
|
+
"direct URL image input; pass bytes/base64 or a Gemini provider-file"
|
|
4634
|
+
);
|
|
4635
|
+
if (part.source.type === "provider-file" && part.source.provider !== this.name)
|
|
4636
|
+
failWith("image", "provider-file");
|
|
4637
|
+
if (part.source.type === "provider-file" && !part.source.mediaType)
|
|
4638
|
+
failWith("image", "provider-file", "Interactions URI image mediaType");
|
|
4639
|
+
};
|
|
4302
4640
|
if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
|
|
4303
4641
|
fail(void 0, "invalid model providerOptions");
|
|
4304
4642
|
}
|
|
@@ -4320,26 +4658,10 @@ var GeminiProvider = class {
|
|
|
4320
4658
|
for (const message of request.history) {
|
|
4321
4659
|
if (!Array.isArray(message.content)) continue;
|
|
4322
4660
|
if (message.role !== "user") fail(void 0, "rich non-user history");
|
|
4323
|
-
for (const part of message.content)
|
|
4324
|
-
if (part.type !== "image") continue;
|
|
4325
|
-
if (part.source.type === "url")
|
|
4326
|
-
fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
|
|
4327
|
-
if (part.source.type === "provider-file" && part.source.provider !== this.name)
|
|
4328
|
-
fail("provider-file");
|
|
4329
|
-
if (part.source.type === "provider-file" && !part.source.mediaType)
|
|
4330
|
-
fail("provider-file", "Interactions URI image mediaType");
|
|
4331
|
-
}
|
|
4661
|
+
for (const part of message.content) checkPart(part);
|
|
4332
4662
|
}
|
|
4333
4663
|
if (Array.isArray(request.input)) {
|
|
4334
|
-
for (const part of request.input)
|
|
4335
|
-
if (part.type !== "image") continue;
|
|
4336
|
-
if (part.source.type === "url")
|
|
4337
|
-
fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
|
|
4338
|
-
if (part.source.type === "provider-file" && part.source.provider !== this.name)
|
|
4339
|
-
fail("provider-file");
|
|
4340
|
-
if (part.source.type === "provider-file" && !part.source.mediaType)
|
|
4341
|
-
fail("provider-file", "Interactions URI image mediaType");
|
|
4342
|
-
}
|
|
4664
|
+
for (const part of request.input) checkPart(part);
|
|
4343
4665
|
}
|
|
4344
4666
|
return { effectiveModel };
|
|
4345
4667
|
}
|
|
@@ -4475,6 +4797,7 @@ var GeminiProvider = class {
|
|
|
4475
4797
|
async chatInteraction(messages, options) {
|
|
4476
4798
|
this.assertSafeInteractionOptions(options);
|
|
4477
4799
|
const body = this.buildInteractionRequestBody(messages, options, false);
|
|
4800
|
+
const pricingContext = this.interactionPricingContext(this.requestModel(options), body);
|
|
4478
4801
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4479
4802
|
const recorder = new CallTimingRecorder(options.requestLifecycle);
|
|
4480
4803
|
const res = await fetchWithRetry(
|
|
@@ -4495,12 +4818,17 @@ var GeminiProvider = class {
|
|
|
4495
4818
|
}
|
|
4496
4819
|
return withChatTiming(
|
|
4497
4820
|
recorder,
|
|
4498
|
-
this.parseInteractionResponse(
|
|
4821
|
+
this.parseInteractionResponse(
|
|
4822
|
+
await res.json(),
|
|
4823
|
+
pricingContext,
|
|
4824
|
+
res.headers
|
|
4825
|
+
)
|
|
4499
4826
|
);
|
|
4500
4827
|
}
|
|
4501
4828
|
async *streamInteraction(messages, options) {
|
|
4502
4829
|
this.assertSafeInteractionOptions(options);
|
|
4503
4830
|
const body = this.buildInteractionRequestBody(messages, options, true);
|
|
4831
|
+
const pricingContext = this.interactionPricingContext(this.requestModel(options), body);
|
|
4504
4832
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4505
4833
|
const recorder = new CallTimingRecorder(options.requestLifecycle);
|
|
4506
4834
|
const res = await fetchWithRetry(
|
|
@@ -4522,7 +4850,7 @@ var GeminiProvider = class {
|
|
|
4522
4850
|
if (!res.body) throw new Error("Gemini Interactions stream has no body");
|
|
4523
4851
|
yield* withCallTiming(
|
|
4524
4852
|
recorder,
|
|
4525
|
-
this.parseInteractionSSEStream(res.body, res.headers, recorder)
|
|
4853
|
+
this.parseInteractionSSEStream(res.body, res.headers, recorder, pricingContext)
|
|
4526
4854
|
);
|
|
4527
4855
|
}
|
|
4528
4856
|
buildInteractionRequestBody(messages, options, stream) {
|
|
@@ -4617,6 +4945,7 @@ var GeminiProvider = class {
|
|
|
4617
4945
|
}
|
|
4618
4946
|
mapInteractionInput(messages, model) {
|
|
4619
4947
|
const steps = [];
|
|
4948
|
+
const functionNames = /* @__PURE__ */ new Map();
|
|
4620
4949
|
for (const message of messages) {
|
|
4621
4950
|
if (message.role === "user") {
|
|
4622
4951
|
steps.push({
|
|
@@ -4626,6 +4955,10 @@ var GeminiProvider = class {
|
|
|
4626
4955
|
} else if (message.role === "assistant") {
|
|
4627
4956
|
const nativeSteps = message.providerMetadata?.geminiInteractionSteps;
|
|
4628
4957
|
if (Array.isArray(nativeSteps) && nativeSteps.every(isGeminiInteractionStep)) {
|
|
4958
|
+
for (const step of nativeSteps) {
|
|
4959
|
+
if (step.type === "function_call" && typeof step.id === "string" && step.name)
|
|
4960
|
+
functionNames.set(step.id, step.name);
|
|
4961
|
+
}
|
|
4629
4962
|
steps.push(...nativeSteps);
|
|
4630
4963
|
continue;
|
|
4631
4964
|
}
|
|
@@ -4633,6 +4966,7 @@ var GeminiProvider = class {
|
|
|
4633
4966
|
steps.push({ type: "model_output", content: [{ type: "text", text: message.content }] });
|
|
4634
4967
|
}
|
|
4635
4968
|
for (const call of message.tool_calls ?? []) {
|
|
4969
|
+
functionNames.set(call.id, call.function.name);
|
|
4636
4970
|
steps.push({
|
|
4637
4971
|
type: "function_call",
|
|
4638
4972
|
id: call.id,
|
|
@@ -4641,9 +4975,17 @@ var GeminiProvider = class {
|
|
|
4641
4975
|
});
|
|
4642
4976
|
}
|
|
4643
4977
|
} else if (message.role === "tool") {
|
|
4978
|
+
const callId = message.tool_call_id ?? "";
|
|
4979
|
+
const name = functionNames.get(callId) ?? message.name;
|
|
4980
|
+
if (!name) {
|
|
4981
|
+
throw new InvalidModelInputError(
|
|
4982
|
+
`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'`
|
|
4983
|
+
);
|
|
4984
|
+
}
|
|
4644
4985
|
steps.push({
|
|
4645
4986
|
type: "function_result",
|
|
4646
|
-
|
|
4987
|
+
name,
|
|
4988
|
+
call_id: callId,
|
|
4647
4989
|
result: [
|
|
4648
4990
|
{ type: "text", text: typeof message.content === "string" ? message.content : "" }
|
|
4649
4991
|
]
|
|
@@ -4652,7 +4994,7 @@ var GeminiProvider = class {
|
|
|
4652
4994
|
}
|
|
4653
4995
|
return steps;
|
|
4654
4996
|
}
|
|
4655
|
-
parseInteractionResponse(json) {
|
|
4997
|
+
parseInteractionResponse(json, pricingContext, responseHeaders) {
|
|
4656
4998
|
let content = "";
|
|
4657
4999
|
let thinkingContent = "";
|
|
4658
5000
|
const toolCalls = [];
|
|
@@ -4668,18 +5010,63 @@ var GeminiProvider = class {
|
|
|
4668
5010
|
}
|
|
4669
5011
|
}
|
|
4670
5012
|
this.assertInteractionTerminalStatus(json.status, toolCalls.length);
|
|
4671
|
-
const
|
|
5013
|
+
const normalized = normalizeInteractionUsage(json.usage);
|
|
4672
5014
|
return {
|
|
4673
5015
|
content,
|
|
4674
5016
|
thinking_content: thinkingContent || void 0,
|
|
4675
5017
|
tool_calls: toolCalls.length ? toolCalls : void 0,
|
|
4676
|
-
usage,
|
|
4677
|
-
|
|
4678
|
-
|
|
5018
|
+
usage: normalized?.usage,
|
|
5019
|
+
cost: this.interactionCost({
|
|
5020
|
+
normalized,
|
|
5021
|
+
steps: json.steps,
|
|
5022
|
+
responseTierEvidence: geminiResponseTierEvidence(
|
|
5023
|
+
responseHeaders,
|
|
5024
|
+
json.service_tier,
|
|
5025
|
+
json.usage?.service_tier
|
|
5026
|
+
),
|
|
5027
|
+
effectiveModel: typeof json.model === "string" ? json.model : pricingContext.model,
|
|
5028
|
+
pricingContext,
|
|
5029
|
+
// The whole body is in hand: every part is on `json.steps`, so there
|
|
5030
|
+
// is nothing this path could have failed to materialize.
|
|
5031
|
+
unmodeledOutput: false
|
|
5032
|
+
}),
|
|
4679
5033
|
providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
|
|
4680
5034
|
};
|
|
4681
5035
|
}
|
|
4682
|
-
|
|
5036
|
+
/**
|
|
5037
|
+
* Cost for one Interactions call, or `undefined` when any precondition
|
|
5038
|
+
* fails. Streaming and non-streaming share this one body, so identical usage
|
|
5039
|
+
* always produces an identical number on both paths.
|
|
5040
|
+
*/
|
|
5041
|
+
interactionCost(call) {
|
|
5042
|
+
const { normalized, steps, responseTierEvidence, effectiveModel, pricingContext } = call;
|
|
5043
|
+
if (!normalized?.pricingUsage || normalized.hasUnmodeledBilledUsage) return void 0;
|
|
5044
|
+
if (call.unmodeledOutput || !this.isTextOnlyInteractionOutput(steps)) return void 0;
|
|
5045
|
+
if (!isEligibleGeminiPricing(
|
|
5046
|
+
pricingContext,
|
|
5047
|
+
!responseTierEvidence.reportsTier || responseTierEvidence.hasDefinitiveStandardTier,
|
|
5048
|
+
responseTierEvidence.hasInvalidTier
|
|
5049
|
+
)) {
|
|
5050
|
+
return void 0;
|
|
5051
|
+
}
|
|
5052
|
+
return estimateGeminiCost(effectiveModel, normalized.pricingUsage);
|
|
5053
|
+
}
|
|
5054
|
+
/**
|
|
5055
|
+
* Whether every model-produced step is text. A non-text reply part (image,
|
|
5056
|
+
* audio, inline data) is billed on an output track this adapter carries no
|
|
5057
|
+
* rate for, so the call stays unpriced. Echoed `user_input` and
|
|
5058
|
+
* Axl-authored `function_result` steps are not model output and are ignored.
|
|
5059
|
+
*/
|
|
5060
|
+
isTextOnlyInteractionOutput(steps) {
|
|
5061
|
+
return (steps ?? []).every((step) => {
|
|
5062
|
+
if (step.type !== "model_output" && step.type !== "thought") return true;
|
|
5063
|
+
const parts = [...step.content ?? [], ...step.summary ?? []];
|
|
5064
|
+
return parts.every(
|
|
5065
|
+
(part) => part !== null && typeof part === "object" && part.type === "text"
|
|
5066
|
+
);
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
async *parseInteractionSSEStream(body, responseHeaders, timing, pricingContext) {
|
|
4683
5070
|
const reader = body.getReader();
|
|
4684
5071
|
const decoder = new TextDecoder();
|
|
4685
5072
|
let buffer = "";
|
|
@@ -4687,6 +5074,8 @@ var GeminiProvider = class {
|
|
|
4687
5074
|
const argumentBuffers = /* @__PURE__ */ new Map();
|
|
4688
5075
|
const stopped = /* @__PURE__ */ new Set();
|
|
4689
5076
|
let completed = false;
|
|
5077
|
+
const responseTierEvidence = geminiResponseTierEvidence(responseHeaders);
|
|
5078
|
+
let unmodeledOutput = false;
|
|
4690
5079
|
try {
|
|
4691
5080
|
while (true) {
|
|
4692
5081
|
const { done, value } = await timing.read(reader);
|
|
@@ -4704,6 +5093,8 @@ var GeminiProvider = class {
|
|
|
4704
5093
|
} catch {
|
|
4705
5094
|
continue;
|
|
4706
5095
|
}
|
|
5096
|
+
recordGeminiResponseTier(responseTierEvidence, event.interaction?.service_tier);
|
|
5097
|
+
recordGeminiResponseTier(responseTierEvidence, event.interaction?.usage?.service_tier);
|
|
4707
5098
|
if (event.event_type === "error") {
|
|
4708
5099
|
const status = geminiInteractionErrorStatus(event.error?.code);
|
|
4709
5100
|
const message = event.error?.message ?? "Gemini Interactions stream failed";
|
|
@@ -4731,6 +5122,7 @@ var GeminiProvider = class {
|
|
|
4731
5122
|
}
|
|
4732
5123
|
const step = cloneInteractionStep(event.step);
|
|
4733
5124
|
steps.set(index, step);
|
|
5125
|
+
if (!this.isTextOnlyInteractionOutput([step])) unmodeledOutput = true;
|
|
4734
5126
|
if (step.type === "model_output") {
|
|
4735
5127
|
for (const text of interactionTextChunks(step.content)) {
|
|
4736
5128
|
yield { type: "text_delta", content: text };
|
|
@@ -4753,6 +5145,12 @@ var GeminiProvider = class {
|
|
|
4753
5145
|
const step = steps.get(index);
|
|
4754
5146
|
if (!step)
|
|
4755
5147
|
throw new Error("Gemini Interactions stream delta arrived before step.start");
|
|
5148
|
+
if (step.type === "model_output" && !(delta.type === "text" && typeof delta.text === "string")) {
|
|
5149
|
+
unmodeledOutput = true;
|
|
5150
|
+
}
|
|
5151
|
+
if (step.type === "thought" && !(delta.type === "thought_summary" && !!delta.content) && !(delta.type === "thought_signature" && typeof delta.signature === "string")) {
|
|
5152
|
+
unmodeledOutput = true;
|
|
5153
|
+
}
|
|
4756
5154
|
if (delta.type === "text" && typeof delta.text === "string" && step.type === "model_output") {
|
|
4757
5155
|
appendInteractionContent(step, "content", { type: "text", text: delta.text });
|
|
4758
5156
|
yield { type: "text_delta", content: delta.text };
|
|
@@ -4800,12 +5198,20 @@ var GeminiProvider = class {
|
|
|
4800
5198
|
).length;
|
|
4801
5199
|
this.assertInteractionTerminalStatus(event.interaction?.status, toolCallCount);
|
|
4802
5200
|
completed = true;
|
|
5201
|
+
const orderedSteps = [...steps.entries()].sort(([a], [b]) => a - b).map(([, step]) => step);
|
|
5202
|
+
const normalized = normalizeInteractionUsage(event.interaction?.usage);
|
|
4803
5203
|
yield {
|
|
4804
5204
|
type: "done",
|
|
4805
|
-
usage:
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
5205
|
+
usage: normalized?.usage,
|
|
5206
|
+
cost: this.interactionCost({
|
|
5207
|
+
normalized,
|
|
5208
|
+
steps: orderedSteps,
|
|
5209
|
+
responseTierEvidence,
|
|
5210
|
+
effectiveModel: typeof event.interaction?.model === "string" ? event.interaction.model : pricingContext.model,
|
|
5211
|
+
pricingContext,
|
|
5212
|
+
unmodeledOutput
|
|
5213
|
+
}),
|
|
5214
|
+
providerMetadata: steps.size ? { geminiInteractionSteps: orderedSteps } : void 0
|
|
4809
5215
|
};
|
|
4810
5216
|
return;
|
|
4811
5217
|
}
|
|
@@ -4847,6 +5253,51 @@ var GeminiProvider = class {
|
|
|
4847
5253
|
}
|
|
4848
5254
|
return `Gemini API error (${status}): ${body}`;
|
|
4849
5255
|
}
|
|
5256
|
+
/**
|
|
5257
|
+
* Pricing context for the stateless Interactions transport.
|
|
5258
|
+
*
|
|
5259
|
+
* The endpoint reports a per-modality input split, so a rich call IS
|
|
5260
|
+
* priceable — but only from the canonical endpoint, on a standard tier, with
|
|
5261
|
+
* a request whose billing is fully represented by the token totals.
|
|
5262
|
+
*/
|
|
5263
|
+
interactionPricingContext(fallbackModel, body) {
|
|
5264
|
+
return {
|
|
5265
|
+
model: typeof body.model === "string" ? body.model : fallbackModel,
|
|
5266
|
+
serviceTier: body.service_tier ?? body.serviceTier,
|
|
5267
|
+
eligibleRequest: this.baseUrl === CANONICAL_GEMINI_BASE_URL && isStandardGeminiRequestTier(body.serviceTier) && this.isModeledInteractionRequest(body)
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5270
|
+
/**
|
|
5271
|
+
* Whether an Interactions request body's billing is fully represented by the
|
|
5272
|
+
* reported token totals. `providerOptions` merges arbitrary keys into the
|
|
5273
|
+
* body, so anything that can move billing off the local totals — a
|
|
5274
|
+
* server-side cache, a server-executed tool, a residency/region override, a
|
|
5275
|
+
* non-text response modality — makes the call unpriced.
|
|
5276
|
+
*
|
|
5277
|
+
* Input CONTENT is deliberately not inspected: `input_tokens_by_modality` is
|
|
5278
|
+
* the authority on what the input billed as, and it unprices any modality
|
|
5279
|
+
* without a billing category (video, documents, anything new).
|
|
5280
|
+
*/
|
|
5281
|
+
isModeledInteractionRequest(body) {
|
|
5282
|
+
if ([
|
|
5283
|
+
"cached_content",
|
|
5284
|
+
"cachedContent",
|
|
5285
|
+
"previous_interaction_id",
|
|
5286
|
+
"region",
|
|
5287
|
+
"inference_geo",
|
|
5288
|
+
"data_residency"
|
|
5289
|
+
].some((key) => body[key] !== void 0)) {
|
|
5290
|
+
return false;
|
|
5291
|
+
}
|
|
5292
|
+
if (body.tools !== void 0 && (!Array.isArray(body.tools) || body.tools.some(
|
|
5293
|
+
(tool2) => tool2 === null || typeof tool2 !== "object" || tool2.type !== "function"
|
|
5294
|
+
))) {
|
|
5295
|
+
return false;
|
|
5296
|
+
}
|
|
5297
|
+
const generationConfig = body.generation_config;
|
|
5298
|
+
const modalities = generationConfig !== null && typeof generationConfig === "object" ? generationConfig.response_modalities : void 0;
|
|
5299
|
+
return modalities === void 0 || modalities === "TEXT" || Array.isArray(modalities) && modalities.length === 1 && modalities[0] === "TEXT";
|
|
5300
|
+
}
|
|
4850
5301
|
pricingContext(fallbackModel, body) {
|
|
4851
5302
|
return {
|
|
4852
5303
|
// Gemini's model is normally selected in the URL. providerOptions is an
|
|
@@ -4854,7 +5305,7 @@ var GeminiProvider = class {
|
|
|
4854
5305
|
// the URL and pricing rather than pricing a different request.
|
|
4855
5306
|
model: typeof body.model === "string" ? body.model : fallbackModel,
|
|
4856
5307
|
serviceTier: body.serviceTier,
|
|
4857
|
-
eligibleRequest: this.baseUrl ===
|
|
5308
|
+
eligibleRequest: this.baseUrl === CANONICAL_GEMINI_BASE_URL && this.isTextOnlyClientRequest(body)
|
|
4858
5309
|
};
|
|
4859
5310
|
}
|
|
4860
5311
|
requestModel(options) {
|
|
@@ -5177,7 +5628,7 @@ var GeminiProvider = class {
|
|
|
5177
5628
|
}
|
|
5178
5629
|
const normalized = json.usageMetadata ? normalizeGeminiUsage(json.usageMetadata) : void 0;
|
|
5179
5630
|
const effectiveModel = typeof json.modelVersion === "string" ? json.modelVersion : pricingContext.model;
|
|
5180
|
-
const cost = normalized && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
|
|
5631
|
+
const cost = normalized?.pricingUsage && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
|
|
5181
5632
|
pricingContext,
|
|
5182
5633
|
isDefinitiveStandardGeminiResponseTier(json.usageMetadata?.serviceTier),
|
|
5183
5634
|
json.usageMetadata?.serviceTier !== void 0 && !isDefinitiveStandardGeminiResponseTier(json.usageMetadata.serviceTier)
|
|
@@ -5270,7 +5721,7 @@ var GeminiProvider = class {
|
|
|
5270
5721
|
yield {
|
|
5271
5722
|
type: "done",
|
|
5272
5723
|
usage: normalizedUsage?.usage,
|
|
5273
|
-
cost: normalizedUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
|
|
5724
|
+
cost: normalizedUsage?.pricingUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
|
|
5274
5725
|
pricingContext,
|
|
5275
5726
|
hasDefinitiveStandardResponseTier,
|
|
5276
5727
|
hasInvalidResponseTier
|
|
@@ -5316,20 +5767,73 @@ function safeJsonObject(value) {
|
|
|
5316
5767
|
return {};
|
|
5317
5768
|
}
|
|
5318
5769
|
}
|
|
5770
|
+
var KNOWN_GEMINI_MODALITIES = /* @__PURE__ */ new Set(["text", "image", "audio"]);
|
|
5771
|
+
function foldGeminiModalities(raw, totalInputTokens) {
|
|
5772
|
+
if (!Array.isArray(raw)) return void 0;
|
|
5773
|
+
let audioTokens = 0;
|
|
5774
|
+
let nonAudioTokens = 0;
|
|
5775
|
+
for (const entry of raw) {
|
|
5776
|
+
if (entry === null || typeof entry !== "object") return void 0;
|
|
5777
|
+
const { modality, tokens } = entry;
|
|
5778
|
+
if (typeof modality !== "string" || !isValidTokenCount2(tokens)) return void 0;
|
|
5779
|
+
const normalized = modality.toLowerCase();
|
|
5780
|
+
if (!KNOWN_GEMINI_MODALITIES.has(normalized)) return void 0;
|
|
5781
|
+
if (normalized === "audio") audioTokens += tokens;
|
|
5782
|
+
else nonAudioTokens += tokens;
|
|
5783
|
+
}
|
|
5784
|
+
const total = safeTokenSum2(audioTokens, nonAudioTokens);
|
|
5785
|
+
if (total === void 0 || total !== totalInputTokens) return void 0;
|
|
5786
|
+
return { audioTokens, nonAudioTokens };
|
|
5787
|
+
}
|
|
5788
|
+
function reportedGeminiAudioTokens(raw, totalInputTokens) {
|
|
5789
|
+
if (!Array.isArray(raw)) return void 0;
|
|
5790
|
+
let audioTokens;
|
|
5791
|
+
for (const entry of raw) {
|
|
5792
|
+
if (entry === null || typeof entry !== "object") return void 0;
|
|
5793
|
+
const { modality, tokens } = entry;
|
|
5794
|
+
if (typeof modality !== "string" || !isValidTokenCount2(tokens)) return void 0;
|
|
5795
|
+
if (modality.toLowerCase() === "audio") audioTokens = (audioTokens ?? 0) + tokens;
|
|
5796
|
+
}
|
|
5797
|
+
return audioTokens !== void 0 && audioTokens <= totalInputTokens ? audioTokens : void 0;
|
|
5798
|
+
}
|
|
5319
5799
|
function normalizeInteractionUsage(usage) {
|
|
5320
5800
|
if (!usage || !isValidTokenCount2(usage.total_input_tokens) || !isValidTokenCount2(usage.total_output_tokens) || !isValidTokenCount2(usage.total_tokens)) {
|
|
5321
5801
|
return void 0;
|
|
5322
5802
|
}
|
|
5323
5803
|
const cached = usage.total_cached_tokens;
|
|
5324
5804
|
const thinking = usage.total_thought_tokens;
|
|
5325
|
-
|
|
5805
|
+
const toolUse = usage.total_tool_use_tokens;
|
|
5806
|
+
if (cached !== void 0 && (!isValidTokenCount2(cached) || cached > usage.total_input_tokens) || thinking !== void 0 && !isValidTokenCount2(thinking) || toolUse !== void 0 && !isValidTokenCount2(toolUse))
|
|
5326
5807
|
return void 0;
|
|
5808
|
+
const inputTokens = usage.total_input_tokens;
|
|
5809
|
+
const audioInputTokens = reportedGeminiAudioTokens(usage.input_tokens_by_modality, inputTokens);
|
|
5810
|
+
const modalities = foldGeminiModalities(usage.input_tokens_by_modality, inputTokens);
|
|
5811
|
+
const billedOutputTokens = safeTokenSum2(usage.total_output_tokens, thinking ?? 0);
|
|
5812
|
+
const reportedTotal = safeTokenSum2(
|
|
5813
|
+
inputTokens,
|
|
5814
|
+
usage.total_output_tokens,
|
|
5815
|
+
thinking ?? 0,
|
|
5816
|
+
toolUse ?? 0
|
|
5817
|
+
);
|
|
5818
|
+
const reconciles = reportedTotal !== void 0 && usage.total_tokens === reportedTotal;
|
|
5327
5819
|
return {
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5820
|
+
usage: {
|
|
5821
|
+
prompt_tokens: inputTokens,
|
|
5822
|
+
completion_tokens: usage.total_output_tokens,
|
|
5823
|
+
total_tokens: usage.total_tokens,
|
|
5824
|
+
...cached ? { cached_tokens: cached } : {},
|
|
5825
|
+
...thinking ? { reasoning_tokens: thinking } : {},
|
|
5826
|
+
...audioInputTokens !== void 0 ? { audio_input_tokens: audioInputTokens } : {}
|
|
5827
|
+
},
|
|
5828
|
+
...modalities !== void 0 && billedOutputTokens !== void 0 && reconciles ? {
|
|
5829
|
+
pricingUsage: {
|
|
5830
|
+
inputTokens,
|
|
5831
|
+
outputTokens: billedOutputTokens,
|
|
5832
|
+
cachedTokens: cached ?? 0,
|
|
5833
|
+
modalities
|
|
5834
|
+
}
|
|
5835
|
+
} : {},
|
|
5836
|
+
hasUnmodeledBilledUsage: toolUse !== void 0 && toolUse !== 0
|
|
5333
5837
|
};
|
|
5334
5838
|
}
|
|
5335
5839
|
|
|
@@ -5349,6 +5853,14 @@ var OPENROUTER_PROFILE = {
|
|
|
5349
5853
|
capture: "reasoning_details",
|
|
5350
5854
|
roundTrip: "on-tool-call-turns"
|
|
5351
5855
|
},
|
|
5856
|
+
capabilities: {
|
|
5857
|
+
// Images: `image_url` data URLs or remote URLs. Audio: `input_audio`, which
|
|
5858
|
+
// OpenRouter documents as base64-only — bytes are encoded by the engine.
|
|
5859
|
+
inputModalities: {
|
|
5860
|
+
image: { sources: ["url", "bytes", "base64"] },
|
|
5861
|
+
audio: { sources: ["bytes", "base64"], formats: OPENROUTER_AUDIO_FORMATS }
|
|
5862
|
+
}
|
|
5863
|
+
},
|
|
5352
5864
|
// OpenRouter normalizes to max_tokens across its backends.
|
|
5353
5865
|
maxTokensField: "max_tokens"
|
|
5354
5866
|
};
|
|
@@ -6844,87 +7356,98 @@ function validBase64(value) {
|
|
|
6844
7356
|
function decodedBase64Bytes(value) {
|
|
6845
7357
|
return Math.floor(value.length * 3 / 4) - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
|
|
6846
7358
|
}
|
|
7359
|
+
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";
|
|
7360
|
+
var IMAGE_SOURCE_TYPES = ["url", "bytes", "base64", "provider-file"];
|
|
7361
|
+
var AUDIO_SOURCE_TYPES = ["bytes", "base64", "provider-file"];
|
|
7362
|
+
function cloneMediaSource(source, index, allowed, reserveInlineBytes) {
|
|
7363
|
+
if (!source || typeof source !== "object") invalid(`part ${index}.source must be an object`);
|
|
7364
|
+
const raw = source;
|
|
7365
|
+
const kind = raw.type;
|
|
7366
|
+
if (typeof kind !== "string" || !allowed.includes(kind))
|
|
7367
|
+
invalid(`part ${index}.source.type is unsupported`);
|
|
7368
|
+
let clone;
|
|
7369
|
+
switch (kind) {
|
|
7370
|
+
case "url": {
|
|
7371
|
+
const url = nonEmptyString2(raw.url, `part ${index}.source.url`);
|
|
7372
|
+
let parsed;
|
|
7373
|
+
try {
|
|
7374
|
+
parsed = new URL(url);
|
|
7375
|
+
} catch {
|
|
7376
|
+
invalid(`part ${index}.source.url must be an http(s) URL`);
|
|
7377
|
+
}
|
|
7378
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
7379
|
+
invalid(`part ${index}.source.url must be an http(s) URL`);
|
|
7380
|
+
clone = {
|
|
7381
|
+
type: "url",
|
|
7382
|
+
url,
|
|
7383
|
+
...raw.mediaType === void 0 ? {} : { mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`) }
|
|
7384
|
+
};
|
|
7385
|
+
break;
|
|
7386
|
+
}
|
|
7387
|
+
case "bytes": {
|
|
7388
|
+
if (!(raw.data instanceof Uint8Array) || raw.data.byteLength === 0)
|
|
7389
|
+
invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
|
|
7390
|
+
reserveInlineBytes(raw.data.byteLength);
|
|
7391
|
+
clone = {
|
|
7392
|
+
type: "bytes",
|
|
7393
|
+
data: new Uint8Array(raw.data),
|
|
7394
|
+
mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`)
|
|
7395
|
+
};
|
|
7396
|
+
break;
|
|
7397
|
+
}
|
|
7398
|
+
case "base64": {
|
|
7399
|
+
const data = nonEmptyString2(raw.data, `part ${index}.source.data`);
|
|
7400
|
+
if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3))
|
|
7401
|
+
invalid(INLINE_MEDIA_LIMIT_MESSAGE);
|
|
7402
|
+
if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
|
|
7403
|
+
reserveInlineBytes(decodedBase64Bytes(data));
|
|
7404
|
+
clone = {
|
|
7405
|
+
type: "base64",
|
|
7406
|
+
data,
|
|
7407
|
+
mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`)
|
|
7408
|
+
};
|
|
7409
|
+
break;
|
|
7410
|
+
}
|
|
7411
|
+
default:
|
|
7412
|
+
clone = {
|
|
7413
|
+
type: "provider-file",
|
|
7414
|
+
provider: nonEmptyString2(raw.provider, `part ${index}.source.provider`),
|
|
7415
|
+
reference: nonEmptyString2(raw.reference, `part ${index}.source.reference`),
|
|
7416
|
+
...raw.mediaType === void 0 ? {} : { mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`) }
|
|
7417
|
+
};
|
|
7418
|
+
}
|
|
7419
|
+
return clone;
|
|
7420
|
+
}
|
|
6847
7421
|
function normalizeModelInput(input) {
|
|
6848
7422
|
if (typeof input === "string") return input;
|
|
6849
7423
|
if (!Array.isArray(input) || input.length === 0)
|
|
6850
7424
|
invalid("ModelInput parts must be a non-empty array");
|
|
6851
7425
|
let inlineBytes = 0;
|
|
6852
7426
|
const reserveInlineBytes = (bytes) => {
|
|
6853
|
-
if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes)
|
|
6854
|
-
invalid(
|
|
6855
|
-
"Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
|
|
6856
|
-
);
|
|
6857
|
-
}
|
|
7427
|
+
if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes) invalid(INLINE_MEDIA_LIMIT_MESSAGE);
|
|
6858
7428
|
inlineBytes += bytes;
|
|
6859
7429
|
};
|
|
6860
7430
|
return input.map((part, index) => {
|
|
6861
7431
|
if (!part || typeof part !== "object") invalid(`ModelInput part ${index} must be an object`);
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
let parsed;
|
|
6872
|
-
try {
|
|
6873
|
-
parsed = new URL(url);
|
|
6874
|
-
} catch {
|
|
6875
|
-
invalid(`part ${index}.source.url must be an http(s) URL`);
|
|
6876
|
-
}
|
|
6877
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
6878
|
-
invalid(`part ${index}.source.url must be an http(s) URL`);
|
|
6879
|
-
clone = {
|
|
6880
|
-
type: "url",
|
|
6881
|
-
url,
|
|
6882
|
-
...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
|
|
6883
|
-
};
|
|
6884
|
-
break;
|
|
6885
|
-
}
|
|
6886
|
-
case "bytes":
|
|
6887
|
-
if (!(source.data instanceof Uint8Array) || source.data.byteLength === 0)
|
|
6888
|
-
invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
|
|
6889
|
-
reserveInlineBytes(source.data.byteLength);
|
|
6890
|
-
clone = {
|
|
6891
|
-
type: "bytes",
|
|
6892
|
-
data: source.data.slice(),
|
|
6893
|
-
mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
|
|
6894
|
-
};
|
|
6895
|
-
break;
|
|
6896
|
-
case "base64": {
|
|
6897
|
-
const data = nonEmptyString2(source.data, `part ${index}.source.data`);
|
|
6898
|
-
if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3)) {
|
|
6899
|
-
invalid(
|
|
6900
|
-
"Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
|
|
6901
|
-
);
|
|
6902
|
-
}
|
|
6903
|
-
if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
|
|
6904
|
-
reserveInlineBytes(decodedBase64Bytes(data));
|
|
6905
|
-
clone = {
|
|
6906
|
-
type: "base64",
|
|
6907
|
-
data,
|
|
6908
|
-
mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
|
|
7432
|
+
const mediaLabel = () => part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) };
|
|
7433
|
+
switch (part.type) {
|
|
7434
|
+
case "text":
|
|
7435
|
+
return { type: "text", text: nonEmptyString2(part.text, `part ${index}.text`) };
|
|
7436
|
+
case "image":
|
|
7437
|
+
return {
|
|
7438
|
+
type: "image",
|
|
7439
|
+
source: cloneMediaSource(part.source, index, IMAGE_SOURCE_TYPES, reserveInlineBytes),
|
|
7440
|
+
...mediaLabel()
|
|
6909
7441
|
};
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
provider: nonEmptyString2(source.provider, `part ${index}.source.provider`),
|
|
6916
|
-
reference: nonEmptyString2(source.reference, `part ${index}.source.reference`),
|
|
6917
|
-
...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
|
|
7442
|
+
case "audio":
|
|
7443
|
+
return {
|
|
7444
|
+
type: "audio",
|
|
7445
|
+
source: cloneMediaSource(part.source, index, AUDIO_SOURCE_TYPES, reserveInlineBytes),
|
|
7446
|
+
...mediaLabel()
|
|
6918
7447
|
};
|
|
6919
|
-
break;
|
|
6920
7448
|
default:
|
|
6921
|
-
invalid(`part ${index}.
|
|
7449
|
+
invalid(`part ${index}.type must be 'text', 'image', or 'audio'`);
|
|
6922
7450
|
}
|
|
6923
|
-
return {
|
|
6924
|
-
type: "image",
|
|
6925
|
-
source: clone,
|
|
6926
|
-
...part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) }
|
|
6927
|
-
};
|
|
6928
7451
|
});
|
|
6929
7452
|
}
|
|
6930
7453
|
function inputText(input) {
|
|
@@ -6933,33 +7456,54 @@ function inputText(input) {
|
|
|
6933
7456
|
function cloneModelInput(input) {
|
|
6934
7457
|
return typeof input === "string" ? input : normalizeModelInput(input);
|
|
6935
7458
|
}
|
|
7459
|
+
function unsupportedPart(part) {
|
|
7460
|
+
return invalid(
|
|
7461
|
+
`Unsupported model input part type '${String(part.type)}'`
|
|
7462
|
+
);
|
|
7463
|
+
}
|
|
7464
|
+
function describeSource(source, label) {
|
|
7465
|
+
const media = source;
|
|
7466
|
+
const locator = media.type === "url" ? media.url : media.type === "provider-file" ? media.reference : void 0;
|
|
7467
|
+
return {
|
|
7468
|
+
source: source.type,
|
|
7469
|
+
...media.mediaType ? { mediaType: media.mediaType } : {},
|
|
7470
|
+
...media.type === "bytes" ? { bytes: media.data.byteLength } : {},
|
|
7471
|
+
...media.type === "base64" ? { bytes: decodedBase64Bytes(media.data) } : {},
|
|
7472
|
+
...locator ? { locator } : {},
|
|
7473
|
+
...label ? { label } : {}
|
|
7474
|
+
};
|
|
7475
|
+
}
|
|
6936
7476
|
function describeModelInput(input) {
|
|
6937
7477
|
if (typeof input === "string") return void 0;
|
|
6938
7478
|
return {
|
|
6939
7479
|
parts: input.map((part) => {
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
...(source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0) ? {
|
|
6951
|
-
locator: source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0
|
|
6952
|
-
} : {},
|
|
6953
|
-
...part.label ? { label: part.label } : {}
|
|
6954
|
-
};
|
|
7480
|
+
switch (part.type) {
|
|
7481
|
+
case "text":
|
|
7482
|
+
return { type: "text", characters: part.text.length };
|
|
7483
|
+
case "image":
|
|
7484
|
+
return { type: "image", ...describeSource(part.source, part.label) };
|
|
7485
|
+
case "audio":
|
|
7486
|
+
return { type: "audio", ...describeSource(part.source, part.label) };
|
|
7487
|
+
default:
|
|
7488
|
+
return unsupportedPart(part);
|
|
7489
|
+
}
|
|
6955
7490
|
})
|
|
6956
7491
|
};
|
|
6957
7492
|
}
|
|
6958
7493
|
function summarizeModelInput(input) {
|
|
6959
7494
|
if (typeof input === "string") return input;
|
|
6960
|
-
return input.map(
|
|
6961
|
-
(part
|
|
6962
|
-
|
|
7495
|
+
return input.map((part) => {
|
|
7496
|
+
switch (part.type) {
|
|
7497
|
+
case "text":
|
|
7498
|
+
return part.text;
|
|
7499
|
+
case "image":
|
|
7500
|
+
return `[image ${part.source.mediaType ?? "media"}]`;
|
|
7501
|
+
case "audio":
|
|
7502
|
+
return `[audio ${part.source.mediaType ?? "media"}]`;
|
|
7503
|
+
default:
|
|
7504
|
+
return unsupportedPart(part);
|
|
7505
|
+
}
|
|
7506
|
+
}).join("\n");
|
|
6963
7507
|
}
|
|
6964
7508
|
function sanitizeModelInputForTrace(input) {
|
|
6965
7509
|
return inputText(input);
|
|
@@ -6973,15 +7517,86 @@ function normalizePersistedSessionHistory(history) {
|
|
|
6973
7517
|
);
|
|
6974
7518
|
}
|
|
6975
7519
|
const content = normalizeModelInput(message.content);
|
|
6976
|
-
if (typeof content !== "string" && content.some((part) => part.type
|
|
7520
|
+
if (typeof content !== "string" && content.some((part) => part.type !== "text" && part.source.type === "bytes")) {
|
|
6977
7521
|
throw new InvalidModelInputError(
|
|
6978
|
-
"Uint8Array
|
|
7522
|
+
"Uint8Array media input cannot be persisted in session history"
|
|
6979
7523
|
);
|
|
6980
7524
|
}
|
|
6981
7525
|
return { ...message, content };
|
|
6982
7526
|
});
|
|
6983
7527
|
}
|
|
6984
7528
|
|
|
7529
|
+
// src/session-input.ts
|
|
7530
|
+
var MODEL_INPUT_PART_TYPES = /* @__PURE__ */ new Set(["text", "image", "audio"]);
|
|
7531
|
+
var sessionInputMarkers = /* @__PURE__ */ new WeakMap();
|
|
7532
|
+
function isModelInputParts(input) {
|
|
7533
|
+
return Array.isArray(input) && input.length > 0 && input.every(
|
|
7534
|
+
(part) => typeof part === "object" && part !== null && MODEL_INPUT_PART_TYPES.has(part.type)
|
|
7535
|
+
);
|
|
7536
|
+
}
|
|
7537
|
+
function prepareSessionInput(input) {
|
|
7538
|
+
if (typeof input === "string") return { content: input, normalizedInput: input };
|
|
7539
|
+
if (isModelInputParts(input)) {
|
|
7540
|
+
const normalizedInput = normalizeModelInput(input);
|
|
7541
|
+
return { content: summarizeModelInput(normalizedInput), normalizedInput };
|
|
7542
|
+
}
|
|
7543
|
+
const content = JSON.stringify(input);
|
|
7544
|
+
return {
|
|
7545
|
+
content,
|
|
7546
|
+
...typeof content === "string" ? { normalizedInput: content } : {}
|
|
7547
|
+
};
|
|
7548
|
+
}
|
|
7549
|
+
function registerSessionInput(history, message, normalizedInput) {
|
|
7550
|
+
sessionInputMarkers.set(history, {
|
|
7551
|
+
message,
|
|
7552
|
+
content: message.content,
|
|
7553
|
+
normalizedInput: cloneModelInput(normalizedInput)
|
|
7554
|
+
});
|
|
7555
|
+
}
|
|
7556
|
+
function clearSessionInput(history) {
|
|
7557
|
+
sessionInputMarkers.delete(history);
|
|
7558
|
+
}
|
|
7559
|
+
function equalBytes(left, right) {
|
|
7560
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
7561
|
+
for (let index = 0; index < left.byteLength; index++) {
|
|
7562
|
+
if (left[index] !== right[index]) return false;
|
|
7563
|
+
}
|
|
7564
|
+
return true;
|
|
7565
|
+
}
|
|
7566
|
+
function equalSource(left, right) {
|
|
7567
|
+
if (left.type !== right.type) return false;
|
|
7568
|
+
switch (left.type) {
|
|
7569
|
+
case "url":
|
|
7570
|
+
return right.type === "url" && left.url === right.url && left.mediaType === right.mediaType;
|
|
7571
|
+
case "bytes":
|
|
7572
|
+
return right.type === "bytes" && left.mediaType === right.mediaType && equalBytes(left.data, right.data);
|
|
7573
|
+
case "base64":
|
|
7574
|
+
return right.type === "base64" && left.data === right.data && left.mediaType === right.mediaType;
|
|
7575
|
+
case "provider-file":
|
|
7576
|
+
return right.type === "provider-file" && left.provider === right.provider && left.reference === right.reference && left.mediaType === right.mediaType;
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
function equalPart(left, right) {
|
|
7580
|
+
if (left.type !== right.type) return false;
|
|
7581
|
+
if (left.type === "text") return right.type === "text" && left.text === right.text;
|
|
7582
|
+
return right.type === left.type && left.label === right.label && equalSource(left.source, right.source);
|
|
7583
|
+
}
|
|
7584
|
+
function equalModelInput(left, right) {
|
|
7585
|
+
if (typeof left === "string" || typeof right === "string") return left === right;
|
|
7586
|
+
return left.length === right.length && left.every((part, index) => equalPart(part, right[index]));
|
|
7587
|
+
}
|
|
7588
|
+
function sessionHistoryForAsk(history, normalizedInput) {
|
|
7589
|
+
const snapshot = history.slice();
|
|
7590
|
+
const marker = sessionInputMarkers.get(history);
|
|
7591
|
+
if (!marker) return snapshot;
|
|
7592
|
+
const last = snapshot[snapshot.length - 1];
|
|
7593
|
+
if (last !== marker.message || last.role !== "user" || last.content !== marker.content || !equalModelInput(marker.normalizedInput, normalizedInput)) {
|
|
7594
|
+
return snapshot;
|
|
7595
|
+
}
|
|
7596
|
+
snapshot.pop();
|
|
7597
|
+
return snapshot;
|
|
7598
|
+
}
|
|
7599
|
+
|
|
6985
7600
|
// src/partial-json.ts
|
|
6986
7601
|
var MAX_DEPTH = 256;
|
|
6987
7602
|
function parsePartialJson(input) {
|
|
@@ -9780,7 +10395,7 @@ function estimateMessagesTokens(messages) {
|
|
|
9780
10395
|
for (const msg of messages) {
|
|
9781
10396
|
total += estimateTokens(inputText(msg.content));
|
|
9782
10397
|
if (typeof msg.content !== "string") {
|
|
9783
|
-
unmeasured ||= msg.content.some((part) => part.type
|
|
10398
|
+
unmeasured ||= msg.content.some((part) => part.type !== "text");
|
|
9784
10399
|
}
|
|
9785
10400
|
if (msg.tool_calls) {
|
|
9786
10401
|
for (const tc of msg.tool_calls) {
|
|
@@ -9826,10 +10441,6 @@ function normalizeSessionHistory(messages) {
|
|
|
9826
10441
|
function hasRichMessage(messages) {
|
|
9827
10442
|
return messages.some((message) => typeof message.content !== "string");
|
|
9828
10443
|
}
|
|
9829
|
-
function firstImageSource(input, history) {
|
|
9830
|
-
const find = (value) => typeof value === "string" ? void 0 : value.find((part) => part.type === "image")?.source.type;
|
|
9831
|
-
return find(input) ?? history.map((message) => find(message.content)).find(Boolean);
|
|
9832
|
-
}
|
|
9833
10444
|
function appendHandoffInstruction(input, originalText, instruction) {
|
|
9834
10445
|
if (typeof input === "string" || instruction.length === 0 || instruction === originalText)
|
|
9835
10446
|
return input;
|
|
@@ -10387,7 +10998,8 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10387
10998
|
// ── ctx.ask() ─────────────────────────────────────────────────────────
|
|
10388
10999
|
async ask(agent2, prompt, options) {
|
|
10389
11000
|
const normalizedInput = normalizeModelInput(prompt);
|
|
10390
|
-
const
|
|
11001
|
+
const sessionHistorySnapshot = sessionHistoryForAsk(this.sessionHistory, normalizedInput);
|
|
11002
|
+
const normalizedHistory = normalizeSessionHistory(sessionHistorySnapshot);
|
|
10391
11003
|
const delegateInputHolder = agent2;
|
|
10392
11004
|
if (delegateInputHolder._delegateOriginalInput !== void 0) {
|
|
10393
11005
|
delegateInputHolder._delegateOriginalInput = normalizeModelInput(
|
|
@@ -10597,20 +11209,31 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10597
11209
|
const providerOptions = options?.providerOptions ?? agent2._config.providerOptions;
|
|
10598
11210
|
const richRequest = typeof input !== "string" || hasRichMessage(sessionHistory);
|
|
10599
11211
|
if (richRequest) {
|
|
11212
|
+
const richPart = firstRichPart(input, sessionHistory);
|
|
11213
|
+
const modality = richPart?.type ?? "image";
|
|
11214
|
+
const source = richPart?.source.type;
|
|
10600
11215
|
if (providerOptions && ("messages" in providerOptions || "input" in providerOptions)) {
|
|
10601
11216
|
throw new UnsupportedModelInputError({
|
|
10602
11217
|
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
10603
11218
|
model,
|
|
10604
|
-
modality
|
|
11219
|
+
modality,
|
|
10605
11220
|
feature: "raw input-container providerOptions"
|
|
10606
11221
|
});
|
|
10607
11222
|
}
|
|
10608
|
-
const
|
|
11223
|
+
const audioPart = firstRichPart(input, sessionHistory, "audio");
|
|
11224
|
+
if (audioPart && !provider.inputCapabilities?.(model)?.audio) {
|
|
11225
|
+
throw new UnsupportedModelInputError({
|
|
11226
|
+
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
11227
|
+
model,
|
|
11228
|
+
modality: "audio",
|
|
11229
|
+
source: audioPart.source.type
|
|
11230
|
+
});
|
|
11231
|
+
}
|
|
10609
11232
|
if (!provider.validateInput) {
|
|
10610
11233
|
throw new UnsupportedModelInputError({
|
|
10611
11234
|
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
10612
11235
|
model,
|
|
10613
|
-
modality
|
|
11236
|
+
modality,
|
|
10614
11237
|
...source ? { source } : {}
|
|
10615
11238
|
});
|
|
10616
11239
|
}
|
|
@@ -10629,10 +11252,20 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10629
11252
|
throw new UnsupportedModelInputError({
|
|
10630
11253
|
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
10631
11254
|
model,
|
|
10632
|
-
modality
|
|
11255
|
+
modality,
|
|
10633
11256
|
...source ? { source } : {}
|
|
10634
11257
|
});
|
|
10635
11258
|
}
|
|
11259
|
+
if (audioPart && validation.effectiveModel !== model) {
|
|
11260
|
+
if (!provider.inputCapabilities?.(validation.effectiveModel)?.audio) {
|
|
11261
|
+
throw new UnsupportedModelInputError({
|
|
11262
|
+
provider: provider.name ?? modelUri.split(":", 1)[0],
|
|
11263
|
+
model: validation.effectiveModel,
|
|
11264
|
+
modality: "audio",
|
|
11265
|
+
source: audioPart.source.type
|
|
11266
|
+
});
|
|
11267
|
+
}
|
|
11268
|
+
}
|
|
10636
11269
|
model = validation.effectiveModel;
|
|
10637
11270
|
}
|
|
10638
11271
|
const effectiveModelUri = !richRequest ? modelUri : `${modelUri.slice(0, modelUri.indexOf(":") + 1)}${model}`;
|
|
@@ -10645,25 +11278,17 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10645
11278
|
(descriptor) => descriptor !== void 0
|
|
10646
11279
|
);
|
|
10647
11280
|
const parts = descriptors.flatMap((descriptor) => descriptor.parts);
|
|
11281
|
+
const media = parts.filter((part) => part.type !== "text");
|
|
11282
|
+
const withSource = (source) => media.filter((part) => part.source === source).length;
|
|
10648
11283
|
this.spanManager?.addEventToActiveSpan("axl.model_input", {
|
|
10649
11284
|
"axl.input.parts": parts.length,
|
|
10650
11285
|
"axl.input.images": parts.filter((part) => part.type === "image").length,
|
|
10651
|
-
"axl.input.
|
|
10652
|
-
|
|
10653
|
-
)
|
|
10654
|
-
"axl.input.source.
|
|
10655
|
-
|
|
10656
|
-
).
|
|
10657
|
-
"axl.input.source.url": parts.filter(
|
|
10658
|
-
(part) => part.type === "image" && part.source === "url"
|
|
10659
|
-
).length,
|
|
10660
|
-
"axl.input.source.provider_file": parts.filter(
|
|
10661
|
-
(part) => part.type === "image" && part.source === "provider-file"
|
|
10662
|
-
).length,
|
|
10663
|
-
"axl.input.inline_bytes": parts.reduce(
|
|
10664
|
-
(sum, part) => sum + (part.type === "image" ? part.bytes ?? 0 : 0),
|
|
10665
|
-
0
|
|
10666
|
-
)
|
|
11286
|
+
"axl.input.audio": parts.filter((part) => part.type === "audio").length,
|
|
11287
|
+
"axl.input.source.bytes": withSource("bytes"),
|
|
11288
|
+
"axl.input.source.base64": withSource("base64"),
|
|
11289
|
+
"axl.input.source.url": withSource("url"),
|
|
11290
|
+
"axl.input.source.provider_file": withSource("provider-file"),
|
|
11291
|
+
"axl.input.inline_bytes": media.reduce((sum, part) => sum + (part.bytes ?? 0), 0)
|
|
10667
11292
|
});
|
|
10668
11293
|
}
|
|
10669
11294
|
assertRichMessagesAreUserOnly(sessionHistory);
|
|
@@ -13230,6 +13855,11 @@ ${summaryResponse.content}`
|
|
|
13230
13855
|
}
|
|
13231
13856
|
});
|
|
13232
13857
|
const routerInput = options?.routerInput === "text" ? inputText(prompt) : prompt;
|
|
13858
|
+
if (routerInput === "") {
|
|
13859
|
+
throw new InvalidModelInputError(
|
|
13860
|
+
"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"
|
|
13861
|
+
);
|
|
13862
|
+
}
|
|
13233
13863
|
routerAgent._delegateOriginalInput = prompt;
|
|
13234
13864
|
return this.ask(routerAgent, routerInput, {
|
|
13235
13865
|
schema: options?.schema,
|
|
@@ -14326,9 +14956,13 @@ var Session = class _Session {
|
|
|
14326
14956
|
}
|
|
14327
14957
|
async sendImpl(workflowName, input, signal, events) {
|
|
14328
14958
|
const { history, metadata } = await this.prepareHistory(input);
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
|
|
14959
|
+
try {
|
|
14960
|
+
const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
|
|
14961
|
+
await this.commitHistory(history, result);
|
|
14962
|
+
return result;
|
|
14963
|
+
} finally {
|
|
14964
|
+
clearSessionInput(history);
|
|
14965
|
+
}
|
|
14332
14966
|
}
|
|
14333
14967
|
/** Read history + summary, apply maxMessages limit (with optional
|
|
14334
14968
|
* summarization), and push the new user message. Returns the mutable
|
|
@@ -14338,6 +14972,7 @@ var Session = class _Session {
|
|
|
14338
14972
|
* `streamImpl` so future fixes (e.g., to summarization wiring) land in
|
|
14339
14973
|
* one place. */
|
|
14340
14974
|
async prepareHistory(input) {
|
|
14975
|
+
const preparedInput = prepareSessionInput(input);
|
|
14341
14976
|
const history = await this.store.getSession(this.sessionId);
|
|
14342
14977
|
let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
|
|
14343
14978
|
const maxMessages = this.options.history?.maxMessages;
|
|
@@ -14360,10 +14995,11 @@ var Session = class _Session {
|
|
|
14360
14995
|
history.length = 0;
|
|
14361
14996
|
history.push(...trimmed);
|
|
14362
14997
|
}
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14998
|
+
const userMessage = { role: "user", content: preparedInput.content };
|
|
14999
|
+
history.push(userMessage);
|
|
15000
|
+
if (this.options.deduplicateInput !== false && preparedInput.normalizedInput !== void 0) {
|
|
15001
|
+
registerSessionInput(history, userMessage, preparedInput.normalizedInput);
|
|
15002
|
+
}
|
|
14367
15003
|
const metadata = {
|
|
14368
15004
|
sessionId: this.sessionId,
|
|
14369
15005
|
sessionHistory: history,
|
|
@@ -14422,21 +15058,26 @@ var Session = class _Session {
|
|
|
14422
15058
|
events
|
|
14423
15059
|
});
|
|
14424
15060
|
} catch (err) {
|
|
15061
|
+
if (history) clearSessionInput(history);
|
|
14425
15062
|
rejectReady(err);
|
|
14426
15063
|
throw err;
|
|
14427
15064
|
}
|
|
14428
15065
|
resolveReady(axlStream);
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
this.
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
|
|
15066
|
+
try {
|
|
15067
|
+
const completion = axlStream.promise.then(
|
|
15068
|
+
(result) => this.commitHistory(history, result).catch((err) => {
|
|
15069
|
+
this.runtime.emit("error", {
|
|
15070
|
+
type: "session_history_save_failed",
|
|
15071
|
+
sessionId: this.sessionId,
|
|
15072
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15073
|
+
});
|
|
15074
|
+
}),
|
|
15075
|
+
() => void 0
|
|
15076
|
+
);
|
|
15077
|
+
await completion;
|
|
15078
|
+
} finally {
|
|
15079
|
+
clearSessionInput(history);
|
|
15080
|
+
}
|
|
14440
15081
|
}
|
|
14441
15082
|
async history() {
|
|
14442
15083
|
return this.store.getSession(this.sessionId);
|
|
@@ -18050,6 +18691,7 @@ export {
|
|
|
18050
18691
|
resolveRuntime,
|
|
18051
18692
|
resolveThinkingOptions,
|
|
18052
18693
|
stringStreamFromEvents,
|
|
18694
|
+
summarizeModelInput,
|
|
18053
18695
|
tool,
|
|
18054
18696
|
workflow,
|
|
18055
18697
|
zodToJsonSchema
|