@terminaluse/vercel-ai-sdk-provider 0.0.3 → 0.1.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,15 +1,59 @@
1
1
  # @terminaluse/vercel-ai-sdk-provider
2
2
 
3
- To install dependencies:
3
+ Vercel AI SDK provider for TerminalUse agents. Implements `LanguageModelV3`.
4
+
5
+ Use [`@terminaluse/sdk`](https://www.npmjs.com/package/@terminaluse/sdk) to create projects, filesystems, tasks, and manage your TerminalUse resources.
6
+
7
+ ## Install
4
8
 
5
9
  ```bash
6
- bun install
10
+ npm install @terminaluse/vercel-ai-sdk-provider
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { createTerminalUseProvider } from '@terminaluse/vercel-ai-sdk-provider';
17
+ import { streamText } from 'ai';
18
+
19
+ const terminaluse = createTerminalUseProvider({
20
+ apiKey: process.env.TERMINALUSE_API_KEY!,
21
+ baseURL: process.env.TERMINALUSE_BASE_URL!,
22
+ });
23
+
24
+ // Stream messages to an existing task
25
+ const result = await streamText({
26
+ model: terminaluse.agent('namespace/agent-name'),
27
+ messages: [{ role: 'user', content: 'Hello' }],
28
+ providerOptions: {
29
+ terminaluse: { taskId: 'existing-task-id' },
30
+ },
31
+ });
7
32
  ```
8
33
 
9
- To run:
34
+ ## Provider Options
35
+
36
+ | Option | Type | Description |
37
+ |--------|------|-------------|
38
+ | `taskId` | `string` | Task ID to send messages to (required) |
39
+
40
+ Tasks must be created separately using the [`@terminaluse/sdk`](https://www.npmjs.com/package/@terminaluse/sdk).
41
+
42
+ ## Requirements
43
+
44
+ - Node.js 18+
45
+ - ESM projects (this package is ESM-only)
46
+ - `ai` package v6.0.0+ (peer dependency)
47
+
48
+ ## Development
10
49
 
11
50
  ```bash
12
- bun run index.ts
51
+ bun install
52
+ bun run dev # watch mode
53
+ bun test # run tests
54
+ bun run build # production build
13
55
  ```
14
56
 
15
- This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
57
+ ## License
58
+
59
+ Apache-2.0
package/dist/index.d.mts CHANGED
@@ -1 +1,51 @@
1
- export { };
1
+ import { LanguageModel } from "ai";
2
+
3
+ //#region src/provider.d.ts
4
+ interface TerminalUseProviderConfig {
5
+ /** API key for authentication (Bearer token) */
6
+ apiKey: string;
7
+ /** TerminalUse API base URL */
8
+ baseURL: string;
9
+ }
10
+ interface TerminalUseProvider {
11
+ /** Create a language model for the specified agent */
12
+ agent(agentName: string): LanguageModel;
13
+ }
14
+ /**
15
+ * Provider options for TerminalUse agents.
16
+ * Task must be created separately via the /api/tasks endpoint.
17
+ */
18
+ interface TerminalUseProviderOptions {
19
+ /** Task ID to send messages to (required) */
20
+ taskId: string;
21
+ }
22
+ /**
23
+ * Creates a custom AI SDK provider for TerminalUse agents.
24
+ *
25
+ * This provider implements the LanguageModelV3 interface, bridging
26
+ * Vercel AI SDK's useChat/streamText to TerminalUse's RPC-based API.
27
+ *
28
+ * Note: Tasks must be created separately via the /api/tasks endpoint
29
+ * before sending messages.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const terminaluse = createTerminalUseProvider({
34
+ * apiKey: process.env.TERMINALUSE_API_KEY!,
35
+ * baseURL: process.env.TERMINALUSE_API_BASE_URL!,
36
+ * });
37
+ *
38
+ * const result = await streamText({
39
+ * model: terminaluse.agent('namespace/agent-name'),
40
+ * messages,
41
+ * providerOptions: {
42
+ * terminaluse: {
43
+ * taskId: 'existing-task-id',
44
+ * },
45
+ * },
46
+ * });
47
+ * ```
48
+ */
49
+ declare function createTerminalUseProvider(config: TerminalUseProviderConfig): TerminalUseProvider;
50
+ //#endregion
51
+ export { type TerminalUseProvider, type TerminalUseProviderConfig, type TerminalUseProviderOptions, createTerminalUseProvider };
package/dist/index.mjs CHANGED
@@ -1,5 +1,412 @@
1
- //#region index.ts
2
- console.log("Hello via Bun!");
1
+ import { UnsupportedFunctionalityError } from "ai";
2
+ import { createEventSourceResponseHandler, getFromApi } from "@ai-sdk/provider-utils";
3
+ import { z } from "zod";
4
+
5
+ //#region src/http.ts
6
+ /**
7
+ * Fetch wrapper with TerminalUse authentication headers.
8
+ */
9
+ async function terminaluseFetch(config, path, options = {}) {
10
+ const headers = new Headers(options.headers);
11
+ if (config.apiKey) headers.set("Authorization", `Bearer ${config.apiKey}`);
12
+ if (!headers.has("Content-Type") && options.body) headers.set("Content-Type", "application/json");
13
+ return fetch(`${config.baseURL}${path}`, {
14
+ ...options,
15
+ headers
16
+ });
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/ndjson-stream.ts
21
+ /**
22
+ * SSE streaming utility for TerminalUse task events.
23
+ * Uses @ai-sdk/provider-utils for clean SSE parsing.
24
+ *
25
+ * This module handles the v2 TextStreamPart format from the backend,
26
+ * which is based on Vercel AI SDK's TextStreamPart with extensions.
27
+ */
28
+ const streamMetadataSchema = z.object({
29
+ costUsd: z.number().optional(),
30
+ durationMs: z.number().optional(),
31
+ cacheCreationInputTokens: z.number().optional(),
32
+ cacheReadInputTokens: z.number().optional(),
33
+ parentToolUseId: z.string().optional()
34
+ }).optional();
35
+ const streamUsageSchema = z.object({
36
+ inputTokens: z.number(),
37
+ outputTokens: z.number(),
38
+ totalTokens: z.number().optional()
39
+ });
40
+ const finishReasonSchema = z.enum([
41
+ "stop",
42
+ "tool-calls",
43
+ "length",
44
+ "content-filter",
45
+ "error",
46
+ "other"
47
+ ]);
48
+ const startPartSchema = z.object({ type: z.literal("start") });
49
+ const startStepPartSchema = z.object({
50
+ type: z.literal("start-step"),
51
+ request: z.record(z.unknown()).optional(),
52
+ warnings: z.array(z.unknown()).optional(),
53
+ metadata: streamMetadataSchema
54
+ });
55
+ const finishStepPartSchema = z.object({
56
+ type: z.literal("finish-step"),
57
+ finishReason: finishReasonSchema,
58
+ usage: streamUsageSchema,
59
+ metadata: streamMetadataSchema
60
+ });
61
+ const finishPartSchema = z.object({
62
+ type: z.literal("finish"),
63
+ finishReason: finishReasonSchema,
64
+ totalUsage: streamUsageSchema,
65
+ metadata: streamMetadataSchema
66
+ });
67
+ const textStartPartSchema = z.object({
68
+ type: z.literal("text-start"),
69
+ id: z.string(),
70
+ metadata: streamMetadataSchema
71
+ });
72
+ const textDeltaPartSchema = z.object({
73
+ type: z.literal("text-delta"),
74
+ id: z.string(),
75
+ text: z.string(),
76
+ metadata: streamMetadataSchema
77
+ });
78
+ const textEndPartSchema = z.object({
79
+ type: z.literal("text-end"),
80
+ id: z.string(),
81
+ metadata: streamMetadataSchema
82
+ });
83
+ const reasoningStartPartSchema = z.object({
84
+ type: z.literal("reasoning-start"),
85
+ id: z.string(),
86
+ metadata: streamMetadataSchema
87
+ });
88
+ const reasoningDeltaPartSchema = z.object({
89
+ type: z.literal("reasoning-delta"),
90
+ id: z.string(),
91
+ text: z.string(),
92
+ metadata: streamMetadataSchema
93
+ });
94
+ const reasoningEndPartSchema = z.object({
95
+ type: z.literal("reasoning-end"),
96
+ id: z.string(),
97
+ metadata: streamMetadataSchema
98
+ });
99
+ const toolInputStartPartSchema = z.object({
100
+ type: z.literal("tool-input-start"),
101
+ id: z.string(),
102
+ toolName: z.string(),
103
+ metadata: streamMetadataSchema
104
+ });
105
+ const toolInputDeltaPartSchema = z.object({
106
+ type: z.literal("tool-input-delta"),
107
+ id: z.string(),
108
+ delta: z.string(),
109
+ metadata: streamMetadataSchema
110
+ });
111
+ const toolInputEndPartSchema = z.object({
112
+ type: z.literal("tool-input-end"),
113
+ id: z.string(),
114
+ metadata: streamMetadataSchema
115
+ });
116
+ const toolCallPartSchema = z.object({
117
+ type: z.literal("tool-call"),
118
+ toolCallId: z.string(),
119
+ toolName: z.string(),
120
+ input: z.record(z.unknown()),
121
+ metadata: streamMetadataSchema
122
+ });
123
+ const toolResultPartSchema = z.object({
124
+ type: z.literal("tool-result"),
125
+ toolCallId: z.string(),
126
+ toolName: z.string(),
127
+ output: z.unknown(),
128
+ metadata: streamMetadataSchema
129
+ });
130
+ const errorPartSchema = z.object({
131
+ type: z.literal("error"),
132
+ error: z.string(),
133
+ rawContent: z.unknown().optional()
134
+ });
135
+ /**
136
+ * Zod schema for v2 TextStreamPart events from the backend.
137
+ * Based on Vercel AI SDK's TextStreamPart with extensions for reasoning and tool streaming.
138
+ */
139
+ const textStreamPartSchema = z.discriminatedUnion("type", [
140
+ startPartSchema,
141
+ startStepPartSchema,
142
+ finishStepPartSchema,
143
+ finishPartSchema,
144
+ textStartPartSchema,
145
+ textDeltaPartSchema,
146
+ textEndPartSchema,
147
+ reasoningStartPartSchema,
148
+ reasoningDeltaPartSchema,
149
+ reasoningEndPartSchema,
150
+ toolInputStartPartSchema,
151
+ toolInputDeltaPartSchema,
152
+ toolInputEndPartSchema,
153
+ toolCallPartSchema,
154
+ toolResultPartSchema,
155
+ errorPartSchema
156
+ ]);
157
+ /**
158
+ * Creates an async generator that yields v2 TextStreamPart events.
159
+ * Uses @ai-sdk/provider-utils for SSE parsing.
160
+ */
161
+ async function* createTaskEventGenerator(config, taskId, options = {}) {
162
+ const { signal } = options;
163
+ const { value: responseStream } = await getFromApi({
164
+ url: `${config.baseURL}/tasks/${encodeURIComponent(taskId)}/stream`,
165
+ headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
166
+ abortSignal: signal,
167
+ successfulResponseHandler: createEventSourceResponseHandler(textStreamPartSchema),
168
+ failedResponseHandler: async ({ response }) => {
169
+ const text = await response.text();
170
+ return { value: /* @__PURE__ */ new Error(`Stream request failed: ${response.status} - ${text}`) };
171
+ }
172
+ });
173
+ const reader = responseStream.getReader();
174
+ try {
175
+ while (true) {
176
+ const { done, value } = await reader.read();
177
+ if (done) break;
178
+ const parseResult = value;
179
+ if (parseResult.success) yield parseResult.value;
180
+ }
181
+ } finally {
182
+ reader.releaseLock();
183
+ }
184
+ }
185
+
186
+ //#endregion
187
+ //#region src/stream.ts
188
+ /**
189
+ * Creates a ReadableStream that transforms v2 TextStreamPart events to AI SDK v3 format.
190
+ * Implements a thin passthrough adapter with minimal transformation logic.
191
+ */
192
+ function createTerminalUseTransformStream(config, taskId, signal) {
193
+ const eventGenerator = createTaskEventGenerator(config, taskId, { signal });
194
+ let emittedStreamStart = false;
195
+ return new ReadableStream({
196
+ async pull(controller) {
197
+ if (!emittedStreamStart) {
198
+ emittedStreamStart = true;
199
+ controller.enqueue({
200
+ type: "stream-start",
201
+ warnings: []
202
+ });
203
+ return;
204
+ }
205
+ try {
206
+ while (true) {
207
+ const { value, done } = await eventGenerator.next();
208
+ if (done || signal?.aborted) {
209
+ controller.close();
210
+ return;
211
+ }
212
+ const event = value;
213
+ const transformed = transformEvent(event, taskId);
214
+ if (event.type === "finish" || event.type === "error") {
215
+ if (transformed) controller.enqueue(transformed);
216
+ controller.close();
217
+ return;
218
+ }
219
+ if (transformed) {
220
+ controller.enqueue(transformed);
221
+ return;
222
+ }
223
+ }
224
+ } catch (error) {
225
+ eventGenerator.return?.(void 0);
226
+ controller.error(error);
227
+ }
228
+ },
229
+ cancel() {
230
+ eventGenerator.return?.(void 0);
231
+ }
232
+ });
233
+ }
234
+ /**
235
+ * Transforms a single v2 TextStreamPart event to AI SDK v3 LanguageModelV3StreamPart.
236
+ * Returns null for events that should be skipped (e.g., finish-step, start).
237
+ */
238
+ function transformEvent(event, taskId) {
239
+ switch (event.type) {
240
+ case "start": return null;
241
+ case "start-step": return {
242
+ type: "response-metadata",
243
+ id: taskId,
244
+ timestamp: /* @__PURE__ */ new Date(),
245
+ modelId: "terminaluse-agent"
246
+ };
247
+ case "finish-step": return null;
248
+ case "finish": return {
249
+ type: "finish",
250
+ finishReason: {
251
+ unified: event.finishReason,
252
+ raw: event.finishReason
253
+ },
254
+ usage: {
255
+ inputTokens: {
256
+ total: event.totalUsage.inputTokens,
257
+ noCache: void 0,
258
+ cacheRead: void 0,
259
+ cacheWrite: void 0
260
+ },
261
+ outputTokens: {
262
+ total: event.totalUsage.outputTokens,
263
+ text: void 0,
264
+ reasoning: void 0
265
+ }
266
+ }
267
+ };
268
+ case "text-start": return {
269
+ type: "text-start",
270
+ id: event.id
271
+ };
272
+ case "text-delta": return {
273
+ type: "text-delta",
274
+ id: event.id,
275
+ delta: event.text
276
+ };
277
+ case "text-end": return {
278
+ type: "text-end",
279
+ id: event.id
280
+ };
281
+ case "reasoning-start": return {
282
+ type: "reasoning-start",
283
+ id: event.id
284
+ };
285
+ case "reasoning-delta": return {
286
+ type: "reasoning-delta",
287
+ id: event.id,
288
+ delta: event.text
289
+ };
290
+ case "reasoning-end": return {
291
+ type: "reasoning-end",
292
+ id: event.id
293
+ };
294
+ case "tool-input-start": return {
295
+ type: "tool-input-start",
296
+ id: event.id,
297
+ toolName: event.toolName,
298
+ providerExecuted: true,
299
+ dynamic: true
300
+ };
301
+ case "tool-input-delta": return {
302
+ type: "tool-input-delta",
303
+ id: event.id,
304
+ delta: event.delta
305
+ };
306
+ case "tool-input-end": return {
307
+ type: "tool-input-end",
308
+ id: event.id
309
+ };
310
+ case "tool-call": return {
311
+ type: "tool-call",
312
+ toolCallId: event.toolCallId,
313
+ toolName: event.toolName,
314
+ input: JSON.stringify(event.input),
315
+ providerExecuted: true,
316
+ dynamic: true
317
+ };
318
+ case "tool-result": {
319
+ const result = event.output ?? "";
320
+ return {
321
+ type: "tool-result",
322
+ toolCallId: event.toolCallId,
323
+ toolName: event.toolName,
324
+ result
325
+ };
326
+ }
327
+ case "error": return {
328
+ type: "error",
329
+ error: event.error
330
+ };
331
+ default: return null;
332
+ }
333
+ }
334
+
335
+ //#endregion
336
+ //#region src/provider.ts
337
+ /**
338
+ * Creates a custom AI SDK provider for TerminalUse agents.
339
+ *
340
+ * This provider implements the LanguageModelV3 interface, bridging
341
+ * Vercel AI SDK's useChat/streamText to TerminalUse's RPC-based API.
342
+ *
343
+ * Note: Tasks must be created separately via the /api/tasks endpoint
344
+ * before sending messages.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * const terminaluse = createTerminalUseProvider({
349
+ * apiKey: process.env.TERMINALUSE_API_KEY!,
350
+ * baseURL: process.env.TERMINALUSE_API_BASE_URL!,
351
+ * });
352
+ *
353
+ * const result = await streamText({
354
+ * model: terminaluse.agent('namespace/agent-name'),
355
+ * messages,
356
+ * providerOptions: {
357
+ * terminaluse: {
358
+ * taskId: 'existing-task-id',
359
+ * },
360
+ * },
361
+ * });
362
+ * ```
363
+ */
364
+ function createTerminalUseProvider(config) {
365
+ const tuConfig = {
366
+ baseURL: config.baseURL,
367
+ apiKey: config.apiKey
368
+ };
369
+ return { agent(agentName) {
370
+ return {
371
+ specificationVersion: "v3",
372
+ provider: "terminaluse",
373
+ modelId: agentName,
374
+ supportedUrls: {},
375
+ async doGenerate() {
376
+ throw new UnsupportedFunctionalityError({
377
+ functionality: "non-streaming generation",
378
+ message: "TerminalUse provider only supports streaming. Use streamText() instead of generateText()."
379
+ });
380
+ },
381
+ async doStream(options) {
382
+ const { prompt, providerOptions, abortSignal } = options;
383
+ const tuOptions = providerOptions?.terminaluse;
384
+ if (!tuOptions?.taskId) throw new Error("taskId is required. Create a task via /api/tasks first.");
385
+ const { taskId } = tuOptions;
386
+ const userContent = prompt.at(-1)?.content;
387
+ let textContent;
388
+ if (Array.isArray(userContent)) {
389
+ const textPart = userContent.find((c) => c.type === "text");
390
+ if (textPart && textPart.type === "text") textContent = textPart.text;
391
+ } else if (typeof userContent === "string") textContent = userContent;
392
+ const sendResponse = await terminaluseFetch(tuConfig, `/tasks/${encodeURIComponent(taskId)}/events`, {
393
+ method: "POST",
394
+ body: JSON.stringify({ content: {
395
+ type: "text",
396
+ content: textContent || "",
397
+ author: "user",
398
+ format: "plain"
399
+ } })
400
+ });
401
+ if (!sendResponse.ok) {
402
+ const errorText = await sendResponse.text();
403
+ throw new Error(`Failed to send message: ${sendResponse.status} - ${errorText}`);
404
+ }
405
+ return { stream: createTerminalUseTransformStream(tuConfig, taskId, abortSignal) };
406
+ }
407
+ };
408
+ } };
409
+ }
3
410
 
4
411
  //#endregion
5
- export { };
412
+ export { createTerminalUseProvider };
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@terminaluse/vercel-ai-sdk-provider",
3
- "version": "0.0.3",
4
- "description": "Vercel AI SDK provider for Terminal Use",
3
+ "version": "0.1.0",
4
+ "description": "Vercel AI SDK provider for TerminalUse agents",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/terminaluse/vercel-ai-sdk-provider.git"
7
+ "url": "git+https://github.com/terminal-use/monorepo.git",
8
+ "directory": "packages/vercel-ai-sdk-provider"
8
9
  },
10
+ "license": "Apache-2.0",
9
11
  "type": "module",
10
12
  "main": "./dist/index.mjs",
11
13
  "module": "./dist/index.mjs",
@@ -23,16 +25,30 @@
23
25
  "scripts": {
24
26
  "build": "tsdown",
25
27
  "dev": "tsdown --watch",
26
- "test": "bun test",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
27
30
  "typecheck": "tsc --noEmit",
31
+ "lint": "biome lint .",
32
+ "format": "biome format --write .",
33
+ "check": "biome check --write .",
28
34
  "prepublishOnly": "bun run build"
29
35
  },
36
+ "dependencies": {
37
+ "@ai-sdk/provider": "^3.0.0",
38
+ "@ai-sdk/provider-utils": "^4.0.0",
39
+ "zod": "^3.24.0"
40
+ },
41
+ "peerDependencies": {
42
+ "ai": "^6.0.0"
43
+ },
30
44
  "devDependencies": {
31
- "@types/bun": "latest",
45
+ "@biomejs/biome": "^2.3.11",
46
+ "@types/node": "^20",
32
47
  "tsdown": "^0.16.0",
33
- "typescript": "^5.9.3"
48
+ "typescript": "^5",
49
+ "vitest": "^4.0.16"
34
50
  },
35
- "peerDependencies": {
36
- "typescript": "^5"
51
+ "engines": {
52
+ "node": ">=18.0.0"
37
53
  }
38
54
  }