@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.
Files changed (131) hide show
  1. package/README.md +192 -16
  2. package/dist/adapters/PrismaAdapter.d.ts +115 -0
  3. package/dist/adapters/PrismaAdapter.d.ts.map +1 -0
  4. package/dist/adapters/PrismaAdapter.js +331 -0
  5. package/dist/adapters/PrismaAdapter.js.map +1 -0
  6. package/dist/adapters/index.d.ts +6 -0
  7. package/dist/adapters/index.d.ts.map +1 -0
  8. package/dist/adapters/index.js +5 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/cjs/adapters/PrismaAdapter.d.ts +115 -0
  11. package/dist/cjs/adapters/PrismaAdapter.d.ts.map +1 -0
  12. package/dist/cjs/adapters/PrismaAdapter.js +335 -0
  13. package/dist/cjs/adapters/PrismaAdapter.js.map +1 -0
  14. package/dist/cjs/adapters/index.d.ts +6 -0
  15. package/dist/cjs/adapters/index.d.ts.map +1 -0
  16. package/dist/cjs/adapters/index.js +9 -0
  17. package/dist/cjs/adapters/index.js.map +1 -0
  18. package/dist/cjs/core/Agent.d.ts +35 -0
  19. package/dist/cjs/core/Agent.d.ts.map +1 -1
  20. package/dist/cjs/core/Agent.js +153 -0
  21. package/dist/cjs/core/Agent.js.map +1 -1
  22. package/dist/cjs/core/PersistenceManager.d.ts +77 -0
  23. package/dist/cjs/core/PersistenceManager.d.ts.map +1 -0
  24. package/dist/cjs/core/PersistenceManager.js +153 -0
  25. package/dist/cjs/core/PersistenceManager.js.map +1 -0
  26. package/dist/cjs/index.d.ts +6 -0
  27. package/dist/cjs/index.d.ts.map +1 -1
  28. package/dist/cjs/index.js +8 -1
  29. package/dist/cjs/index.js.map +1 -1
  30. package/dist/cjs/providers/AnthropicProvider.d.ts +43 -0
  31. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -0
  32. package/dist/cjs/providers/AnthropicProvider.js +328 -0
  33. package/dist/cjs/providers/AnthropicProvider.js.map +1 -0
  34. package/dist/cjs/providers/GeminiProvider.d.ts +4 -1
  35. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  36. package/dist/cjs/providers/GeminiProvider.js +96 -0
  37. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  38. package/dist/cjs/providers/OpenAIProvider.d.ts +4 -1
  39. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  40. package/dist/cjs/providers/OpenAIProvider.js +115 -0
  41. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  42. package/dist/cjs/providers/OpenRouterProvider.d.ts +4 -1
  43. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  44. package/dist/cjs/providers/OpenRouterProvider.js +115 -0
  45. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  46. package/dist/cjs/providers/index.d.ts +13 -0
  47. package/dist/cjs/providers/index.d.ts.map +1 -0
  48. package/dist/cjs/providers/index.js +16 -0
  49. package/dist/cjs/providers/index.js.map +1 -0
  50. package/dist/cjs/types/agent.d.ts +3 -0
  51. package/dist/cjs/types/agent.d.ts.map +1 -1
  52. package/dist/cjs/types/agent.js.map +1 -1
  53. package/dist/cjs/types/ai.d.ts +28 -0
  54. package/dist/cjs/types/ai.d.ts.map +1 -1
  55. package/dist/cjs/types/index.d.ts +1 -0
  56. package/dist/cjs/types/index.d.ts.map +1 -1
  57. package/dist/cjs/types/persistence.d.ts +194 -0
  58. package/dist/cjs/types/persistence.d.ts.map +1 -0
  59. package/dist/cjs/types/persistence.js +7 -0
  60. package/dist/cjs/types/persistence.js.map +1 -0
  61. package/dist/core/Agent.d.ts +35 -0
  62. package/dist/core/Agent.d.ts.map +1 -1
  63. package/dist/core/Agent.js +153 -0
  64. package/dist/core/Agent.js.map +1 -1
  65. package/dist/core/PersistenceManager.d.ts +77 -0
  66. package/dist/core/PersistenceManager.d.ts.map +1 -0
  67. package/dist/core/PersistenceManager.js +149 -0
  68. package/dist/core/PersistenceManager.js.map +1 -0
  69. package/dist/index.d.ts +6 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +4 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/providers/AnthropicProvider.d.ts +43 -0
  74. package/dist/providers/AnthropicProvider.d.ts.map +1 -0
  75. package/dist/providers/AnthropicProvider.js +321 -0
  76. package/dist/providers/AnthropicProvider.js.map +1 -0
  77. package/dist/providers/GeminiProvider.d.ts +4 -1
  78. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  79. package/dist/providers/GeminiProvider.js +96 -0
  80. package/dist/providers/GeminiProvider.js.map +1 -1
  81. package/dist/providers/OpenAIProvider.d.ts +4 -1
  82. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  83. package/dist/providers/OpenAIProvider.js +115 -0
  84. package/dist/providers/OpenAIProvider.js.map +1 -1
  85. package/dist/providers/OpenRouterProvider.d.ts +4 -1
  86. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  87. package/dist/providers/OpenRouterProvider.js +115 -0
  88. package/dist/providers/OpenRouterProvider.js.map +1 -1
  89. package/dist/providers/index.d.ts +13 -0
  90. package/dist/providers/index.d.ts.map +1 -0
  91. package/dist/providers/index.js +9 -0
  92. package/dist/providers/index.js.map +1 -0
  93. package/dist/types/agent.d.ts +3 -0
  94. package/dist/types/agent.d.ts.map +1 -1
  95. package/dist/types/agent.js.map +1 -1
  96. package/dist/types/ai.d.ts +28 -0
  97. package/dist/types/ai.d.ts.map +1 -1
  98. package/dist/types/index.d.ts +1 -0
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/types/persistence.d.ts +194 -0
  101. package/dist/types/persistence.d.ts.map +1 -0
  102. package/dist/types/persistence.js +6 -0
  103. package/dist/types/persistence.js.map +1 -0
  104. package/docs/API_REFERENCE.md +260 -2
  105. package/docs/PERSISTENCE.md +419 -0
  106. package/docs/PROVIDERS.md +139 -2
  107. package/examples/business-onboarding.ts +5 -4
  108. package/examples/declarative-agent.ts +1 -1
  109. package/examples/domain-scoping.ts +5 -4
  110. package/examples/healthcare-agent.ts +4 -4
  111. package/examples/openai-agent.ts +6 -4
  112. package/examples/prisma-persistence.ts +313 -0
  113. package/examples/prisma-schema.example.prisma +74 -0
  114. package/examples/rules-prohibitions.ts +4 -4
  115. package/examples/streaming-agent.ts +371 -0
  116. package/examples/travel-agent.ts +7 -4
  117. package/package.json +10 -1
  118. package/src/adapters/PrismaAdapter.ts +510 -0
  119. package/src/adapters/index.ts +10 -0
  120. package/src/core/Agent.ts +205 -0
  121. package/src/core/PersistenceManager.ts +222 -0
  122. package/src/index.ts +23 -0
  123. package/src/providers/AnthropicProvider.ts +467 -0
  124. package/src/providers/GeminiProvider.ts +135 -0
  125. package/src/providers/OpenAIProvider.ts +157 -0
  126. package/src/providers/OpenRouterProvider.ts +157 -0
  127. package/src/providers/index.ts +16 -0
  128. package/src/types/agent.ts +3 -0
  129. package/src/types/ai.ts +32 -0
  130. package/src/types/index.ts +14 -0
  131. 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
  }