@falai/agent 2.2.0 → 2.2.2
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/cjs/core/BranchEvaluator.d.ts +2 -2
- package/dist/cjs/core/BranchEvaluator.js +2 -2
- package/dist/cjs/core/BranchEvaluator.js.map +1 -1
- package/dist/cjs/core/Flow.d.ts +2 -1
- package/dist/cjs/core/Flow.d.ts.map +1 -1
- package/dist/cjs/core/Flow.js +2 -1
- package/dist/cjs/core/Flow.js.map +1 -1
- package/dist/cjs/core/FlowRouter.js +2 -2
- package/dist/cjs/core/FlowRouter.js.map +1 -1
- package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
- package/dist/cjs/core/PromptComposer.js +8 -20
- package/dist/cjs/core/PromptComposer.js.map +1 -1
- package/dist/cjs/core/Step.d.ts +2 -1
- package/dist/cjs/core/Step.d.ts.map +1 -1
- package/dist/cjs/core/Step.js +2 -1
- package/dist/cjs/core/Step.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts +61 -0
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -0
- package/dist/cjs/providers/DeepSeekProvider.js +450 -0
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -0
- package/dist/cjs/providers/index.d.ts +2 -0
- package/dist/cjs/providers/index.d.ts.map +1 -1
- package/dist/cjs/providers/index.js +3 -1
- package/dist/cjs/providers/index.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +5 -3
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/flow.d.ts +4 -4
- package/dist/core/BranchEvaluator.d.ts +2 -2
- package/dist/core/BranchEvaluator.js +2 -2
- package/dist/core/BranchEvaluator.js.map +1 -1
- package/dist/core/Flow.d.ts +2 -1
- package/dist/core/Flow.d.ts.map +1 -1
- package/dist/core/Flow.js +2 -1
- package/dist/core/Flow.js.map +1 -1
- package/dist/core/FlowRouter.js +2 -2
- package/dist/core/FlowRouter.js.map +1 -1
- package/dist/core/PromptComposer.d.ts.map +1 -1
- package/dist/core/PromptComposer.js +8 -20
- package/dist/core/PromptComposer.js.map +1 -1
- package/dist/core/Step.d.ts +2 -1
- package/dist/core/Step.d.ts.map +1 -1
- package/dist/core/Step.js +2 -1
- package/dist/core/Step.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts +61 -0
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -0
- package/dist/providers/DeepSeekProvider.js +443 -0
- package/dist/providers/DeepSeekProvider.js.map +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/types/agent.d.ts +5 -3
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/flow.d.ts +4 -4
- package/docs/guides/branching.md +3 -1
- package/docs/guides/conditions.md +9 -8
- package/docs/guides/instructions.md +7 -7
- package/docs/reference/branches.md +2 -2
- package/docs/reference/flow.md +1 -1
- package/docs/reference/instruction.md +7 -5
- package/docs/reference/providers.md +48 -2
- package/docs/reference/step.md +2 -2
- package/package.json +11 -11
- package/src/core/BranchEvaluator.ts +4 -4
- package/src/core/Flow.ts +2 -1
- package/src/core/FlowRouter.ts +2 -2
- package/src/core/PromptComposer.ts +9 -20
- package/src/core/Step.ts +2 -1
- package/src/index.ts +2 -0
- package/src/providers/DeepSeekProvider.ts +666 -0
- package/src/providers/index.ts +3 -0
- package/src/types/agent.ts +5 -3
- package/src/types/flow.ts +4 -4
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek provider implementation (OpenAI-compatible API)
|
|
3
|
+
* Supports deepseek-chat, deepseek-reasoner with optional thinking/reasoning mode.
|
|
4
|
+
* DeepSeek streams reasoning content via `delta.reasoning_content` when thinking is enabled.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import OpenAI from "openai";
|
|
8
|
+
import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions";
|
|
9
|
+
import { FunctionParameters } from "openai/resources/shared.mjs";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AiProvider,
|
|
13
|
+
GenerateMessageInput,
|
|
14
|
+
GenerateMessageOutput,
|
|
15
|
+
GenerateMessageStreamChunk,
|
|
16
|
+
AgentStructuredResponse,
|
|
17
|
+
StructuredSchema,
|
|
18
|
+
} from "../types";
|
|
19
|
+
import type { HistoryItem } from "../types/history";
|
|
20
|
+
import { withTimeoutAndRetry, logger } from "../utils";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
23
|
+
timeout: 60000,
|
|
24
|
+
retries: 3,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Configuration options for DeepSeek provider
|
|
29
|
+
* Uses types from openai package (DeepSeek is OpenAI-compatible)
|
|
30
|
+
*/
|
|
31
|
+
export interface DeepSeekProviderOptions {
|
|
32
|
+
/** DeepSeek API key */
|
|
33
|
+
apiKey: string;
|
|
34
|
+
/** Model to use (required) - e.g., "deepseek-chat", "deepseek-reasoner" */
|
|
35
|
+
model: string;
|
|
36
|
+
/** Backup models to try if primary fails (default: []) */
|
|
37
|
+
backupModels?: string[];
|
|
38
|
+
/** Custom base URL (default: "https://api.deepseek.com") */
|
|
39
|
+
baseURL?: string;
|
|
40
|
+
/** Default parameters - uses ChatCompletionCreateParamsNonStreaming from openai package */
|
|
41
|
+
config?: Partial<
|
|
42
|
+
Omit<ChatCompletionCreateParamsNonStreaming, "model" | "messages">
|
|
43
|
+
>;
|
|
44
|
+
/** Retry configuration */
|
|
45
|
+
retryConfig?: {
|
|
46
|
+
timeout?: number;
|
|
47
|
+
retries?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Type guard for errors with status/code properties
|
|
53
|
+
*/
|
|
54
|
+
interface ErrorWithStatus {
|
|
55
|
+
status?: number;
|
|
56
|
+
code?: string;
|
|
57
|
+
message?: string;
|
|
58
|
+
type?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Type guard to check if error is ErrorWithStatus
|
|
63
|
+
*/
|
|
64
|
+
function isErrorWithStatus(error: unknown): error is ErrorWithStatus {
|
|
65
|
+
return (
|
|
66
|
+
typeof error === "object" &&
|
|
67
|
+
error !== null &&
|
|
68
|
+
("status" in error || "code" in error || "message" in error)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Safely extract error message
|
|
74
|
+
*/
|
|
75
|
+
function getErrorMessage(error: unknown): string {
|
|
76
|
+
if (error instanceof Error) {
|
|
77
|
+
return error.message;
|
|
78
|
+
}
|
|
79
|
+
if (isErrorWithStatus(error) && error.message) {
|
|
80
|
+
return error.message;
|
|
81
|
+
}
|
|
82
|
+
return String(error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determines if an error should trigger backup model usage
|
|
87
|
+
*/
|
|
88
|
+
const shouldUseBackupModel = (error: unknown): boolean => {
|
|
89
|
+
if (!isErrorWithStatus(error)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Server errors
|
|
94
|
+
if (error.status === 500 || error.status === 503) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Rate limiting
|
|
99
|
+
if (error.status === 429) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Model overloaded or unavailable
|
|
104
|
+
if (error.code === "model_not_found" || error.code === "model_overloaded") {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const message = getErrorMessage(error);
|
|
109
|
+
if (
|
|
110
|
+
message.includes("overloaded") ||
|
|
111
|
+
message.includes("unavailable") ||
|
|
112
|
+
message.includes("internal error") ||
|
|
113
|
+
message.includes("Internal error")
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* DeepSeek provider implementation using the OpenAI-compatible API.
|
|
123
|
+
* Supports deepseek-chat and deepseek-reasoner with optional thinking mode.
|
|
124
|
+
*
|
|
125
|
+
* DeepSeek streams reasoning content via `delta.reasoning_content` when
|
|
126
|
+
* thinking mode is enabled. Tool calls follow the standard OpenAI format.
|
|
127
|
+
*/
|
|
128
|
+
export class DeepSeekProvider implements AiProvider {
|
|
129
|
+
public readonly name = "deepseek";
|
|
130
|
+
private client: OpenAI;
|
|
131
|
+
private primaryModel: string;
|
|
132
|
+
private backupModels: string[];
|
|
133
|
+
private config?: Partial<
|
|
134
|
+
Omit<ChatCompletionCreateParamsNonStreaming, "model" | "messages">
|
|
135
|
+
>;
|
|
136
|
+
private retryConfig: { timeout: number; retries: number };
|
|
137
|
+
|
|
138
|
+
constructor(options: DeepSeekProviderOptions) {
|
|
139
|
+
const {
|
|
140
|
+
apiKey,
|
|
141
|
+
model,
|
|
142
|
+
backupModels = [],
|
|
143
|
+
baseURL = "https://api.deepseek.com",
|
|
144
|
+
config,
|
|
145
|
+
retryConfig,
|
|
146
|
+
} = options;
|
|
147
|
+
|
|
148
|
+
if (!apiKey) {
|
|
149
|
+
throw new Error("DeepSeek API key is required");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!model) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
"Model is required. Example: 'deepseek-chat' or 'deepseek-reasoner'"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.client = new OpenAI({
|
|
159
|
+
apiKey,
|
|
160
|
+
baseURL,
|
|
161
|
+
});
|
|
162
|
+
this.primaryModel = model;
|
|
163
|
+
this.backupModels = backupModels;
|
|
164
|
+
this.config = config;
|
|
165
|
+
this.retryConfig = {
|
|
166
|
+
timeout: retryConfig?.timeout || DEFAULT_RETRY_CONFIG.timeout,
|
|
167
|
+
retries: retryConfig?.retries || DEFAULT_RETRY_CONFIG.retries,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Build OpenAI-formatted messages from HistoryItem[] array.
|
|
173
|
+
* DeepSeek uses OpenAI-compatible message format.
|
|
174
|
+
*/
|
|
175
|
+
private buildMessages(history: HistoryItem[]): Array<unknown> {
|
|
176
|
+
const messages: Array<unknown> = [];
|
|
177
|
+
|
|
178
|
+
for (const item of history) {
|
|
179
|
+
switch (item.role) {
|
|
180
|
+
case "system":
|
|
181
|
+
messages.push({ role: "system", content: item.content });
|
|
182
|
+
break;
|
|
183
|
+
case "user":
|
|
184
|
+
messages.push({ role: "user", content: item.content });
|
|
185
|
+
break;
|
|
186
|
+
case "assistant":
|
|
187
|
+
if (item.tool_calls && item.tool_calls.length > 0) {
|
|
188
|
+
messages.push({
|
|
189
|
+
role: "assistant",
|
|
190
|
+
content: item.content || null,
|
|
191
|
+
tool_calls: item.tool_calls.map((tc) => ({
|
|
192
|
+
id: tc.id,
|
|
193
|
+
type: "function",
|
|
194
|
+
function: {
|
|
195
|
+
name: tc.name,
|
|
196
|
+
arguments: JSON.stringify(tc.arguments),
|
|
197
|
+
},
|
|
198
|
+
})),
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
messages.push({ role: "assistant", content: item.content || "" });
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case "tool":
|
|
205
|
+
messages.push({
|
|
206
|
+
role: "tool",
|
|
207
|
+
tool_call_id: item.tool_call_id,
|
|
208
|
+
content:
|
|
209
|
+
typeof item.content === "string"
|
|
210
|
+
? item.content
|
|
211
|
+
: JSON.stringify(item.content),
|
|
212
|
+
});
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return messages;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Adapt common schema format to DeepSeek's format.
|
|
222
|
+
* DeepSeek is OpenAI-compatible and uses standard JSON Schema.
|
|
223
|
+
*/
|
|
224
|
+
private adaptSchema(schema: StructuredSchema): Record<string, unknown> {
|
|
225
|
+
return schema as Record<string, unknown>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async generateMessage<
|
|
229
|
+
TContext = unknown,
|
|
230
|
+
TStructured = AgentStructuredResponse,
|
|
231
|
+
>(
|
|
232
|
+
input: GenerateMessageInput<TContext>
|
|
233
|
+
): Promise<GenerateMessageOutput<TStructured>> {
|
|
234
|
+
return this.generateWithBackup<TContext, TStructured>(input);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async *generateMessageStream<
|
|
238
|
+
TContext = unknown,
|
|
239
|
+
TStructured = AgentStructuredResponse,
|
|
240
|
+
>(
|
|
241
|
+
input: GenerateMessageInput<TContext>
|
|
242
|
+
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
243
|
+
yield* this.generateStreamWithBackup<TContext, TStructured>(input);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async generateWithBackup<
|
|
247
|
+
TContext = unknown,
|
|
248
|
+
TStructured = AgentStructuredResponse,
|
|
249
|
+
>(
|
|
250
|
+
input: GenerateMessageInput<TContext>
|
|
251
|
+
): Promise<GenerateMessageOutput<TStructured>> {
|
|
252
|
+
try {
|
|
253
|
+
return await this.generateWithModel<TContext, TStructured>(
|
|
254
|
+
this.primaryModel,
|
|
255
|
+
input
|
|
256
|
+
);
|
|
257
|
+
} catch (primaryError: unknown) {
|
|
258
|
+
const primaryErrMsg = getErrorMessage(primaryError);
|
|
259
|
+
logger.warn(
|
|
260
|
+
`[DEEPSEEK] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (!shouldUseBackupModel(primaryError)) {
|
|
264
|
+
throw primaryError;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
logger.debug(`[DEEPSEEK] Trying backup models`);
|
|
268
|
+
|
|
269
|
+
let lastBackupError: unknown = primaryError;
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < this.backupModels.length; i++) {
|
|
272
|
+
const backupModel = this.backupModels[i];
|
|
273
|
+
logger.debug(
|
|
274
|
+
`[DEEPSEEK] Trying backup model ${i + 1}/${this.backupModels.length}: ${backupModel}`
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const result = await this.generateWithModel(backupModel, input);
|
|
279
|
+
logger.debug(`[DEEPSEEK] Backup model ${backupModel} succeeded`);
|
|
280
|
+
return result as GenerateMessageOutput<TStructured>;
|
|
281
|
+
} catch (backupError: unknown) {
|
|
282
|
+
const backupErrMsg = getErrorMessage(backupError);
|
|
283
|
+
logger.warn(
|
|
284
|
+
`[DEEPSEEK] Backup model ${backupModel} failed: ${backupErrMsg}`
|
|
285
|
+
);
|
|
286
|
+
lastBackupError = backupError;
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
!shouldUseBackupModel(backupError) &&
|
|
290
|
+
i < this.backupModels.length - 1
|
|
291
|
+
) {
|
|
292
|
+
logger.debug(
|
|
293
|
+
`[DEEPSEEK] Backup model error doesn't qualify for further attempts`
|
|
294
|
+
);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const lastBackupErrMsg = getErrorMessage(lastBackupError);
|
|
301
|
+
logger.error(
|
|
302
|
+
`[DEEPSEEK] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
|
|
303
|
+
);
|
|
304
|
+
throw lastBackupError;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async generateWithModel<
|
|
309
|
+
TContext = unknown,
|
|
310
|
+
TStructured = AgentStructuredResponse,
|
|
311
|
+
>(
|
|
312
|
+
model: string,
|
|
313
|
+
input: GenerateMessageInput<TContext>
|
|
314
|
+
): Promise<GenerateMessageOutput<TStructured>> {
|
|
315
|
+
const operation = async (): Promise<GenerateMessageOutput> => {
|
|
316
|
+
const historyMessages = this.buildMessages(input.history);
|
|
317
|
+
historyMessages.push({ role: "user", content: input.prompt });
|
|
318
|
+
|
|
319
|
+
const params: ChatCompletionCreateParamsNonStreaming = {
|
|
320
|
+
model,
|
|
321
|
+
messages:
|
|
322
|
+
historyMessages as ChatCompletionCreateParamsNonStreaming["messages"],
|
|
323
|
+
...this.config,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Override with input parameters if provided
|
|
327
|
+
if (input.parameters?.maxOutputTokens !== undefined) {
|
|
328
|
+
params.max_tokens = input.parameters.maxOutputTokens;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Add tools if provided
|
|
332
|
+
if (input.tools && input.tools.length > 0) {
|
|
333
|
+
params.tools = input.tools.map((tool) => ({
|
|
334
|
+
type: "function" as const,
|
|
335
|
+
function: {
|
|
336
|
+
name: tool.name || tool.id,
|
|
337
|
+
description: tool.description,
|
|
338
|
+
parameters: tool.parameters as FunctionParameters,
|
|
339
|
+
},
|
|
340
|
+
}));
|
|
341
|
+
params.tool_choice = "auto";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Use structured output if JSON schema is provided
|
|
345
|
+
if (input.parameters?.jsonSchema) {
|
|
346
|
+
params.response_format = {
|
|
347
|
+
type: "json_schema" as const,
|
|
348
|
+
json_schema: {
|
|
349
|
+
name: input.parameters.schemaName || "structured_output",
|
|
350
|
+
schema: this.adaptSchema(input.parameters.jsonSchema),
|
|
351
|
+
},
|
|
352
|
+
} as ChatCompletionCreateParamsNonStreaming["response_format"];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const response = await this.client.chat.completions.create(params);
|
|
356
|
+
|
|
357
|
+
const message = response.choices[0]?.message?.content || "";
|
|
358
|
+
|
|
359
|
+
let toolCalls: Array<{
|
|
360
|
+
toolName: string;
|
|
361
|
+
arguments: Record<string, unknown>;
|
|
362
|
+
}> = [];
|
|
363
|
+
if (response.choices?.[0]?.message?.tool_calls) {
|
|
364
|
+
toolCalls = response.choices[0].message.tool_calls
|
|
365
|
+
.filter((toolCall) => toolCall.type === "function")
|
|
366
|
+
.map((toolCall) => {
|
|
367
|
+
let toolCallArguments: Record<string, unknown> = {};
|
|
368
|
+
try {
|
|
369
|
+
toolCallArguments = JSON.parse(
|
|
370
|
+
toolCall.function.arguments
|
|
371
|
+
) as Record<string, unknown>;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
logger.warn(
|
|
374
|
+
`[DEEPSEEK] Failed to parse tool call arguments: ${getErrorMessage(error)}`
|
|
375
|
+
);
|
|
376
|
+
toolCallArguments = {};
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
toolName: toolCall.function.name,
|
|
380
|
+
arguments: toolCallArguments,
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Only throw error if we have no text AND no function calls
|
|
386
|
+
if (!message && toolCalls.length === 0) {
|
|
387
|
+
throw new Error("No response from DeepSeek");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Parse structured output if schema was provided
|
|
391
|
+
let structured: AgentStructuredResponse | undefined;
|
|
392
|
+
if (input.parameters?.jsonSchema && message) {
|
|
393
|
+
try {
|
|
394
|
+
structured = JSON.parse(message) as AgentStructuredResponse;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
logger.warn("[DEEPSEEK] Failed to parse JSON response:", error);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// If tools were used, include them in structured response
|
|
401
|
+
if (toolCalls.length > 0) {
|
|
402
|
+
structured = {
|
|
403
|
+
...(structured || {}),
|
|
404
|
+
message: structured?.message || message,
|
|
405
|
+
toolCalls,
|
|
406
|
+
} as AgentStructuredResponse;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
message,
|
|
411
|
+
metadata: {
|
|
412
|
+
model: response.model,
|
|
413
|
+
finishReason: response.choices[0]?.finish_reason,
|
|
414
|
+
tokensUsed: response.usage?.total_tokens,
|
|
415
|
+
promptTokens: response.usage?.prompt_tokens,
|
|
416
|
+
completionTokens: response.usage?.completion_tokens,
|
|
417
|
+
},
|
|
418
|
+
structured,
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
return withTimeoutAndRetry(
|
|
423
|
+
operation,
|
|
424
|
+
this.retryConfig.timeout,
|
|
425
|
+
this.retryConfig.retries,
|
|
426
|
+
`DeepSeek ${model}`
|
|
427
|
+
) as Promise<GenerateMessageOutput<TStructured>>;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private async *generateStreamWithBackup<
|
|
431
|
+
TContext = unknown,
|
|
432
|
+
TStructured = AgentStructuredResponse,
|
|
433
|
+
>(
|
|
434
|
+
input: GenerateMessageInput<TContext>
|
|
435
|
+
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
436
|
+
try {
|
|
437
|
+
yield* this.generateStreamWithModel<TContext, TStructured>(
|
|
438
|
+
this.primaryModel,
|
|
439
|
+
input
|
|
440
|
+
);
|
|
441
|
+
} catch (primaryError: unknown) {
|
|
442
|
+
const primaryErrMsg = getErrorMessage(primaryError);
|
|
443
|
+
logger.warn(
|
|
444
|
+
`[DEEPSEEK] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
if (!shouldUseBackupModel(primaryError)) {
|
|
448
|
+
throw primaryError;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
logger.debug(`[DEEPSEEK] Trying backup models for streaming`);
|
|
452
|
+
|
|
453
|
+
let lastBackupError: unknown = primaryError;
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < this.backupModels.length; i++) {
|
|
456
|
+
const backupModel = this.backupModels[i];
|
|
457
|
+
logger.debug(
|
|
458
|
+
`[DEEPSEEK] Trying backup model ${i + 1}/${this.backupModels.length}: ${backupModel}`
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
yield* this.generateStreamWithModel<TContext, TStructured>(
|
|
463
|
+
backupModel,
|
|
464
|
+
input
|
|
465
|
+
);
|
|
466
|
+
logger.debug(`[DEEPSEEK] Backup model ${backupModel} succeeded`);
|
|
467
|
+
return;
|
|
468
|
+
} catch (backupError: unknown) {
|
|
469
|
+
const backupErrMsg = getErrorMessage(backupError);
|
|
470
|
+
logger.warn(
|
|
471
|
+
`[DEEPSEEK] Backup model ${backupModel} failed: ${backupErrMsg}`
|
|
472
|
+
);
|
|
473
|
+
lastBackupError = backupError;
|
|
474
|
+
|
|
475
|
+
if (
|
|
476
|
+
!shouldUseBackupModel(backupError) &&
|
|
477
|
+
i < this.backupModels.length - 1
|
|
478
|
+
) {
|
|
479
|
+
logger.debug(
|
|
480
|
+
`[DEEPSEEK] Backup model error doesn't qualify for further attempts`
|
|
481
|
+
);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const lastBackupErrMsg = getErrorMessage(lastBackupError);
|
|
488
|
+
logger.error(
|
|
489
|
+
`[DEEPSEEK] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
|
|
490
|
+
);
|
|
491
|
+
throw lastBackupError;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private async *generateStreamWithModel<
|
|
496
|
+
TContext = unknown,
|
|
497
|
+
TStructured = AgentStructuredResponse,
|
|
498
|
+
>(
|
|
499
|
+
model: string,
|
|
500
|
+
input: GenerateMessageInput<TContext>
|
|
501
|
+
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
502
|
+
const historyMessages = this.buildMessages(input.history);
|
|
503
|
+
historyMessages.push({ role: "user" as const, content: input.prompt });
|
|
504
|
+
|
|
505
|
+
const params = {
|
|
506
|
+
...this.config,
|
|
507
|
+
model,
|
|
508
|
+
messages:
|
|
509
|
+
historyMessages as ChatCompletionCreateParamsNonStreaming["messages"],
|
|
510
|
+
stream: true as const,
|
|
511
|
+
stream_options: { include_usage: true },
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// Override with input parameters if provided
|
|
515
|
+
if (input.parameters?.maxOutputTokens !== undefined) {
|
|
516
|
+
params.max_tokens = input.parameters.maxOutputTokens;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Add tools if provided
|
|
520
|
+
if (input.tools && input.tools.length > 0) {
|
|
521
|
+
params.tools = input.tools.map((tool) => ({
|
|
522
|
+
type: "function" as const,
|
|
523
|
+
function: {
|
|
524
|
+
name: tool.name || tool.id,
|
|
525
|
+
description: tool.description,
|
|
526
|
+
parameters: tool.parameters as FunctionParameters,
|
|
527
|
+
},
|
|
528
|
+
}));
|
|
529
|
+
params.tool_choice = "auto";
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Request JSON schema output if schema is provided
|
|
533
|
+
if (input.parameters?.jsonSchema) {
|
|
534
|
+
params.response_format = {
|
|
535
|
+
type: "json_schema" as const,
|
|
536
|
+
json_schema: {
|
|
537
|
+
name: input.parameters.schemaName || "structured_output",
|
|
538
|
+
schema: this.adaptSchema(input.parameters.jsonSchema),
|
|
539
|
+
},
|
|
540
|
+
} as ChatCompletionCreateParamsNonStreaming["response_format"];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const stream = await this.client.chat.completions.create(params);
|
|
544
|
+
|
|
545
|
+
let accumulated = "";
|
|
546
|
+
let currentModel = model;
|
|
547
|
+
let finishReason: string | undefined;
|
|
548
|
+
let promptTokens: number | undefined;
|
|
549
|
+
let completionTokens: number | undefined;
|
|
550
|
+
let totalTokens: number | undefined;
|
|
551
|
+
const toolCalls: Array<{
|
|
552
|
+
toolName: string;
|
|
553
|
+
arguments: Record<string, unknown>;
|
|
554
|
+
}> = [];
|
|
555
|
+
|
|
556
|
+
for await (const chunk of stream) {
|
|
557
|
+
currentModel = chunk.model;
|
|
558
|
+
const choice = chunk.choices?.[0];
|
|
559
|
+
|
|
560
|
+
// DeepSeek may include usage in chunks with include_usage
|
|
561
|
+
if (chunk.usage) {
|
|
562
|
+
promptTokens = chunk.usage.prompt_tokens;
|
|
563
|
+
completionTokens = chunk.usage.completion_tokens;
|
|
564
|
+
totalTokens = chunk.usage.total_tokens;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!choice) continue;
|
|
568
|
+
|
|
569
|
+
const delta = choice.delta as Record<string, unknown>;
|
|
570
|
+
|
|
571
|
+
// Extract tool calls from delta
|
|
572
|
+
const deltaToolCalls = delta?.tool_calls as
|
|
573
|
+
| Array<{
|
|
574
|
+
index: number;
|
|
575
|
+
id?: string;
|
|
576
|
+
function?: { name?: string; arguments?: string };
|
|
577
|
+
}>
|
|
578
|
+
| undefined;
|
|
579
|
+
|
|
580
|
+
if (deltaToolCalls) {
|
|
581
|
+
for (const toolCall of deltaToolCalls) {
|
|
582
|
+
if (toolCall.function) {
|
|
583
|
+
let toolCallArguments: Record<string, unknown> = {};
|
|
584
|
+
try {
|
|
585
|
+
toolCallArguments = toolCall.function.arguments
|
|
586
|
+
? (JSON.parse(toolCall.function.arguments) as Record<
|
|
587
|
+
string,
|
|
588
|
+
unknown
|
|
589
|
+
>)
|
|
590
|
+
: {};
|
|
591
|
+
} catch (error) {
|
|
592
|
+
logger.warn(
|
|
593
|
+
`[DEEPSEEK] Failed to parse tool call arguments in stream: ${getErrorMessage(error)}`
|
|
594
|
+
);
|
|
595
|
+
toolCallArguments = {};
|
|
596
|
+
}
|
|
597
|
+
toolCalls.push({
|
|
598
|
+
toolName: toolCall.function.name || "",
|
|
599
|
+
arguments: toolCallArguments,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// DeepSeek streams reasoning via `reasoning_content` on the delta
|
|
606
|
+
const reasoning =
|
|
607
|
+
(delta?.reasoning_content as string | undefined) ?? undefined;
|
|
608
|
+
if (reasoning) {
|
|
609
|
+
logger.debug(`[DEEPSEEK] Reasoning: ${reasoning}`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const content = (delta?.content as string | undefined) ?? "";
|
|
613
|
+
if (content) {
|
|
614
|
+
accumulated += content;
|
|
615
|
+
yield {
|
|
616
|
+
delta: content,
|
|
617
|
+
accumulated,
|
|
618
|
+
done: false,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (choice.finish_reason) {
|
|
623
|
+
finishReason = choice.finish_reason;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Parse JSON response if schema was provided
|
|
628
|
+
let structured: TStructured | undefined;
|
|
629
|
+
if (input.parameters?.jsonSchema && accumulated) {
|
|
630
|
+
try {
|
|
631
|
+
structured = JSON.parse(accumulated) as TStructured;
|
|
632
|
+
} catch (error) {
|
|
633
|
+
logger.warn(
|
|
634
|
+
"[DEEPSEEK] Failed to parse JSON response in stream:",
|
|
635
|
+
error
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Include tool calls in structured response
|
|
641
|
+
if (toolCalls.length > 0) {
|
|
642
|
+
structured = {
|
|
643
|
+
...(structured || {}),
|
|
644
|
+
message:
|
|
645
|
+
(structured as AgentStructuredResponse | undefined)?.message ||
|
|
646
|
+
accumulated,
|
|
647
|
+
toolCalls,
|
|
648
|
+
} as TStructured;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Yield final chunk
|
|
652
|
+
yield {
|
|
653
|
+
delta: "",
|
|
654
|
+
accumulated,
|
|
655
|
+
done: true,
|
|
656
|
+
metadata: {
|
|
657
|
+
model: currentModel,
|
|
658
|
+
finishReason,
|
|
659
|
+
tokensUsed: totalTokens,
|
|
660
|
+
promptTokens,
|
|
661
|
+
completionTokens,
|
|
662
|
+
},
|
|
663
|
+
structured,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
package/src/providers/index.ts
CHANGED
|
@@ -14,3 +14,6 @@ export type { OpenAIProviderOptions } from "./OpenAIProvider";
|
|
|
14
14
|
|
|
15
15
|
export { OpenRouterProvider } from "./OpenRouterProvider";
|
|
16
16
|
export type { OpenRouterProviderOptions } from "./OpenRouterProvider";
|
|
17
|
+
|
|
18
|
+
export { DeepSeekProvider } from "./DeepSeekProvider";
|
|
19
|
+
export type { DeepSeekProviderOptions } from "./DeepSeekProvider";
|
package/src/types/agent.ts
CHANGED
|
@@ -267,7 +267,7 @@ export interface Instruction<TContext = unknown, TData = unknown> {
|
|
|
267
267
|
*/
|
|
268
268
|
kind?: 'must' | 'never' | 'should';
|
|
269
269
|
/**
|
|
270
|
-
* AI-evaluated activation condition. String or array of strings (
|
|
270
|
+
* AI-evaluated activation condition. String or array of strings (OR semantics).
|
|
271
271
|
* Undefined = always active. Functions are NOT allowed here — use `if`.
|
|
272
272
|
*/
|
|
273
273
|
when?: ConditionWhen;
|
|
@@ -307,7 +307,8 @@ export interface ScopedInstructions<TContext = unknown, TData = unknown> {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
/**
|
|
310
|
-
* Observability record for an instruction that was
|
|
310
|
+
* Observability record for an instruction that was rendered into a turn's prompt.
|
|
311
|
+
* Textual `when` conditions are included in the prompt for the AI to evaluate.
|
|
311
312
|
* Deterministic — derived from rendering, not from LLM self-report.
|
|
312
313
|
*/
|
|
313
314
|
export interface AppliedInstruction {
|
|
@@ -329,7 +330,8 @@ export interface AgentResponse<TData = Record<string, unknown>> {
|
|
|
329
330
|
/** Why execution stopped (for multi-step execution) */
|
|
330
331
|
stoppedReason?: StoppedReason;
|
|
331
332
|
/**
|
|
332
|
-
* Instructions
|
|
333
|
+
* Instructions rendered into this turn's prompt after code-evaluated gates passed.
|
|
334
|
+
* Textual `when` conditions remain in the prompt for the AI to evaluate.
|
|
333
335
|
* Deterministic — derived from rendering, not from LLM self-report.
|
|
334
336
|
*/
|
|
335
337
|
appliedInstructions?: AppliedInstruction[];
|