@wabot-dev/framework 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/src/addon/async/pg/PgCronJobRepository.js +2 -0
  2. package/dist/src/addon/async/pg/PgJobRepository.js +2 -0
  3. package/dist/src/addon/auth/api-key/@apiKeyGuard.js +2 -2
  4. package/dist/src/addon/auth/api-key/PgApiKeyRepository.js +3 -1
  5. package/dist/src/addon/auth/jwt/@jwtGuard.js +2 -2
  6. package/dist/src/addon/auth/jwt/PgJwtRefreshTokenRepository.js +2 -0
  7. package/dist/src/addon/chat-bot/anthropic/AnthropicChatAdapter.js +38 -14
  8. package/dist/src/addon/chat-bot/deepseek/DeepSeekChatAdapter.js +42 -13
  9. package/dist/src/addon/chat-bot/google/GoogleChatAdapter.js +77 -26
  10. package/dist/src/addon/chat-bot/openia/OpenaiChatAdapter.js +31 -9
  11. package/dist/src/addon/chat-bot/openrouter/OpenRouterChatAdapter.js +23 -4
  12. package/dist/src/addon/chat-bot/pg/PgChatMemory.js +6 -1
  13. package/dist/src/addon/chat-bot/pg/PgChatRepository.js +5 -0
  14. package/dist/src/addon/chat-bot/ram/RamChatRepository.js +3 -0
  15. package/dist/src/addon/chat-bot/wabot/WabotChatAdapter.js +9 -0
  16. package/dist/src/addon/chat-controller/cmd/CmdChannel.js +3 -3
  17. package/dist/src/addon/chat-controller/wasender/WasenderWebhookController.js +1 -1
  18. package/dist/src/addon/chat-controller/whatsapp/PgWhatsAppRepository.js +2 -0
  19. package/dist/src/addon/chat-controller/whatsapp/WhatsAppSender.js +3 -0
  20. package/dist/src/addon/chat-controller/whatsapp/cloud-api/WhatsAppSenderByCloudApi.js +3 -0
  21. package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppSenderByWabotProxy.js +3 -0
  22. package/dist/src/feature/chat-bot/Chat.js +2 -1
  23. package/dist/src/feature/chat-bot/ChatAdapterRegistry.js +26 -0
  24. package/dist/src/feature/chat-bot/ChatBot.js +9 -6
  25. package/dist/src/feature/chat-bot/UnionChatAdapter.js +64 -0
  26. package/dist/src/feature/chat-bot/isChatMessageEmpty.js +1 -1
  27. package/dist/src/feature/chat-bot/isRetryableError.js +63 -0
  28. package/dist/src/feature/chat-bot/metadata/@chatAdapter.js +11 -0
  29. package/dist/src/feature/chat-bot/metadata/ChatAdapterMetadataStore.js +17 -0
  30. package/dist/src/feature/chat-bot/runChatAdapters.js +22 -0
  31. package/dist/src/feature/chat-controller/ChatResolver.js +3 -0
  32. package/dist/src/feature/chat-controller/runChatControllers.js +3 -0
  33. package/dist/src/feature/http/HttpServerProvider.js +1 -1
  34. package/dist/src/feature/mindset/IMindset.js +4 -0
  35. package/dist/src/feature/mindset/MindsetOperator.js +30 -2
  36. package/dist/src/feature/pg/pgStorage.js +1 -1
  37. package/dist/src/feature/pg/query/@pgJsonRepository.js +73 -0
  38. package/dist/src/feature/pg/query/@query.js +14 -0
  39. package/dist/src/feature/pg/query/PgJsonRepository.js +23 -0
  40. package/dist/src/feature/pg/query/PgRepositoryMetadataStore.js +44 -0
  41. package/dist/src/feature/pg/query/buildQuerySql.js +164 -0
  42. package/dist/src/feature/pg/query/parseQueryMethodName.js +151 -0
  43. package/dist/src/feature/pg/withPgClient.js +1 -1
  44. package/dist/src/feature/rest-controller/runRestControllers.js +2 -2
  45. package/dist/src/index.d.ts +134 -17
  46. package/dist/src/index.js +14 -2
  47. package/dist/src/node_modules/cron-parser/dist/CronFileParser.js +2 -2
  48. package/package.json +1 -1
@@ -7,6 +7,8 @@ import 'debug';
7
7
  import 'node:crypto';
8
8
  import '../../../feature/pg/withPgClient.js';
9
9
  import '../../../feature/pg/pgStorage.js';
10
+ import '../../../feature/pg/query/PgJsonRepository.js';
11
+ import '../../../feature/pg/query/PgRepositoryMetadataStore.js';
10
12
  import { Pool } from 'pg';
11
13
 
12
14
  let PgCronJobRepository = class PgCronJobRepository extends PgCrudRepository {
@@ -7,6 +7,8 @@ import 'debug';
7
7
  import 'node:crypto';
8
8
  import { withPgClient } from '../../../feature/pg/withPgClient.js';
9
9
  import '../../../feature/pg/pgStorage.js';
10
+ import '../../../feature/pg/query/PgJsonRepository.js';
11
+ import '../../../feature/pg/query/PgRepositoryMetadataStore.js';
10
12
  import '../../../feature/async/AsyncMetadataStore.js';
11
13
  import '../../../feature/async/Async.js';
12
14
  import '../../../_virtual/index.js';
@@ -6,8 +6,8 @@ import 'debug';
6
6
  import '../../../core/validation/metadata/ValidationMetadataStore.js';
7
7
  import '../../../feature/express/ExpressProvider.js';
8
8
  import 'express';
9
- import 'path';
10
- import 'http';
9
+ import 'node:path';
10
+ import 'node:http';
11
11
  import { ApiKeyGuardMiddleware } from './ApiKeyGuardMiddleware.js';
12
12
 
13
13
  function apiKeyGuard() {
@@ -6,10 +6,12 @@ import { CustomError } from '../../../core/error/CustomError.js';
6
6
  import 'node:crypto';
7
7
  import '../../../feature/pg/withPgClient.js';
8
8
  import '../../../feature/pg/pgStorage.js';
9
+ import { singleton } from '../../../core/injection/index.js';
10
+ import '../../../feature/pg/query/PgJsonRepository.js';
11
+ import '../../../feature/pg/query/PgRepositoryMetadataStore.js';
9
12
  import { Pool } from 'pg';
10
13
  import { ApiKey } from './ApiKey.js';
11
14
  import '../../../core/error/setupErrorHandlers.js';
12
- import { singleton } from '../../../core/injection/index.js';
13
15
 
14
16
  let PgApiKeyRepository = class PgApiKeyRepository extends PgCrudRepository {
15
17
  constructor(pool) {
@@ -6,8 +6,8 @@ import 'debug';
6
6
  import '../../../core/validation/metadata/ValidationMetadataStore.js';
7
7
  import '../../../feature/express/ExpressProvider.js';
8
8
  import 'express';
9
- import 'path';
10
- import 'http';
9
+ import 'node:path';
10
+ import 'node:http';
11
11
  import { JwtGuardMiddleware } from './JwtGuardMiddleware.js';
12
12
 
13
13
  function jwtGuard() {
@@ -8,6 +8,8 @@ import 'debug';
8
8
  import 'node:crypto';
9
9
  import '../../../feature/pg/withPgClient.js';
10
10
  import '../../../feature/pg/pgStorage.js';
11
+ import '../../../feature/pg/query/PgJsonRepository.js';
12
+ import '../../../feature/pg/query/PgRepositoryMetadataStore.js';
11
13
  import { Pool } from 'pg';
12
14
  import { JwtRefreshToken } from './JwtRefreshToken.js';
13
15
 
@@ -2,13 +2,18 @@ import { __decorate, __metadata } from 'tslib';
2
2
  import { Env } from '../../../core/env/Env.js';
3
3
  import { singleton } from '../../../core/injection/index.js';
4
4
  import { Logger } from '../../../core/logger/Logger.js';
5
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
5
6
  import '../../../feature/chat-bot/ChatBot.js';
6
7
  import '../../../feature/chat-bot/ChatOperator.js';
8
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
9
+ import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
10
+ import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
11
+ import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
12
+ import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
7
13
  import 'uuid';
8
14
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
15
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
9
16
  import { safeJsonParse } from '../../../feature/chat-bot/safeJsonParse.js';
10
- import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
11
- import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
12
17
  import { Anthropic } from '@anthropic-ai/sdk';
13
18
 
14
19
  const ANTHROPIC_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -30,16 +35,28 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
30
35
  async nextItems(req) {
31
36
  const tools = req.tools.map((x) => this.mapTool(x));
32
37
  const messages = this.mapChatItems(req.prevItems);
33
- const request = {
34
- model: req.model,
35
- max_tokens: 4096,
36
- system: req.systemPrompt,
37
- messages,
38
- tools: tools.length > 0 ? tools : undefined,
39
- };
40
- this.logger.debug(`Call Claude API with model: ${request.model}, messages: ${request.messages.length}, tools: ${request.tools?.length ?? 0}`);
41
- const response = await this.anthropic.messages.create(request);
42
- return this.mapResponse(response);
38
+ let lastError;
39
+ for (const ref of req.models) {
40
+ const request = {
41
+ model: ref.model,
42
+ max_tokens: 4096,
43
+ system: req.systemPrompt,
44
+ messages,
45
+ tools: tools.length > 0 ? tools : undefined,
46
+ };
47
+ this.logger.debug(`Call Claude API with model: ${request.model}, messages: ${request.messages.length}, tools: ${request.tools?.length ?? 0}`);
48
+ try {
49
+ const response = await this.anthropic.messages.create(request);
50
+ return this.mapResponse(response, ref.model);
51
+ }
52
+ catch (err) {
53
+ if (!isRetryableError(err))
54
+ throw err;
55
+ this.logger.warn(`Anthropic model '${ref.model}' failed with retryable error, trying next`, err instanceof Error ? { message: err.message } : { err });
56
+ lastError = err;
57
+ }
58
+ }
59
+ throw lastError ?? new Error('No Anthropic model could handle the request');
43
60
  }
44
61
  mapChatItems(chatItems) {
45
62
  const messages = [];
@@ -158,12 +175,18 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
158
175
  },
159
176
  };
160
177
  }
161
- mapResponse(response) {
178
+ mapResponse(response, modelName) {
162
179
  let usage;
163
180
  if (response.usage) {
181
+ const cacheRead = response.usage.cache_read_input_tokens ?? 0;
182
+ const cacheWrite = response.usage.cache_creation_input_tokens ?? 0;
164
183
  usage = {
165
- inputTokens: response.usage.input_tokens,
184
+ inputTokens: response.usage.input_tokens + cacheRead + cacheWrite,
166
185
  outputTokens: response.usage.output_tokens,
186
+ cacheReadTokens: cacheRead || undefined,
187
+ cacheWriteTokens: cacheWrite || undefined,
188
+ provider: 'anthropic',
189
+ model: response.model ?? modelName,
167
190
  };
168
191
  }
169
192
  else {
@@ -189,6 +212,7 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
189
212
  }
190
213
  };
191
214
  AnthropicChatAdapter = __decorate([
215
+ chatAdapter({ provider: 'anthropic' }),
192
216
  singleton(),
193
217
  __metadata("design:paramtypes", [Env])
194
218
  ], AnthropicChatAdapter);
@@ -1,15 +1,21 @@
1
+ import { __decorate, __metadata } from 'tslib';
2
+ import { singleton } from '../../../core/injection/index.js';
1
3
  import { Logger } from '../../../core/logger/Logger.js';
4
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
2
5
  import '../../../feature/chat-bot/ChatBot.js';
3
6
  import '../../../feature/chat-bot/ChatOperator.js';
4
- import '../../../core/injection/index.js';
7
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
8
+ import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
9
+ import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
10
+ import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
11
+ import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
5
12
  import 'uuid';
6
13
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
14
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
7
15
  import '../../../core/error/setupErrorHandlers.js';
8
- import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
9
- import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
10
16
  import { OpenAI } from 'openai';
11
17
 
12
- class DeepSeekChatAdapter {
18
+ let DeepSeekChatAdapter = class DeepSeekChatAdapter {
13
19
  deepSeek;
14
20
  logger = new Logger('wabot:deepseek-chat-adapter');
15
21
  constructor() {
@@ -31,13 +37,25 @@ class DeepSeekChatAdapter {
31
37
  deepSeekInput.push({ role: 'system', content: req.systemPrompt });
32
38
  deepSeekInput.push(...this.mapChatItems(req.prevItems));
33
39
  const tools = req.tools.map((x) => this.mapTool(x));
34
- const response = await this.deepSeek.chat.completions.create({
35
- model: req.model,
36
- messages: deepSeekInput,
37
- tools,
38
- tool_choice: 'auto',
39
- });
40
- return this.mapResponse(response);
40
+ let lastError;
41
+ for (const ref of req.models) {
42
+ try {
43
+ const response = await this.deepSeek.chat.completions.create({
44
+ model: ref.model,
45
+ messages: deepSeekInput,
46
+ tools,
47
+ tool_choice: 'auto',
48
+ });
49
+ return this.mapResponse(response, ref.model);
50
+ }
51
+ catch (err) {
52
+ if (!isRetryableError(err))
53
+ throw err;
54
+ this.logger.warn(`DeepSeek model '${ref.model}' failed with retryable error, trying next`, err instanceof Error ? { message: err.message } : { err });
55
+ lastError = err;
56
+ }
57
+ }
58
+ throw lastError ?? new Error('No DeepSeek model could handle the request');
41
59
  }
42
60
  mapChatItems(chatItems) {
43
61
  const deepSeekInput = [];
@@ -115,7 +133,7 @@ class DeepSeekChatAdapter {
115
133
  },
116
134
  };
117
135
  }
118
- mapResponse(response) {
136
+ mapResponse(response, modelName) {
119
137
  let chatItem;
120
138
  const { tool_calls: responseFunctionCall, content: responseText } = response.choices?.[0]?.message ?? {};
121
139
  if (responseText) {
@@ -136,9 +154,15 @@ class DeepSeekChatAdapter {
136
154
  }
137
155
  let usage;
138
156
  if (response.usage) {
157
+ // DeepSeek exposes prompt_cache_hit_tokens (not in OpenAI SDK types)
158
+ const cacheRead = response.usage
159
+ .prompt_cache_hit_tokens ?? 0;
139
160
  usage = {
140
161
  inputTokens: response.usage.prompt_tokens,
141
162
  outputTokens: response.usage.completion_tokens,
163
+ cacheReadTokens: cacheRead || undefined,
164
+ provider: 'deepseek',
165
+ model: response.model ?? modelName,
142
166
  };
143
167
  }
144
168
  else {
@@ -146,6 +170,11 @@ class DeepSeekChatAdapter {
146
170
  }
147
171
  return { nextItems: [chatItem], usage };
148
172
  }
149
- }
173
+ };
174
+ DeepSeekChatAdapter = __decorate([
175
+ chatAdapter({ provider: 'deepseek' }),
176
+ singleton(),
177
+ __metadata("design:paramtypes", [])
178
+ ], DeepSeekChatAdapter);
150
179
 
151
180
  export { DeepSeekChatAdapter };
@@ -3,13 +3,18 @@ import { Env } from '../../../core/env/Env.js';
3
3
  import { singleton } from '../../../core/injection/index.js';
4
4
  import { Logger } from '../../../core/logger/Logger.js';
5
5
  import { Random } from '../../../core/random/Random.js';
6
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
6
7
  import '../../../feature/chat-bot/ChatBot.js';
7
8
  import '../../../feature/chat-bot/ChatOperator.js';
9
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
10
+ import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
11
+ import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
12
+ import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
13
+ import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
8
14
  import 'uuid';
9
15
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
16
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
10
17
  import { safeJsonParse } from '../../../feature/chat-bot/safeJsonParse.js';
11
- import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
12
- import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
13
18
  import { GoogleGenAI } from '@google/genai';
14
19
 
15
20
  const GOOGLE_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -38,21 +43,37 @@ let GoogleChatAdapter = class GoogleChatAdapter {
38
43
  async nextItems(req) {
39
44
  const contents = [];
40
45
  contents.push({ role: 'user', parts: [{ text: req.systemPrompt }] });
41
- contents.push(...this.mapChatItems(req.prevItems));
46
+ contents.push(...(await this.mapChatItems(req.prevItems)));
42
47
  const functionDeclarations = req.tools.map((x) => this.mapTool(x));
43
- const response = await this.ai.models.generateContent({
44
- model: req.model,
45
- contents,
46
- config: { tools: [{ functionDeclarations }] },
47
- });
48
- return this.mapResponse(response);
48
+ const hasUnsignedFunctionCall = req.prevItems.some((item) => item.type === 'functionCall' && !item.functionCall.signature);
49
+ let lastError;
50
+ for (const ref of req.models) {
51
+ try {
52
+ const response = await this.ai.models.generateContent({
53
+ model: ref.model,
54
+ contents,
55
+ config: {
56
+ tools: [{ functionDeclarations }],
57
+ ...(hasUnsignedFunctionCall ? { thinkingConfig: { thinkingBudget: 0 } } : {}),
58
+ },
59
+ });
60
+ return this.mapResponse(response, ref.model);
61
+ }
62
+ catch (err) {
63
+ if (!isRetryableError(err))
64
+ throw err;
65
+ this.logger.warn(`Google model '${ref.model}' failed with retryable error, trying next`, err instanceof Error ? { message: err.message } : { err });
66
+ lastError = err;
67
+ }
68
+ }
69
+ throw lastError ?? new Error('No Google model could handle the request');
49
70
  }
50
- mapChatItems(chatItems) {
71
+ async mapChatItems(chatItems) {
51
72
  const contents = [];
52
73
  for (const chatItem of chatItems) {
53
74
  switch (chatItem.type) {
54
75
  case 'humanMessage':
55
- contents.push(this.mapHumanMessage(chatItem.humanMessage));
76
+ contents.push(await this.mapHumanMessage(chatItem.humanMessage));
56
77
  break;
57
78
  case 'botMessage':
58
79
  contents.push(this.mapBotMessage(chatItem.botMessage));
@@ -64,7 +85,7 @@ let GoogleChatAdapter = class GoogleChatAdapter {
64
85
  }
65
86
  return contents;
66
87
  }
67
- mapHumanMessage(item) {
88
+ async mapHumanMessage(item) {
68
89
  if (isChatMessageEmpty(item)) {
69
90
  throw new Error('User message content is empty');
70
91
  }
@@ -75,25 +96,32 @@ let GoogleChatAdapter = class GoogleChatAdapter {
75
96
  supportedDocumentMimeTypes: GOOGLE_SUPPORTED_DOCUMENT_MIME_TYPES,
76
97
  }),
77
98
  });
99
+ const filesToSend = [];
78
100
  if (item.images) {
79
101
  for (const image of item.images) {
80
102
  if (!GOOGLE_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
81
103
  continue;
82
- parts.push(this.toGoogleFilePart(image));
104
+ filesToSend.push(image);
83
105
  }
84
106
  }
85
107
  if (item.documents) {
86
108
  for (const doc of item.documents) {
87
109
  if (!GOOGLE_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
88
110
  continue;
89
- parts.push(this.toGoogleFilePart(doc));
111
+ filesToSend.push(doc);
90
112
  }
91
113
  }
114
+ const fileParts = await Promise.all(filesToSend.map((f) => this.toGoogleFilePart(f)));
115
+ parts.push(...fileParts);
92
116
  return { role: 'user', parts };
93
117
  }
94
- toGoogleFilePart(file) {
118
+ async toGoogleFilePart(file) {
95
119
  if (file.publicUrl) {
96
- return { fileData: { fileUri: file.publicUrl, mimeType: file.mimeType } };
120
+ if (isGoogleNativeFileUri(file.publicUrl)) {
121
+ return { fileData: { fileUri: file.publicUrl, mimeType: file.mimeType } };
122
+ }
123
+ const data = await fetchAsBase64(file.publicUrl);
124
+ return { inlineData: { data, mimeType: file.mimeType } };
97
125
  }
98
126
  return {
99
127
  inlineData: { data: stripDataUrlPrefix(file.base64Url), mimeType: file.mimeType },
@@ -106,18 +134,20 @@ let GoogleChatAdapter = class GoogleChatAdapter {
106
134
  return { role: 'model', parts: [{ text: extractChatMessageText(item) }] };
107
135
  }
108
136
  mapFunctionCall(item) {
137
+ const callPart = {
138
+ functionCall: {
139
+ id: item.id,
140
+ name: item.name,
141
+ args: safeJsonParse(item.arguments, 'function call arguments'),
142
+ },
143
+ };
144
+ if (item.signature) {
145
+ callPart.thoughtSignature = item.signature;
146
+ }
109
147
  return [
110
148
  {
111
149
  role: 'model',
112
- parts: [
113
- {
114
- functionCall: {
115
- id: item.id,
116
- name: item.name,
117
- args: safeJsonParse(item.arguments, 'function call arguments'),
118
- },
119
- },
120
- ],
150
+ parts: [callPart],
121
151
  },
122
152
  {
123
153
  role: 'function',
@@ -148,7 +178,7 @@ let GoogleChatAdapter = class GoogleChatAdapter {
148
178
  },
149
179
  };
150
180
  }
151
- mapResponse(response) {
181
+ mapResponse(response, modelName) {
152
182
  if (!response.candidates || !response.candidates.length) {
153
183
  throw new Error('No candidates in response');
154
184
  }
@@ -177,18 +207,24 @@ let GoogleChatAdapter = class GoogleChatAdapter {
177
207
  id: id ?? Random.alphaNumericLowerCase(10),
178
208
  name,
179
209
  arguments: args && JSON.stringify(args),
210
+ signature: part.thoughtSignature,
180
211
  },
181
212
  });
182
213
  }
183
214
  }
215
+ const cachedTokens = response.usageMetadata.cachedContentTokenCount ?? 0;
184
216
  let usage = {
185
217
  inputTokens: response.usageMetadata.promptTokenCount,
186
218
  outputTokens: response.usageMetadata.candidatesTokenCount,
219
+ cacheReadTokens: cachedTokens || undefined,
220
+ provider: 'google',
221
+ model: response.modelVersion ?? modelName,
187
222
  };
188
223
  return { usage, nextItems };
189
224
  }
190
225
  };
191
226
  GoogleChatAdapter = __decorate([
227
+ chatAdapter({ provider: 'google' }),
192
228
  singleton(),
193
229
  __metadata("design:paramtypes", [Env])
194
230
  ], GoogleChatAdapter);
@@ -196,5 +232,20 @@ function stripDataUrlPrefix(dataUrl) {
196
232
  const commaIndex = dataUrl.indexOf(',');
197
233
  return commaIndex >= 0 && dataUrl.startsWith('data:') ? dataUrl.slice(commaIndex + 1) : dataUrl;
198
234
  }
235
+ function isGoogleNativeFileUri(url) {
236
+ if (url.startsWith('gs://'))
237
+ return true;
238
+ if (url.startsWith('https://generativelanguage.googleapis.com/'))
239
+ return true;
240
+ return false;
241
+ }
242
+ async function fetchAsBase64(url) {
243
+ const res = await fetch(url);
244
+ if (!res.ok) {
245
+ throw new Error(`Failed to fetch '${url}': ${res.status} ${res.statusText}`);
246
+ }
247
+ const buf = Buffer.from(await res.arrayBuffer());
248
+ return buf.toString('base64');
249
+ }
199
250
 
200
251
  export { GoogleChatAdapter };
@@ -1,12 +1,17 @@
1
1
  import { __decorate } from 'tslib';
2
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
2
3
  import '../../../feature/chat-bot/ChatBot.js';
3
4
  import '../../../feature/chat-bot/ChatOperator.js';
5
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
6
+ import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
7
+ import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
8
+ import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
9
+ import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
4
10
  import { singleton } from '../../../core/injection/index.js';
5
11
  import 'uuid';
6
12
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
13
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
7
14
  import '../../../core/error/setupErrorHandlers.js';
8
- import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
9
- import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
10
15
  import { Logger } from '../../../core/logger/Logger.js';
11
16
  import { OpenAI } from 'openai';
12
17
 
@@ -25,12 +30,24 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
25
30
  openIaInput.push({ role: 'system', content: req.systemPrompt });
26
31
  openIaInput.push(...this.mapChatItems(req.prevItems));
27
32
  const tools = req.tools.map((x) => this.mapTool(x));
28
- const response = await this.openai.responses.create({
29
- model: req.model,
30
- input: openIaInput,
31
- tools,
32
- });
33
- return this.mapResponse(response);
33
+ let lastError;
34
+ for (const ref of req.models) {
35
+ try {
36
+ const response = await this.openai.responses.create({
37
+ model: ref.model,
38
+ input: openIaInput,
39
+ tools,
40
+ });
41
+ return this.mapResponse(response, ref.model);
42
+ }
43
+ catch (err) {
44
+ if (!isRetryableError(err))
45
+ throw err;
46
+ this.logger.warn(`OpenAI model '${ref.model}' failed with retryable error, trying next`, err instanceof Error ? { message: err.message } : { err });
47
+ lastError = err;
48
+ }
49
+ }
50
+ throw lastError ?? new Error('No OpenAI model could handle the request');
34
51
  }
35
52
  mapChatItems(chatItems) {
36
53
  const openIaInput = [];
@@ -128,12 +145,16 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
128
145
  strict: true,
129
146
  };
130
147
  }
131
- mapResponse(response) {
148
+ mapResponse(response, modelName) {
132
149
  let usage;
133
150
  if (response.usage) {
151
+ const cacheRead = response.usage.input_tokens_details?.cached_tokens ?? 0;
134
152
  usage = {
135
153
  inputTokens: response.usage.input_tokens,
136
154
  outputTokens: response.usage.output_tokens,
155
+ cacheReadTokens: cacheRead || undefined,
156
+ provider: 'openai',
157
+ model: response.model ?? modelName,
137
158
  };
138
159
  }
139
160
  else {
@@ -184,6 +205,7 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
184
205
  }
185
206
  };
186
207
  OpenaiChatAdapter = __decorate([
208
+ chatAdapter({ provider: 'openai' }),
187
209
  singleton()
188
210
  ], OpenaiChatAdapter);
189
211
 
@@ -2,13 +2,17 @@ import { __decorate, __metadata } from 'tslib';
2
2
  import { Env } from '../../../core/env/Env.js';
3
3
  import { singleton } from '../../../core/injection/index.js';
4
4
  import { Logger } from '../../../core/logger/Logger.js';
5
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
5
6
  import '../../../feature/chat-bot/ChatBot.js';
6
7
  import '../../../feature/chat-bot/ChatOperator.js';
8
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
9
+ import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
10
+ import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
11
+ import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
7
12
  import 'uuid';
8
13
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
14
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
9
15
  import '../../../core/error/setupErrorHandlers.js';
10
- import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
11
- import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
12
16
  import { OpenRouter } from '@openrouter/sdk';
13
17
 
14
18
  const OPENROUTER_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -32,10 +36,13 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
32
36
  messages.push({ role: 'system', content: req.systemPrompt });
33
37
  messages.push(...this.mapChatItems(req.prevItems));
34
38
  const tools = req.tools.map((x) => this.mapTool(x));
35
- this.logger.debug(`Call OpenRouter with model: ${req.model}, messages: ${messages.length}, tools: ${tools.length}`);
39
+ const modelNames = req.models.map((m) => m.model);
40
+ const [primary, ...fallbacks] = modelNames;
41
+ this.logger.debug(`Call OpenRouter with model: ${primary}, fallbacks: ${fallbacks.length}, messages: ${messages.length}, tools: ${tools.length}`);
36
42
  const response = await this.openRouter.chat.send({
37
43
  chatRequest: {
38
- model: req.model,
44
+ model: primary,
45
+ models: fallbacks.length > 0 ? fallbacks : undefined,
39
46
  messages,
40
47
  tools: tools.length > 0 ? tools : undefined,
41
48
  },
@@ -160,9 +167,20 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
160
167
  }
161
168
  let usage;
162
169
  if (response.usage) {
170
+ const cacheRead = response.usage.promptTokensDetails?.cachedTokens ?? 0;
171
+ const cacheWrite = response.usage.promptTokensDetails?.cacheWriteTokens ?? 0;
172
+ // OpenRouter exposes `cost` (and provider) on the response but the SDK
173
+ // types haven't surfaced them yet — read via cast.
174
+ const costUsd = response.usage.cost;
175
+ const upstreamProvider = response.provider;
163
176
  usage = {
164
177
  inputTokens: response.usage.promptTokens,
165
178
  outputTokens: response.usage.completionTokens,
179
+ cacheReadTokens: cacheRead || undefined,
180
+ cacheWriteTokens: cacheWrite || undefined,
181
+ costUsd: typeof costUsd === 'number' ? costUsd : undefined,
182
+ provider: upstreamProvider ? `openrouter/${upstreamProvider}` : 'openrouter',
183
+ model: response.model,
166
184
  };
167
185
  }
168
186
  else {
@@ -172,6 +190,7 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
172
190
  }
173
191
  };
174
192
  OpenRouterChatAdapter = __decorate([
193
+ chatAdapter({ provider: 'openrouter' }),
175
194
  singleton(),
176
195
  __metadata("design:paramtypes", [Env])
177
196
  ], OpenRouterChatAdapter);
@@ -4,10 +4,15 @@ import 'debug';
4
4
  import 'node:crypto';
5
5
  import '../../../feature/pg/withPgClient.js';
6
6
  import '../../../feature/pg/pgStorage.js';
7
+ import '../../../core/injection/index.js';
8
+ import '../../../feature/pg/query/PgJsonRepository.js';
9
+ import '../../../feature/pg/query/PgRepositoryMetadataStore.js';
10
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
7
11
  import '../../../feature/chat-bot/ChatBot.js';
8
12
  import { ChatItem } from '../../../feature/chat-bot/ChatItem.js';
9
13
  import '../../../feature/chat-bot/ChatOperator.js';
10
- import '../../../core/injection/index.js';
14
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
15
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
11
16
  import 'uuid';
12
17
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
13
18
  import '../../../core/error/setupErrorHandlers.js';
@@ -8,9 +8,14 @@ import 'debug';
8
8
  import 'node:crypto';
9
9
  import '../../../feature/pg/withPgClient.js';
10
10
  import '../../../feature/pg/pgStorage.js';
11
+ import '../../../feature/pg/query/PgJsonRepository.js';
12
+ import '../../../feature/pg/query/PgRepositoryMetadataStore.js';
11
13
  import { Chat } from '../../../feature/chat-bot/Chat.js';
14
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
12
15
  import '../../../feature/chat-bot/ChatBot.js';
13
16
  import { ChatOperator } from '../../../feature/chat-bot/ChatOperator.js';
17
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
18
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
14
19
  import 'uuid';
15
20
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
16
21
  import '../../../core/error/setupErrorHandlers.js';
@@ -2,8 +2,11 @@ import { __decorate } from 'tslib';
2
2
  import { v4 } from 'uuid';
3
3
  import { RamChatMemory } from './RamChatMemory.js';
4
4
  import { singleton } from '../../../core/injection/index.js';
5
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
5
6
  import '../../../feature/chat-bot/ChatBot.js';
6
7
  import { ChatOperator } from '../../../feature/chat-bot/ChatOperator.js';
8
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
9
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
7
10
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
8
11
  import '../../../core/error/setupErrorHandlers.js';
9
12