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.
- package/dist/core/agent.js +14 -0
- package/dist/core/agent.js.map +1 -1
- package/dist/extensions/runner.d.ts +2 -0
- package/dist/extensions/runner.js +13 -0
- package/dist/extensions/runner.js.map +1 -1
- package/dist/extensions/types.d.ts +2 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/provider.d.ts +2 -11
- package/dist/llm/provider.js +18 -221
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/types.d.ts +4 -0
- package/dist/providers/anthropic.d.ts +7 -0
- package/dist/providers/anthropic.js +173 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/google.d.ts +8 -0
- package/dist/providers/google.js +148 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/index.d.ts +6 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/kimi.d.ts +4 -0
- package/dist/providers/kimi.js +8 -0
- package/dist/providers/kimi.js.map +1 -0
- package/dist/providers/minimax.d.ts +4 -0
- package/dist/providers/minimax.js +8 -0
- package/dist/providers/minimax.js.map +1 -0
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.js +170 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/shared.d.ts +10 -0
- package/dist/providers/shared.js +49 -0
- package/dist/providers/shared.js.map +1 -0
- package/dist/providers/volcengine.d.ts +4 -0
- package/dist/providers/volcengine.js +8 -0
- package/dist/providers/volcengine.js.map +1 -0
- package/package.json +4 -3
- package/src/core/agent.ts +14 -0
- package/src/extensions/runner.ts +16 -0
- package/src/extensions/types.ts +2 -1
- package/src/index.ts +7 -0
- package/src/llm/provider.ts +26 -244
- package/src/llm/types.ts +2 -0
- package/src/providers/anthropic.ts +185 -0
- package/src/providers/google.ts +171 -0
- package/src/providers/index.ts +6 -0
- package/src/providers/kimi.ts +12 -0
- package/src/providers/minimax.ts +12 -0
- package/src/providers/openai.ts +197 -0
- package/src/providers/shared.ts +60 -0
- 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,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.
|
|
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
|
-
"@
|
|
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
|
package/src/extensions/runner.ts
CHANGED
|
@@ -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 = [];
|
package/src/extensions/types.ts
CHANGED
|
@@ -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,
|
package/src/llm/provider.ts
CHANGED
|
@@ -1,247 +1,29 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
@@ -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
|
+
}
|