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.
- package/MERGE_REPORT.md +265 -0
- package/README.md +127 -0
- package/package.json +55 -0
- package/packages/cli/src/index.ts +271 -0
- package/packages/cli/src/interactive.ts +190 -0
- package/packages/cli/src/once.ts +50 -0
- package/packages/core/src/agent/config-loader.ts +174 -0
- package/packages/core/src/agent/engine.ts +266 -0
- package/packages/core/src/agent/registry-tools.ts +58 -0
- package/packages/core/src/agent/registry.ts +213 -0
- package/packages/core/src/commands/index.ts +116 -0
- package/packages/core/src/config/index.ts +139 -0
- package/packages/core/src/confirmation/message-bus.ts +206 -0
- package/packages/core/src/diff/index.ts +232 -0
- package/packages/core/src/diff-detect/index.ts +207 -0
- package/packages/core/src/edit-coder/index.ts +194 -0
- package/packages/core/src/events/index.ts +151 -0
- package/packages/core/src/file-tracker/index.ts +183 -0
- package/packages/core/src/git/index.ts +305 -0
- package/packages/core/src/history/index.ts +236 -0
- package/packages/core/src/image/index.ts +131 -0
- package/packages/core/src/index.ts +131 -0
- package/packages/core/src/lsp/index.ts +251 -0
- package/packages/core/src/mcp/index.ts +186 -0
- package/packages/core/src/memory/index.ts +141 -0
- package/packages/core/src/memory/prompts.ts +52 -0
- package/packages/core/src/orchestrator/protocol.ts +140 -0
- package/packages/core/src/orchestrator/server.ts +319 -0
- package/packages/core/src/output/index.ts +164 -0
- package/packages/core/src/permissions/index.ts +126 -0
- package/packages/core/src/prompts/engine.ts +188 -0
- package/packages/core/src/providers/registry.ts +687 -0
- package/packages/core/src/routing/model-router.ts +160 -0
- package/packages/core/src/server/index.ts +232 -0
- package/packages/core/src/session/index.ts +174 -0
- package/packages/core/src/session/service.ts +322 -0
- package/packages/core/src/skills/index.ts +205 -0
- package/packages/core/src/streaming/index.ts +166 -0
- package/packages/core/src/sub-agent/index.ts +179 -0
- package/packages/core/src/tools/builtin.ts +568 -0
- package/packages/core/src/types.ts +356 -0
- package/packages/sdk/src/index.ts +247 -0
- package/packages/tui/src/index.ts +141 -0
- 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
|
+
}
|