@deepstrike/sdk 0.1.7 → 0.1.9
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/README.md +19 -6
- package/dist/agent.js +4 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +10 -1
- package/dist/providers/anthropic.d.ts +11 -2
- package/dist/providers/anthropic.js +46 -10
- package/dist/providers/base.d.ts +3 -0
- package/dist/providers/base.js +79 -0
- package/dist/providers/catalog.d.ts +14 -0
- package/dist/providers/catalog.js +45 -0
- package/dist/providers/deepseek.d.ts +9 -0
- package/dist/providers/deepseek.js +64 -0
- package/dist/providers/gemini.d.ts +14 -0
- package/dist/providers/gemini.js +142 -0
- package/dist/providers/kimi.d.ts +7 -0
- package/dist/providers/kimi.js +8 -0
- package/dist/providers/minimax.d.ts +7 -0
- package/dist/providers/minimax.js +10 -0
- package/dist/providers/openai-chat.d.ts +27 -0
- package/dist/providers/openai-chat.js +41 -0
- package/dist/providers/openai-responses.d.ts +42 -0
- package/dist/providers/openai-responses.js +220 -0
- package/dist/providers/openai.d.ts +4 -36
- package/dist/providers/openai.js +12 -121
- package/dist/providers/profiles.d.ts +342 -0
- package/dist/providers/profiles.js +190 -0
- package/dist/providers/qwen.d.ts +18 -0
- package/dist/providers/qwen.js +103 -0
- package/dist/runtime/server.d.ts +7 -0
- package/dist/runtime/server.js +37 -0
- package/dist/types.d.ts +17 -2
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -33,9 +33,9 @@ The correct platform package is selected and installed automatically via `option
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
35
35
|
```typescript
|
|
36
|
-
import { Agent,
|
|
36
|
+
import { Agent, OpenAIResponsesProvider, tool } from "@deepstrike/sdk"
|
|
37
37
|
|
|
38
|
-
const provider = new
|
|
38
|
+
const provider = new OpenAIResponsesProvider(process.env.OPENAI_API_KEY!, "gpt-5-mini")
|
|
39
39
|
|
|
40
40
|
const add = tool("add", "Add two numbers.", {
|
|
41
41
|
type: "object",
|
|
@@ -67,16 +67,29 @@ for await (const event of agent.runStreaming("Summarize README.md")) {
|
|
|
67
67
|
|
|
68
68
|
| Class | Backend | Notes |
|
|
69
69
|
|-------|---------|-------|
|
|
70
|
-
| `
|
|
70
|
+
| `OpenAIChatProvider` | OpenAI Chat Completions API | SSE tool-call accumulation |
|
|
71
|
+
| `OpenAIProvider` | OpenAI Chat Completions API | Compatibility alias for `OpenAIChatProvider` |
|
|
72
|
+
| `OpenAIResponsesProvider` | OpenAI Responses API | Native `previous_response_id` continuation |
|
|
71
73
|
| `AnthropicProvider` | Anthropic API | Native SSE, `ThinkingDelta` support |
|
|
72
74
|
| `QwenProvider` | DashScope | `enable_thinking` via extensions |
|
|
73
|
-
| `DeepSeekProvider` | DeepSeek API |
|
|
74
|
-
| `MiniMaxProvider` | MiniMax API |
|
|
75
|
+
| `DeepSeekProvider` | DeepSeek API | V4 thinking controls + reasoning replay across tool turns |
|
|
76
|
+
| `MiniMaxProvider` | MiniMax API | Anthropic-compatible M2.7/M2.5 path |
|
|
75
77
|
| `OllamaProvider` | Local Ollama | `http://localhost:11434` default |
|
|
76
|
-
| `KimiProvider` | Moonshot API | |
|
|
78
|
+
| `KimiProvider` | Moonshot API | K2.6 default; K2.5 also supported |
|
|
77
79
|
|
|
78
80
|
All providers accept `RetryConfig` for exponential backoff and share a `CircuitBreaker`.
|
|
79
81
|
|
|
82
|
+
OpenAI can also be selected through the provider catalog:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { createProvider } from "@deepstrike/sdk"
|
|
86
|
+
|
|
87
|
+
const provider = createProvider({
|
|
88
|
+
model: "openai/gpt-5-mini",
|
|
89
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
80
93
|
---
|
|
81
94
|
|
|
82
95
|
## Agent options
|
package/dist/agent.js
CHANGED
|
@@ -62,6 +62,7 @@ export class Agent {
|
|
|
62
62
|
}
|
|
63
63
|
const kernel = getKernel();
|
|
64
64
|
const ext = { ...this.extensions, ...(extensions ?? {}) };
|
|
65
|
+
const providerState = this.provider.createRunState?.();
|
|
65
66
|
const sm = new kernel.LoopStateMachine({
|
|
66
67
|
maxTokens: this.options.maxTokens,
|
|
67
68
|
maxTurns: this.options.maxTurns ?? 25,
|
|
@@ -156,7 +157,7 @@ export class Agent {
|
|
|
156
157
|
const tools = (action.tools ?? []);
|
|
157
158
|
let turnTokens = 0;
|
|
158
159
|
try {
|
|
159
|
-
for await (const evt of this.provider.stream(messages, tools, Object.keys(ext).length ? ext : undefined)) {
|
|
160
|
+
for await (const evt of this.provider.stream(messages, tools, Object.keys(ext).length ? ext : undefined, providerState)) {
|
|
160
161
|
if (evt.type === "usage") {
|
|
161
162
|
turnTokens = evt.totalTokens;
|
|
162
163
|
continue;
|
|
@@ -330,7 +331,8 @@ export class Agent {
|
|
|
330
331
|
throw new Error(`unexpected action after feedTrigger: ${action1.kind}`);
|
|
331
332
|
}
|
|
332
333
|
let synthesisText = "";
|
|
333
|
-
|
|
334
|
+
const providerState = this.provider.createRunState?.();
|
|
335
|
+
for await (const evt of this.provider.stream((action1.messages ?? []), [], undefined, providerState)) {
|
|
334
336
|
if (evt.type === "text_delta")
|
|
335
337
|
synthesisText += evt.delta;
|
|
336
338
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
export { Agent } from "./agent.js";
|
|
2
2
|
export type { AgentOptions } from "./agent.js";
|
|
3
3
|
export { AnthropicProvider } from "./providers/anthropic.js";
|
|
4
|
-
export {
|
|
4
|
+
export { OpenAIChatProvider, OpenAIProvider } from "./providers/openai.js";
|
|
5
|
+
export { DeepSeekProvider } from "./providers/deepseek.js";
|
|
6
|
+
export { KimiProvider } from "./providers/kimi.js";
|
|
7
|
+
export { QwenProvider } from "./providers/qwen.js";
|
|
8
|
+
export { GeminiProvider } from "./providers/gemini.js";
|
|
9
|
+
export { MiniMaxProvider } from "./providers/minimax.js";
|
|
5
10
|
export { OllamaProvider } from "./providers/ollama.js";
|
|
6
11
|
export { CircuitBreaker, normalizeToolCall } from "./providers/base.js";
|
|
12
|
+
export { OpenAIChatAdapter } from "./providers/openai-chat.js";
|
|
13
|
+
export { OpenAIResponsesAdapter, OpenAIResponsesProvider } from "./providers/openai-responses.js";
|
|
14
|
+
export type { OpenAIResponsesRunState } from "./providers/openai-responses.js";
|
|
15
|
+
export { endpointProfiles, modelProfiles, getModelProfile } from "./providers/profiles.js";
|
|
16
|
+
export { createProvider } from "./providers/catalog.js";
|
|
17
|
+
export type { CreateProviderOptions, EndpointProfileId } from "./providers/catalog.js";
|
|
7
18
|
export { tool, executeTools, readFile } from "./tools/index.js";
|
|
8
19
|
export type { RegisteredTool } from "./tools/index.js";
|
|
9
20
|
export { scanSkillDir, readSkillFile } from "./skills/loader.js";
|
|
@@ -20,4 +31,4 @@ export { PermissionManager, PermissionMode } from "./safety/permissions.js";
|
|
|
20
31
|
export type { PermissionDecision, Permission } from "./safety/permissions.js";
|
|
21
32
|
export { Governance } from "./governance.js";
|
|
22
33
|
export type { GovernanceVerdict } from "./governance.js";
|
|
23
|
-
export type { Message, ToolCall, ToolResult, ToolSchema, ContentPart, TextPart, ImagePart, AudioPart, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolResultEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, LLMProvider, RetryConfig, TokenUsage, ProviderToolSpec, } from "./types.js";
|
|
34
|
+
export type { Message, ToolCall, ToolResult, ToolSchema, ContentPart, TextPart, ImagePart, AudioPart, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolResultEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, LLMProvider, RetryConfig, TokenUsage, ProviderToolSpec, ProviderRunState, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
export { Agent } from "./agent.js";
|
|
2
2
|
export { AnthropicProvider } from "./providers/anthropic.js";
|
|
3
|
-
export {
|
|
3
|
+
export { OpenAIChatProvider, OpenAIProvider } from "./providers/openai.js";
|
|
4
|
+
export { DeepSeekProvider } from "./providers/deepseek.js";
|
|
5
|
+
export { KimiProvider } from "./providers/kimi.js";
|
|
6
|
+
export { QwenProvider } from "./providers/qwen.js";
|
|
7
|
+
export { GeminiProvider } from "./providers/gemini.js";
|
|
8
|
+
export { MiniMaxProvider } from "./providers/minimax.js";
|
|
4
9
|
export { OllamaProvider } from "./providers/ollama.js";
|
|
5
10
|
export { CircuitBreaker, normalizeToolCall } from "./providers/base.js";
|
|
11
|
+
export { OpenAIChatAdapter } from "./providers/openai-chat.js";
|
|
12
|
+
export { OpenAIResponsesAdapter, OpenAIResponsesProvider } from "./providers/openai-responses.js";
|
|
13
|
+
export { endpointProfiles, modelProfiles, getModelProfile } from "./providers/profiles.js";
|
|
14
|
+
export { createProvider } from "./providers/catalog.js";
|
|
6
15
|
export { tool, executeTools, readFile } from "./tools/index.js";
|
|
7
16
|
export { scanSkillDir, readSkillFile } from "./skills/loader.js";
|
|
8
17
|
export { WorkingMemory } from "./memory/working.js";
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import type { Message, ToolSchema, StreamEvent, LLMProvider } from "../types.js";
|
|
2
|
+
interface AnthropicProviderOptions {
|
|
3
|
+
baseURL?: string;
|
|
4
|
+
authMode?: "api-key" | "bearer";
|
|
5
|
+
}
|
|
2
6
|
export declare class AnthropicProvider implements LLMProvider {
|
|
3
|
-
|
|
7
|
+
protected readonly model: string;
|
|
4
8
|
private client;
|
|
5
9
|
private circuit;
|
|
6
10
|
private maxRetries;
|
|
7
11
|
private baseDelay;
|
|
12
|
+
private nativeAssistantBlocks;
|
|
8
13
|
constructor(apiKey: string, model?: string, retry?: {
|
|
9
14
|
maxRetries: number;
|
|
10
15
|
baseDelay: number;
|
|
11
|
-
});
|
|
16
|
+
}, options?: AnthropicProviderOptions);
|
|
12
17
|
private buildTools;
|
|
13
18
|
complete(messages: Message[], tools: ToolSchema[]): Promise<Message>;
|
|
14
19
|
stream(messages: Message[], tools: ToolSchema[], extensions?: Record<string, unknown>): AsyncIterable<StreamEvent>;
|
|
20
|
+
private buildMessages;
|
|
21
|
+
private rememberNativeBlocks;
|
|
22
|
+
private assistantReplayKey;
|
|
15
23
|
}
|
|
24
|
+
export {};
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
import {
|
|
2
|
+
import { withServerRuntimeGuard } from "../runtime/server.js";
|
|
3
|
+
import { CircuitBreaker, normalizeToolCall, splitAnthropicSystem, toAnthropicMessages } from "./base.js";
|
|
3
4
|
export class AnthropicProvider {
|
|
4
5
|
model;
|
|
5
6
|
client;
|
|
6
7
|
circuit;
|
|
7
8
|
maxRetries;
|
|
8
9
|
baseDelay;
|
|
9
|
-
|
|
10
|
+
nativeAssistantBlocks = new Map();
|
|
11
|
+
constructor(apiKey, model = "claude-sonnet-4-6", retry = { maxRetries: 3, baseDelay: 1000 }, options = {}) {
|
|
10
12
|
this.model = model;
|
|
11
|
-
this.client = new Anthropic({
|
|
13
|
+
this.client = withServerRuntimeGuard(() => new Anthropic({
|
|
14
|
+
...(options.authMode === "bearer" ? { authToken: apiKey } : { apiKey }),
|
|
15
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
16
|
+
}));
|
|
12
17
|
this.circuit = new CircuitBreaker();
|
|
13
18
|
this.maxRetries = retry.maxRetries;
|
|
14
19
|
this.baseDelay = retry.baseDelay;
|
|
@@ -23,8 +28,8 @@ export class AnthropicProvider {
|
|
|
23
28
|
async complete(messages, tools) {
|
|
24
29
|
if (this.circuit.isOpen())
|
|
25
30
|
throw new Error("Circuit breaker open");
|
|
26
|
-
const system = messages
|
|
27
|
-
const msgs =
|
|
31
|
+
const system = splitAnthropicSystem(messages);
|
|
32
|
+
const msgs = this.buildMessages(messages);
|
|
28
33
|
let lastErr;
|
|
29
34
|
for (let i = 0; i < this.maxRetries; i++) {
|
|
30
35
|
try {
|
|
@@ -47,7 +52,9 @@ export class AnthropicProvider {
|
|
|
47
52
|
toolCalls.push(tc);
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
|
-
|
|
55
|
+
const message = { role: "assistant", content, tokenCount: resp.usage.input_tokens + resp.usage.output_tokens, toolCalls };
|
|
56
|
+
this.rememberNativeBlocks(message, resp.content);
|
|
57
|
+
return message;
|
|
51
58
|
}
|
|
52
59
|
catch (err) {
|
|
53
60
|
lastErr = err;
|
|
@@ -59,9 +66,12 @@ export class AnthropicProvider {
|
|
|
59
66
|
throw lastErr;
|
|
60
67
|
}
|
|
61
68
|
async *stream(messages, tools, extensions) {
|
|
62
|
-
const system = messages
|
|
63
|
-
const msgs =
|
|
69
|
+
const system = splitAnthropicSystem(messages);
|
|
70
|
+
const msgs = this.buildMessages(messages);
|
|
64
71
|
const toolBlocks = {};
|
|
72
|
+
const nativeBlocks = {};
|
|
73
|
+
let finalText = "";
|
|
74
|
+
const finalToolCalls = [];
|
|
65
75
|
const stream = this.client.messages.stream({
|
|
66
76
|
model: this.model,
|
|
67
77
|
max_tokens: 8096,
|
|
@@ -70,17 +80,26 @@ export class AnthropicProvider {
|
|
|
70
80
|
...(tools.length ? { tools: this.buildTools(tools) } : {}),
|
|
71
81
|
});
|
|
72
82
|
for await (const evt of stream) {
|
|
73
|
-
if (evt.type === "content_block_start"
|
|
74
|
-
|
|
83
|
+
if (evt.type === "content_block_start") {
|
|
84
|
+
nativeBlocks[evt.index] = { ...evt.content_block };
|
|
85
|
+
if (evt.content_block.type === "tool_use") {
|
|
86
|
+
toolBlocks[evt.index] = { id: evt.content_block.id, name: evt.content_block.name, argsBuf: "" };
|
|
87
|
+
}
|
|
75
88
|
}
|
|
76
89
|
else if (evt.type === "content_block_delta") {
|
|
77
90
|
const d = evt.delta;
|
|
78
91
|
if (d.type === "text_delta") {
|
|
92
|
+
finalText += d.text;
|
|
93
|
+
nativeBlocks[evt.index] = { ...nativeBlocks[evt.index], text: String(nativeBlocks[evt.index]?.text ?? "") + d.text };
|
|
79
94
|
yield { type: "text_delta", delta: d.text };
|
|
80
95
|
}
|
|
81
96
|
else if (d.type === "thinking_delta") {
|
|
97
|
+
nativeBlocks[evt.index] = { ...nativeBlocks[evt.index], thinking: String(nativeBlocks[evt.index]?.thinking ?? "") + d.thinking };
|
|
82
98
|
yield { type: "thinking_delta", delta: d.thinking };
|
|
83
99
|
}
|
|
100
|
+
else if (d.type === "signature_delta") {
|
|
101
|
+
nativeBlocks[evt.index] = { ...nativeBlocks[evt.index], signature: String(nativeBlocks[evt.index]?.signature ?? "") + d.signature };
|
|
102
|
+
}
|
|
84
103
|
else if (d.type === "input_json_delta" && toolBlocks[evt.index]) {
|
|
85
104
|
toolBlocks[evt.index].argsBuf += d.partial_json;
|
|
86
105
|
}
|
|
@@ -95,8 +114,25 @@ export class AnthropicProvider {
|
|
|
95
114
|
catch {
|
|
96
115
|
args = {};
|
|
97
116
|
}
|
|
117
|
+
nativeBlocks[evt.index] = { ...nativeBlocks[evt.index], input: args };
|
|
118
|
+
finalToolCalls.push({ id: tb.id, name: tb.name, arguments: JSON.stringify(args) });
|
|
98
119
|
yield { type: "tool_call", id: tb.id, name: tb.name, arguments: args };
|
|
99
120
|
}
|
|
100
121
|
}
|
|
122
|
+
this.rememberNativeBlocks({ content: finalText, toolCalls: finalToolCalls }, Object.keys(nativeBlocks).map(Number).sort((a, b) => a - b).map(index => nativeBlocks[index]));
|
|
123
|
+
}
|
|
124
|
+
buildMessages(messages) {
|
|
125
|
+
return toAnthropicMessages(messages, message => this.nativeAssistantBlocks.get(this.assistantReplayKey(message)));
|
|
126
|
+
}
|
|
127
|
+
rememberNativeBlocks(message, blocks) {
|
|
128
|
+
if (!message.toolCalls?.length)
|
|
129
|
+
return;
|
|
130
|
+
this.nativeAssistantBlocks.set(this.assistantReplayKey(message), blocks);
|
|
131
|
+
}
|
|
132
|
+
assistantReplayKey(message) {
|
|
133
|
+
return JSON.stringify({
|
|
134
|
+
content: message.content,
|
|
135
|
+
toolCalls: message.toolCalls ?? [],
|
|
136
|
+
});
|
|
101
137
|
}
|
|
102
138
|
}
|
package/dist/providers/base.d.ts
CHANGED
|
@@ -16,3 +16,6 @@ export declare function normalizeToolCall(id: string, name: string, args: unknow
|
|
|
16
16
|
import type { Message } from "../types.js";
|
|
17
17
|
export declare function toAnthropicContent(msg: Message): string | Array<Record<string, unknown>>;
|
|
18
18
|
export declare function toOpenAIContent(msg: Message): string | Array<Record<string, unknown>>;
|
|
19
|
+
export declare function splitAnthropicSystem(messages: Message[]): string;
|
|
20
|
+
export declare function toAnthropicMessages(messages: Message[], nativeReplay?: (message: Message) => Array<Record<string, unknown>> | undefined): Array<Record<string, unknown>>;
|
|
21
|
+
export declare function toOpenAIMessageParams(messages: Message[]): Array<Record<string, unknown>>;
|
package/dist/providers/base.js
CHANGED
|
@@ -59,6 +59,9 @@ export function toAnthropicContent(msg) {
|
|
|
59
59
|
if (p.type === "audio") {
|
|
60
60
|
return { type: "text", text: `[audio: ${p.mediaType}]` };
|
|
61
61
|
}
|
|
62
|
+
if (p.type === "tool_result") {
|
|
63
|
+
return { type: "tool_result", tool_use_id: p.callId, content: p.output, is_error: p.isError };
|
|
64
|
+
}
|
|
62
65
|
return { type: "text", text: "" };
|
|
63
66
|
});
|
|
64
67
|
}
|
|
@@ -75,6 +78,82 @@ export function toOpenAIContent(msg) {
|
|
|
75
78
|
if (p.type === "audio") {
|
|
76
79
|
return { type: "input_audio", input_audio: { data: p.data, format: p.mediaType?.split("/")[1] ?? "wav" } };
|
|
77
80
|
}
|
|
81
|
+
if (p.type === "tool_result") {
|
|
82
|
+
return { type: "text", text: p.output };
|
|
83
|
+
}
|
|
78
84
|
return { type: "text", text: "" };
|
|
79
85
|
});
|
|
80
86
|
}
|
|
87
|
+
function parseToolArguments(args) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(args || "{}");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function splitAnthropicSystem(messages) {
|
|
96
|
+
return messages.filter(m => m.role === "system").map(m => m.content).join("\n\n");
|
|
97
|
+
}
|
|
98
|
+
export function toAnthropicMessages(messages, nativeReplay) {
|
|
99
|
+
const result = [];
|
|
100
|
+
for (const msg of messages.filter(m => m.role !== "system")) {
|
|
101
|
+
if (msg.role === "tool") {
|
|
102
|
+
const parts = (msg.contentParts ?? [])
|
|
103
|
+
.filter((p) => p.type === "tool_result")
|
|
104
|
+
.map(p => ({ type: "tool_result", tool_use_id: p.callId, content: p.output, is_error: p.isError }));
|
|
105
|
+
if (parts.length)
|
|
106
|
+
result.push({ role: "user", content: parts });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (msg.role === "assistant" && msg.toolCalls?.length) {
|
|
110
|
+
const replay = nativeReplay?.(msg);
|
|
111
|
+
if (replay) {
|
|
112
|
+
result.push({ role: "assistant", content: replay });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const blocks = [];
|
|
116
|
+
if (msg.content)
|
|
117
|
+
blocks.push({ type: "text", text: msg.content });
|
|
118
|
+
blocks.push(...msg.toolCalls.map(tc => ({
|
|
119
|
+
type: "tool_use",
|
|
120
|
+
id: tc.id,
|
|
121
|
+
name: tc.name,
|
|
122
|
+
input: parseToolArguments(tc.arguments),
|
|
123
|
+
})));
|
|
124
|
+
result.push({ role: "assistant", content: blocks });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
result.push({
|
|
128
|
+
role: msg.role,
|
|
129
|
+
content: toAnthropicContent(msg),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
export function toOpenAIMessageParams(messages) {
|
|
135
|
+
const result = [];
|
|
136
|
+
for (const msg of messages) {
|
|
137
|
+
if (msg.role === "tool") {
|
|
138
|
+
const parts = (msg.contentParts ?? [])
|
|
139
|
+
.filter((p) => p.type === "tool_result");
|
|
140
|
+
for (const p of parts) {
|
|
141
|
+
result.push({ role: "tool", tool_call_id: p.callId, content: p.output });
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const next = {
|
|
146
|
+
role: msg.role,
|
|
147
|
+
content: toOpenAIContent(msg),
|
|
148
|
+
};
|
|
149
|
+
if (msg.role === "assistant" && msg.toolCalls?.length) {
|
|
150
|
+
next.tool_calls = msg.toolCalls.map(tc => ({
|
|
151
|
+
id: tc.id,
|
|
152
|
+
type: "function",
|
|
153
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
result.push(next);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LLMProvider } from "../types.js";
|
|
2
|
+
import { endpointProfiles, type ModelProfileId } from "./profiles.js";
|
|
3
|
+
export type EndpointProfileId = keyof typeof endpointProfiles;
|
|
4
|
+
export interface CreateProviderOptions {
|
|
5
|
+
model: ModelProfileId;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
endpoint?: EndpointProfileId;
|
|
8
|
+
retry?: {
|
|
9
|
+
maxRetries: number;
|
|
10
|
+
baseDelay: number;
|
|
11
|
+
};
|
|
12
|
+
baseURL?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function createProvider(options: CreateProviderOptions): LLMProvider;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { OpenAIChatProvider } from "./openai.js";
|
|
2
|
+
import { DeepSeekProvider } from "./deepseek.js";
|
|
3
|
+
import { KimiProvider } from "./kimi.js";
|
|
4
|
+
import { OpenAIResponsesProvider } from "./openai-responses.js";
|
|
5
|
+
import { MiniMaxProvider } from "./minimax.js";
|
|
6
|
+
import { QwenProvider } from "./qwen.js";
|
|
7
|
+
import { GeminiProvider } from "./gemini.js";
|
|
8
|
+
import { endpointProfiles, getModelProfile } from "./profiles.js";
|
|
9
|
+
export function createProvider(options) {
|
|
10
|
+
const profile = getModelProfile(options.model);
|
|
11
|
+
const endpointId = (options.endpoint ?? profile.defaultEndpointId);
|
|
12
|
+
const endpoint = endpointProfiles[endpointId];
|
|
13
|
+
if (!endpoint) {
|
|
14
|
+
throw new Error(`Unknown endpoint profile: ${endpointId}`);
|
|
15
|
+
}
|
|
16
|
+
if (endpoint.providerId !== profile.providerId) {
|
|
17
|
+
throw new Error(`Endpoint ${endpoint.id} does not belong to provider ${profile.providerId}`);
|
|
18
|
+
}
|
|
19
|
+
const model = options.model.slice(`${profile.providerId}/`.length);
|
|
20
|
+
const baseURL = options.baseURL ?? endpoint.baseURL;
|
|
21
|
+
if (profile.providerId === "openai") {
|
|
22
|
+
if (endpoint.protocol === "openai-chat") {
|
|
23
|
+
return new OpenAIChatProvider(options.apiKey, model, options.retry, baseURL);
|
|
24
|
+
}
|
|
25
|
+
if (endpoint.protocol === "openai-responses") {
|
|
26
|
+
return new OpenAIResponsesProvider(options.apiKey, model, options.retry, baseURL);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (profile.providerId === "minimax" && endpoint.protocol === "anthropic-messages") {
|
|
30
|
+
return new MiniMaxProvider(options.apiKey, model, options.retry, baseURL);
|
|
31
|
+
}
|
|
32
|
+
if (profile.providerId === "deepseek" && endpoint.protocol === "openai-chat") {
|
|
33
|
+
return new DeepSeekProvider(options.apiKey, model, options.retry, baseURL);
|
|
34
|
+
}
|
|
35
|
+
if (profile.providerId === "kimi" && endpoint.protocol === "openai-chat") {
|
|
36
|
+
return new KimiProvider(options.apiKey, model, options.retry, baseURL);
|
|
37
|
+
}
|
|
38
|
+
if (profile.providerId === "qwen" && endpoint.protocol === "openai-chat") {
|
|
39
|
+
return new QwenProvider(options.apiKey, model, options.retry, baseURL);
|
|
40
|
+
}
|
|
41
|
+
if (profile.providerId === "gemini" && endpoint.protocol === "gemini") {
|
|
42
|
+
return new GeminiProvider(options.apiKey, model, options.retry, baseURL);
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`No Node provider factory for ${profile.id} on ${endpoint.id}`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Message, ToolSchema, StreamEvent } from "../types.js";
|
|
2
|
+
import { OpenAIChatProvider } from "./openai.js";
|
|
3
|
+
export declare class DeepSeekProvider extends OpenAIChatProvider {
|
|
4
|
+
constructor(apiKey: string, model?: "deepseek-v4-flash" | "deepseek-v4-pro", retry?: {
|
|
5
|
+
maxRetries: number;
|
|
6
|
+
baseDelay: number;
|
|
7
|
+
}, baseURL?: string);
|
|
8
|
+
stream(messages: Message[], tools: ToolSchema[], extensions?: Record<string, unknown>): AsyncIterable<StreamEvent>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { OpenAIChatProvider } from "./openai.js";
|
|
2
|
+
import { endpointProfiles } from "./profiles.js";
|
|
3
|
+
const DEEPSEEK_BASE = endpointProfiles["deepseek.openai"].baseURL;
|
|
4
|
+
export class DeepSeekProvider extends OpenAIChatProvider {
|
|
5
|
+
constructor(apiKey, model = "deepseek-v4-flash", retry, baseURL = DEEPSEEK_BASE) {
|
|
6
|
+
super(apiKey, model, retry, baseURL);
|
|
7
|
+
}
|
|
8
|
+
async *stream(messages, tools, extensions) {
|
|
9
|
+
const exposeReasoning = extensions?.exposeReasoning ?? false;
|
|
10
|
+
const thinking = extensions?.thinking === false ? "disabled" : "enabled";
|
|
11
|
+
const reasoningEffort = extensions?.reasoningEffort === "max" ? "max" : "high";
|
|
12
|
+
const msgs = this.chat.buildMessages(messages);
|
|
13
|
+
const toolCallBufs = {};
|
|
14
|
+
let reasoningContent = "";
|
|
15
|
+
let finalText = "";
|
|
16
|
+
const stream = await this.client.chat.completions.create({
|
|
17
|
+
model: this.model,
|
|
18
|
+
messages: msgs,
|
|
19
|
+
...(tools.length ? { tools: this.chat.buildTools(tools) } : {}),
|
|
20
|
+
stream: true,
|
|
21
|
+
reasoning_effort: reasoningEffort,
|
|
22
|
+
extra_body: { thinking: { type: thinking } },
|
|
23
|
+
});
|
|
24
|
+
for await (const chunk of stream) {
|
|
25
|
+
const choice = chunk.choices[0];
|
|
26
|
+
if (!choice)
|
|
27
|
+
continue;
|
|
28
|
+
const delta = choice.delta;
|
|
29
|
+
if (exposeReasoning && delta.reasoning_content) {
|
|
30
|
+
yield { type: "thinking_delta", delta: delta.reasoning_content };
|
|
31
|
+
}
|
|
32
|
+
if (delta.reasoning_content)
|
|
33
|
+
reasoningContent += String(delta.reasoning_content);
|
|
34
|
+
if (delta.content) {
|
|
35
|
+
finalText += String(delta.content);
|
|
36
|
+
yield { type: "text_delta", delta: delta.content };
|
|
37
|
+
}
|
|
38
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
39
|
+
const idx = tc.index;
|
|
40
|
+
if (!toolCallBufs[idx])
|
|
41
|
+
toolCallBufs[idx] = { id: tc.id ?? "", name: "", argsBuf: "" };
|
|
42
|
+
if (tc.function?.name)
|
|
43
|
+
toolCallBufs[idx].name += tc.function.name;
|
|
44
|
+
toolCallBufs[idx].argsBuf += tc.function?.arguments ?? "";
|
|
45
|
+
}
|
|
46
|
+
if (choice.finish_reason === "tool_calls") {
|
|
47
|
+
const toolCalls = Object.values(toolCallBufs).map(tb => ({
|
|
48
|
+
id: tb.id, name: tb.name, arguments: tb.argsBuf || "{}",
|
|
49
|
+
}));
|
|
50
|
+
this.chat.rememberReplayFields({ content: finalText, toolCalls }, { reasoning_content: reasoningContent });
|
|
51
|
+
for (const tb of Object.values(toolCallBufs)) {
|
|
52
|
+
let args = {};
|
|
53
|
+
try {
|
|
54
|
+
args = JSON.parse(tb.argsBuf || "{}");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
args = {};
|
|
58
|
+
}
|
|
59
|
+
yield { type: "tool_call", id: tb.id, name: tb.name, arguments: args };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Message, ToolSchema, StreamEvent, LLMProvider } from "../types.js";
|
|
2
|
+
export declare class GeminiProvider implements LLMProvider {
|
|
3
|
+
private readonly model;
|
|
4
|
+
private genAI;
|
|
5
|
+
private circuit;
|
|
6
|
+
private maxRetries;
|
|
7
|
+
private baseDelay;
|
|
8
|
+
constructor(apiKey: string, model?: string, retry?: {
|
|
9
|
+
maxRetries: number;
|
|
10
|
+
baseDelay: number;
|
|
11
|
+
}, baseURL?: string);
|
|
12
|
+
complete(messages: Message[], tools: ToolSchema[]): Promise<Message>;
|
|
13
|
+
stream(messages: Message[], tools: ToolSchema[], extensions?: Record<string, unknown>): AsyncIterable<StreamEvent>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
2
|
+
import { withServerRuntimeGuard } from "../runtime/server.js";
|
|
3
|
+
import { CircuitBreaker, normalizeToolCall } from "./base.js";
|
|
4
|
+
import { endpointProfiles } from "./profiles.js";
|
|
5
|
+
const GEMINI_BASE = endpointProfiles["gemini.google"].baseURL;
|
|
6
|
+
function buildContents(messages) {
|
|
7
|
+
const contents = [];
|
|
8
|
+
for (const msg of messages) {
|
|
9
|
+
if (msg.role === "system")
|
|
10
|
+
continue;
|
|
11
|
+
if (msg.role === "tool") {
|
|
12
|
+
const parts = (msg.contentParts ?? [])
|
|
13
|
+
.filter(p => p.type === "tool_result")
|
|
14
|
+
.map(p => p.type === "tool_result" ? ({
|
|
15
|
+
functionResponse: { name: p.callId, response: { output: p.output } },
|
|
16
|
+
}) : ({ text: "" }));
|
|
17
|
+
if (parts.length)
|
|
18
|
+
contents.push({ role: "user", parts });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const role = msg.role === "assistant" ? "model" : "user";
|
|
22
|
+
const parts = [];
|
|
23
|
+
if (msg.toolCalls?.length) {
|
|
24
|
+
for (const tc of msg.toolCalls) {
|
|
25
|
+
let args = {};
|
|
26
|
+
try {
|
|
27
|
+
args = JSON.parse(tc.arguments);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
args = {};
|
|
31
|
+
}
|
|
32
|
+
parts.push({ functionCall: { name: tc.name, args } });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (msg.content)
|
|
36
|
+
parts.push({ text: msg.content });
|
|
37
|
+
if (parts.length)
|
|
38
|
+
contents.push({ role, parts });
|
|
39
|
+
}
|
|
40
|
+
return contents;
|
|
41
|
+
}
|
|
42
|
+
function buildTools(tools) {
|
|
43
|
+
if (!tools.length)
|
|
44
|
+
return [];
|
|
45
|
+
return [{
|
|
46
|
+
functionDeclarations: tools.map(t => ({
|
|
47
|
+
name: t.name,
|
|
48
|
+
description: t.description,
|
|
49
|
+
parameters: JSON.parse(t.parameters),
|
|
50
|
+
})),
|
|
51
|
+
}];
|
|
52
|
+
}
|
|
53
|
+
function systemInstruction(messages) {
|
|
54
|
+
return messages.find(m => m.role === "system")?.content;
|
|
55
|
+
}
|
|
56
|
+
export class GeminiProvider {
|
|
57
|
+
model;
|
|
58
|
+
genAI;
|
|
59
|
+
circuit;
|
|
60
|
+
maxRetries;
|
|
61
|
+
baseDelay;
|
|
62
|
+
constructor(apiKey, model = "gemini-2.0-flash", retry = { maxRetries: 3, baseDelay: 1000 }, baseURL = GEMINI_BASE) {
|
|
63
|
+
this.model = model;
|
|
64
|
+
this.genAI = withServerRuntimeGuard(() => new GoogleGenerativeAI(apiKey));
|
|
65
|
+
this.circuit = new CircuitBreaker();
|
|
66
|
+
this.maxRetries = retry.maxRetries;
|
|
67
|
+
this.baseDelay = retry.baseDelay;
|
|
68
|
+
}
|
|
69
|
+
async complete(messages, tools) {
|
|
70
|
+
if (this.circuit.isOpen())
|
|
71
|
+
throw new Error("Circuit breaker open");
|
|
72
|
+
const system = systemInstruction(messages);
|
|
73
|
+
const contents = buildContents(messages);
|
|
74
|
+
const geminiTools = buildTools(tools);
|
|
75
|
+
let lastErr;
|
|
76
|
+
for (let i = 0; i < this.maxRetries; i++) {
|
|
77
|
+
try {
|
|
78
|
+
const m = this.genAI.getGenerativeModel({
|
|
79
|
+
model: this.model,
|
|
80
|
+
...(system ? { systemInstruction: system } : {}),
|
|
81
|
+
...(geminiTools.length ? { tools: geminiTools } : {}),
|
|
82
|
+
});
|
|
83
|
+
const resp = await m.generateContent({ contents });
|
|
84
|
+
this.circuit.recordSuccess();
|
|
85
|
+
const candidate = resp.response.candidates?.[0];
|
|
86
|
+
let content = "";
|
|
87
|
+
const toolCalls = [];
|
|
88
|
+
for (const part of candidate?.content.parts ?? []) {
|
|
89
|
+
if (part.text)
|
|
90
|
+
content += part.text;
|
|
91
|
+
else if (part.functionCall) {
|
|
92
|
+
const tc = normalizeToolCall(part.functionCall.name, part.functionCall.name, part.functionCall.args);
|
|
93
|
+
if (tc)
|
|
94
|
+
toolCalls.push(tc);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const usage = resp.response.usageMetadata;
|
|
98
|
+
return {
|
|
99
|
+
role: "assistant",
|
|
100
|
+
content,
|
|
101
|
+
tokenCount: usage?.totalTokenCount,
|
|
102
|
+
toolCalls,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
lastErr = err;
|
|
107
|
+
this.circuit.recordFailure();
|
|
108
|
+
if (i < this.maxRetries - 1)
|
|
109
|
+
await new Promise(r => setTimeout(r, this.baseDelay * 2 ** i));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw lastErr;
|
|
113
|
+
}
|
|
114
|
+
async *stream(messages, tools, extensions) {
|
|
115
|
+
const system = systemInstruction(messages);
|
|
116
|
+
const contents = buildContents(messages);
|
|
117
|
+
const geminiTools = buildTools(tools);
|
|
118
|
+
const m = this.genAI.getGenerativeModel({
|
|
119
|
+
model: this.model,
|
|
120
|
+
...(system ? { systemInstruction: system } : {}),
|
|
121
|
+
...(geminiTools.length ? { tools: geminiTools } : {}),
|
|
122
|
+
});
|
|
123
|
+
const result = await m.generateContentStream({ contents });
|
|
124
|
+
const toolCallBufs = {};
|
|
125
|
+
for await (const chunk of result.stream) {
|
|
126
|
+
for (const part of chunk.candidates?.[0]?.content.parts ?? []) {
|
|
127
|
+
if (part.text)
|
|
128
|
+
yield { type: "text_delta", delta: part.text };
|
|
129
|
+
else if (part.functionCall) {
|
|
130
|
+
const { name, args } = part.functionCall;
|
|
131
|
+
toolCallBufs[name] = { name, args: args };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const [id, tc] of Object.entries(toolCallBufs)) {
|
|
136
|
+
yield { type: "tool_call", id, name: tc.name, arguments: tc.args };
|
|
137
|
+
}
|
|
138
|
+
const usage = (await result.response).usageMetadata;
|
|
139
|
+
if (usage?.totalTokenCount)
|
|
140
|
+
yield { type: "usage", totalTokens: usage.totalTokenCount };
|
|
141
|
+
}
|
|
142
|
+
}
|