@axlsdk/axl 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -496,7 +496,7 @@ async function fetchWithRetry(input, init, opts) {
496
496
  for (let attempt = 0; ; attempt++) {
497
497
  let res;
498
498
  try {
499
- res = await fetch(input, init);
499
+ res = await fetch(input, { ...init, redirect: "manual" });
500
500
  } catch (err) {
501
501
  if (isAbortError2(err, init?.signal ?? void 0)) throw err;
502
502
  if (attempt >= maxRetries) {
@@ -666,6 +666,74 @@ var RateLimiter = class {
666
666
  }
667
667
  };
668
668
 
669
+ // src/providers/builtin-table-pricing.ts
670
+ var policies = /* @__PURE__ */ new WeakMap();
671
+ function attachBuiltinTablePricingPolicy(profile, policy) {
672
+ if (profile.pricing.kind !== "table") {
673
+ throw new Error("Built-in table pricing policy requires table pricing.");
674
+ }
675
+ policies.set(profile.pricing.table, policy);
676
+ }
677
+ function hasFunctionToolsOnly(value) {
678
+ return value === void 0 || Array.isArray(value) && value.every(
679
+ (tool2) => tool2 !== null && typeof tool2 === "object" && tool2.type === "function"
680
+ );
681
+ }
682
+ function hasValidRequiredCacheSplit(context) {
683
+ const usage = context.response?.usage;
684
+ const hit = usage?.prompt_cache_hit_tokens;
685
+ const miss = usage?.prompt_cache_miss_tokens;
686
+ return typeof hit === "number" && typeof miss === "number" && Number.isSafeInteger(hit) && Number.isSafeInteger(miss) && hit >= 0 && miss >= 0 && hit + miss === usage?.prompt_tokens;
687
+ }
688
+ function isBuiltinTablePricingEligible(profile, context) {
689
+ const policy = profile.pricing.kind === "table" ? policies.get(profile.pricing.table) : void 0;
690
+ if (!policy) return true;
691
+ if (policy.canonicalBaseUrl && context.baseUrl !== policy.canonicalBaseUrl) return false;
692
+ if (policy.requireCacheSplit && !hasValidRequiredCacheSplit(context)) return false;
693
+ if (policy.functionToolsOnly && !hasFunctionToolsOnly(context.request?.tools)) return false;
694
+ if (policy.chargedRequestModifiers?.some((modifier) => context.request?.[modifier] !== void 0)) {
695
+ return false;
696
+ }
697
+ if (!policy.supportedServiceTiers) return true;
698
+ const requestTier = context.request?.service_tier ?? context.request?.serviceTier;
699
+ const responseTier = context.response?.service_tier ?? context.response?.serviceTier;
700
+ const effectiveTier = responseTier ?? requestTier;
701
+ if (effectiveTier === void 0) return true;
702
+ return typeof effectiveTier === "string" && policy.supportedServiceTiers.includes(effectiveTier);
703
+ }
704
+
705
+ // src/http-transport.ts
706
+ function assertSafeProviderBaseUrl(baseUrl, surface, dangerouslyAllowInsecureHttp = false) {
707
+ assertSafeHttpEndpoint(baseUrl, surface, dangerouslyAllowInsecureHttp, "prompts and credentials");
708
+ }
709
+ function assertSafeHttpEndpoint(endpoint, surface, dangerouslyAllowInsecureHttp = false, exposedData = "sensitive data") {
710
+ let url;
711
+ try {
712
+ url = new URL(endpoint);
713
+ } catch {
714
+ throw unsafeTransportError(surface, "a valid absolute HTTP(S) URL", exposedData);
715
+ }
716
+ if (url.protocol === "https:") return;
717
+ if (url.protocol !== "http:") {
718
+ throw unsafeTransportError(surface, "an HTTPS URL", exposedData);
719
+ }
720
+ if (dangerouslyAllowInsecureHttp || isLiteralLoopbackHost(url.hostname)) return;
721
+ throw unsafeTransportError(
722
+ surface,
723
+ "an HTTPS URL, or set dangerouslyAllowInsecureHttp: true only for a deliberately insecure endpoint",
724
+ exposedData
725
+ );
726
+ }
727
+ function isLiteralLoopbackHost(hostname) {
728
+ return hostname === "localhost" || hostname === "localhost." || /^127(?:\.\d{1,3}){3}$/.test(hostname) || hostname === "[::1]";
729
+ }
730
+ function unsafeTransportError(surface, remediation, exposedData) {
731
+ return new AxlError(
732
+ "UNSAFE_TRANSPORT",
733
+ `${surface} requires ${remediation}. Remote HTTP can expose ${exposedData} in transit.`
734
+ );
735
+ }
736
+
669
737
  // src/providers/openai-compatible.ts
670
738
  function resolvePerModel(value, model, fallback) {
671
739
  if (value === void 0) return fallback;
@@ -680,9 +748,13 @@ function sortedKeys(table) {
680
748
  }
681
749
  return keys;
682
750
  }
683
- function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens) {
751
+ function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens, match = "prefix") {
752
+ const cached = cachedTokens ?? 0;
753
+ if (!Number.isSafeInteger(promptTokens) || !Number.isSafeInteger(completionTokens) || !Number.isSafeInteger(cached) || promptTokens < 0 || completionTokens < 0 || cached < 0 || cached > promptTokens) {
754
+ return void 0;
755
+ }
684
756
  let pricing = table[model];
685
- if (!pricing) {
757
+ if (!pricing && match === "prefix") {
686
758
  for (const key of sortedKeys(table)) {
687
759
  if (model.startsWith(key)) {
688
760
  pricing = table[key];
@@ -692,7 +764,6 @@ function priceFromTable(table, model, promptTokens, completionTokens, cachedToke
692
764
  }
693
765
  if (!pricing) return void 0;
694
766
  const [inputRate, outputRate, cacheMultiplier] = pricing;
695
- const cached = cachedTokens ?? 0;
696
767
  const inputCost = (promptTokens - cached) * inputRate + cached * inputRate * cacheMultiplier;
697
768
  return inputCost + completionTokens * outputRate;
698
769
  }
@@ -806,6 +877,11 @@ var OpenAICompatibleProvider = class {
806
877
  const envBase = p.envBaseUrl ? process.env[p.envBaseUrl] : void 0;
807
878
  const explicitBase = options.baseUrl ?? envBase;
808
879
  this.baseUrl = (explicitBase ?? p.defaultBaseUrl).replace(/\/$/, "");
880
+ assertSafeProviderBaseUrl(
881
+ this.baseUrl,
882
+ `${p.label ?? p.name} provider`,
883
+ options.dangerouslyAllowInsecureHttp
884
+ );
809
885
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
810
886
  const label = p.label ?? p.name;
811
887
  if (p.requireExplicitBaseUrl && explicitBase === void 0) {
@@ -853,7 +929,7 @@ var OpenAICompatibleProvider = class {
853
929
  });
854
930
  }
855
931
  const json = await res.json();
856
- return this.parseResponse(json, options.model);
932
+ return this.parseResponse(json, this.requestModel(body, options.model), body);
857
933
  }
858
934
  async *stream(messages, options) {
859
935
  const headers = this.buildHeaders(await this.resolveKey());
@@ -881,7 +957,7 @@ var OpenAICompatibleProvider = class {
881
957
  if (!res.body) {
882
958
  throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
883
959
  }
884
- yield* this.parseSSEStream(res.body, options.model);
960
+ yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
885
961
  }
886
962
  // ---------------------------------------------------------------------------
887
963
  // Request building
@@ -902,12 +978,13 @@ var OpenAICompatibleProvider = class {
902
978
  buildRequestBody(messages, options, stream) {
903
979
  const profile = this.profile;
904
980
  const resolved = resolveThinkingOptions(options);
981
+ const effectiveModel = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
905
982
  const body = {
906
- model: options.model,
907
- messages: messages.map((m) => this.formatMessage(m, options.model)),
983
+ model: effectiveModel,
984
+ messages: messages.map((m) => this.formatMessage(m, effectiveModel)),
908
985
  stream
909
986
  };
910
- const emitResult = profile.reasoning.emit(body, resolved, options.model) ?? {};
987
+ const emitResult = profile.reasoning.emit(body, resolved, effectiveModel) ?? {};
911
988
  const stripTemperature = emitResult.stripTemperature ?? false;
912
989
  if (options.temperature !== void 0 && !stripTemperature) {
913
990
  body.temperature = options.temperature;
@@ -918,7 +995,7 @@ var OpenAICompatibleProvider = class {
918
995
  if (options.stop) body.stop = options.stop;
919
996
  if (options.tools && options.tools.length > 0) {
920
997
  body.tools = options.tools;
921
- if (resolvePerModel(profile.parallelToolCalls, options.model, false)) {
998
+ if (resolvePerModel(profile.parallelToolCalls, effectiveModel, false)) {
922
999
  body.parallel_tool_calls = true;
923
1000
  }
924
1001
  }
@@ -926,7 +1003,7 @@ var OpenAICompatibleProvider = class {
926
1003
  if (options.responseFormat) {
927
1004
  const supportsSchema = resolvePerModel(
928
1005
  profile.capabilities?.supportsJsonSchema,
929
- options.model,
1006
+ effectiveModel,
930
1007
  true
931
1008
  );
932
1009
  body.response_format = options.responseFormat.type === "json_schema" && !supportsSchema ? { type: "json_object" } : options.responseFormat;
@@ -937,7 +1014,7 @@ var OpenAICompatibleProvider = class {
937
1014
  if (profile.requestDefaults) Object.assign(body, profile.requestDefaults);
938
1015
  const userKeys = options.providerOptions ? Object.keys(options.providerOptions) : [];
939
1016
  if (options.providerOptions) Object.assign(body, options.providerOptions);
940
- const forbidden = resolvePerModel(profile.capabilities?.forbiddenParams, options.model, []);
1017
+ const forbidden = resolvePerModel(profile.capabilities?.forbiddenParams, effectiveModel, []);
941
1018
  for (const key of forbidden) {
942
1019
  if (!userKeys.includes(key)) delete body[key];
943
1020
  }
@@ -972,6 +1049,9 @@ var OpenAICompatibleProvider = class {
972
1049
  }
973
1050
  return `${label} API error (${status}): ${body}`;
974
1051
  }
1052
+ requestModel(body, fallback) {
1053
+ return typeof body.model === "string" ? body.model : fallback;
1054
+ }
975
1055
  toUsage(raw) {
976
1056
  if (!raw) return void 0;
977
1057
  return {
@@ -979,10 +1059,27 @@ var OpenAICompatibleProvider = class {
979
1059
  completion_tokens: raw.completion_tokens,
980
1060
  total_tokens: raw.total_tokens,
981
1061
  reasoning_tokens: raw.completion_tokens_details?.reasoning_tokens,
982
- cached_tokens: raw.prompt_tokens_details?.cached_tokens
1062
+ // DeepSeek reports the cache split at the top level rather than under
1063
+ // prompt_tokens_details. Preserve the normalized hit count for the
1064
+ // generic table estimator; its split is validated before pricing.
1065
+ cached_tokens: raw.prompt_cache_hit_tokens ?? raw.prompt_tokens_details?.cached_tokens,
1066
+ cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens
983
1067
  };
984
1068
  }
985
- computeCost(model, usage, reportedCost) {
1069
+ reportedCost(raw) {
1070
+ if (!raw) return void 0;
1071
+ if (this.profile.name === "xai") {
1072
+ const ticks = raw.cost_in_usd_ticks;
1073
+ return typeof ticks === "number" && Number.isSafeInteger(ticks) && ticks >= 0 ? ticks / 1e10 : void 0;
1074
+ }
1075
+ return typeof raw.cost === "number" && Number.isFinite(raw.cost) && raw.cost >= 0 ? raw.cost : void 0;
1076
+ }
1077
+ /**
1078
+ * The generic profile path stays flat and public. Native subclasses may
1079
+ * override this protected seam to use a provider-internal estimator without
1080
+ * widening `PricingSource` or `ProviderProfile`.
1081
+ */
1082
+ computeCost(model, usage, reportedCost, context) {
986
1083
  const p = this.profile.pricing;
987
1084
  if (p.kind === "zero") return 0;
988
1085
  if (!usage) return void 0;
@@ -990,14 +1087,22 @@ var OpenAICompatibleProvider = class {
990
1087
  case "unknown":
991
1088
  return void 0;
992
1089
  case "from-response":
993
- return typeof reportedCost === "number" && Number.isFinite(reportedCost) ? reportedCost : void 0;
1090
+ return typeof reportedCost === "number" && Number.isFinite(reportedCost) && reportedCost >= 0 ? reportedCost : void 0;
994
1091
  case "table": {
1092
+ if (!isBuiltinTablePricingEligible(this.profile, {
1093
+ baseUrl: this.baseUrl,
1094
+ request: context?.request,
1095
+ response: context?.response
1096
+ })) {
1097
+ return void 0;
1098
+ }
995
1099
  const c = priceFromTable(
996
1100
  p.table,
997
1101
  model,
998
1102
  usage.prompt_tokens,
999
1103
  usage.completion_tokens,
1000
- usage.cached_tokens
1104
+ usage.cached_tokens,
1105
+ p.match
1001
1106
  );
1002
1107
  return c !== void 0 && Number.isFinite(c) ? c : void 0;
1003
1108
  }
@@ -1007,7 +1112,7 @@ var OpenAICompatibleProvider = class {
1007
1112
  }
1008
1113
  }
1009
1114
  }
1010
- parseResponse(json, model) {
1115
+ parseResponse(json, model, request) {
1011
1116
  const message = json.choices?.[0]?.message ?? {};
1012
1117
  let content = normalizeMessageContent(message.content);
1013
1118
  let thinking;
@@ -1053,7 +1158,10 @@ var OpenAICompatibleProvider = class {
1053
1158
  }
1054
1159
  }
1055
1160
  const usage = this.toUsage(json.usage);
1056
- const cost = this.computeCost(model, usage, json.usage?.cost);
1161
+ const cost = this.computeCost(json.model ?? model, usage, this.reportedCost(json.usage), {
1162
+ request,
1163
+ response: json
1164
+ });
1057
1165
  return {
1058
1166
  content,
1059
1167
  thinking_content: thinking || void 0,
@@ -1076,11 +1184,12 @@ var OpenAICompatibleProvider = class {
1076
1184
  // ---------------------------------------------------------------------------
1077
1185
  // SSE streaming
1078
1186
  // ---------------------------------------------------------------------------
1079
- async *parseSSEStream(body, model) {
1187
+ async *parseSSEStream(body, model, request) {
1080
1188
  const reader = body.getReader();
1081
1189
  const decoder = new TextDecoder();
1082
1190
  let buffer = "";
1083
1191
  let usageData;
1192
+ let pricingResponse;
1084
1193
  const capture = this.profile.reasoning.capture;
1085
1194
  const thinkScanner = capture === "think_tags" ? new ThinkTagScanner() : void 0;
1086
1195
  let reasoningText = "";
@@ -1121,7 +1230,15 @@ var OpenAICompatibleProvider = class {
1121
1230
  return {
1122
1231
  type: "done",
1123
1232
  usage,
1124
- cost: this.computeCost(model, usage, usageData?.cost),
1233
+ cost: this.computeCost(
1234
+ pricingResponse?.model ?? model,
1235
+ usage,
1236
+ this.reportedCost(usageData),
1237
+ {
1238
+ request,
1239
+ response: pricingResponse
1240
+ }
1241
+ ),
1125
1242
  providerMetadata
1126
1243
  };
1127
1244
  };
@@ -1162,8 +1279,12 @@ var OpenAICompatibleProvider = class {
1162
1279
  body: JSON.stringify(parsed.error)
1163
1280
  });
1164
1281
  }
1282
+ if (parsed.model !== void 0 || parsed.service_tier !== void 0 || parsed.serviceTier !== void 0) {
1283
+ pricingResponse = { ...pricingResponse, ...parsed };
1284
+ }
1165
1285
  if (parsed.usage) {
1166
1286
  usageData = parsed.usage;
1287
+ pricingResponse = { ...pricingResponse, ...parsed };
1167
1288
  if (!parsed.choices || parsed.choices.length === 0) continue;
1168
1289
  }
1169
1290
  const delta = parsed.choices?.[0]?.delta;
@@ -1237,38 +1358,328 @@ var OpenAICompatibleProvider = class {
1237
1358
 
1238
1359
  // src/providers/openai.ts
1239
1360
  var OPENAI_PRICING = {
1240
- // gpt-4o era — cache reads at 50% of input rate
1361
+ // Flat, exact Standard rows only. Context-tiered and cache-write-priced
1362
+ // models intentionally stay out of this compatibility view.
1241
1363
  "gpt-4o": [25e-7, 1e-5, 0.5],
1242
1364
  "gpt-4o-mini": [15e-8, 6e-7, 0.5],
1243
- "gpt-4-turbo": [1e-5, 3e-5, 0.5],
1244
- "gpt-4": [3e-5, 6e-5, 0.5],
1245
- "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
1246
1365
  o1: [15e-6, 6e-5, 0.5],
1247
- "o1-mini": [3e-6, 12e-6, 0.5],
1248
- "o1-pro": [15e-5, 6e-4, 0.5],
1249
- // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
1250
1366
  "gpt-4.1": [2e-6, 8e-6, 0.25],
1251
1367
  "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
1252
1368
  "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
1253
- o3: [1e-5, 4e-5, 0.25],
1369
+ o3: [2e-6, 8e-6, 0.25],
1254
1370
  "o3-mini": [11e-7, 44e-7, 0.25],
1255
- "o3-pro": [2e-5, 8e-5, 0.25],
1256
1371
  "o4-mini": [11e-7, 44e-7, 0.25],
1257
- // gpt-5 era — cache reads at 10% of input rate
1258
1372
  "gpt-5": [125e-8, 1e-5, 0.1],
1259
1373
  "gpt-5-mini": [25e-8, 2e-6, 0.1],
1260
1374
  "gpt-5-nano": [5e-8, 4e-7, 0.1],
1261
1375
  "gpt-5.1": [125e-8, 1e-5, 0.1],
1262
- "gpt-5.2": [175e-8, 14e-6, 0.1],
1263
- "gpt-5.3": [175e-8, 14e-6, 0.1],
1264
- "gpt-5.4": [25e-7, 15e-6, 0.1],
1265
- "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
1266
- // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
1267
- "gpt-5.5": [5e-6, 3e-5, 0.1],
1268
- "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
1376
+ "gpt-5.2": [175e-8, 14e-6, 0.1]
1269
1377
  };
1378
+ var M = 1e6;
1379
+ var LONG_CONTEXT_BOUNDARY = 272e3;
1380
+ var withLongContext = (short) => ({
1381
+ input: short.input * 2,
1382
+ cachedInput: short.cachedInput === void 0 ? void 0 : short.cachedInput * 2,
1383
+ cacheWrite: short.cacheWrite === void 0 ? void 0 : short.cacheWrite * 2,
1384
+ output: short.output * 1.5
1385
+ });
1386
+ var DIRECT_OPENAI_CATALOG = [
1387
+ {
1388
+ aliases: ["gpt-5.6", "gpt-5.6-sol"],
1389
+ snapshotBase: "gpt-5.6-sol",
1390
+ short: { input: 5 / M, cachedInput: 0.5 / M, cacheWrite: 6.25 / M, output: 30 / M },
1391
+ long: withLongContext({
1392
+ input: 5 / M,
1393
+ cachedInput: 0.5 / M,
1394
+ cacheWrite: 6.25 / M,
1395
+ output: 30 / M
1396
+ }),
1397
+ contextBoundary: LONG_CONTEXT_BOUNDARY
1398
+ },
1399
+ {
1400
+ aliases: ["gpt-5.6-terra"],
1401
+ snapshotBase: "gpt-5.6-terra",
1402
+ short: { input: 2 / M, cachedInput: 0.2 / M, cacheWrite: 2.5 / M, output: 12 / M },
1403
+ long: withLongContext({
1404
+ input: 2 / M,
1405
+ cachedInput: 0.2 / M,
1406
+ cacheWrite: 2.5 / M,
1407
+ output: 12 / M
1408
+ }),
1409
+ contextBoundary: LONG_CONTEXT_BOUNDARY
1410
+ },
1411
+ {
1412
+ aliases: ["gpt-5.6-luna"],
1413
+ snapshotBase: "gpt-5.6-luna",
1414
+ short: { input: 0.2 / M, cachedInput: 0.02 / M, cacheWrite: 0.25 / M, output: 1.2 / M },
1415
+ long: withLongContext({
1416
+ input: 0.2 / M,
1417
+ cachedInput: 0.02 / M,
1418
+ cacheWrite: 0.25 / M,
1419
+ output: 1.2 / M
1420
+ }),
1421
+ contextBoundary: LONG_CONTEXT_BOUNDARY
1422
+ },
1423
+ // Existing direct models retain literal current Standard rows. These flat
1424
+ // rows keep native pricing aligned with the compatibility wrapper while the
1425
+ // private estimator still validates categories and billing mode strictly.
1426
+ {
1427
+ aliases: ["gpt-4o", "gpt-4o-2024-08-06", "gpt-4o-2024-11-20"],
1428
+ snapshotBase: "gpt-4o",
1429
+ short: { input: 2.5 / M, cachedInput: 1.25 / M, output: 10 / M }
1430
+ },
1431
+ {
1432
+ aliases: ["gpt-4o-mini", "gpt-4o-mini-2024-07-18"],
1433
+ snapshotBase: "gpt-4o-mini",
1434
+ short: { input: 0.15 / M, cachedInput: 0.075 / M, output: 0.6 / M }
1435
+ },
1436
+ {
1437
+ aliases: ["gpt-3.5-turbo"],
1438
+ snapshotBase: "gpt-3.5-turbo",
1439
+ short: { input: 0.5 / M, output: 1.5 / M }
1440
+ },
1441
+ {
1442
+ aliases: ["o1", "o1-2024-12-17"],
1443
+ snapshotBase: "o1",
1444
+ short: { input: 15 / M, cachedInput: 7.5 / M, output: 60 / M }
1445
+ },
1446
+ {
1447
+ aliases: ["o1-pro", "o1-pro-2025-03-19"],
1448
+ snapshotBase: "o1-pro",
1449
+ short: { input: 150 / M, output: 600 / M }
1450
+ },
1451
+ {
1452
+ aliases: ["gpt-4.1", "gpt-4.1-2025-04-14"],
1453
+ snapshotBase: "gpt-4.1",
1454
+ short: { input: 2 / M, cachedInput: 0.5 / M, output: 8 / M }
1455
+ },
1456
+ {
1457
+ aliases: ["gpt-4.1-mini", "gpt-4.1-mini-2025-04-14"],
1458
+ snapshotBase: "gpt-4.1-mini",
1459
+ short: { input: 0.4 / M, cachedInput: 0.1 / M, output: 1.6 / M }
1460
+ },
1461
+ {
1462
+ aliases: ["gpt-4.1-nano", "gpt-4.1-nano-2025-04-14"],
1463
+ snapshotBase: "gpt-4.1-nano",
1464
+ short: { input: 0.1 / M, cachedInput: 0.025 / M, output: 0.4 / M }
1465
+ },
1466
+ {
1467
+ aliases: ["o3", "o3-2025-04-16"],
1468
+ snapshotBase: "o3",
1469
+ short: { input: 2 / M, cachedInput: 0.5 / M, output: 8 / M }
1470
+ },
1471
+ {
1472
+ aliases: ["o3-mini", "o3-mini-2025-01-31"],
1473
+ snapshotBase: "o3-mini",
1474
+ short: { input: 1.1 / M, cachedInput: 0.55 / M, output: 4.4 / M }
1475
+ },
1476
+ {
1477
+ aliases: ["o3-pro", "o3-pro-2025-06-10"],
1478
+ snapshotBase: "o3-pro",
1479
+ short: { input: 20 / M, output: 80 / M }
1480
+ },
1481
+ {
1482
+ aliases: ["o4-mini", "o4-mini-2025-04-16"],
1483
+ snapshotBase: "o4-mini",
1484
+ short: { input: 1.1 / M, cachedInput: 0.275 / M, output: 4.4 / M }
1485
+ },
1486
+ {
1487
+ aliases: ["gpt-5", "gpt-5-2025-08-07"],
1488
+ snapshotBase: "gpt-5",
1489
+ short: { input: 1.25 / M, cachedInput: 0.125 / M, output: 10 / M }
1490
+ },
1491
+ {
1492
+ aliases: ["gpt-5-mini", "gpt-5-mini-2025-08-07"],
1493
+ snapshotBase: "gpt-5-mini",
1494
+ short: { input: 0.25 / M, cachedInput: 0.025 / M, output: 2 / M }
1495
+ },
1496
+ {
1497
+ aliases: ["gpt-5-nano", "gpt-5-nano-2025-08-07"],
1498
+ snapshotBase: "gpt-5-nano",
1499
+ short: { input: 0.05 / M, cachedInput: 5e-3 / M, output: 0.4 / M }
1500
+ },
1501
+ {
1502
+ aliases: ["gpt-5.1", "gpt-5.1-2025-11-13"],
1503
+ snapshotBase: "gpt-5.1",
1504
+ short: { input: 1.25 / M, cachedInput: 0.125 / M, output: 10 / M }
1505
+ },
1506
+ {
1507
+ aliases: ["gpt-5.2", "gpt-5.2-2025-12-11"],
1508
+ snapshotBase: "gpt-5.2",
1509
+ short: { input: 1.75 / M, cachedInput: 0.175 / M, output: 14 / M }
1510
+ },
1511
+ {
1512
+ aliases: ["gpt-5.4", "gpt-5.4-2026-03-05"],
1513
+ snapshotBase: "gpt-5.4",
1514
+ short: { input: 2.5 / M, cachedInput: 0.25 / M, output: 15 / M },
1515
+ long: withLongContext({ input: 2.5 / M, cachedInput: 0.25 / M, output: 15 / M }),
1516
+ contextBoundary: LONG_CONTEXT_BOUNDARY
1517
+ },
1518
+ {
1519
+ aliases: ["gpt-5.4-pro", "gpt-5.4-pro-2026-03-05"],
1520
+ snapshotBase: "gpt-5.4-pro",
1521
+ short: { input: 30 / M, output: 180 / M },
1522
+ long: withLongContext({ input: 30 / M, output: 180 / M }),
1523
+ contextBoundary: LONG_CONTEXT_BOUNDARY
1524
+ },
1525
+ {
1526
+ aliases: ["gpt-5.5", "gpt-5.5-2026-04-23"],
1527
+ snapshotBase: "gpt-5.5",
1528
+ short: { input: 5 / M, cachedInput: 0.5 / M, output: 30 / M },
1529
+ long: withLongContext({ input: 5 / M, cachedInput: 0.5 / M, output: 30 / M }),
1530
+ contextBoundary: LONG_CONTEXT_BOUNDARY
1531
+ },
1532
+ {
1533
+ aliases: ["gpt-5.5-pro", "gpt-5.5-pro-2026-04-23"],
1534
+ snapshotBase: "gpt-5.5-pro",
1535
+ short: { input: 30 / M, output: 180 / M },
1536
+ long: withLongContext({ input: 30 / M, output: 180 / M }),
1537
+ contextBoundary: LONG_CONTEXT_BOUNDARY
1538
+ },
1539
+ {
1540
+ aliases: ["gpt-5.4-mini", "gpt-5.4-mini-2026-03-17"],
1541
+ short: { input: 0.75 / M, cachedInput: 0.075 / M, output: 4.5 / M }
1542
+ },
1543
+ {
1544
+ aliases: ["gpt-5.4-nano", "gpt-5.4-nano-2026-03-17"],
1545
+ short: { input: 0.2 / M, cachedInput: 0.02 / M, output: 1.25 / M }
1546
+ },
1547
+ {
1548
+ aliases: ["gpt-5.2-pro", "gpt-5.2-pro-2025-12-11"],
1549
+ short: { input: 21 / M, output: 168 / M }
1550
+ },
1551
+ { aliases: ["gpt-5-pro", "gpt-5-pro-2025-10-06"], short: { input: 15 / M, output: 120 / M } },
1552
+ { aliases: ["gpt-3.5-turbo-instruct"], short: { input: 1.5 / M, output: 2 / M } },
1553
+ { aliases: ["davinci-002"], short: { input: 2 / M, output: 2 / M } },
1554
+ { aliases: ["babbage-002"], short: { input: 0.4 / M, output: 0.4 / M } },
1555
+ // Explicit snapshots with their own current dedicated-table rows. Do not
1556
+ // generalize these dates: a plausible-looking unlisted snapshot is unpriced.
1557
+ { aliases: ["gpt-4o-2024-05-13"], short: { input: 5 / M, output: 15 / M } },
1558
+ { aliases: ["gpt-4-turbo-2024-04-09"], short: { input: 10 / M, output: 30 / M } },
1559
+ { aliases: ["gpt-4-0613"], short: { input: 30 / M, output: 60 / M } },
1560
+ {
1561
+ aliases: ["gpt-3.5-turbo-0125"],
1562
+ short: { input: 0.5 / M, output: 1.5 / M }
1563
+ },
1564
+ { aliases: ["gpt-3.5-turbo-1106"], short: { input: 1 / M, output: 2 / M } }
1565
+ ];
1566
+ function directOpenAIModel(model) {
1567
+ return DIRECT_OPENAI_CATALOG.find((entry) => entry.aliases.includes(model));
1568
+ }
1569
+ var CANONICAL_OPENAI_BASE_URL = "https://api.openai.com/v1";
1570
+ function isTextContentPart(value, type) {
1571
+ return value !== null && typeof value === "object" && value.type === type && typeof value.text === "string";
1572
+ }
1573
+ function hasUnmodeledDirectOpenAIContent(request) {
1574
+ if ("messages" in request) {
1575
+ if (!Array.isArray(request.messages)) return true;
1576
+ for (const message of request.messages) {
1577
+ if (message === null || typeof message !== "object") return true;
1578
+ const content = message.content;
1579
+ if (content === void 0 || content === null || typeof content === "string") continue;
1580
+ if (!Array.isArray(content) || content.some((part) => !isTextContentPart(part, "text"))) {
1581
+ return true;
1582
+ }
1583
+ }
1584
+ return false;
1585
+ }
1586
+ if ("input" in request) {
1587
+ const input = request.input;
1588
+ if (typeof input === "string") return false;
1589
+ if (!Array.isArray(input)) return true;
1590
+ for (const item of input) {
1591
+ if (item === null || typeof item !== "object") return true;
1592
+ const typed = item;
1593
+ switch (typed.type) {
1594
+ case "message": {
1595
+ const content = typed.content;
1596
+ if (typeof content === "string") break;
1597
+ if (!Array.isArray(content) || content.some((part) => !isTextContentPart(part, "input_text"))) {
1598
+ return true;
1599
+ }
1600
+ break;
1601
+ }
1602
+ case "function_call":
1603
+ break;
1604
+ case "function_call_output": {
1605
+ const output = typed.output;
1606
+ if (typeof output === "string") break;
1607
+ if (!Array.isArray(output) || output.some((part) => !isTextContentPart(part, "input_text"))) {
1608
+ return true;
1609
+ }
1610
+ break;
1611
+ }
1612
+ case "custom_tool_call":
1613
+ if (typeof typed.input !== "string") return true;
1614
+ break;
1615
+ case "custom_tool_call_output":
1616
+ if (typeof typed.output !== "string") return true;
1617
+ break;
1618
+ case "reasoning":
1619
+ break;
1620
+ default:
1621
+ return true;
1622
+ }
1623
+ }
1624
+ return false;
1625
+ }
1626
+ return false;
1627
+ }
1628
+ function isEligibleDirectOpenAIContext(context) {
1629
+ const request = context?.request;
1630
+ const response = context?.response;
1631
+ if (context?.baseUrl !== void 0 && context.baseUrl !== CANONICAL_OPENAI_BASE_URL) return false;
1632
+ if (request && hasUnmodeledDirectOpenAIContent(request)) return false;
1633
+ const tier = response?.service_tier ?? response?.serviceTier ?? request?.service_tier ?? request?.serviceTier;
1634
+ if (tier !== void 0 && (typeof tier !== "string" || !["standard", "default"].includes(tier.toLowerCase()))) {
1635
+ return false;
1636
+ }
1637
+ if (["region", "inference_geo", "data_residency"].some((key) => request?.[key] !== void 0)) {
1638
+ return false;
1639
+ }
1640
+ if (["previous_response_id", "conversation", "prompt"].some((key) => request?.[key] !== void 0)) {
1641
+ return false;
1642
+ }
1643
+ const reasoning = request?.reasoning;
1644
+ if (reasoning !== null && typeof reasoning === "object" && "mode" in reasoning && reasoning.mode !== void 0) {
1645
+ return false;
1646
+ }
1647
+ if (request?.modalities !== void 0 && (!Array.isArray(request.modalities) || request.modalities.length === 0 || request.modalities.some((modality) => modality !== "text")) || ["audio", "image_generation", "web_search_options"].some((key) => request?.[key] !== void 0)) {
1648
+ return false;
1649
+ }
1650
+ if (Array.isArray(request?.tools) && request.tools.some(
1651
+ (tool2) => tool2 === null || typeof tool2 !== "object" || !["function", "custom"].includes(tool2.type)
1652
+ )) {
1653
+ return false;
1654
+ }
1655
+ return true;
1656
+ }
1657
+ function estimateDirectOpenAICost(model, usage, context) {
1658
+ if (!isEligibleDirectOpenAIContext(context)) return void 0;
1659
+ const entry = directOpenAIModel(model);
1660
+ if (!entry) return void 0;
1661
+ const { prompt_tokens, completion_tokens, cached_tokens, cache_write_tokens } = usage;
1662
+ const cached = cached_tokens ?? 0;
1663
+ const cacheWrite = cache_write_tokens ?? 0;
1664
+ if (![prompt_tokens, completion_tokens, cached, cacheWrite].every(
1665
+ (count) => Number.isSafeInteger(count) && count >= 0
1666
+ ) || cached + cacheWrite > prompt_tokens) {
1667
+ return void 0;
1668
+ }
1669
+ const rates = entry.long !== void 0 && entry.contextBoundary !== void 0 && prompt_tokens > entry.contextBoundary ? entry.long : entry.short;
1670
+ if (cached > 0 && rates.cachedInput === void 0 || cacheWrite > 0 && rates.cacheWrite === void 0) {
1671
+ return void 0;
1672
+ }
1673
+ const ordinary = prompt_tokens - cached - cacheWrite;
1674
+ return ordinary * rates.input + cached * (rates.cachedInput ?? 0) + cacheWrite * (rates.cacheWrite ?? 0) + completion_tokens * rates.output;
1675
+ }
1270
1676
  function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
1271
- return priceFromTable(OPENAI_PRICING, model, promptTokens, completionTokens, cachedTokens);
1677
+ return estimateDirectOpenAICost(model, {
1678
+ prompt_tokens: promptTokens,
1679
+ completion_tokens: completionTokens,
1680
+ total_tokens: promptTokens + completionTokens,
1681
+ cached_tokens: cachedTokens
1682
+ });
1272
1683
  }
1273
1684
  function isOSeriesModel(model) {
1274
1685
  return /^(o1|o3|o4-mini)/.test(model);
@@ -1276,6 +1687,15 @@ function isOSeriesModel(model) {
1276
1687
  function supportsReasoningEffort(model) {
1277
1688
  return isOSeriesModel(model) || /^gpt-5/.test(model);
1278
1689
  }
1690
+ var GPT_56_REASONING_MODELS = /* @__PURE__ */ new Set([
1691
+ "gpt-5.6",
1692
+ "gpt-5.6-sol",
1693
+ "gpt-5.6-terra",
1694
+ "gpt-5.6-luna"
1695
+ ]);
1696
+ function supportsMaxReasoningEffort(model) {
1697
+ return GPT_56_REASONING_MODELS.has(model);
1698
+ }
1279
1699
  function supportsReasoningNone(model) {
1280
1700
  return /^gpt-5\.[1-9]/.test(model);
1281
1701
  }
@@ -1285,31 +1705,46 @@ function supportsXhigh(model) {
1285
1705
  function clampReasoningEffort(model, effort) {
1286
1706
  if (model.startsWith("gpt-5-pro")) return "high";
1287
1707
  if (effort === "none" && !supportsReasoningNone(model)) return "minimal";
1708
+ if (effort === "max" && !supportsMaxReasoningEffort(model)) {
1709
+ return clampReasoningEffort(model, "xhigh");
1710
+ }
1288
1711
  if (effort === "xhigh" && !supportsXhigh(model)) return "high";
1289
1712
  return effort;
1290
1713
  }
1291
- function effortToReasoningEffort(effort) {
1292
- return effort === "max" ? "xhigh" : effort;
1714
+ function effortToReasoningEffort(effort, model) {
1715
+ return effort === "max" && model !== void 0 && supportsMaxReasoningEffort(model) ? "max" : effort === "max" ? "xhigh" : effort;
1293
1716
  }
1294
1717
  function budgetToReasoningEffort(budget) {
1295
1718
  if (budget <= 1024) return "low";
1296
1719
  if (budget <= 8192) return "medium";
1297
1720
  return "high";
1298
1721
  }
1722
+ function resolveOpenAIReasoningEffort(model, resolved) {
1723
+ if (!supportsReasoningEffort(model)) return void 0;
1724
+ if (resolved.hasBudgetOverride) {
1725
+ return clampReasoningEffort(model, budgetToReasoningEffort(resolved.thinkingBudget));
1726
+ }
1727
+ if (!resolved.thinkingDisabled && resolved.activeEffort) {
1728
+ return clampReasoningEffort(model, effortToReasoningEffort(resolved.activeEffort, model));
1729
+ }
1730
+ return resolved.thinkingDisabled ? clampReasoningEffort(model, "none") : void 0;
1731
+ }
1732
+ var warnedChatMaxDowngrades = /* @__PURE__ */ new Set();
1733
+ function resolveOpenAIChatReasoningEffort(model, resolved) {
1734
+ const effort = resolveOpenAIReasoningEffort(model, resolved);
1735
+ if (effort !== "max") return effort;
1736
+ if (resolved.activeEffort === "max" && !warnedChatMaxDowngrades.has(model)) {
1737
+ warnedChatMaxDowngrades.add(model);
1738
+ console.warn(
1739
+ `[axl] OpenAI Chat Completions does not accept effort "max" for ${model}; using "xhigh". Use the OpenAI Responses provider for native "max".`
1740
+ );
1741
+ }
1742
+ return "xhigh";
1743
+ }
1299
1744
  var openaiReasoningEmit = (body, resolved, model) => {
1300
1745
  const oSeries = isOSeriesModel(model);
1301
1746
  const reasoningCapable = supportsReasoningEffort(model);
1302
- const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolved;
1303
- let wireEffort;
1304
- if (reasoningCapable) {
1305
- if (hasBudgetOverride) {
1306
- wireEffort = clampReasoningEffort(model, budgetToReasoningEffort(thinkingBudget));
1307
- } else if (!thinkingDisabled && activeEffort) {
1308
- wireEffort = clampReasoningEffort(model, effortToReasoningEffort(activeEffort));
1309
- } else if (thinkingDisabled) {
1310
- wireEffort = clampReasoningEffort(model, "none");
1311
- }
1312
- }
1747
+ const wireEffort = resolveOpenAIChatReasoningEffort(model, resolved);
1313
1748
  if (wireEffort) body.reasoning_effort = wireEffort;
1314
1749
  return { stripTemperature: oSeries || reasoningCapable && wireEffort !== void 0 };
1315
1750
  };
@@ -1319,7 +1754,7 @@ var OPENAI_PROFILE = {
1319
1754
  defaultBaseUrl: "https://api.openai.com/v1",
1320
1755
  envApiKey: "OPENAI_API_KEY",
1321
1756
  envBaseUrl: "OPENAI_BASE_URL",
1322
- pricing: { kind: "table", table: OPENAI_PRICING },
1757
+ pricing: { kind: "table", table: OPENAI_PRICING, match: "exact" },
1323
1758
  reasoning: { emit: openaiReasoningEmit, capture: "none" },
1324
1759
  roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
1325
1760
  maxTokensField: "max_completion_tokens",
@@ -1329,6 +1764,9 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
1329
1764
  constructor(options = {}) {
1330
1765
  super({ profile: OPENAI_PROFILE, ...options });
1331
1766
  }
1767
+ computeCost(model, usage, _reportedCost, context) {
1768
+ return usage ? estimateDirectOpenAICost(model, usage, { ...context, baseUrl: this.baseUrl }) : void 0;
1769
+ }
1332
1770
  };
1333
1771
 
1334
1772
  // src/providers/openai-responses.ts
@@ -1344,6 +1782,11 @@ var OpenAIResponsesProvider = class {
1344
1782
  constructor(options = {}) {
1345
1783
  this.apiKeySource = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
1346
1784
  this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
1785
+ assertSafeProviderBaseUrl(
1786
+ this.baseUrl,
1787
+ "OpenAI Responses provider",
1788
+ options.dangerouslyAllowInsecureHttp
1789
+ );
1347
1790
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1348
1791
  if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1349
1792
  throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
@@ -1389,7 +1832,7 @@ var OpenAIResponsesProvider = class {
1389
1832
  });
1390
1833
  }
1391
1834
  const json = await res.json();
1392
- return this.parseResponse(json, options.model);
1835
+ return this.parseResponse(json, this.requestModel(body, options.model), body);
1393
1836
  }
1394
1837
  // ---------------------------------------------------------------------------
1395
1838
  // stream - SSE streaming
@@ -1421,30 +1864,23 @@ var OpenAIResponsesProvider = class {
1421
1864
  if (!res.body) {
1422
1865
  throw new Error("OpenAI Responses stream has no body");
1423
1866
  }
1424
- yield* this.parseSSEStream(res.body, options.model);
1867
+ yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
1425
1868
  }
1426
1869
  // ---------------------------------------------------------------------------
1427
1870
  // Internal: build request body
1428
1871
  // ---------------------------------------------------------------------------
1429
1872
  buildRequestBody(messages, options, stream) {
1430
- const oSeries = isOSeriesModel(options.model);
1431
- const reasoningCapable = supportsReasoningEffort(options.model);
1432
- const { thinkingBudget, includeThoughts, thinkingDisabled, activeEffort, hasBudgetOverride } = resolveThinkingOptions(options);
1433
- let wireEffort;
1434
- if (reasoningCapable) {
1435
- if (hasBudgetOverride) {
1436
- wireEffort = clampReasoningEffort(options.model, budgetToReasoningEffort(thinkingBudget));
1437
- } else if (!thinkingDisabled && activeEffort) {
1438
- wireEffort = clampReasoningEffort(options.model, effortToReasoningEffort(activeEffort));
1439
- } else if (thinkingDisabled) {
1440
- wireEffort = clampReasoningEffort(options.model, "none");
1441
- }
1442
- }
1873
+ const effectiveModel = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
1874
+ const oSeries = isOSeriesModel(effectiveModel);
1875
+ const reasoningCapable = supportsReasoningEffort(effectiveModel);
1876
+ const resolved = resolveThinkingOptions(options);
1877
+ const { includeThoughts } = resolved;
1878
+ const wireEffort = resolveOpenAIReasoningEffort(effectiveModel, resolved);
1443
1879
  const stripTemp = oSeries || reasoningCapable && wireEffort !== void 0;
1444
1880
  const systemMessages = messages.filter((m) => m.role === "system");
1445
1881
  const nonSystemMessages = messages.filter((m) => m.role !== "system");
1446
1882
  const body = {
1447
- model: options.model,
1883
+ model: effectiveModel,
1448
1884
  input: this.buildInput(nonSystemMessages),
1449
1885
  store: false,
1450
1886
  stream
@@ -1491,6 +1927,9 @@ var OpenAIResponsesProvider = class {
1491
1927
  }
1492
1928
  return body;
1493
1929
  }
1930
+ requestModel(body, fallback) {
1931
+ return typeof body.model === "string" ? body.model : fallback;
1932
+ }
1494
1933
  // ---------------------------------------------------------------------------
1495
1934
  // Internal: message → input mapping
1496
1935
  // ---------------------------------------------------------------------------
@@ -1558,7 +1997,7 @@ var OpenAIResponsesProvider = class {
1558
1997
  // ---------------------------------------------------------------------------
1559
1998
  // Internal: parse non-streaming response
1560
1999
  // ---------------------------------------------------------------------------
1561
- parseResponse(json, model) {
2000
+ parseResponse(json, model, request) {
1562
2001
  let content = "";
1563
2002
  let thinkingContent = "";
1564
2003
  const toolCalls = [];
@@ -1595,9 +2034,14 @@ var OpenAIResponsesProvider = class {
1595
2034
  completion_tokens: json.usage.output_tokens,
1596
2035
  total_tokens: json.usage.total_tokens,
1597
2036
  reasoning_tokens: json.usage.output_tokens_details?.reasoning_tokens,
1598
- cached_tokens: json.usage.input_tokens_details?.cached_tokens
2037
+ cached_tokens: json.usage.input_tokens_details?.cached_tokens,
2038
+ cache_write_tokens: json.usage.input_tokens_details?.cache_write_tokens
1599
2039
  } : void 0;
1600
- const cost = usage ? estimateOpenAICost(model, usage.prompt_tokens, usage.completion_tokens, usage.cached_tokens) : void 0;
2040
+ const cost = usage ? estimateDirectOpenAICost(json.model ?? model, usage, {
2041
+ baseUrl: this.baseUrl,
2042
+ request,
2043
+ response: json
2044
+ }) : void 0;
1601
2045
  const providerMetadata = reasoningItems.length > 0 ? { openaiReasoningItems: reasoningItems } : void 0;
1602
2046
  return {
1603
2047
  content,
@@ -1611,7 +2055,7 @@ var OpenAIResponsesProvider = class {
1611
2055
  // ---------------------------------------------------------------------------
1612
2056
  // Internal: parse SSE stream
1613
2057
  // ---------------------------------------------------------------------------
1614
- async *parseSSEStream(body, model) {
2058
+ async *parseSSEStream(body, model, request) {
1615
2059
  const reader = body.getReader();
1616
2060
  const decoder = new TextDecoder();
1617
2061
  let buffer = "";
@@ -1639,7 +2083,7 @@ var OpenAIResponsesProvider = class {
1639
2083
  } catch {
1640
2084
  continue;
1641
2085
  }
1642
- const chunk = this.handleStreamEvent(eventType, data, model, callIdMap);
2086
+ const chunk = this.handleStreamEvent(eventType, data, model, callIdMap, request);
1643
2087
  if (chunk) {
1644
2088
  yield chunk;
1645
2089
  if (chunk.type === "done") return;
@@ -1653,7 +2097,7 @@ var OpenAIResponsesProvider = class {
1653
2097
  reader.releaseLock();
1654
2098
  }
1655
2099
  }
1656
- handleStreamEvent(eventType, data, model, callIdMap) {
2100
+ handleStreamEvent(eventType, data, model, callIdMap, request) {
1657
2101
  switch (eventType) {
1658
2102
  case "response.output_text.delta":
1659
2103
  return { type: "text_delta", content: data.delta ?? "" };
@@ -1687,19 +2131,19 @@ var OpenAIResponsesProvider = class {
1687
2131
  completion_tokens: response.usage.output_tokens,
1688
2132
  total_tokens: response.usage.total_tokens,
1689
2133
  reasoning_tokens: response.usage.output_tokens_details?.reasoning_tokens,
1690
- cached_tokens: response.usage.input_tokens_details?.cached_tokens
2134
+ cached_tokens: response.usage.input_tokens_details?.cached_tokens,
2135
+ cache_write_tokens: response.usage.input_tokens_details?.cache_write_tokens
1691
2136
  } : void 0;
1692
2137
  const reasoningItems = response?.output?.filter((item) => item.type === "reasoning") ?? [];
1693
2138
  const providerMetadata = reasoningItems.length > 0 ? { openaiReasoningItems: reasoningItems } : void 0;
1694
2139
  return {
1695
2140
  type: "done",
1696
2141
  usage,
1697
- cost: usage ? estimateOpenAICost(
1698
- model,
1699
- usage.prompt_tokens,
1700
- usage.completion_tokens,
1701
- usage.cached_tokens
1702
- ) : void 0,
2142
+ cost: usage ? estimateDirectOpenAICost(response?.model ?? model, usage, {
2143
+ baseUrl: this.baseUrl,
2144
+ request,
2145
+ response
2146
+ }) : void 0,
1703
2147
  providerMetadata
1704
2148
  };
1705
2149
  }
@@ -1733,68 +2177,357 @@ var OpenAIResponsesProvider = class {
1733
2177
 
1734
2178
  // src/providers/anthropic.ts
1735
2179
  var ANTHROPIC_API_VERSION = "2023-06-01";
1736
- var ANTHROPIC_PRICING = {
1737
- "claude-opus-4-8": [5e-6, 25e-6],
1738
- "claude-opus-4-7": [5e-6, 25e-6],
1739
- "claude-opus-4-6": [5e-6, 25e-6],
1740
- "claude-sonnet-4-6": [3e-6, 15e-6],
1741
- "claude-opus-4-5": [5e-6, 25e-6],
1742
- "claude-opus-4-1": [15e-6, 75e-6],
1743
- "claude-sonnet-4-5": [3e-6, 15e-6],
1744
- "claude-haiku-4-5": [1e-6, 5e-6],
1745
- "claude-sonnet-4": [3e-6, 15e-6],
1746
- "claude-opus-4": [15e-6, 75e-6],
1747
- "claude-3-7-sonnet": [3e-6, 15e-6],
1748
- "claude-3-5-sonnet": [3e-6, 15e-6],
1749
- "claude-3-5-haiku": [8e-7, 4e-6],
1750
- "claude-3-opus": [15e-6, 75e-6],
1751
- "claude-3-sonnet": [3e-6, 15e-6],
1752
- "claude-3-haiku": [25e-8, 125e-8]
2180
+ var CLAUDE_CAPABILITIES = {
2181
+ "claude-fable-5": {
2182
+ thinking: "adaptive-always-on",
2183
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
2184
+ manualBudget: false,
2185
+ stripTemperature: true
2186
+ },
2187
+ "claude-opus-5": {
2188
+ thinking: "adaptive-default-on",
2189
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
2190
+ manualBudget: false,
2191
+ stripTemperature: true,
2192
+ disableAt: "high"
2193
+ },
2194
+ "claude-sonnet-5": {
2195
+ thinking: "adaptive-default-on",
2196
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
2197
+ manualBudget: false,
2198
+ stripTemperature: true,
2199
+ disableAt: "max"
2200
+ },
2201
+ "claude-opus-4-8": {
2202
+ thinking: "adaptive-optional",
2203
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
2204
+ manualBudget: false,
2205
+ stripTemperature: true
2206
+ },
2207
+ "claude-opus-4-7": {
2208
+ thinking: "adaptive-optional",
2209
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
2210
+ manualBudget: false,
2211
+ stripTemperature: true
2212
+ },
2213
+ "claude-opus-4-6": {
2214
+ thinking: "adaptive-optional",
2215
+ effortLevels: ["low", "medium", "high", "max"],
2216
+ manualBudget: true
2217
+ },
2218
+ "claude-sonnet-4-6": {
2219
+ thinking: "adaptive-optional",
2220
+ effortLevels: ["low", "medium", "high", "max"],
2221
+ manualBudget: true
2222
+ },
2223
+ "claude-opus-4-5": {
2224
+ thinking: "effort-only",
2225
+ effortLevels: ["low", "medium", "high"],
2226
+ manualBudget: true
2227
+ },
2228
+ "claude-opus-4-5-20251101": {
2229
+ thinking: "effort-only",
2230
+ effortLevels: ["low", "medium", "high"],
2231
+ manualBudget: true
2232
+ }
1753
2233
  };
1754
- var ANTHROPIC_PRICING_KEYS_BY_LENGTH = Object.keys(ANTHROPIC_PRICING).sort(
1755
- (a, b) => b.length - a.length
1756
- );
1757
- function estimateAnthropicCost(model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens) {
1758
- let pricing = ANTHROPIC_PRICING[model];
1759
- if (!pricing) {
1760
- for (const key of ANTHROPIC_PRICING_KEYS_BY_LENGTH) {
1761
- if (model.startsWith(key)) {
1762
- pricing = ANTHROPIC_PRICING[key];
1763
- break;
1764
- }
1765
- }
2234
+ var LEGACY_CLAUDE_CAPABILITY = {
2235
+ thinking: "legacy-manual",
2236
+ manualBudget: true
2237
+ };
2238
+ var LEGACY_CLAUDE_MODELS = /* @__PURE__ */ new Set([
2239
+ "claude-opus-4-1",
2240
+ "claude-opus-4-1-20250805",
2241
+ "claude-sonnet-4-5",
2242
+ "claude-sonnet-4-5-20250929",
2243
+ "claude-haiku-4-5",
2244
+ "claude-haiku-4-5-20251001",
2245
+ "claude-sonnet-4",
2246
+ "claude-sonnet-4-20250514",
2247
+ "claude-opus-4",
2248
+ "claude-opus-4-20250514",
2249
+ "claude-3-7-sonnet",
2250
+ "claude-3-7-sonnet-20250219",
2251
+ "claude-3-5-sonnet",
2252
+ "claude-3-5-sonnet-20241022",
2253
+ "claude-3-5-sonnet-20240620",
2254
+ "claude-3-5-haiku",
2255
+ "claude-3-5-haiku-20241022",
2256
+ "claude-3-opus",
2257
+ "claude-3-opus-20240229",
2258
+ "claude-3-sonnet",
2259
+ "claude-3-sonnet-20240229",
2260
+ "claude-3-haiku",
2261
+ "claude-3-haiku-20240307"
2262
+ ]);
2263
+ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
2264
+ ...LEGACY_CLAUDE_MODELS,
2265
+ "claude-opus-4-5",
2266
+ "claude-opus-4-5-20251101"
2267
+ ]);
2268
+ function resolveClaudeCapability(model) {
2269
+ return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
2270
+ }
2271
+ var ANTHROPIC_RATES = {
2272
+ "claude-fable-5": { input: 1e-5, output: 5e-5 },
2273
+ "claude-opus-5": { input: 5e-6, output: 25e-6 },
2274
+ "claude-sonnet-5": { input: 3e-6, output: 15e-6, sonnet5Intro: true },
2275
+ "claude-opus-4-8": { input: 5e-6, output: 25e-6 },
2276
+ "claude-opus-4-7": { input: 5e-6, output: 25e-6 },
2277
+ "claude-opus-4-6": { input: 5e-6, output: 25e-6 },
2278
+ "claude-sonnet-4-6": { input: 3e-6, output: 15e-6 },
2279
+ "claude-opus-4-5": { input: 5e-6, output: 25e-6 },
2280
+ "claude-opus-4-5-20251101": { input: 5e-6, output: 25e-6 },
2281
+ "claude-opus-4-1": { input: 15e-6, output: 75e-6 },
2282
+ "claude-opus-4-1-20250805": { input: 15e-6, output: 75e-6 },
2283
+ "claude-sonnet-4-5": { input: 3e-6, output: 15e-6 },
2284
+ "claude-sonnet-4-5-20250929": { input: 3e-6, output: 15e-6 },
2285
+ "claude-haiku-4-5": { input: 1e-6, output: 5e-6 },
2286
+ "claude-haiku-4-5-20251001": { input: 1e-6, output: 5e-6 }
2287
+ };
2288
+ function isValidTokenCount(value) {
2289
+ return value !== void 0 && Number.isSafeInteger(value) && value >= 0;
2290
+ }
2291
+ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
2292
+ if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
2293
+ return void 0;
1766
2294
  }
1767
- if (!pricing) return void 0;
1768
- const [inputRate, outputRate] = pricing;
1769
- const cacheRead = cacheReadTokens ?? 0;
1770
- const cacheWrite = cacheWriteTokens ?? 0;
1771
- const inputCost = (inputTokens - cacheRead - cacheWrite) * inputRate + cacheRead * inputRate * 0.1 + cacheWrite * inputRate * 1.25;
1772
- return inputCost + outputTokens * outputRate;
2295
+ const rate = ANTHROPIC_RATES[model];
2296
+ if (!rate) return void 0;
2297
+ const cacheRead = usage.cacheReadTokens ?? 0;
2298
+ const cacheWrite5m = usage.cacheWrite5mTokens ?? 0;
2299
+ const cacheWrite1h = usage.cacheWrite1hTokens ?? 0;
2300
+ if (!isValidTokenCount(cacheRead) || !isValidTokenCount(cacheWrite5m) || !isValidTokenCount(cacheWrite1h)) {
2301
+ return void 0;
2302
+ }
2303
+ const cacheWriteTotal = safeTokenSum(cacheWrite5m, cacheWrite1h);
2304
+ if (cacheWriteTotal === void 0 || safeTokenSum(usage.inputTokens, cacheRead, cacheWriteTotal) === void 0) {
2305
+ return void 0;
2306
+ }
2307
+ if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
2308
+ return void 0;
2309
+ }
2310
+ const { input, output } = rate.sonnet5Intro && now < /* @__PURE__ */ new Date("2026-09-01T00:00:00Z") ? { input: 2e-6, output: 1e-5 } : rate;
2311
+ return usage.inputTokens * input + cacheRead * input * 0.1 + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
1773
2312
  }
1774
2313
  var THINKING_BUDGETS = {
1775
2314
  low: 1024,
1776
2315
  medium: 5e3,
1777
2316
  high: 1e4,
2317
+ xhigh: 2e4,
1778
2318
  // 30000 (not 32000) to stay under the 32K max_tokens limit on Opus 4/4.1.
1779
2319
  // With auto-bump (+1024), max_tokens becomes 31024 which fits all models.
1780
2320
  max: 3e4
1781
2321
  };
1782
- function supportsAdaptiveThinking(model) {
1783
- return model.startsWith("claude-opus-4-8") || model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6");
2322
+ function clampAnthropicEffort(capability, effort) {
2323
+ const levels = capability.effortLevels;
2324
+ if (!levels) return effort === "max" || effort === "xhigh" ? "high" : effort;
2325
+ if (levels.includes(effort)) return effort;
2326
+ return "high";
1784
2327
  }
1785
- function supportsMaxEffort(model) {
1786
- return model.startsWith("claude-opus-4-8") || model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6");
2328
+ function budgetToClaudeEffort(budget) {
2329
+ if (budget <= THINKING_BUDGETS.low) return "low";
2330
+ if (budget <= THINKING_BUDGETS.medium) return "medium";
2331
+ if (budget <= THINKING_BUDGETS.high) return "high";
2332
+ if (budget <= THINKING_BUDGETS.xhigh) return "xhigh";
2333
+ return "max";
2334
+ }
2335
+ var warnedFableDisable = /* @__PURE__ */ new Set();
2336
+ function warnFableDisable(model) {
2337
+ if (warnedFableDisable.has(model)) return;
2338
+ warnedFableDisable.add(model);
2339
+ console.warn(
2340
+ `[axl] thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'.`
2341
+ );
1787
2342
  }
1788
- function supportsXhigh2(model) {
1789
- return model.startsWith("claude-opus-4-8") || model.startsWith("claude-opus-4-7");
2343
+ function resolveClaudeThinking(model, resolved) {
2344
+ const capability = resolveClaudeCapability(model);
2345
+ if (!capability) return { stripTemperature: false };
2346
+ const activeEffort = resolved.activeEffort ? clampAnthropicEffort(capability, resolved.activeEffort) : void 0;
2347
+ const isClaude5 = capability.thinking === "adaptive-default-on" || capability.thinking === "adaptive-always-on";
2348
+ if (isClaude5) {
2349
+ if (resolved.hasBudgetOverride) {
2350
+ const effort = clampAnthropicEffort(
2351
+ capability,
2352
+ budgetToClaudeEffort(resolved.thinkingBudget)
2353
+ );
2354
+ return {
2355
+ thinking: { type: "adaptive" },
2356
+ outputConfig: { effort },
2357
+ stripTemperature: true
2358
+ };
2359
+ }
2360
+ if (resolved.thinkingDisabled) {
2361
+ if (capability.thinking === "adaptive-always-on") {
2362
+ warnFableDisable(model);
2363
+ return {
2364
+ thinking: { type: "adaptive" },
2365
+ outputConfig: { effort: "low" },
2366
+ stripTemperature: true
2367
+ };
2368
+ }
2369
+ if (activeEffort && capability.disableAt && !isEffortAtOrBelow(activeEffort, capability.disableAt)) {
2370
+ return {
2371
+ thinking: { type: "adaptive" },
2372
+ outputConfig: { effort: activeEffort },
2373
+ stripTemperature: true
2374
+ };
2375
+ }
2376
+ return {
2377
+ thinking: { type: "disabled" },
2378
+ outputConfig: activeEffort ? { effort: activeEffort } : void 0,
2379
+ stripTemperature: true
2380
+ };
2381
+ }
2382
+ return activeEffort ? {
2383
+ thinking: { type: "adaptive" },
2384
+ outputConfig: { effort: activeEffort },
2385
+ stripTemperature: true
2386
+ } : { stripTemperature: true };
2387
+ }
2388
+ if (resolved.hasBudgetOverride) {
2389
+ if (!capability.manualBudget) {
2390
+ const effort = clampAnthropicEffort(
2391
+ capability,
2392
+ budgetToClaudeEffort(resolved.thinkingBudget)
2393
+ );
2394
+ return capability.thinking === "adaptive-optional" ? {
2395
+ thinking: { type: "adaptive" },
2396
+ outputConfig: { effort },
2397
+ stripTemperature: capability.stripTemperature ?? true
2398
+ } : { outputConfig: { effort }, stripTemperature: capability.stripTemperature ?? false };
2399
+ }
2400
+ return {
2401
+ thinking: { type: "enabled", budget_tokens: resolved.thinkingBudget },
2402
+ outputConfig: activeEffort && capability.effortLevels ? { effort: activeEffort } : void 0,
2403
+ manualBudget: resolved.thinkingBudget,
2404
+ stripTemperature: capability.stripTemperature ?? true
2405
+ };
2406
+ }
2407
+ if (resolved.thinkingDisabled) {
2408
+ return {
2409
+ outputConfig: activeEffort && capability.effortLevels ? { effort: activeEffort } : void 0,
2410
+ stripTemperature: capability.stripTemperature ?? false
2411
+ };
2412
+ }
2413
+ if (activeEffort && capability.thinking === "adaptive-optional") {
2414
+ return {
2415
+ thinking: { type: "adaptive" },
2416
+ outputConfig: { effort: activeEffort },
2417
+ stripTemperature: capability.stripTemperature ?? true
2418
+ };
2419
+ }
2420
+ if (activeEffort && capability.thinking === "effort-only") {
2421
+ return { outputConfig: { effort: activeEffort }, stripTemperature: false };
2422
+ }
2423
+ if (activeEffort) {
2424
+ const budget = THINKING_BUDGETS[activeEffort] ?? THINKING_BUDGETS.medium;
2425
+ return {
2426
+ thinking: { type: "enabled", budget_tokens: budget },
2427
+ manualBudget: budget,
2428
+ stripTemperature: true
2429
+ };
2430
+ }
2431
+ return { stripTemperature: capability.stripTemperature ?? false };
1790
2432
  }
1791
- function supportsEffort(model) {
1792
- return model.startsWith("claude-opus-4-8") || model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6") || model.startsWith("claude-opus-4-5");
2433
+ function isEffortAtOrBelow(effort, ceiling) {
2434
+ const levels = ["low", "medium", "high", "xhigh", "max"];
2435
+ return levels.indexOf(effort) <= levels.indexOf(ceiling);
1793
2436
  }
1794
- function clampAnthropicEffort(model, effort) {
1795
- if (effort === "max" && !supportsMaxEffort(model)) return "high";
1796
- if (effort === "xhigh" && !supportsXhigh2(model)) return "high";
1797
- return effort;
2437
+ function pricingContextFromBody(body) {
2438
+ return {
2439
+ model: typeof body.model === "string" ? body.model : void 0,
2440
+ unpricedModifier: isAnthropicModifier("inference_geo", body.inference_geo) || isAnthropicModifier("speed", body.speed) || hasAnthropicServerTool(body.tools) || body.fallbacks !== void 0
2441
+ };
2442
+ }
2443
+ function isModifiedAnthropicResponse(json, model) {
2444
+ return isAnthropicResponseModifier("inference_geo", json.inference_geo, model) || isAnthropicResponseModifier("speed", json.speed, model) || isAnthropicResponseModifier("inference_geo", json.usage?.inference_geo, model) || isAnthropicResponseModifier("speed", json.usage?.speed, model) || hasBilledUnmodeledUsage(json.usage?.server_tool_use) || hasUnmodeledIterations(json.usage?.iterations);
2445
+ }
2446
+ function isAnthropicModifier(kind, value) {
2447
+ if (value === void 0) return false;
2448
+ return kind === "speed" ? value !== "standard" : value !== "global";
2449
+ }
2450
+ function isAnthropicResponseModifier(kind, value, model) {
2451
+ if (value === void 0) return false;
2452
+ if (kind === "speed") return value !== "standard";
2453
+ if (value === "global") return false;
2454
+ return value !== "not_available" || model === void 0 || !PRE_INFERENCE_GEO_MODELS.has(model);
2455
+ }
2456
+ function hasAnthropicServerTool(tools) {
2457
+ return Array.isArray(tools) && tools.some(
2458
+ (tool2) => tool2 !== null && typeof tool2 === "object" && typeof tool2.type === "string"
2459
+ );
2460
+ }
2461
+ function hasBilledUnmodeledUsage(value) {
2462
+ return value !== void 0 && value !== 0;
2463
+ }
2464
+ function hasUnmodeledIterations(value) {
2465
+ return value !== void 0;
2466
+ }
2467
+ function hasFallbackIteration(value) {
2468
+ return Array.isArray(value) && value.some(
2469
+ (iteration) => iteration !== null && typeof iteration === "object" && iteration.type === "fallback_message"
2470
+ );
2471
+ }
2472
+ function safeTokenSum(...values) {
2473
+ const total = values.reduce((sum, value) => sum + value, 0);
2474
+ return Number.isSafeInteger(total) ? total : void 0;
2475
+ }
2476
+ function normalizeAnthropicUsage(raw) {
2477
+ const inputTokens = raw.input_tokens;
2478
+ const outputTokens = raw.output_tokens;
2479
+ const cacheReadTokens = raw.cache_read_input_tokens ?? 0;
2480
+ const cacheWrite5mTokens = raw.cache_creation?.ephemeral_5m_input_tokens ?? 0;
2481
+ const cacheWrite1hTokens = raw.cache_creation?.ephemeral_1h_input_tokens ?? 0;
2482
+ if (!isValidTokenCount(inputTokens) || !isValidTokenCount(outputTokens) || !isValidTokenCount(cacheReadTokens) || !isValidTokenCount(cacheWrite5mTokens) || !isValidTokenCount(cacheWrite1hTokens)) {
2483
+ return void 0;
2484
+ }
2485
+ const hasTtlBreakdown = raw.cache_creation?.ephemeral_5m_input_tokens !== void 0 || raw.cache_creation?.ephemeral_1h_input_tokens !== void 0;
2486
+ const aggregateCacheWriteTokens = raw.cache_creation_input_tokens;
2487
+ const normalizedCacheWrite = hasTtlBreakdown ? safeTokenSum(cacheWrite5mTokens, cacheWrite1hTokens) : void 0;
2488
+ if (hasTtlBreakdown && normalizedCacheWrite === void 0) return void 0;
2489
+ const promptCacheWrite = aggregateCacheWriteTokens ?? normalizedCacheWrite ?? 0;
2490
+ if (!isValidTokenCount(promptCacheWrite)) return void 0;
2491
+ const promptTokens = safeTokenSum(inputTokens, cacheReadTokens, promptCacheWrite);
2492
+ if (promptTokens === void 0) return void 0;
2493
+ const totalTokens = safeTokenSum(promptTokens, outputTokens);
2494
+ if (totalTokens === void 0) return void 0;
2495
+ return {
2496
+ usage: {
2497
+ prompt_tokens: promptTokens,
2498
+ completion_tokens: outputTokens,
2499
+ total_tokens: totalTokens,
2500
+ cached_tokens: cacheReadTokens > 0 ? cacheReadTokens : void 0,
2501
+ // The aggregate is still useful public usage telemetry even though it
2502
+ // cannot be priced without the 5m/1h split. Keep pricingUsage separate
2503
+ // so estimateAnthropicCost continues to fail closed in that case.
2504
+ cache_write_tokens: (normalizedCacheWrite ?? aggregateCacheWriteTokens ?? 0) > 0 ? normalizedCacheWrite ?? aggregateCacheWriteTokens : void 0
2505
+ },
2506
+ pricingUsage: {
2507
+ inputTokens,
2508
+ outputTokens,
2509
+ cacheReadTokens,
2510
+ cacheWrite5mTokens,
2511
+ cacheWrite1hTokens,
2512
+ aggregateCacheWriteTokens
2513
+ }
2514
+ };
2515
+ }
2516
+ function mergeAnthropicUsage(initial, update) {
2517
+ return {
2518
+ input_tokens: update.input_tokens ?? initial?.input_tokens,
2519
+ output_tokens: update.output_tokens ?? initial?.output_tokens,
2520
+ cache_read_input_tokens: update.cache_read_input_tokens ?? initial?.cache_read_input_tokens,
2521
+ cache_creation_input_tokens: update.cache_creation_input_tokens ?? initial?.cache_creation_input_tokens,
2522
+ cache_creation: {
2523
+ ephemeral_5m_input_tokens: update.cache_creation?.ephemeral_5m_input_tokens ?? initial?.cache_creation?.ephemeral_5m_input_tokens,
2524
+ ephemeral_1h_input_tokens: update.cache_creation?.ephemeral_1h_input_tokens ?? initial?.cache_creation?.ephemeral_1h_input_tokens
2525
+ },
2526
+ speed: update.speed ?? initial?.speed,
2527
+ inference_geo: update.inference_geo ?? initial?.inference_geo,
2528
+ iterations: update.iterations ?? initial?.iterations,
2529
+ server_tool_use: update.server_tool_use ?? initial?.server_tool_use
2530
+ };
1798
2531
  }
1799
2532
  var AnthropicProvider = class {
1800
2533
  name = "anthropic";
@@ -1809,6 +2542,11 @@ var AnthropicProvider = class {
1809
2542
  constructor(options = {}) {
1810
2543
  this.apiKeySource = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1811
2544
  this.baseUrl = (options.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/$/, "");
2545
+ assertSafeProviderBaseUrl(
2546
+ this.baseUrl,
2547
+ "Anthropic provider",
2548
+ options.dangerouslyAllowInsecureHttp
2549
+ );
1812
2550
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1813
2551
  if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1814
2552
  throw new Error(
@@ -1832,6 +2570,7 @@ var AnthropicProvider = class {
1832
2570
  async chat(messages, options) {
1833
2571
  const headers = this.buildHeaders(await this.resolveKey());
1834
2572
  const body = this.buildRequestBody(messages, options, false);
2573
+ const pricingContext = pricingContextFromBody(body);
1835
2574
  const res = await fetchWithRetry(
1836
2575
  `${this.baseUrl}/messages`,
1837
2576
  {
@@ -1854,7 +2593,7 @@ var AnthropicProvider = class {
1854
2593
  });
1855
2594
  }
1856
2595
  const json = await res.json();
1857
- return this.parseResponse(json, options.model);
2596
+ return this.parseResponse(json, pricingContext);
1858
2597
  }
1859
2598
  // ---------------------------------------------------------------------------
1860
2599
  // stream - SSE streaming completion
@@ -1862,6 +2601,7 @@ var AnthropicProvider = class {
1862
2601
  async *stream(messages, options) {
1863
2602
  const headers = this.buildHeaders(await this.resolveKey());
1864
2603
  const body = this.buildRequestBody(messages, options, true);
2604
+ const pricingContext = pricingContextFromBody(body);
1865
2605
  const res = await fetchWithRetry(
1866
2606
  `${this.baseUrl}/messages`,
1867
2607
  {
@@ -1886,7 +2626,7 @@ var AnthropicProvider = class {
1886
2626
  if (!res.body) {
1887
2627
  throw new Error("Anthropic stream response has no body");
1888
2628
  }
1889
- yield* this.parseSSEStream(res.body, options.model);
2629
+ yield* this.parseSSEStream(res.body, pricingContext);
1890
2630
  }
1891
2631
  // ---------------------------------------------------------------------------
1892
2632
  // Internal: request building
@@ -1910,11 +2650,12 @@ var AnthropicProvider = class {
1910
2650
  return `Anthropic API error (${status}): ${body}`;
1911
2651
  }
1912
2652
  buildRequestBody(messages, options, stream) {
2653
+ const effectiveModel = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
1913
2654
  const systemMessages = messages.filter((m) => m.role === "system");
1914
2655
  const nonSystemMessages = messages.filter((m) => m.role !== "system");
1915
2656
  const systemText = systemMessages.map((m) => m.content).join("\n\n");
1916
2657
  const body = {
1917
- model: options.model,
2658
+ model: effectiveModel,
1918
2659
  messages: this.mapMessages(nonSystemMessages),
1919
2660
  max_tokens: options.maxTokens ?? 4096,
1920
2661
  stream
@@ -1931,35 +2672,16 @@ var AnthropicProvider = class {
1931
2672
  if (options.toolChoice !== void 0) {
1932
2673
  body.tool_choice = this.mapToolChoice(options.toolChoice);
1933
2674
  }
1934
- const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolveThinkingOptions(options);
1935
- const resolvedEffort = activeEffort ? clampAnthropicEffort(options.model, activeEffort) : void 0;
1936
- if (hasBudgetOverride) {
1937
- body.thinking = { type: "enabled", budget_tokens: thinkingBudget };
1938
- const currentMax = body.max_tokens;
1939
- if (currentMax < thinkingBudget + 1024) {
1940
- body.max_tokens = thinkingBudget + 1024;
1941
- }
1942
- if (resolvedEffort && supportsEffort(options.model)) {
1943
- body.output_config = { effort: resolvedEffort };
1944
- }
1945
- } else if (thinkingDisabled) {
1946
- if (resolvedEffort && supportsEffort(options.model)) {
1947
- body.output_config = { effort: resolvedEffort };
1948
- }
1949
- } else if (resolvedEffort && supportsAdaptiveThinking(options.model)) {
1950
- body.thinking = { type: "adaptive" };
1951
- body.output_config = { effort: resolvedEffort };
1952
- } else if (resolvedEffort && supportsEffort(options.model)) {
1953
- body.output_config = { effort: resolvedEffort };
1954
- } else if (resolvedEffort) {
1955
- const budget = THINKING_BUDGETS[resolvedEffort] ?? 5e3;
1956
- body.thinking = { type: "enabled", budget_tokens: budget };
2675
+ const thinking = resolveClaudeThinking(effectiveModel, resolveThinkingOptions(options));
2676
+ if (thinking.thinking) body.thinking = thinking.thinking;
2677
+ if (thinking.outputConfig) body.output_config = thinking.outputConfig;
2678
+ if (thinking.manualBudget) {
1957
2679
  const currentMax = body.max_tokens;
1958
- if (currentMax < budget + 1024) {
1959
- body.max_tokens = budget + 1024;
2680
+ if (currentMax < thinking.manualBudget + 1024) {
2681
+ body.max_tokens = thinking.manualBudget + 1024;
1960
2682
  }
1961
2683
  }
1962
- if (options.temperature !== void 0 && !body.thinking) {
2684
+ if (options.temperature !== void 0 && !thinking.stripTemperature) {
1963
2685
  body.temperature = options.temperature;
1964
2686
  }
1965
2687
  if (options.responseFormat && options.responseFormat.type !== "text") {
@@ -1984,8 +2706,9 @@ ${jsonInstruction}` : jsonInstruction;
1984
2706
  const result = [];
1985
2707
  for (const msg of messages) {
1986
2708
  if (msg.role === "assistant") {
2709
+ const replayedThinking = this.getReplayedThinkingBlocks(msg);
1987
2710
  if (msg.tool_calls && msg.tool_calls.length > 0) {
1988
- const content = [];
2711
+ const content = [...replayedThinking];
1989
2712
  if (msg.content) {
1990
2713
  content.push({ type: "text", text: msg.content });
1991
2714
  }
@@ -2004,6 +2727,10 @@ ${jsonInstruction}` : jsonInstruction;
2004
2727
  });
2005
2728
  }
2006
2729
  result.push({ role: "assistant", content });
2730
+ } else if (replayedThinking.length > 0) {
2731
+ const content = [...replayedThinking];
2732
+ if (msg.content) content.push({ type: "text", text: msg.content });
2733
+ result.push({ role: "assistant", content });
2007
2734
  } else {
2008
2735
  result.push({ role: "assistant", content: msg.content });
2009
2736
  }
@@ -2024,6 +2751,13 @@ ${jsonInstruction}` : jsonInstruction;
2024
2751
  }
2025
2752
  return this.mergeConsecutiveRoles(result);
2026
2753
  }
2754
+ getReplayedThinkingBlocks(msg) {
2755
+ const blocks = msg.providerMetadata?.anthropicThinkingBlocks;
2756
+ if (!Array.isArray(blocks)) return [];
2757
+ return blocks.filter(
2758
+ (block) => block !== null && typeof block === "object" && (block.type === "thinking" || block.type === "redacted_thinking")
2759
+ );
2760
+ }
2027
2761
  /**
2028
2762
  * Merge consecutive messages with the same role into a single message.
2029
2763
  * This handles cases where multiple tool_result blocks need to be in one user message.
@@ -2079,16 +2813,24 @@ ${jsonInstruction}` : jsonInstruction;
2079
2813
  // ---------------------------------------------------------------------------
2080
2814
  // Internal: response parsing
2081
2815
  // ---------------------------------------------------------------------------
2082
- parseResponse(json, model) {
2816
+ parseResponse(json, pricingContext) {
2083
2817
  let content = "";
2084
2818
  let thinkingContent = "";
2085
2819
  const toolCalls = [];
2086
- for (const block of json.content) {
2820
+ const thinkingBlocks = [];
2821
+ const fallbackIndex = lastFallbackIndex(json.content);
2822
+ const hasFallbackBoundary = fallbackIndex >= 0 || hasFallbackIteration(json.usage?.iterations);
2823
+ let refused = json.stop_reason === "refusal";
2824
+ for (const [index, block] of json.content.entries()) {
2087
2825
  if (block.type === "thinking") {
2088
2826
  thinkingContent += block.thinking;
2827
+ thinkingBlocks.push(block);
2828
+ } else if (block.type === "redacted_thinking") {
2829
+ thinkingBlocks.push(block);
2089
2830
  } else if (block.type === "text") {
2090
2831
  content += block.text;
2091
2832
  } else if (block.type === "tool_use") {
2833
+ if (fallbackIndex >= 0 && index < fallbackIndex) continue;
2092
2834
  toolCalls.push({
2093
2835
  id: block.id,
2094
2836
  type: "function",
@@ -2097,37 +2839,58 @@ ${jsonInstruction}` : jsonInstruction;
2097
2839
  arguments: JSON.stringify(block.input)
2098
2840
  }
2099
2841
  });
2842
+ } else if (block.type === "fallback") {
2843
+ } else if (block.type === "refusal") {
2844
+ refused = true;
2100
2845
  }
2101
2846
  }
2102
- const cacheRead = json.usage?.cache_read_input_tokens ?? 0;
2103
- const cacheWrite = json.usage?.cache_creation_input_tokens ?? 0;
2104
- const totalInput = (json.usage?.input_tokens ?? 0) + cacheRead + cacheWrite;
2105
- const usage = json.usage ? {
2106
- prompt_tokens: totalInput,
2107
- completion_tokens: json.usage.output_tokens,
2108
- total_tokens: totalInput + json.usage.output_tokens,
2109
- cached_tokens: cacheRead > 0 ? cacheRead : void 0
2110
- } : void 0;
2111
- const cost = json.usage ? estimateAnthropicCost(model, totalInput, json.usage.output_tokens, cacheRead, cacheWrite) : void 0;
2847
+ const normalized = json.usage ? normalizeAnthropicUsage(json.usage) : void 0;
2848
+ const effectiveModel = typeof json.model === "string" ? json.model : pricingContext.model;
2849
+ if (hasFallbackBoundary) {
2850
+ throw new Error(
2851
+ "Anthropic fallback responses cannot be continued safely; retry without fallbacks."
2852
+ );
2853
+ }
2854
+ const cost = normalized && !pricingContext.unpricedModifier && !isModifiedAnthropicResponse(json, effectiveModel) && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, normalized.pricingUsage) : void 0;
2112
2855
  return {
2113
2856
  content,
2114
2857
  thinking_content: thinkingContent || void 0,
2115
2858
  tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
2116
- usage,
2117
- cost
2859
+ usage: normalized?.usage,
2860
+ cost,
2861
+ providerMetadata: thinkingBlocks.length > 0 && !hasFallbackBoundary ? { anthropicThinkingBlocks: thinkingBlocks } : void 0
2118
2862
  };
2119
2863
  }
2120
2864
  // ---------------------------------------------------------------------------
2121
2865
  // Internal: SSE stream parsing
2122
2866
  // ---------------------------------------------------------------------------
2123
- async *parseSSEStream(body, model) {
2867
+ async *parseSSEStream(body, pricingContext) {
2124
2868
  const reader = body.getReader();
2125
2869
  const decoder = new TextDecoder();
2126
2870
  let buffer = "";
2127
2871
  let currentToolId = "";
2128
2872
  let currentToolName = "";
2129
2873
  let usage;
2130
- let cacheWrite = 0;
2874
+ let pricingUsage;
2875
+ let rawUsage;
2876
+ const thinkingBlocks = [];
2877
+ let currentThinkingBlock;
2878
+ let effectiveModel = pricingContext.model;
2879
+ let unpricedModifier = pricingContext.unpricedModifier;
2880
+ let hasFallbackBoundary = false;
2881
+ let hasFallbackIterationSignal = false;
2882
+ let refused = false;
2883
+ const finalizeUsage = () => {
2884
+ const normalized = rawUsage ? normalizeAnthropicUsage(rawUsage) : void 0;
2885
+ usage = normalized?.usage;
2886
+ pricingUsage = normalized?.pricingUsage;
2887
+ };
2888
+ const doneChunk = () => ({
2889
+ type: "done",
2890
+ usage,
2891
+ cost: pricingUsage && !unpricedModifier && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, pricingUsage) : void 0,
2892
+ providerMetadata: thinkingBlocks.length > 0 && !hasFallbackBoundary && !hasFallbackIterationSignal ? { anthropicThinkingBlocks: thinkingBlocks } : void 0
2893
+ });
2131
2894
  try {
2132
2895
  while (true) {
2133
2896
  const { done, value } = await reader.read();
@@ -2158,13 +2921,31 @@ ${jsonInstruction}` : jsonInstruction;
2158
2921
  name: currentToolName,
2159
2922
  arguments: ""
2160
2923
  };
2924
+ } else if (block?.type === "thinking" || block?.type === "redacted_thinking") {
2925
+ currentThinkingBlock = {
2926
+ ...block,
2927
+ type: block.type
2928
+ };
2929
+ } else if (block?.type === "fallback") {
2930
+ hasFallbackBoundary = true;
2931
+ } else if (block?.type === "refusal") {
2932
+ refused = true;
2161
2933
  }
2162
2934
  break;
2163
2935
  }
2164
2936
  case "content_block_delta": {
2165
2937
  const delta = event.delta;
2166
2938
  if (delta?.type === "thinking_delta" && delta.thinking) {
2939
+ if (currentThinkingBlock?.type === "thinking") {
2940
+ const current = currentThinkingBlock.thinking;
2941
+ currentThinkingBlock.thinking = `${typeof current === "string" ? current : ""}${delta.thinking}`;
2942
+ }
2167
2943
  yield { type: "thinking_delta", content: delta.thinking };
2944
+ } else if (delta?.type === "signature_delta" && delta.signature) {
2945
+ if (currentThinkingBlock) {
2946
+ const current = currentThinkingBlock.signature;
2947
+ currentThinkingBlock.signature = `${typeof current === "string" ? current : ""}${delta.signature}`;
2948
+ }
2168
2949
  } else if (delta?.type === "text_delta" && delta.text) {
2169
2950
  yield { type: "text_delta", content: delta.text };
2170
2951
  } else if (delta?.type === "input_json_delta" && delta.partial_json) {
@@ -2177,76 +2958,82 @@ ${jsonInstruction}` : jsonInstruction;
2177
2958
  break;
2178
2959
  }
2179
2960
  case "content_block_stop": {
2961
+ if (currentThinkingBlock) {
2962
+ thinkingBlocks.push(currentThinkingBlock);
2963
+ currentThinkingBlock = void 0;
2964
+ }
2180
2965
  currentToolId = "";
2181
2966
  currentToolName = "";
2182
2967
  break;
2183
2968
  }
2184
2969
  case "message_start": {
2185
2970
  if (event.message?.usage) {
2186
- const cacheRead = event.message.usage.cache_read_input_tokens ?? 0;
2187
- cacheWrite = event.message.usage.cache_creation_input_tokens ?? 0;
2188
- const inputTokens = (event.message.usage.input_tokens ?? 0) + cacheRead + cacheWrite;
2189
- usage = {
2190
- prompt_tokens: inputTokens,
2191
- completion_tokens: 0,
2192
- total_tokens: inputTokens,
2193
- cached_tokens: cacheRead > 0 ? cacheRead : void 0
2194
- };
2971
+ rawUsage = mergeAnthropicUsage(rawUsage, event.message.usage);
2972
+ }
2973
+ if (typeof event.message?.model === "string") effectiveModel = event.message.model;
2974
+ const messageFallback = hasFallbackIteration(event.message?.usage?.iterations);
2975
+ if (isAnthropicResponseModifier(
2976
+ "inference_geo",
2977
+ event.message?.inference_geo,
2978
+ effectiveModel
2979
+ ) || isAnthropicResponseModifier("speed", event.message?.speed, effectiveModel) || isAnthropicResponseModifier(
2980
+ "inference_geo",
2981
+ event.message?.usage?.inference_geo,
2982
+ effectiveModel
2983
+ ) || isAnthropicResponseModifier("speed", event.message?.usage?.speed, effectiveModel) || hasBilledUnmodeledUsage(event.message?.usage?.server_tool_use) || hasUnmodeledIterations(event.message?.usage?.iterations)) {
2984
+ unpricedModifier = true;
2195
2985
  }
2986
+ if (messageFallback) hasFallbackIterationSignal = true;
2196
2987
  break;
2197
2988
  }
2198
2989
  case "message_delta": {
2199
2990
  if (event.usage) {
2200
- const outputTokens = event.usage.output_tokens ?? 0;
2201
- if (usage) {
2202
- usage.completion_tokens = outputTokens;
2203
- usage.total_tokens = usage.prompt_tokens + outputTokens;
2204
- } else {
2205
- usage = {
2206
- prompt_tokens: 0,
2207
- completion_tokens: outputTokens,
2208
- total_tokens: outputTokens
2209
- };
2991
+ rawUsage = mergeAnthropicUsage(rawUsage, event.usage);
2992
+ finalizeUsage();
2993
+ const terminalFallback = hasFallbackIteration(event.usage.iterations);
2994
+ if (isAnthropicResponseModifier("speed", event.usage.speed, effectiveModel) || isAnthropicResponseModifier(
2995
+ "inference_geo",
2996
+ event.usage.inference_geo,
2997
+ effectiveModel
2998
+ ) || hasBilledUnmodeledUsage(event.usage.server_tool_use) || hasUnmodeledIterations(event.usage.iterations)) {
2999
+ unpricedModifier = true;
2210
3000
  }
3001
+ if (terminalFallback) hasFallbackIterationSignal = true;
2211
3002
  }
3003
+ if (event.delta?.stop_reason === "refusal") refused = true;
2212
3004
  break;
2213
3005
  }
2214
3006
  case "message_stop": {
2215
- if (usage) {
2216
- usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
3007
+ finalizeUsage();
3008
+ if (hasFallbackBoundary || hasFallbackIterationSignal) {
3009
+ throw new Error(
3010
+ "Anthropic fallback streams cannot be continued safely; retry without fallbacks."
3011
+ );
2217
3012
  }
2218
- yield {
2219
- type: "done",
2220
- usage,
2221
- cost: usage ? estimateAnthropicCost(
2222
- model,
2223
- usage.prompt_tokens,
2224
- usage.completion_tokens,
2225
- usage.cached_tokens,
2226
- cacheWrite
2227
- ) : void 0
2228
- };
3013
+ yield doneChunk();
2229
3014
  return;
2230
3015
  }
2231
3016
  }
2232
3017
  }
2233
3018
  }
2234
- yield {
2235
- type: "done",
2236
- usage,
2237
- cost: usage ? estimateAnthropicCost(
2238
- model,
2239
- usage.prompt_tokens,
2240
- usage.completion_tokens,
2241
- usage.cached_tokens,
2242
- cacheWrite
2243
- ) : void 0
2244
- };
3019
+ finalizeUsage();
3020
+ if (hasFallbackBoundary || hasFallbackIterationSignal) {
3021
+ throw new Error(
3022
+ "Anthropic fallback streams cannot be continued safely; retry without fallbacks."
3023
+ );
3024
+ }
3025
+ yield doneChunk();
2245
3026
  } finally {
2246
3027
  reader.releaseLock();
2247
3028
  }
2248
3029
  }
2249
3030
  };
3031
+ function lastFallbackIndex(blocks) {
3032
+ for (let index = blocks.length - 1; index >= 0; index--) {
3033
+ if (blocks[index].type === "fallback") return index;
3034
+ }
3035
+ return -1;
3036
+ }
2250
3037
 
2251
3038
  // src/providers/gemini.ts
2252
3039
  var GEMINI_DISALLOWED_SCHEMA_KEYS = /* @__PURE__ */ new Set([
@@ -2294,36 +3081,92 @@ function parseGeminiFunctionResponse(content) {
2294
3081
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { result: parsed };
2295
3082
  }
2296
3083
  var GEMINI_PRICING = {
2297
- "gemini-2.5-pro": [125e-8, 1e-5],
2298
- "gemini-2.5-flash": [3e-7, 25e-7],
2299
- "gemini-2.5-flash-lite": [1e-7, 4e-7],
2300
- "gemini-2.0-flash": [1e-7, 4e-7],
2301
- "gemini-2.0-flash-lite": [1e-7, 4e-7],
2302
- "gemini-3-pro-preview": [2e-6, 12e-6],
2303
- "gemini-3-flash-preview": [5e-7, 3e-6],
2304
- "gemini-3.1-pro-preview": [2e-6, 12e-6],
2305
- "gemini-3.1-flash-lite": [25e-8, 15e-7],
2306
- "gemini-3.1-flash-lite-preview": [25e-8, 15e-7],
2307
- "gemini-3.5-flash": [15e-7, 9e-6]
3084
+ "gemini-2.5-pro": {
3085
+ input: 125e-8,
3086
+ cached: 125e-9,
3087
+ output: 1e-5,
3088
+ longContext: { input: 25e-7, cached: 25e-8, output: 15e-6 }
3089
+ },
3090
+ "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3091
+ "gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
3092
+ "gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
3093
+ "gemini-3.1-pro-preview": {
3094
+ input: 2e-6,
3095
+ cached: 2e-7,
3096
+ output: 12e-6,
3097
+ longContext: { input: 4e-6, cached: 4e-7, output: 18e-6 }
3098
+ },
3099
+ "gemini-3.1-pro-preview-customtools": {
3100
+ input: 2e-6,
3101
+ cached: 2e-7,
3102
+ output: 12e-6,
3103
+ longContext: { input: 4e-6, cached: 4e-7, output: 18e-6 }
3104
+ },
3105
+ "gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
3106
+ "gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
3107
+ "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3108
+ "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
2308
3109
  };
2309
- var GEMINI_PRICING_KEYS_BY_LENGTH = Object.keys(GEMINI_PRICING).sort(
2310
- (a, b) => b.length - a.length
2311
- );
2312
- function estimateGeminiCost(model, inputTokens, outputTokens, cachedTokens) {
2313
- let pricing = GEMINI_PRICING[model];
2314
- if (!pricing) {
2315
- for (const key of GEMINI_PRICING_KEYS_BY_LENGTH) {
2316
- if (model.startsWith(key)) {
2317
- pricing = GEMINI_PRICING[key];
2318
- break;
2319
- }
2320
- }
3110
+ function isValidTokenCount2(value) {
3111
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3112
+ }
3113
+ function isStandardGeminiRequestTier(value) {
3114
+ return value === void 0 || value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD";
3115
+ }
3116
+ function isDefinitiveStandardGeminiResponseTier(value) {
3117
+ return value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD" || value === "standard";
3118
+ }
3119
+ function isEligibleGeminiPricing(context, hasDefinitiveStandardResponseTier, hasInvalidResponseTier) {
3120
+ return context.eligibleRequest && isStandardGeminiRequestTier(context.serviceTier) && hasDefinitiveStandardResponseTier && !hasInvalidResponseTier;
3121
+ }
3122
+ function estimateGeminiCost(model, usage) {
3123
+ const pricing = GEMINI_PRICING[model];
3124
+ const cached = usage.cachedTokens ?? 0;
3125
+ if (!pricing || !isValidTokenCount2(usage.inputTokens) || !isValidTokenCount2(usage.outputTokens) || !isValidTokenCount2(cached) || cached > usage.inputTokens) {
3126
+ return void 0;
2321
3127
  }
2322
- if (!pricing) return void 0;
2323
- const [inputRate, outputRate] = pricing;
2324
- const cached = cachedTokens ?? 0;
2325
- const inputCost = (inputTokens - cached) * inputRate + cached * inputRate * 0.1;
2326
- return inputCost + outputTokens * outputRate;
3128
+ const rate = usage.inputTokens > 2e5 && pricing.longContext ? pricing.longContext : pricing;
3129
+ return (usage.inputTokens - cached) * rate.input + cached * rate.cached + usage.outputTokens * rate.output;
3130
+ }
3131
+ function safeTokenSum2(...values) {
3132
+ const total = values.reduce((sum, value) => sum + value, 0);
3133
+ return Number.isSafeInteger(total) ? total : void 0;
3134
+ }
3135
+ function normalizeGeminiUsage(raw) {
3136
+ const inputTokens = raw.promptTokenCount;
3137
+ const completionTokens = raw.candidatesTokenCount;
3138
+ const totalTokens = raw.totalTokenCount;
3139
+ const cachedTokens = raw.cachedContentTokenCount ?? 0;
3140
+ const reasoningTokens = raw.thoughtsTokenCount ?? 0;
3141
+ const toolUsePromptTokens = raw.toolUsePromptTokenCount ?? 0;
3142
+ if (!isValidTokenCount2(inputTokens) || !isValidTokenCount2(completionTokens) || !isValidTokenCount2(totalTokens) || !isValidTokenCount2(cachedTokens) || !isValidTokenCount2(reasoningTokens) || !isValidTokenCount2(toolUsePromptTokens) || cachedTokens > inputTokens) {
3143
+ return void 0;
3144
+ }
3145
+ const billedOutputTokens = safeTokenSum2(completionTokens, reasoningTokens);
3146
+ const expectedTotal = safeTokenSum2(
3147
+ inputTokens,
3148
+ completionTokens,
3149
+ reasoningTokens,
3150
+ toolUsePromptTokens
3151
+ );
3152
+ if (billedOutputTokens === void 0 || expectedTotal === void 0 || totalTokens !== expectedTotal) {
3153
+ return void 0;
3154
+ }
3155
+ return {
3156
+ usage: {
3157
+ prompt_tokens: inputTokens,
3158
+ completion_tokens: completionTokens,
3159
+ total_tokens: totalTokens,
3160
+ cached_tokens: cachedTokens > 0 ? cachedTokens : void 0,
3161
+ reasoning_tokens: reasoningTokens > 0 ? reasoningTokens : void 0
3162
+ },
3163
+ pricingUsage: {
3164
+ inputTokens,
3165
+ outputTokens: billedOutputTokens,
3166
+ cachedTokens
3167
+ },
3168
+ hasUnmodeledBilledUsage: toolUsePromptTokens !== 0
3169
+ };
2327
3170
  }
2328
3171
  var THINKING_BUDGETS2 = {
2329
3172
  low: 1024,
@@ -2342,8 +3185,23 @@ var THINKING_LEVELS = {
2342
3185
  max: "high"
2343
3186
  // 3.x caps at 'high'
2344
3187
  };
3188
+ var GEMINI_3X_MIN_THINKING_LEVEL = /* @__PURE__ */ new Map([
3189
+ ["gemini-3-pro-preview", "minimal"],
3190
+ ["gemini-3-flash-preview", "minimal"],
3191
+ ["gemini-3.1-pro-preview", "low"],
3192
+ ["gemini-3.1-pro-preview-customtools", "low"],
3193
+ ["gemini-3.1-flash-lite", "minimal"],
3194
+ ["gemini-3.1-flash-lite-preview", "minimal"],
3195
+ ["gemini-3.5-flash", "minimal"],
3196
+ ["gemini-3.5-flash-lite", "minimal"],
3197
+ ["gemini-3.6-flash", "minimal"]
3198
+ ]);
3199
+ var GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE = /* @__PURE__ */ new Set([
3200
+ "gemini-3.5-flash-lite",
3201
+ "gemini-3.6-flash"
3202
+ ]);
2345
3203
  function isGemini3x(model) {
2346
- return /^gemini-3[.-]/.test(model);
3204
+ return GEMINI_3X_MIN_THINKING_LEVEL.has(model);
2347
3205
  }
2348
3206
  function budgetToThinkingLevel(budgetTokens) {
2349
3207
  if (budgetTokens <= 1024) return "low";
@@ -2351,8 +3209,7 @@ function budgetToThinkingLevel(budgetTokens) {
2351
3209
  return "high";
2352
3210
  }
2353
3211
  function minThinkingLevel(model) {
2354
- if (model.startsWith("gemini-3.1-pro")) return "low";
2355
- return "minimal";
3212
+ return GEMINI_3X_MIN_THINKING_LEVEL.get(model) ?? "minimal";
2356
3213
  }
2357
3214
  var _warned3xEffortNone = /* @__PURE__ */ new Set();
2358
3215
  function warnGemini3xEffortNone(model) {
@@ -2380,6 +3237,11 @@ var GeminiProvider = class {
2380
3237
  /\/$/,
2381
3238
  ""
2382
3239
  );
3240
+ assertSafeProviderBaseUrl(
3241
+ this.baseUrl,
3242
+ "Google provider",
3243
+ options.dangerouslyAllowInsecureHttp
3244
+ );
2383
3245
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
2384
3246
  if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
2385
3247
  throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
@@ -2397,10 +3259,12 @@ var GeminiProvider = class {
2397
3259
  // chat - non-streaming completion
2398
3260
  // ---------------------------------------------------------------------------
2399
3261
  async chat(messages, options) {
2400
- const headers = this.buildHeaders(await this.resolveKey());
2401
3262
  const body = this.buildRequestBody(messages, options);
3263
+ const pricingContext = this.pricingContext(this.requestModel(options), body);
3264
+ this.assertSafeGemini3Continuation(messages, pricingContext.model);
3265
+ const headers = this.buildHeaders(await this.resolveKey());
2402
3266
  const res = await fetchWithRetry(
2403
- `${this.baseUrl}/models/${options.model}:generateContent`,
3267
+ `${this.baseUrl}/models/${pricingContext.model}:generateContent`,
2404
3268
  {
2405
3269
  method: "POST",
2406
3270
  headers,
@@ -2421,16 +3285,18 @@ var GeminiProvider = class {
2421
3285
  });
2422
3286
  }
2423
3287
  const json = await res.json();
2424
- return this.parseResponse(json, options.model);
3288
+ return this.parseResponse(json, pricingContext);
2425
3289
  }
2426
3290
  // ---------------------------------------------------------------------------
2427
3291
  // stream - SSE streaming completion
2428
3292
  // ---------------------------------------------------------------------------
2429
3293
  async *stream(messages, options) {
2430
- const headers = this.buildHeaders(await this.resolveKey());
2431
3294
  const body = this.buildRequestBody(messages, options);
3295
+ const pricingContext = this.pricingContext(this.requestModel(options), body);
3296
+ this.assertSafeGemini3Continuation(messages, pricingContext.model);
3297
+ const headers = this.buildHeaders(await this.resolveKey());
2432
3298
  const res = await fetchWithRetry(
2433
- `${this.baseUrl}/models/${options.model}:streamGenerateContent?alt=sse`,
3299
+ `${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
2434
3300
  {
2435
3301
  method: "POST",
2436
3302
  headers,
@@ -2453,7 +3319,7 @@ var GeminiProvider = class {
2453
3319
  if (!res.body) {
2454
3320
  throw new Error("Gemini stream response has no body");
2455
3321
  }
2456
- yield* this.parseSSEStream(res.body, options.model);
3322
+ yield* this.parseSSEStream(res.body, pricingContext);
2457
3323
  }
2458
3324
  // ---------------------------------------------------------------------------
2459
3325
  // Internal: request building
@@ -2474,7 +3340,114 @@ var GeminiProvider = class {
2474
3340
  }
2475
3341
  return `Gemini API error (${status}): ${body}`;
2476
3342
  }
3343
+ pricingContext(fallbackModel, body) {
3344
+ return {
3345
+ // Gemini's model is normally selected in the URL. providerOptions is an
3346
+ // explicit raw escape hatch, so honor a post-merge model override for both
3347
+ // the URL and pricing rather than pricing a different request.
3348
+ model: typeof body.model === "string" ? body.model : fallbackModel,
3349
+ serviceTier: body.serviceTier,
3350
+ eligibleRequest: this.baseUrl === "https://generativelanguage.googleapis.com/v1beta" && this.isTextOnlyClientRequest(body)
3351
+ };
3352
+ }
3353
+ requestModel(options) {
3354
+ return typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3355
+ }
3356
+ assertSafeGemini3Continuation(messages, model) {
3357
+ if (GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE.has(model)) {
3358
+ const mapped = this.mapMessages(messages.filter((message) => message.role !== "system"));
3359
+ if (mapped.at(-1)?.role === "model") {
3360
+ throw new Error(
3361
+ `${model} does not support a terminal assistant/model prefill; end the request with a user or tool message.`
3362
+ );
3363
+ }
3364
+ }
3365
+ if (!isGemini3x(model)) return;
3366
+ const replayedNativeCallIds = /* @__PURE__ */ new Set();
3367
+ for (const message of messages) {
3368
+ if (message.role !== "assistant" || !Array.isArray(message.providerMetadata?.geminiParts)) {
3369
+ continue;
3370
+ }
3371
+ for (const part of message.providerMetadata.geminiParts) {
3372
+ const id = part !== null && typeof part === "object" ? part.functionCall?.id : void 0;
3373
+ if (!id) continue;
3374
+ if (replayedNativeCallIds.has(id)) {
3375
+ throw new Error(
3376
+ `${model} native functionCall ids must be globally unique across a replay.`
3377
+ );
3378
+ }
3379
+ replayedNativeCallIds.add(id);
3380
+ }
3381
+ }
3382
+ const consumedToolMessages = /* @__PURE__ */ new Set();
3383
+ for (const [assistantIndex, message] of messages.entries()) {
3384
+ if (message.role !== "assistant" || !message.tool_calls?.length) continue;
3385
+ const rawParts = message.providerMetadata?.geminiParts;
3386
+ if (!Array.isArray(rawParts)) {
3387
+ throw new Error(
3388
+ `${model} tool continuations require providerMetadata.geminiParts with thought signatures.`
3389
+ );
3390
+ }
3391
+ const nativeCalls = rawParts.flatMap((part) => {
3392
+ const functionCall = part !== null && typeof part === "object" ? part.functionCall : void 0;
3393
+ return functionCall?.id ? [{ id: functionCall.id, name: functionCall.name, part }] : [];
3394
+ });
3395
+ const expectedCalls = new Map(
3396
+ message.tool_calls.map((call) => [call.id, call.function.name])
3397
+ );
3398
+ if (expectedCalls.size !== message.tool_calls.length || nativeCalls.length !== message.tool_calls.length || new Set(nativeCalls.map((call) => call.id)).size !== nativeCalls.length || typeof nativeCalls[0]?.part.thoughtSignature !== "string" || nativeCalls[0].part.thoughtSignature.length === 0 || nativeCalls.some((call) => expectedCalls.get(call.id) !== call.name)) {
3399
+ throw new Error(
3400
+ `${model} tool continuations require exact native functionCall id/name and a thoughtSignature on the first function call.`
3401
+ );
3402
+ }
3403
+ const seenResponses = /* @__PURE__ */ new Set();
3404
+ let toolIndex = assistantIndex + 1;
3405
+ while (messages[toolIndex]?.role === "tool") {
3406
+ const toolMessage = messages[toolIndex];
3407
+ const toolCallId = toolMessage.tool_call_id;
3408
+ if (!toolCallId || !expectedCalls.has(toolCallId) || seenResponses.has(toolCallId)) {
3409
+ throw new Error(
3410
+ `${model} functionResponse requires exactly one matching native functionCall id and name.`
3411
+ );
3412
+ }
3413
+ seenResponses.add(toolCallId);
3414
+ consumedToolMessages.add(toolIndex);
3415
+ toolIndex++;
3416
+ }
3417
+ if (seenResponses.size !== expectedCalls.size) {
3418
+ throw new Error(
3419
+ `${model} tool continuations require one contiguous functionResponse for every native functionCall.`
3420
+ );
3421
+ }
3422
+ }
3423
+ for (const [index, message] of messages.entries()) {
3424
+ if (message.role === "tool" && !consumedToolMessages.has(index)) {
3425
+ throw new Error(
3426
+ `${model} functionResponse requires exactly one matching native functionCall id and name.`
3427
+ );
3428
+ }
3429
+ }
3430
+ }
3431
+ isTextOnlyClientRequest(body) {
3432
+ if (!Array.isArray(body.contents) || body.cachedContent !== void 0) return false;
3433
+ if (body.contents.some(
3434
+ (content) => content === null || typeof content !== "object" || !Array.isArray(content.parts) || content.parts.some((part) => this.isNonTextGeminiPart(part))
3435
+ )) {
3436
+ return false;
3437
+ }
3438
+ if (body.tools !== void 0 && (!Array.isArray(body.tools) || body.tools.some(
3439
+ (tool2) => tool2 === null || typeof tool2 !== "object" || !Array.isArray(tool2.functionDeclarations) || Object.keys(tool2).some((key) => key !== "functionDeclarations")
3440
+ ))) {
3441
+ return false;
3442
+ }
3443
+ const modalities = body.generationConfig?.responseModalities;
3444
+ return modalities === void 0 || modalities === "TEXT" || Array.isArray(modalities) && modalities.length === 1 && modalities[0] === "TEXT";
3445
+ }
3446
+ isNonTextGeminiPart(part) {
3447
+ return part === null || typeof part !== "object" || ["inlineData", "fileData", "executableCode", "codeExecutionResult"].some((key) => key in part);
3448
+ }
2477
3449
  buildRequestBody(messages, options) {
3450
+ const requestModel = this.requestModel(options);
2478
3451
  const systemMessages = messages.filter((m) => m.role === "system");
2479
3452
  const nonSystemMessages = messages.filter((m) => m.role !== "system");
2480
3453
  const systemText = systemMessages.map((m) => m.content).join("\n\n");
@@ -2492,7 +3465,7 @@ var GeminiProvider = class {
2492
3465
  ];
2493
3466
  }
2494
3467
  const generationConfig = {};
2495
- if (options.temperature !== void 0) {
3468
+ if (options.temperature !== void 0 && !GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE.has(requestModel)) {
2496
3469
  generationConfig.temperature = options.temperature;
2497
3470
  }
2498
3471
  if (options.maxTokens !== void 0) {
@@ -2521,18 +3494,18 @@ var GeminiProvider = class {
2521
3494
  hasBudgetOverride
2522
3495
  } = resolveThinkingOptions(options);
2523
3496
  if (thinkingDisabled) {
2524
- if (isGemini3x(options.model)) {
3497
+ if (isGemini3x(requestModel)) {
2525
3498
  if (effort === "none") {
2526
- warnGemini3xEffortNone(options.model);
3499
+ warnGemini3xEffortNone(requestModel);
2527
3500
  }
2528
- generationConfig.thinkingConfig = { thinkingLevel: minThinkingLevel(options.model) };
3501
+ generationConfig.thinkingConfig = { thinkingLevel: minThinkingLevel(requestModel) };
2529
3502
  } else {
2530
3503
  generationConfig.thinkingConfig = { thinkingBudget: 0 };
2531
3504
  }
2532
3505
  if (!body.generationConfig) body.generationConfig = generationConfig;
2533
3506
  } else if (hasBudgetOverride) {
2534
3507
  const config = {};
2535
- if (isGemini3x(options.model)) {
3508
+ if (isGemini3x(requestModel)) {
2536
3509
  config.thinkingLevel = budgetToThinkingLevel(thinkingBudget);
2537
3510
  } else {
2538
3511
  config.thinkingBudget = thinkingBudget;
@@ -2542,10 +3515,10 @@ var GeminiProvider = class {
2542
3515
  if (!body.generationConfig) body.generationConfig = generationConfig;
2543
3516
  } else if (activeEffort) {
2544
3517
  const config = {};
2545
- if (isGemini3x(options.model)) {
3518
+ if (isGemini3x(requestModel)) {
2546
3519
  config.thinkingLevel = THINKING_LEVELS[activeEffort] ?? "medium";
2547
3520
  } else {
2548
- if (activeEffort === "max" && options.model.startsWith("gemini-2.5-pro")) {
3521
+ if (activeEffort === "max" && requestModel === "gemini-2.5-pro") {
2549
3522
  config.thinkingBudget = 32768;
2550
3523
  } else {
2551
3524
  config.thinkingBudget = THINKING_BUDGETS2[activeEffort] ?? 5e3;
@@ -2562,7 +3535,9 @@ var GeminiProvider = class {
2562
3535
  body.toolConfig = { functionCallingConfig: this.mapToolChoice(options.toolChoice) };
2563
3536
  }
2564
3537
  if (options.providerOptions) {
2565
- Object.assign(body, options.providerOptions);
3538
+ const bodyOptions = { ...options.providerOptions };
3539
+ delete bodyOptions.model;
3540
+ Object.assign(body, bodyOptions);
2566
3541
  }
2567
3542
  return body;
2568
3543
  }
@@ -2580,6 +3555,7 @@ var GeminiProvider = class {
2580
3555
  mapMessages(messages) {
2581
3556
  const toolCallIdToName = /* @__PURE__ */ new Map();
2582
3557
  const geminiNativeIds = /* @__PURE__ */ new Set();
3558
+ const geminiNativeNames = /* @__PURE__ */ new Map();
2583
3559
  for (const msg of messages) {
2584
3560
  if (msg.role === "assistant" && msg.tool_calls) {
2585
3561
  for (const tc of msg.tool_calls) {
@@ -2590,7 +3566,10 @@ var GeminiProvider = class {
2590
3566
  const rawParts = msg.providerMetadata?.geminiParts;
2591
3567
  if (rawParts) {
2592
3568
  for (const p of rawParts) {
2593
- if (p.functionCall?.id) geminiNativeIds.add(p.functionCall.id);
3569
+ if (p.functionCall?.id) {
3570
+ geminiNativeIds.add(p.functionCall.id);
3571
+ geminiNativeNames.set(p.functionCall.id, p.functionCall.name);
3572
+ }
2594
3573
  }
2595
3574
  }
2596
3575
  }
@@ -2627,7 +3606,7 @@ var GeminiProvider = class {
2627
3606
  }
2628
3607
  }
2629
3608
  } else if (msg.role === "tool") {
2630
- const functionName = toolCallIdToName.get(msg.tool_call_id) ?? "unknown";
3609
+ const functionName = (msg.tool_call_id ? geminiNativeNames.get(msg.tool_call_id) : void 0) ?? toolCallIdToName.get(msg.tool_call_id) ?? "unknown";
2631
3610
  const functionResponse = {
2632
3611
  name: functionName,
2633
3612
  response: parseGeminiFunctionResponse(msg.content)
@@ -2692,7 +3671,7 @@ var GeminiProvider = class {
2692
3671
  // ---------------------------------------------------------------------------
2693
3672
  // Internal: response parsing
2694
3673
  // ---------------------------------------------------------------------------
2695
- parseResponse(json, model) {
3674
+ parseResponse(json, pricingContext) {
2696
3675
  const candidate = json.candidates?.[0];
2697
3676
  let content = "";
2698
3677
  let thinkingContent = "";
@@ -2715,35 +3694,46 @@ var GeminiProvider = class {
2715
3694
  }
2716
3695
  }
2717
3696
  }
2718
- const cachedTokens = json.usageMetadata?.cachedContentTokenCount;
2719
- const reasoningTokens = json.usageMetadata?.thoughtsTokenCount;
2720
- const usage = json.usageMetadata ? {
2721
- prompt_tokens: json.usageMetadata.promptTokenCount ?? 0,
2722
- completion_tokens: json.usageMetadata.candidatesTokenCount ?? 0,
2723
- total_tokens: json.usageMetadata.totalTokenCount ?? 0,
2724
- cached_tokens: cachedTokens && cachedTokens > 0 ? cachedTokens : void 0,
2725
- reasoning_tokens: reasoningTokens && reasoningTokens > 0 ? reasoningTokens : void 0
2726
- } : void 0;
2727
- const cost = usage ? estimateGeminiCost(model, usage.prompt_tokens, usage.completion_tokens, usage.cached_tokens) : void 0;
3697
+ if (!candidate) {
3698
+ throw new Error("Gemini response did not include a candidate.");
3699
+ }
3700
+ if (candidate.finishReason && !["STOP", "MAX_TOKENS"].includes(candidate.finishReason)) {
3701
+ throw new Error(`Gemini returned non-success finish reason: ${candidate.finishReason}`);
3702
+ }
3703
+ const normalized = json.usageMetadata ? normalizeGeminiUsage(json.usageMetadata) : void 0;
3704
+ const effectiveModel = typeof json.modelVersion === "string" ? json.modelVersion : pricingContext.model;
3705
+ const cost = normalized && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
3706
+ pricingContext,
3707
+ isDefinitiveStandardGeminiResponseTier(json.usageMetadata?.serviceTier),
3708
+ json.usageMetadata?.serviceTier !== void 0 && !isDefinitiveStandardGeminiResponseTier(json.usageMetadata.serviceTier)
3709
+ ) ? estimateGeminiCost(effectiveModel, normalized.pricingUsage) : void 0;
2728
3710
  const rawParts = candidate?.content?.parts;
2729
3711
  const providerMetadata = rawParts ? { geminiParts: rawParts } : void 0;
2730
3712
  return {
2731
3713
  content,
2732
3714
  thinking_content: thinkingContent || void 0,
2733
3715
  tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
2734
- usage,
3716
+ usage: normalized?.usage,
2735
3717
  cost,
2736
3718
  providerMetadata
2737
3719
  };
2738
3720
  }
3721
+ isTextOnlyGeminiResponse(candidate) {
3722
+ return candidate?.content?.parts.every((part) => !this.isNonTextGeminiPart(part)) ?? false;
3723
+ }
2739
3724
  // ---------------------------------------------------------------------------
2740
3725
  // Internal: SSE stream parsing
2741
3726
  // ---------------------------------------------------------------------------
2742
- async *parseSSEStream(body, model) {
3727
+ async *parseSSEStream(body, pricingContext) {
2743
3728
  const reader = body.getReader();
2744
3729
  const decoder = new TextDecoder();
2745
3730
  let buffer = "";
2746
- let usage;
3731
+ let normalizedUsage;
3732
+ let effectiveModel = pricingContext.model;
3733
+ let hasDefinitiveStandardResponseTier = false;
3734
+ let hasInvalidResponseTier = false;
3735
+ let responseIsTextOnly = true;
3736
+ let terminalFailure;
2747
3737
  const accumulatedParts = [];
2748
3738
  try {
2749
3739
  while (true) {
@@ -2763,20 +3753,21 @@ var GeminiProvider = class {
2763
3753
  } catch {
2764
3754
  continue;
2765
3755
  }
3756
+ if (typeof chunk.modelVersion === "string") effectiveModel = chunk.modelVersion;
2766
3757
  if (chunk.usageMetadata) {
2767
- const cached = chunk.usageMetadata.cachedContentTokenCount;
2768
- const reasoning = chunk.usageMetadata.thoughtsTokenCount;
2769
- usage = {
2770
- prompt_tokens: chunk.usageMetadata.promptTokenCount ?? 0,
2771
- completion_tokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
2772
- total_tokens: chunk.usageMetadata.totalTokenCount ?? 0,
2773
- cached_tokens: cached && cached > 0 ? cached : void 0,
2774
- reasoning_tokens: reasoning && reasoning > 0 ? reasoning : void 0
2775
- };
3758
+ normalizedUsage = normalizeGeminiUsage(chunk.usageMetadata);
3759
+ if (chunk.usageMetadata.serviceTier !== void 0) {
3760
+ if (isDefinitiveStandardGeminiResponseTier(chunk.usageMetadata.serviceTier)) {
3761
+ hasDefinitiveStandardResponseTier = true;
3762
+ } else {
3763
+ hasInvalidResponseTier = true;
3764
+ }
3765
+ }
2776
3766
  }
2777
3767
  const candidate = chunk.candidates?.[0];
2778
3768
  if (candidate?.content?.parts) {
2779
3769
  for (const part of candidate.content.parts) {
3770
+ if (this.isNonTextGeminiPart(part)) responseIsTextOnly = false;
2780
3771
  accumulatedParts.push(part);
2781
3772
  if (part.thought && part.text) {
2782
3773
  yield { type: "thinking_delta", content: part.text };
@@ -2792,18 +3783,23 @@ var GeminiProvider = class {
2792
3783
  }
2793
3784
  }
2794
3785
  }
3786
+ if (candidate?.finishReason && !["STOP", "MAX_TOKENS"].includes(candidate.finishReason)) {
3787
+ terminalFailure = candidate.finishReason;
3788
+ }
2795
3789
  }
2796
3790
  }
3791
+ if (terminalFailure) {
3792
+ throw new Error(`Gemini returned non-success finish reason: ${terminalFailure}`);
3793
+ }
2797
3794
  const providerMetadata = accumulatedParts.length > 0 ? { geminiParts: accumulatedParts } : void 0;
2798
3795
  yield {
2799
3796
  type: "done",
2800
- usage,
2801
- cost: usage ? estimateGeminiCost(
2802
- model,
2803
- usage.prompt_tokens,
2804
- usage.completion_tokens,
2805
- usage.cached_tokens
2806
- ) : void 0,
3797
+ usage: normalizedUsage?.usage,
3798
+ cost: normalizedUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
3799
+ pricingContext,
3800
+ hasDefinitiveStandardResponseTier,
3801
+ hasInvalidResponseTier
3802
+ ) ? estimateGeminiCost(effectiveModel, normalizedUsage.pricingUsage) : void 0,
2807
3803
  providerMetadata
2808
3804
  };
2809
3805
  } finally {
@@ -2820,8 +3816,6 @@ var OPENROUTER_PROFILE = {
2820
3816
  envApiKey: "OPENROUTER_API_KEY",
2821
3817
  envBaseUrl: "OPENROUTER_BASE_URL",
2822
3818
  pricing: { kind: "from-response" },
2823
- // Ask OpenRouter to report per-call cost in usage.cost.
2824
- requestDefaults: { usage: { include: true } },
2825
3819
  reasoning: {
2826
3820
  // OpenRouter's effort vocabulary is low|medium|high; xhigh/max clamp to high.
2827
3821
  emit: reasoningObjectEmit(
@@ -2844,7 +3838,7 @@ var AZURE_PROFILE = {
2844
3838
  envApiKey: "AZURE_OPENAI_API_KEY",
2845
3839
  envBaseUrl: "AZURE_OPENAI_BASE_URL",
2846
3840
  authHeader: "api-key",
2847
- pricing: { kind: "table", table: OPENAI_PRICING },
3841
+ pricing: { kind: "unknown" },
2848
3842
  reasoning: { emit: openaiReasoningEmit, capture: "none" },
2849
3843
  roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
2850
3844
  maxTokensField: "max_completion_tokens",
@@ -2852,29 +3846,100 @@ var AZURE_PROFILE = {
2852
3846
  };
2853
3847
 
2854
3848
  // src/providers/profiles/xai.ts
2855
- var acceptsReasoningEffort = (model) => model.startsWith("grok-3-mini");
2856
- var isReasoningModel = (model) => /^grok-(3-mini|4)/.test(model);
3849
+ var GROK_45 = /* @__PURE__ */ new Set(["grok-4.5", "grok-4.5-latest", "grok-build-latest"]);
3850
+ var GROK_43 = /* @__PURE__ */ new Set(["grok-4.3", "grok-4.3-latest", "grok-latest"]);
3851
+ var GROK_420_REASONING = /* @__PURE__ */ new Set([
3852
+ "grok-4.20-0309-reasoning",
3853
+ "grok-4.20-reasoning-latest",
3854
+ "grok-4.20",
3855
+ "grok-4.20-reasoning",
3856
+ "grok-4.20-0309",
3857
+ "grok-4.20-beta-0309-reasoning",
3858
+ "grok-4.20-beta",
3859
+ "grok-4.20-beta-0309",
3860
+ "grok-4.20-beta-latest",
3861
+ "grok-4.20-beta-latest-reasoning",
3862
+ "grok-4.20-beta-reasoning",
3863
+ "grok-4.20-experimental-beta-0304-reasoning",
3864
+ "grok-4.20-experimental-beta-0304",
3865
+ "grok-4.20-experimental-beta-reasoning-latest",
3866
+ "grok-4.20-experimental-beta-latest",
3867
+ "grok-4.20-reasoning-gv2"
3868
+ ]);
3869
+ var GROK_420_NON_REASONING = /* @__PURE__ */ new Set([
3870
+ "grok-4.20-0309-non-reasoning",
3871
+ "grok-4.20-non-reasoning",
3872
+ "grok-4.20-non-reasoning-latest",
3873
+ "grok-4.20-beta-non-reasoning",
3874
+ "grok-4.20-beta-latest-non-reasoning",
3875
+ "grok-4.20-experimental-beta-0304-non-reasoning",
3876
+ "grok-4.20-experimental-beta-non-reasoning-latest",
3877
+ "grok-4.20-beta-0309-non-reasoning",
3878
+ "grok-4.20-non-reasoning-gv2"
3879
+ ]);
3880
+ var warnedIrreducible45 = /* @__PURE__ */ new Set();
3881
+ var warnedEffortClamp = /* @__PURE__ */ new Set();
3882
+ function warnIrreducible45(model) {
3883
+ if (warnedIrreducible45.has(model)) return;
3884
+ warnedIrreducible45.add(model);
3885
+ console.warn(
3886
+ `[axl] effort: 'none' on xAI ${model} maps to 'low'; Grok 4.5 cannot disable reasoning.`
3887
+ );
3888
+ }
3889
+ function warnEffortClamp(model, effort) {
3890
+ const key = `${model}:${effort}`;
3891
+ if (warnedEffortClamp.has(key)) return;
3892
+ warnedEffortClamp.add(key);
3893
+ console.warn(
3894
+ `[axl] effort: '${effort}' on xAI ${model} maps to 'high'; this model supports at most 'high'.`
3895
+ );
3896
+ }
3897
+ var isReasoningChatModel = (model) => GROK_45.has(model) || GROK_43.has(model) || GROK_420_REASONING.has(model);
2857
3898
  var XAI_PROFILE = {
2858
3899
  name: "xai",
2859
3900
  label: "xAI",
2860
3901
  defaultBaseUrl: "https://api.x.ai/v1",
2861
3902
  envApiKey: "XAI_API_KEY",
2862
3903
  envBaseUrl: "XAI_BASE_URL",
2863
- pricing: { kind: "unknown" },
3904
+ pricing: { kind: "from-response" },
2864
3905
  reasoning: {
2865
3906
  emit: reasoningEffortEmit((resolved, model) => {
2866
- if (!acceptsReasoningEffort(model)) return void 0;
2867
- if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2868
- return resolved.activeEffort === "low" ? "low" : "high";
3907
+ if (GROK_45.has(model)) {
3908
+ if (resolved.thinkingDisabled) {
3909
+ warnIrreducible45(model);
3910
+ return "low";
3911
+ }
3912
+ if (!resolved.activeEffort) return void 0;
3913
+ if (resolved.activeEffort === "xhigh" || resolved.activeEffort === "max") {
3914
+ warnEffortClamp(model, resolved.activeEffort);
3915
+ return "high";
3916
+ }
3917
+ return resolved.activeEffort;
3918
+ }
3919
+ if (GROK_43.has(model)) {
3920
+ if (resolved.thinkingDisabled) return "none";
3921
+ if (!resolved.activeEffort) return void 0;
3922
+ if (resolved.activeEffort === "xhigh" || resolved.activeEffort === "max") {
3923
+ warnEffortClamp(model, resolved.activeEffort);
3924
+ return "high";
3925
+ }
3926
+ return resolved.activeEffort;
3927
+ }
3928
+ if (GROK_420_NON_REASONING.has(model)) return void 0;
3929
+ return void 0;
2869
3930
  }),
2870
3931
  capture: "reasoning"
2871
3932
  },
2872
3933
  capabilities: {
2873
- forbiddenParams: (model) => isReasoningModel(model) ? ["stop", "presence_penalty", "frequency_penalty"] : []
3934
+ forbiddenParams: (model) => isReasoningChatModel(model) ? ["stop", "presence_penalty", "frequency_penalty"] : []
2874
3935
  }
2875
3936
  };
2876
3937
 
2877
3938
  // src/providers/profiles/deepseek.ts
3939
+ var DEEPSEEK_PRICING = {
3940
+ "deepseek-v4-flash": [14e-8, 28e-8, 0.02],
3941
+ "deepseek-v4-pro": [435e-9, 87e-8, 1 / 120]
3942
+ };
2878
3943
  var isReasoner = (model) => /reasoner|v4/i.test(model);
2879
3944
  var deepseekEmit = (_body, _resolved, model) => ({
2880
3945
  stripTemperature: isReasoner(model)
@@ -2885,7 +3950,7 @@ var DEEPSEEK_PROFILE = {
2885
3950
  defaultBaseUrl: "https://api.deepseek.com/v1",
2886
3951
  envApiKey: "DEEPSEEK_API_KEY",
2887
3952
  envBaseUrl: "DEEPSEEK_BASE_URL",
2888
- pricing: { kind: "unknown" },
3953
+ pricing: { kind: "table", table: DEEPSEEK_PRICING, match: "exact" },
2889
3954
  reasoning: {
2890
3955
  emit: deepseekEmit,
2891
3956
  capture: "reasoning_content",
@@ -2896,27 +3961,56 @@ var DEEPSEEK_PROFILE = {
2896
3961
  forbiddenParams: (model) => isReasoner(model) ? ["top_p", "presence_penalty", "frequency_penalty"] : []
2897
3962
  }
2898
3963
  };
3964
+ attachBuiltinTablePricingPolicy(DEEPSEEK_PROFILE, {
3965
+ canonicalBaseUrl: "https://api.deepseek.com/v1",
3966
+ requireCacheSplit: true
3967
+ });
2899
3968
 
2900
3969
  // src/providers/profiles/mistral.ts
2901
- var acceptsReasoningEffort2 = (model) => /^mistral-(small|medium)/i.test(model);
3970
+ var MISTRAL_PRICING = {
3971
+ "mistral-medium-latest": [15e-7, 75e-7, 0.1],
3972
+ "mistral-small-latest": [15e-8, 6e-7, 0.1],
3973
+ "mistral-large-latest": [5e-7, 15e-7, 0.1],
3974
+ "devstral-medium-latest": [4e-7, 2e-6, 0.1],
3975
+ "devstral-small-latest": [1e-7, 3e-7, 0.1],
3976
+ "codestral-latest": [3e-7, 9e-7, 0.1],
3977
+ "magistral-medium-latest": [2e-6, 5e-6, 0.1],
3978
+ "magistral-small-latest": [5e-7, 15e-7, 0.1],
3979
+ "ministral-3b-latest": [1e-7, 1e-7, 0.1],
3980
+ "ministral-8b-latest": [15e-8, 15e-8, 0.1],
3981
+ "ministral-14b-latest": [2e-7, 2e-7, 0.1],
3982
+ "open-mistral-nemo": [15e-8, 15e-8, 0.1],
3983
+ "open-mixtral-8x7b": [7e-7, 7e-7, 0.1],
3984
+ "open-mixtral-8x22b": [2e-6, 6e-6, 0.1]
3985
+ };
3986
+ var acceptsReasoningEffort = (model) => /^mistral-(small|medium)/i.test(model);
2902
3987
  var MISTRAL_PROFILE = {
2903
3988
  name: "mistral",
2904
3989
  label: "Mistral",
2905
3990
  defaultBaseUrl: "https://api.mistral.ai/v1",
2906
3991
  envApiKey: "MISTRAL_API_KEY",
2907
3992
  envBaseUrl: "MISTRAL_BASE_URL",
2908
- pricing: { kind: "unknown" },
3993
+ pricing: { kind: "table", table: MISTRAL_PRICING, match: "exact" },
2909
3994
  reasoning: {
2910
3995
  emit: reasoningEffortEmit((resolved, model) => {
2911
- if (!acceptsReasoningEffort2(model)) return void 0;
3996
+ if (!acceptsReasoningEffort(model)) return void 0;
2912
3997
  if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2913
3998
  return "high";
2914
3999
  }),
2915
4000
  capture: "reasoning_content"
2916
4001
  }
2917
4002
  };
4003
+ attachBuiltinTablePricingPolicy(MISTRAL_PROFILE, {
4004
+ canonicalBaseUrl: "https://api.mistral.ai/v1"
4005
+ });
2918
4006
 
2919
4007
  // src/providers/profiles/groq.ts
4008
+ var GROQ_PRICING = {
4009
+ "llama-3.1-8b-instant": [5e-8, 8e-8, 1],
4010
+ "llama-3.3-70b-versatile": [59e-8, 79e-8, 1],
4011
+ "openai/gpt-oss-120b": [15e-8, 6e-7, 0.5],
4012
+ "openai/gpt-oss-20b": [75e-9, 3e-7, 0.5]
4013
+ };
2920
4014
  var isGptOss = (model) => /gpt-oss/i.test(model);
2921
4015
  var GROQ_PROFILE = {
2922
4016
  name: "groq",
@@ -2924,7 +4018,11 @@ var GROQ_PROFILE = {
2924
4018
  defaultBaseUrl: "https://api.groq.com/openai/v1",
2925
4019
  envApiKey: "GROQ_API_KEY",
2926
4020
  envBaseUrl: "GROQ_BASE_URL",
2927
- pricing: { kind: "unknown" },
4021
+ pricing: {
4022
+ kind: "table",
4023
+ table: GROQ_PRICING,
4024
+ match: "exact"
4025
+ },
2928
4026
  reasoning: {
2929
4027
  emit: reasoningEffortEmit((resolved, model) => {
2930
4028
  if (!isGptOss(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
@@ -2945,6 +4043,21 @@ var GROQ_PROFILE = {
2945
4043
  supportsJsonSchema: isGptOss
2946
4044
  }
2947
4045
  };
4046
+ attachBuiltinTablePricingPolicy(GROQ_PROFILE, {
4047
+ canonicalBaseUrl: "https://api.groq.com/openai/v1",
4048
+ supportedServiceTiers: ["on_demand", "flex"],
4049
+ functionToolsOnly: true,
4050
+ chargedRequestModifiers: [
4051
+ "browser_search",
4052
+ "code_interpreter",
4053
+ "web_search",
4054
+ "visit_website",
4055
+ "wolfram_alpha",
4056
+ "remote_mcp",
4057
+ "mcp",
4058
+ "compound_custom"
4059
+ ]
4060
+ });
2948
4061
 
2949
4062
  // src/providers/profiles/bedrock.ts
2950
4063
  var isGptOss2 = (model) => /gpt-oss/i.test(model);
@@ -3016,6 +4129,7 @@ var builtinFactories = {
3016
4129
  return new OpenAIProvider({
3017
4130
  apiKey: opts.apiKey,
3018
4131
  baseUrl: opts.baseUrl,
4132
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
3019
4133
  rateLimit: opts.rateLimit
3020
4134
  });
3021
4135
  },
@@ -3028,6 +4142,7 @@ var builtinFactories = {
3028
4142
  return new OpenAIResponsesProvider({
3029
4143
  apiKey: opts.apiKey,
3030
4144
  baseUrl: opts.baseUrl,
4145
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
3031
4146
  rateLimit: opts.rateLimit
3032
4147
  });
3033
4148
  },
@@ -3036,6 +4151,7 @@ var builtinFactories = {
3036
4151
  return new AnthropicProvider({
3037
4152
  apiKey: opts.apiKey,
3038
4153
  baseUrl: opts.baseUrl,
4154
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
3039
4155
  rateLimit: opts.rateLimit
3040
4156
  });
3041
4157
  },
@@ -3044,6 +4160,7 @@ var builtinFactories = {
3044
4160
  return new GeminiProvider({
3045
4161
  apiKey: opts.apiKey,
3046
4162
  baseUrl: opts.baseUrl,
4163
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
3047
4164
  rateLimit: opts.rateLimit
3048
4165
  });
3049
4166
  }
@@ -3055,6 +4172,7 @@ function presetFactory(profile) {
3055
4172
  profile,
3056
4173
  apiKey: opts.apiKey,
3057
4174
  baseUrl: opts.baseUrl,
4175
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
3058
4176
  authHeader: opts.authHeader,
3059
4177
  rateLimit: opts.rateLimit
3060
4178
  });
@@ -3759,7 +4877,7 @@ var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
3759
4877
  function hasPositiveTokens(event) {
3760
4878
  const t = event.tokens;
3761
4879
  if (!t) return false;
3762
- return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) > 0;
4880
+ return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cached ?? 0) + (t.cacheWrite ?? 0) > 0;
3763
4881
  }
3764
4882
  function isUsableCost(cost) {
3765
4883
  return typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
@@ -6372,6 +7490,11 @@ var WorkflowContext = class _WorkflowContext {
6372
7490
  "axl.agent.cached_tokens",
6373
7491
  usageCapture.value.cached_tokens
6374
7492
  );
7493
+ if (usageCapture.value.cache_write_tokens)
7494
+ span.setAttribute(
7495
+ "axl.agent.cache_write_tokens",
7496
+ usageCapture.value.cache_write_tokens
7497
+ );
6375
7498
  }
6376
7499
  return r;
6377
7500
  }
@@ -6817,7 +7940,9 @@ ${appendedSchemaText}`;
6817
7940
  tokens: response.usage ? {
6818
7941
  input: response.usage.prompt_tokens,
6819
7942
  output: response.usage.completion_tokens,
6820
- reasoning: response.usage.reasoning_tokens
7943
+ reasoning: response.usage.reasoning_tokens,
7944
+ cached: response.usage.cached_tokens,
7945
+ cacheWrite: response.usage.cache_write_tokens
6821
7946
  } : void 0,
6822
7947
  duration: Date.now() - turnStart,
6823
7948
  data: {
@@ -10058,6 +11183,14 @@ var HttpMcpClient = class {
10058
11183
  this.config = config;
10059
11184
  this.name = config.name;
10060
11185
  this.baseUrl = (config.uri ?? "").replace(/\/$/, "");
11186
+ if (this.baseUrl) {
11187
+ assertSafeHttpEndpoint(
11188
+ this.baseUrl,
11189
+ `MCP server "${this.name}"`,
11190
+ config.dangerouslyAllowInsecureHttp,
11191
+ "tool arguments and results"
11192
+ );
11193
+ }
10061
11194
  }
10062
11195
  name;
10063
11196
  tools = [];
@@ -10087,7 +11220,9 @@ var HttpMcpClient = class {
10087
11220
  method: "POST",
10088
11221
  headers: { "Content-Type": "application/json" },
10089
11222
  body: JSON.stringify(request),
10090
- signal: AbortSignal.timeout(timeoutMs)
11223
+ signal: AbortSignal.timeout(timeoutMs),
11224
+ // Tool calls can be non-idempotent: never forward or retry their body.
11225
+ redirect: "manual"
10091
11226
  });
10092
11227
  if (!res.ok) {
10093
11228
  throw new Error(`MCP HTTP error (${res.status}): ${await res.text()}`);
@@ -12879,6 +14014,11 @@ var OpenAIEmbedder = class {
12879
14014
  }
12880
14015
  this.model = options.model ?? "text-embedding-3-small";
12881
14016
  this.baseUrl = options.baseUrl ?? "https://api.openai.com";
14017
+ assertSafeProviderBaseUrl(
14018
+ this.baseUrl,
14019
+ "OpenAI embedder",
14020
+ options.dangerouslyAllowInsecureHttp
14021
+ );
12882
14022
  this.dimensions = options.dimensions ?? 1536;
12883
14023
  }
12884
14024
  async embed(texts, signal) {