@absolutejs/ai 0.1.2 → 0.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
+ ## 0.2.0 — 2026-09-16
10
+
11
+ ### Added
12
+
13
+ - **Expose each tool-loop turn response metadata so consumers can price the actual served processing tier** (`streamAIWithTools`, `StreamAIWithToolsEvent`)
14
+ - **Route exact model IDs to the same provider for token counting, capacity checks, preparation and generation** (`createAIProviderRouter`)
15
+
16
+ ## 0.1.3 — 2026-09-16
17
+
18
+ ### Fixed
19
+
20
+ - **Exclude OpenAI cache-write tokens from ordinary input so cache-aware billing never charges the same tokens twice** (`openai`, `openaiResponses`)
21
+
9
22
  ## 0.1.2 — 2026-09-16
10
23
 
11
24
  ### Fixed
package/README.md CHANGED
@@ -496,3 +496,21 @@ its explicit `false` remains an opt-out unless `contextPolicy` is supplied.
496
496
  Applications should surface typed failures with saved-input retry UI. These
497
497
  changes do not guarantee detection of every provider-side accounting discrepancy
498
498
  or provide exactly-once external execution after a process crash.
499
+
500
+ ### Models from different providers
501
+
502
+ `createAIProviderRouter` routes exact model IDs for generation, token counting and
503
+ capacity checks. It snapshots the route map and fails explicitly for unknown
504
+ models or a route without counting support. It does not choose a model or add a
505
+ fallback. For example, preparation and final answering can use different vendors:
506
+
507
+ ```ts
508
+ const provider = createAIProviderRouter({
509
+ "claude-haiku-4-5-20251001": anthropicProvider,
510
+ "gpt-5.6-luna": openaiResponsesProvider,
511
+ });
512
+ ```
513
+
514
+ `streamAIWithTools` includes each completed turn's provider response `metadata`
515
+ on its `turn` event. Price that turn using the returned `serviceTier`, when
516
+ available, rather than assuming that the requested processing tier was served.
package/changelog.json CHANGED
@@ -2,6 +2,41 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/ai",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "kind": "added",
9
+ "summary": "Expose each tool-loop turn response metadata so consumers can price the actual served processing tier",
10
+ "symbols": [
11
+ "streamAIWithTools",
12
+ "StreamAIWithToolsEvent"
13
+ ]
14
+ },
15
+ {
16
+ "kind": "added",
17
+ "summary": "Route exact model IDs to the same provider for token counting, capacity checks, preparation and generation",
18
+ "symbols": [
19
+ "createAIProviderRouter"
20
+ ]
21
+ }
22
+ ],
23
+ "date": "2026-09-16",
24
+ "version": "0.2.0"
25
+ },
26
+ {
27
+ "changes": [
28
+ {
29
+ "kind": "fixed",
30
+ "summary": "Exclude OpenAI cache-write tokens from ordinary input so cache-aware billing never charges the same tokens twice",
31
+ "symbols": [
32
+ "openai",
33
+ "openaiResponses"
34
+ ]
35
+ }
36
+ ],
37
+ "date": "2026-09-16",
38
+ "version": "0.1.3"
39
+ },
5
40
  {
6
41
  "changes": [
7
42
  {
package/dist/ai/index.js CHANGED
@@ -877,11 +877,12 @@ var extractUsage = (response) => {
877
877
  const outputDetails = isRecord(usage.output_tokens_details) ? usage.output_tokens_details : undefined;
878
878
  const inputDetails = isRecord(usage.input_tokens_details) ? usage.input_tokens_details : undefined;
879
879
  const costDetails = isRecord(usage.cost_details) ? usage.cost_details : undefined;
880
+ const written = inputDetails && typeof inputDetails.cache_write_tokens === "number" ? inputDetails.cache_write_tokens : 0;
880
881
  const normalized = {
881
882
  cacheReadInputTokens: cached,
882
- cacheWriteInputTokens: inputDetails && typeof inputDetails.cache_write_tokens === "number" ? inputDetails.cache_write_tokens : undefined,
883
+ cacheWriteInputTokens: written || undefined,
883
884
  costCredits: typeof usage.cost === "number" ? usage.cost : undefined,
884
- inputTokens: Math.max(0, input - cached),
885
+ inputTokens: Math.max(0, input - cached - written),
885
886
  outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0,
886
887
  reasoningTokens: outputDetails && typeof outputDetails.reasoning_tokens === "number" ? outputDetails.reasoning_tokens : undefined,
887
888
  upstreamInferenceCostCredits: costDetails && typeof costDetails.upstream_inference_cost === "number" ? costDetails.upstream_inference_cost : undefined
@@ -1410,11 +1411,12 @@ var flushPendingToolCalls = function* (pendingToolCalls) {
1410
1411
  var extractUsage2 = (parsedUsage) => {
1411
1412
  const prompt = parsedUsage.prompt_tokens ?? 0;
1412
1413
  const cached = parsedUsage.cached_tokens ?? 0;
1414
+ const written = parsedUsage.cache_write_tokens ?? 0;
1413
1415
  return {
1414
1416
  cacheReadInputTokens: cached,
1415
- cacheWriteInputTokens: parsedUsage.cache_write_tokens || undefined,
1417
+ cacheWriteInputTokens: written || undefined,
1416
1418
  costCredits: parsedUsage.cost,
1417
- inputTokens: Math.max(0, prompt - cached),
1419
+ inputTokens: Math.max(0, prompt - cached - written),
1418
1420
  outputTokens: parsedUsage.completion_tokens ?? 0,
1419
1421
  reasoningTokens: parsedUsage.reasoning_tokens || undefined,
1420
1422
  upstreamInferenceCostCredits: parsedUsage.upstream_inference_cost || undefined
@@ -5830,6 +5832,7 @@ var streamAIWithTools = async function* (options) {
5830
5832
  let thinking = null;
5831
5833
  let stopReason;
5832
5834
  let turnUsage;
5835
+ let turnMetadata;
5833
5836
  let completed = false;
5834
5837
  for await (const chunk of stream) {
5835
5838
  if (base.signal?.aborted)
@@ -5875,19 +5878,30 @@ var streamAIWithTools = async function* (options) {
5875
5878
  thinking = flushThinking3(blocks, thinking);
5876
5879
  stopReason = chunk.stopReason;
5877
5880
  turnUsage = chunk.usage;
5881
+ turnMetadata = chunk.metadata;
5878
5882
  completed = true;
5879
5883
  }
5880
5884
  }
5881
5885
  thinking = flushThinking3(blocks, thinking);
5882
5886
  if (policy !== false && !completed)
5883
5887
  throw new AIInputError("capacity_unavailable", "The model response was interrupted before completion. Preserve the original input for retry.");
5884
- return { blocks, pending, stopReason, usage: turnUsage };
5888
+ return {
5889
+ blocks,
5890
+ pending,
5891
+ stopReason,
5892
+ usage: turnUsage,
5893
+ metadata: turnMetadata
5894
+ };
5885
5895
  };
5886
5896
  while (turn < maxTurns) {
5887
5897
  turn += 1;
5888
5898
  const outcome = yield* streamOneTurn();
5889
5899
  usage = mergeUsage(usage, outcome.usage);
5890
- yield { type: "turn", usage: outcome.usage };
5900
+ yield {
5901
+ type: "turn",
5902
+ usage: outcome.usage,
5903
+ ...outcome.metadata ? { metadata: outcome.metadata } : {}
5904
+ };
5891
5905
  const { blocks, pending } = outcome;
5892
5906
  allToolCalls.push(...pending);
5893
5907
  if (outcome.stopReason === "pause_turn" && turn < maxTurns && base.signal?.aborted !== true) {
@@ -5937,7 +5951,11 @@ var streamAIWithTools = async function* (options) {
5937
5951
  turn += 1;
5938
5952
  const final = yield* streamOneTurn("none");
5939
5953
  usage = mergeUsage(usage, final.usage);
5940
- yield { type: "turn", usage: final.usage };
5954
+ yield {
5955
+ type: "turn",
5956
+ usage: final.usage,
5957
+ ...final.metadata ? { metadata: final.metadata } : {}
5958
+ };
5941
5959
  allToolCalls.push(...final.pending);
5942
5960
  break;
5943
5961
  }
@@ -8595,6 +8613,30 @@ var createAIStoredToolResultRecovery = (toolNames) => async ({ params }) => {
8595
8613
  })
8596
8614
  }));
8597
8615
  };
8616
+ // src/ai/providerRouter.ts
8617
+ var createAIProviderRouter = (routes) => {
8618
+ const providers = new Map(Object.entries(routes));
8619
+ const resolve = (params) => {
8620
+ params.signal?.throwIfAborted();
8621
+ const provider = providers.get(params.model);
8622
+ if (!provider)
8623
+ throw new AIInputError("capacity_unavailable", `No provider route for model ${params.model}.`);
8624
+ return provider;
8625
+ };
8626
+ const capacity = (params) => {
8627
+ const result = resolve(params).inputCapacity;
8628
+ if (!result)
8629
+ throw new AIInputError("capacity_unavailable", `The provider for ${params.model} does not expose model capacity and token counting.`);
8630
+ return result;
8631
+ };
8632
+ return {
8633
+ inputCapacity: {
8634
+ getLimits: async (params) => capacity(params).getLimits(params),
8635
+ countTokens: async (params) => capacity(params).countTokens(params)
8636
+ },
8637
+ stream: (params) => resolve(params).stream(params)
8638
+ };
8639
+ };
8598
8640
  export {
8599
8641
  AIInputError,
8600
8642
  BUILTIN_UI_CARDS,
@@ -8628,6 +8670,7 @@ export {
8628
8670
  configureProviderResilience,
8629
8671
  confirmCard,
8630
8672
  createAIConnection,
8673
+ createAIProviderRouter,
8631
8674
  createAIStoredToolResultRecovery,
8632
8675
  createAIStream,
8633
8676
  createAITextSource,
@@ -8704,5 +8747,5 @@ export {
8704
8747
  xai
8705
8748
  };
8706
8749
 
8707
- //# debugId=5DA989DD67F99C1B64756E2164756E21
8750
+ //# debugId=0CA4631EC8525EB564756E2164756E21
8708
8751
  //# sourceMappingURL=index.js.map