@chatu-ai/app-sdk 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @chatu-ai/app-sdk
2
2
 
3
- Data SDK for apps generated by **ChatU Builder**. One API, driver picked from environment variables — works in the Builder preview, on your own server, or with no config at all.
3
+ Data + AI SDK for apps generated by **ChatU Builder**. One API, driver picked from environment variables — works in the Builder preview, on your own server, or with no config at all.
4
4
 
5
5
  ```ts
6
6
  import { kv, storage } from '@chatu-ai/app-sdk' // server-side only (Route Handlers / Server Components / Server Actions)
@@ -21,4 +21,49 @@ const src = await storage.url('avatars/u1.png', { expiresIn: 3600 }) //
21
21
  | `REDIS_URL` and/or `S3_BUCKET` (+ `S3_ENDPOINT` `S3_REGION` `S3_ACCESS_KEY` `S3_SECRET_KEY` `S3_PREFIX`) | **byo** — your own Redis / S3-compatible bucket (Tencent COS, MinIO, AWS) | install optional deps: `npm i ioredis @aws-sdk/client-s3 @aws-sdk/s3-request-presigner` |
22
22
  | none | **memory** — in-process, lost on restart | local dev / fallback |
23
23
 
24
+ ## AI (LLM relay)
25
+
26
+ `ai` calls the platform's OpenAI-compatible endpoint (`{origin}/v1/chat/completions`) with the same app key used by the Data API. **Server-side only** — call it from a Route Handler / Server Action and let the browser `fetch` your own API; never ship the key to the client. Usage is metered and **billed to the app owner's ChatU points**.
27
+
28
+ | Env | Notes |
29
+ | --- | --- |
30
+ | `CHATU_DATA_URL` + `CHATU_APP_KEY` | same as the Data API — the AI base URL is derived by replacing the trailing `/data/v1` with `/v1` |
31
+ | `CHATU_AI_URL` (optional) | explicit override of the AI base URL, e.g. `https://api.chatuapi.com/v1` |
32
+ | `CHATU_AI_MODEL` / `PRIMARY_MODEL` (optional) | default model id when the caller does not pass `model`; the Builder sandbox sets `PRIMARY_MODEL`; if neither is set the server default is used |
33
+
34
+ ```ts
35
+ // app/api/summarize/route.ts — one-shot
36
+ import { ai } from '@chatu-ai/app-sdk'
37
+
38
+ export async function POST(req: Request) {
39
+ const { text } = await req.json()
40
+ const { content, usage } = await ai.chat([
41
+ { role: 'system', content: 'Summarize the user text in one sentence.' },
42
+ { role: 'user', content: text },
43
+ ], { temperature: 0.3, maxTokens: 200 }) // model optional; extra: { top_p, stop, response_format … } passes through
44
+ return Response.json({ summary: content, usage })
45
+ }
46
+ ```
47
+
48
+ ```ts
49
+ // app/api/chat/route.ts — streaming (text/plain chunks; consume in the browser with res.body.getReader())
50
+ import { ai } from '@chatu-ai/app-sdk'
51
+
52
+ export async function POST(req: Request) {
53
+ const { messages } = await req.json() // [{ role: 'user', content: '…' }, …]
54
+ const stream = new ReadableStream<Uint8Array>({
55
+ async start(controller) {
56
+ const enc = new TextEncoder()
57
+ try {
58
+ for await (const delta of ai.stream(messages, { signal: req.signal })) controller.enqueue(enc.encode(delta))
59
+ controller.close()
60
+ } catch (e) { controller.error(e) }
61
+ },
62
+ })
63
+ return new Response(stream, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-cache' } })
64
+ }
65
+ ```
66
+
67
+ `ai.chat('hello')` accepts a plain string as a single user message; `ai.models()` lists available model ids. Without platform env vars (memory / byo drivers) every call rejects with `AppSdkError('AI_NOT_CONFIGURED')` — there is no local fallback for LLM calls.
68
+
24
69
  Never expose `CHATU_APP_KEY` to the browser. MIT.
package/dist/ai.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 应用内 AI 能力(LLM 中继):走平台的 OpenAI 兼容端点 `POST {aiBaseUrl}/chat/completions`,
3
+ * 用与 Data API 相同的应用密钥(sk-conv-…)鉴权,用量由平台按 api-key 计入应用所有者的 ChatU 点数。
4
+ * 只在服务端使用(Route Handler / Server Action);密钥不得暴露给浏览器。
5
+ */
6
+ export interface AiMessage {
7
+ role: 'system' | 'user' | 'assistant';
8
+ content: string;
9
+ }
10
+ export interface AiChatOptions {
11
+ /** 模型 id;缺省用 CHATU_AI_MODEL / PRIMARY_MODEL,都没有则不传由服务端决定 */
12
+ model?: string;
13
+ temperature?: number;
14
+ maxTokens?: number;
15
+ signal?: AbortSignal;
16
+ /** 透传到请求体的其它 OpenAI 兼容字段(如 top_p、stop、response_format) */
17
+ extra?: Record<string, unknown>;
18
+ }
19
+ export interface AiUsage {
20
+ promptTokens?: number;
21
+ completionTokens?: number;
22
+ totalTokens?: number;
23
+ }
24
+ export interface AiChatResult {
25
+ content: string;
26
+ model?: string;
27
+ usage?: AiUsage;
28
+ }
29
+ export interface AiClient {
30
+ /** 一次性对话,返回完整回复 */
31
+ chat(messages: AiMessage[] | string, opts?: AiChatOptions): Promise<AiChatResult>;
32
+ /** 流式对话,逐段产出文本增量 */
33
+ stream(messages: AiMessage[] | string, opts?: AiChatOptions): AsyncIterable<string>;
34
+ /** 可用模型 id 列表 */
35
+ models(): Promise<string[]>;
36
+ }
37
+ /** 解析 OpenAI 风格 SSE:`data: {...}` 行,`[DONE]` 结束;产出 choices[0].delta.content */
38
+ export declare function parseSseDeltas(body: ReadableStream<Uint8Array>): AsyncGenerator<string>;
39
+ /** 按当前配置取 AI 客户端(惰性、缓存;configure() 后自动重建) */
40
+ export declare function getAi(): AiClient;
41
+ /** 便捷单例:`import { ai } from '@chatu-ai/app-sdk'` */
42
+ export declare const ai: AiClient;
package/dist/ai.js ADDED
@@ -0,0 +1,145 @@
1
+ import { resolveConfig } from './config.js';
2
+ import { AppSdkError } from './errors.js';
3
+ const toMessages = (input) => (typeof input === 'string' ? [{ role: 'user', content: input }] : input);
4
+ function buildBody(cfg, messages, opts, stream) {
5
+ const model = opts?.model ?? cfg.aiModel;
6
+ const body = { ...(opts?.extra ?? {}), messages: toMessages(messages) };
7
+ if (model)
8
+ body.model = model;
9
+ if (opts?.temperature !== undefined)
10
+ body.temperature = opts.temperature;
11
+ if (opts?.maxTokens !== undefined)
12
+ body.max_tokens = opts.maxTokens;
13
+ if (stream)
14
+ body.stream = true;
15
+ return body;
16
+ }
17
+ async function throwHttpError(res, what) {
18
+ let json = null;
19
+ let text = '';
20
+ try {
21
+ text = await res.text();
22
+ json = JSON.parse(text);
23
+ }
24
+ catch { /* not json */ }
25
+ const err = json?.error;
26
+ const code = (typeof err === 'object' && err?.code) || (typeof err === 'string' && err) || json?.code || `HTTP_${res.status}`;
27
+ const message = (typeof err === 'object' && err?.message) || json?.message || (text ? text.slice(0, 300) : `${what} failed (${res.status})`);
28
+ throw new AppSdkError(String(code), String(message), res.status);
29
+ }
30
+ /** 解析 OpenAI 风格 SSE:`data: {...}` 行,`[DONE]` 结束;产出 choices[0].delta.content */
31
+ export async function* parseSseDeltas(body) {
32
+ const reader = body.getReader();
33
+ const decoder = new TextDecoder();
34
+ let buf = '';
35
+ const handle = (line) => {
36
+ const t = line.trim();
37
+ if (!t.startsWith('data:'))
38
+ return undefined;
39
+ const payload = t.slice(5).trim();
40
+ if (!payload || payload === '[DONE]')
41
+ return payload === '[DONE]' ? null : undefined;
42
+ try {
43
+ const json = JSON.parse(payload);
44
+ const delta = json?.choices?.[0]?.delta?.content ?? json?.choices?.[0]?.text;
45
+ return typeof delta === 'string' && delta.length ? delta : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ };
51
+ try {
52
+ while (true) {
53
+ const { value, done } = await reader.read();
54
+ if (done)
55
+ break;
56
+ buf += decoder.decode(value, { stream: true });
57
+ let idx;
58
+ while ((idx = buf.indexOf('\n')) >= 0) {
59
+ const line = buf.slice(0, idx);
60
+ buf = buf.slice(idx + 1);
61
+ const r = handle(line);
62
+ if (r === null)
63
+ return;
64
+ if (r !== undefined)
65
+ yield r;
66
+ }
67
+ }
68
+ if (buf.trim()) {
69
+ const r = handle(buf);
70
+ if (r)
71
+ yield r;
72
+ }
73
+ }
74
+ finally {
75
+ reader.releaseLock();
76
+ }
77
+ }
78
+ // ---------- platform driver ----------
79
+ function platformAi(cfg) {
80
+ const headers = { authorization: `Bearer ${cfg.apiKey}`, 'content-type': 'application/json' };
81
+ return {
82
+ async chat(messages, opts) {
83
+ const res = await cfg.fetchImpl(`${cfg.aiBaseUrl}/chat/completions`, {
84
+ method: 'POST', headers, body: JSON.stringify(buildBody(cfg, messages, opts, false)), signal: opts?.signal,
85
+ });
86
+ if (!res.ok)
87
+ await throwHttpError(res, 'ai.chat');
88
+ const json = await res.json();
89
+ const msg = json?.choices?.[0]?.message;
90
+ const content = typeof msg?.content === 'string' ? msg.content : Array.isArray(msg?.content) ? msg.content.map((p) => (typeof p?.text === 'string' ? p.text : '')).join('') : '';
91
+ const u = json?.usage;
92
+ return {
93
+ content,
94
+ model: typeof json?.model === 'string' ? json.model : undefined,
95
+ usage: u ? { promptTokens: u.prompt_tokens, completionTokens: u.completion_tokens, totalTokens: u.total_tokens } : undefined,
96
+ };
97
+ },
98
+ stream(messages, opts) {
99
+ const start = async () => {
100
+ const res = await cfg.fetchImpl(`${cfg.aiBaseUrl}/chat/completions`, {
101
+ method: 'POST', headers: { ...headers, accept: 'text/event-stream' }, body: JSON.stringify(buildBody(cfg, messages, opts, true)), signal: opts?.signal,
102
+ });
103
+ if (!res.ok)
104
+ await throwHttpError(res, 'ai.stream');
105
+ if (!res.body)
106
+ throw new AppSdkError('EMPTY_BODY', 'ai.stream: response has no body');
107
+ return res.body;
108
+ };
109
+ return { [Symbol.asyncIterator]: async function* () { yield* parseSseDeltas(await start()); } };
110
+ },
111
+ async models() {
112
+ const res = await cfg.fetchImpl(`${cfg.aiBaseUrl}/models`, { method: 'GET', headers: { authorization: headers.authorization } });
113
+ if (!res.ok)
114
+ await throwHttpError(res, 'ai.models');
115
+ const json = await res.json();
116
+ const list = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
117
+ return list.map(m => (typeof m === 'string' ? m : m?.id)).filter((id) => typeof id === 'string');
118
+ },
119
+ };
120
+ }
121
+ const NOT_CONFIGURED = 'AI is only available with the platform driver: set CHATU_DATA_URL/CHATU_APP_KEY (or CHATU_AI_URL) — copied from the Builder publish panel; there is no memory/byo fallback for LLM calls';
122
+ function notConfigured() {
123
+ const fail = () => { throw new AppSdkError('AI_NOT_CONFIGURED', NOT_CONFIGURED); };
124
+ return {
125
+ chat: async () => fail(),
126
+ stream: () => ({ [Symbol.asyncIterator]: async function* () { fail(); } }),
127
+ models: async () => fail(),
128
+ };
129
+ }
130
+ let cached = null;
131
+ /** 按当前配置取 AI 客户端(惰性、缓存;configure() 后自动重建) */
132
+ export function getAi() {
133
+ const cfg = resolveConfig();
134
+ const key = cfg.kind === 'platform' ? `platform|${cfg.aiBaseUrl}|${cfg.aiModel ?? ''}|${cfg.apiKey.slice(-4)}` : cfg.kind;
135
+ const fetchImpl = cfg.kind === 'platform' ? cfg.fetchImpl : undefined;
136
+ if (!cached || cached.key !== key || cached.fetchImpl !== fetchImpl)
137
+ cached = { key, fetchImpl, client: cfg.kind === 'platform' ? platformAi(cfg) : notConfigured() };
138
+ return cached.client;
139
+ }
140
+ /** 便捷单例:`import { ai } from '@chatu-ai/app-sdk'` */
141
+ export const ai = {
142
+ chat: (m, o) => getAi().chat(m, o),
143
+ stream: (m, o) => getAi().stream(m, o),
144
+ models: () => getAi().models(),
145
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ import { describe as d, expect, it } from 'vitest';
2
+ import { ai, configure, getAi } from './index';
3
+ import { deriveAiBaseUrl } from './config';
4
+ const sse = (lines) => new ReadableStream({
5
+ start(c) {
6
+ const enc = new TextEncoder();
7
+ // 故意把行切开成不规则的 chunk,验证跨 chunk 拼接
8
+ const text = lines.map(l => `${l}\n\n`).join('');
9
+ for (let i = 0; i < text.length; i += 7)
10
+ c.enqueue(enc.encode(text.slice(i, i + 7)));
11
+ c.close();
12
+ },
13
+ });
14
+ d('deriveAiBaseUrl', () => {
15
+ it('maps /data/v1 to /v1 and falls back to origin', () => {
16
+ expect(deriveAiBaseUrl('https://api.chatuapi.com/data/v1')).toBe('https://api.chatuapi.com/v1');
17
+ expect(deriveAiBaseUrl('http://chatu-function.chatu.svc.cluster.local/data/v1/')).toBe('http://chatu-function.chatu.svc.cluster.local/v1');
18
+ expect(deriveAiBaseUrl('https://api.test/other')).toBe('https://api.test/v1');
19
+ });
20
+ });
21
+ d('ai platform driver', () => {
22
+ it('chat: posts OpenAI-compatible body with Bearer auth and parses content/usage', async () => {
23
+ const calls = [];
24
+ const fetchImpl = (async (url, init) => {
25
+ calls.push({ url, init });
26
+ return new Response(JSON.stringify({ id: 'x', model: 'gpt-x', choices: [{ index: 0, message: { role: 'assistant', content: 'hello' } }], usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 } }), { status: 200 });
27
+ });
28
+ configure({ driver: 'platform', baseUrl: 'https://api.test/data/v1/', apiKey: 'sk-conv-abc', model: 'default-m', fetchImpl });
29
+ const r = await ai.chat('hi', { temperature: 0.2, maxTokens: 10, extra: { top_p: 0.9 } });
30
+ expect(r).toEqual({ content: 'hello', model: 'gpt-x', usage: { promptTokens: 3, completionTokens: 2, totalTokens: 5 } });
31
+ expect(calls[0].url).toBe('https://api.test/v1/chat/completions');
32
+ const h = calls[0].init.headers;
33
+ expect(h.authorization).toBe('Bearer sk-conv-abc');
34
+ expect(h['content-type']).toBe('application/json');
35
+ expect(JSON.parse(String(calls[0].init.body))).toEqual({ top_p: 0.9, model: 'default-m', messages: [{ role: 'user', content: 'hi' }], temperature: 0.2, max_tokens: 10 });
36
+ // 显式 model 覆盖默认;aiBaseUrl 显式覆盖推导
37
+ configure({ driver: 'platform', baseUrl: 'https://api.test/data/v1', aiBaseUrl: 'https://ai.test/v1/', apiKey: 'sk-conv-abc', fetchImpl });
38
+ await getAi().chat([{ role: 'system', content: 's' }, { role: 'user', content: 'u' }], { model: 'm2' });
39
+ expect(calls[1].url).toBe('https://ai.test/v1/chat/completions');
40
+ expect(JSON.parse(String(calls[1].init.body))).toEqual({ model: 'm2', messages: [{ role: 'system', content: 's' }, { role: 'user', content: 'u' }] });
41
+ });
42
+ it('chat: non-2xx becomes AppSdkError with server code/status', async () => {
43
+ const fetchImpl = (async () => new Response(JSON.stringify({ error: { code: 'INSUFFICIENT_POINTS', message: 'no points' } }), { status: 402 }));
44
+ configure({ driver: 'platform', baseUrl: 'https://api.test/data/v1', apiKey: 'sk-conv-abc', fetchImpl });
45
+ await expect(ai.chat('hi')).rejects.toMatchObject({ name: 'AppSdkError', code: 'INSUFFICIENT_POINTS', message: 'no points', status: 402 });
46
+ });
47
+ it('stream: yields delta text from SSE and stops at [DONE]', async () => {
48
+ let body;
49
+ const fetchImpl = (async (_url, init) => {
50
+ body = JSON.parse(String(init.body));
51
+ return new Response(sse([
52
+ 'data: {"choices":[{"delta":{"role":"assistant"}}]}',
53
+ 'data: {"choices":[{"delta":{"content":"Hel"}}]}',
54
+ ': keep-alive',
55
+ 'data: {"choices":[{"delta":{"content":"lo, "}}]}',
56
+ 'data: {"choices":[{"delta":{"content":"世界"}}]}',
57
+ 'data: [DONE]',
58
+ 'data: {"choices":[{"delta":{"content":"IGNORED"}}]}',
59
+ ]), { status: 200, headers: { 'content-type': 'text/event-stream' } });
60
+ });
61
+ configure({ driver: 'platform', baseUrl: 'https://api.test/data/v1', apiKey: 'sk-conv-abc', fetchImpl });
62
+ const parts = [];
63
+ for await (const t of ai.stream('hi'))
64
+ parts.push(t);
65
+ expect(parts).toEqual(['Hel', 'lo, ', '世界']);
66
+ expect(body).toMatchObject({ stream: true, messages: [{ role: 'user', content: 'hi' }] });
67
+ expect(body.model).toBeUndefined();
68
+ });
69
+ it('models: lists ids', async () => {
70
+ const fetchImpl = (async () => new Response(JSON.stringify({ object: 'list', data: [{ id: 'a' }, { id: 'b' }] }), { status: 200 }));
71
+ configure({ driver: 'platform', baseUrl: 'https://api.test/data/v1', apiKey: 'sk-conv-abc', fetchImpl });
72
+ expect(await ai.models()).toEqual(['a', 'b']);
73
+ });
74
+ });
75
+ d('ai without platform config', () => {
76
+ it('throws AI_NOT_CONFIGURED for memory driver', async () => {
77
+ configure({ driver: 'memory' });
78
+ await expect(ai.chat('hi')).rejects.toMatchObject({ code: 'AI_NOT_CONFIGURED' });
79
+ await expect(ai.models()).rejects.toMatchObject({ code: 'AI_NOT_CONFIGURED' });
80
+ await expect((async () => { for await (const _ of ai.stream('hi')) { /* noop */ } })()).rejects.toMatchObject({ code: 'AI_NOT_CONFIGURED' });
81
+ });
82
+ });
package/dist/config.d.ts CHANGED
@@ -11,6 +11,10 @@ export interface PlatformConfig {
11
11
  apiKey: string;
12
12
  env: 'dev' | 'prod';
13
13
  fetchImpl: typeof fetch;
14
+ /** OpenAI 兼容的 LLM 中继地址(`{origin}/v1`):CHATU_AI_URL 显式指定,否则由 CHATU_DATA_URL 去掉 `/data/v1` 推导 */
15
+ aiBaseUrl: string;
16
+ /** 默认模型:CHATU_AI_MODEL → PRIMARY_MODEL(沙箱注入的平台默认模型);都没有则不传,由服务端决定 */
17
+ aiModel?: string;
14
18
  }
15
19
  export interface MemoryConfig {
16
20
  kind: 'memory';
@@ -37,10 +41,16 @@ export interface ConfigureOptions {
37
41
  env?: 'dev' | 'prod';
38
42
  driver?: DriverKind;
39
43
  fetchImpl?: typeof fetch;
44
+ /** LLM 中继地址(默认由 baseUrl 推导) */
45
+ aiBaseUrl?: string;
46
+ /** LLM 默认模型 */
47
+ model?: string;
40
48
  }
41
49
  /** 显式配置(测试或非 env 场景);不调用则完全由环境变量决定 */
42
50
  export declare function configure(options: ConfigureOptions): void;
43
51
  export declare function resolveConfig(): ResolvedConfig;
52
+ /** `https://api.chatuapi.com/data/v1` → `https://api.chatuapi.com/v1`(Data API 与 LLM 中继同源) */
53
+ export declare function deriveAiBaseUrl(dataBaseUrl: string): string;
44
54
  /** 当前生效的驱动与环境(诊断用,不含密钥) */
45
55
  export declare function describe(): {
46
56
  driver: DriverKind;
package/dist/config.js CHANGED
@@ -34,10 +34,31 @@ export function resolveConfig() {
34
34
  if (!baseUrl || !apiKey)
35
35
  throw new Error('@chatu-ai/app-sdk: platform driver requires CHATU_DATA_URL and CHATU_APP_KEY');
36
36
  const dataEnv = (override.env ?? env.CHATU_DATA_ENV ?? 'dev').toLowerCase() === 'prod' ? 'prod' : 'dev';
37
- return { kind: 'platform', baseUrl: baseUrl.replace(/\/+$/, ''), apiKey, env: dataEnv, fetchImpl: override.fetchImpl ?? fetch };
37
+ const normalizedBase = baseUrl.replace(/\/+$/, '');
38
+ return {
39
+ kind: 'platform',
40
+ baseUrl: normalizedBase,
41
+ apiKey,
42
+ env: dataEnv,
43
+ fetchImpl: override.fetchImpl ?? fetch,
44
+ aiBaseUrl: (override.aiBaseUrl ?? env.CHATU_AI_URL ?? deriveAiBaseUrl(normalizedBase)).replace(/\/+$/, ''),
45
+ aiModel: override.model ?? env.CHATU_AI_MODEL ?? env.PRIMARY_MODEL,
46
+ };
38
47
  }
39
48
  return { kind: 'memory' };
40
49
  }
50
+ /** `https://api.chatuapi.com/data/v1` → `https://api.chatuapi.com/v1`(Data API 与 LLM 中继同源) */
51
+ export function deriveAiBaseUrl(dataBaseUrl) {
52
+ const trimmed = dataBaseUrl.replace(/\/+$/, '');
53
+ if (/\/data\/v1$/.test(trimmed))
54
+ return trimmed.replace(/\/data\/v1$/, '/v1');
55
+ try {
56
+ return `${new URL(trimmed).origin}/v1`;
57
+ }
58
+ catch {
59
+ return `${trimmed}/v1`;
60
+ }
61
+ }
41
62
  /** 当前生效的驱动与环境(诊断用,不含密钥) */
42
63
  export function describe() {
43
64
  const c = resolveConfig();
package/dist/index.d.ts CHANGED
@@ -5,3 +5,5 @@ export type { ConfigureOptions, DriverKind } from './config.js';
5
5
  export { AppSdkError } from './errors.js';
6
6
  export { storage, getStorage } from './storage.js';
7
7
  export type { StorageClient, StorageObject, StorageListResult, UploadUrlResult } from './storage.js';
8
+ export { ai, getAi } from './ai.js';
9
+ export type { AiClient, AiMessage, AiChatOptions, AiChatResult, AiUsage } from './ai.js';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { kv, getKv } from './kv.js';
2
2
  export { configure, describe } from './config.js';
3
3
  export { AppSdkError } from './errors.js';
4
4
  export { storage, getStorage } from './storage.js';
5
+ export { ai, getAi } from './ai.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chatu-ai/app-sdk",
3
- "version": "0.6.2",
4
- "description": "Runtime data SDK for apps generated by ChatU Builder: kv (and more) with platform / memory drivers selected by environment variables",
3
+ "version": "0.7.0",
4
+ "description": "Runtime SDK for apps generated by ChatU Builder: kv, storage and ai (OpenAI-compatible LLM relay) with platform / byo / memory drivers selected by environment variables",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -30,6 +30,8 @@
30
30
  "kv",
31
31
  "storage",
32
32
  "app-sdk",
33
+ "ai",
34
+ "llm",
33
35
  "nextjs"
34
36
  ],
35
37
  "publishConfig": {