@ubean/ai 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soybean
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.
@@ -0,0 +1,239 @@
1
+ import { safeParse } from "valibot";
2
+ import { toJsonSchema } from "@valibot/to-json-schema";
3
+ //#region src/core.ts
4
+ const registeredProviders = /* @__PURE__ */ new Map();
5
+ let defaultProviderId;
6
+ /** Register a provider (idempotent by id). */
7
+ function defineProvider(definition) {
8
+ if (definition.id && !definition.id.includes("/")) registeredProviders.set(definition.id, definition);
9
+ }
10
+ /** Configure the fallback provider used when a model string has no provider segment. */
11
+ function configureAI(options) {
12
+ if (options.defaultProvider) defaultProviderId = options.defaultProvider;
13
+ }
14
+ /**
15
+ * Resolve a `provider/model` string to `{ provider, modelId }`.
16
+ * Falls back to the configured default provider, then to an env-driven
17
+ * OpenAI-compatible provider (mirrors devtools' DeepSeek defaults).
18
+ */
19
+ function resolveModel(model) {
20
+ const slash = model.indexOf("/");
21
+ const providerId = slash === -1 ? void 0 : model.slice(0, slash);
22
+ const modelId = slash === -1 ? model : model.slice(slash + 1);
23
+ let provider = providerId ? registeredProviders.get(providerId) : void 0;
24
+ if (!provider && defaultProviderId) provider = registeredProviders.get(defaultProviderId);
25
+ if (!provider) provider = envProvider();
26
+ if (!provider) throw new Error(`[@ubean/ai] No provider resolved for "${model}". Define a provider via defineProvider() or configure a default with configureAI().`);
27
+ return {
28
+ provider,
29
+ modelId
30
+ };
31
+ }
32
+ function envProvider() {
33
+ const apiKey = process.env.DEEPSEEK_API_KEY || process.env.UBEAN_AI_API_KEY || process.env.OPENAI_API_KEY;
34
+ const baseURL = process.env.UBEAN_AI_API_BASE || "https://api.deepseek.com/v1";
35
+ if (!apiKey) return void 0;
36
+ return {
37
+ id: "env",
38
+ kind: "openai-compatible",
39
+ baseURL,
40
+ apiKey
41
+ };
42
+ }
43
+ /** Declare a typed ubean tool. Returns the tool unchanged (type-safe identity). */
44
+ function defineAgentTool(tool) {
45
+ return tool;
46
+ }
47
+ async function loadAiSdk() {
48
+ try {
49
+ const [aiMod, compatMod] = await Promise.all([import("ai"), import("@ai-sdk/openai-compatible")]);
50
+ return {
51
+ ai: aiMod,
52
+ createOpenAICompatible: compatMod.createOpenAICompatible
53
+ };
54
+ } catch (err) {
55
+ const msg = err instanceof Error ? err.message : String(err);
56
+ throw new Error(`[@ubean/ai] The AI SDK packages are not installed. Install them with: pnpm add ai @ai-sdk/openai-compatible (or npm/yarn/bun equivalent). Original error: ${msg}`, { cause: err });
57
+ }
58
+ }
59
+ function buildSystemPrompt(config) {
60
+ let system = config.system ?? "";
61
+ if (config.context && Object.keys(config.context).length > 0) {
62
+ const ctx = JSON.stringify(config.context, null, 2);
63
+ system += `\n\n<context>\n${ctx}\n</context>`;
64
+ }
65
+ return system;
66
+ }
67
+ /**
68
+ * Build a declarative agent. The loop is delegated to the AI SDK's
69
+ * `streamText`/`generateText` (with `stopWhen: stepCountIs(maxSteps)`); this
70
+ * only returns the ubean-idiomatic surface (`run`/`stream`/`asHandler`).
71
+ */
72
+ function defineAgent(config) {
73
+ const system = buildSystemPrompt(config);
74
+ async function createModel() {
75
+ const { ai, createOpenAICompatible } = await loadAiSdk();
76
+ const { provider, modelId } = resolveModel(config.model);
77
+ return {
78
+ ai,
79
+ model: createOpenAICompatible({
80
+ baseURL: provider.baseURL,
81
+ name: provider.id,
82
+ apiKey: provider.apiKey,
83
+ headers: provider.headers
84
+ }).chatModel(modelId),
85
+ provider
86
+ };
87
+ }
88
+ async function buildTools(input) {
89
+ const { ai } = await loadAiSdk();
90
+ const tools = {};
91
+ for (const [name, tool] of Object.entries(config.tools ?? {})) {
92
+ const toolName = tool.name || name;
93
+ const inputSchema = ai.jsonSchema(toJsonSchema(tool.input), { validate: (value) => {
94
+ const result = safeParse(tool.input, value);
95
+ if (result.success) return {
96
+ success: true,
97
+ value: result.output
98
+ };
99
+ return {
100
+ success: false,
101
+ error: new Error(result.issues.map((i) => i.message).join("; "))
102
+ };
103
+ } });
104
+ tools[toolName] = ai.tool({
105
+ description: tool.description,
106
+ inputSchema,
107
+ execute: async (args, ctx) => {
108
+ const toolCtx = {
109
+ runId: input.meta?.runId ?? "run",
110
+ meta: input.meta,
111
+ signal: ctx.abortSignal
112
+ };
113
+ try {
114
+ return await tool.execute(args, toolCtx);
115
+ } catch (err) {
116
+ throw err instanceof Error ? err : new Error(String(err));
117
+ }
118
+ }
119
+ });
120
+ }
121
+ return tools;
122
+ }
123
+ function toCoreMessages(input) {
124
+ if (input.messages) return input.messages;
125
+ return input.prompt ? [{
126
+ role: "user",
127
+ content: input.prompt
128
+ }] : [];
129
+ }
130
+ async function run(input) {
131
+ const { ai, model } = await createModel();
132
+ const tools = await buildTools(input);
133
+ const messages = toCoreMessages(input);
134
+ const result = await ai.generateText({
135
+ model,
136
+ system,
137
+ messages,
138
+ tools,
139
+ stopWhen: ai.stepCountIs(config.maxSteps ?? 5),
140
+ maxOutputTokens: config.maxTokens,
141
+ temperature: config.temperature,
142
+ headers: config.headers
143
+ });
144
+ const toolResults = (result.toolResults ?? []).map((tr) => ({
145
+ toolName: tr.toolName,
146
+ toolCallId: tr.toolCallId,
147
+ result: tr.output
148
+ }));
149
+ return {
150
+ text: result.text,
151
+ output: result.output,
152
+ toolResults,
153
+ usage: result.usage ? {
154
+ inputTokens: result.usage.inputTokens ?? 0,
155
+ outputTokens: result.usage.outputTokens ?? 0,
156
+ totalTokens: (result.usage.inputTokens ?? 0) + (result.usage.outputTokens ?? 0)
157
+ } : void 0
158
+ };
159
+ }
160
+ async function* stream(input) {
161
+ const { ai, model } = await createModel();
162
+ const tools = await buildTools(input);
163
+ const messages = toCoreMessages(input);
164
+ const result = ai.streamText({
165
+ model,
166
+ system,
167
+ messages,
168
+ tools,
169
+ stopWhen: ai.stepCountIs(config.maxSteps ?? 5),
170
+ maxOutputTokens: config.maxTokens,
171
+ temperature: config.temperature,
172
+ headers: config.headers
173
+ });
174
+ for await (const part of result.fullStream) switch (part.type) {
175
+ case "text-delta":
176
+ yield {
177
+ type: "text-delta",
178
+ text: part.text
179
+ };
180
+ break;
181
+ case "tool-call":
182
+ yield {
183
+ type: "tool-call",
184
+ toolCallId: part.toolCallId,
185
+ toolName: part.toolName,
186
+ input: part.input
187
+ };
188
+ break;
189
+ case "tool-result":
190
+ yield {
191
+ type: "tool-result",
192
+ toolCallId: part.toolCallId,
193
+ toolName: part.toolName,
194
+ result: part.output
195
+ };
196
+ break;
197
+ case "error":
198
+ yield {
199
+ type: "error",
200
+ error: part.error instanceof Error ? part.error.message : String(part.error)
201
+ };
202
+ return;
203
+ case "finish": {
204
+ const finish = part;
205
+ yield {
206
+ type: "done",
207
+ usage: finish.usage ? {
208
+ inputTokens: finish.usage.inputTokens ?? 0,
209
+ outputTokens: finish.usage.outputTokens ?? 0,
210
+ totalTokens: (finish.usage.inputTokens ?? 0) + (finish.usage.outputTokens ?? 0)
211
+ } : void 0
212
+ };
213
+ return;
214
+ }
215
+ }
216
+ }
217
+ /**
218
+ * Convert this agent into a Hono handler. Reads the request body as
219
+ * `AgentInput` and streams the result as SSE (`data: <json>\n\n`), directly
220
+ * compatible with `useChat`'s SSE parser.
221
+ */
222
+ function asHandler() {
223
+ return async (c) => {
224
+ const input = await c.req.json().catch(() => ({}));
225
+ const { streamSSE } = await import("hono/streaming");
226
+ return streamSSE(c, async (sse) => {
227
+ for await (const chunk of stream(input)) await sse.writeSSE({ data: JSON.stringify(chunk) });
228
+ });
229
+ };
230
+ }
231
+ return {
232
+ config,
233
+ run,
234
+ stream,
235
+ asHandler
236
+ };
237
+ }
238
+ //#endregion
239
+ export { resolveModel as a, defineProvider as i, defineAgent as n, defineAgentTool as r, configureAI as t };
@@ -0,0 +1,52 @@
1
+ import { m as ProviderDefinition } from "./types-B5I0sGbf.js";
2
+ //#region src/gateway.d.ts
3
+ /**
4
+ * DeepSeek (https://www.deepseek.com/).
5
+ * Default base URL is `https://api.deepseek.com/v1`.
6
+ * Requires `DEEPSEEK_API_KEY` (or `UBEAN_AI_API_KEY`) env variable or manual `apiKey`.
7
+ */
8
+ declare const deepseek: ProviderDefinition;
9
+ /**
10
+ * OpenRouter (https://openrouter.ai/).
11
+ * Base URL `https://openrouter.ai/api/v1`.
12
+ * Requires `OPENROUTER_API_KEY`.
13
+ */
14
+ declare const openrouter: ProviderDefinition;
15
+ /**
16
+ * OmniRoute (https://github.com/diegosouzapw/OmniRoute) — the Free AI Gateway.
17
+ * This assumes you are running OmniRoute locally or have deployed it.
18
+ * Default base URL is the default local endpoint `http://localhost:8080/v1`.
19
+ * If you are running OmniRoute elsewhere, override `baseURL` when registering.
20
+ */
21
+ declare const omniRoute: ProviderDefinition;
22
+ /**
23
+ * OpenAI (https://platform.openai.com/).
24
+ * Base URL `https://api.openai.com/v1`.
25
+ * Requires `OPENAI_API_KEY`.
26
+ *
27
+ * Note: you can also use this provider via `@ai-sdk/openai` directly; this
28
+ * definition is for when you are routing OpenAI through a proxy/gateway.
29
+ */
30
+ declare const openai: ProviderDefinition;
31
+ /**
32
+ * Anthropic via OpenAI-compatible endpoint.
33
+ * Anthropic's native endpoint is not OpenAI-compatible out of the box, but
34
+ * many gateways (including OmniRoute) expose it as such.
35
+ */
36
+ declare const anthropic: ProviderDefinition;
37
+ /**
38
+ * Google Gemini via OpenAI-compatible endpoint.
39
+ * Gemini's native endpoint is not OpenAI-compatible out of the box; this
40
+ * definition is for when you are using a gateway that exposes it as such.
41
+ */
42
+ declare const google: ProviderDefinition;
43
+ /**
44
+ * Groq (https://groq.com/).
45
+ * OpenAI-compatible endpoint out of the box.
46
+ * Requires `GROQ_API_KEY`.
47
+ */
48
+ declare const groq: ProviderDefinition;
49
+ /** All built-in gateway providers exported from this module. */
50
+ declare const allGatewayProviders: ProviderDefinition[];
51
+ //#endregion
52
+ export { type ProviderDefinition, allGatewayProviders, anthropic, deepseek, google, groq, omniRoute, openai, openrouter };
@@ -0,0 +1,87 @@
1
+ //#region src/gateway.ts
2
+ /**
3
+ * DeepSeek (https://www.deepseek.com/).
4
+ * Default base URL is `https://api.deepseek.com/v1`.
5
+ * Requires `DEEPSEEK_API_KEY` (or `UBEAN_AI_API_KEY`) env variable or manual `apiKey`.
6
+ */
7
+ const deepseek = {
8
+ id: "deepseek",
9
+ kind: "openai-compatible",
10
+ baseURL: "https://api.deepseek.com/v1"
11
+ };
12
+ /**
13
+ * OpenRouter (https://openrouter.ai/).
14
+ * Base URL `https://openrouter.ai/api/v1`.
15
+ * Requires `OPENROUTER_API_KEY`.
16
+ */
17
+ const openrouter = {
18
+ id: "openrouter",
19
+ kind: "openai-compatible",
20
+ baseURL: "https://openrouter.ai/api/v1"
21
+ };
22
+ /**
23
+ * OmniRoute (https://github.com/diegosouzapw/OmniRoute) — the Free AI Gateway.
24
+ * This assumes you are running OmniRoute locally or have deployed it.
25
+ * Default base URL is the default local endpoint `http://localhost:8080/v1`.
26
+ * If you are running OmniRoute elsewhere, override `baseURL` when registering.
27
+ */
28
+ const omniRoute = {
29
+ id: "omniroute",
30
+ kind: "openai-compatible",
31
+ baseURL: "http://localhost:8080/v1"
32
+ };
33
+ /**
34
+ * OpenAI (https://platform.openai.com/).
35
+ * Base URL `https://api.openai.com/v1`.
36
+ * Requires `OPENAI_API_KEY`.
37
+ *
38
+ * Note: you can also use this provider via `@ai-sdk/openai` directly; this
39
+ * definition is for when you are routing OpenAI through a proxy/gateway.
40
+ */
41
+ const openai = {
42
+ id: "openai",
43
+ kind: "openai-compatible",
44
+ baseURL: "https://api.openai.com/v1"
45
+ };
46
+ /**
47
+ * Anthropic via OpenAI-compatible endpoint.
48
+ * Anthropic's native endpoint is not OpenAI-compatible out of the box, but
49
+ * many gateways (including OmniRoute) expose it as such.
50
+ */
51
+ const anthropic = {
52
+ id: "anthropic",
53
+ kind: "openai-compatible",
54
+ baseURL: "https://api.anthropic.com/v1"
55
+ };
56
+ /**
57
+ * Google Gemini via OpenAI-compatible endpoint.
58
+ * Gemini's native endpoint is not OpenAI-compatible out of the box; this
59
+ * definition is for when you are using a gateway that exposes it as such.
60
+ */
61
+ const google = {
62
+ id: "google",
63
+ kind: "openai-compatible",
64
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai"
65
+ };
66
+ /**
67
+ * Groq (https://groq.com/).
68
+ * OpenAI-compatible endpoint out of the box.
69
+ * Requires `GROQ_API_KEY`.
70
+ */
71
+ const groq = {
72
+ id: "groq",
73
+ kind: "openai-compatible",
74
+ baseURL: "https://api.groq.com/openai/v1"
75
+ };
76
+ /** All built-in gateway providers exported from this module. */
77
+ const allGatewayProviders = [
78
+ deepseek,
79
+ openrouter,
80
+ omniRoute,
81
+ openai,
82
+ anthropic,
83
+ google,
84
+ groq
85
+ ];
86
+ //#endregion
87
+ export { allGatewayProviders, anthropic, deepseek, google, groq, omniRoute, openai, openrouter };
@@ -0,0 +1,25 @@
1
+ import { _ as UbeanAIProviderOptions, a as AgentStreamChunk, c as AgentToolContext, d as AgentUsage, f as AgentUserMessage, g as ResolvedModel, h as ProviderKind, i as AgentRunResult, l as AgentToolResult, m as ProviderDefinition, n as AgentConfig, o as AgentTool, p as ModelString, r as AgentInput, s as AgentToolCall, t as AgentAssistantMessage, u as AgentToolSet, v as UbeanAgent } from "./types-B5I0sGbf.js";
2
+ import * as v from "valibot";
3
+ //#region src/core.d.ts
4
+ /** Register a provider (idempotent by id). */
5
+ declare function defineProvider(definition: ProviderDefinition): void;
6
+ /** Configure the fallback provider used when a model string has no provider segment. */
7
+ declare function configureAI(options: {
8
+ defaultProvider?: string;
9
+ }): void;
10
+ /**
11
+ * Resolve a `provider/model` string to `{ provider, modelId }`.
12
+ * Falls back to the configured default provider, then to an env-driven
13
+ * OpenAI-compatible provider (mirrors devtools' DeepSeek defaults).
14
+ */
15
+ declare function resolveModel(model: string): ResolvedModel;
16
+ /** Declare a typed ubean tool. Returns the tool unchanged (type-safe identity). */
17
+ declare function defineAgentTool<T extends AgentTool['input']>(tool: AgentTool<T>): AgentTool<T>;
18
+ /**
19
+ * Build a declarative agent. The loop is delegated to the AI SDK's
20
+ * `streamText`/`generateText` (with `stopWhen: stepCountIs(maxSteps)`); this
21
+ * only returns the ubean-idiomatic surface (`run`/`stream`/`asHandler`).
22
+ */
23
+ declare function defineAgent<T = unknown>(config: AgentConfig): UbeanAgent<T>;
24
+ //#endregion
25
+ export { type AgentAssistantMessage, type AgentConfig, type AgentInput, type AgentRunResult, type AgentStreamChunk, type AgentTool, type AgentToolCall, type AgentToolContext, type AgentToolResult, type AgentToolSet, type AgentUsage, type AgentUserMessage, type ModelString, type ProviderDefinition, type ProviderKind, type ResolvedModel, type UbeanAIProviderOptions, type UbeanAgent, configureAI, defineAgent, defineAgentTool, defineProvider, resolveModel, v };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { a as resolveModel, i as defineProvider, n as defineAgent, r as defineAgentTool, t as configureAI } from "./core-B-pZdSgG.js";
2
+ import * as v from "valibot";
3
+ export { configureAI, defineAgent, defineAgentTool, defineProvider, resolveModel, v };
@@ -0,0 +1,74 @@
1
+ import { a as AgentStreamChunk, i as AgentRunResult, l as AgentToolResult, r as AgentInput, s as AgentToolCall, v as UbeanAgent } from "../types-B5I0sGbf.js";
2
+ import { ComputedRef, Ref } from "vue";
3
+ //#region src/runtime/vue.d.ts
4
+ /** A single chat message managed by `useChat`. */
5
+ interface ChatMessage {
6
+ id: string;
7
+ role: 'user' | 'assistant';
8
+ content: string;
9
+ /** Tool calls emitted during this assistant turn. */
10
+ toolCalls?: AgentToolCall[];
11
+ /** Tool results collected during this assistant turn. */
12
+ toolResults?: AgentToolResult[];
13
+ createdAt?: number;
14
+ }
15
+ type ChatStatus = 'ready' | 'streaming' | 'error';
16
+ interface UseChatOptions {
17
+ /** SSE endpoint. Defaults to a relative `./chat` route if not provided. */
18
+ url?: string;
19
+ /** Initial conversation history. */
20
+ initialMessages?: ChatMessage[];
21
+ /** Extra fields merged into the request body (e.g. `meta`). */
22
+ body?: Record<string, unknown>;
23
+ /** Custom fetch implementation (for wire mocking / tests). */
24
+ fetch?: typeof fetch;
25
+ }
26
+ /** Set the provider config used by `useAIProvider` (called by the Vite plugin). */
27
+ declare function setAIProviderConfig(config: {
28
+ defaultProvider?: string;
29
+ defaultModel?: string;
30
+ }): void;
31
+ interface UseChatReturn {
32
+ /** Reactive message list. */
33
+ messages: Ref<ChatMessage[]>;
34
+ /** 'ready' | 'streaming' | 'error'. */
35
+ status: Ref<ChatStatus>;
36
+ /** Last error, if any. */
37
+ error: Ref<Error | null>;
38
+ /** Text input bound to the chat box. */
39
+ input: Ref<string>;
40
+ /** Raw stream chunks of the latest turn. */
41
+ data: Ref<AgentStreamChunk[]>;
42
+ /** Set the input text programmatically. */
43
+ setInput: (value: string) => void;
44
+ /** Send a message (uses `input` if `content` omitted). */
45
+ sendMessage: (content?: string) => Promise<void>;
46
+ /** Abort the in-flight stream. */
47
+ abort: () => void;
48
+ /** Re-send the last user message. */
49
+ reload: () => Promise<void>;
50
+ /** Whether the chat is currently streaming. */
51
+ isStreaming: ComputedRef<boolean>;
52
+ }
53
+ /**
54
+ * SSR-safe chat composable. Maintains reactive message state and streams
55
+ * `AgentStreamChunk` SSE frames from the `asHandler()` endpoint.
56
+ */
57
+ declare function useChat(options?: UseChatOptions): UseChatReturn;
58
+ interface UseAgentReturn<T = unknown> {
59
+ isStreaming: Ref<boolean>;
60
+ error: Ref<Error | null>;
61
+ result: Ref<AgentRunResult<T> | null>;
62
+ chunks: Ref<AgentStreamChunk[]>;
63
+ run: (input: AgentInput) => Promise<void>;
64
+ stream: (input: AgentInput) => Promise<void>;
65
+ }
66
+ /** Drive a `UbeanAgent` directly from a component with reactive state. */
67
+ declare function useAgent<T = unknown>(agent: UbeanAgent<T>): UseAgentReturn<T>;
68
+ /** Read the current fallback provider/model config (set by the Vite plugin). */
69
+ declare function useAIProvider(): {
70
+ defaultProvider?: string;
71
+ defaultModel?: string;
72
+ };
73
+ //#endregion
74
+ export { type AgentInput, type AgentRunResult, type AgentStreamChunk, type AgentToolCall, type AgentToolResult, ChatMessage, ChatStatus, type UbeanAgent, UseAgentReturn, UseChatOptions, UseChatReturn, setAIProviderConfig, useAIProvider, useAgent, useChat };
@@ -0,0 +1,213 @@
1
+ import { computed, ref, shallowRef } from "vue";
2
+ //#region src/runtime/vue.ts
3
+ /**
4
+ * `@ubean/ai/runtime/vue` — Vue runtime composables.
5
+ *
6
+ * Self-built, SSR-safe thin layer over the `@ubean/ai` kernel. It does NOT
7
+ * depend on `@ai-sdk/vue`; it talks directly to the SSE protocol emitted by
8
+ * `UbeanAgent.asHandler()` (chunks of `AgentStreamChunk`).
9
+ *
10
+ * Rendering is intentionally out of scope (decision C): this module only
11
+ * produces message state; users pick their own markdown/code renderer.
12
+ */
13
+ /** Parse a raw SSE byte stream into a JSON `AgentStreamChunk` generator. */
14
+ async function* parseSseStream(stream, onChunk) {
15
+ const reader = stream.getReader();
16
+ const decoder = new TextDecoder();
17
+ let buffer = "";
18
+ while (true) {
19
+ const { done, value } = await reader.read();
20
+ if (done) break;
21
+ buffer += decoder.decode(value, { stream: true });
22
+ let sepIndex;
23
+ while ((sepIndex = buffer.indexOf("\n\n")) !== -1) {
24
+ const rawEvent = buffer.slice(0, sepIndex);
25
+ buffer = buffer.slice(sepIndex + 2);
26
+ const event = parseSseEvent(rawEvent);
27
+ if (!event.data) continue;
28
+ let chunk;
29
+ try {
30
+ chunk = JSON.parse(event.data);
31
+ } catch {
32
+ continue;
33
+ }
34
+ onChunk(chunk);
35
+ yield chunk;
36
+ }
37
+ }
38
+ }
39
+ function parseSseEvent(raw) {
40
+ let data = "";
41
+ for (const line of raw.split(/\r?\n/)) if (line.startsWith("data:")) data += line.slice(5).trimStart();
42
+ return { data };
43
+ }
44
+ function nextId() {
45
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
46
+ }
47
+ let providerConfig = {};
48
+ /** Set the provider config used by `useAIProvider` (called by the Vite plugin). */
49
+ function setAIProviderConfig(config) {
50
+ providerConfig = config;
51
+ }
52
+ /**
53
+ * SSR-safe chat composable. Maintains reactive message state and streams
54
+ * `AgentStreamChunk` SSE frames from the `asHandler()` endpoint.
55
+ */
56
+ function useChat(options = {}) {
57
+ const messages = ref(options.initialMessages ?? []);
58
+ const status = ref("ready");
59
+ const error = ref(null);
60
+ const input = ref("");
61
+ const data = ref([]);
62
+ let controller = null;
63
+ let lastUserPrompt = null;
64
+ const isStreaming = computed(() => status.value === "streaming");
65
+ const url = options.url ?? "./chat";
66
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
67
+ function setInput(value) {
68
+ input.value = value;
69
+ }
70
+ function abort() {
71
+ controller?.abort();
72
+ }
73
+ async function sendMessage(content) {
74
+ const text = content ?? input.value;
75
+ if (!text || status.value === "streaming") return;
76
+ const userMsg = {
77
+ id: nextId(),
78
+ role: "user",
79
+ content: text,
80
+ createdAt: Date.now()
81
+ };
82
+ messages.value = [...messages.value, userMsg];
83
+ input.value = "";
84
+ data.value = [];
85
+ lastUserPrompt = text;
86
+ const body = {
87
+ ...options.body,
88
+ messages: messages.value.map(({ role, content: c }) => ({
89
+ role,
90
+ content: c
91
+ }))
92
+ };
93
+ if (!fetchImpl) {
94
+ status.value = "error";
95
+ error.value = /* @__PURE__ */ new Error("fetch is not available (SSR?) — useChat should only run on the client");
96
+ return;
97
+ }
98
+ controller = new AbortController();
99
+ status.value = "streaming";
100
+ error.value = null;
101
+ let assistantId = nextId();
102
+ const assistantMsg = {
103
+ id: assistantId,
104
+ role: "assistant",
105
+ content: "",
106
+ createdAt: Date.now()
107
+ };
108
+ messages.value = [...messages.value, assistantMsg];
109
+ try {
110
+ const res = await fetchImpl(url, {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify(body),
114
+ signal: controller.signal
115
+ });
116
+ if (!res.ok || !res.body) throw new Error(`Chat request failed with status ${res.status}`);
117
+ const applyChunk = (chunk) => {
118
+ data.value.push(chunk);
119
+ const idx = messages.value.findIndex((m) => m.id === assistantId);
120
+ if (idx === -1) return;
121
+ const current = messages.value[idx];
122
+ if (chunk.type === "text-delta") current.content += chunk.text;
123
+ else if (chunk.type === "tool-call") current.toolCalls = [...current.toolCalls ?? [], {
124
+ toolCallId: chunk.toolCallId,
125
+ toolName: chunk.toolName,
126
+ input: chunk.input
127
+ }];
128
+ else if (chunk.type === "tool-result") current.toolResults = [...current.toolResults ?? [], {
129
+ toolName: chunk.toolName,
130
+ toolCallId: chunk.toolCallId,
131
+ result: chunk.result,
132
+ error: chunk.error
133
+ }];
134
+ messages.value = [...messages.value];
135
+ };
136
+ for await (const _chunk of parseSseStream(res.body, applyChunk));
137
+ status.value = "ready";
138
+ } catch (err) {
139
+ if (controller.signal.aborted) {
140
+ status.value = "ready";
141
+ return;
142
+ }
143
+ status.value = "error";
144
+ error.value = err instanceof Error ? err : new Error(String(err));
145
+ } finally {
146
+ controller = null;
147
+ }
148
+ }
149
+ async function reload() {
150
+ if (!lastUserPrompt || status.value === "streaming") return;
151
+ const last = messages.value[messages.value.length - 1];
152
+ if (last && last.role === "assistant" && !last.content && !last.toolCalls?.length && !last.toolResults?.length) messages.value = messages.value.slice(0, -1);
153
+ await sendMessage(lastUserPrompt);
154
+ }
155
+ return {
156
+ messages,
157
+ status,
158
+ error,
159
+ input,
160
+ data,
161
+ setInput,
162
+ sendMessage,
163
+ abort,
164
+ reload,
165
+ isStreaming
166
+ };
167
+ }
168
+ /** Drive a `UbeanAgent` directly from a component with reactive state. */
169
+ function useAgent(agent) {
170
+ const isStreaming = ref(false);
171
+ const error = ref(null);
172
+ const result = shallowRef(null);
173
+ const chunks = ref([]);
174
+ async function run(input) {
175
+ isStreaming.value = true;
176
+ error.value = null;
177
+ chunks.value = [];
178
+ try {
179
+ result.value = await agent.run(input);
180
+ } catch (err) {
181
+ error.value = err instanceof Error ? err : new Error(String(err));
182
+ } finally {
183
+ isStreaming.value = false;
184
+ }
185
+ }
186
+ async function stream(input) {
187
+ isStreaming.value = true;
188
+ error.value = null;
189
+ result.value = null;
190
+ chunks.value = [];
191
+ try {
192
+ for await (const chunk of agent.stream(input)) chunks.value.push(chunk);
193
+ } catch (err) {
194
+ error.value = err instanceof Error ? err : new Error(String(err));
195
+ } finally {
196
+ isStreaming.value = false;
197
+ }
198
+ }
199
+ return {
200
+ isStreaming,
201
+ error,
202
+ result,
203
+ chunks,
204
+ run,
205
+ stream
206
+ };
207
+ }
208
+ /** Read the current fallback provider/model config (set by the Vite plugin). */
209
+ function useAIProvider() {
210
+ return providerConfig;
211
+ }
212
+ //#endregion
213
+ export { setAIProviderConfig, useAIProvider, useAgent, useChat };
@@ -0,0 +1,195 @@
1
+ import { GenericSchema, InferOutput } from "valibot";
2
+ import { Handler } from "hono";
3
+ //#region src/types.d.ts
4
+ /**
5
+ * @ubean/ai type definitions.
6
+ *
7
+ * Kernel design (B2): a thin, declarative orchestration layer over the Vercel
8
+ * AI SDK (`ai` / `@ai-sdk/openai-compatible`). The agent loop, tool-calling
9
+ * protocol and streaming are delegated to the AI SDK; this package only adds
10
+ * a ubean-idiomatic configuration surface, provider resolution and a Vue
11
+ * runtime. It does NOT re-implement an agent loop or a gateway.
12
+ */
13
+ /**
14
+ * A model reference in the form `"provider/model"`, e.g. `"deepseek/deepseek-chat"`,
15
+ * `"openai/gpt-5.4"`, `"openrouter/anthropic/claude-3.5-sonnet"`.
16
+ *
17
+ * The provider segment is resolved against the provider registry (see
18
+ * `resolveProvider`). The special provider `"openai-compatible"` (or any
19
+ * provider resolved through the gateway/baseURL mechanism) maps to
20
+ * `@ai-sdk/openai-compatible`.
21
+ */
22
+ type ModelString = string;
23
+ /** Provider kind used to pick the underlying AI SDK provider factory. */
24
+ type ProviderKind = 'openai-compatible' | 'openai' | 'anthropic' | 'google' | 'custom';
25
+ /** A registered provider that `@ubean/ai` can resolve `provider/model` strings against. */
26
+ interface ProviderDefinition {
27
+ /** Provider identifier used in the `provider/model` string. */
28
+ id: string;
29
+ /** Provider kind — currently only `openai-compatible` is bundled. */
30
+ kind: ProviderKind;
31
+ /** Base URL for the OpenAI-compatible API. */
32
+ baseURL: string;
33
+ /** API key. Prefer injecting via runtime config / env rather than hardcoding. */
34
+ apiKey?: string;
35
+ /** Optional extra headers forwarded to the provider. */
36
+ headers?: Record<string, string>;
37
+ /** Optional per-provider model alias map (alias → canonical model id). */
38
+ modelAliases?: Record<string, string>;
39
+ }
40
+ /** The result of resolving a `provider/model` string. */
41
+ interface ResolvedModel {
42
+ /** The provider definition that handled the model string. */
43
+ provider: ProviderDefinition;
44
+ /** The model id to pass to the provider factory. */
45
+ modelId: string;
46
+ }
47
+ /** Context passed to a tool's `execute`. Carries per-run request metadata. */
48
+ interface AgentToolContext {
49
+ /** A stable id for the current agent run. */
50
+ runId: string;
51
+ /** Free-form metadata injected by the caller (e.g. an authenticated user). */
52
+ meta?: Record<string, unknown>;
53
+ /** Abort signal to cancel the run. */
54
+ signal?: AbortSignal;
55
+ }
56
+ /**
57
+ * A declarative ubean tool. `execute` may use ubean server helpers
58
+ * (defineHandler, fetch, storage, ...) and must return a JSON-serializable value.
59
+ */
60
+ interface AgentTool<TInput extends GenericSchema = GenericSchema> {
61
+ /** Unique tool name sent to the model (lowercase, safe identifier). */
62
+ name: string;
63
+ /** Description used by the model for tool selection. */
64
+ description: string;
65
+ /** Valibot schema describing the tool's arguments. */
66
+ input: TInput;
67
+ /** Executes the tool; `args` is the parsed input. */
68
+ execute: (args: InferOutput<TInput>, ctx: AgentToolContext) => unknown | Promise<unknown>;
69
+ }
70
+ /** A map of named tools. */
71
+ type AgentToolSet = Record<string, AgentTool<any>>;
72
+ interface AgentConfig {
73
+ /** `provider/model` string resolved through the provider registry. */
74
+ model: ModelString;
75
+ /** System prompt. */
76
+ system?: string;
77
+ /** Static context injected into the system prompt (serialized as JSON snippet). */
78
+ context?: Record<string, unknown>;
79
+ /** Named tools available to the agent. */
80
+ tools?: AgentToolSet;
81
+ /** Max tool-calling steps before the loop stops (default 5). */
82
+ maxSteps?: number;
83
+ /** Sampling temperature (default provider-dependent). */
84
+ temperature?: number;
85
+ /** Max output tokens. */
86
+ maxTokens?: number;
87
+ /** Extra headers forwarded to the provider. */
88
+ headers?: Record<string, string>;
89
+ /** Optional provider override (takes precedence over `model`'s provider). */
90
+ provider?: ProviderDefinition;
91
+ }
92
+ /** Input to a single agent run. Either a raw prompt or a message list. */
93
+ interface AgentInput {
94
+ /** Single user prompt. */
95
+ prompt?: string;
96
+ /** Full message history (prefer when resuming a conversation). */
97
+ messages?: Array<AgentUserMessage | AgentAssistantMessage>;
98
+ /** Per-run metadata exposed to tools via `AgentToolContext.meta`. */
99
+ meta?: Record<string, unknown>;
100
+ }
101
+ /** User message shape accepted by the kernel. */
102
+ interface AgentUserMessage {
103
+ role: 'user';
104
+ content: string;
105
+ }
106
+ /** Assistant message shape accepted by the kernel. */
107
+ interface AgentAssistantMessage {
108
+ role: 'assistant';
109
+ content: string;
110
+ }
111
+ /** Token usage for a run. */
112
+ interface AgentUsage {
113
+ inputTokens: number;
114
+ outputTokens: number;
115
+ totalTokens: number;
116
+ }
117
+ /** A tool call emitted by the model during a run. */
118
+ interface AgentToolCall {
119
+ toolCallId: string;
120
+ toolName: string;
121
+ /** The arguments the model provided for the call. */
122
+ input: Record<string, unknown>;
123
+ }
124
+ /** Result of a completed tool call. */
125
+ interface AgentToolResult {
126
+ toolName: string;
127
+ toolCallId: string;
128
+ /** Returned value (may be undefined). */
129
+ result?: unknown;
130
+ /** Present when the tool threw. */
131
+ error?: string;
132
+ }
133
+ /** Result of a non-streaming `run()`. */
134
+ interface AgentRunResult<T = unknown> {
135
+ /** Final assistant text. */
136
+ text: string;
137
+ /** Structured output when the agent is configured with an output schema. */
138
+ output?: T;
139
+ /** Every tool call executed during the run. */
140
+ toolResults: AgentToolResult[];
141
+ /** Token usage. */
142
+ usage?: AgentUsage;
143
+ }
144
+ /** Discriminated streaming chunk emitted by `stream()`. */
145
+ type AgentStreamChunk = {
146
+ type: 'text-delta';
147
+ text: string;
148
+ } | {
149
+ type: 'tool-call';
150
+ toolCallId: string;
151
+ toolName: string;
152
+ input: Record<string, unknown>;
153
+ } | {
154
+ type: 'tool-result';
155
+ toolCallId: string;
156
+ toolName: string;
157
+ result?: unknown;
158
+ error?: string;
159
+ } | {
160
+ type: 'error';
161
+ error: string;
162
+ } | {
163
+ type: 'done';
164
+ usage?: AgentUsage;
165
+ };
166
+ /**
167
+ * The concrete agent returned by `defineAgent`. All orchestration is delegated
168
+ * to the AI SDK (`ToolLoopAgent` / `streamText`); this object only exposes the
169
+ * ubean-idiomatic surface.
170
+ */
171
+ interface UbeanAgent<T = unknown> {
172
+ /** The normalized config this agent was built from. */
173
+ readonly config: AgentConfig;
174
+ /** Run once and await the full result. */
175
+ run(input: AgentInput): Promise<AgentRunResult<T>>;
176
+ /** Stream the run as an async iterable of chunks. */
177
+ stream(input: AgentInput): AsyncIterable<AgentStreamChunk>;
178
+ /**
179
+ * Convert this agent into a Hono handler for an API route. Reads the request
180
+ * body as `AgentInput`, streams the response as SSE. Compatible with
181
+ * `defineHandler` / `defineHandlerMeta` conventions.
182
+ */
183
+ asHandler(): Handler;
184
+ }
185
+ /** Options for `useUbeanAI` / plugin configuration. */
186
+ interface UbeanAIProviderOptions {
187
+ /** Providers to register. */
188
+ providers?: ProviderDefinition[];
189
+ /** Default provider id used when the model string has no provider segment. */
190
+ defaultProvider?: string;
191
+ /** Default model id used when neither the model string nor defaultProvider resolve. */
192
+ defaultModel?: string;
193
+ }
194
+ //#endregion
195
+ export { UbeanAIProviderOptions as _, AgentStreamChunk as a, AgentToolContext as c, AgentUsage as d, AgentUserMessage as f, ResolvedModel as g, ProviderKind as h, AgentRunResult as i, AgentToolResult as l, ProviderDefinition as m, AgentConfig as n, AgentTool as o, ModelString as p, AgentInput as r, AgentToolCall as s, AgentAssistantMessage as t, AgentToolSet as u, UbeanAgent as v };
package/dist/vite.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { m as ProviderDefinition } from "./types-B5I0sGbf.js";
2
+ import { Plugin } from "vite";
3
+ //#region src/vite.d.ts
4
+ interface UbeanAiOptions {
5
+ /** Master switch. Disabled plugin returns a noop. */
6
+ enabled?: boolean;
7
+ /** Provider presets to register on the server kernel. */
8
+ providers?: ProviderDefinition[];
9
+ /** Default provider id used when a model string has no provider segment. */
10
+ defaultProvider?: string;
11
+ /** Auto-import the runtime composables (default true). */
12
+ autoImport?: boolean;
13
+ }
14
+ /** Type-safe helper for defining AI options (pass-through). */
15
+ declare function defineAiConfig(options: UbeanAiOptions): UbeanAiOptions;
16
+ declare function ubeanAiPlugin(userOptions?: UbeanAiOptions): Plugin;
17
+ //#endregion
18
+ export { UbeanAiOptions, ubeanAiPlugin as default, ubeanAiPlugin, defineAiConfig };
package/dist/vite.js ADDED
@@ -0,0 +1,58 @@
1
+ import { i as defineProvider } from "./core-B-pZdSgG.js";
2
+ import { registerComponentResolver } from "@ubean/build";
3
+ //#region src/vite.ts
4
+ /** Composable API names auto-importable from `@ubean/ai/runtime/vue`. */
5
+ const RUNTIME_COMPOSABLES = [
6
+ "useChat",
7
+ "useAgent",
8
+ "useAIProvider"
9
+ ];
10
+ /** Type-safe helper for defining AI options (pass-through). */
11
+ function defineAiConfig(options) {
12
+ return options;
13
+ }
14
+ const VIRTUAL_RUNTIME_ID = "virtual:ubean-ai/runtime";
15
+ const RESOLVED_VIRTUAL_RUNTIME_ID = `\0${VIRTUAL_RUNTIME_ID}`;
16
+ function ubeanAiPlugin(userOptions = {}) {
17
+ const options = userOptions;
18
+ if (options.enabled === false) return {
19
+ name: "ubean:ai:noop",
20
+ enforce: "post"
21
+ };
22
+ const autoImportEnabled = options.autoImport !== false;
23
+ for (const provider of options.providers ?? []) defineProvider(provider);
24
+ if (autoImportEnabled) registerComponentResolver((name) => {
25
+ if (RUNTIME_COMPOSABLES.includes(name)) return {
26
+ name,
27
+ from: "@ubean/ai/runtime/vue"
28
+ };
29
+ });
30
+ return {
31
+ name: "ubean:ai",
32
+ enforce: "pre",
33
+ resolveId(id) {
34
+ if (id === VIRTUAL_RUNTIME_ID) return RESOLVED_VIRTUAL_RUNTIME_ID;
35
+ return null;
36
+ },
37
+ load(id) {
38
+ if (id === RESOLVED_VIRTUAL_RUNTIME_ID) return generateRuntimeVirtualModule(options);
39
+ return null;
40
+ },
41
+ config() {
42
+ return { optimizeDeps: { include: ["@ubean/ai/runtime/vue"] } };
43
+ }
44
+ };
45
+ }
46
+ /** Generate the client virtual module that injects config and re-exports the runtime. */
47
+ function generateRuntimeVirtualModule(options) {
48
+ const defaultProvider = options.defaultProvider ?? "";
49
+ return `
50
+ import { useChat, useAgent, useAIProvider, setAIProviderConfig } from '@ubean/ai/runtime/vue';
51
+
52
+ ${defaultProvider ? `setAIProviderConfig({ defaultProvider: ${JSON.stringify(defaultProvider)} });` : ""}
53
+
54
+ export { useChat, useAgent, useAIProvider, setAIProviderConfig };
55
+ `;
56
+ }
57
+ //#endregion
58
+ export { ubeanAiPlugin as default, ubeanAiPlugin, defineAiConfig };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@ubean/ai",
3
+ "version": "0.2.0",
4
+ "description": "AI large-model integration for the ubean framework: defineAgent + defineAgentTool + Vue runtime (thin orchestration over the Vercel AI SDK)",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./vite": {
18
+ "types": "./dist/vite.d.ts",
19
+ "import": "./dist/vite.js"
20
+ },
21
+ "./runtime/vue": {
22
+ "types": "./dist/runtime/vue.d.ts",
23
+ "import": "./dist/runtime/vue.js"
24
+ },
25
+ "./gateway": {
26
+ "types": "./dist/gateway.d.ts",
27
+ "import": "./dist/gateway.js"
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "@valibot/to-json-schema": "^1.7.1",
32
+ "valibot": "^1.4.2",
33
+ "@ubean/build": "0.2.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^26.2.0",
37
+ "hono": "4.13.2",
38
+ "typescript": "7.0.2",
39
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.9",
40
+ "vite-plus": "0.2.9",
41
+ "vue": "^3.5.41"
42
+ },
43
+ "peerDependencies": {
44
+ "@ai-sdk/openai-compatible": "^1.0.0 || ^2.0.0 || ^3.0.0",
45
+ "ai": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
46
+ "hono": "^4.0.0",
47
+ "vite": "^5.0.0 || ^6.0.0",
48
+ "vue": "^3.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "ai": {
52
+ "optional": true
53
+ },
54
+ "@ai-sdk/openai-compatible": {
55
+ "optional": true
56
+ },
57
+ "hono": {
58
+ "optional": true
59
+ },
60
+ "vite": {
61
+ "optional": true
62
+ },
63
+ "vue": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "scripts": {
68
+ "build": "vp pack",
69
+ "dev": "vp pack --watch",
70
+ "test": "vp test",
71
+ "typecheck": "tsc --noEmit --skipLibCheck"
72
+ }
73
+ }