@tanstack/ai 0.23.1 → 0.25.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.
Files changed (74) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +3 -1
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +33 -9
  4. package/dist/esm/activities/chat/index.js +19 -9
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/messages.js +2 -1
  7. package/dist/esm/activities/chat/messages.js.map +1 -1
  8. package/dist/esm/activities/chat/middleware/compose.d.ts +14 -14
  9. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  10. package/dist/esm/activities/chat/middleware/types.d.ts +21 -21
  11. package/dist/esm/activities/chat/runtime-context-types.d.ts +43 -0
  12. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  13. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  14. package/dist/esm/activities/chat/stream/processor.d.ts +1 -0
  15. package/dist/esm/activities/chat/stream/processor.js +35 -12
  16. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  17. package/dist/esm/activities/chat/tools/tool-calls.d.ts +15 -5
  18. package/dist/esm/activities/chat/tools/tool-calls.js +59 -19
  19. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  20. package/dist/esm/activities/chat/tools/tool-definition.d.ts +12 -8
  21. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  22. package/dist/esm/activities/error-payload.d.ts +26 -0
  23. package/dist/esm/activities/error-payload.js +12 -1
  24. package/dist/esm/activities/error-payload.js.map +1 -1
  25. package/dist/esm/activities/generateAudio/index.js +9 -0
  26. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  27. package/dist/esm/activities/generateSpeech/index.js +9 -0
  28. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  29. package/dist/esm/adapter-internals.d.ts +1 -1
  30. package/dist/esm/adapter-internals.js +3 -2
  31. package/dist/esm/client.d.ts +1 -1
  32. package/dist/esm/client.js +3 -1
  33. package/dist/esm/client.js.map +1 -1
  34. package/dist/esm/index.d.ts +3 -1
  35. package/dist/esm/index.js +9 -1
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/tool-registry.d.ts +7 -7
  38. package/dist/esm/tool-registry.js +1 -1
  39. package/dist/esm/tool-registry.js.map +1 -1
  40. package/dist/esm/types.d.ts +56 -59
  41. package/dist/esm/utilities/ag-ui-wire.js +1 -1
  42. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  43. package/dist/esm/utilities/chat-params.d.ts +8 -3
  44. package/dist/esm/utilities/chat-params.js +6 -2
  45. package/dist/esm/utilities/chat-params.js.map +1 -1
  46. package/dist/esm/utilities/tool-result.d.ts +21 -0
  47. package/dist/esm/utilities/tool-result.js +37 -0
  48. package/dist/esm/utilities/tool-result.js.map +1 -0
  49. package/dist/esm/utilities/usage.d.ts +31 -0
  50. package/dist/esm/utilities/usage.js +11 -0
  51. package/dist/esm/utilities/usage.js.map +1 -0
  52. package/package.json +2 -2
  53. package/src/activities/chat/adapter.ts +3 -0
  54. package/src/activities/chat/index.ts +219 -47
  55. package/src/activities/chat/messages.ts +2 -1
  56. package/src/activities/chat/middleware/compose.ts +23 -17
  57. package/src/activities/chat/middleware/types.ts +21 -21
  58. package/src/activities/chat/runtime-context-types.ts +68 -0
  59. package/src/activities/chat/stream/message-updaters.ts +2 -1
  60. package/src/activities/chat/stream/processor.ts +48 -8
  61. package/src/activities/chat/tools/tool-calls.ts +138 -43
  62. package/src/activities/chat/tools/tool-definition.ts +25 -31
  63. package/src/activities/error-payload.ts +44 -0
  64. package/src/activities/generateAudio/index.ts +10 -0
  65. package/src/activities/generateSpeech/index.ts +10 -0
  66. package/src/adapter-internals.ts +4 -1
  67. package/src/client.ts +5 -1
  68. package/src/index.ts +10 -0
  69. package/src/tool-registry.ts +16 -14
  70. package/src/types.ts +118 -79
  71. package/src/utilities/ag-ui-wire.ts +4 -1
  72. package/src/utilities/chat-params.ts +22 -7
  73. package/src/utilities/tool-result.ts +60 -0
  74. package/src/utilities/usage.ts +41 -0
package/src/types.ts CHANGED
@@ -4,6 +4,16 @@ import type {
4
4
  } from '@standard-schema/spec'
5
5
  import type { InternalLogger } from './logger/internal-logger'
6
6
  import type { SystemPrompt } from './system-prompts'
7
+ // The canonical usage types live in the leaf `@tanstack/ai-event-client`
8
+ // package (which `@tanstack/ai` already depends on) so there is a single source
9
+ // of truth without a dependency cycle. They are re-exported below.
10
+ import type {
11
+ CompletionTokensDetails,
12
+ PromptTokensDetails,
13
+ ProviderUsageDetails,
14
+ TokenUsage,
15
+ UsageCostBreakdown,
16
+ } from '@tanstack/ai-event-client'
7
17
  import type {
8
18
  BaseEvent as AGUIBaseEvent,
9
19
  CustomEvent as AGUICustomEvent,
@@ -50,6 +60,8 @@ export type ToolResultState =
50
60
  | 'complete' // Result is complete
51
61
  | 'error' // Error occurred
52
62
 
63
+ export type ToolOutputState = 'output-available' | 'output-error'
64
+
53
65
  /**
54
66
  * JSON Schema type for defining tool input/output schemas as raw JSON Schema objects.
55
67
  * This allows tools to be defined without schema libraries when you have JSON Schema definitions available.
@@ -355,7 +367,7 @@ export interface ToolCallPart<TMetadata = unknown> {
355
367
  export interface ToolResultPart {
356
368
  type: 'tool-result'
357
369
  toolCallId: string
358
- content: string
370
+ content: string | Array<ContentPart>
359
371
  state: ToolResultState
360
372
  error?: string // Error message if state is "error"
361
373
  }
@@ -443,33 +455,75 @@ export type ConstrainedModelMessage<
443
455
  content: ConstrainedContent<TInputModalitiesTypes>
444
456
  }
445
457
 
458
+ type IsUnknown<T> = unknown extends T
459
+ ? [T] extends [unknown]
460
+ ? true
461
+ : false
462
+ : false
463
+
464
+ type RuntimeContextField<TContext> =
465
+ IsUnknown<TContext> extends true
466
+ ? {
467
+ /**
468
+ * Runtime context provided by the caller.
469
+ *
470
+ * This is request-local application state for tool and middleware
471
+ * implementations, not the AG-UI `Context[]` protocol field.
472
+ */
473
+ context?: TContext
474
+ }
475
+ : {
476
+ /**
477
+ * Runtime context provided by the caller.
478
+ *
479
+ * This is request-local application state for tool and middleware
480
+ * implementations, not the AG-UI `Context[]` protocol field.
481
+ */
482
+ context: TContext
483
+ }
484
+
446
485
  /**
447
486
  * Context passed to tool execute functions, providing capabilities like
448
487
  * emitting custom events during execution.
449
488
  */
450
- export interface ToolExecutionContext {
451
- /** The ID of the tool call being executed */
452
- toolCallId?: string
453
- /**
454
- * Emit a custom event during tool execution.
455
- * Events are streamed to the client in real-time as AG-UI CUSTOM events.
456
- *
457
- * @param eventName - Name of the custom event
458
- * @param value - Event payload value
459
- *
460
- * @example
461
- * ```ts
462
- * const tool = toolDefinition({ ... }).server(async (args, context) => {
463
- * context?.emitCustomEvent('progress', { step: 1, total: 3 })
464
- * // ... do work ...
465
- * context?.emitCustomEvent('progress', { step: 2, total: 3 })
466
- * // ... do more work ...
467
- * return result
468
- * })
469
- * ```
470
- */
471
- emitCustomEvent: (eventName: string, value: Record<string, any>) => void
472
- }
489
+ export type ToolExecutionContext<TContext = unknown> =
490
+ RuntimeContextField<TContext> & {
491
+ /** The ID of the tool call being executed */
492
+ toolCallId?: string
493
+ /**
494
+ * Emit a custom event during tool execution.
495
+ * Events are streamed to the client in real-time as AG-UI CUSTOM events.
496
+ *
497
+ * @param eventName - Name of the custom event
498
+ * @param value - Event payload value
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * const tool = toolDefinition({ ... }).server(async (args, context) => {
503
+ * context?.emitCustomEvent('progress', { step: 1, total: 3 })
504
+ * // ... do work ...
505
+ * context?.emitCustomEvent('progress', { step: 2, total: 3 })
506
+ * // ... do more work ...
507
+ * return result
508
+ * })
509
+ * ```
510
+ */
511
+ emitCustomEvent: (eventName: string, value: Record<string, any>) => void
512
+ }
513
+
514
+ export type ToolExecuteFunction<
515
+ TInput extends SchemaInput = SchemaInput,
516
+ TOutput extends SchemaInput = SchemaInput,
517
+ TContext = unknown,
518
+ > = undefined extends TContext
519
+ ? (
520
+ args: InferSchemaType<TInput>,
521
+ context?: ToolExecutionContext<TContext>,
522
+ ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>
523
+ : (
524
+ args: InferSchemaType<TInput>,
525
+ context: ToolExecutionContext<TContext>,
526
+ ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>
473
527
 
474
528
  /**
475
529
  * Tool/Function definition for function calling.
@@ -488,6 +542,7 @@ export interface Tool<
488
542
  TInput extends SchemaInput = SchemaInput,
489
543
  TOutput extends SchemaInput = SchemaInput,
490
544
  TName extends string = string,
545
+ TContext = unknown,
491
546
  > {
492
547
  /**
493
548
  * Unique name of the tool (used by the model to call it).
@@ -587,9 +642,7 @@ export interface Tool<
587
642
  * return weather; // Can return object or string
588
643
  * }
589
644
  */
590
- execute?:
591
- | ((args: any, context?: ToolExecutionContext) => Promise<any> | any)
592
- | undefined
645
+ execute?: ToolExecuteFunction<TInput, TOutput, TContext> | undefined
593
646
 
594
647
  /** If true, tool execution requires user approval before running. Works with both server and client tools. */
595
648
  needsApproval?: boolean
@@ -601,6 +654,10 @@ export interface Tool<
601
654
  metadata?: Record<string, any> | undefined
602
655
  }
603
656
 
657
+ export type AnyTool = Omit<Tool<any, any, any, any>, 'execute'> & {
658
+ execute?: ((args: any, context?: any) => any) | undefined
659
+ }
660
+
604
661
  export interface ToolConfig {
605
662
  [key: string]: Tool
606
663
  }
@@ -729,10 +786,16 @@ export type AgentLoopStrategy = (state: AgentLoopState) => boolean
729
786
  export interface TextOptions<
730
787
  TProviderOptionsSuperset extends Record<string, any> = Record<string, any>,
731
788
  TProviderOptionsForModel = TProviderOptionsSuperset,
789
+ TContext = unknown,
732
790
  > {
733
791
  model: string
734
792
  messages: Array<ModelMessage>
735
- tools?: Array<Tool<any, any, any>> | undefined
793
+ tools?: Array<AnyTool> | undefined
794
+ /**
795
+ * Runtime context provided by the caller and passed to middleware and
796
+ * server-side tool implementations.
797
+ */
798
+ context?: TContext
736
799
  /**
737
800
  * System prompts to include with the request.
738
801
  *
@@ -925,38 +988,22 @@ export interface RunStartedEvent extends AGUIRunStartedEvent {
925
988
  model?: string
926
989
  }
927
990
 
928
- /**
929
- * Provider-reported cost breakdown for a single request, normalized onto a
930
- * canonical shape so consumer code is portable across gateways. Each adapter's
931
- * extractor maps its provider-specific wire keys (e.g. OpenRouter's
932
- * `upstream_inference_prompt_cost`, `upstream_inference_input_cost`) onto these
933
- * fields at runtime.
934
- */
935
- export interface UsageCostBreakdown {
936
- /** Total cost the gateway paid the upstream provider. */
937
- upstreamCost?: number
938
- /** Upstream cost for input (prompt) tokens. */
939
- upstreamInputCost?: number
940
- /** Upstream cost for output (completion) tokens. */
941
- upstreamOutputCost?: number
991
+ // Re-export the canonical usage types (defined in `@tanstack/ai-event-client`)
992
+ // so `@tanstack/ai` consumers keep importing them from here unchanged.
993
+ export type {
994
+ CompletionTokensDetails,
995
+ PromptTokensDetails,
996
+ ProviderUsageDetails,
997
+ TokenUsage,
998
+ UsageCostBreakdown,
942
999
  }
943
1000
 
944
1001
  /**
945
- * Token usage totals for a run, optionally including provider-reported cost.
946
- *
947
- * `cost` and `costDetails` are populated only by adapters whose provider returns
948
- * authoritative per-request cost (e.g. OpenRouter). They are absent for adapters
949
- * that do not report cost, so consumers must treat them as optional.
1002
+ * @deprecated Renamed to {@link TokenUsage}. Kept as an alias for backward
1003
+ * compatibility with `@tanstack/ai@0.23` and earlier; will be removed in a
1004
+ * future release.
950
1005
  */
951
- export interface UsageTotals {
952
- promptTokens: number
953
- completionTokens: number
954
- totalTokens: number
955
- /** Provider-reported cost for the request, when available. */
956
- cost?: number
957
- /** Provider-reported cost breakdown, when available. */
958
- costDetails?: UsageCostBreakdown
959
- }
1006
+ export type UsageTotals = TokenUsage
960
1007
 
961
1008
  /**
962
1009
  * Emitted when a run completes successfully.
@@ -969,8 +1016,8 @@ export interface RunFinishedEvent extends AGUIRunFinishedEvent {
969
1016
  model?: string
970
1017
  /** Why the generation stopped */
971
1018
  finishReason?: 'stop' | 'length' | 'content_filter' | 'tool_calls' | null
972
- /** Token usage statistics, optionally including provider-reported cost. */
973
- usage?: UsageTotals
1019
+ /** Token usage statistics with optional detailed breakdowns and provider-reported cost. */
1020
+ usage?: TokenUsage
974
1021
  }
975
1022
 
976
1023
  /**
@@ -1084,7 +1131,9 @@ export interface ToolCallEndEvent extends AGUIToolCallEndEvent {
1084
1131
  /** Final parsed input arguments (TanStack AI internal) */
1085
1132
  input?: unknown
1086
1133
  /** Tool execution result (TanStack AI internal) */
1087
- result?: string
1134
+ result?: string | Array<ContentPart>
1135
+ /** Tool execution output state (TanStack AI internal) */
1136
+ state?: ToolOutputState
1088
1137
  }
1089
1138
 
1090
1139
  /**
@@ -1096,6 +1145,8 @@ export interface ToolCallEndEvent extends AGUIToolCallEndEvent {
1096
1145
  export interface ToolCallResultEvent extends AGUIToolCallResultEvent {
1097
1146
  /** Model identifier for multi-model support */
1098
1147
  model?: string
1148
+ /** Tool execution output state (TanStack AI internal) */
1149
+ state?: ToolOutputState
1099
1150
  }
1100
1151
 
1101
1152
  /**
@@ -1416,11 +1467,7 @@ export interface TextCompletionChunk {
1416
1467
  content: string
1417
1468
  role?: 'assistant'
1418
1469
  finishReason?: 'stop' | 'length' | 'content_filter' | null
1419
- usage?: {
1420
- promptTokens: number
1421
- completionTokens: number
1422
- totalTokens: number
1423
- }
1470
+ usage?: TokenUsage
1424
1471
  }
1425
1472
 
1426
1473
  export interface SummarizationOptions<
@@ -1444,11 +1491,7 @@ export interface SummarizationResult {
1444
1491
  id: string
1445
1492
  model: string
1446
1493
  summary: string
1447
- usage: {
1448
- promptTokens: number
1449
- completionTokens: number
1450
- totalTokens: number
1451
- }
1494
+ usage: TokenUsage
1452
1495
  }
1453
1496
 
1454
1497
  // ============================================================================
@@ -1517,11 +1560,7 @@ export interface ImageGenerationResult {
1517
1560
  /** Array of generated images */
1518
1561
  images: Array<GeneratedImage>
1519
1562
  /** Token usage information (if available) */
1520
- usage?: {
1521
- inputTokens?: number
1522
- outputTokens?: number
1523
- totalTokens?: number
1524
- }
1563
+ usage?: TokenUsage
1525
1564
  }
1526
1565
 
1527
1566
  // ============================================================================
@@ -1572,11 +1611,7 @@ export interface AudioGenerationResult {
1572
1611
  /** The generated audio */
1573
1612
  audio: GeneratedAudio
1574
1613
  /** Token usage information (if available) */
1575
- usage?: {
1576
- inputTokens?: number
1577
- outputTokens?: number
1578
- totalTokens?: number
1579
- }
1614
+ usage?: TokenUsage
1580
1615
  }
1581
1616
 
1582
1617
  // ============================================================================
@@ -1697,6 +1732,8 @@ export interface TTSResult {
1697
1732
  duration?: number
1698
1733
  /** Content type of the audio (e.g., 'audio/mp3') */
1699
1734
  contentType?: string
1735
+ /** Token usage information (if provided by the adapter) */
1736
+ usage?: TokenUsage
1700
1737
  }
1701
1738
 
1702
1739
  // ============================================================================
@@ -1778,6 +1815,8 @@ export interface TranscriptionResult {
1778
1815
  segments?: Array<TranscriptionSegment>
1779
1816
  /** Word-level timestamps, if available */
1780
1817
  words?: Array<TranscriptionWord>
1818
+ /** Token usage information (if provided by the adapter) */
1819
+ usage?: TokenUsage
1781
1820
  }
1782
1821
 
1783
1822
  /**
@@ -103,7 +103,10 @@ export function uiMessagesToWire(
103
103
  role: 'tool',
104
104
  id: deriveToolMessageId(part.toolCallId),
105
105
  toolCallId: part.toolCallId,
106
- content: part.content,
106
+ content:
107
+ typeof part.content === 'string'
108
+ ? part.content
109
+ : JSON.stringify(part.content),
107
110
  ...(part.error !== undefined && { error: part.error }),
108
111
  })
109
112
  }
@@ -1,6 +1,12 @@
1
1
  import { AGUIError, RunAgentInputSchema } from '@ag-ui/core'
2
2
  import type { Context as AGUIContext } from '@ag-ui/core'
3
- import type { JSONSchema, ModelMessage, Tool, UIMessage } from '../types'
3
+ import type {
4
+ JSONSchema,
5
+ ModelMessage,
6
+ SchemaInput,
7
+ Tool,
8
+ UIMessage,
9
+ } from '../types'
4
10
 
5
11
  const KNOWN_PART_TYPES = new Set([
6
12
  'text',
@@ -43,7 +49,12 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
43
49
  tools: Array<{ name: string; description: string; parameters: JSONSchema }>
44
50
  forwardedProps: Record<string, unknown>
45
51
  state: unknown
52
+ /**
53
+ * @deprecated Use `aguiContext` instead. This alias will be removed in a
54
+ * future release.
55
+ */
46
56
  context: Array<AGUIContext>
57
+ aguiContext: Array<AGUIContext>
47
58
  }> {
48
59
  const parseResult = RunAgentInputSchema.safeParse(body)
49
60
  if (!parseResult.success) {
@@ -58,6 +69,7 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
58
69
  }
59
70
 
60
71
  const parsed = parseResult.data
72
+ const aguiContext = parsed.context
61
73
 
62
74
  // AG-UI Zod uses `.strip()` so extra fields like `parts` on messages are
63
75
  // dropped during parse. We re-attach them from the original body so the
@@ -89,7 +101,8 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
89
101
  }>,
90
102
  forwardedProps: (parsed.forwardedProps ?? {}) as Record<string, unknown>,
91
103
  state: parsed.state,
92
- context: parsed.context,
104
+ context: aguiContext,
105
+ aguiContext,
93
106
  })
94
107
  }
95
108
 
@@ -171,16 +184,18 @@ export async function chatParamsFromRequest(
171
184
  * `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.
172
185
  * @returns A merged array suitable for `chat({ tools })`.
173
186
  */
174
- export function mergeAgentTools(
175
- serverTools: ReadonlyArray<Tool>,
187
+ export function mergeAgentTools<TContext = unknown>(
188
+ serverTools: ReadonlyArray<Tool<SchemaInput, SchemaInput, string, TContext>>,
176
189
  clientTools: ReadonlyArray<{
177
190
  name: string
178
191
  description: string
179
192
  parameters: JSONSchema
180
193
  }>,
181
- ): Array<Tool> {
194
+ ): Array<Tool<SchemaInput, SchemaInput, string, TContext>> {
182
195
  const seen = new Set(serverTools.map((t) => t.name))
183
- const merged: Array<Tool> = [...serverTools]
196
+ const merged: Array<Tool<SchemaInput, SchemaInput, string, TContext>> = [
197
+ ...serverTools,
198
+ ]
184
199
  for (const ct of clientTools) {
185
200
  if (seen.has(ct.name)) {
186
201
  // Server wins on name collision.
@@ -193,7 +208,7 @@ export function mergeAgentTools(
193
208
  inputSchema: ct.parameters,
194
209
  // No `execute` — runtime treats this as a client-side tool and
195
210
  // emits ClientToolRequest events.
196
- } as Tool)
211
+ } as Tool<SchemaInput, SchemaInput, string, TContext>)
197
212
  }
198
213
  return merged
199
214
  }
@@ -0,0 +1,60 @@
1
+ import type { ContentPart } from '../types'
2
+
3
+ const CONTENT_PART_TYPES = new Set([
4
+ 'text',
5
+ 'image',
6
+ 'audio',
7
+ 'video',
8
+ 'document',
9
+ ])
10
+
11
+ /**
12
+ * Structural check for a single `ContentPart`. A text part must carry a string
13
+ * `content`; every other modality must carry a `source` with `type` of
14
+ * `'url' | 'data'` and a string `value`.
15
+ */
16
+ export function isContentPart(value: unknown): value is ContentPart {
17
+ if (typeof value !== 'object' || value === null) return false
18
+ const part = value as Record<string, unknown>
19
+ if (typeof part.type !== 'string' || !CONTENT_PART_TYPES.has(part.type)) {
20
+ return false
21
+ }
22
+ if (part.type === 'text') {
23
+ return typeof part.content === 'string'
24
+ }
25
+ const source = part.source
26
+ if (typeof source !== 'object' || source === null) return false
27
+ const src = source as Record<string, unknown>
28
+ if (typeof src.value !== 'string') return false
29
+ // `data` sources require a mimeType (matches ContentPartDataSource); `url`
30
+ // sources don't. Requiring it here keeps the runtime guard consistent with
31
+ // the type and avoids emitting `data:undefined;base64,...` downstream.
32
+ if (src.type === 'data') return typeof src.mimeType === 'string'
33
+ return src.type === 'url'
34
+ }
35
+
36
+ /**
37
+ * True iff `value` is a NON-EMPTY array whose every element is a valid
38
+ * `ContentPart`. Empty arrays and mixed arrays return false so they continue
39
+ * to be treated as ordinary (stringified) data — this keeps the auto-detection
40
+ * footgun narrow.
41
+ */
42
+ export function isContentPartArray(
43
+ value: unknown,
44
+ ): value is Array<ContentPart> {
45
+ return Array.isArray(value) && value.length > 0 && value.every(isContentPart)
46
+ }
47
+
48
+ /**
49
+ * Normalize a tool's return value for transport:
50
+ * - string → unchanged
51
+ * - ContentPart array → unchanged (multimodal, passed through to the adapter)
52
+ * - anything else → `JSON.stringify`
53
+ */
54
+ export function normalizeToolResult(
55
+ result: unknown,
56
+ ): string | Array<ContentPart> {
57
+ if (typeof result === 'string') return result
58
+ if (isContentPartArray(result)) return result
59
+ return JSON.stringify(result)
60
+ }
@@ -0,0 +1,41 @@
1
+ import type { ProviderUsageDetails, TokenUsage } from '../types'
2
+
3
+ /**
4
+ * Input parameters for building base TokenUsage.
5
+ * Provider functions should extract these from their SDK's response.
6
+ */
7
+ export interface BaseUsageInput {
8
+ /** Total input/prompt tokens */
9
+ promptTokens: number
10
+ /** Total output/completion tokens */
11
+ completionTokens: number
12
+ /** Total tokens (prompt + completion) */
13
+ totalTokens: number
14
+ }
15
+
16
+ /**
17
+ * Builds the base TokenUsage object with core fields.
18
+ * Provider-specific functions should use this and then add their own details.
19
+ *
20
+ * @param input - The base token counts
21
+ * @returns A TokenUsage object with promptTokens, completionTokens, totalTokens
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const base = buildBaseUsage({
26
+ * promptTokens: 100,
27
+ * completionTokens: 50,
28
+ * totalTokens: 150
29
+ * });
30
+ * // Returns: { promptTokens: 100, completionTokens: 50, totalTokens: 150 }
31
+ * ```
32
+ */
33
+ export function buildBaseUsage<TProviderDetails = ProviderUsageDetails>(
34
+ input: BaseUsageInput,
35
+ ): TokenUsage<TProviderDetails> {
36
+ return {
37
+ promptTokens: input.promptTokens,
38
+ completionTokens: input.completionTokens,
39
+ totalTokens: input.totalTokens,
40
+ }
41
+ }