@graphorin/provider 0.6.0 → 0.7.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 +42 -0
- package/README.md +8 -3
- package/dist/adapters/llamacpp-server.d.ts +1 -1
- package/dist/adapters/llamacpp-server.js.map +1 -1
- package/dist/adapters/ollama.d.ts.map +1 -1
- package/dist/adapters/ollama.js +19 -6
- package/dist/adapters/ollama.js.map +1 -1
- package/dist/adapters/vercel.d.ts +1 -1
- package/dist/adapters/vercel.d.ts.map +1 -1
- package/dist/adapters/vercel.js +111 -33
- package/dist/adapters/vercel.js.map +1 -1
- package/dist/errors/errors.d.ts +1 -1
- package/dist/errors/errors.js +1 -1
- package/dist/errors/errors.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -8
- package/dist/index.js.map +1 -1
- package/dist/internal/http.js +111 -7
- package/dist/internal/http.js.map +1 -1
- package/dist/internal/openai-shaped.js +21 -4
- package/dist/internal/openai-shaped.js.map +1 -1
- package/dist/middleware/with-fallback.js +1 -1
- package/dist/middleware/with-rate-limit.d.ts +21 -8
- package/dist/middleware/with-rate-limit.d.ts.map +1 -1
- package/dist/middleware/with-rate-limit.js +65 -12
- package/dist/middleware/with-rate-limit.js.map +1 -1
- package/dist/middleware/with-retry.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +18 -16
- package/src/adapters/index.ts +14 -0
- package/src/adapters/llamacpp-server.ts +102 -0
- package/src/adapters/ollama.ts +382 -0
- package/src/adapters/openai-compatible.ts +95 -0
- package/src/adapters/vercel-messages.ts +308 -0
- package/src/adapters/vercel.ts +706 -0
- package/src/counters/anthropic-wire.ts +199 -0
- package/src/counters/anthropic.ts +114 -0
- package/src/counters/bedrock.ts +46 -0
- package/src/counters/dispatcher.ts +127 -0
- package/src/counters/global.ts +39 -0
- package/src/counters/google.ts +46 -0
- package/src/counters/heuristic.ts +107 -0
- package/src/counters/index.ts +35 -0
- package/src/counters/js-tiktoken.ts +135 -0
- package/src/counters/serialize.ts +85 -0
- package/src/errors/errors.ts +316 -0
- package/src/errors/index.ts +20 -0
- package/src/index.ts +42 -0
- package/src/internal/abort.ts +30 -0
- package/src/internal/http.ts +388 -0
- package/src/internal/openai-shaped.ts +555 -0
- package/src/internal/sse.ts +112 -0
- package/src/internal/url-utils.ts +20 -0
- package/src/middleware/compose.ts +213 -0
- package/src/middleware/index.ts +37 -0
- package/src/middleware/production-hook.ts +47 -0
- package/src/middleware/with-cost-limit.ts +131 -0
- package/src/middleware/with-cost-tracking.ts +216 -0
- package/src/middleware/with-fallback.ts +157 -0
- package/src/middleware/with-rate-limit.ts +306 -0
- package/src/middleware/with-redaction.ts +671 -0
- package/src/middleware/with-retry.ts +274 -0
- package/src/middleware/with-tracing.ts +117 -0
- package/src/model-tier/classify.ts +125 -0
- package/src/model-tier/index.ts +11 -0
- package/src/provider.ts +121 -0
- package/src/reasoning/apply-policy.ts +89 -0
- package/src/reasoning/classify-contract.ts +120 -0
- package/src/reasoning/index.ts +21 -0
- package/src/reasoning/retention.ts +64 -0
- package/src/tool-examples.ts +54 -0
- package/src/trust/classify-local-provider.ts +254 -0
- package/src/trust/index.ts +14 -0
- package/dist/adapters/index.js +0 -6
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vercelAdapter` - wraps a Vercel AI SDK `LanguageModel`-shaped value
|
|
3
|
+
* into a Graphorin {@link Provider}. The adapter is the default cloud
|
|
4
|
+
* path: it speaks the AI SDK's `streamText` / `generateText` API and
|
|
5
|
+
* maps the resulting events onto the canonical
|
|
6
|
+
* `ProviderEvent` discriminated union.
|
|
7
|
+
*
|
|
8
|
+
* Outbound, the adapter converts Graphorin messages / tools onto the
|
|
9
|
+
* AI SDK call contract (see `vercel-messages.ts`): tool definitions
|
|
10
|
+
* become a name-keyed record with `jsonSchema()`-shaped input schemas,
|
|
11
|
+
* assistant `toolCalls` become `tool-call` content parts, and
|
|
12
|
+
* `ToolMessage`s become `tool-result` messages - the SDK zod-validates
|
|
13
|
+
* all of these and rejects the raw Graphorin shapes.
|
|
14
|
+
*
|
|
15
|
+
* The AI SDK is an **optional peer dependency** of `@graphorin/provider`.
|
|
16
|
+
* Production callers leave `runtimeOverrides` unset and the adapter
|
|
17
|
+
* dynamically imports the package on first use; test fixtures pass a
|
|
18
|
+
* `runtimeOverrides` value to short-circuit the import and feed
|
|
19
|
+
* fixture chunks directly. The overrides shape is intentionally
|
|
20
|
+
* structural so users can supply hand-rolled stubs or any compatible
|
|
21
|
+
* library.
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
FinishReason,
|
|
28
|
+
Provider,
|
|
29
|
+
ProviderCapabilities,
|
|
30
|
+
ProviderEvent,
|
|
31
|
+
ProviderRequest,
|
|
32
|
+
ProviderResponse,
|
|
33
|
+
Usage,
|
|
34
|
+
} from '@graphorin/core';
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
classifyHttpStatus,
|
|
38
|
+
ProviderHttpError,
|
|
39
|
+
ProviderStreamParseError,
|
|
40
|
+
} from '../errors/errors.js';
|
|
41
|
+
import { isAbortError } from '../internal/abort.js';
|
|
42
|
+
import { applyReasoningPolicy } from '../reasoning/apply-policy.js';
|
|
43
|
+
import { inferReasoningContract } from '../reasoning/classify-contract.js';
|
|
44
|
+
import { resolveReasoningRetention } from '../reasoning/retention.js';
|
|
45
|
+
import { foldToolExamples } from '../tool-examples.js';
|
|
46
|
+
import {
|
|
47
|
+
applyCacheAnchors,
|
|
48
|
+
toAiSdkPrompt,
|
|
49
|
+
toAiSdkToolChoice,
|
|
50
|
+
toAiSdkTools,
|
|
51
|
+
} from './vercel-messages.js';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Structural shape the adapter expects from the AI SDK language model
|
|
55
|
+
* value. The real `LanguageModelV4` matches this shape. Re-declared
|
|
56
|
+
* here so we do not pin a hard dependency on `@ai-sdk/provider`.
|
|
57
|
+
*
|
|
58
|
+
* @stable
|
|
59
|
+
*/
|
|
60
|
+
export interface LanguageModelLike {
|
|
61
|
+
readonly provider: string;
|
|
62
|
+
readonly modelId: string;
|
|
63
|
+
readonly specificationVersion?: string | number;
|
|
64
|
+
/**
|
|
65
|
+
* Optional capability flags carried by the AI SDK model. The adapter
|
|
66
|
+
* forwards them onto the canonical `ProviderCapabilities` shape;
|
|
67
|
+
* missing values are filled in with conservative defaults.
|
|
68
|
+
*/
|
|
69
|
+
readonly supportedToolCallTypes?: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Loose chunk shape emitted by the AI SDK's `streamText`. The shape is
|
|
74
|
+
* intentionally permissive - we accept anything that carries the
|
|
75
|
+
* fields we use and ignore the rest. This keeps the adapter tolerant
|
|
76
|
+
* of additive AI SDK schema changes.
|
|
77
|
+
*
|
|
78
|
+
* The fields we read are normalized in the adapter via narrow helper
|
|
79
|
+
* functions, so we deliberately type each as `unknown` and gate
|
|
80
|
+
* access behind `typeof` checks at runtime.
|
|
81
|
+
*
|
|
82
|
+
* @stable
|
|
83
|
+
*/
|
|
84
|
+
export interface AISDKChunk {
|
|
85
|
+
readonly type: string;
|
|
86
|
+
readonly [extra: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Subset of the AI SDK surface used by the adapter.
|
|
91
|
+
*
|
|
92
|
+
* @stable
|
|
93
|
+
*/
|
|
94
|
+
export interface VercelRuntimeOverrides {
|
|
95
|
+
readonly streamText: (args: {
|
|
96
|
+
model: LanguageModelLike;
|
|
97
|
+
/** AI SDK `ModelMessage`-shaped values (converted, NOT Graphorin `Message`s). */
|
|
98
|
+
messages: ReadonlyArray<Readonly<Record<string, unknown>>>;
|
|
99
|
+
system?: string;
|
|
100
|
+
tools?: unknown;
|
|
101
|
+
toolChoice?: unknown;
|
|
102
|
+
temperature?: number;
|
|
103
|
+
maxTokens?: number;
|
|
104
|
+
abortSignal?: AbortSignal;
|
|
105
|
+
providerOptions?: Readonly<Record<string, unknown>>;
|
|
106
|
+
}) => {
|
|
107
|
+
readonly fullStream: AsyncIterable<AISDKChunk>;
|
|
108
|
+
};
|
|
109
|
+
readonly generateText: (args: {
|
|
110
|
+
model: LanguageModelLike;
|
|
111
|
+
/** AI SDK `ModelMessage`-shaped values (converted, NOT Graphorin `Message`s). */
|
|
112
|
+
messages: ReadonlyArray<Readonly<Record<string, unknown>>>;
|
|
113
|
+
system?: string;
|
|
114
|
+
tools?: unknown;
|
|
115
|
+
toolChoice?: unknown;
|
|
116
|
+
temperature?: number;
|
|
117
|
+
maxTokens?: number;
|
|
118
|
+
abortSignal?: AbortSignal;
|
|
119
|
+
providerOptions?: Readonly<Record<string, unknown>>;
|
|
120
|
+
}) => Promise<{
|
|
121
|
+
readonly text?: string;
|
|
122
|
+
readonly toolCalls?: ReadonlyArray<{
|
|
123
|
+
readonly toolCallId: string;
|
|
124
|
+
readonly toolName: string;
|
|
125
|
+
readonly args: unknown;
|
|
126
|
+
}>;
|
|
127
|
+
readonly usage?: Partial<Usage> & {
|
|
128
|
+
readonly inputTokens?: number;
|
|
129
|
+
readonly outputTokens?: number;
|
|
130
|
+
readonly totalTokens?: number;
|
|
131
|
+
};
|
|
132
|
+
readonly finishReason?: string;
|
|
133
|
+
readonly providerMetadata?: Readonly<Record<string, unknown>>;
|
|
134
|
+
}>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Options accepted by {@link vercelAdapter}.
|
|
139
|
+
*
|
|
140
|
+
* @stable
|
|
141
|
+
*/
|
|
142
|
+
export interface VercelAdapterOptions {
|
|
143
|
+
/**
|
|
144
|
+
* Fully-qualified provider name, used for span / log labelling.
|
|
145
|
+
* Defaults to `${model.provider}-${model.modelId}`.
|
|
146
|
+
*/
|
|
147
|
+
readonly name?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Capability declaration. The adapter merges these on top of a
|
|
150
|
+
* conservative defaults table (`streaming: true`, `toolCalling: true`,
|
|
151
|
+
* `multimodal: true`, …); supply explicit values to narrow them.
|
|
152
|
+
*/
|
|
153
|
+
readonly capabilities?: Partial<ProviderCapabilities>;
|
|
154
|
+
/**
|
|
155
|
+
* Runtime override for the AI SDK functions. When unset, the adapter
|
|
156
|
+
* lazily `await import('ai')` on first call. Test suites pass a
|
|
157
|
+
* fixture-driven implementation directly.
|
|
158
|
+
*/
|
|
159
|
+
readonly runtimeOverrides?: VercelRuntimeOverrides;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const DEFAULT_CAPABILITIES: Omit<ProviderCapabilities, 'reasoningContract'> = {
|
|
163
|
+
streaming: true,
|
|
164
|
+
toolCalling: true,
|
|
165
|
+
parallelToolCalls: true,
|
|
166
|
+
multimodal: true,
|
|
167
|
+
structuredOutput: true,
|
|
168
|
+
reasoning: true,
|
|
169
|
+
contextWindow: 200_000,
|
|
170
|
+
maxOutput: 16_384,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Wrap a Vercel AI SDK language-model value in a Graphorin
|
|
175
|
+
* {@link Provider}. Outbound requests are converted onto the AI SDK
|
|
176
|
+
* call contract (name-keyed tools, `tool-call` / `tool-result` content
|
|
177
|
+
* parts - see `vercel-messages.ts`); the streaming chunks emitted by
|
|
178
|
+
* the AI SDK are translated back onto Graphorin `ProviderEvent`s.
|
|
179
|
+
*
|
|
180
|
+
* The adapter auto-detects the model's
|
|
181
|
+
* `ReasoningContract` from its
|
|
182
|
+
* `modelId` (e.g. Anthropic Claude → `'round-trip-required'`,
|
|
183
|
+
* OpenAI o1 / o3 → `'hidden'`, Gemini reasoning variants →
|
|
184
|
+
* `'hidden'`, everything else → `'optional'`). Callers can override
|
|
185
|
+
* the inferred value via `options.capabilities.reasoningContract`.
|
|
186
|
+
*
|
|
187
|
+
* @stable
|
|
188
|
+
*/
|
|
189
|
+
export function vercelAdapter(
|
|
190
|
+
model: LanguageModelLike,
|
|
191
|
+
options: VercelAdapterOptions = {},
|
|
192
|
+
): Provider {
|
|
193
|
+
const name = options.name ?? `${model.provider}-${model.modelId}`;
|
|
194
|
+
const inferredContract = inferReasoningContract({
|
|
195
|
+
modelId: model.modelId,
|
|
196
|
+
provider: model.provider,
|
|
197
|
+
});
|
|
198
|
+
const capabilities: ProviderCapabilities = {
|
|
199
|
+
...DEFAULT_CAPABILITIES,
|
|
200
|
+
reasoningContract: inferredContract,
|
|
201
|
+
...options.capabilities,
|
|
202
|
+
};
|
|
203
|
+
const runtime = options.runtimeOverrides;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
name,
|
|
207
|
+
modelId: model.modelId,
|
|
208
|
+
capabilities,
|
|
209
|
+
stream(req) {
|
|
210
|
+
return streamFromVercel(model, name, capabilities, req, runtime);
|
|
211
|
+
},
|
|
212
|
+
async generate(req) {
|
|
213
|
+
return generateFromVercel(model, name, capabilities, req, runtime);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function* streamFromVercel(
|
|
219
|
+
model: LanguageModelLike,
|
|
220
|
+
providerName: string,
|
|
221
|
+
capabilities: ProviderCapabilities,
|
|
222
|
+
req: ProviderRequest,
|
|
223
|
+
overrides: VercelRuntimeOverrides | undefined,
|
|
224
|
+
): AsyncIterable<ProviderEvent> {
|
|
225
|
+
const sdk = await loadRuntime(overrides);
|
|
226
|
+
const callArgs = buildCallArgs(model, applyRequestPreflight(req, capabilities));
|
|
227
|
+
let stream: AsyncIterable<AISDKChunk>;
|
|
228
|
+
try {
|
|
229
|
+
const result = sdk.streamText(callArgs);
|
|
230
|
+
stream = result.fullStream;
|
|
231
|
+
} catch (cause) {
|
|
232
|
+
const headers = headersFromCause(cause);
|
|
233
|
+
throw new ProviderHttpError({
|
|
234
|
+
providerName,
|
|
235
|
+
status: statusFromCause(cause),
|
|
236
|
+
message: 'streamText() failed before yielding any chunks',
|
|
237
|
+
cause,
|
|
238
|
+
...(headers !== undefined ? { headers } : {}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// W-023: the AI SDK never throws from streamText() synchronously -
|
|
243
|
+
// transport/HTTP failures (429/500/529) arrive as in-band
|
|
244
|
+
// `{type:'error'}` chunks. Emitting `stream-start` eagerly made every
|
|
245
|
+
// such failure post-yield, which forbids `withRetry`/`withFallback`
|
|
246
|
+
// from restarting the stream (PS-1). Defer it until the first REAL
|
|
247
|
+
// mapped event, so a pre-content failure can be THROWN as a typed
|
|
248
|
+
// `ProviderHttpError` (retryable / fallback-eligible) instead.
|
|
249
|
+
let emittedStart = false;
|
|
250
|
+
const startEvent = (): ProviderEvent => {
|
|
251
|
+
emittedStart = true;
|
|
252
|
+
return {
|
|
253
|
+
type: 'stream-start',
|
|
254
|
+
metadata: {
|
|
255
|
+
providerName,
|
|
256
|
+
modelId: model.modelId,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
let finalUsage: Usage | undefined;
|
|
262
|
+
let finishReason: FinishReason = 'stop';
|
|
263
|
+
let sawError = false;
|
|
264
|
+
|
|
265
|
+
for await (const chunk of stream) {
|
|
266
|
+
if (req.signal?.aborted) {
|
|
267
|
+
// PS-12: an aborted stream must report 'aborted', not the initial 'stop'
|
|
268
|
+
// - mirrors the openai-shaped and ollama adapters.
|
|
269
|
+
finishReason = 'aborted';
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
switch (chunk.type) {
|
|
273
|
+
case 'text-delta': {
|
|
274
|
+
const delta = pickString(chunk.textDelta) ?? pickString(chunk.text) ?? '';
|
|
275
|
+
if (delta.length > 0) {
|
|
276
|
+
if (!emittedStart) yield startEvent();
|
|
277
|
+
yield { type: 'text-delta', delta };
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case 'reasoning':
|
|
282
|
+
case 'reasoning-delta': {
|
|
283
|
+
const delta =
|
|
284
|
+
pickString(chunk.textDelta) ?? pickString(chunk.delta) ?? pickString(chunk.text) ?? '';
|
|
285
|
+
if (delta.length > 0) {
|
|
286
|
+
if (!emittedStart) yield startEvent();
|
|
287
|
+
yield { type: 'reasoning-delta', delta };
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
// W-024: per-block reasoning terminators. AI SDK v4 streams a
|
|
292
|
+
// separate 'reasoning-signature' chunk ({signature}) and
|
|
293
|
+
// 'redacted-reasoning' ({data}); v7 closes each block with
|
|
294
|
+
// 'reasoning-end' carrying providerMetadata.anthropic.signature /
|
|
295
|
+
// .redactedData. All three become the Graphorin 'reasoning-end'
|
|
296
|
+
// event whose meta the retention pipeline round-trips
|
|
297
|
+
// ('pass-through-claude' -> providerOptions.anthropic.signature).
|
|
298
|
+
case 'reasoning-signature': {
|
|
299
|
+
const signature = pickString(chunk.signature);
|
|
300
|
+
if (!emittedStart) yield startEvent();
|
|
301
|
+
yield {
|
|
302
|
+
type: 'reasoning-end',
|
|
303
|
+
meta: { provider: 'anthropic', ...(signature !== undefined ? { signature } : {}) },
|
|
304
|
+
};
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case 'redacted-reasoning': {
|
|
308
|
+
const data = pickString(chunk.data);
|
|
309
|
+
if (!emittedStart) yield startEvent();
|
|
310
|
+
yield {
|
|
311
|
+
type: 'reasoning-end',
|
|
312
|
+
meta: { provider: 'anthropic', ...(data !== undefined ? { data } : {}) },
|
|
313
|
+
};
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case 'reasoning-end': {
|
|
317
|
+
const anthropicMeta =
|
|
318
|
+
typeof chunk.providerMetadata === 'object' && chunk.providerMetadata !== null
|
|
319
|
+
? ((chunk.providerMetadata as { anthropic?: unknown }).anthropic as
|
|
320
|
+
| { signature?: unknown; redactedData?: unknown }
|
|
321
|
+
| undefined)
|
|
322
|
+
: undefined;
|
|
323
|
+
const signature = pickString(anthropicMeta?.signature);
|
|
324
|
+
const data = pickString(anthropicMeta?.redactedData);
|
|
325
|
+
if (!emittedStart) yield startEvent();
|
|
326
|
+
yield {
|
|
327
|
+
type: 'reasoning-end',
|
|
328
|
+
meta: {
|
|
329
|
+
provider: 'anthropic',
|
|
330
|
+
...(signature !== undefined ? { signature } : {}),
|
|
331
|
+
...(data !== undefined ? { data } : {}),
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
// PS-6 dual-shape: AI SDK v4 streams `tool-call-streaming-start` /
|
|
337
|
+
// `tool-call-delta` keyed by `toolCallId`/`argsTextDelta`; v7 streams
|
|
338
|
+
// `tool-input-start` / `tool-input-delta` keyed by `id`/`delta`.
|
|
339
|
+
case 'tool-call-streaming-start':
|
|
340
|
+
case 'tool-input-start': {
|
|
341
|
+
const toolCallId = pickString(chunk.toolCallId) ?? pickString(chunk.id);
|
|
342
|
+
const toolName = pickString(chunk.toolName);
|
|
343
|
+
if (toolCallId !== undefined && toolName !== undefined) {
|
|
344
|
+
if (!emittedStart) yield startEvent();
|
|
345
|
+
yield {
|
|
346
|
+
type: 'tool-call-start',
|
|
347
|
+
toolCallId,
|
|
348
|
+
toolName,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case 'tool-call-delta':
|
|
354
|
+
case 'tool-input-delta': {
|
|
355
|
+
const toolCallId = pickString(chunk.toolCallId) ?? pickString(chunk.id);
|
|
356
|
+
const argsDelta =
|
|
357
|
+
pickString(chunk.argsTextDelta) ??
|
|
358
|
+
pickString(chunk.delta) ??
|
|
359
|
+
pickString(chunk.inputTextDelta);
|
|
360
|
+
if (toolCallId !== undefined && argsDelta !== undefined && argsDelta.length > 0) {
|
|
361
|
+
if (!emittedStart) yield startEvent();
|
|
362
|
+
yield {
|
|
363
|
+
type: 'tool-call-input-delta',
|
|
364
|
+
toolCallId,
|
|
365
|
+
argsDelta,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case 'tool-call': {
|
|
371
|
+
const toolCallId = pickString(chunk.toolCallId) ?? pickString(chunk.id);
|
|
372
|
+
if (toolCallId !== undefined) {
|
|
373
|
+
if (!emittedStart) yield startEvent();
|
|
374
|
+
yield {
|
|
375
|
+
type: 'tool-call-end',
|
|
376
|
+
toolCallId,
|
|
377
|
+
// v4 carries `args`; v7 carries `input`.
|
|
378
|
+
finalArgs: chunk.args ?? chunk.input,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case 'finish': {
|
|
384
|
+
finishReason = mapFinishReason(pickString(chunk.finishReason));
|
|
385
|
+
// v4 carries `usage`; v7 carries `totalUsage` (zeroing the v4
|
|
386
|
+
// read nulled cost tracking on streaming).
|
|
387
|
+
finalUsage = mapUsage(chunk.totalUsage ?? chunk.usage);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case 'error': {
|
|
391
|
+
const errorField = chunk.error;
|
|
392
|
+
const message =
|
|
393
|
+
typeof errorField === 'string'
|
|
394
|
+
? errorField
|
|
395
|
+
: typeof errorField === 'object' && errorField !== null
|
|
396
|
+
? (pickString((errorField as { message?: unknown }).message) ?? 'unknown error')
|
|
397
|
+
: 'unknown error';
|
|
398
|
+
// W-023: an abort surfacing as an error chunk is a cancellation,
|
|
399
|
+
// never a retryable failure (mirrors PS-12).
|
|
400
|
+
if (isAbortError(errorField) || req.signal?.aborted === true) {
|
|
401
|
+
finishReason = 'aborted';
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
const status = statusFromCause(errorField);
|
|
405
|
+
if (!emittedStart) {
|
|
406
|
+
// Pre-content failure: nothing was yielded, so by PS-1 the
|
|
407
|
+
// middleware may restart this stream. Throw the classified
|
|
408
|
+
// typed error - `withRetry` retries 429/5xx pre-yield,
|
|
409
|
+
// `withFallback` switches providers, and the agent-level
|
|
410
|
+
// fallback chain reads `errorKind` off the throw.
|
|
411
|
+
const headers = headersFromCause(errorField);
|
|
412
|
+
throw new ProviderHttpError({
|
|
413
|
+
providerName,
|
|
414
|
+
status,
|
|
415
|
+
message,
|
|
416
|
+
cause: errorField,
|
|
417
|
+
...(headers !== undefined ? { headers } : {}),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// Mid-stream failure: a restart is forbidden (PS-1) - surface a
|
|
421
|
+
// structural error event with the canonical kind so the
|
|
422
|
+
// agent-level per-step fallback can act on rate-limit/capacity
|
|
423
|
+
// instead of an inert 'unknown'. Status 0 classifies as
|
|
424
|
+
// 'transient' by the PS-2 network policy.
|
|
425
|
+
sawError = true;
|
|
426
|
+
yield {
|
|
427
|
+
type: 'error',
|
|
428
|
+
error: { kind: classifyHttpStatus(status, message), message },
|
|
429
|
+
};
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
default:
|
|
433
|
+
// Unknown chunk types are forward-compatible no-ops; we keep
|
|
434
|
+
// streaming so additive AI SDK upgrades do not break callers.
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!emittedStart) yield startEvent();
|
|
440
|
+
yield {
|
|
441
|
+
type: 'finish',
|
|
442
|
+
// W-023: a stream that surfaced an error event must not end with a
|
|
443
|
+
// synthetic 'stop' + zero usage - parity with llamacpp-node (PS-4).
|
|
444
|
+
finishReason: sawError ? 'error' : finishReason,
|
|
445
|
+
usage: finalUsage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function generateFromVercel(
|
|
450
|
+
model: LanguageModelLike,
|
|
451
|
+
providerName: string,
|
|
452
|
+
capabilities: ProviderCapabilities,
|
|
453
|
+
req: ProviderRequest,
|
|
454
|
+
overrides: VercelRuntimeOverrides | undefined,
|
|
455
|
+
): Promise<ProviderResponse> {
|
|
456
|
+
const sdk = await loadRuntime(overrides);
|
|
457
|
+
const callArgs = buildCallArgs(model, applyRequestPreflight(req, capabilities));
|
|
458
|
+
let result: Awaited<ReturnType<VercelRuntimeOverrides['generateText']>>;
|
|
459
|
+
try {
|
|
460
|
+
result = await sdk.generateText(callArgs);
|
|
461
|
+
} catch (cause) {
|
|
462
|
+
const headers = headersFromCause(cause);
|
|
463
|
+
throw new ProviderHttpError({
|
|
464
|
+
providerName,
|
|
465
|
+
status: statusFromCause(cause),
|
|
466
|
+
message: 'generateText() rejected',
|
|
467
|
+
cause,
|
|
468
|
+
...(headers !== undefined ? { headers } : {}),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
const usage = mapUsage(result.usage) ?? {
|
|
472
|
+
promptTokens: 0,
|
|
473
|
+
completionTokens: 0,
|
|
474
|
+
totalTokens: 0,
|
|
475
|
+
};
|
|
476
|
+
const finishReason = mapFinishReason(result.finishReason);
|
|
477
|
+
const response: ProviderResponse = {
|
|
478
|
+
usage,
|
|
479
|
+
finishReason,
|
|
480
|
+
...(result.text !== undefined ? { text: result.text } : {}),
|
|
481
|
+
...(result.toolCalls !== undefined
|
|
482
|
+
? { toolCalls: result.toolCalls.map(normalizeToolCall) }
|
|
483
|
+
: {}),
|
|
484
|
+
...(result.providerMetadata !== undefined ? { providerMetadata: result.providerMetadata } : {}),
|
|
485
|
+
};
|
|
486
|
+
return response;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function applyRequestPreflight(
|
|
490
|
+
req: ProviderRequest,
|
|
491
|
+
capabilities: ProviderCapabilities,
|
|
492
|
+
): ProviderRequest {
|
|
493
|
+
const retention = resolveReasoningRetention({
|
|
494
|
+
...(req.reasoningRetention !== undefined ? { requested: req.reasoningRetention } : {}),
|
|
495
|
+
...(capabilities.reasoningContract !== undefined
|
|
496
|
+
? { contract: capabilities.reasoningContract }
|
|
497
|
+
: {}),
|
|
498
|
+
});
|
|
499
|
+
if (retention === 'pass-through-all') return req;
|
|
500
|
+
const filtered = applyReasoningPolicy({ messages: req.messages, retention });
|
|
501
|
+
if (filtered === req.messages) return req;
|
|
502
|
+
return { ...req, messages: filtered };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function buildCallArgs(model: LanguageModelLike, req: ProviderRequest) {
|
|
506
|
+
const prompt = toAiSdkPrompt(req.messages);
|
|
507
|
+
// System-role transcript messages are hoisted here - the SDK rejects
|
|
508
|
+
// them inside `messages`. The request-level systemMessage leads.
|
|
509
|
+
const system = [req.systemMessage, prompt.system]
|
|
510
|
+
.filter((s): s is string => s !== undefined && s.length > 0)
|
|
511
|
+
.join('\n\n');
|
|
512
|
+
const messages =
|
|
513
|
+
req.cachePolicy?.breakpoints === 'auto'
|
|
514
|
+
? applyCacheAnchors(prompt.messages, req.cachePolicy.ttl)
|
|
515
|
+
: prompt.messages;
|
|
516
|
+
return {
|
|
517
|
+
model,
|
|
518
|
+
messages,
|
|
519
|
+
...(system.length > 0 ? { system } : {}),
|
|
520
|
+
// C2: fold worked examples in the adapter itself, so raw-adapter use
|
|
521
|
+
// (no createProvider wrapper) still puts them in front of the model.
|
|
522
|
+
// Idempotent: an upstream fold already dropped the structured field.
|
|
523
|
+
...(req.tools !== undefined && req.tools.length > 0
|
|
524
|
+
? { tools: toAiSdkTools(foldToolExamples(req.tools)) }
|
|
525
|
+
: {}),
|
|
526
|
+
...(req.toolChoice !== undefined ? { toolChoice: toAiSdkToolChoice(req.toolChoice) } : {}),
|
|
527
|
+
...(req.temperature !== undefined ? { temperature: req.temperature } : {}),
|
|
528
|
+
// v4 reads `maxTokens`; v7 renamed it `maxOutputTokens` - send both
|
|
529
|
+
// so the cap is honoured against either peer (PS-6).
|
|
530
|
+
...(req.maxTokens !== undefined
|
|
531
|
+
? { maxTokens: req.maxTokens, maxOutputTokens: req.maxTokens }
|
|
532
|
+
: {}),
|
|
533
|
+
...(req.signal !== undefined ? { abortSignal: req.signal } : {}),
|
|
534
|
+
...(req.providerOptions !== undefined ? { providerOptions: req.providerOptions } : {}),
|
|
535
|
+
} as const;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function pickString(value: unknown): string | undefined {
|
|
539
|
+
return typeof value === 'string' ? value : undefined;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Normalize a generate() tool call across peers: AI SDK v4 carries
|
|
544
|
+
* `args`, v7 carries `input` (PS-6). The framework shape is `args`.
|
|
545
|
+
*/
|
|
546
|
+
function normalizeToolCall(tc: unknown): { toolCallId: string; toolName: string; args: unknown } {
|
|
547
|
+
const t = tc as {
|
|
548
|
+
toolCallId?: unknown;
|
|
549
|
+
id?: unknown;
|
|
550
|
+
toolName?: unknown;
|
|
551
|
+
args?: unknown;
|
|
552
|
+
input?: unknown;
|
|
553
|
+
};
|
|
554
|
+
return {
|
|
555
|
+
toolCallId: pickString(t.toolCallId) ?? pickString(t.id) ?? '',
|
|
556
|
+
toolName: pickString(t.toolName) ?? '',
|
|
557
|
+
args: t.args ?? t.input,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Lift a real HTTP status from a rejected AI SDK call (PS-2). The SDK's
|
|
563
|
+
* `APICallError` carries a numeric `statusCode`; surfacing it lets
|
|
564
|
+
* `withRetry` / `withFallback` see a genuine 429 / 5xx instead of the
|
|
565
|
+
* `status: 0` network-error placeholder. Returns `0` when no status is
|
|
566
|
+
* present (a true transport-level failure or an abort) - `0` is itself
|
|
567
|
+
* retryable / fallback-eligible by default, while an abort is excluded by
|
|
568
|
+
* the predicates via the wrapped `cause`.
|
|
569
|
+
*/
|
|
570
|
+
function statusFromCause(cause: unknown): number {
|
|
571
|
+
if (cause !== null && typeof cause === 'object') {
|
|
572
|
+
const c = cause as { statusCode?: unknown; status?: unknown };
|
|
573
|
+
if (typeof c.statusCode === 'number' && Number.isFinite(c.statusCode)) return c.statusCode;
|
|
574
|
+
if (typeof c.status === 'number' && Number.isFinite(c.status)) return c.status;
|
|
575
|
+
}
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Lift backoff-relevant response headers from a rejected AI SDK call.
|
|
581
|
+
* `APICallError` carries `responseHeaders: Record<string, string>`;
|
|
582
|
+
* forwarding `retry-after` / `x-ratelimit-*` lets `withRetry` honour
|
|
583
|
+
* server-provided delays on 429s.
|
|
584
|
+
*/
|
|
585
|
+
function headersFromCause(cause: unknown): Readonly<Record<string, string>> | undefined {
|
|
586
|
+
if (cause === null || typeof cause !== 'object') return undefined;
|
|
587
|
+
const raw = (cause as { responseHeaders?: unknown }).responseHeaders;
|
|
588
|
+
if (raw === null || typeof raw !== 'object') return undefined;
|
|
589
|
+
const picked: Record<string, string> = {};
|
|
590
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
591
|
+
const lower = key.toLowerCase();
|
|
592
|
+
if (
|
|
593
|
+
(lower === 'retry-after' || lower.startsWith('x-ratelimit-')) &&
|
|
594
|
+
typeof value === 'string'
|
|
595
|
+
) {
|
|
596
|
+
picked[lower] = value;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return Object.keys(picked).length > 0 ? picked : undefined;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function mapFinishReason(value: string | undefined): FinishReason {
|
|
603
|
+
switch (value) {
|
|
604
|
+
case 'stop':
|
|
605
|
+
case 'length':
|
|
606
|
+
case 'tool-calls':
|
|
607
|
+
case 'content-filter':
|
|
608
|
+
case 'error':
|
|
609
|
+
return value;
|
|
610
|
+
case 'aborted':
|
|
611
|
+
case 'cancelled':
|
|
612
|
+
return 'aborted';
|
|
613
|
+
default:
|
|
614
|
+
return 'stop';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function mapUsage(input: unknown): Usage | undefined {
|
|
619
|
+
if (input === undefined || input === null || typeof input !== 'object') return undefined;
|
|
620
|
+
const u = input as {
|
|
621
|
+
promptTokens?: number;
|
|
622
|
+
completionTokens?: number;
|
|
623
|
+
inputTokens?: number;
|
|
624
|
+
outputTokens?: number;
|
|
625
|
+
reasoningTokens?: number;
|
|
626
|
+
totalTokens?: number;
|
|
627
|
+
// v7 runtime-normalized detail splits (core-provider-02): inputTokens
|
|
628
|
+
// is the TOTAL including cache reads/writes; outputTokens the TOTAL
|
|
629
|
+
// including reasoning.
|
|
630
|
+
inputTokenDetails?: {
|
|
631
|
+
cacheReadTokens?: number;
|
|
632
|
+
cacheWriteTokens?: number;
|
|
633
|
+
noCacheTokens?: number;
|
|
634
|
+
};
|
|
635
|
+
outputTokenDetails?: { reasoningTokens?: number; textTokens?: number };
|
|
636
|
+
};
|
|
637
|
+
const promptTokens = u.promptTokens ?? u.inputTokens ?? 0;
|
|
638
|
+
const rawCompletion = u.completionTokens ?? u.outputTokens ?? 0;
|
|
639
|
+
const totalTokens = u.totalTokens ?? promptTokens + rawCompletion;
|
|
640
|
+
const cachedReadTokens = numberOrUndefined(u.inputTokenDetails?.cacheReadTokens);
|
|
641
|
+
const cacheWriteTokens = numberOrUndefined(u.inputTokenDetails?.cacheWriteTokens);
|
|
642
|
+
// The v7 detail reports reasoning as a SUBSET of outputTokens; core Usage
|
|
643
|
+
// declares reasoningTokens EXCLUSIVE of completionTokens, so split the
|
|
644
|
+
// total (sum unchanged). The flat field (v4/v5 peers) is already exclusive.
|
|
645
|
+
const detailReasoning = numberOrUndefined(u.outputTokenDetails?.reasoningTokens);
|
|
646
|
+
let completionTokens = rawCompletion;
|
|
647
|
+
let reasoningTokens = u.reasoningTokens;
|
|
648
|
+
if (reasoningTokens === undefined && detailReasoning !== undefined && detailReasoning > 0) {
|
|
649
|
+
reasoningTokens = detailReasoning;
|
|
650
|
+
completionTokens = Math.max(0, rawCompletion - detailReasoning);
|
|
651
|
+
}
|
|
652
|
+
const usage: Usage = {
|
|
653
|
+
promptTokens,
|
|
654
|
+
completionTokens,
|
|
655
|
+
totalTokens,
|
|
656
|
+
...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
|
|
657
|
+
...(cachedReadTokens !== undefined && cachedReadTokens > 0 ? { cachedReadTokens } : {}),
|
|
658
|
+
...(cacheWriteTokens !== undefined && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}),
|
|
659
|
+
};
|
|
660
|
+
return usage;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function numberOrUndefined(value: unknown): number | undefined {
|
|
664
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let cachedRuntime: VercelRuntimeOverrides | null = null;
|
|
668
|
+
|
|
669
|
+
async function loadRuntime(
|
|
670
|
+
overrides: VercelRuntimeOverrides | undefined,
|
|
671
|
+
): Promise<VercelRuntimeOverrides> {
|
|
672
|
+
if (overrides !== undefined) return overrides;
|
|
673
|
+
if (cachedRuntime !== null) return cachedRuntime;
|
|
674
|
+
let mod: { streamText?: unknown; generateText?: unknown };
|
|
675
|
+
try {
|
|
676
|
+
mod = (await import('ai')) as { streamText?: unknown; generateText?: unknown };
|
|
677
|
+
} catch (cause) {
|
|
678
|
+
throw new ProviderStreamParseError(
|
|
679
|
+
'vercel',
|
|
680
|
+
"Failed to import the 'ai' peer dependency. Install it with `pnpm add ai` or pass a runtimeOverrides value.",
|
|
681
|
+
cause,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
if (typeof mod.streamText !== 'function' || typeof mod.generateText !== 'function') {
|
|
685
|
+
throw new ProviderStreamParseError(
|
|
686
|
+
'vercel',
|
|
687
|
+
"The installed 'ai' package does not expose streamText / generateText functions.",
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
cachedRuntime = {
|
|
691
|
+
streamText: mod.streamText as VercelRuntimeOverrides['streamText'],
|
|
692
|
+
generateText: mod.generateText as VercelRuntimeOverrides['generateText'],
|
|
693
|
+
};
|
|
694
|
+
return cachedRuntime;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Test-only hook that resets the cached AI SDK runtime. Provider tests
|
|
699
|
+
* that mutate the cache (e.g. by injecting a mock then verifying the
|
|
700
|
+
* default loader runs) call this between scenarios.
|
|
701
|
+
*
|
|
702
|
+
* @internal
|
|
703
|
+
*/
|
|
704
|
+
export function __resetVercelRuntimeCache(): void {
|
|
705
|
+
cachedRuntime = null;
|
|
706
|
+
}
|