@templatical/ai-kit 0.0.0-stage → 0.51.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/LICENSE +21 -0
- package/README.md +63 -2
- package/dist/adapters/anthropic.d.ts +31 -0
- package/dist/adapters/anthropic.js +139 -0
- package/dist/adapters/anthropic.js.map +1 -0
- package/dist/adapters/openai.d.ts +29 -0
- package/dist/adapters/openai.js +131 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/adapters/vercel.d.ts +17 -0
- package/dist/adapters/vercel.js +137 -0
- package/dist/adapters/vercel.js.map +1 -0
- package/dist/checks-DvLD3892.js +656 -0
- package/dist/checks-DvLD3892.js.map +1 -0
- package/dist/client/index.d.ts +24 -0
- package/dist/client/index.js +3 -0
- package/dist/errors-DWshWx18.js +55 -0
- package/dist/errors-DWshWx18.js.map +1 -0
- package/dist/http-provider-Cb41NXd5.d.ts +22 -0
- package/dist/http-provider-Cx2ZcLRB.js +278 -0
- package/dist/http-provider-Cx2ZcLRB.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/model-xJvGTfLB.d.ts +84 -0
- package/dist/server/index.d.ts +313 -0
- package/dist/server/index.js +355 -0
- package/dist/server/index.js.map +1 -0
- package/dist/shared-BuhccY6c.js +61 -0
- package/dist/shared-BuhccY6c.js.map +1 -0
- package/dist/testing/index.d.ts +277 -0
- package/dist/testing/index.js +2445 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/wire-B6AF_UGT.js +132 -0
- package/dist/wire-B6AF_UGT.js.map +1 -0
- package/dist/wire-Cv7Fk-7n.d.ts +136 -0
- package/eval/MANIFEST.sha256 +23 -0
- package/eval/cases.json +287 -0
- package/eval/designs/newsletter.png +0 -0
- package/eval/designs/one-page.pdf +0 -0
- package/eval/designs/promo.png +0 -0
- package/eval/domain-mismatch.json +25 -0
- package/eval/empty.json +11 -0
- package/eval/images/banner.png +0 -0
- package/eval/images/chart.png +0 -0
- package/eval/images/divider.png +0 -0
- package/eval/images/probe.png +0 -0
- package/eval/images/product.png +0 -0
- package/eval/injection.json +13 -0
- package/eval/low-text.json +41 -0
- package/eval/promo.json +12 -0
- package/eval/sources/banner.html +16 -0
- package/eval/sources/chart.html +16 -0
- package/eval/sources/divider.html +16 -0
- package/eval/sources/newsletter.html +16 -0
- package/eval/sources/one-page.html +16 -0
- package/eval/sources/probe.html +16 -0
- package/eval/sources/product.html +16 -0
- package/eval/sources/promo.html +16 -0
- package/eval/welcome.json +12 -0
- package/package.json +99 -3
- package/prompts/alt-text.md +7 -0
- package/prompts/chat-attachments.md +1 -0
- package/prompts/chat.md +2 -0
- package/prompts/editing.md +10 -0
- package/prompts/fix-rules.json +9 -0
- package/prompts/fix.md +8 -0
- package/prompts/operations.md +127 -0
- package/prompts/rewrite-presets.json +8 -0
- package/prompts/rewrite.md +4 -0
- package/schema/template-content.schema.json +1884 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Templatical
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @templatical/ai-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Bring your own AI backend to the [Templatical](https://templatical.com) email editor.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@templatical/ai-kit)
|
|
6
|
+
[](https://github.com/templatical/sdk/blob/main/LICENSE-MIT)
|
|
7
|
+
|
|
8
|
+
The editor's AI (the AI assistant with Undo and attachments, Rewrite, Fix with AI, Write alt text and static localized chat starters) runs on an `AiProvider` you pass to `init({ ai })`. This package is everything around that contract, with no LLM SDK and no Node-only API: it runs in Node, Deno, Bun, Workers and the browser.
|
|
9
|
+
|
|
10
|
+
| Entry | What it is |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `@templatical/ai-kit` | The wire: routes, headers, events, bodies, error statuses, limits; the contract types |
|
|
13
|
+
| `@templatical/ai-kit/client` | `createHttpAiProvider()`: an `AiProvider` over the wire, for the browser |
|
|
14
|
+
| `@templatical/ai-kit/server` | Prompts, the `apply_operations` tool, `createEditingSession`, `runEditingTurn`, `aiEventStream`, output checks |
|
|
15
|
+
| `@templatical/ai-kit/testing` | `runAiProviderConformance`, `runAiWireConformance`, `createMockAiProvider`, the test set and `runAiEvalSet` |
|
|
16
|
+
| `@templatical/ai-kit/prompts/*`, `/schema/*`, `/eval/*` | The prompt files, the JSON schemas and the test set, for backends in any language |
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @templatical/ai-kit
|
|
22
|
+
pnpm add @templatical/ai-kit
|
|
23
|
+
yarn add @templatical/ai-kit
|
|
24
|
+
bun add @templatical/ai-kit
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Choose the command for your package manager.
|
|
28
|
+
|
|
29
|
+
## In the editor
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { init } from "@templatical/editor";
|
|
33
|
+
import "@templatical/editor/style.css";
|
|
34
|
+
import { createHttpAiProvider } from "@templatical/ai-kit/client";
|
|
35
|
+
|
|
36
|
+
const editor = await init({
|
|
37
|
+
container: "#editor",
|
|
38
|
+
templates: {
|
|
39
|
+
load: async (id) => {
|
|
40
|
+
const response = await fetch(`/api/templates/${encodeURIComponent(id)}`);
|
|
41
|
+
if (!response.ok) throw new Error("Template load failed.");
|
|
42
|
+
return response.json();
|
|
43
|
+
},
|
|
44
|
+
create: false,
|
|
45
|
+
save: false,
|
|
46
|
+
},
|
|
47
|
+
ai: createHttpAiProvider({
|
|
48
|
+
baseUrl: "/api/ai",
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
await editor.load("tpl_123");
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
AI needs a loaded template: the AI assistant and other entry points appear after `editor.load(id)` or `editor.create()` resolves. Omit `ai` to leave the feature out. Fix with AI and Write alt text also require `@templatical/quality` and lint on. The AI kit is MIT; the editor UI is source-available under FSL-1.1-MIT.
|
|
55
|
+
|
|
56
|
+
## On your server
|
|
57
|
+
|
|
58
|
+
See [Bring your own AI backend](https://docs.templatical.com/guide/bring-your-own-ai) for a complete Fetch-API server, model adapters for the Vercel AI SDK, OpenAI and Anthropic, and the wire for other languages.
|
|
59
|
+
|
|
60
|
+
Model adapters: `@templatical/ai-kit/adapters/anthropic` (`createAnthropicAdapter({ client: new Anthropic(), model })`), `/adapters/openai` (`createOpenAiAdapter({ client: new OpenAI(), model })`) and `/adapters/vercel` (`createVercelAdapter({ model })`). Install the SDK of the one you import; they're optional peers.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { t as AiModelAdapter } from "../model-xJvGTfLB.js";
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
//#region src/adapters/anthropic.d.ts
|
|
4
|
+
type StreamParams = Anthropic.MessageStreamParams;
|
|
5
|
+
/** The one method the adapter calls: `new Anthropic()` fits it. */
|
|
6
|
+
export interface AnthropicAdapterClient {
|
|
7
|
+
messages: {
|
|
8
|
+
stream(body: StreamParams, options?: {
|
|
9
|
+
signal?: AbortSignal | null;
|
|
10
|
+
}): AsyncIterable<Anthropic.RawMessageStreamEvent> & {
|
|
11
|
+
finalMessage(): Promise<Anthropic.Message>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface AnthropicAdapterOptions {
|
|
16
|
+
/** A client you construct: `new Anthropic()`, or one with its own `baseURL`. */
|
|
17
|
+
client: AnthropicAdapterClient;
|
|
18
|
+
/** The model id. No default. */
|
|
19
|
+
model: string;
|
|
20
|
+
/** `max_tokens`. Default 16_000. */
|
|
21
|
+
maxTokens?: number;
|
|
22
|
+
/** Prompt caching: a breakpoint after the system prompt plus automatic caching. Default true. */
|
|
23
|
+
cache?: boolean;
|
|
24
|
+
/** Further request fields, e.g. `output_config` or `metadata`. */
|
|
25
|
+
params?: Omit<StreamParams, "model" | "system" | "messages" | "tools" | "max_tokens" | "stream" | "cache_control">;
|
|
26
|
+
}
|
|
27
|
+
/** An `AiModelAdapter` over the Anthropic Messages API. */
|
|
28
|
+
export declare function createAnthropicAdapter(options: AnthropicAdapterOptions): AiModelAdapter;
|
|
29
|
+
//#endregion
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { a as refused, i as providerError, n as cutOff, r as ownState, s as toolInput, t as blobBase64 } from "../shared-BuhccY6c.js";
|
|
2
|
+
//#region src/adapters/anthropic.ts
|
|
3
|
+
const ADAPTER = "anthropic";
|
|
4
|
+
const CUT_OFF = /* @__PURE__ */ new Set(["max_tokens", "model_context_window_exceeded"]);
|
|
5
|
+
const EPHEMERAL = { type: "ephemeral" };
|
|
6
|
+
function toTool(tool) {
|
|
7
|
+
return {
|
|
8
|
+
name: tool.name,
|
|
9
|
+
description: tool.description,
|
|
10
|
+
input_schema: tool.inputSchema
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function toBlock(attachment) {
|
|
14
|
+
if (attachment.kind === "image-url") return {
|
|
15
|
+
type: "image",
|
|
16
|
+
source: {
|
|
17
|
+
type: "url",
|
|
18
|
+
url: attachment.url
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const data = await blobBase64(attachment.data);
|
|
22
|
+
return attachment.kind === "pdf" ? {
|
|
23
|
+
type: "document",
|
|
24
|
+
source: {
|
|
25
|
+
type: "base64",
|
|
26
|
+
media_type: "application/pdf",
|
|
27
|
+
data
|
|
28
|
+
}
|
|
29
|
+
} : {
|
|
30
|
+
type: "image",
|
|
31
|
+
source: {
|
|
32
|
+
type: "base64",
|
|
33
|
+
media_type: attachment.mediaType,
|
|
34
|
+
data
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function toMessages(messages) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const message of messages) if (message.role === "tool") {
|
|
41
|
+
const result = {
|
|
42
|
+
type: "tool_result",
|
|
43
|
+
tool_use_id: message.toolCallId,
|
|
44
|
+
content: message.content
|
|
45
|
+
};
|
|
46
|
+
const last = out.at(-1);
|
|
47
|
+
if (last?.role === "user" && Array.isArray(last.content) && last.content.every((block) => block.type === "tool_result")) last.content.push(result);
|
|
48
|
+
else out.push({
|
|
49
|
+
role: "user",
|
|
50
|
+
content: [result]
|
|
51
|
+
});
|
|
52
|
+
} else if (message.role === "assistant") {
|
|
53
|
+
const content = ownState(message.state, ADAPTER) ?? [...message.content ? [{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: message.content
|
|
56
|
+
}] : [], ...(message.toolCalls ?? []).map((call) => ({
|
|
57
|
+
type: "tool_use",
|
|
58
|
+
id: call.id,
|
|
59
|
+
name: call.name,
|
|
60
|
+
input: toolInput(call.input)
|
|
61
|
+
}))];
|
|
62
|
+
if (content.length > 0) out.push({
|
|
63
|
+
role: "assistant",
|
|
64
|
+
content
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
const files = await Promise.all((message.attachments ?? []).map(toBlock));
|
|
68
|
+
out.push({
|
|
69
|
+
role: "user",
|
|
70
|
+
content: [...files, {
|
|
71
|
+
type: "text",
|
|
72
|
+
text: message.content
|
|
73
|
+
}]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** An `AiModelAdapter` over the Anthropic Messages API. */
|
|
79
|
+
function createAnthropicAdapter(options) {
|
|
80
|
+
const cache = options.cache ?? true;
|
|
81
|
+
return { async *stream({ system, messages, tools, signal }) {
|
|
82
|
+
const body = {
|
|
83
|
+
...options.params,
|
|
84
|
+
model: options.model,
|
|
85
|
+
max_tokens: options.maxTokens ?? 16e3,
|
|
86
|
+
system: [{
|
|
87
|
+
type: "text",
|
|
88
|
+
text: system,
|
|
89
|
+
...cache ? { cache_control: EPHEMERAL } : {}
|
|
90
|
+
}],
|
|
91
|
+
messages: await toMessages(messages),
|
|
92
|
+
...tools.length > 0 ? { tools: tools.map(toTool) } : {},
|
|
93
|
+
...cache ? { cache_control: EPHEMERAL } : {}
|
|
94
|
+
};
|
|
95
|
+
let final;
|
|
96
|
+
try {
|
|
97
|
+
const stream = options.client.messages.stream(body, { signal });
|
|
98
|
+
for await (const event of stream) {
|
|
99
|
+
signal?.throwIfAborted();
|
|
100
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") yield {
|
|
101
|
+
type: "text",
|
|
102
|
+
text: event.delta.text
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
final = await stream.finalMessage();
|
|
106
|
+
signal?.throwIfAborted();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw providerError(error);
|
|
109
|
+
}
|
|
110
|
+
if (final.stop_reason === "refusal") throw refused();
|
|
111
|
+
const calls = final.content.filter((block) => block.type === "tool_use");
|
|
112
|
+
if (calls.length > 0 && final.stop_reason !== null && CUT_OFF.has(final.stop_reason)) throw cutOff();
|
|
113
|
+
yield {
|
|
114
|
+
type: "state",
|
|
115
|
+
state: {
|
|
116
|
+
adapter: ADAPTER,
|
|
117
|
+
content: final.content
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
for (const block of calls) yield {
|
|
121
|
+
type: "tool-call",
|
|
122
|
+
id: block.id,
|
|
123
|
+
name: block.name,
|
|
124
|
+
input: block.input
|
|
125
|
+
};
|
|
126
|
+
const usage = final.usage;
|
|
127
|
+
yield {
|
|
128
|
+
type: "usage",
|
|
129
|
+
inputTokens: usage.input_tokens,
|
|
130
|
+
outputTokens: usage.output_tokens,
|
|
131
|
+
...usage.cache_read_input_tokens != null ? { cacheReadInputTokens: usage.cache_read_input_tokens } : {},
|
|
132
|
+
...usage.cache_creation_input_tokens != null ? { cacheWriteInputTokens: usage.cache_creation_input_tokens } : {}
|
|
133
|
+
};
|
|
134
|
+
} };
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { createAnthropicAdapter };
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","names":[],"sources":["../../src/adapters/anthropic.ts"],"sourcesContent":["// @templatical/ai-kit/adapters/anthropic — the kit's model contract over the\n// Anthropic SDK's `messages.stream`. Types only from the SDK: you pass the client.\nimport type Anthropic from \"@anthropic-ai/sdk\";\nimport type {\n AiModelAdapter,\n AiModelAttachment,\n AiModelMessage,\n AiModelPart,\n} from \"../server/model\";\nimport type { AiToolDefinition } from \"../server/tool\";\nimport {\n blobBase64,\n cutOff,\n ownState,\n providerError,\n refused,\n toolInput,\n} from \"./shared\";\n\ntype StreamParams = Anthropic.MessageStreamParams;\n\n/** The one method the adapter calls: `new Anthropic()` fits it. */\nexport interface AnthropicAdapterClient {\n messages: {\n stream(\n body: StreamParams,\n options?: { signal?: AbortSignal | null },\n ): AsyncIterable<Anthropic.RawMessageStreamEvent> & {\n finalMessage(): Promise<Anthropic.Message>;\n };\n };\n}\n\nexport interface AnthropicAdapterOptions {\n /** A client you construct: `new Anthropic()`, or one with its own `baseURL`. */\n client: AnthropicAdapterClient;\n /** The model id. No default. */\n model: string;\n /** `max_tokens`. Default 16_000. */\n maxTokens?: number;\n /** Prompt caching: a breakpoint after the system prompt plus automatic caching. Default true. */\n cache?: boolean;\n /** Further request fields, e.g. `output_config` or `metadata`. */\n params?: Omit<\n StreamParams,\n | \"model\"\n | \"system\"\n | \"messages\"\n | \"tools\"\n | \"max_tokens\"\n | \"stream\"\n | \"cache_control\"\n >;\n}\n\nconst ADAPTER = \"anthropic\";\nconst CUT_OFF = new Set([\"max_tokens\", \"model_context_window_exceeded\"]);\nconst EPHEMERAL = { type: \"ephemeral\" } as const;\n\nfunction toTool(tool: AiToolDefinition): Anthropic.Tool {\n return {\n name: tool.name,\n description: tool.description,\n input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,\n };\n}\n\nasync function toBlock(\n attachment: AiModelAttachment,\n): Promise<Anthropic.ContentBlockParam> {\n if (attachment.kind === \"image-url\")\n return { type: \"image\", source: { type: \"url\", url: attachment.url } };\n const data = await blobBase64(attachment.data);\n return attachment.kind === \"pdf\"\n ? {\n type: \"document\",\n source: { type: \"base64\", media_type: \"application/pdf\", data },\n }\n : {\n type: \"image\",\n source: {\n type: \"base64\",\n media_type:\n attachment.mediaType as Anthropic.Base64ImageSource[\"media_type\"],\n data,\n },\n };\n}\n\nasync function toMessages(\n messages: AiModelMessage[],\n): Promise<Anthropic.MessageParam[]> {\n const out: Anthropic.MessageParam[] = [];\n for (const message of messages) {\n if (message.role === \"tool\") {\n const result: Anthropic.ToolResultBlockParam = {\n type: \"tool_result\",\n tool_use_id: message.toolCallId,\n content: message.content,\n };\n const last = out.at(-1);\n // All results of one step go back in one user message.\n if (\n last?.role === \"user\" &&\n Array.isArray(last.content) &&\n last.content.every((block) => block.type === \"tool_result\")\n ) {\n last.content.push(result);\n } else {\n out.push({ role: \"user\", content: [result] });\n }\n } else if (message.role === \"assistant\") {\n const own = ownState<Anthropic.ContentBlockParam>(message.state, ADAPTER);\n const content = own ?? [\n ...(message.content\n ? [{ type: \"text\" as const, text: message.content }]\n : []),\n ...(message.toolCalls ?? []).map((call) => ({\n type: \"tool_use\" as const,\n id: call.id,\n name: call.name,\n input: toolInput(call.input),\n })),\n ];\n // An empty assistant turn would be refused; skipping it keeps the history valid.\n if (content.length > 0) out.push({ role: \"assistant\", content });\n } else {\n const files = await Promise.all((message.attachments ?? []).map(toBlock));\n out.push({\n role: \"user\",\n content: [...files, { type: \"text\", text: message.content }],\n });\n }\n }\n return out;\n}\n\n/** An `AiModelAdapter` over the Anthropic Messages API. */\nexport function createAnthropicAdapter(\n options: AnthropicAdapterOptions,\n): AiModelAdapter {\n const cache = options.cache ?? true;\n return {\n async *stream({\n system,\n messages,\n tools,\n signal,\n }): AsyncIterable<AiModelPart> {\n const body: StreamParams = {\n ...options.params,\n model: options.model,\n max_tokens: options.maxTokens ?? 16_000,\n system: [\n {\n type: \"text\",\n text: system,\n ...(cache ? { cache_control: EPHEMERAL } : {}),\n },\n ],\n messages: await toMessages(messages),\n ...(tools.length > 0 ? { tools: tools.map(toTool) } : {}),\n ...(cache ? { cache_control: EPHEMERAL } : {}),\n };\n let final: Anthropic.Message;\n try {\n const stream = options.client.messages.stream(body, { signal });\n for await (const event of stream) {\n signal?.throwIfAborted();\n if (\n event.type === \"content_block_delta\" &&\n event.delta.type === \"text_delta\"\n )\n yield { type: \"text\", text: event.delta.text };\n }\n final = await stream.finalMessage();\n signal?.throwIfAborted();\n } catch (error) {\n throw providerError(error);\n }\n if (final.stop_reason === \"refusal\") throw refused();\n const calls = final.content.filter(\n (block): block is Anthropic.ToolUseBlock => block.type === \"tool_use\",\n );\n if (\n calls.length > 0 &&\n final.stop_reason !== null &&\n CUT_OFF.has(final.stop_reason)\n )\n throw cutOff();\n yield {\n type: \"state\",\n state: { adapter: ADAPTER, content: final.content },\n };\n for (const block of calls) {\n yield {\n type: \"tool-call\",\n id: block.id,\n name: block.name,\n input: block.input as Record<string, unknown>,\n };\n }\n const usage = final.usage;\n yield {\n type: \"usage\",\n inputTokens: usage.input_tokens,\n outputTokens: usage.output_tokens,\n ...(usage.cache_read_input_tokens != null\n ? { cacheReadInputTokens: usage.cache_read_input_tokens }\n : {}),\n ...(usage.cache_creation_input_tokens != null\n ? { cacheWriteInputTokens: usage.cache_creation_input_tokens }\n : {}),\n };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { t as AiModelAdapter } from "../model-xJvGTfLB.js";
|
|
2
|
+
import OpenAI from "openai";
|
|
3
|
+
//#region src/adapters/openai.d.ts
|
|
4
|
+
type CreateParams = OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming;
|
|
5
|
+
/** The one method the adapter calls: `new OpenAI()` and `new AzureOpenAI()` fit it. */
|
|
6
|
+
export interface OpenAiAdapterClient {
|
|
7
|
+
chat: {
|
|
8
|
+
completions: {
|
|
9
|
+
create(body: CreateParams, options?: {
|
|
10
|
+
signal?: AbortSignal | null;
|
|
11
|
+
}): PromiseLike<AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface OpenAiAdapterOptions {
|
|
16
|
+
/** A client you construct: `new OpenAI()`, `new AzureOpenAI()`, or one with a `baseURL` for an OpenAI-compatible server. */
|
|
17
|
+
client: OpenAiAdapterClient;
|
|
18
|
+
/** The model id. No default. */
|
|
19
|
+
model: string;
|
|
20
|
+
/** `max_completion_tokens`. Default: the API's own. */
|
|
21
|
+
maxTokens?: number;
|
|
22
|
+
/** Further request fields, e.g. `reasoning_effort`. */
|
|
23
|
+
params?: Omit<CreateParams, "model" | "messages" | "tools" | "stream" | "stream_options" | "max_completion_tokens" | "max_tokens" | "n">;
|
|
24
|
+
}
|
|
25
|
+
/** An `AiModelAdapter` over OpenAI's Chat Completions API. */
|
|
26
|
+
export declare function createOpenAiAdapter(options: OpenAiAdapterOptions): AiModelAdapter;
|
|
27
|
+
//#endregion
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=openai.d.ts.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { a as refused, i as providerError, n as cutOff, o as toolArguments, t as blobBase64 } from "../shared-BuhccY6c.js";
|
|
2
|
+
//#region src/adapters/openai.ts
|
|
3
|
+
function toTool(tool) {
|
|
4
|
+
return {
|
|
5
|
+
type: "function",
|
|
6
|
+
function: {
|
|
7
|
+
name: tool.name,
|
|
8
|
+
description: tool.description,
|
|
9
|
+
parameters: tool.inputSchema
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function toPart(attachment, index) {
|
|
14
|
+
if (attachment.kind === "image-url") return {
|
|
15
|
+
type: "image_url",
|
|
16
|
+
image_url: { url: attachment.url }
|
|
17
|
+
};
|
|
18
|
+
const url = `data:${attachment.mediaType};base64,${await blobBase64(attachment.data)}`;
|
|
19
|
+
return attachment.kind === "pdf" ? {
|
|
20
|
+
type: "file",
|
|
21
|
+
file: {
|
|
22
|
+
filename: `attachment-${index + 1}.pdf`,
|
|
23
|
+
file_data: url
|
|
24
|
+
}
|
|
25
|
+
} : {
|
|
26
|
+
type: "image_url",
|
|
27
|
+
image_url: { url }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function toMessage(message) {
|
|
31
|
+
if (message.role === "tool") return {
|
|
32
|
+
role: "tool",
|
|
33
|
+
tool_call_id: message.toolCallId,
|
|
34
|
+
content: message.content
|
|
35
|
+
};
|
|
36
|
+
if (message.role === "assistant") {
|
|
37
|
+
if (message.content.length === 0 && !message.toolCalls?.length) return null;
|
|
38
|
+
return {
|
|
39
|
+
role: "assistant",
|
|
40
|
+
content: message.content.length > 0 ? message.content : null,
|
|
41
|
+
...message.toolCalls?.length ? { tool_calls: message.toolCalls.map((call) => ({
|
|
42
|
+
id: call.id,
|
|
43
|
+
type: "function",
|
|
44
|
+
function: {
|
|
45
|
+
name: call.name,
|
|
46
|
+
arguments: toolArguments(call.input)
|
|
47
|
+
}
|
|
48
|
+
})) } : {}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
role: "user",
|
|
53
|
+
content: [...await Promise.all((message.attachments ?? []).map(toPart)), {
|
|
54
|
+
type: "text",
|
|
55
|
+
text: message.content
|
|
56
|
+
}]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** An `AiModelAdapter` over OpenAI's Chat Completions API. */
|
|
60
|
+
function createOpenAiAdapter(options) {
|
|
61
|
+
return { async *stream({ system, messages, tools, signal }) {
|
|
62
|
+
const body = {
|
|
63
|
+
...options.params,
|
|
64
|
+
model: options.model,
|
|
65
|
+
stream: true,
|
|
66
|
+
stream_options: { include_usage: true },
|
|
67
|
+
...options.maxTokens !== void 0 ? { max_completion_tokens: options.maxTokens } : {},
|
|
68
|
+
messages: [{
|
|
69
|
+
role: "system",
|
|
70
|
+
content: system
|
|
71
|
+
}, ...(await Promise.all(messages.map(toMessage))).filter((message) => message !== null)],
|
|
72
|
+
...tools.length > 0 ? { tools: tools.map(toTool) } : {}
|
|
73
|
+
};
|
|
74
|
+
const calls = /* @__PURE__ */ new Map();
|
|
75
|
+
let finish = null;
|
|
76
|
+
let usage;
|
|
77
|
+
try {
|
|
78
|
+
const stream = await options.client.chat.completions.create(body, { signal });
|
|
79
|
+
for await (const chunk of stream) {
|
|
80
|
+
signal?.throwIfAborted();
|
|
81
|
+
const choice = chunk.choices.find((choice) => choice.index === 0);
|
|
82
|
+
const delta = choice?.delta;
|
|
83
|
+
if (delta?.refusal) throw refused();
|
|
84
|
+
if (delta?.content) yield {
|
|
85
|
+
type: "text",
|
|
86
|
+
text: delta.content
|
|
87
|
+
};
|
|
88
|
+
for (const call of delta?.tool_calls ?? []) {
|
|
89
|
+
const seen = calls.get(call.index) ?? {
|
|
90
|
+
id: "",
|
|
91
|
+
name: "",
|
|
92
|
+
args: ""
|
|
93
|
+
};
|
|
94
|
+
calls.set(call.index, {
|
|
95
|
+
id: call.id ?? seen.id,
|
|
96
|
+
name: call.function?.name ?? seen.name,
|
|
97
|
+
args: seen.args + (call.function?.arguments ?? "")
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (choice?.finish_reason) finish = choice.finish_reason;
|
|
101
|
+
if (chunk.usage) {
|
|
102
|
+
const cached = chunk.usage.prompt_tokens_details?.cached_tokens;
|
|
103
|
+
usage = {
|
|
104
|
+
inputTokens: chunk.usage.prompt_tokens - (cached ?? 0),
|
|
105
|
+
outputTokens: chunk.usage.completion_tokens,
|
|
106
|
+
...cached !== void 0 ? { cacheReadInputTokens: cached } : {}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
signal?.throwIfAborted();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw providerError(error);
|
|
113
|
+
}
|
|
114
|
+
if (finish === "content_filter") throw refused();
|
|
115
|
+
if (finish === "length" && calls.size > 0) throw cutOff();
|
|
116
|
+
for (const [, call] of [...calls].sort(([a], [b]) => a - b)) yield {
|
|
117
|
+
type: "tool-call",
|
|
118
|
+
id: call.id,
|
|
119
|
+
name: call.name,
|
|
120
|
+
input: call.args
|
|
121
|
+
};
|
|
122
|
+
if (usage) yield {
|
|
123
|
+
type: "usage",
|
|
124
|
+
...usage
|
|
125
|
+
};
|
|
126
|
+
} };
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { createOpenAiAdapter };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","names":[],"sources":["../../src/adapters/openai.ts"],"sourcesContent":["// @templatical/ai-kit/adapters/openai — the kit's model contract over OpenAI's\n// Chat Completions API, which OpenAI-compatible servers implement too. Types\n// only from the SDK: you pass the client.\nimport type OpenAI from \"openai\";\nimport type {\n AiModelAdapter,\n AiModelAttachment,\n AiModelMessage,\n AiModelPart,\n AiUsage,\n} from \"../server/model\";\nimport type { AiToolDefinition } from \"../server/tool\";\nimport {\n blobBase64,\n cutOff,\n providerError,\n refused,\n toolArguments,\n} from \"./shared\";\n\ntype CreateParams = OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming;\ntype MessageParam = OpenAI.Chat.Completions.ChatCompletionMessageParam;\ntype ContentPart = OpenAI.Chat.Completions.ChatCompletionContentPart;\n\n/** The one method the adapter calls: `new OpenAI()` and `new AzureOpenAI()` fit it. */\nexport interface OpenAiAdapterClient {\n chat: {\n completions: {\n create(\n body: CreateParams,\n options?: { signal?: AbortSignal | null },\n ): PromiseLike<\n AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>\n >;\n };\n };\n}\n\nexport interface OpenAiAdapterOptions {\n /** A client you construct: `new OpenAI()`, `new AzureOpenAI()`, or one with a `baseURL` for an OpenAI-compatible server. */\n client: OpenAiAdapterClient;\n /** The model id. No default. */\n model: string;\n /** `max_completion_tokens`. Default: the API's own. */\n maxTokens?: number;\n /** Further request fields, e.g. `reasoning_effort`. */\n params?: Omit<\n CreateParams,\n | \"model\"\n | \"messages\"\n | \"tools\"\n | \"stream\"\n | \"stream_options\"\n | \"max_completion_tokens\"\n | \"max_tokens\"\n | \"n\"\n >;\n}\n\nfunction toTool(\n tool: AiToolDefinition,\n): OpenAI.Chat.Completions.ChatCompletionTool {\n return {\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.inputSchema,\n },\n };\n}\n\nasync function toPart(\n attachment: AiModelAttachment,\n index: number,\n): Promise<ContentPart> {\n if (attachment.kind === \"image-url\")\n return { type: \"image_url\", image_url: { url: attachment.url } };\n const url = `data:${attachment.mediaType};base64,${await blobBase64(attachment.data)}`;\n return attachment.kind === \"pdf\"\n ? {\n type: \"file\",\n file: { filename: `attachment-${index + 1}.pdf`, file_data: url },\n }\n : { type: \"image_url\", image_url: { url } };\n}\n\nasync function toMessage(\n message: AiModelMessage,\n): Promise<MessageParam | null> {\n if (message.role === \"tool\")\n return {\n role: \"tool\",\n tool_call_id: message.toolCallId,\n content: message.content,\n };\n if (message.role === \"assistant\") {\n if (message.content.length === 0 && !message.toolCalls?.length) return null;\n return {\n role: \"assistant\",\n content: message.content.length > 0 ? message.content : null,\n ...(message.toolCalls?.length\n ? {\n tool_calls: message.toolCalls.map((call) => ({\n id: call.id,\n type: \"function\" as const,\n function: {\n name: call.name,\n arguments: toolArguments(call.input),\n },\n })),\n }\n : {}),\n };\n }\n const files = await Promise.all((message.attachments ?? []).map(toPart));\n return {\n role: \"user\",\n content: [...files, { type: \"text\", text: message.content }],\n };\n}\n\n/** An `AiModelAdapter` over OpenAI's Chat Completions API. */\nexport function createOpenAiAdapter(\n options: OpenAiAdapterOptions,\n): AiModelAdapter {\n return {\n async *stream({\n system,\n messages,\n tools,\n signal,\n }): AsyncIterable<AiModelPart> {\n const body: CreateParams = {\n ...options.params,\n model: options.model,\n stream: true,\n stream_options: { include_usage: true },\n ...(options.maxTokens !== undefined\n ? { max_completion_tokens: options.maxTokens }\n : {}),\n messages: [\n { role: \"system\", content: system },\n ...(await Promise.all(messages.map(toMessage))).filter(\n (message) => message !== null,\n ),\n ],\n ...(tools.length > 0 ? { tools: tools.map(toTool) } : {}),\n };\n const calls = new Map<\n number,\n { id: string; name: string; args: string }\n >();\n let finish: string | null = null;\n let usage: AiUsage | undefined;\n try {\n const stream = await options.client.chat.completions.create(body, {\n signal,\n });\n for await (const chunk of stream) {\n signal?.throwIfAborted();\n const choice = chunk.choices.find((choice) => choice.index === 0);\n const delta = choice?.delta;\n if (delta?.refusal) throw refused();\n if (delta?.content) yield { type: \"text\", text: delta.content };\n for (const call of delta?.tool_calls ?? []) {\n const seen = calls.get(call.index) ?? {\n id: \"\",\n name: \"\",\n args: \"\",\n };\n calls.set(call.index, {\n id: call.id ?? seen.id,\n name: call.function?.name ?? seen.name,\n args: seen.args + (call.function?.arguments ?? \"\"),\n });\n }\n if (choice?.finish_reason) finish = choice.finish_reason;\n if (chunk.usage) {\n const cached = chunk.usage.prompt_tokens_details?.cached_tokens;\n usage = {\n inputTokens: chunk.usage.prompt_tokens - (cached ?? 0),\n outputTokens: chunk.usage.completion_tokens,\n ...(cached !== undefined ? { cacheReadInputTokens: cached } : {}),\n };\n }\n }\n signal?.throwIfAborted();\n } catch (error) {\n throw providerError(error);\n }\n if (finish === \"content_filter\") throw refused();\n if (finish === \"length\" && calls.size > 0) throw cutOff();\n for (const [, call] of [...calls].sort(([a], [b]) => a - b)) {\n yield {\n type: \"tool-call\",\n id: call.id,\n name: call.name,\n input: call.args,\n };\n }\n if (usage) yield { type: \"usage\", ...usage };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { t as AiModelAdapter } from "../model-xJvGTfLB.js";
|
|
2
|
+
import { LanguageModel, streamText } from "ai";
|
|
3
|
+
//#region src/adapters/vercel.d.ts
|
|
4
|
+
type StreamTextParams = Parameters<typeof streamText>[0];
|
|
5
|
+
export interface VercelAdapterOptions {
|
|
6
|
+
/** An AI SDK model, e.g. `anthropic('…')` from `@ai-sdk/anthropic`, or a gateway model id. */
|
|
7
|
+
model: LanguageModel;
|
|
8
|
+
/** `maxOutputTokens`. Default: the provider's own. */
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
/** Further `streamText` options, e.g. `providerOptions` or `maxRetries`. */
|
|
11
|
+
params?: Omit<StreamTextParams, "model" | "system" | "instructions" | "messages" | "prompt" | "tools" | "abortSignal" | "maxOutputTokens">;
|
|
12
|
+
}
|
|
13
|
+
/** An `AiModelAdapter` over the Vercel AI SDK. */
|
|
14
|
+
export declare function createVercelAdapter(options: VercelAdapterOptions): AiModelAdapter;
|
|
15
|
+
//#endregion
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=vercel.d.ts.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { a as refused, i as providerError, n as cutOff, r as ownState, s as toolInput } from "../shared-BuhccY6c.js";
|
|
2
|
+
import { jsonSchema, streamText, tool } from "ai";
|
|
3
|
+
//#region src/adapters/vercel.ts
|
|
4
|
+
const ADAPTER = "vercel";
|
|
5
|
+
function toTools(tools) {
|
|
6
|
+
return Object.fromEntries(tools.map((definition) => [definition.name, tool({
|
|
7
|
+
description: definition.description,
|
|
8
|
+
inputSchema: jsonSchema(definition.inputSchema)
|
|
9
|
+
})]));
|
|
10
|
+
}
|
|
11
|
+
async function toFilePart(attachment) {
|
|
12
|
+
if (attachment.kind === "image-url") return {
|
|
13
|
+
type: "file",
|
|
14
|
+
data: new URL(attachment.url),
|
|
15
|
+
mediaType: "image"
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
type: "file",
|
|
19
|
+
data: new Uint8Array(await attachment.data.arrayBuffer()),
|
|
20
|
+
mediaType: attachment.kind === "pdf" ? "application/pdf" : attachment.mediaType
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function toMessages(messages) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const message of messages) if (message.role === "tool") {
|
|
26
|
+
const result = {
|
|
27
|
+
type: "tool-result",
|
|
28
|
+
toolCallId: message.toolCallId,
|
|
29
|
+
toolName: message.name,
|
|
30
|
+
output: {
|
|
31
|
+
type: "text",
|
|
32
|
+
value: message.content
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const last = out.at(-1);
|
|
36
|
+
if (last?.role === "tool") last.content.push(result);
|
|
37
|
+
else out.push({
|
|
38
|
+
role: "tool",
|
|
39
|
+
content: [result]
|
|
40
|
+
});
|
|
41
|
+
} else if (message.role === "assistant") {
|
|
42
|
+
const content = ownState(message.state, ADAPTER) ?? [...message.content ? [{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: message.content
|
|
45
|
+
}] : [], ...(message.toolCalls ?? []).map((call) => ({
|
|
46
|
+
type: "tool-call",
|
|
47
|
+
toolCallId: call.id,
|
|
48
|
+
toolName: call.name,
|
|
49
|
+
input: toolInput(call.input)
|
|
50
|
+
}))];
|
|
51
|
+
if (content.length > 0) out.push({
|
|
52
|
+
role: "assistant",
|
|
53
|
+
content
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
const files = await Promise.all((message.attachments ?? []).map(toFilePart));
|
|
57
|
+
out.push({
|
|
58
|
+
role: "user",
|
|
59
|
+
content: [...files, {
|
|
60
|
+
type: "text",
|
|
61
|
+
text: message.content
|
|
62
|
+
}]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** An `AiModelAdapter` over the Vercel AI SDK. */
|
|
68
|
+
function createVercelAdapter(options) {
|
|
69
|
+
return { async *stream({ system, messages, tools, signal }) {
|
|
70
|
+
const calls = [];
|
|
71
|
+
let toolInputStarted = false;
|
|
72
|
+
let finishReason;
|
|
73
|
+
let usage;
|
|
74
|
+
let state;
|
|
75
|
+
try {
|
|
76
|
+
const result = streamText({
|
|
77
|
+
...options.params,
|
|
78
|
+
model: options.model,
|
|
79
|
+
instructions: system,
|
|
80
|
+
messages: await toMessages(messages),
|
|
81
|
+
...tools.length > 0 ? { tools: toTools(tools) } : {},
|
|
82
|
+
...options.maxTokens !== void 0 ? { maxOutputTokens: options.maxTokens } : {},
|
|
83
|
+
abortSignal: signal
|
|
84
|
+
});
|
|
85
|
+
for await (const part of result.stream) {
|
|
86
|
+
signal?.throwIfAborted();
|
|
87
|
+
if (part.type === "text-delta") yield {
|
|
88
|
+
type: "text",
|
|
89
|
+
text: part.text
|
|
90
|
+
};
|
|
91
|
+
else if (part.type === "tool-input-start") toolInputStarted = true;
|
|
92
|
+
else if (part.type === "tool-call") calls.push({
|
|
93
|
+
id: part.toolCallId,
|
|
94
|
+
name: part.toolName,
|
|
95
|
+
input: part.input
|
|
96
|
+
});
|
|
97
|
+
else if (part.type === "finish") {
|
|
98
|
+
finishReason = part.finishReason;
|
|
99
|
+
usage = part.totalUsage;
|
|
100
|
+
} else if (part.type === "error") throw part.error;
|
|
101
|
+
else if (part.type === "abort") throw new Error(part.reason ?? "The AI SDK aborted the stream.");
|
|
102
|
+
}
|
|
103
|
+
signal?.throwIfAborted();
|
|
104
|
+
if (finishReason === "content-filter") throw refused();
|
|
105
|
+
if (finishReason === "length" && (toolInputStarted || calls.length > 0)) throw cutOff();
|
|
106
|
+
const assistant = (await result.responseMessages).find((message) => message.role === "assistant");
|
|
107
|
+
if (assistant && typeof assistant.content !== "string") state = assistant.content;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw providerError(error);
|
|
110
|
+
}
|
|
111
|
+
if (state) yield {
|
|
112
|
+
type: "state",
|
|
113
|
+
state: {
|
|
114
|
+
adapter: ADAPTER,
|
|
115
|
+
content: state
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
for (const call of calls) yield {
|
|
119
|
+
type: "tool-call",
|
|
120
|
+
...call
|
|
121
|
+
};
|
|
122
|
+
if (usage) {
|
|
123
|
+
const details = usage.inputTokenDetails;
|
|
124
|
+
yield {
|
|
125
|
+
type: "usage",
|
|
126
|
+
inputTokens: details?.noCacheTokens ?? (usage.inputTokens !== void 0 ? Math.max(0, usage.inputTokens - (details?.cacheReadTokens ?? 0) - (details?.cacheWriteTokens ?? 0)) : 0),
|
|
127
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
128
|
+
...details?.cacheReadTokens !== void 0 ? { cacheReadInputTokens: details.cacheReadTokens } : {},
|
|
129
|
+
...details?.cacheWriteTokens !== void 0 ? { cacheWriteInputTokens: details.cacheWriteTokens } : {}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
} };
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { createVercelAdapter };
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=vercel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel.js","names":[],"sources":["../../src/adapters/vercel.ts"],"sourcesContent":["// @templatical/ai-kit/adapters/vercel — the kit's model contract over the\n// Vercel AI SDK's `streamText`, for any AI SDK provider model.\nimport {\n jsonSchema,\n streamText,\n tool,\n type AssistantModelMessage,\n type LanguageModel,\n type ModelMessage,\n type ToolSet,\n} from \"ai\";\nimport type {\n AiModelAdapter,\n AiModelAttachment,\n AiModelMessage,\n AiModelPart,\n AiModelToolCall,\n} from \"../server/model\";\nimport type { AiToolDefinition } from \"../server/tool\";\nimport { cutOff, ownState, providerError, refused, toolInput } from \"./shared\";\n\ntype StreamTextParams = Parameters<typeof streamText>[0];\ntype AssistantPart = Exclude<AssistantModelMessage[\"content\"], string>[number];\n\nexport interface VercelAdapterOptions {\n /** An AI SDK model, e.g. `anthropic('…')` from `@ai-sdk/anthropic`, or a gateway model id. */\n model: LanguageModel;\n /** `maxOutputTokens`. Default: the provider's own. */\n maxTokens?: number;\n /** Further `streamText` options, e.g. `providerOptions` or `maxRetries`. */\n params?: Omit<\n StreamTextParams,\n | \"model\"\n | \"system\"\n | \"instructions\"\n | \"messages\"\n | \"prompt\"\n | \"tools\"\n | \"abortSignal\"\n | \"maxOutputTokens\"\n >;\n}\n\nconst ADAPTER = \"vercel\";\n\nfunction toTools(tools: AiToolDefinition[]): ToolSet {\n return Object.fromEntries(\n tools.map((definition) => [\n definition.name,\n // No `execute`: the kit applies the call, so the SDK stops after one step.\n tool({\n description: definition.description,\n inputSchema: jsonSchema(definition.inputSchema),\n }),\n ]),\n );\n}\n\nasync function toFilePart(attachment: AiModelAttachment) {\n if (attachment.kind === \"image-url\")\n return {\n type: \"file\" as const,\n data: new URL(attachment.url),\n mediaType: \"image\",\n };\n const data = new Uint8Array(await attachment.data.arrayBuffer());\n return {\n type: \"file\" as const,\n data,\n mediaType:\n attachment.kind === \"pdf\" ? \"application/pdf\" : attachment.mediaType,\n };\n}\n\nasync function toMessages(messages: AiModelMessage[]): Promise<ModelMessage[]> {\n const out: ModelMessage[] = [];\n for (const message of messages) {\n if (message.role === \"tool\") {\n const result = {\n type: \"tool-result\" as const,\n toolCallId: message.toolCallId,\n toolName: message.name,\n output: { type: \"text\" as const, value: message.content },\n };\n const last = out.at(-1);\n // All results of one step go back in one tool message.\n if (last?.role === \"tool\") last.content.push(result);\n else out.push({ role: \"tool\", content: [result] });\n } else if (message.role === \"assistant\") {\n const content: AssistantPart[] = ownState<AssistantPart>(\n message.state,\n ADAPTER,\n ) ?? [\n ...(message.content\n ? [{ type: \"text\" as const, text: message.content }]\n : []),\n ...(message.toolCalls ?? []).map((call) => ({\n type: \"tool-call\" as const,\n toolCallId: call.id,\n toolName: call.name,\n input: toolInput(call.input),\n })),\n ];\n if (content.length > 0) out.push({ role: \"assistant\", content });\n } else {\n const files = await Promise.all(\n (message.attachments ?? []).map(toFilePart),\n );\n out.push({\n role: \"user\",\n content: [...files, { type: \"text\", text: message.content }],\n });\n }\n }\n return out;\n}\n\n/** An `AiModelAdapter` over the Vercel AI SDK. */\nexport function createVercelAdapter(\n options: VercelAdapterOptions,\n): AiModelAdapter {\n return {\n async *stream({\n system,\n messages,\n tools,\n signal,\n }): AsyncIterable<AiModelPart> {\n const calls: AiModelToolCall[] = [];\n let toolInputStarted = false;\n let finishReason: string | undefined;\n let usage:\n | {\n inputTokens?: number;\n outputTokens?: number;\n inputTokenDetails?: {\n noCacheTokens?: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n }\n | undefined;\n let state: AssistantPart[] | undefined;\n try {\n const result = streamText({\n ...options.params,\n model: options.model,\n instructions: system,\n messages: await toMessages(messages),\n ...(tools.length > 0 ? { tools: toTools(tools) } : {}),\n ...(options.maxTokens !== undefined\n ? { maxOutputTokens: options.maxTokens }\n : {}),\n abortSignal: signal,\n });\n for await (const part of result.stream) {\n signal?.throwIfAborted();\n if (part.type === \"text-delta\")\n yield { type: \"text\", text: part.text };\n else if (part.type === \"tool-input-start\") toolInputStarted = true;\n else if (part.type === \"tool-call\")\n calls.push({\n id: part.toolCallId,\n name: part.toolName,\n input: part.input as Record<string, unknown>,\n });\n else if (part.type === \"finish\") {\n finishReason = part.finishReason;\n usage = part.totalUsage;\n } else if (part.type === \"error\") throw part.error;\n // Our own signal is checked at the top of the loop, so this abort\n // came from the SDK itself, e.g. `params.timeout`.\n else if (part.type === \"abort\")\n throw new Error(part.reason ?? \"The AI SDK aborted the stream.\");\n }\n signal?.throwIfAborted();\n if (finishReason === \"content-filter\") throw refused();\n if (finishReason === \"length\" && (toolInputStarted || calls.length > 0))\n throw cutOff();\n const assistant = (await result.responseMessages).find(\n (message) => message.role === \"assistant\",\n );\n if (assistant && typeof assistant.content !== \"string\")\n state = assistant.content;\n } catch (error) {\n throw providerError(error);\n }\n if (state)\n yield { type: \"state\", state: { adapter: ADAPTER, content: state } };\n for (const call of calls) yield { type: \"tool-call\", ...call };\n if (usage) {\n const details = usage.inputTokenDetails;\n yield {\n type: \"usage\",\n inputTokens:\n details?.noCacheTokens ??\n (usage.inputTokens !== undefined\n ? Math.max(\n 0,\n usage.inputTokens -\n (details?.cacheReadTokens ?? 0) -\n (details?.cacheWriteTokens ?? 0),\n )\n : 0),\n outputTokens: usage.outputTokens ?? 0,\n ...(details?.cacheReadTokens !== undefined\n ? { cacheReadInputTokens: details.cacheReadTokens }\n : {}),\n ...(details?.cacheWriteTokens !== undefined\n ? { cacheWriteInputTokens: details.cacheWriteTokens }\n : {}),\n };\n }\n },\n };\n}\n"],"mappings":"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"}
|