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,171 @@
|
|
|
1
|
+
// Google Gemini LLM provider
|
|
2
|
+
|
|
3
|
+
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
4
|
+
import type { Content, Part, FunctionDeclaration } from "@google/generative-ai";
|
|
5
|
+
import type {
|
|
6
|
+
LLMProvider,
|
|
7
|
+
LLMRequestOptions,
|
|
8
|
+
AssistantMessageEvent,
|
|
9
|
+
AssistantMessage,
|
|
10
|
+
AssistantContentBlock,
|
|
11
|
+
TokenUsage,
|
|
12
|
+
Message,
|
|
13
|
+
} from "../llm/types.js";
|
|
14
|
+
import { generateId } from "../utils/id.js";
|
|
15
|
+
import { withRetry, RETRYABLE_PATTERNS, mapThinkingBudget } from "./shared.js";
|
|
16
|
+
|
|
17
|
+
export class GeminiProvider implements LLMProvider {
|
|
18
|
+
private client: GoogleGenerativeAI;
|
|
19
|
+
private baseUrl?: string;
|
|
20
|
+
|
|
21
|
+
constructor(apiKey?: string, baseUrl?: string) {
|
|
22
|
+
this.client = new GoogleGenerativeAI(apiKey || process.env.GOOGLE_API_KEY || "");
|
|
23
|
+
this.baseUrl = baseUrl;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async *stream(options: LLMRequestOptions): AsyncIterable<AssistantMessageEvent> {
|
|
27
|
+
yield* withRetry(() => this._streamOnce(options), RETRYABLE_PATTERNS.google);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async *_streamOnce(options: LLMRequestOptions): AsyncIterable<AssistantMessageEvent> {
|
|
31
|
+
const { model: modelId, systemPrompt, messages, tools, thinkingLevel, maxTokens, signal } = options;
|
|
32
|
+
|
|
33
|
+
const thinkingBudget = mapThinkingBudget(thinkingLevel);
|
|
34
|
+
|
|
35
|
+
const genModel = this.client.getGenerativeModel(
|
|
36
|
+
{
|
|
37
|
+
model: modelId,
|
|
38
|
+
systemInstruction: systemPrompt,
|
|
39
|
+
...(tools?.length ? {
|
|
40
|
+
tools: [{
|
|
41
|
+
functionDeclarations: tools.map((t): FunctionDeclaration => ({
|
|
42
|
+
name: t.name,
|
|
43
|
+
description: t.description,
|
|
44
|
+
parameters: t.inputSchema as any,
|
|
45
|
+
})),
|
|
46
|
+
}],
|
|
47
|
+
} : {}),
|
|
48
|
+
generationConfig: {
|
|
49
|
+
maxOutputTokens: maxTokens ?? 8192,
|
|
50
|
+
...(thinkingBudget ? {
|
|
51
|
+
thinkingConfig: { thinkingBudget },
|
|
52
|
+
} as any : {}),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
this.baseUrl ? { baseUrl: this.baseUrl } : undefined,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const contents = mapMessages(messages);
|
|
59
|
+
const result = await genModel.generateContentStream({ contents }, { signal });
|
|
60
|
+
|
|
61
|
+
const contentBlocks: AssistantContentBlock[] = [];
|
|
62
|
+
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
63
|
+
|
|
64
|
+
for await (const chunk of result.stream) {
|
|
65
|
+
const text = chunk.text?.();
|
|
66
|
+
if (text) {
|
|
67
|
+
if (contentBlocks.length === 0 || contentBlocks[contentBlocks.length - 1].type !== "text") {
|
|
68
|
+
contentBlocks.push({ type: "text", text: "" });
|
|
69
|
+
}
|
|
70
|
+
const block = contentBlocks[contentBlocks.length - 1];
|
|
71
|
+
if (block.type === "text") {
|
|
72
|
+
block.text += text;
|
|
73
|
+
}
|
|
74
|
+
yield { type: "text", text };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Function calls (Gemini returns them complete, not streamed)
|
|
78
|
+
const fnCalls = chunk.functionCalls?.();
|
|
79
|
+
if (fnCalls) {
|
|
80
|
+
for (const fc of fnCalls) {
|
|
81
|
+
const id = generateId();
|
|
82
|
+
const input = (fc.args ?? {}) as Record<string, unknown>;
|
|
83
|
+
contentBlocks.push({ type: "tool_call", id, name: fc.name, input });
|
|
84
|
+
yield { type: "tool_call_start", id, name: fc.name };
|
|
85
|
+
yield { type: "tool_call_delta", id, input: JSON.stringify(input) };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Usage
|
|
90
|
+
if (chunk.usageMetadata) {
|
|
91
|
+
usage = {
|
|
92
|
+
inputTokens: chunk.usageMetadata.promptTokenCount ?? 0,
|
|
93
|
+
outputTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
|
|
94
|
+
totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const message: AssistantMessage = { role: "assistant", content: contentBlocks };
|
|
100
|
+
yield { type: "done", message, usage };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function mapMessages(messages: Message[]): Content[] {
|
|
105
|
+
const contents: Content[] = [];
|
|
106
|
+
|
|
107
|
+
for (const m of messages) {
|
|
108
|
+
if (m.role === "user") {
|
|
109
|
+
const parts: Part[] = [];
|
|
110
|
+
if (typeof m.content === "string") {
|
|
111
|
+
parts.push({ text: m.content });
|
|
112
|
+
} else {
|
|
113
|
+
for (const block of m.content) {
|
|
114
|
+
if (block.type === "text") {
|
|
115
|
+
parts.push({ text: block.text });
|
|
116
|
+
} else if (block.type === "image" && block.source.type === "base64") {
|
|
117
|
+
parts.push({
|
|
118
|
+
inlineData: { mimeType: block.source.mediaType, data: block.source.data },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
contents.push({ role: "user", parts });
|
|
124
|
+
} else if (m.role === "assistant") {
|
|
125
|
+
const parts: Part[] = [];
|
|
126
|
+
for (const block of m.content) {
|
|
127
|
+
if (block.type === "text") {
|
|
128
|
+
parts.push({ text: block.text });
|
|
129
|
+
} else if (block.type === "tool_call") {
|
|
130
|
+
parts.push({
|
|
131
|
+
functionCall: { name: block.name, args: block.input },
|
|
132
|
+
});
|
|
133
|
+
} else if (block.type === "thinking") {
|
|
134
|
+
parts.push({ text: block.thinking });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
contents.push({ role: "model", parts });
|
|
138
|
+
} else {
|
|
139
|
+
// tool_result → functionResponse
|
|
140
|
+
// Need to find the tool name from previous assistant messages
|
|
141
|
+
const toolName = findToolName(messages, m.toolCallId) ?? "unknown";
|
|
142
|
+
const responseContent = typeof m.content === "string"
|
|
143
|
+
? m.content
|
|
144
|
+
: m.content.map((b) => b.type === "text" ? b.text : JSON.stringify(b)).join("\n");
|
|
145
|
+
contents.push({
|
|
146
|
+
role: "user",
|
|
147
|
+
parts: [{
|
|
148
|
+
functionResponse: {
|
|
149
|
+
name: toolName,
|
|
150
|
+
response: { content: responseContent },
|
|
151
|
+
},
|
|
152
|
+
}],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return contents;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function findToolName(messages: Message[], toolCallId: string): string | undefined {
|
|
161
|
+
for (const m of messages) {
|
|
162
|
+
if (m.role === "assistant") {
|
|
163
|
+
for (const block of m.content) {
|
|
164
|
+
if (block.type === "tool_call" && block.id === toolCallId) {
|
|
165
|
+
return block.name;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AnthropicProvider } from "./anthropic.js";
|
|
2
|
+
export { OpenAIProvider } from "./openai.js";
|
|
3
|
+
export { GeminiProvider } from "./google.js";
|
|
4
|
+
export { MiniMaxProvider } from "./minimax.js";
|
|
5
|
+
export { KimiProvider } from "./kimi.js";
|
|
6
|
+
export { VolcengineProvider } from "./volcengine.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Kimi LLM provider (Anthropic Messages API compatible)
|
|
2
|
+
|
|
3
|
+
import { AnthropicProvider } from "./anthropic.js";
|
|
4
|
+
|
|
5
|
+
export class KimiProvider extends AnthropicProvider {
|
|
6
|
+
constructor(apiKey?: string, baseUrl?: string) {
|
|
7
|
+
super(
|
|
8
|
+
apiKey ?? process.env.KIMI_API_KEY,
|
|
9
|
+
baseUrl ?? "https://api.kimi.com/coding",
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// MiniMax LLM provider (Anthropic Messages API compatible)
|
|
2
|
+
|
|
3
|
+
import { AnthropicProvider } from "./anthropic.js";
|
|
4
|
+
|
|
5
|
+
export class MiniMaxProvider extends AnthropicProvider {
|
|
6
|
+
constructor(apiKey?: string, baseUrl?: string) {
|
|
7
|
+
super(
|
|
8
|
+
apiKey ?? process.env.MINIMAX_API_KEY,
|
|
9
|
+
baseUrl ?? "https://api.minimaxi.com/anthropic",
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// OpenAI LLM provider
|
|
2
|
+
|
|
3
|
+
import OpenAI from "openai";
|
|
4
|
+
import type {
|
|
5
|
+
LLMProvider,
|
|
6
|
+
LLMRequestOptions,
|
|
7
|
+
AssistantMessageEvent,
|
|
8
|
+
AssistantMessage,
|
|
9
|
+
AssistantContentBlock,
|
|
10
|
+
TokenUsage,
|
|
11
|
+
ThinkingLevel,
|
|
12
|
+
Message,
|
|
13
|
+
} from "../llm/types.js";
|
|
14
|
+
import { withRetry, RETRYABLE_PATTERNS } from "./shared.js";
|
|
15
|
+
|
|
16
|
+
function mapReasoningEffort(level?: ThinkingLevel): "low" | "medium" | "high" | undefined {
|
|
17
|
+
if (!level || level === "off") return undefined;
|
|
18
|
+
if (level === "minimal" || level === "low") return "low";
|
|
19
|
+
if (level === "medium") return "medium";
|
|
20
|
+
return "high";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class OpenAIProvider implements LLMProvider {
|
|
24
|
+
private client: OpenAI;
|
|
25
|
+
|
|
26
|
+
constructor(apiKey?: string, baseUrl?: string) {
|
|
27
|
+
this.client = new OpenAI({
|
|
28
|
+
apiKey: apiKey || process.env.OPENAI_API_KEY,
|
|
29
|
+
...(baseUrl ? { baseURL: baseUrl } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async *stream(options: LLMRequestOptions): AsyncIterable<AssistantMessageEvent> {
|
|
34
|
+
yield* withRetry(() => this._streamOnce(options), RETRYABLE_PATTERNS.openai);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async *_streamOnce(options: LLMRequestOptions): AsyncIterable<AssistantMessageEvent> {
|
|
38
|
+
const { model, systemPrompt, messages, tools, thinkingLevel, maxTokens, signal } = options;
|
|
39
|
+
|
|
40
|
+
const reasoningEffort = mapReasoningEffort(thinkingLevel);
|
|
41
|
+
|
|
42
|
+
const params: OpenAI.ChatCompletionCreateParamsStreaming = {
|
|
43
|
+
model,
|
|
44
|
+
messages: [
|
|
45
|
+
{ role: "system" as const, content: systemPrompt },
|
|
46
|
+
...messages.map((m) => mapMessage(m)),
|
|
47
|
+
],
|
|
48
|
+
stream: true,
|
|
49
|
+
stream_options: { include_usage: true },
|
|
50
|
+
max_tokens: maxTokens ?? 8192,
|
|
51
|
+
...(tools?.length ? {
|
|
52
|
+
tools: tools.map((t) => ({
|
|
53
|
+
type: "function" as const,
|
|
54
|
+
function: { name: t.name, description: t.description, parameters: t.inputSchema },
|
|
55
|
+
})),
|
|
56
|
+
} : {}),
|
|
57
|
+
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const contentBlocks: AssistantContentBlock[] = [];
|
|
61
|
+
const toolCalls = new Map<number, { id: string; name: string; args: string }>();
|
|
62
|
+
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
63
|
+
|
|
64
|
+
const stream = await this.client.chat.completions.create(params, { signal });
|
|
65
|
+
|
|
66
|
+
for await (const chunk of stream) {
|
|
67
|
+
const choice = chunk.choices?.[0];
|
|
68
|
+
|
|
69
|
+
if (choice?.delta) {
|
|
70
|
+
const delta = choice.delta;
|
|
71
|
+
|
|
72
|
+
// Text
|
|
73
|
+
if (delta.content) {
|
|
74
|
+
if (contentBlocks.length === 0 || contentBlocks[contentBlocks.length - 1].type !== "text") {
|
|
75
|
+
contentBlocks.push({ type: "text", text: "" });
|
|
76
|
+
}
|
|
77
|
+
const block = contentBlocks[contentBlocks.length - 1];
|
|
78
|
+
if (block.type === "text") {
|
|
79
|
+
block.text += delta.content;
|
|
80
|
+
}
|
|
81
|
+
yield { type: "text", text: delta.content };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Reasoning/thinking content (o1/o3 series)
|
|
85
|
+
if ((delta as any).reasoning_content) {
|
|
86
|
+
const thinking = (delta as any).reasoning_content as string;
|
|
87
|
+
if (contentBlocks.length === 0 || contentBlocks[contentBlocks.length - 1].type !== "thinking") {
|
|
88
|
+
contentBlocks.push({ type: "thinking", thinking: "" });
|
|
89
|
+
}
|
|
90
|
+
const block = contentBlocks[contentBlocks.length - 1];
|
|
91
|
+
if (block.type === "thinking") {
|
|
92
|
+
block.thinking += thinking;
|
|
93
|
+
}
|
|
94
|
+
yield { type: "thinking", thinking };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Tool calls
|
|
98
|
+
if (delta.tool_calls) {
|
|
99
|
+
for (const tc of delta.tool_calls) {
|
|
100
|
+
const idx = tc.index;
|
|
101
|
+
if (!toolCalls.has(idx)) {
|
|
102
|
+
// New tool call
|
|
103
|
+
const id = tc.id ?? `call_${idx}`;
|
|
104
|
+
const name = tc.function?.name ?? "";
|
|
105
|
+
toolCalls.set(idx, { id, name, args: "" });
|
|
106
|
+
contentBlocks.push({ type: "tool_call", id, name, input: {} });
|
|
107
|
+
yield { type: "tool_call_start", id, name };
|
|
108
|
+
}
|
|
109
|
+
if (tc.function?.arguments) {
|
|
110
|
+
const entry = toolCalls.get(idx)!;
|
|
111
|
+
entry.args += tc.function.arguments;
|
|
112
|
+
yield { type: "tool_call_delta", id: entry.id, input: tc.function.arguments };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Usage info (comes in the final chunk with stream_options.include_usage)
|
|
119
|
+
if (chunk.usage) {
|
|
120
|
+
usage = {
|
|
121
|
+
inputTokens: chunk.usage.prompt_tokens,
|
|
122
|
+
outputTokens: chunk.usage.completion_tokens,
|
|
123
|
+
totalTokens: chunk.usage.total_tokens,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Finalize tool call inputs
|
|
129
|
+
for (const [, entry] of toolCalls) {
|
|
130
|
+
const block = contentBlocks.find(
|
|
131
|
+
(b) => b.type === "tool_call" && b.id === entry.id,
|
|
132
|
+
);
|
|
133
|
+
if (block && block.type === "tool_call") {
|
|
134
|
+
try {
|
|
135
|
+
block.input = JSON.parse(entry.args || "{}");
|
|
136
|
+
} catch {
|
|
137
|
+
block.input = {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const message: AssistantMessage = { role: "assistant", content: contentBlocks };
|
|
143
|
+
yield { type: "done", message, usage };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mapMessage(m: Message): OpenAI.ChatCompletionMessageParam {
|
|
148
|
+
if (m.role === "user") {
|
|
149
|
+
if (typeof m.content === "string") {
|
|
150
|
+
return { role: "user", content: m.content };
|
|
151
|
+
}
|
|
152
|
+
const parts: OpenAI.ChatCompletionContentPart[] = m.content.map((block) => {
|
|
153
|
+
if (block.type === "text") {
|
|
154
|
+
return { type: "text" as const, text: block.text };
|
|
155
|
+
}
|
|
156
|
+
if (block.type === "image") {
|
|
157
|
+
if (block.source.type === "url") {
|
|
158
|
+
return { type: "image_url" as const, image_url: { url: block.source.url } };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
type: "image_url" as const,
|
|
162
|
+
image_url: { url: `data:${block.source.mediaType};base64,${block.source.data}` },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { type: "text" as const, text: JSON.stringify(block) };
|
|
166
|
+
});
|
|
167
|
+
return { role: "user", content: parts };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (m.role === "assistant") {
|
|
171
|
+
let text = "";
|
|
172
|
+
const toolCalls: OpenAI.ChatCompletionMessageToolCall[] = [];
|
|
173
|
+
for (const b of m.content) {
|
|
174
|
+
if (b.type === "text") {
|
|
175
|
+
text += b.text;
|
|
176
|
+
} else if (b.type === "tool_call") {
|
|
177
|
+
toolCalls.push({
|
|
178
|
+
id: b.id,
|
|
179
|
+
type: "function" as const,
|
|
180
|
+
function: { name: b.name, arguments: JSON.stringify(b.input) },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
role: "assistant",
|
|
186
|
+
content: text || null,
|
|
187
|
+
...(toolCalls.length ? { tool_calls: toolCalls } : {}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// tool_result
|
|
192
|
+
return {
|
|
193
|
+
role: "tool",
|
|
194
|
+
tool_call_id: m.toolCallId,
|
|
195
|
+
content: typeof m.content === "string" ? m.content : m.content.map((b) => b.type === "text" ? b.text : JSON.stringify(b)).join("\n"),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Shared streaming utilities for LLM providers
|
|
2
|
+
|
|
3
|
+
import type { AssistantMessageEvent, ThinkingLevel } from "../llm/types.js";
|
|
4
|
+
|
|
5
|
+
// Retryable error patterns common across providers
|
|
6
|
+
const COMMON_RETRYABLE = ["429", "500", "503", "ECONNRESET"];
|
|
7
|
+
|
|
8
|
+
export const RETRYABLE_PATTERNS: Record<string, string[]> = {
|
|
9
|
+
anthropic: [...COMMON_RETRYABLE, "rate_limit", "overloaded", "529"],
|
|
10
|
+
openai: [...COMMON_RETRYABLE, "rate_limit"],
|
|
11
|
+
google: [...COMMON_RETRYABLE],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function isRetryableError(error: Error, patterns: string[]): boolean {
|
|
15
|
+
return patterns.some((p) => error.message.includes(p));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wraps a streaming generator with exponential backoff retry logic.
|
|
20
|
+
* Only retries on connection-level failures before streaming starts.
|
|
21
|
+
*/
|
|
22
|
+
export async function* withRetry(
|
|
23
|
+
streamOnce: () => AsyncIterable<AssistantMessageEvent>,
|
|
24
|
+
retryablePatterns: string[],
|
|
25
|
+
maxRetries = 3,
|
|
26
|
+
): AsyncIterable<AssistantMessageEvent> {
|
|
27
|
+
let lastError: Error | null = null;
|
|
28
|
+
|
|
29
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
30
|
+
if (attempt > 0) {
|
|
31
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 4000);
|
|
32
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
yield* streamOnce();
|
|
37
|
+
return;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
40
|
+
|
|
41
|
+
if (!isRetryableError(lastError, retryablePatterns) || attempt === maxRetries) {
|
|
42
|
+
yield { type: "error", error: lastError };
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Maps ThinkingLevel to token budget (shared by Anthropic and Gemini). */
|
|
50
|
+
export function mapThinkingBudget(level?: ThinkingLevel): number | undefined {
|
|
51
|
+
if (!level || level === "off") return undefined;
|
|
52
|
+
const budgets: Record<string, number> = {
|
|
53
|
+
minimal: 1024,
|
|
54
|
+
low: 4096,
|
|
55
|
+
medium: 10240,
|
|
56
|
+
high: 20480,
|
|
57
|
+
xhigh: 40960,
|
|
58
|
+
};
|
|
59
|
+
return budgets[level];
|
|
60
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Volcengine (Doubao) LLM provider (OpenAI Chat Completions API compatible)
|
|
2
|
+
|
|
3
|
+
import { OpenAIProvider } from "./openai.js";
|
|
4
|
+
|
|
5
|
+
export class VolcengineProvider extends OpenAIProvider {
|
|
6
|
+
constructor(apiKey?: string, baseUrl?: string) {
|
|
7
|
+
super(
|
|
8
|
+
apiKey ?? process.env.VOLCENGINE_API_KEY,
|
|
9
|
+
baseUrl ?? "https://ark.cn-beijing.volces.com/api/v3",
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|