@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +8 -3
  3. package/dist/adapters/llamacpp-server.d.ts +1 -1
  4. package/dist/adapters/llamacpp-server.js.map +1 -1
  5. package/dist/adapters/ollama.d.ts.map +1 -1
  6. package/dist/adapters/ollama.js +19 -6
  7. package/dist/adapters/ollama.js.map +1 -1
  8. package/dist/adapters/vercel.d.ts +1 -1
  9. package/dist/adapters/vercel.d.ts.map +1 -1
  10. package/dist/adapters/vercel.js +111 -33
  11. package/dist/adapters/vercel.js.map +1 -1
  12. package/dist/errors/errors.d.ts +1 -1
  13. package/dist/errors/errors.js +1 -1
  14. package/dist/errors/errors.js.map +1 -1
  15. package/dist/index.d.ts +1 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -8
  18. package/dist/index.js.map +1 -1
  19. package/dist/internal/http.js +111 -7
  20. package/dist/internal/http.js.map +1 -1
  21. package/dist/internal/openai-shaped.js +21 -4
  22. package/dist/internal/openai-shaped.js.map +1 -1
  23. package/dist/middleware/with-fallback.js +1 -1
  24. package/dist/middleware/with-rate-limit.d.ts +21 -8
  25. package/dist/middleware/with-rate-limit.d.ts.map +1 -1
  26. package/dist/middleware/with-rate-limit.js +65 -12
  27. package/dist/middleware/with-rate-limit.js.map +1 -1
  28. package/dist/middleware/with-retry.js +1 -1
  29. package/dist/package.js +6 -0
  30. package/dist/package.js.map +1 -0
  31. package/package.json +18 -16
  32. package/src/adapters/index.ts +14 -0
  33. package/src/adapters/llamacpp-server.ts +102 -0
  34. package/src/adapters/ollama.ts +382 -0
  35. package/src/adapters/openai-compatible.ts +95 -0
  36. package/src/adapters/vercel-messages.ts +308 -0
  37. package/src/adapters/vercel.ts +706 -0
  38. package/src/counters/anthropic-wire.ts +199 -0
  39. package/src/counters/anthropic.ts +114 -0
  40. package/src/counters/bedrock.ts +46 -0
  41. package/src/counters/dispatcher.ts +127 -0
  42. package/src/counters/global.ts +39 -0
  43. package/src/counters/google.ts +46 -0
  44. package/src/counters/heuristic.ts +107 -0
  45. package/src/counters/index.ts +35 -0
  46. package/src/counters/js-tiktoken.ts +135 -0
  47. package/src/counters/serialize.ts +85 -0
  48. package/src/errors/errors.ts +316 -0
  49. package/src/errors/index.ts +20 -0
  50. package/src/index.ts +42 -0
  51. package/src/internal/abort.ts +30 -0
  52. package/src/internal/http.ts +388 -0
  53. package/src/internal/openai-shaped.ts +555 -0
  54. package/src/internal/sse.ts +112 -0
  55. package/src/internal/url-utils.ts +20 -0
  56. package/src/middleware/compose.ts +213 -0
  57. package/src/middleware/index.ts +37 -0
  58. package/src/middleware/production-hook.ts +47 -0
  59. package/src/middleware/with-cost-limit.ts +131 -0
  60. package/src/middleware/with-cost-tracking.ts +216 -0
  61. package/src/middleware/with-fallback.ts +157 -0
  62. package/src/middleware/with-rate-limit.ts +306 -0
  63. package/src/middleware/with-redaction.ts +671 -0
  64. package/src/middleware/with-retry.ts +274 -0
  65. package/src/middleware/with-tracing.ts +117 -0
  66. package/src/model-tier/classify.ts +125 -0
  67. package/src/model-tier/index.ts +11 -0
  68. package/src/provider.ts +121 -0
  69. package/src/reasoning/apply-policy.ts +89 -0
  70. package/src/reasoning/classify-contract.ts +120 -0
  71. package/src/reasoning/index.ts +21 -0
  72. package/src/reasoning/retention.ts +64 -0
  73. package/src/tool-examples.ts +54 -0
  74. package/src/trust/classify-local-provider.ts +254 -0
  75. package/src/trust/index.ts +14 -0
  76. 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
+ }