@falai/agent 0.3.10 → 0.3.12
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 +192 -16
- package/dist/adapters/PrismaAdapter.d.ts +115 -0
- package/dist/adapters/PrismaAdapter.d.ts.map +1 -0
- package/dist/adapters/PrismaAdapter.js +331 -0
- package/dist/adapters/PrismaAdapter.js.map +1 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +5 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/cjs/adapters/PrismaAdapter.d.ts +115 -0
- package/dist/cjs/adapters/PrismaAdapter.d.ts.map +1 -0
- package/dist/cjs/adapters/PrismaAdapter.js +335 -0
- package/dist/cjs/adapters/PrismaAdapter.js.map +1 -0
- package/dist/cjs/adapters/index.d.ts +6 -0
- package/dist/cjs/adapters/index.d.ts.map +1 -0
- package/dist/cjs/adapters/index.js +9 -0
- package/dist/cjs/adapters/index.js.map +1 -0
- package/dist/cjs/core/Agent.d.ts +35 -0
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +153 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/PersistenceManager.d.ts +77 -0
- package/dist/cjs/core/PersistenceManager.d.ts.map +1 -0
- package/dist/cjs/core/PersistenceManager.js +153 -0
- package/dist/cjs/core/PersistenceManager.js.map +1 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +43 -0
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/cjs/providers/AnthropicProvider.js +328 -0
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -0
- package/dist/cjs/providers/GeminiProvider.d.ts +4 -1
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +96 -0
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts +4 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +115 -0
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts +4 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +115 -0
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/index.d.ts +13 -0
- package/dist/cjs/providers/index.d.ts.map +1 -0
- package/dist/cjs/providers/index.js +16 -0
- package/dist/cjs/providers/index.js.map +1 -0
- package/dist/cjs/types/agent.d.ts +3 -0
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/agent.js.map +1 -1
- package/dist/cjs/types/ai.d.ts +28 -0
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/persistence.d.ts +194 -0
- package/dist/cjs/types/persistence.d.ts.map +1 -0
- package/dist/cjs/types/persistence.js +7 -0
- package/dist/cjs/types/persistence.js.map +1 -0
- package/dist/core/Agent.d.ts +35 -0
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +153 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/PersistenceManager.d.ts +77 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -0
- package/dist/core/PersistenceManager.js +149 -0
- package/dist/core/PersistenceManager.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +43 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/providers/AnthropicProvider.js +321 -0
- package/dist/providers/AnthropicProvider.js.map +1 -0
- package/dist/providers/GeminiProvider.d.ts +4 -1
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +96 -0
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts +4 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +115 -0
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts +4 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +115 -0
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +9 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/types/agent.d.ts +3 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/agent.js.map +1 -1
- package/dist/types/ai.d.ts +28 -0
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/persistence.d.ts +194 -0
- package/dist/types/persistence.d.ts.map +1 -0
- package/dist/types/persistence.js +6 -0
- package/dist/types/persistence.js.map +1 -0
- package/docs/API_REFERENCE.md +260 -2
- package/docs/PERSISTENCE.md +419 -0
- package/docs/PROVIDERS.md +139 -2
- package/examples/business-onboarding.ts +5 -4
- package/examples/declarative-agent.ts +1 -1
- package/examples/domain-scoping.ts +5 -4
- package/examples/healthcare-agent.ts +4 -4
- package/examples/openai-agent.ts +6 -4
- package/examples/prisma-persistence.ts +313 -0
- package/examples/prisma-schema.example.prisma +74 -0
- package/examples/rules-prohibitions.ts +4 -4
- package/examples/streaming-agent.ts +371 -0
- package/examples/travel-agent.ts +7 -4
- package/package.json +10 -1
- package/src/adapters/PrismaAdapter.ts +510 -0
- package/src/adapters/index.ts +10 -0
- package/src/core/Agent.ts +205 -0
- package/src/core/PersistenceManager.ts +222 -0
- package/src/index.ts +23 -0
- package/src/providers/AnthropicProvider.ts +467 -0
- package/src/providers/GeminiProvider.ts +135 -0
- package/src/providers/OpenAIProvider.ts +157 -0
- package/src/providers/OpenRouterProvider.ts +157 -0
- package/src/providers/index.ts +16 -0
- package/src/types/agent.ts +3 -0
- package/src/types/ai.ts +32 -0
- package/src/types/index.ts +14 -0
- package/src/types/persistence.ts +234 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic (Claude) provider implementation with retry and backup models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
6
|
+
import type { MessageCreateParamsNonStreaming } from "@anthropic-ai/sdk/resources/messages";
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AiProvider,
|
|
10
|
+
GenerateMessageInput,
|
|
11
|
+
GenerateMessageOutput,
|
|
12
|
+
GenerateMessageStreamChunk,
|
|
13
|
+
AgentStructuredResponse,
|
|
14
|
+
} from "../types/ai";
|
|
15
|
+
import { withTimeoutAndRetry } from "../utils/retry";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
18
|
+
timeout: 60000,
|
|
19
|
+
retries: 3,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration options for Anthropic provider
|
|
24
|
+
* Uses types from @anthropic-ai/sdk package
|
|
25
|
+
*/
|
|
26
|
+
export interface AnthropicProviderOptions {
|
|
27
|
+
/** Anthropic API key */
|
|
28
|
+
apiKey: string;
|
|
29
|
+
/** Model to use (required) - e.g., "claude-sonnet-4-5", "claude-opus-4-1" */
|
|
30
|
+
model: string;
|
|
31
|
+
/** Backup models to try if primary fails (default: []) */
|
|
32
|
+
backupModels?: string[];
|
|
33
|
+
/** Default parameters - uses MessageCreateParamsNonStreaming from @anthropic-ai/sdk */
|
|
34
|
+
config?: Partial<Omit<MessageCreateParamsNonStreaming, "model" | "messages">>;
|
|
35
|
+
/** Retry configuration */
|
|
36
|
+
retryConfig?: {
|
|
37
|
+
timeout?: number;
|
|
38
|
+
retries?: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Type guard for errors with status/code properties
|
|
44
|
+
*/
|
|
45
|
+
interface ErrorWithStatus {
|
|
46
|
+
status?: number;
|
|
47
|
+
code?: string;
|
|
48
|
+
message?: string;
|
|
49
|
+
type?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Type guard to check if error is ErrorWithStatus
|
|
54
|
+
*/
|
|
55
|
+
function isErrorWithStatus(error: unknown): error is ErrorWithStatus {
|
|
56
|
+
return (
|
|
57
|
+
typeof error === "object" &&
|
|
58
|
+
error !== null &&
|
|
59
|
+
("status" in error || "code" in error || "message" in error)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Safely extract error message
|
|
65
|
+
*/
|
|
66
|
+
function getErrorMessage(error: unknown): string {
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
return error.message;
|
|
69
|
+
}
|
|
70
|
+
if (isErrorWithStatus(error) && error.message) {
|
|
71
|
+
return error.message;
|
|
72
|
+
}
|
|
73
|
+
return String(error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Determines if an error should trigger backup model usage
|
|
78
|
+
*/
|
|
79
|
+
const shouldUseBackupModel = (error: unknown): boolean => {
|
|
80
|
+
if (!isErrorWithStatus(error)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Server errors
|
|
85
|
+
if (error.status === 500 || error.status === 503 || error.status === 529) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Rate limiting
|
|
90
|
+
if (error.status === 429) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Model overloaded or unavailable
|
|
95
|
+
if (
|
|
96
|
+
error.type === "overloaded_error" ||
|
|
97
|
+
error.type === "api_error" ||
|
|
98
|
+
error.code === "overloaded"
|
|
99
|
+
) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const message = getErrorMessage(error);
|
|
104
|
+
if (
|
|
105
|
+
message.includes("overloaded") ||
|
|
106
|
+
message.includes("unavailable") ||
|
|
107
|
+
message.includes("internal error") ||
|
|
108
|
+
message.includes("Internal error")
|
|
109
|
+
) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Anthropic provider implementation with backup models and retry logic
|
|
118
|
+
*/
|
|
119
|
+
export class AnthropicProvider implements AiProvider {
|
|
120
|
+
public readonly name = "anthropic";
|
|
121
|
+
private client: Anthropic;
|
|
122
|
+
private primaryModel: string;
|
|
123
|
+
private backupModels: string[];
|
|
124
|
+
private config?: Partial<
|
|
125
|
+
Omit<MessageCreateParamsNonStreaming, "model" | "messages">
|
|
126
|
+
>;
|
|
127
|
+
private retryConfig: { timeout: number; retries: number };
|
|
128
|
+
|
|
129
|
+
constructor(options: AnthropicProviderOptions) {
|
|
130
|
+
const { apiKey, model, backupModels = [], config, retryConfig } = options;
|
|
131
|
+
|
|
132
|
+
if (!apiKey) {
|
|
133
|
+
throw new Error("Anthropic API key is required");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!model) {
|
|
137
|
+
throw new Error("Model is required. Example: 'claude-sonnet-4-5'");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.client = new Anthropic({
|
|
141
|
+
apiKey,
|
|
142
|
+
});
|
|
143
|
+
this.primaryModel = model;
|
|
144
|
+
this.backupModels = backupModels;
|
|
145
|
+
this.config = config;
|
|
146
|
+
this.retryConfig = {
|
|
147
|
+
timeout: retryConfig?.timeout || DEFAULT_RETRY_CONFIG.timeout,
|
|
148
|
+
retries: retryConfig?.retries || DEFAULT_RETRY_CONFIG.retries,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async generateMessage<TContext = unknown>(
|
|
153
|
+
input: GenerateMessageInput<TContext>
|
|
154
|
+
): Promise<GenerateMessageOutput> {
|
|
155
|
+
return this.generateWithBackup(input);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async *generateMessageStream<TContext = unknown>(
|
|
159
|
+
input: GenerateMessageInput<TContext>
|
|
160
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
161
|
+
yield* this.generateStreamWithBackup(input);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async generateWithBackup<TContext = unknown>(
|
|
165
|
+
input: GenerateMessageInput<TContext>
|
|
166
|
+
): Promise<GenerateMessageOutput> {
|
|
167
|
+
// Try primary model first
|
|
168
|
+
try {
|
|
169
|
+
return await this.generateWithModel(this.primaryModel, input);
|
|
170
|
+
} catch (primaryError: unknown) {
|
|
171
|
+
const primaryErrMsg = getErrorMessage(primaryError);
|
|
172
|
+
console.warn(
|
|
173
|
+
`[ANTHROPIC] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!shouldUseBackupModel(primaryError)) {
|
|
177
|
+
throw primaryError;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`[ANTHROPIC] Trying backup models`);
|
|
181
|
+
|
|
182
|
+
let lastBackupError: unknown = primaryError;
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < this.backupModels.length; i++) {
|
|
185
|
+
const backupModel = this.backupModels[i];
|
|
186
|
+
console.log(
|
|
187
|
+
`[ANTHROPIC] Trying backup model ${i + 1}/${
|
|
188
|
+
this.backupModels.length
|
|
189
|
+
}: ${backupModel}`
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await this.generateWithModel(backupModel, input);
|
|
194
|
+
console.log(`[ANTHROPIC] Backup model ${backupModel} succeeded`);
|
|
195
|
+
return result;
|
|
196
|
+
} catch (backupError: unknown) {
|
|
197
|
+
const backupErrMsg = getErrorMessage(backupError);
|
|
198
|
+
console.warn(
|
|
199
|
+
`[ANTHROPIC] Backup model ${backupModel} failed: ${backupErrMsg}`
|
|
200
|
+
);
|
|
201
|
+
lastBackupError = backupError;
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
!shouldUseBackupModel(backupError) &&
|
|
205
|
+
i < this.backupModels.length - 1
|
|
206
|
+
) {
|
|
207
|
+
console.log(
|
|
208
|
+
`[ANTHROPIC] Backup model error doesn't qualify for further attempts`
|
|
209
|
+
);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const lastBackupErrMsg = getErrorMessage(lastBackupError);
|
|
216
|
+
console.error(
|
|
217
|
+
`[ANTHROPIC] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
|
|
218
|
+
);
|
|
219
|
+
throw lastBackupError;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async generateWithModel<TContext = unknown>(
|
|
224
|
+
model: string,
|
|
225
|
+
input: GenerateMessageInput<TContext>
|
|
226
|
+
): Promise<GenerateMessageOutput> {
|
|
227
|
+
const operation = async (): Promise<GenerateMessageOutput> => {
|
|
228
|
+
// Anthropic requires max_tokens to be specified
|
|
229
|
+
const maxTokens = input.parameters?.maxOutputTokens || 4096;
|
|
230
|
+
|
|
231
|
+
const params: MessageCreateParamsNonStreaming = {
|
|
232
|
+
model,
|
|
233
|
+
max_tokens: maxTokens,
|
|
234
|
+
messages: [
|
|
235
|
+
{
|
|
236
|
+
role: "user",
|
|
237
|
+
content: input.prompt,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
...this.config,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Handle JSON mode if requested
|
|
244
|
+
// Note: Anthropic doesn't have a native JSON mode like OpenAI,
|
|
245
|
+
// but we can add it to the system prompt
|
|
246
|
+
if (input.parameters?.jsonMode) {
|
|
247
|
+
// Add system message requesting JSON output
|
|
248
|
+
const systemPrompt =
|
|
249
|
+
"You must respond with valid JSON only. Do not include any text outside the JSON structure.";
|
|
250
|
+
|
|
251
|
+
// Merge with existing system if present
|
|
252
|
+
if (typeof this.config?.system === "string") {
|
|
253
|
+
params.system = `${this.config.system}\n\n${systemPrompt}`;
|
|
254
|
+
} else if (Array.isArray(this.config?.system)) {
|
|
255
|
+
params.system = [
|
|
256
|
+
...this.config.system,
|
|
257
|
+
{
|
|
258
|
+
type: "text" as const,
|
|
259
|
+
text: systemPrompt,
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
} else {
|
|
263
|
+
params.system = systemPrompt;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const response = await this.client.messages.create(params);
|
|
268
|
+
|
|
269
|
+
// Extract text from response
|
|
270
|
+
const textContent = response.content.find(
|
|
271
|
+
(block) => block.type === "text"
|
|
272
|
+
);
|
|
273
|
+
const message = textContent?.type === "text" ? textContent.text : "";
|
|
274
|
+
|
|
275
|
+
if (!message) {
|
|
276
|
+
throw new Error("No response from Anthropic");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Parse JSON response if JSON mode was enabled
|
|
280
|
+
let structured: AgentStructuredResponse | undefined;
|
|
281
|
+
if (input.parameters?.jsonMode) {
|
|
282
|
+
try {
|
|
283
|
+
structured = JSON.parse(message) as AgentStructuredResponse;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.warn("[ANTHROPIC] Failed to parse JSON response:", error);
|
|
286
|
+
// Fall back to treating the message as plain text
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
message,
|
|
292
|
+
metadata: {
|
|
293
|
+
model: response.model,
|
|
294
|
+
stopReason: response.stop_reason,
|
|
295
|
+
tokensUsed:
|
|
296
|
+
response.usage.input_tokens + response.usage.output_tokens,
|
|
297
|
+
promptTokens: response.usage.input_tokens,
|
|
298
|
+
completionTokens: response.usage.output_tokens,
|
|
299
|
+
},
|
|
300
|
+
structured,
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return withTimeoutAndRetry(
|
|
305
|
+
operation,
|
|
306
|
+
this.retryConfig.timeout,
|
|
307
|
+
this.retryConfig.retries,
|
|
308
|
+
`Anthropic ${model}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private async *generateStreamWithBackup<TContext = unknown>(
|
|
313
|
+
input: GenerateMessageInput<TContext>
|
|
314
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
315
|
+
// Try primary model first
|
|
316
|
+
try {
|
|
317
|
+
yield* this.generateStreamWithModel(this.primaryModel, input);
|
|
318
|
+
} catch (primaryError: unknown) {
|
|
319
|
+
const primaryErrMsg = getErrorMessage(primaryError);
|
|
320
|
+
console.warn(
|
|
321
|
+
`[ANTHROPIC] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (!shouldUseBackupModel(primaryError)) {
|
|
325
|
+
throw primaryError;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`[ANTHROPIC] Trying backup models for streaming`);
|
|
329
|
+
|
|
330
|
+
let lastBackupError: unknown = primaryError;
|
|
331
|
+
|
|
332
|
+
for (let i = 0; i < this.backupModels.length; i++) {
|
|
333
|
+
const backupModel = this.backupModels[i];
|
|
334
|
+
console.log(
|
|
335
|
+
`[ANTHROPIC] Trying backup model ${i + 1}/${
|
|
336
|
+
this.backupModels.length
|
|
337
|
+
}: ${backupModel}`
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
yield* this.generateStreamWithModel(backupModel, input);
|
|
342
|
+
console.log(`[ANTHROPIC] Backup model ${backupModel} succeeded`);
|
|
343
|
+
return;
|
|
344
|
+
} catch (backupError: unknown) {
|
|
345
|
+
const backupErrMsg = getErrorMessage(backupError);
|
|
346
|
+
console.warn(
|
|
347
|
+
`[ANTHROPIC] Backup model ${backupModel} failed: ${backupErrMsg}`
|
|
348
|
+
);
|
|
349
|
+
lastBackupError = backupError;
|
|
350
|
+
|
|
351
|
+
if (
|
|
352
|
+
!shouldUseBackupModel(backupError) &&
|
|
353
|
+
i < this.backupModels.length - 1
|
|
354
|
+
) {
|
|
355
|
+
console.log(
|
|
356
|
+
`[ANTHROPIC] Backup model error doesn't qualify for further attempts`
|
|
357
|
+
);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const lastBackupErrMsg = getErrorMessage(lastBackupError);
|
|
364
|
+
console.error(
|
|
365
|
+
`[ANTHROPIC] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
|
|
366
|
+
);
|
|
367
|
+
throw lastBackupError;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async *generateStreamWithModel<TContext = unknown>(
|
|
372
|
+
model: string,
|
|
373
|
+
input: GenerateMessageInput<TContext>
|
|
374
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
375
|
+
// Anthropic requires max_tokens to be specified
|
|
376
|
+
const maxTokens = input.parameters?.maxOutputTokens || 4096;
|
|
377
|
+
|
|
378
|
+
const params = {
|
|
379
|
+
model,
|
|
380
|
+
max_tokens: maxTokens,
|
|
381
|
+
messages: [
|
|
382
|
+
{
|
|
383
|
+
role: "user" as const,
|
|
384
|
+
content: input.prompt,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
stream: true,
|
|
388
|
+
...this.config,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Handle JSON mode if requested
|
|
392
|
+
if (input.parameters?.jsonMode) {
|
|
393
|
+
const systemPrompt =
|
|
394
|
+
"You must respond with valid JSON only. Do not include any text outside the JSON structure.";
|
|
395
|
+
|
|
396
|
+
if (typeof this.config?.system === "string") {
|
|
397
|
+
params.system = `${this.config.system}\n\n${systemPrompt}`;
|
|
398
|
+
} else if (Array.isArray(this.config?.system)) {
|
|
399
|
+
params.system = [
|
|
400
|
+
...this.config.system,
|
|
401
|
+
{
|
|
402
|
+
type: "text" as const,
|
|
403
|
+
text: systemPrompt,
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
} else {
|
|
407
|
+
params.system = systemPrompt;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const stream = this.client.messages.stream(params);
|
|
412
|
+
|
|
413
|
+
let accumulated = "";
|
|
414
|
+
let currentModel = model;
|
|
415
|
+
let stopReason: string | undefined;
|
|
416
|
+
let inputTokens = 0;
|
|
417
|
+
let outputTokens = 0;
|
|
418
|
+
|
|
419
|
+
for await (const chunk of stream) {
|
|
420
|
+
if (chunk.type === "message_start") {
|
|
421
|
+
currentModel = chunk.message.model;
|
|
422
|
+
inputTokens = chunk.message.usage.input_tokens;
|
|
423
|
+
} else if (chunk.type === "content_block_delta") {
|
|
424
|
+
if (chunk.delta.type === "text_delta") {
|
|
425
|
+
const delta = chunk.delta.text;
|
|
426
|
+
accumulated += delta;
|
|
427
|
+
yield {
|
|
428
|
+
delta,
|
|
429
|
+
accumulated,
|
|
430
|
+
done: false,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
} else if (chunk.type === "message_delta") {
|
|
434
|
+
stopReason = chunk.delta.stop_reason || undefined;
|
|
435
|
+
outputTokens = chunk.usage.output_tokens;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Parse JSON response if JSON mode was enabled
|
|
440
|
+
let structured: AgentStructuredResponse | undefined;
|
|
441
|
+
if (input.parameters?.jsonMode && accumulated) {
|
|
442
|
+
try {
|
|
443
|
+
structured = JSON.parse(accumulated) as AgentStructuredResponse;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.warn(
|
|
446
|
+
"[ANTHROPIC] Failed to parse JSON response in stream:",
|
|
447
|
+
error
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Yield final chunk
|
|
453
|
+
yield {
|
|
454
|
+
delta: "",
|
|
455
|
+
accumulated,
|
|
456
|
+
done: true,
|
|
457
|
+
metadata: {
|
|
458
|
+
model: currentModel,
|
|
459
|
+
stopReason,
|
|
460
|
+
tokensUsed: inputTokens + outputTokens,
|
|
461
|
+
promptTokens: inputTokens,
|
|
462
|
+
completionTokens: outputTokens,
|
|
463
|
+
},
|
|
464
|
+
structured,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
AiProvider,
|
|
14
14
|
GenerateMessageInput,
|
|
15
15
|
GenerateMessageOutput,
|
|
16
|
+
GenerateMessageStreamChunk,
|
|
16
17
|
AgentStructuredResponse,
|
|
17
18
|
} from "../types/ai";
|
|
18
19
|
import { withTimeoutAndRetry } from "../utils/retry";
|
|
@@ -124,6 +125,12 @@ export class GeminiProvider implements AiProvider {
|
|
|
124
125
|
return this.generateWithBackup(input);
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
async *generateMessageStream<TContext = unknown>(
|
|
129
|
+
input: GenerateMessageInput<TContext>
|
|
130
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
131
|
+
yield* this.generateStreamWithBackup(input);
|
|
132
|
+
}
|
|
133
|
+
|
|
127
134
|
private async generateWithBackup<TContext = unknown>(
|
|
128
135
|
input: GenerateMessageInput<TContext>
|
|
129
136
|
): Promise<GenerateMessageOutput> {
|
|
@@ -236,4 +243,132 @@ export class GeminiProvider implements AiProvider {
|
|
|
236
243
|
`Gemini ${model}`
|
|
237
244
|
);
|
|
238
245
|
}
|
|
246
|
+
|
|
247
|
+
private async *generateStreamWithBackup<TContext = unknown>(
|
|
248
|
+
input: GenerateMessageInput<TContext>
|
|
249
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
250
|
+
// Try primary model first
|
|
251
|
+
try {
|
|
252
|
+
yield* this.generateStreamWithModel(this.primaryModel, input);
|
|
253
|
+
} catch (primaryError: unknown) {
|
|
254
|
+
const primaryErrMsg = String(primaryError);
|
|
255
|
+
console.warn(
|
|
256
|
+
`[GEMINI] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (!shouldUseBackupModel(primaryError)) {
|
|
260
|
+
throw primaryError;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(`[GEMINI] Trying backup models for streaming`);
|
|
264
|
+
|
|
265
|
+
let lastBackupError: unknown = primaryError;
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < this.backupModels.length; i++) {
|
|
268
|
+
const backupModel = this.backupModels[i];
|
|
269
|
+
console.log(
|
|
270
|
+
`[GEMINI] Trying backup model ${i + 1}/${
|
|
271
|
+
this.backupModels.length
|
|
272
|
+
}: ${backupModel}`
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
yield* this.generateStreamWithModel(backupModel, input);
|
|
277
|
+
console.log(`[GEMINI] Backup model ${backupModel} succeeded`);
|
|
278
|
+
return;
|
|
279
|
+
} catch (backupError: unknown) {
|
|
280
|
+
const backupErrMsg = String(backupError);
|
|
281
|
+
console.warn(
|
|
282
|
+
`[GEMINI] Backup model ${backupModel} failed: ${backupErrMsg}`
|
|
283
|
+
);
|
|
284
|
+
lastBackupError = backupError;
|
|
285
|
+
|
|
286
|
+
if (
|
|
287
|
+
!shouldUseBackupModel(backupError) &&
|
|
288
|
+
i < this.backupModels.length - 1
|
|
289
|
+
) {
|
|
290
|
+
console.log(
|
|
291
|
+
`[GEMINI] Backup model error doesn't qualify for further attempts`
|
|
292
|
+
);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const lastBackupErrMsg = String(lastBackupError);
|
|
299
|
+
console.error(
|
|
300
|
+
`[GEMINI] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
|
|
301
|
+
);
|
|
302
|
+
throw lastBackupError;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async *generateStreamWithModel<TContext = unknown>(
|
|
307
|
+
model: string,
|
|
308
|
+
input: GenerateMessageInput<TContext>
|
|
309
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
310
|
+
// Enable JSON mode if requested
|
|
311
|
+
const configOverride: Partial<GenerateContentConfig> = { ...this.config };
|
|
312
|
+
if (input.parameters?.jsonMode) {
|
|
313
|
+
configOverride.responseMimeType = "application/json";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const stream = await this.genAI.models.generateContentStream({
|
|
317
|
+
model,
|
|
318
|
+
contents: input.prompt,
|
|
319
|
+
config: configOverride,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
let accumulated = "";
|
|
323
|
+
let promptTokenCount = 0;
|
|
324
|
+
let candidatesTokenCount = 0;
|
|
325
|
+
let totalTokenCount = 0;
|
|
326
|
+
|
|
327
|
+
for await (const chunk of stream) {
|
|
328
|
+
const delta = chunk.text || "";
|
|
329
|
+
|
|
330
|
+
if (delta) {
|
|
331
|
+
accumulated += delta;
|
|
332
|
+
yield {
|
|
333
|
+
delta,
|
|
334
|
+
accumulated,
|
|
335
|
+
done: false,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Update token counts if available
|
|
340
|
+
if (chunk.usageMetadata) {
|
|
341
|
+
promptTokenCount = chunk.usageMetadata.promptTokenCount || 0;
|
|
342
|
+
candidatesTokenCount = chunk.usageMetadata.candidatesTokenCount || 0;
|
|
343
|
+
totalTokenCount = chunk.usageMetadata.totalTokenCount || 0;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Parse JSON response if JSON mode was enabled
|
|
348
|
+
let structured: AgentStructuredResponse | undefined;
|
|
349
|
+
if (input.parameters?.jsonMode && accumulated) {
|
|
350
|
+
try {
|
|
351
|
+
structured = JSON.parse(accumulated) as AgentStructuredResponse;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
console.warn(
|
|
354
|
+
"[GEMINI] Failed to parse JSON response in stream:",
|
|
355
|
+
error
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Yield final chunk
|
|
361
|
+
yield {
|
|
362
|
+
delta: "",
|
|
363
|
+
accumulated,
|
|
364
|
+
done: true,
|
|
365
|
+
metadata: {
|
|
366
|
+
model,
|
|
367
|
+
tokensUsed: totalTokenCount,
|
|
368
|
+
promptTokens: promptTokenCount,
|
|
369
|
+
completionTokens: candidatesTokenCount,
|
|
370
|
+
},
|
|
371
|
+
structured,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
239
374
|
}
|