klaus-agent 0.1.0 → 0.2.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 (52) hide show
  1. package/dist/core/agent.js +14 -0
  2. package/dist/core/agent.js.map +1 -1
  3. package/dist/extensions/runner.d.ts +2 -0
  4. package/dist/extensions/runner.js +13 -0
  5. package/dist/extensions/runner.js.map +1 -1
  6. package/dist/extensions/types.d.ts +2 -1
  7. package/dist/index.d.ts +7 -1
  8. package/dist/index.js +6 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/llm/provider.d.ts +2 -11
  11. package/dist/llm/provider.js +18 -221
  12. package/dist/llm/provider.js.map +1 -1
  13. package/dist/llm/types.d.ts +4 -0
  14. package/dist/providers/anthropic.d.ts +7 -0
  15. package/dist/providers/anthropic.js +173 -0
  16. package/dist/providers/anthropic.js.map +1 -0
  17. package/dist/providers/google.d.ts +8 -0
  18. package/dist/providers/google.js +148 -0
  19. package/dist/providers/google.js.map +1 -0
  20. package/dist/providers/index.d.ts +6 -0
  21. package/dist/providers/index.js +7 -0
  22. package/dist/providers/index.js.map +1 -0
  23. package/dist/providers/kimi.d.ts +4 -0
  24. package/dist/providers/kimi.js +8 -0
  25. package/dist/providers/kimi.js.map +1 -0
  26. package/dist/providers/minimax.d.ts +4 -0
  27. package/dist/providers/minimax.js +8 -0
  28. package/dist/providers/minimax.js.map +1 -0
  29. package/dist/providers/openai.d.ts +7 -0
  30. package/dist/providers/openai.js +170 -0
  31. package/dist/providers/openai.js.map +1 -0
  32. package/dist/providers/shared.d.ts +10 -0
  33. package/dist/providers/shared.js +49 -0
  34. package/dist/providers/shared.js.map +1 -0
  35. package/dist/providers/volcengine.d.ts +4 -0
  36. package/dist/providers/volcengine.js +8 -0
  37. package/dist/providers/volcengine.js.map +1 -0
  38. package/package.json +4 -3
  39. package/src/core/agent.ts +14 -0
  40. package/src/extensions/runner.ts +16 -0
  41. package/src/extensions/types.ts +2 -1
  42. package/src/index.ts +7 -0
  43. package/src/llm/provider.ts +26 -244
  44. package/src/llm/types.ts +2 -0
  45. package/src/providers/anthropic.ts +185 -0
  46. package/src/providers/google.ts +171 -0
  47. package/src/providers/index.ts +6 -0
  48. package/src/providers/kimi.ts +12 -0
  49. package/src/providers/minimax.ts +12 -0
  50. package/src/providers/openai.ts +197 -0
  51. package/src/providers/shared.ts +60 -0
  52. package/src/providers/volcengine.ts +12 -0
@@ -0,0 +1,49 @@
1
+ // Shared streaming utilities for LLM providers
2
+ // Retryable error patterns common across providers
3
+ const COMMON_RETRYABLE = ["429", "500", "503", "ECONNRESET"];
4
+ export const RETRYABLE_PATTERNS = {
5
+ anthropic: [...COMMON_RETRYABLE, "rate_limit", "overloaded", "529"],
6
+ openai: [...COMMON_RETRYABLE, "rate_limit"],
7
+ google: [...COMMON_RETRYABLE],
8
+ };
9
+ export function isRetryableError(error, patterns) {
10
+ return patterns.some((p) => error.message.includes(p));
11
+ }
12
+ /**
13
+ * Wraps a streaming generator with exponential backoff retry logic.
14
+ * Only retries on connection-level failures before streaming starts.
15
+ */
16
+ export async function* withRetry(streamOnce, retryablePatterns, maxRetries = 3) {
17
+ let lastError = null;
18
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
19
+ if (attempt > 0) {
20
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 4000);
21
+ await new Promise((r) => setTimeout(r, delay));
22
+ }
23
+ try {
24
+ yield* streamOnce();
25
+ return;
26
+ }
27
+ catch (err) {
28
+ lastError = err instanceof Error ? err : new Error(String(err));
29
+ if (!isRetryableError(lastError, retryablePatterns) || attempt === maxRetries) {
30
+ yield { type: "error", error: lastError };
31
+ return;
32
+ }
33
+ }
34
+ }
35
+ }
36
+ /** Maps ThinkingLevel to token budget (shared by Anthropic and Gemini). */
37
+ export function mapThinkingBudget(level) {
38
+ if (!level || level === "off")
39
+ return undefined;
40
+ const budgets = {
41
+ minimal: 1024,
42
+ low: 4096,
43
+ medium: 10240,
44
+ high: 20480,
45
+ xhigh: 40960,
46
+ };
47
+ return budgets[level];
48
+ }
49
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/providers/shared.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAI/C,mDAAmD;AACnD,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;AAE7D,MAAM,CAAC,MAAM,kBAAkB,GAA6B;IAC1D,SAAS,EAAE,CAAC,GAAG,gBAAgB,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,CAAC;IACnE,MAAM,EAAE,CAAC,GAAG,gBAAgB,EAAE,YAAY,CAAC;IAC3C,MAAM,EAAE,CAAC,GAAG,gBAAgB,CAAC;CAC9B,CAAC;AAEF,MAAM,UAAU,gBAAgB,CAAC,KAAY,EAAE,QAAkB;IAC/D,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,SAAS,CAC9B,UAAsD,EACtD,iBAA2B,EAC3B,UAAU,GAAG,CAAC;IAEd,IAAI,SAAS,GAAiB,IAAI,CAAC;IAEnC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAChB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAC9D,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QACjD,CAAC;QAED,IAAI,CAAC;YACH,KAAK,CAAC,CAAC,UAAU,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,SAAS,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAEhE,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,iBAAiB,CAAC,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;gBAC9E,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;gBAC1C,OAAO;YACT,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,KAAqB;IACrD,IAAI,CAAC,KAAK,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,OAAO,GAA2B;QACtC,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,IAAI;QACT,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,KAAK;KACb,CAAC;IACF,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { OpenAIProvider } from "./openai.js";
2
+ export declare class VolcengineProvider extends OpenAIProvider {
3
+ constructor(apiKey?: string, baseUrl?: string);
4
+ }
@@ -0,0 +1,8 @@
1
+ // Volcengine (Doubao) LLM provider (OpenAI Chat Completions API compatible)
2
+ import { OpenAIProvider } from "./openai.js";
3
+ export class VolcengineProvider extends OpenAIProvider {
4
+ constructor(apiKey, baseUrl) {
5
+ super(apiKey ?? process.env.VOLCENGINE_API_KEY, baseUrl ?? "https://ark.cn-beijing.volces.com/api/v3");
6
+ }
7
+ }
8
+ //# sourceMappingURL=volcengine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"volcengine.js","sourceRoot":"","sources":["../../src/providers/volcengine.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAE5E,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,OAAO,kBAAmB,SAAQ,cAAc;IACpD,YAAY,MAAe,EAAE,OAAgB;QAC3C,KAAK,CACH,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,EACxC,OAAO,IAAI,0CAA0C,CACtD,CAAC;IACJ,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klaus-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Universal agent framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,10 +17,11 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@anthropic-ai/sdk": "^0.39.0",
20
- "@sinclair/typebox": "^0.34.0"
20
+ "@google/generative-ai": "^0.21.0",
21
+ "@sinclair/typebox": "^0.34.0",
22
+ "openai": "^4.104.0"
21
23
  },
22
24
  "devDependencies": {
23
- "@types/node": "^25.5.0",
24
25
  "typescript": "^5.7.0"
25
26
  }
26
27
  }
package/src/core/agent.ts CHANGED
@@ -289,6 +289,20 @@ export class Agent {
289
289
  if (this._config.extensions?.length) {
290
290
  this._extensionRunner = new ExtensionRunner();
291
291
  await this._extensionRunner.loadExtensions(this._config.extensions);
292
+
293
+ // Re-resolve provider if extensions registered one for the configured provider
294
+ const extProviders = this._extensionRunner.getRegisteredProviders();
295
+ const extFactory = extProviders.get(this._config.model.provider);
296
+ if (extFactory) {
297
+ try {
298
+ this._provider = extFactory({
299
+ apiKey: this._config.model.apiKey,
300
+ baseUrl: this._config.model.baseUrl,
301
+ });
302
+ } catch {
303
+ // Extension provider factory failed, keep the original provider
304
+ }
305
+ }
292
306
  }
293
307
 
294
308
  // Notify extensions of session start
@@ -22,12 +22,14 @@ import type {
22
22
  } from "./types.js";
23
23
  import type { AgentTool } from "../tools/types.js";
24
24
  import type { AgentMessage } from "../types.js";
25
+ import type { LLMProviderFactory } from "../llm/types.js";
25
26
  import type { TSchema } from "@sinclair/typebox";
26
27
 
27
28
  interface RegisteredExtension {
28
29
  handlers: Map<ExtensionEventType, ExtensionHandler<any>[]>;
29
30
  tools: AgentTool[];
30
31
  commands: Map<string, CommandHandler>;
32
+ providers: Map<string, LLMProviderFactory>;
31
33
  }
32
34
 
33
35
  export class ExtensionRunner {
@@ -40,6 +42,7 @@ export class ExtensionRunner {
40
42
  handlers: new Map(),
41
43
  tools: [],
42
44
  commands: new Map(),
45
+ providers: new Map(),
43
46
  };
44
47
 
45
48
  const api: ExtensionAPI = {
@@ -53,6 +56,9 @@ export class ExtensionRunner {
53
56
  registerCommand: (name: string, handler: CommandHandler) => {
54
57
  ext.commands.set(name, handler);
55
58
  },
59
+ registerProvider: (name: string, factory: LLMProviderFactory) => {
60
+ ext.providers.set(name, factory);
61
+ },
56
62
  sendMessage: (message: AgentMessage) => {
57
63
  this._pendingMessages.push(message);
58
64
  },
@@ -75,6 +81,16 @@ export class ExtensionRunner {
75
81
  return undefined;
76
82
  }
77
83
 
84
+ getRegisteredProviders(): Map<string, LLMProviderFactory> {
85
+ const merged = new Map<string, LLMProviderFactory>();
86
+ for (const ext of this._extensions) {
87
+ for (const [name, factory] of ext.providers) {
88
+ merged.set(name, factory);
89
+ }
90
+ }
91
+ return merged;
92
+ }
93
+
78
94
  drainPendingMessages(): AgentMessage[] {
79
95
  const msgs = [...this._pendingMessages];
80
96
  this._pendingMessages = [];
@@ -3,7 +3,7 @@
3
3
  import type { TSchema } from "@sinclair/typebox";
4
4
  import type { AgentTool, AgentToolResult } from "../tools/types.js";
5
5
  import type { AgentMessage, ThinkingLevel } from "../types.js";
6
- import type { Message, ToolDefinition } from "../llm/types.js";
6
+ import type { Message, ToolDefinition, LLMProviderFactory } from "../llm/types.js";
7
7
 
8
8
  // --- Extension factory ---
9
9
 
@@ -15,6 +15,7 @@ export interface ExtensionAPI {
15
15
  on<E extends ExtensionEventType>(event: E, handler: ExtensionHandler<E>): void;
16
16
  registerTool<TParams extends TSchema>(tool: AgentTool<TParams>): void;
17
17
  registerCommand(name: string, handler: CommandHandler): void;
18
+ registerProvider(name: string, factory: LLMProviderFactory): void;
18
19
  sendMessage(message: AgentMessage): void;
19
20
  }
20
21
 
package/src/index.ts CHANGED
@@ -78,6 +78,12 @@ export function createAgent(config: CreateAgentConfig): Agent {
78
78
  export { Agent } from "./core/agent.js";
79
79
  export { ApprovalImpl } from "./approval/approval.js";
80
80
  export { registerProvider, resolveProvider } from "./llm/provider.js";
81
+ export { AnthropicProvider } from "./providers/anthropic.js";
82
+ export { OpenAIProvider } from "./providers/openai.js";
83
+ export { GeminiProvider } from "./providers/google.js";
84
+ export { MiniMaxProvider } from "./providers/minimax.js";
85
+ export { KimiProvider } from "./providers/kimi.js";
86
+ export { VolcengineProvider } from "./providers/volcengine.js";
81
87
  export { executeToolCalls } from "./tools/executor.js";
82
88
  export { SessionManager } from "./session/session-manager.js";
83
89
  export { buildSessionContext } from "./session/session-context-builder.js";
@@ -130,6 +136,7 @@ export type {
130
136
  // LLM types
131
137
  export type {
132
138
  LLMProvider,
139
+ LLMProviderFactory,
133
140
  LLMRequestOptions,
134
141
  AssistantMessage,
135
142
  AssistantMessageEvent,
@@ -1,247 +1,29 @@
1
- // Anthropic LLM provider
2
-
3
- import Anthropic from "@anthropic-ai/sdk";
4
- import type {
5
- LLMProvider,
6
- LLMRequestOptions,
7
- AssistantMessageEvent,
8
- AssistantMessage,
9
- AssistantContentBlock,
10
- ToolDefinition,
11
- TokenUsage,
12
- ThinkingLevel,
13
- } from "./types.js";
14
-
15
- function mapThinkingLevel(level?: ThinkingLevel): number | undefined {
16
- if (!level || level === "off") return undefined;
17
- const budgets: Record<string, number> = {
18
- minimal: 1024,
19
- low: 4096,
20
- medium: 10240,
21
- high: 20480,
22
- xhigh: 40960,
23
- };
24
- return budgets[level];
25
- }
26
-
27
- export class AnthropicProvider implements LLMProvider {
28
- private client: Anthropic;
29
-
30
- constructor(apiKey?: string, baseUrl?: string) {
31
- this.client = new Anthropic({
32
- apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
33
- ...(baseUrl ? { baseURL: baseUrl } : {}),
34
- });
35
- }
36
-
37
- async *stream(options: LLMRequestOptions): AsyncIterable<AssistantMessageEvent> {
38
- const maxRetries = 3;
39
- let lastError: Error | null = null;
40
-
41
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
42
- if (attempt > 0) {
43
- const delay = Math.min(1000 * Math.pow(2, attempt - 1), 4000);
44
- await new Promise((r) => setTimeout(r, delay));
45
- }
46
-
47
- try {
48
- // On last attempt or first attempt, stream directly for real-time output.
49
- // On intermediate retries, errors will be caught and retried.
50
- yield* this._streamOnce(options);
51
- return;
52
- } catch (err) {
53
- lastError = err instanceof Error ? err : new Error(String(err));
54
-
55
- // Only retry on transient errors, and only if we haven't yielded
56
- // any events yet (connection-level failures before streaming starts)
57
- const isRetryable = lastError.message.includes("rate_limit")
58
- || lastError.message.includes("overloaded")
59
- || lastError.message.includes("529")
60
- || lastError.message.includes("500")
61
- || lastError.message.includes("ECONNRESET");
62
-
63
- if (!isRetryable || attempt === maxRetries) {
64
- yield { type: "error", error: lastError };
65
- return;
66
- }
67
- // Note: if _streamOnce yielded partial events before throwing,
68
- // those events are already consumed by the caller. Retries only
69
- // help with pre-stream connection failures, not mid-stream breaks.
70
- }
71
- }
72
- }
73
-
74
- private async *_streamOnce(options: LLMRequestOptions): AsyncIterable<AssistantMessageEvent> {
75
- const { model, systemPrompt, messages, tools, thinkingLevel, maxTokens, signal } = options;
76
-
77
- const thinkingBudget = mapThinkingLevel(thinkingLevel);
78
-
79
- const params: Anthropic.MessageCreateParamsStreaming = {
80
- model,
81
- max_tokens: maxTokens ?? 8192,
82
- system: systemPrompt,
83
- messages: messages.map((m) => {
84
- if (m.role === "user") {
85
- return { role: "user" as const, content: typeof m.content === "string" ? m.content : m.content.map(mapContentBlock) };
86
- }
87
- if (m.role === "assistant") {
88
- return { role: "assistant" as const, content: m.content.map(mapAssistantBlock) };
89
- }
90
- // tool_result
91
- return {
92
- role: "user" as const,
93
- content: [{
94
- type: "tool_result" as const,
95
- tool_use_id: m.toolCallId,
96
- content: typeof m.content === "string" ? m.content : m.content.map(mapToolResultContent),
97
- ...(m.isError ? { is_error: true as const } : {}),
98
- }],
99
- };
100
- }),
101
- stream: true,
102
- ...(tools?.length ? {
103
- tools: tools.map((t) => ({
104
- name: t.name,
105
- description: t.description,
106
- input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
107
- })),
108
- } : {}),
109
- ...(thinkingBudget ? {
110
- thinking: { type: "enabled" as const, budget_tokens: thinkingBudget },
111
- } : {}),
112
- };
113
-
114
- const contentBlocks: AssistantContentBlock[] = [];
115
- const toolInputBuffers = new Map<number, string>();
116
- let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
117
-
118
- const stream = this.client.messages.stream(params, { signal });
119
-
120
- try {
121
- for await (const event of stream) {
122
- if (event.type === "content_block_start") {
123
- const block = event.content_block;
124
- if (block.type === "text") {
125
- contentBlocks.push({ type: "text", text: "" });
126
- } else if (block.type === "tool_use") {
127
- contentBlocks.push({ type: "tool_call", id: block.id, name: block.name, input: {} });
128
- toolInputBuffers.set(event.index, "");
129
- yield { type: "tool_call_start", id: block.id, name: block.name };
130
- } else if (block.type === "thinking") {
131
- contentBlocks.push({ type: "thinking", thinking: "" });
132
- }
133
- } else if (event.type === "content_block_delta") {
134
- const delta = event.delta;
135
- if (delta.type === "text_delta") {
136
- const block = contentBlocks[event.index];
137
- if (block && block.type === "text") {
138
- block.text += delta.text;
139
- }
140
- yield { type: "text", text: delta.text };
141
- } else if (delta.type === "input_json_delta") {
142
- const buf = (toolInputBuffers.get(event.index) ?? "") + delta.partial_json;
143
- toolInputBuffers.set(event.index, buf);
144
- const block = contentBlocks[event.index];
145
- if (block && block.type === "tool_call") {
146
- yield { type: "tool_call_delta", id: block.id, input: delta.partial_json };
147
- }
148
- } else if (delta.type === "thinking_delta") {
149
- const block = contentBlocks[event.index];
150
- if (block && block.type === "thinking") {
151
- block.thinking += delta.thinking;
152
- }
153
- yield { type: "thinking", thinking: delta.thinking };
154
- }
155
- } else if (event.type === "content_block_stop") {
156
- // Parse accumulated tool input JSON
157
- const block = contentBlocks[event.index];
158
- if (block && block.type === "tool_call") {
159
- const buf = toolInputBuffers.get(event.index) ?? "{}";
160
- try {
161
- block.input = JSON.parse(buf || "{}");
162
- } catch {
163
- block.input = {};
164
- }
165
- toolInputBuffers.delete(event.index);
166
- }
167
- } else if (event.type === "message_delta") {
168
- if (event.usage) {
169
- usage = {
170
- inputTokens: usage.inputTokens,
171
- outputTokens: event.usage.output_tokens,
172
- totalTokens: usage.inputTokens + event.usage.output_tokens,
173
- };
174
- }
175
- } else if (event.type === "message_start") {
176
- if (event.message.usage) {
177
- usage = {
178
- inputTokens: event.message.usage.input_tokens,
179
- outputTokens: event.message.usage.output_tokens,
180
- totalTokens: event.message.usage.input_tokens + event.message.usage.output_tokens,
181
- cacheReadTokens: (event.message.usage as any).cache_read_input_tokens,
182
- cacheWriteTokens: (event.message.usage as any).cache_creation_input_tokens,
183
- };
184
- }
185
- }
186
- }
187
- } catch (err) {
188
- throw err; // Let retry wrapper handle it
189
- }
190
-
191
- const message: AssistantMessage = { role: "assistant", content: contentBlocks };
192
- yield { type: "done", message, usage };
193
- }
194
- }
195
-
196
- function mapToolResultContent(block: { type: string; [key: string]: any }): Anthropic.TextBlockParam | Anthropic.ImageBlockParam {
197
- if (block.type === "image" && block.source?.type === "base64") {
198
- return {
199
- type: "image",
200
- source: { type: "base64", media_type: block.source.mediaType, data: block.source.data },
201
- };
202
- }
203
- if (block.type === "image" && block.source?.type === "url") {
204
- return { type: "image", source: { type: "url", url: block.source.url } };
205
- }
206
- return { type: "text", text: block.type === "text" ? block.text : JSON.stringify(block) };
207
- }
208
-
209
- function mapContentBlock(block: { type: string; [key: string]: any }): Anthropic.ContentBlockParam {
210
- if (block.type === "text") {
211
- return { type: "text", text: block.text };
212
- }
213
- if (block.type === "image") {
214
- if (block.source.type === "base64") {
215
- return {
216
- type: "image",
217
- source: { type: "base64", media_type: block.source.mediaType, data: block.source.data },
218
- };
219
- }
220
- return { type: "image", source: { type: "url", url: block.source.url } };
221
- }
222
- return { type: "text", text: JSON.stringify(block) };
223
- }
224
-
225
- function mapAssistantBlock(block: AssistantContentBlock): Anthropic.ContentBlockParam {
226
- if (block.type === "text") {
227
- return { type: "text", text: block.text };
228
- }
229
- if (block.type === "tool_call") {
230
- return { type: "tool_use", id: block.id, name: block.name, input: block.input };
231
- }
232
- if (block.type === "thinking") {
233
- return { type: "thinking", thinking: block.thinking, signature: "" };
234
- }
235
- return { type: "text", text: JSON.stringify(block) };
236
- }
237
-
238
- // --- Provider registry ---
239
-
240
- const providers = new Map<string, (config: { apiKey?: string; baseUrl?: string }) => LLMProvider>();
241
-
242
- providers.set("anthropic", (config) => new AnthropicProvider(config.apiKey, config.baseUrl));
243
-
244
- export function registerProvider(name: string, factory: (config: { apiKey?: string; baseUrl?: string }) => LLMProvider): void {
1
+ // LLM provider registry
2
+
3
+ import { AnthropicProvider } from "../providers/anthropic.js";
4
+ import { OpenAIProvider } from "../providers/openai.js";
5
+ import { GeminiProvider } from "../providers/google.js";
6
+ import { MiniMaxProvider } from "../providers/minimax.js";
7
+ import { KimiProvider } from "../providers/kimi.js";
8
+ import { VolcengineProvider } from "../providers/volcengine.js";
9
+ import type { LLMProvider, LLMProviderFactory } from "./types.js";
10
+
11
+ const providers = new Map<string, LLMProviderFactory>();
12
+
13
+ // Built-in providers
14
+ providers.set("anthropic", (c) => new AnthropicProvider(c.apiKey, c.baseUrl));
15
+ providers.set("openai", (c) => new OpenAIProvider(c.apiKey, c.baseUrl));
16
+ providers.set("google", (c) => new GeminiProvider(c.apiKey, c.baseUrl));
17
+ providers.set("minimax", (c) => new MiniMaxProvider(c.apiKey, c.baseUrl));
18
+ providers.set("kimi", (c) => new KimiProvider(c.apiKey, c.baseUrl));
19
+ providers.set("volcengine", (c) => new VolcengineProvider(c.apiKey, c.baseUrl));
20
+
21
+ // Protocol-compatible proxies — user provides baseUrl to connect any compatible service
22
+ providers.set("openai-compatible", (c) => new OpenAIProvider(c.apiKey, c.baseUrl));
23
+ providers.set("anthropic-compatible", (c) => new AnthropicProvider(c.apiKey, c.baseUrl));
24
+ providers.set("gemini-compatible", (c) => new GeminiProvider(c.apiKey, c.baseUrl));
25
+
26
+ export function registerProvider(name: string, factory: LLMProviderFactory): void {
245
27
  providers.set(name, factory);
246
28
  }
247
29
 
package/src/llm/types.ts CHANGED
@@ -144,3 +144,5 @@ export interface LLMRequestOptions {
144
144
  maxTokens?: number;
145
145
  signal?: AbortSignal;
146
146
  }
147
+
148
+ export type LLMProviderFactory = (config: { apiKey?: string; baseUrl?: string }) => LLMProvider;
@@ -0,0 +1,185 @@
1
+ // Anthropic LLM provider
2
+
3
+ import Anthropic from "@anthropic-ai/sdk";
4
+ import type {
5
+ LLMProvider,
6
+ LLMRequestOptions,
7
+ AssistantMessageEvent,
8
+ AssistantMessage,
9
+ AssistantContentBlock,
10
+ TokenUsage,
11
+ } from "../llm/types.js";
12
+ import { withRetry, RETRYABLE_PATTERNS, mapThinkingBudget } from "./shared.js";
13
+
14
+ export class AnthropicProvider implements LLMProvider {
15
+ private client: Anthropic;
16
+
17
+ constructor(apiKey?: string, baseUrl?: string) {
18
+ this.client = new Anthropic({
19
+ apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
20
+ ...(baseUrl ? { baseURL: baseUrl } : {}),
21
+ });
22
+ }
23
+
24
+ async *stream(options: LLMRequestOptions): AsyncIterable<AssistantMessageEvent> {
25
+ yield* withRetry(() => this._streamOnce(options), RETRYABLE_PATTERNS.anthropic);
26
+ }
27
+
28
+ private async *_streamOnce(options: LLMRequestOptions): AsyncIterable<AssistantMessageEvent> {
29
+ const { model, systemPrompt, messages, tools, thinkingLevel, maxTokens, signal } = options;
30
+
31
+ const thinkingBudget = mapThinkingBudget(thinkingLevel);
32
+
33
+ const params: Anthropic.MessageCreateParamsStreaming = {
34
+ model,
35
+ max_tokens: maxTokens ?? 8192,
36
+ system: systemPrompt,
37
+ messages: messages.map((m) => {
38
+ if (m.role === "user") {
39
+ return { role: "user" as const, content: typeof m.content === "string" ? m.content : m.content.map(mapContentBlock) };
40
+ }
41
+ if (m.role === "assistant") {
42
+ return { role: "assistant" as const, content: m.content.map(mapAssistantBlock) };
43
+ }
44
+ // tool_result
45
+ return {
46
+ role: "user" as const,
47
+ content: [{
48
+ type: "tool_result" as const,
49
+ tool_use_id: m.toolCallId,
50
+ content: typeof m.content === "string" ? m.content : m.content.map(mapToolResultContent),
51
+ ...(m.isError ? { is_error: true as const } : {}),
52
+ }],
53
+ };
54
+ }),
55
+ stream: true,
56
+ ...(tools?.length ? {
57
+ tools: tools.map((t) => ({
58
+ name: t.name,
59
+ description: t.description,
60
+ input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
61
+ })),
62
+ } : {}),
63
+ ...(thinkingBudget ? {
64
+ thinking: { type: "enabled" as const, budget_tokens: thinkingBudget },
65
+ } : {}),
66
+ };
67
+
68
+ const contentBlocks: AssistantContentBlock[] = [];
69
+ const toolInputBuffers = new Map<number, string>();
70
+ let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
71
+
72
+ const stream = this.client.messages.stream(params, { signal });
73
+
74
+ for await (const event of stream) {
75
+ if (event.type === "content_block_start") {
76
+ const block = event.content_block;
77
+ if (block.type === "text") {
78
+ contentBlocks.push({ type: "text", text: "" });
79
+ } else if (block.type === "tool_use") {
80
+ contentBlocks.push({ type: "tool_call", id: block.id, name: block.name, input: {} });
81
+ toolInputBuffers.set(event.index, "");
82
+ yield { type: "tool_call_start", id: block.id, name: block.name };
83
+ } else if (block.type === "thinking") {
84
+ contentBlocks.push({ type: "thinking", thinking: "" });
85
+ }
86
+ } else if (event.type === "content_block_delta") {
87
+ const delta = event.delta;
88
+ if (delta.type === "text_delta") {
89
+ const block = contentBlocks[event.index];
90
+ if (block && block.type === "text") {
91
+ block.text += delta.text;
92
+ }
93
+ yield { type: "text", text: delta.text };
94
+ } else if (delta.type === "input_json_delta") {
95
+ const buf = (toolInputBuffers.get(event.index) ?? "") + delta.partial_json;
96
+ toolInputBuffers.set(event.index, buf);
97
+ const block = contentBlocks[event.index];
98
+ if (block && block.type === "tool_call") {
99
+ yield { type: "tool_call_delta", id: block.id, input: delta.partial_json };
100
+ }
101
+ } else if (delta.type === "thinking_delta") {
102
+ const block = contentBlocks[event.index];
103
+ if (block && block.type === "thinking") {
104
+ block.thinking += delta.thinking;
105
+ }
106
+ yield { type: "thinking", thinking: delta.thinking };
107
+ }
108
+ } else if (event.type === "content_block_stop") {
109
+ const block = contentBlocks[event.index];
110
+ if (block && block.type === "tool_call") {
111
+ const buf = toolInputBuffers.get(event.index) ?? "{}";
112
+ try {
113
+ block.input = JSON.parse(buf || "{}");
114
+ } catch {
115
+ block.input = {};
116
+ }
117
+ toolInputBuffers.delete(event.index);
118
+ }
119
+ } else if (event.type === "message_delta") {
120
+ if (event.usage) {
121
+ usage = {
122
+ inputTokens: usage.inputTokens,
123
+ outputTokens: event.usage.output_tokens,
124
+ totalTokens: usage.inputTokens + event.usage.output_tokens,
125
+ };
126
+ }
127
+ } else if (event.type === "message_start") {
128
+ if (event.message.usage) {
129
+ usage = {
130
+ inputTokens: event.message.usage.input_tokens,
131
+ outputTokens: event.message.usage.output_tokens,
132
+ totalTokens: event.message.usage.input_tokens + event.message.usage.output_tokens,
133
+ cacheReadTokens: (event.message.usage as any).cache_read_input_tokens,
134
+ cacheWriteTokens: (event.message.usage as any).cache_creation_input_tokens,
135
+ };
136
+ }
137
+ }
138
+ }
139
+
140
+ const message: AssistantMessage = { role: "assistant", content: contentBlocks };
141
+ yield { type: "done", message, usage };
142
+ }
143
+ }
144
+
145
+ function mapToolResultContent(block: { type: string; [key: string]: any }): Anthropic.TextBlockParam | Anthropic.ImageBlockParam {
146
+ if (block.type === "image" && block.source?.type === "base64") {
147
+ return {
148
+ type: "image",
149
+ source: { type: "base64", media_type: block.source.mediaType, data: block.source.data },
150
+ };
151
+ }
152
+ if (block.type === "image" && block.source?.type === "url") {
153
+ return { type: "image", source: { type: "url", url: block.source.url } };
154
+ }
155
+ return { type: "text", text: block.type === "text" ? block.text : JSON.stringify(block) };
156
+ }
157
+
158
+ function mapContentBlock(block: { type: string; [key: string]: any }): Anthropic.ContentBlockParam {
159
+ if (block.type === "text") {
160
+ return { type: "text", text: block.text };
161
+ }
162
+ if (block.type === "image") {
163
+ if (block.source.type === "base64") {
164
+ return {
165
+ type: "image",
166
+ source: { type: "base64", media_type: block.source.mediaType, data: block.source.data },
167
+ };
168
+ }
169
+ return { type: "image", source: { type: "url", url: block.source.url } };
170
+ }
171
+ return { type: "text", text: JSON.stringify(block) };
172
+ }
173
+
174
+ function mapAssistantBlock(block: AssistantContentBlock): Anthropic.ContentBlockParam {
175
+ if (block.type === "text") {
176
+ return { type: "text", text: block.text };
177
+ }
178
+ if (block.type === "tool_call") {
179
+ return { type: "tool_use", id: block.id, name: block.name, input: block.input };
180
+ }
181
+ if (block.type === "thinking") {
182
+ return { type: "thinking", thinking: block.thinking, signature: "" };
183
+ }
184
+ return { type: "text", text: JSON.stringify(block) };
185
+ }