zubo 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.
Files changed (222) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/README.md +149 -0
  3. package/bun.lock +216 -0
  4. package/desktop/README.md +57 -0
  5. package/desktop/package.json +12 -0
  6. package/desktop/src-tauri/Cargo.toml +25 -0
  7. package/desktop/src-tauri/build.rs +3 -0
  8. package/desktop/src-tauri/icons/README.md +17 -0
  9. package/desktop/src-tauri/icons/icon.png +0 -0
  10. package/desktop/src-tauri/src/main.rs +189 -0
  11. package/desktop/src-tauri/tauri.conf.json +68 -0
  12. package/docs/ROADMAP.md +490 -0
  13. package/migrations/001_init.sql +9 -0
  14. package/migrations/002_memory.sql +33 -0
  15. package/migrations/003_cron.sql +24 -0
  16. package/migrations/004_usage.sql +12 -0
  17. package/migrations/005_secrets.sql +8 -0
  18. package/migrations/006_agents.sql +1 -0
  19. package/migrations/007_workflows.sql +22 -0
  20. package/migrations/008_proactive.sql +24 -0
  21. package/migrations/009_uploads.sql +9 -0
  22. package/migrations/010_observability.sql +22 -0
  23. package/migrations/011_api_keys.sql +7 -0
  24. package/migrations/012_indexes.sql +5 -0
  25. package/migrations/013_budget.sql +11 -0
  26. package/migrations/014_usage_session_idx.sql +2 -0
  27. package/package.json +39 -0
  28. package/site/404.html +156 -0
  29. package/site/CNAME +1 -0
  30. package/site/docs/agents.html +294 -0
  31. package/site/docs/api.html +446 -0
  32. package/site/docs/channels.html +345 -0
  33. package/site/docs/cli.html +238 -0
  34. package/site/docs/config.html +1034 -0
  35. package/site/docs/index.html +433 -0
  36. package/site/docs/integrations.html +381 -0
  37. package/site/docs/memory.html +254 -0
  38. package/site/docs/security.html +375 -0
  39. package/site/docs/skills.html +322 -0
  40. package/site/docs.css +412 -0
  41. package/site/index.html +638 -0
  42. package/site/install.sh +98 -0
  43. package/site/logo.svg +1 -0
  44. package/site/og-image.png +0 -0
  45. package/site/robots.txt +4 -0
  46. package/site/script.js +361 -0
  47. package/site/sitemap.xml +63 -0
  48. package/site/skills.html +532 -0
  49. package/site/style.css +1686 -0
  50. package/src/agent/agents.ts +159 -0
  51. package/src/agent/compaction.ts +53 -0
  52. package/src/agent/context.ts +18 -0
  53. package/src/agent/delegate.ts +118 -0
  54. package/src/agent/loop.ts +318 -0
  55. package/src/agent/prompts.ts +111 -0
  56. package/src/agent/session.ts +87 -0
  57. package/src/agent/teams.ts +116 -0
  58. package/src/agent/workflow-executor.ts +192 -0
  59. package/src/agent/workflow.ts +175 -0
  60. package/src/channels/adapter.ts +21 -0
  61. package/src/channels/dashboard.html.ts +2969 -0
  62. package/src/channels/discord.ts +137 -0
  63. package/src/channels/optional-deps.d.ts +17 -0
  64. package/src/channels/router.ts +199 -0
  65. package/src/channels/signal.ts +133 -0
  66. package/src/channels/slack.ts +101 -0
  67. package/src/channels/telegram.ts +102 -0
  68. package/src/channels/utils.ts +18 -0
  69. package/src/channels/webchat.ts +1797 -0
  70. package/src/channels/whatsapp.ts +119 -0
  71. package/src/config/loader.ts +22 -0
  72. package/src/config/paths.ts +43 -0
  73. package/src/config/schema.ts +121 -0
  74. package/src/db/connection.ts +20 -0
  75. package/src/db/export.ts +148 -0
  76. package/src/db/migrations.ts +42 -0
  77. package/src/index.ts +261 -0
  78. package/src/llm/claude.ts +193 -0
  79. package/src/llm/factory.ts +115 -0
  80. package/src/llm/failover.ts +101 -0
  81. package/src/llm/openai-compat.ts +409 -0
  82. package/src/llm/provider.ts +83 -0
  83. package/src/llm/smart-router.ts +241 -0
  84. package/src/logs.ts +53 -0
  85. package/src/memory/chunker.ts +58 -0
  86. package/src/memory/document-parser.ts +115 -0
  87. package/src/memory/embedder.ts +235 -0
  88. package/src/memory/engine.ts +170 -0
  89. package/src/memory/fts-index.ts +55 -0
  90. package/src/memory/hybrid-search.ts +72 -0
  91. package/src/memory/store.ts +56 -0
  92. package/src/memory/vector-index.ts +72 -0
  93. package/src/model.ts +118 -0
  94. package/src/registry/cli.ts +43 -0
  95. package/src/registry/client.ts +54 -0
  96. package/src/registry/installer.ts +67 -0
  97. package/src/scheduler/briefing.ts +71 -0
  98. package/src/scheduler/cron.ts +258 -0
  99. package/src/scheduler/heartbeat.ts +58 -0
  100. package/src/scheduler/memory-triggers.ts +100 -0
  101. package/src/scheduler/natural-cron.ts +163 -0
  102. package/src/scheduler/proactive.ts +25 -0
  103. package/src/scheduler/recipes.ts +110 -0
  104. package/src/secrets/store.ts +64 -0
  105. package/src/setup.ts +413 -0
  106. package/src/skills.ts +293 -0
  107. package/src/start.ts +373 -0
  108. package/src/status.ts +165 -0
  109. package/src/tools/builtin/connect-service.ts +205 -0
  110. package/src/tools/builtin/cron.ts +126 -0
  111. package/src/tools/builtin/datetime.ts +36 -0
  112. package/src/tools/builtin/delegate-task.ts +81 -0
  113. package/src/tools/builtin/delegate.ts +42 -0
  114. package/src/tools/builtin/diagnose.ts +41 -0
  115. package/src/tools/builtin/google-oauth.ts +379 -0
  116. package/src/tools/builtin/manage-agents.ts +149 -0
  117. package/src/tools/builtin/manage-skills.ts +294 -0
  118. package/src/tools/builtin/manage-teams.ts +89 -0
  119. package/src/tools/builtin/manage-triggers.ts +94 -0
  120. package/src/tools/builtin/manage-workflows.ts +119 -0
  121. package/src/tools/builtin/memory-search.ts +38 -0
  122. package/src/tools/builtin/memory-write.ts +30 -0
  123. package/src/tools/builtin/run-workflow.ts +36 -0
  124. package/src/tools/builtin/secrets.ts +122 -0
  125. package/src/tools/builtin/skill-registry.ts +75 -0
  126. package/src/tools/builtin-integrations/api-helpers.ts +26 -0
  127. package/src/tools/builtin-integrations/github/github_issues/SKILL.md +56 -0
  128. package/src/tools/builtin-integrations/github/github_issues/handler.ts +108 -0
  129. package/src/tools/builtin-integrations/github/github_prs/SKILL.md +57 -0
  130. package/src/tools/builtin-integrations/github/github_prs/handler.ts +113 -0
  131. package/src/tools/builtin-integrations/github/github_repos/SKILL.md +37 -0
  132. package/src/tools/builtin-integrations/github/github_repos/handler.ts +88 -0
  133. package/src/tools/builtin-integrations/google/gmail/SKILL.md +51 -0
  134. package/src/tools/builtin-integrations/google/gmail/handler.ts +125 -0
  135. package/src/tools/builtin-integrations/google/google_calendar/SKILL.md +35 -0
  136. package/src/tools/builtin-integrations/google/google_calendar/handler.ts +105 -0
  137. package/src/tools/builtin-integrations/google/google_docs/SKILL.md +35 -0
  138. package/src/tools/builtin-integrations/google/google_docs/handler.ts +108 -0
  139. package/src/tools/builtin-integrations/google/google_drive/SKILL.md +39 -0
  140. package/src/tools/builtin-integrations/google/google_drive/handler.ts +106 -0
  141. package/src/tools/builtin-integrations/google/google_sheets/SKILL.md +36 -0
  142. package/src/tools/builtin-integrations/google/google_sheets/handler.ts +116 -0
  143. package/src/tools/builtin-integrations/jira/jira_boards/SKILL.md +21 -0
  144. package/src/tools/builtin-integrations/jira/jira_boards/handler.ts +74 -0
  145. package/src/tools/builtin-integrations/jira/jira_issues/SKILL.md +28 -0
  146. package/src/tools/builtin-integrations/jira/jira_issues/handler.ts +140 -0
  147. package/src/tools/builtin-integrations/linear/linear_issues/SKILL.md +30 -0
  148. package/src/tools/builtin-integrations/linear/linear_issues/handler.ts +75 -0
  149. package/src/tools/builtin-integrations/linear/linear_projects/SKILL.md +21 -0
  150. package/src/tools/builtin-integrations/linear/linear_projects/handler.ts +43 -0
  151. package/src/tools/builtin-integrations/notion/notion_databases/SKILL.md +39 -0
  152. package/src/tools/builtin-integrations/notion/notion_databases/handler.ts +83 -0
  153. package/src/tools/builtin-integrations/notion/notion_pages/SKILL.md +43 -0
  154. package/src/tools/builtin-integrations/notion/notion_pages/handler.ts +130 -0
  155. package/src/tools/builtin-integrations/notion/notion_search/SKILL.md +27 -0
  156. package/src/tools/builtin-integrations/notion/notion_search/handler.ts +69 -0
  157. package/src/tools/builtin-integrations/slack/slack_messages/SKILL.md +42 -0
  158. package/src/tools/builtin-integrations/slack/slack_messages/handler.ts +72 -0
  159. package/src/tools/builtin-integrations/twitter/twitter_posts/SKILL.md +24 -0
  160. package/src/tools/builtin-integrations/twitter/twitter_posts/handler.ts +133 -0
  161. package/src/tools/builtin-skills/file-read/SKILL.md +26 -0
  162. package/src/tools/builtin-skills/file-read/handler.ts +66 -0
  163. package/src/tools/builtin-skills/file-write/SKILL.md +30 -0
  164. package/src/tools/builtin-skills/file-write/handler.ts +64 -0
  165. package/src/tools/builtin-skills/http-request/SKILL.md +34 -0
  166. package/src/tools/builtin-skills/http-request/handler.ts +87 -0
  167. package/src/tools/builtin-skills/shell/SKILL.md +26 -0
  168. package/src/tools/builtin-skills/shell/handler.ts +96 -0
  169. package/src/tools/builtin-skills/url-fetch/SKILL.md +26 -0
  170. package/src/tools/builtin-skills/url-fetch/handler.ts +37 -0
  171. package/src/tools/builtin-skills/web-search/SKILL.md +26 -0
  172. package/src/tools/builtin-skills/web-search/handler.ts +50 -0
  173. package/src/tools/executor.ts +205 -0
  174. package/src/tools/integration-installer.ts +106 -0
  175. package/src/tools/permissions.ts +45 -0
  176. package/src/tools/registry.ts +39 -0
  177. package/src/tools/sandbox-runner.ts +56 -0
  178. package/src/tools/sandbox.ts +82 -0
  179. package/src/tools/skill-installer.ts +52 -0
  180. package/src/tools/skill-loader.ts +259 -0
  181. package/src/types/optional-deps.d.ts +23 -0
  182. package/src/util/auth.ts +121 -0
  183. package/src/util/costs.ts +59 -0
  184. package/src/util/error-buffer.ts +32 -0
  185. package/src/util/google-tokens.ts +180 -0
  186. package/src/util/logger.ts +73 -0
  187. package/src/util/perf-collector.ts +35 -0
  188. package/src/util/rate-limiter.ts +70 -0
  189. package/src/util/tokens.ts +17 -0
  190. package/src/voice/stt.ts +57 -0
  191. package/src/voice/tts.ts +103 -0
  192. package/tests/agent/session.test.ts +109 -0
  193. package/tests/agent-loop.test.ts +54 -0
  194. package/tests/auth.test.ts +89 -0
  195. package/tests/channels.test.ts +67 -0
  196. package/tests/compaction.test.ts +44 -0
  197. package/tests/config.test.ts +51 -0
  198. package/tests/costs.test.ts +19 -0
  199. package/tests/cron.test.ts +55 -0
  200. package/tests/db/export.test.ts +219 -0
  201. package/tests/executor.test.ts +144 -0
  202. package/tests/export.test.ts +137 -0
  203. package/tests/helpers/mock-llm.ts +34 -0
  204. package/tests/helpers/test-db.ts +74 -0
  205. package/tests/integration/chat-flow.test.ts +48 -0
  206. package/tests/integrations.test.ts +97 -0
  207. package/tests/memory/engine.test.ts +114 -0
  208. package/tests/memory-engine.test.ts +57 -0
  209. package/tests/permissions.test.ts +21 -0
  210. package/tests/rate-limiter.test.ts +70 -0
  211. package/tests/registry.test.ts +67 -0
  212. package/tests/router.test.ts +36 -0
  213. package/tests/session.test.ts +58 -0
  214. package/tests/skill-loader.test.ts +44 -0
  215. package/tests/tokens.test.ts +30 -0
  216. package/tests/tools/executor.test.ts +130 -0
  217. package/tests/util/auth.test.ts +75 -0
  218. package/tests/util/rate-limiter.test.ts +73 -0
  219. package/tests/voice.test.ts +60 -0
  220. package/tests/webchat.test.ts +88 -0
  221. package/tests/workflow.test.ts +38 -0
  222. package/tsconfig.json +16 -0
@@ -0,0 +1,409 @@
1
+ import type {
2
+ LlmProvider,
3
+ LlmRequest,
4
+ LlmResponse,
5
+ LlmContentBlock,
6
+ LlmToolDef,
7
+ LlmStreamEvent,
8
+ } from "./provider";
9
+ import { logger } from "../util/logger";
10
+
11
+ export interface OpenAICompatOptions {
12
+ name: string;
13
+ baseUrl: string;
14
+ apiKey: string;
15
+ model: string;
16
+ maxTokens?: number;
17
+ streaming?: boolean;
18
+ contextWindow?: number;
19
+ }
20
+
21
+ const DEFAULT_CONTEXT_WINDOWS: Record<string, number> = {
22
+ openai: 1_000_000, // GPT-4.1 supports 1M tokens
23
+ groq: 128_000,
24
+ together: 32_000,
25
+ openrouter: 200_000, // Varies by model; conservative default
26
+ deepseek: 64_000,
27
+ fireworks: 128_000,
28
+ cerebras: 128_000,
29
+ perplexity: 128_000,
30
+ xai: 131_072, // Grok 4.1 supports up to 2M; safe default
31
+ ollama: 8_000,
32
+ lmstudio: 8_000,
33
+ };
34
+
35
+ interface OpenAIMessage {
36
+ role: "system" | "user" | "assistant" | "tool";
37
+ content?: string | null;
38
+ tool_calls?: OpenAIToolCall[];
39
+ tool_call_id?: string;
40
+ }
41
+
42
+ interface OpenAIToolCall {
43
+ id: string;
44
+ type: "function";
45
+ function: { name: string; arguments: string };
46
+ }
47
+
48
+ interface OpenAITool {
49
+ type: "function";
50
+ function: {
51
+ name: string;
52
+ description: string;
53
+ parameters: Record<string, unknown>;
54
+ };
55
+ }
56
+
57
+ export class OpenAICompatProvider implements LlmProvider {
58
+ providerName: string;
59
+ model: string;
60
+ contextWindow: number;
61
+ private baseUrl: string;
62
+ private apiKey: string;
63
+ private maxTokens: number;
64
+ private streaming: boolean;
65
+
66
+ constructor(opts: OpenAICompatOptions) {
67
+ this.providerName = opts.name;
68
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
69
+ this.apiKey = opts.apiKey;
70
+ this.model = opts.model;
71
+ this.maxTokens = opts.maxTokens ?? 4096;
72
+ this.streaming = opts.streaming ?? true;
73
+ this.contextWindow =
74
+ opts.contextWindow ?? DEFAULT_CONTEXT_WINDOWS[opts.name] ?? 32_000;
75
+ }
76
+
77
+ async chat(request: LlmRequest): Promise<LlmResponse> {
78
+ logger.debug(`${this.providerName} request`, {
79
+ model: this.model,
80
+ messageCount: request.messages.length,
81
+ toolCount: request.tools?.length ?? 0,
82
+ });
83
+
84
+ const messages = this.convertMessages(request.system, request.messages);
85
+ const tools = request.tools?.length
86
+ ? this.convertTools(request.tools)
87
+ : undefined;
88
+
89
+ const body: Record<string, unknown> = {
90
+ model: this.model,
91
+ messages,
92
+ max_tokens: request.maxTokens ?? this.maxTokens,
93
+ stream: false,
94
+ };
95
+ if (tools) body.tools = tools;
96
+
97
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ Authorization: `Bearer ${this.apiKey}`,
102
+ ...(this.providerName === "openrouter" ? {
103
+ "HTTP-Referer": "https://zubo.dev",
104
+ "X-Title": "Zubo AI Agent",
105
+ } : {}),
106
+ },
107
+ body: JSON.stringify(body),
108
+ });
109
+
110
+ if (!res.ok) {
111
+ const text = await res.text();
112
+ throw new Error(
113
+ `${this.providerName} API error ${res.status}: ${text}`
114
+ );
115
+ }
116
+
117
+ const json = (await res.json()) as any;
118
+ const choice = json.choices?.[0];
119
+ if (!choice) {
120
+ throw new Error(`${this.providerName}: no choices in response`);
121
+ }
122
+
123
+ const content = this.parseChoice(choice);
124
+ const stopReason = this.mapStopReason(choice.finish_reason);
125
+
126
+ return {
127
+ content,
128
+ stopReason,
129
+ usage: {
130
+ inputTokens: json.usage?.prompt_tokens ?? 0,
131
+ outputTokens: json.usage?.completion_tokens ?? 0,
132
+ },
133
+ };
134
+ }
135
+
136
+ private convertMessages(
137
+ system: string,
138
+ messages: LlmRequest["messages"]
139
+ ): OpenAIMessage[] {
140
+ const out: OpenAIMessage[] = [{ role: "system", content: system }];
141
+
142
+ for (const msg of messages) {
143
+ if (typeof msg.content === "string") {
144
+ out.push({ role: msg.role, content: msg.content });
145
+ continue;
146
+ }
147
+
148
+ // Multi-block message (tool_use / tool_result / text mix)
149
+ if (msg.role === "assistant") {
150
+ const textParts: string[] = [];
151
+ const toolCalls: OpenAIToolCall[] = [];
152
+
153
+ for (const block of msg.content) {
154
+ if (block.type === "text" && block.text) {
155
+ textParts.push(block.text);
156
+ } else if (block.type === "tool_use") {
157
+ toolCalls.push({
158
+ id: block.id!,
159
+ type: "function",
160
+ function: {
161
+ name: block.name!,
162
+ arguments: JSON.stringify(block.input ?? {}),
163
+ },
164
+ });
165
+ }
166
+ }
167
+
168
+ const assistantMsg: OpenAIMessage = {
169
+ role: "assistant",
170
+ content: textParts.length ? textParts.join("\n") : null,
171
+ };
172
+ if (toolCalls.length) assistantMsg.tool_calls = toolCalls;
173
+ out.push(assistantMsg);
174
+ } else if (msg.role === "user") {
175
+ // User messages may contain tool_result blocks
176
+ const textParts: string[] = [];
177
+ const toolResults: { tool_call_id: string; content: string }[] = [];
178
+
179
+ for (const block of msg.content) {
180
+ if (block.type === "text" && block.text) {
181
+ textParts.push(block.text);
182
+ } else if (block.type === "tool_result") {
183
+ toolResults.push({
184
+ tool_call_id: block.tool_use_id!,
185
+ content: block.content || "No output",
186
+ });
187
+ }
188
+ }
189
+
190
+ // Emit tool results as separate tool messages
191
+ for (const tr of toolResults) {
192
+ out.push({
193
+ role: "tool",
194
+ tool_call_id: tr.tool_call_id,
195
+ content: tr.content,
196
+ });
197
+ }
198
+
199
+ // Emit any text as a user message
200
+ if (textParts.length) {
201
+ out.push({ role: "user", content: textParts.join("\n") });
202
+ }
203
+ }
204
+ }
205
+
206
+ return out;
207
+ }
208
+
209
+ private convertTools(tools: LlmToolDef[]): OpenAITool[] {
210
+ return tools.map((t) => ({
211
+ type: "function",
212
+ function: {
213
+ name: t.name,
214
+ description: t.description,
215
+ parameters: t.input_schema,
216
+ },
217
+ }));
218
+ }
219
+
220
+ private parseChoice(choice: any): LlmContentBlock[] {
221
+ const blocks: LlmContentBlock[] = [];
222
+ const msg = choice.message;
223
+
224
+ if (msg.content) {
225
+ blocks.push({ type: "text", text: msg.content });
226
+ }
227
+
228
+ if (msg.tool_calls?.length) {
229
+ for (const tc of msg.tool_calls) {
230
+ let input: Record<string, unknown> = {};
231
+ try {
232
+ input = JSON.parse(tc.function.arguments);
233
+ } catch {
234
+ input = { _raw: tc.function.arguments };
235
+ }
236
+ blocks.push({
237
+ type: "tool_use",
238
+ id: tc.id,
239
+ name: tc.function.name,
240
+ input,
241
+ });
242
+ }
243
+ }
244
+
245
+ return blocks;
246
+ }
247
+
248
+ private mapStopReason(reason: string): string {
249
+ switch (reason) {
250
+ case "stop":
251
+ return "end_turn";
252
+ case "tool_calls":
253
+ return "tool_use";
254
+ case "length":
255
+ return "max_tokens";
256
+ default:
257
+ return reason ?? "end_turn";
258
+ }
259
+ }
260
+
261
+ async *chatStream(request: LlmRequest): AsyncIterable<LlmStreamEvent> {
262
+ logger.debug(`${this.providerName} stream request`, {
263
+ model: this.model,
264
+ messageCount: request.messages.length,
265
+ toolCount: request.tools?.length ?? 0,
266
+ });
267
+
268
+ const messages = this.convertMessages(request.system, request.messages);
269
+ const tools = request.tools?.length
270
+ ? this.convertTools(request.tools)
271
+ : undefined;
272
+
273
+ const body: Record<string, unknown> = {
274
+ model: this.model,
275
+ messages,
276
+ max_tokens: request.maxTokens ?? this.maxTokens,
277
+ stream: true,
278
+ stream_options: { include_usage: true },
279
+ };
280
+ if (tools) body.tools = tools;
281
+
282
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
283
+ method: "POST",
284
+ headers: {
285
+ "Content-Type": "application/json",
286
+ Authorization: `Bearer ${this.apiKey}`,
287
+ ...(this.providerName === "openrouter" ? {
288
+ "HTTP-Referer": "https://zubo.dev",
289
+ "X-Title": "Zubo AI Agent",
290
+ } : {}),
291
+ },
292
+ body: JSON.stringify(body),
293
+ });
294
+
295
+ if (!res.ok) {
296
+ const text = await res.text();
297
+ throw new Error(`${this.providerName} API error ${res.status}: ${text}`);
298
+ }
299
+
300
+ if (!res.body) {
301
+ throw new Error(`${this.providerName}: no response body for stream`);
302
+ }
303
+
304
+ // Track accumulated state for final message
305
+ let fullText = "";
306
+ const toolCalls = new Map<number, { id: string; name: string; args: string }>();
307
+ let stopReason = "end_turn";
308
+ let promptTokens = 0;
309
+ let completionTokens = 0;
310
+
311
+ const reader = res.body.getReader();
312
+ const decoder = new TextDecoder();
313
+ let buffer = "";
314
+
315
+ try {
316
+ while (true) {
317
+ const { done, value } = await reader.read();
318
+ if (done) break;
319
+
320
+ buffer += decoder.decode(value, { stream: true });
321
+ const lines = buffer.split("\n");
322
+ buffer = lines.pop() ?? "";
323
+
324
+ for (const line of lines) {
325
+ const trimmed = line.trim();
326
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
327
+ const data = trimmed.slice(6);
328
+ if (data === "[DONE]") continue;
329
+
330
+ let parsed: any;
331
+ try { parsed = JSON.parse(data); } catch { continue; }
332
+
333
+ if (parsed.usage) {
334
+ promptTokens = parsed.usage.prompt_tokens ?? promptTokens;
335
+ completionTokens = parsed.usage.completion_tokens ?? completionTokens;
336
+ }
337
+
338
+ const choice = parsed.choices?.[0];
339
+ if (!choice) continue;
340
+
341
+ if (choice.finish_reason) {
342
+ stopReason = this.mapStopReason(choice.finish_reason);
343
+ }
344
+
345
+ const delta = choice.delta;
346
+ if (!delta) continue;
347
+
348
+ // Text content
349
+ if (delta.content) {
350
+ fullText += delta.content;
351
+ yield { type: "text_delta", text: delta.content };
352
+ }
353
+
354
+ // Tool calls
355
+ if (delta.tool_calls) {
356
+ for (const tc of delta.tool_calls) {
357
+ const idx = tc.index ?? 0;
358
+ if (!toolCalls.has(idx)) {
359
+ toolCalls.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", args: "" });
360
+ if (tc.id && tc.function?.name) {
361
+ yield { type: "tool_use_start", id: tc.id, name: tc.function.name };
362
+ }
363
+ }
364
+ const existing = toolCalls.get(idx)!;
365
+ if (tc.id) existing.id = tc.id;
366
+ if (tc.function?.name) existing.name = tc.function.name;
367
+ if (tc.function?.arguments) {
368
+ existing.args += tc.function.arguments;
369
+ yield { type: "tool_use_delta", id: existing.id, json: tc.function.arguments };
370
+ }
371
+ }
372
+ }
373
+ }
374
+ }
375
+ } finally {
376
+ reader.releaseLock();
377
+ // Ensure the response body is fully consumed/cancelled to free resources
378
+ try { await res.body!.cancel(); } catch (err: any) { logger.warn("Failed to cancel response body stream", { error: (err as Error).message }); }
379
+ }
380
+
381
+ // Emit tool_use_end for all tool calls
382
+ for (const [_, tc] of toolCalls) {
383
+ yield { type: "tool_use_end", id: tc.id };
384
+ }
385
+
386
+ // Build final content blocks
387
+ const content: LlmContentBlock[] = [];
388
+ if (fullText) {
389
+ content.push({ type: "text", text: fullText });
390
+ }
391
+ for (const [_, tc] of toolCalls) {
392
+ let input: Record<string, unknown> = {};
393
+ try { input = JSON.parse(tc.args); } catch { input = { _raw: tc.args }; }
394
+ content.push({ type: "tool_use", id: tc.id, name: tc.name, input });
395
+ }
396
+
397
+ yield {
398
+ type: "message_done",
399
+ response: {
400
+ content,
401
+ stopReason,
402
+ usage: {
403
+ inputTokens: promptTokens,
404
+ outputTokens: completionTokens,
405
+ },
406
+ },
407
+ };
408
+ }
409
+ }
@@ -0,0 +1,83 @@
1
+ export interface LlmMessage {
2
+ role: "user" | "assistant";
3
+ content: string | LlmContentBlock[];
4
+ }
5
+
6
+ export interface LlmContentBlock {
7
+ type: "text" | "tool_use" | "tool_result";
8
+ text?: string;
9
+ id?: string;
10
+ name?: string;
11
+ input?: Record<string, unknown>;
12
+ tool_use_id?: string;
13
+ content?: string;
14
+ is_error?: boolean;
15
+ }
16
+
17
+ export interface LlmToolDef {
18
+ name: string;
19
+ description: string;
20
+ input_schema: Record<string, unknown>;
21
+ }
22
+
23
+ export interface LlmRequest {
24
+ system: string;
25
+ messages: LlmMessage[];
26
+ tools?: LlmToolDef[];
27
+ maxTokens?: number;
28
+ }
29
+
30
+ export interface LlmResponse {
31
+ content: LlmContentBlock[];
32
+ stopReason: string;
33
+ usage: {
34
+ inputTokens: number;
35
+ outputTokens: number;
36
+ cacheCreationTokens?: number;
37
+ cacheReadTokens?: number;
38
+ };
39
+ }
40
+
41
+ // --- Streaming types ---
42
+
43
+ export interface LlmStreamEventTextDelta {
44
+ type: "text_delta";
45
+ text: string;
46
+ }
47
+
48
+ export interface LlmStreamEventToolUseStart {
49
+ type: "tool_use_start";
50
+ id: string;
51
+ name: string;
52
+ }
53
+
54
+ export interface LlmStreamEventToolUseDelta {
55
+ type: "tool_use_delta";
56
+ id: string;
57
+ json: string;
58
+ }
59
+
60
+ export interface LlmStreamEventToolUseEnd {
61
+ type: "tool_use_end";
62
+ id: string;
63
+ }
64
+
65
+ export interface LlmStreamEventMessageDone {
66
+ type: "message_done";
67
+ response: LlmResponse;
68
+ }
69
+
70
+ export type LlmStreamEvent =
71
+ | LlmStreamEventTextDelta
72
+ | LlmStreamEventToolUseStart
73
+ | LlmStreamEventToolUseDelta
74
+ | LlmStreamEventToolUseEnd
75
+ | LlmStreamEventMessageDone;
76
+
77
+ export interface LlmProvider {
78
+ providerName: string;
79
+ model: string;
80
+ contextWindow: number;
81
+ chat(request: LlmRequest): Promise<LlmResponse>;
82
+ chatStream?(request: LlmRequest): AsyncIterable<LlmStreamEvent>;
83
+ }