@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,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversion layer between Graphorin shapes and the Vercel AI SDK
|
|
3
|
+
* call contract (core-provider-01).
|
|
4
|
+
*
|
|
5
|
+
* The AI SDK does NOT accept Graphorin messages or tool definitions
|
|
6
|
+
* verbatim:
|
|
7
|
+
*
|
|
8
|
+
* - `tools` must be a `Record<string, Tool>` keyed by tool name whose
|
|
9
|
+
* `inputSchema` is a zod / Standard Schema value or a `jsonSchema()`
|
|
10
|
+
* wrapped `Schema` object. A `ToolDefinition[]` array advertises
|
|
11
|
+
* tools under index keys (`"0"`, `"1"`, ...) with schemas the SDK
|
|
12
|
+
* cannot consume.
|
|
13
|
+
* - Assistant tool calls must be `{type: 'tool-call'}` *content parts*
|
|
14
|
+
* and tool results `{role: 'tool', content: [{type: 'tool-result'}]}`
|
|
15
|
+
* messages; Graphorin's separate `toolCalls` field and string-content
|
|
16
|
+
* `ToolMessage` fail the SDK's `standardizePrompt` zod validation
|
|
17
|
+
* (`AI_InvalidPromptError`) on the second step of any tool loop.
|
|
18
|
+
*
|
|
19
|
+
* Where the AI SDK renamed keys between major versions the converters
|
|
20
|
+
* emit BOTH spellings (PS-6 dual-shape): `input` + `args` on tool-call
|
|
21
|
+
* parts, `output` + `result` on tool-result parts, `mediaType` +
|
|
22
|
+
* `mimeType` on binary parts, `inputSchema` + `parameters` on tools.
|
|
23
|
+
* Both peers zod-strip the key they do not know.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { Message, MessageContent, ToolChoice, ToolDefinition } from '@graphorin/core';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Registered symbols used by the AI SDK's `asSchema()` detection. They
|
|
32
|
+
* are created with `Symbol.for(...)` in the SDK, so a structurally
|
|
33
|
+
* hand-rolled wrapper is indistinguishable from the SDK's own
|
|
34
|
+
* `jsonSchema()` output - including across duplicated copies of the
|
|
35
|
+
* SDK in a node_modules tree.
|
|
36
|
+
*/
|
|
37
|
+
const AI_SDK_SCHEMA_SYMBOL = Symbol.for('vercel.ai.schema');
|
|
38
|
+
const AI_SDK_VALIDATOR_SYMBOL = Symbol.for('vercel.ai.validator');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wrap a plain JSON Schema record into the AI SDK `Schema` shape the
|
|
42
|
+
* SDK's `asSchema()` accepts. Equivalent to importing `jsonSchema`
|
|
43
|
+
* from `ai`, but hand-rolled so the converter works with fixture
|
|
44
|
+
* runtimes and does not force the optional peer at module scope.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function wrapAsAiSdkSchema(
|
|
49
|
+
schema: Readonly<Record<string, unknown>>,
|
|
50
|
+
): Readonly<Record<string, unknown>> {
|
|
51
|
+
return {
|
|
52
|
+
[AI_SDK_SCHEMA_SYMBOL]: true,
|
|
53
|
+
[AI_SDK_VALIDATOR_SYMBOL]: true,
|
|
54
|
+
_type: undefined,
|
|
55
|
+
jsonSchema: schema,
|
|
56
|
+
validate: undefined,
|
|
57
|
+
} as Readonly<Record<string, unknown>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Convert Graphorin `ToolDefinition[]` to the AI SDK's name-keyed tool
|
|
62
|
+
* record.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export function toAiSdkTools(
|
|
67
|
+
tools: ReadonlyArray<ToolDefinition>,
|
|
68
|
+
): Readonly<Record<string, unknown>> {
|
|
69
|
+
const record: Record<string, unknown> = {};
|
|
70
|
+
for (const t of tools) {
|
|
71
|
+
const schema = wrapAsAiSdkSchema(t.inputSchema);
|
|
72
|
+
record[t.name] = {
|
|
73
|
+
...(t.description !== undefined ? { description: t.description } : {}),
|
|
74
|
+
// v5+/v7 reads `inputSchema`; v4 reads `parameters` (PS-6).
|
|
75
|
+
inputSchema: schema,
|
|
76
|
+
parameters: schema,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return record;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert a Graphorin `ToolChoice` to the AI SDK's spelling. The
|
|
84
|
+
* string modes are identical; the named-tool form differs
|
|
85
|
+
* (`{tool: name}` vs `{type: 'tool', toolName: name}`).
|
|
86
|
+
*
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
export function toAiSdkToolChoice(choice: ToolChoice): unknown {
|
|
90
|
+
if (choice === 'auto' || choice === 'required' || choice === 'none') return choice;
|
|
91
|
+
return { type: 'tool', toolName: choice.tool };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Result of {@link toAiSdkPrompt}: the converted message array plus the
|
|
96
|
+
* hoisted system text (the SDK rejects `role: 'system'` entries inside
|
|
97
|
+
* `messages` - system prompts ride the request-level option).
|
|
98
|
+
*
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
export interface AiSdkPrompt {
|
|
102
|
+
readonly messages: ReadonlyArray<Readonly<Record<string, unknown>>>;
|
|
103
|
+
readonly system?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert Graphorin messages to AI SDK `ModelMessage`s:
|
|
108
|
+
*
|
|
109
|
+
* - system-role messages are hoisted into `system` (v7 rejects them
|
|
110
|
+
* inside the `messages` array outright),
|
|
111
|
+
* - assistant `toolCalls` become `tool-call` content parts,
|
|
112
|
+
* - `ToolMessage`s become `{role: 'tool'}` messages carrying a
|
|
113
|
+
* `tool-result` part whose `toolName` is recovered from the paired
|
|
114
|
+
* assistant call earlier in the transcript,
|
|
115
|
+
* - binary content parts are renamed onto the SDK's part vocabulary
|
|
116
|
+
* (`file` parts carry `data`; audio has no dedicated user part and
|
|
117
|
+
* rides as a `file` part).
|
|
118
|
+
*
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export function toAiSdkPrompt(messages: ReadonlyArray<Message>): AiSdkPrompt {
|
|
122
|
+
const out: Array<Readonly<Record<string, unknown>>> = [];
|
|
123
|
+
const systemParts: string[] = [];
|
|
124
|
+
const toolNameById = new Map<string, string>();
|
|
125
|
+
for (const msg of messages) {
|
|
126
|
+
switch (msg.role) {
|
|
127
|
+
case 'system':
|
|
128
|
+
if (msg.content.length > 0) systemParts.push(msg.content);
|
|
129
|
+
break;
|
|
130
|
+
case 'user':
|
|
131
|
+
out.push({
|
|
132
|
+
role: 'user',
|
|
133
|
+
content: typeof msg.content === 'string' ? msg.content : msg.content.flatMap(toUserPart),
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
case 'assistant': {
|
|
137
|
+
for (const call of msg.toolCalls ?? []) {
|
|
138
|
+
toolNameById.set(call.toolCallId, call.toolName);
|
|
139
|
+
}
|
|
140
|
+
const hasCalls = msg.toolCalls !== undefined && msg.toolCalls.length > 0;
|
|
141
|
+
if (!hasCalls && typeof msg.content === 'string') {
|
|
142
|
+
out.push({ role: 'assistant', content: msg.content });
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
const parts: Array<Readonly<Record<string, unknown>>> = [];
|
|
146
|
+
if (typeof msg.content === 'string') {
|
|
147
|
+
if (msg.content.length > 0) parts.push({ type: 'text', text: msg.content });
|
|
148
|
+
} else {
|
|
149
|
+
for (const part of msg.content) parts.push(...toAssistantPart(part));
|
|
150
|
+
}
|
|
151
|
+
for (const call of msg.toolCalls ?? []) {
|
|
152
|
+
parts.push({
|
|
153
|
+
type: 'tool-call',
|
|
154
|
+
toolCallId: call.toolCallId,
|
|
155
|
+
toolName: call.toolName,
|
|
156
|
+
// v5+/v7 reads `input`; v4 reads `args` (PS-6).
|
|
157
|
+
input: call.args,
|
|
158
|
+
args: call.args,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
out.push({ role: 'assistant', content: parts });
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'tool': {
|
|
165
|
+
const text = flattenToText(msg.content);
|
|
166
|
+
out.push({
|
|
167
|
+
role: 'tool',
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: 'tool-result',
|
|
171
|
+
toolCallId: msg.toolCallId,
|
|
172
|
+
toolName: toolNameById.get(msg.toolCallId) ?? 'unknown_tool',
|
|
173
|
+
// v5+/v7 reads `output`; v4 reads `result` (PS-6).
|
|
174
|
+
output: { type: 'text', value: text },
|
|
175
|
+
result: text,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
messages: out,
|
|
185
|
+
...(systemParts.length > 0 ? { system: systemParts.join('\n\n') } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toUserPart(part: MessageContent): ReadonlyArray<Readonly<Record<string, unknown>>> {
|
|
190
|
+
switch (part.type) {
|
|
191
|
+
case 'text':
|
|
192
|
+
return [{ type: 'text', text: part.text }];
|
|
193
|
+
case 'image':
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
type: 'image',
|
|
197
|
+
image: part.image,
|
|
198
|
+
// v5+/v7 reads `mediaType`; v4 reads `mimeType` (PS-6).
|
|
199
|
+
...(part.mimeType !== undefined
|
|
200
|
+
? { mediaType: part.mimeType, mimeType: part.mimeType }
|
|
201
|
+
: {}),
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
case 'audio':
|
|
205
|
+
return [filePart(part.audio, part.mimeType ?? 'application/octet-stream')];
|
|
206
|
+
case 'file':
|
|
207
|
+
return [filePart(part.file, part.mimeType, part.filename)];
|
|
208
|
+
case 'reasoning':
|
|
209
|
+
// Reasoning belongs to assistant turns; a stray part in user
|
|
210
|
+
// content has no SDK representation. Drop it.
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function toAssistantPart(part: MessageContent): ReadonlyArray<Readonly<Record<string, unknown>>> {
|
|
216
|
+
switch (part.type) {
|
|
217
|
+
case 'text':
|
|
218
|
+
return part.text.length > 0 ? [{ type: 'text', text: part.text }] : [];
|
|
219
|
+
case 'reasoning': {
|
|
220
|
+
const meta = part.meta;
|
|
221
|
+
const anthropic: Record<string, unknown> = {};
|
|
222
|
+
if (meta?.provider === 'anthropic') {
|
|
223
|
+
if (typeof meta.signature === 'string') anthropic.signature = meta.signature;
|
|
224
|
+
if (typeof meta.data === 'string') anthropic.redactedData = meta.data;
|
|
225
|
+
}
|
|
226
|
+
return [
|
|
227
|
+
{
|
|
228
|
+
type: 'reasoning',
|
|
229
|
+
text: part.text,
|
|
230
|
+
...(Object.keys(anthropic).length > 0 ? { providerOptions: { anthropic } } : {}),
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
case 'image':
|
|
235
|
+
return [filePart(part.image, part.mimeType ?? 'image/png')];
|
|
236
|
+
case 'audio':
|
|
237
|
+
return [filePart(part.audio, part.mimeType ?? 'application/octet-stream')];
|
|
238
|
+
case 'file':
|
|
239
|
+
return [filePart(part.file, part.mimeType, part.filename)];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function filePart(
|
|
244
|
+
data: Uint8Array | URL,
|
|
245
|
+
mimeType: string,
|
|
246
|
+
filename?: string,
|
|
247
|
+
): Readonly<Record<string, unknown>> {
|
|
248
|
+
return {
|
|
249
|
+
type: 'file',
|
|
250
|
+
data,
|
|
251
|
+
// v5+/v7 reads `mediaType`; v4 reads `mimeType` (PS-6).
|
|
252
|
+
mediaType: mimeType,
|
|
253
|
+
mimeType,
|
|
254
|
+
...(filename !== undefined ? { filename } : {}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function flattenToText(content: string | ReadonlyArray<MessageContent>): string {
|
|
259
|
+
if (typeof content === 'string') return content;
|
|
260
|
+
const buffer: string[] = [];
|
|
261
|
+
for (const part of content) {
|
|
262
|
+
if (part.type === 'text' || part.type === 'reasoning') buffer.push(part.text);
|
|
263
|
+
}
|
|
264
|
+
return buffer.join('');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Decorate the first and last converted conversation messages with an
|
|
269
|
+
* Anthropic `cacheControl` provider option (core-provider-02).
|
|
270
|
+
*
|
|
271
|
+
* The first-message anchor makes tools + system + the stable prefix a
|
|
272
|
+
* cache segment that is written once and read on every later step; the
|
|
273
|
+
* last-message anchor caches the whole conversation so each step's write
|
|
274
|
+
* becomes the next step's read. Non-Anthropic models ignore the option.
|
|
275
|
+
* A single-message conversation gets one anchor.
|
|
276
|
+
*/
|
|
277
|
+
export function applyCacheAnchors(
|
|
278
|
+
messages: ReadonlyArray<Readonly<Record<string, unknown>>>,
|
|
279
|
+
ttl?: '5m' | '1h',
|
|
280
|
+
): ReadonlyArray<Readonly<Record<string, unknown>>> {
|
|
281
|
+
if (messages.length === 0) return messages;
|
|
282
|
+
const cacheControl: Record<string, unknown> = {
|
|
283
|
+
type: 'ephemeral',
|
|
284
|
+
...(ttl === '1h' ? { ttl: '1h' } : {}),
|
|
285
|
+
};
|
|
286
|
+
const decorate = (msg: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> => {
|
|
287
|
+
const existing =
|
|
288
|
+
typeof msg.providerOptions === 'object' && msg.providerOptions !== null
|
|
289
|
+
? (msg.providerOptions as Record<string, unknown>)
|
|
290
|
+
: {};
|
|
291
|
+
const anthropic =
|
|
292
|
+
typeof existing.anthropic === 'object' && existing.anthropic !== null
|
|
293
|
+
? (existing.anthropic as Record<string, unknown>)
|
|
294
|
+
: {};
|
|
295
|
+
return {
|
|
296
|
+
...msg,
|
|
297
|
+
providerOptions: { ...existing, anthropic: { ...anthropic, cacheControl } },
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
if (messages.length === 1) {
|
|
301
|
+
const only = messages[0];
|
|
302
|
+
return only === undefined ? messages : [decorate(only)];
|
|
303
|
+
}
|
|
304
|
+
const first = messages[0];
|
|
305
|
+
const last = messages[messages.length - 1];
|
|
306
|
+
if (first === undefined || last === undefined) return messages;
|
|
307
|
+
return [decorate(first), ...messages.slice(1, -1), decorate(last)];
|
|
308
|
+
}
|