@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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -2
  3. package/dist/adapters/anthropic.d.ts +31 -0
  4. package/dist/adapters/anthropic.js +139 -0
  5. package/dist/adapters/anthropic.js.map +1 -0
  6. package/dist/adapters/openai.d.ts +29 -0
  7. package/dist/adapters/openai.js +131 -0
  8. package/dist/adapters/openai.js.map +1 -0
  9. package/dist/adapters/vercel.d.ts +17 -0
  10. package/dist/adapters/vercel.js +137 -0
  11. package/dist/adapters/vercel.js.map +1 -0
  12. package/dist/checks-DvLD3892.js +656 -0
  13. package/dist/checks-DvLD3892.js.map +1 -0
  14. package/dist/client/index.d.ts +24 -0
  15. package/dist/client/index.js +3 -0
  16. package/dist/errors-DWshWx18.js +55 -0
  17. package/dist/errors-DWshWx18.js.map +1 -0
  18. package/dist/http-provider-Cb41NXd5.d.ts +22 -0
  19. package/dist/http-provider-Cx2ZcLRB.js +278 -0
  20. package/dist/http-provider-Cx2ZcLRB.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +3 -0
  23. package/dist/model-xJvGTfLB.d.ts +84 -0
  24. package/dist/server/index.d.ts +313 -0
  25. package/dist/server/index.js +355 -0
  26. package/dist/server/index.js.map +1 -0
  27. package/dist/shared-BuhccY6c.js +61 -0
  28. package/dist/shared-BuhccY6c.js.map +1 -0
  29. package/dist/testing/index.d.ts +277 -0
  30. package/dist/testing/index.js +2445 -0
  31. package/dist/testing/index.js.map +1 -0
  32. package/dist/wire-B6AF_UGT.js +132 -0
  33. package/dist/wire-B6AF_UGT.js.map +1 -0
  34. package/dist/wire-Cv7Fk-7n.d.ts +136 -0
  35. package/eval/MANIFEST.sha256 +23 -0
  36. package/eval/cases.json +287 -0
  37. package/eval/designs/newsletter.png +0 -0
  38. package/eval/designs/one-page.pdf +0 -0
  39. package/eval/designs/promo.png +0 -0
  40. package/eval/domain-mismatch.json +25 -0
  41. package/eval/empty.json +11 -0
  42. package/eval/images/banner.png +0 -0
  43. package/eval/images/chart.png +0 -0
  44. package/eval/images/divider.png +0 -0
  45. package/eval/images/probe.png +0 -0
  46. package/eval/images/product.png +0 -0
  47. package/eval/injection.json +13 -0
  48. package/eval/low-text.json +41 -0
  49. package/eval/promo.json +12 -0
  50. package/eval/sources/banner.html +16 -0
  51. package/eval/sources/chart.html +16 -0
  52. package/eval/sources/divider.html +16 -0
  53. package/eval/sources/newsletter.html +16 -0
  54. package/eval/sources/one-page.html +16 -0
  55. package/eval/sources/probe.html +16 -0
  56. package/eval/sources/product.html +16 -0
  57. package/eval/sources/promo.html +16 -0
  58. package/eval/welcome.json +12 -0
  59. package/package.json +99 -3
  60. package/prompts/alt-text.md +7 -0
  61. package/prompts/chat-attachments.md +1 -0
  62. package/prompts/chat.md +2 -0
  63. package/prompts/editing.md +10 -0
  64. package/prompts/fix-rules.json +9 -0
  65. package/prompts/fix.md +8 -0
  66. package/prompts/operations.md +127 -0
  67. package/prompts/rewrite-presets.json +8 -0
  68. package/prompts/rewrite.md +4 -0
  69. 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
- # Temporary Holding Version
1
+ # @templatical/ai-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Bring your own AI backend to the [Templatical](https://templatical.com) email editor.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@templatical/ai-kit?label=npm&color=cb3837)](https://www.npmjs.com/package/@templatical/ai-kit)
6
+ [![License](https://img.shields.io/badge/license-MIT-green)](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":";;;AA2CA,MAAM,UAAU;AAEhB,SAAS,QAAQ,OAAoC;CACnD,OAAO,OAAO,YACZ,MAAM,KAAK,eAAe,CACxB,WAAW,MAEX,KAAK;EACH,aAAa,WAAW;EACxB,aAAa,WAAW,WAAW,WAAW;CAChD,CAAC,CACH,CAAC,CACH;AACF;AAEA,eAAe,WAAW,YAA+B;CACvD,IAAI,WAAW,SAAS,aACtB,OAAO;EACL,MAAM;EACN,MAAM,IAAI,IAAI,WAAW,GAAG;EAC5B,WAAW;CACb;CAEF,OAAO;EACL,MAAM;EACN,MAAA,IAHe,WAAW,MAAM,WAAW,KAAK,YAAY,CAGzD;EACH,WACE,WAAW,SAAS,QAAQ,oBAAoB,WAAW;CAC/D;AACF;AAEA,eAAe,WAAW,UAAqD;CAC7E,MAAM,MAAsB,CAAC;CAC7B,KAAK,MAAM,WAAW,UACpB,IAAI,QAAQ,SAAS,QAAQ;EAC3B,MAAM,SAAS;GACb,MAAM;GACN,YAAY,QAAQ;GACpB,UAAU,QAAQ;GAClB,QAAQ;IAAE,MAAM;IAAiB,OAAO,QAAQ;GAAQ;EAC1D;EACA,MAAM,OAAO,IAAI,GAAG,EAAE;EAEtB,IAAI,MAAM,SAAS,QAAQ,KAAK,QAAQ,KAAK,MAAM;OAC9C,IAAI,KAAK;GAAE,MAAM;GAAQ,SAAS,CAAC,MAAM;EAAE,CAAC;CACnD,OAAO,IAAI,QAAQ,SAAS,aAAa;EACvC,MAAM,UAA2B,SAC/B,QAAQ,OACR,OACF,KAAK,CACH,GAAI,QAAQ,UACR,CAAC;GAAE,MAAM;GAAiB,MAAM,QAAQ;EAAQ,CAAC,IACjD,CAAC,GACL,IAAI,QAAQ,aAAa,CAAC,EAAA,CAAG,KAAK,UAAU;GAC1C,MAAM;GACN,YAAY,KAAK;GACjB,UAAU,KAAK;GACf,OAAO,UAAU,KAAK,KAAK;EAC7B,EAAE,CACJ;EACA,IAAI,QAAQ,SAAS,GAAG,IAAI,KAAK;GAAE,MAAM;GAAa;EAAQ,CAAC;CACjE,OAAO;EACL,MAAM,QAAQ,MAAM,QAAQ,KACzB,QAAQ,eAAe,CAAC,EAAA,CAAG,IAAI,UAAU,CAC5C;EACA,IAAI,KAAK;GACP,MAAM;GACN,SAAS,CAAC,GAAG,OAAO;IAAE,MAAM;IAAQ,MAAM,QAAQ;GAAQ,CAAC;EAC7D,CAAC;CACH;CAEF,OAAO;AACT;;AAGA,SAAgB,oBACd,SACgB;CAChB,OAAO,EACL,OAAO,OAAO,EACZ,QACA,UACA,OACA,UAC6B;EAC7B,MAAM,QAA2B,CAAC;EAClC,IAAI,mBAAmB;EACvB,IAAI;EACJ,IAAI;EAWJ,IAAI;EACJ,IAAI;GACF,MAAM,SAAS,WAAW;IACxB,GAAG,QAAQ;IACX,OAAO,QAAQ;IACf,cAAc;IACd,UAAU,MAAM,WAAW,QAAQ;IACnC,GAAI,MAAM,SAAS,IAAI,EAAE,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC;IACpD,GAAI,QAAQ,cAAc,KAAA,IACtB,EAAE,iBAAiB,QAAQ,UAAU,IACrC,CAAC;IACL,aAAa;GACf,CAAC;GACD,WAAW,MAAM,QAAQ,OAAO,QAAQ;IACtC,QAAQ,eAAe;IACvB,IAAI,KAAK,SAAS,cAChB,MAAM;KAAE,MAAM;KAAQ,MAAM,KAAK;IAAK;SACnC,IAAI,KAAK,SAAS,oBAAoB,mBAAmB;SACzD,IAAI,KAAK,SAAS,aACrB,MAAM,KAAK;KACT,IAAI,KAAK;KACT,MAAM,KAAK;KACX,OAAO,KAAK;IACd,CAAC;SACE,IAAI,KAAK,SAAS,UAAU;KAC/B,eAAe,KAAK;KACpB,QAAQ,KAAK;IACf,OAAO,IAAI,KAAK,SAAS,SAAS,MAAM,KAAK;SAGxC,IAAI,KAAK,SAAS,SACrB,MAAM,IAAI,MAAM,KAAK,UAAU,gCAAgC;GACnE;GACA,QAAQ,eAAe;GACvB,IAAI,iBAAiB,kBAAkB,MAAM,QAAQ;GACrD,IAAI,iBAAiB,aAAa,oBAAoB,MAAM,SAAS,IACnE,MAAM,OAAO;GACf,MAAM,aAAa,MAAM,OAAO,iBAAA,CAAkB,MAC/C,YAAY,QAAQ,SAAS,WAChC;GACA,IAAI,aAAa,OAAO,UAAU,YAAY,UAC5C,QAAQ,UAAU;EACtB,SAAS,OAAO;GACd,MAAM,cAAc,KAAK;EAC3B;EACA,IAAI,OACF,MAAM;GAAE,MAAM;GAAS,OAAO;IAAE,SAAS;IAAS,SAAS;GAAM;EAAE;EACrE,KAAK,MAAM,QAAQ,OAAO,MAAM;GAAE,MAAM;GAAa,GAAG;EAAK;EAC7D,IAAI,OAAO;GACT,MAAM,UAAU,MAAM;GACtB,MAAM;IACJ,MAAM;IACN,aACE,SAAS,kBACR,MAAM,gBAAgB,KAAA,IACnB,KAAK,IACH,GACA,MAAM,eACH,SAAS,mBAAmB,MAC5B,SAAS,oBAAoB,EAClC,IACA;IACN,cAAc,MAAM,gBAAgB;IACpC,GAAI,SAAS,oBAAoB,KAAA,IAC7B,EAAE,sBAAsB,QAAQ,gBAAgB,IAChD,CAAC;IACL,GAAI,SAAS,qBAAqB,KAAA,IAC9B,EAAE,uBAAuB,QAAQ,iBAAiB,IAClD,CAAC;GACP;EACF;CACF,EACF;AACF"}