zero-ai-cli 1.0.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 (44) hide show
  1. package/MERGE_REPORT.md +265 -0
  2. package/README.md +127 -0
  3. package/package.json +55 -0
  4. package/packages/cli/src/index.ts +271 -0
  5. package/packages/cli/src/interactive.ts +190 -0
  6. package/packages/cli/src/once.ts +50 -0
  7. package/packages/core/src/agent/config-loader.ts +174 -0
  8. package/packages/core/src/agent/engine.ts +266 -0
  9. package/packages/core/src/agent/registry-tools.ts +58 -0
  10. package/packages/core/src/agent/registry.ts +213 -0
  11. package/packages/core/src/commands/index.ts +116 -0
  12. package/packages/core/src/config/index.ts +139 -0
  13. package/packages/core/src/confirmation/message-bus.ts +206 -0
  14. package/packages/core/src/diff/index.ts +232 -0
  15. package/packages/core/src/diff-detect/index.ts +207 -0
  16. package/packages/core/src/edit-coder/index.ts +194 -0
  17. package/packages/core/src/events/index.ts +151 -0
  18. package/packages/core/src/file-tracker/index.ts +183 -0
  19. package/packages/core/src/git/index.ts +305 -0
  20. package/packages/core/src/history/index.ts +236 -0
  21. package/packages/core/src/image/index.ts +131 -0
  22. package/packages/core/src/index.ts +131 -0
  23. package/packages/core/src/lsp/index.ts +251 -0
  24. package/packages/core/src/mcp/index.ts +186 -0
  25. package/packages/core/src/memory/index.ts +141 -0
  26. package/packages/core/src/memory/prompts.ts +52 -0
  27. package/packages/core/src/orchestrator/protocol.ts +140 -0
  28. package/packages/core/src/orchestrator/server.ts +319 -0
  29. package/packages/core/src/output/index.ts +164 -0
  30. package/packages/core/src/permissions/index.ts +126 -0
  31. package/packages/core/src/prompts/engine.ts +188 -0
  32. package/packages/core/src/providers/registry.ts +687 -0
  33. package/packages/core/src/routing/model-router.ts +160 -0
  34. package/packages/core/src/server/index.ts +232 -0
  35. package/packages/core/src/session/index.ts +174 -0
  36. package/packages/core/src/session/service.ts +322 -0
  37. package/packages/core/src/skills/index.ts +205 -0
  38. package/packages/core/src/streaming/index.ts +166 -0
  39. package/packages/core/src/sub-agent/index.ts +179 -0
  40. package/packages/core/src/tools/builtin.ts +568 -0
  41. package/packages/core/src/types.ts +356 -0
  42. package/packages/sdk/src/index.ts +247 -0
  43. package/packages/tui/src/index.ts +141 -0
  44. package/tsconfig.json +26 -0
@@ -0,0 +1,687 @@
1
+ /**
2
+ * ZERO LLM Provider System
3
+ * Unified interface for all LLM providers
4
+ *
5
+ * Sources:
6
+ * - gemini-cli: model routing, content generation
7
+ * - qwen-code: multi-provider support with presets
8
+ * - pi: provider abstraction layer
9
+ * - opencode: LLM routing
10
+ * - goose: provider types system
11
+ * - cline: extensive API support
12
+ */
13
+
14
+ import type {
15
+ LLMProvider,
16
+ LLMClient,
17
+ ProviderConfig,
18
+ ProviderType,
19
+ ModelConfig,
20
+ Message,
21
+ ChatOptions,
22
+ ChatResponse,
23
+ StreamChunk,
24
+ MessageContent,
25
+ ToolDefinition,
26
+ TokenUsage,
27
+ StopReason,
28
+ } from "../types.js";
29
+
30
+ // ============================================================================
31
+ // Provider Registry
32
+ // ============================================================================
33
+
34
+ export class ProviderRegistry {
35
+ private providers = new Map<string, LLMProvider>();
36
+
37
+ register(provider: LLMProvider): void {
38
+ this.providers.set(provider.id, provider);
39
+ }
40
+
41
+ get(id: string): LLMProvider | undefined {
42
+ return this.providers.get(id);
43
+ }
44
+
45
+ list(): LLMProvider[] {
46
+ return Array.from(this.providers.values());
47
+ }
48
+
49
+ getAllModels(): { provider: string; model: ModelConfig }[] {
50
+ const models: { provider: string; model: ModelConfig }[] = [];
51
+ for (const provider of this.providers.values()) {
52
+ for (const model of provider.models) {
53
+ models.push({ provider: provider.id, model });
54
+ }
55
+ }
56
+ return models;
57
+ }
58
+
59
+ createClient(providerId: string, config: ProviderConfig): LLMClient {
60
+ const provider = this.providers.get(providerId);
61
+ if (!provider) {
62
+ throw new Error(`Provider "${providerId}" not found. Available: ${Array.from(this.providers.keys()).join(", ")}`);
63
+ }
64
+ return provider.createClient(config);
65
+ }
66
+
67
+ /**
68
+ * Load all built-in providers
69
+ */
70
+ loadBuiltins(): void {
71
+ this.register(createOpenAIProvider());
72
+ this.register(createAnthropicProvider());
73
+ this.register(createGoogleProvider());
74
+ this.register(createOllamaProvider());
75
+ this.register(createCustomProvider());
76
+ }
77
+ }
78
+
79
+ // ============================================================================
80
+ // OpenAI-Compatible Provider
81
+ // ============================================================================
82
+
83
+ function createOpenAIProvider(): LLMProvider {
84
+ return {
85
+ id: "openai",
86
+ name: "OpenAI",
87
+ type: "openai",
88
+ models: [
89
+ {
90
+ id: "gpt-4o",
91
+ name: "GPT-4o",
92
+ contextWindow: 128000,
93
+ maxOutputTokens: 16384,
94
+ supportsImages: true,
95
+ supportsToolUse: true,
96
+ supportsStreaming: true,
97
+ },
98
+ {
99
+ id: "gpt-4o-mini",
100
+ name: "GPT-4o Mini",
101
+ contextWindow: 128000,
102
+ maxOutputTokens: 16384,
103
+ supportsImages: true,
104
+ supportsToolUse: true,
105
+ supportsStreaming: true,
106
+ },
107
+ {
108
+ id: "o1",
109
+ name: "o1",
110
+ contextWindow: 200000,
111
+ maxOutputTokens: 100000,
112
+ supportsImages: true,
113
+ supportsToolUse: true,
114
+ supportsStreaming: false,
115
+ },
116
+ {
117
+ id: "o3-mini",
118
+ name: "o3-mini",
119
+ contextWindow: 200000,
120
+ maxOutputTokens: 100000,
121
+ supportsImages: false,
122
+ supportsToolUse: true,
123
+ supportsStreaming: false,
124
+ },
125
+ ],
126
+ createClient: (config) => new OpenAICompatibleClient(config, "https://api.openai.com/v1"),
127
+ };
128
+ }
129
+
130
+ // ============================================================================
131
+ // Anthropic Provider
132
+ // ============================================================================
133
+
134
+ function createAnthropicProvider(): LLMProvider {
135
+ return {
136
+ id: "anthropic",
137
+ name: "Anthropic",
138
+ type: "anthropic",
139
+ models: [
140
+ {
141
+ id: "claude-sonnet-4-20250514",
142
+ name: "Claude Sonnet 4",
143
+ contextWindow: 200000,
144
+ maxOutputTokens: 8192,
145
+ supportsImages: true,
146
+ supportsToolUse: true,
147
+ supportsStreaming: true,
148
+ },
149
+ {
150
+ id: "claude-3-5-haiku-20241022",
151
+ name: "Claude 3.5 Haiku",
152
+ contextWindow: 200000,
153
+ maxOutputTokens: 8192,
154
+ supportsImages: true,
155
+ supportsToolUse: true,
156
+ supportsStreaming: true,
157
+ },
158
+ ],
159
+ createClient: (config) => new AnthropicClient(config),
160
+ };
161
+ }
162
+
163
+ // ============================================================================
164
+ // Google Provider
165
+ // ============================================================================
166
+
167
+ function createGoogleProvider(): LLMProvider {
168
+ return {
169
+ id: "google",
170
+ name: "Google",
171
+ type: "google",
172
+ models: [
173
+ {
174
+ id: "gemini-2.5-pro",
175
+ name: "Gemini 2.5 Pro",
176
+ contextWindow: 1000000,
177
+ maxOutputTokens: 65536,
178
+ supportsImages: true,
179
+ supportsToolUse: true,
180
+ supportsStreaming: true,
181
+ },
182
+ {
183
+ id: "gemini-2.5-flash",
184
+ name: "Gemini 2.5 Flash",
185
+ contextWindow: 1000000,
186
+ maxOutputTokens: 65536,
187
+ supportsImages: true,
188
+ supportsToolUse: true,
189
+ supportsStreaming: true,
190
+ },
191
+ ],
192
+ createClient: (config) => new GoogleClient(config),
193
+ };
194
+ }
195
+
196
+ // ============================================================================
197
+ // Ollama Provider (Local)
198
+ // ============================================================================
199
+
200
+ function createOllamaProvider(): LLMProvider {
201
+ return {
202
+ id: "ollama",
203
+ name: "Ollama (Local)",
204
+ type: "ollama",
205
+ models: [
206
+ {
207
+ id: "llama3.1",
208
+ name: "Llama 3.1",
209
+ contextWindow: 128000,
210
+ maxOutputTokens: 8192,
211
+ supportsImages: false,
212
+ supportsToolUse: true,
213
+ supportsStreaming: true,
214
+ },
215
+ {
216
+ id: "qwen2.5-coder",
217
+ name: "Qwen 2.5 Coder",
218
+ contextWindow: 32768,
219
+ maxOutputTokens: 8192,
220
+ supportsImages: false,
221
+ supportsToolUse: true,
222
+ supportsStreaming: true,
223
+ },
224
+ {
225
+ id: "codellama",
226
+ name: "Code Llama",
227
+ contextWindow: 16384,
228
+ maxOutputTokens: 4096,
229
+ supportsImages: false,
230
+ supportsToolUse: false,
231
+ supportsStreaming: true,
232
+ },
233
+ ],
234
+ createClient: (config) => new OpenAICompatibleClient(config, "http://localhost:11434/v1"),
235
+ };
236
+ }
237
+
238
+ // ============================================================================
239
+ // Custom Provider (OpenAI-compatible endpoints)
240
+ // ============================================================================
241
+
242
+ function createCustomProvider(): LLMProvider {
243
+ return {
244
+ id: "custom",
245
+ name: "Custom (OpenAI-compatible)",
246
+ type: "custom",
247
+ models: [
248
+ {
249
+ id: "custom-model",
250
+ name: "Custom Model",
251
+ contextWindow: 128000,
252
+ maxOutputTokens: 8192,
253
+ supportsImages: false,
254
+ supportsToolUse: true,
255
+ supportsStreaming: true,
256
+ },
257
+ ],
258
+ createClient: (config) => new OpenAICompatibleClient(config, config.baseUrl || "http://localhost:8080/v1"),
259
+ };
260
+ }
261
+
262
+ // ============================================================================
263
+ // OpenAI-Compatible Client
264
+ // ============================================================================
265
+
266
+ class OpenAICompatibleClient implements LLMClient {
267
+ constructor(private config: ProviderConfig, private baseUrl: string) {}
268
+
269
+ async chat(messages: Message[], options?: ChatOptions): Promise<ChatResponse> {
270
+ const body = this.buildRequestBody(messages, options);
271
+
272
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
273
+ method: "POST",
274
+ headers: {
275
+ "Content-Type": "application/json",
276
+ ...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {}),
277
+ ...this.config.headers,
278
+ },
279
+ body: JSON.stringify(body),
280
+ });
281
+
282
+ if (!response.ok) {
283
+ const error = await response.text();
284
+ throw new Error(`OpenAI API error (${response.status}): ${error}`);
285
+ }
286
+
287
+ const data = await response.json() as any;
288
+ return this.parseResponse(data);
289
+ }
290
+
291
+ async *stream(messages: Message[], options?: ChatOptions): AsyncIterable<StreamChunk> {
292
+ const body = { ...this.buildRequestBody(messages, options), stream: true };
293
+
294
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
295
+ method: "POST",
296
+ headers: {
297
+ "Content-Type": "application/json",
298
+ ...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {}),
299
+ ...this.config.headers,
300
+ },
301
+ body: JSON.stringify(body),
302
+ });
303
+
304
+ if (!response.ok) {
305
+ throw new Error(`OpenAI API error: ${response.status}`);
306
+ }
307
+
308
+ const reader = response.body?.getReader();
309
+ if (!reader) throw new Error("No response body");
310
+
311
+ const decoder = new TextDecoder();
312
+ let buffer = "";
313
+
314
+ while (true) {
315
+ const { done, value } = await reader.read();
316
+ if (done) break;
317
+
318
+ buffer += decoder.decode(value, { stream: true });
319
+ const lines = buffer.split("\n");
320
+ buffer = lines.pop() || "";
321
+
322
+ for (const line of lines) {
323
+ if (line.startsWith("data: ")) {
324
+ const data = line.slice(6).trim();
325
+ if (data === "[DONE]") {
326
+ yield { type: "done", stopReason: "end_turn" };
327
+ return;
328
+ }
329
+ try {
330
+ const parsed = JSON.parse(data);
331
+ const delta = parsed.choices?.[0]?.delta;
332
+ if (delta?.content) {
333
+ yield { type: "text_delta", text: delta.content };
334
+ }
335
+ } catch {}
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ private buildRequestBody(messages: Message[], options?: ChatOptions): Record<string, unknown> {
342
+ const formattedMessages = messages.map((msg) => ({
343
+ role: msg.role === "tool" ? "tool" : msg.role,
344
+ content: this.formatContent(msg.content),
345
+ }));
346
+
347
+ if (options?.systemPrompt) {
348
+ formattedMessages.unshift({ role: "system", content: options.systemPrompt } as any);
349
+ }
350
+
351
+ const body: Record<string, unknown> = {
352
+ model: this.config.model,
353
+ messages: formattedMessages,
354
+ max_tokens: options?.maxTokens || this.config.maxTokens || 4096,
355
+ temperature: options?.temperature ?? this.config.temperature ?? 0.7,
356
+ };
357
+
358
+ if (options?.tools && options.tools.length > 0) {
359
+ body.tools = options.tools.map((tool) => ({
360
+ type: "function",
361
+ function: {
362
+ name: tool.name,
363
+ description: tool.description,
364
+ parameters: {
365
+ type: "object",
366
+ properties: Object.fromEntries(
367
+ Object.entries(tool.parameters).map(([key, param]) => [
368
+ key,
369
+ { type: param.type, description: param.description },
370
+ ])
371
+ ),
372
+ required: Object.entries(tool.parameters)
373
+ .filter(([_, p]) => p.required)
374
+ .map(([key]) => key),
375
+ },
376
+ },
377
+ }));
378
+ }
379
+
380
+ return body;
381
+ }
382
+
383
+ private formatContent(content: MessageContent[]): string | Array<Record<string, unknown>> {
384
+ if (content.length === 1 && content[0].type === "text") {
385
+ return content[0].text;
386
+ }
387
+
388
+ return content.map((c) => {
389
+ switch (c.type) {
390
+ case "text":
391
+ return { type: "text", text: c.text };
392
+ case "image":
393
+ return { type: "image_url", image_url: { url: `data:${c.mimeType};base64,${c.data}` } };
394
+ case "tool_call":
395
+ return { type: "text", text: `[Tool call: ${c.name}]` };
396
+ case "tool_result":
397
+ return { type: "text", text: `[Tool result: ${typeof c.result === "string" ? c.result : JSON.stringify(c.result)}]` };
398
+ }
399
+ });
400
+ }
401
+
402
+ private parseResponse(data: any): ChatResponse {
403
+ const choice = data.choices?.[0];
404
+ const content: MessageContent[] = [];
405
+
406
+ if (choice?.message?.content) {
407
+ content.push({ type: "text", text: choice.message.content });
408
+ }
409
+
410
+ if (choice?.message?.tool_calls) {
411
+ for (const tc of choice.message.tool_calls) {
412
+ content.push({
413
+ type: "tool_call",
414
+ id: tc.id,
415
+ name: tc.function.name,
416
+ arguments: JSON.parse(tc.function.arguments || "{}"),
417
+ });
418
+ }
419
+ }
420
+
421
+ let stopReason: StopReason = "end_turn";
422
+ if (choice?.finish_reason === "tool_calls") stopReason = "tool_use";
423
+ else if (choice?.finish_reason === "length") stopReason = "max_tokens";
424
+ else if (choice?.finish_reason === "stop") stopReason = "end_turn";
425
+
426
+ return {
427
+ id: data.id || `resp_${Date.now()}`,
428
+ content,
429
+ usage: {
430
+ inputTokens: data.usage?.prompt_tokens || 0,
431
+ outputTokens: data.usage?.completion_tokens || 0,
432
+ },
433
+ stopReason,
434
+ model: data.model || this.config.model,
435
+ };
436
+ }
437
+ }
438
+
439
+ // ============================================================================
440
+ // Anthropic Client
441
+ // ============================================================================
442
+
443
+ class AnthropicClient implements LLMClient {
444
+ constructor(private config: ProviderConfig) {}
445
+
446
+ async chat(messages: Message[], options?: ChatOptions): Promise<ChatResponse> {
447
+ const formattedMessages = this.formatMessages(messages);
448
+
449
+ const body: Record<string, unknown> = {
450
+ model: this.config.model,
451
+ max_tokens: options?.maxTokens || this.config.maxTokens || 8192,
452
+ messages: formattedMessages,
453
+ };
454
+
455
+ if (options?.systemPrompt) {
456
+ body.system = options.systemPrompt;
457
+ }
458
+
459
+ if (options?.tools && options.tools.length > 0) {
460
+ body.tools = options.tools.map((tool) => ({
461
+ name: tool.name,
462
+ description: tool.description,
463
+ input_schema: {
464
+ type: "object",
465
+ properties: Object.fromEntries(
466
+ Object.entries(tool.parameters).map(([key, param]) => [
467
+ key,
468
+ { type: param.type, description: param.description },
469
+ ])
470
+ ),
471
+ required: Object.entries(tool.parameters)
472
+ .filter(([_, p]) => p.required)
473
+ .map(([key]) => key),
474
+ },
475
+ }));
476
+ }
477
+
478
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
479
+ method: "POST",
480
+ headers: {
481
+ "Content-Type": "application/json",
482
+ "x-api-key": this.config.apiKey || "",
483
+ "anthropic-version": "2023-06-01",
484
+ ...this.config.headers,
485
+ },
486
+ body: JSON.stringify(body),
487
+ });
488
+
489
+ if (!response.ok) {
490
+ const error = await response.text();
491
+ throw new Error(`Anthropic API error (${response.status}): ${error}`);
492
+ }
493
+
494
+ const data = await response.json() as any;
495
+ return this.parseResponse(data);
496
+ }
497
+
498
+ async *stream(messages: Message[], options?: ChatOptions): AsyncIterable<StreamChunk> {
499
+ // Simplified streaming - would implement SSE parsing in production
500
+ const response = await this.chat(messages, options);
501
+ for (const content of response.content) {
502
+ if (content.type === "text") {
503
+ yield { type: "text_delta", text: content.text };
504
+ }
505
+ }
506
+ yield { type: "done", stopReason: response.stopReason };
507
+ }
508
+
509
+ private formatMessages(messages: Message[]): Array<{ role: string; content: any }> {
510
+ return messages
511
+ .filter((m) => m.role !== "system")
512
+ .map((msg) => ({
513
+ role: msg.role === "tool" ? "user" : msg.role,
514
+ content: msg.content.map((c) => {
515
+ switch (c.type) {
516
+ case "text":
517
+ return { type: "text", text: c.text };
518
+ case "image":
519
+ return { type: "image", source: { type: "base64", media_type: c.mimeType, data: c.data } };
520
+ case "tool_call":
521
+ return { type: "tool_use", id: c.id, name: c.name, input: c.arguments };
522
+ case "tool_result":
523
+ return { type: "tool_result", tool_use_id: c.id, content: typeof c.result === "string" ? c.result : JSON.stringify(c.result) };
524
+ }
525
+ }),
526
+ }));
527
+ }
528
+
529
+ private parseResponse(data: any): ChatResponse {
530
+ const content: MessageContent[] = [];
531
+
532
+ for (const block of data.content || []) {
533
+ if (block.type === "text") {
534
+ content.push({ type: "text", text: block.text });
535
+ } else if (block.type === "tool_use") {
536
+ content.push({
537
+ type: "tool_call",
538
+ id: block.id,
539
+ name: block.name,
540
+ arguments: block.input || {},
541
+ });
542
+ }
543
+ }
544
+
545
+ let stopReason: StopReason = "end_turn";
546
+ if (data.stop_reason === "tool_use") stopReason = "tool_use";
547
+ else if (data.stop_reason === "max_tokens") stopReason = "max_tokens";
548
+
549
+ return {
550
+ id: data.id || `resp_${Date.now()}`,
551
+ content,
552
+ usage: {
553
+ inputTokens: data.usage?.input_tokens || 0,
554
+ outputTokens: data.usage?.output_tokens || 0,
555
+ },
556
+ stopReason,
557
+ model: data.model || this.config.model,
558
+ };
559
+ }
560
+ }
561
+
562
+ // ============================================================================
563
+ // Google (Gemini) Client
564
+ // ============================================================================
565
+
566
+ class GoogleClient implements LLMClient {
567
+ constructor(private config: ProviderConfig) {}
568
+
569
+ async chat(messages: Message[], options?: ChatOptions): Promise<ChatResponse> {
570
+ const contents = this.formatMessages(messages);
571
+ const baseUrl = `https://generativelanguage.googleapis.com/v1beta/models/${this.config.model}:generateContent`;
572
+
573
+ const body: Record<string, unknown> = {
574
+ contents,
575
+ generationConfig: {
576
+ maxOutputTokens: options?.maxTokens || this.config.maxTokens || 8192,
577
+ temperature: options?.temperature ?? this.config.temperature ?? 0.7,
578
+ },
579
+ };
580
+
581
+ if (options?.systemPrompt) {
582
+ body.systemInstruction = { parts: [{ text: options.systemPrompt }] };
583
+ }
584
+
585
+ if (options?.tools && options.tools.length > 0) {
586
+ body.tools = [{
587
+ functionDeclarations: options.tools.map((tool) => ({
588
+ name: tool.name,
589
+ description: tool.description,
590
+ parameters: {
591
+ type: "OBJECT",
592
+ properties: Object.fromEntries(
593
+ Object.entries(tool.parameters).map(([key, param]) => [
594
+ key,
595
+ { type: param.type.toUpperCase(), description: param.description },
596
+ ])
597
+ ),
598
+ required: Object.entries(tool.parameters)
599
+ .filter(([_, p]) => p.required)
600
+ .map(([key]) => key),
601
+ },
602
+ })),
603
+ }];
604
+ }
605
+
606
+ const url = this.config.apiKey
607
+ ? `${baseUrl}?key=${this.config.apiKey}`
608
+ : baseUrl;
609
+
610
+ const response = await fetch(url, {
611
+ method: "POST",
612
+ headers: { "Content-Type": "application/json", ...this.config.headers },
613
+ body: JSON.stringify(body),
614
+ });
615
+
616
+ if (!response.ok) {
617
+ const error = await response.text();
618
+ throw new Error(`Google API error (${response.status}): ${error}`);
619
+ }
620
+
621
+ const data = await response.json() as any;
622
+ return this.parseResponse(data);
623
+ }
624
+
625
+ async *stream(messages: Message[], options?: ChatOptions): AsyncIterable<StreamChunk> {
626
+ const response = await this.chat(messages, options);
627
+ for (const content of response.content) {
628
+ if (content.type === "text") {
629
+ yield { type: "text_delta", text: content.text };
630
+ }
631
+ }
632
+ yield { type: "done", stopReason: response.stopReason };
633
+ }
634
+
635
+ private formatMessages(messages: Message[]): Array<{ role: string; parts: any[] }> {
636
+ return messages
637
+ .filter((m) => m.role !== "system")
638
+ .map((msg) => ({
639
+ role: msg.role === "assistant" ? "model" : "user",
640
+ parts: msg.content.map((c) => {
641
+ switch (c.type) {
642
+ case "text":
643
+ return { text: c.text };
644
+ case "image":
645
+ return { inlineData: { mimeType: c.mimeType, data: c.data } };
646
+ case "tool_call":
647
+ return { functionCall: { name: c.name, args: c.arguments } };
648
+ case "tool_result":
649
+ return { functionResponse: { name: c.name, response: { result: c.result } } };
650
+ }
651
+ }),
652
+ }));
653
+ }
654
+
655
+ private parseResponse(data: any): ChatResponse {
656
+ const content: MessageContent[] = [];
657
+ const candidate = data.candidates?.[0];
658
+
659
+ for (const part of candidate?.content?.parts || []) {
660
+ if (part.text) {
661
+ content.push({ type: "text", text: part.text });
662
+ } else if (part.functionCall) {
663
+ content.push({
664
+ type: "tool_call",
665
+ id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
666
+ name: part.functionCall.name,
667
+ arguments: part.functionCall.args || {},
668
+ });
669
+ }
670
+ }
671
+
672
+ let stopReason: StopReason = "end_turn";
673
+ if (candidate?.finishReason === "FUNCTION_CALL") stopReason = "tool_use";
674
+ else if (candidate?.finishReason === "MAX_TOKENS") stopReason = "max_tokens";
675
+
676
+ return {
677
+ id: `resp_${Date.now()}`,
678
+ content,
679
+ usage: {
680
+ inputTokens: data.usageMetadata?.promptTokenCount || 0,
681
+ outputTokens: data.usageMetadata?.candidatesTokenCount || 0,
682
+ },
683
+ stopReason,
684
+ model: this.config.model,
685
+ };
686
+ }
687
+ }