manifest 5.35.1 → 5.36.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 (63) hide show
  1. package/dist/backend/database/models-dev-sync.service.js +8 -1
  2. package/dist/backend/database/pricing-sync.service.js +1 -1
  3. package/dist/backend/model-discovery/model-discovery.service.js +9 -3
  4. package/dist/backend/model-discovery/model-fallback.js +7 -0
  5. package/dist/backend/model-discovery/provider-model-fetcher.service.js +31 -10
  6. package/dist/backend/model-prices/model-name-normalizer.js +16 -0
  7. package/dist/backend/model-prices/model-pricing-cache.service.js +16 -1
  8. package/dist/backend/notifications/emails/threshold-alert.js +5 -1
  9. package/dist/backend/notifications/notifications.controller.js +16 -1
  10. package/dist/backend/notifications/services/limit-check.service.js +11 -14
  11. package/dist/backend/notifications/services/notification-cron.service.js +15 -18
  12. package/dist/backend/notifications/services/notification-email.service.js +3 -2
  13. package/dist/backend/notifications/services/notification-log.service.js +9 -0
  14. package/dist/backend/routing/proxy/anthropic-adapter.js +1 -3
  15. package/dist/backend/routing/proxy/chatgpt-adapter.js +119 -85
  16. package/dist/backend/routing/proxy/chatgpt-helpers.js +108 -0
  17. package/dist/backend/routing/proxy/google-adapter.js +37 -13
  18. package/dist/backend/routing/proxy/provider-client.js +2 -2
  19. package/dist/backend/routing/proxy/proxy-exception.filter.js +67 -0
  20. package/dist/backend/routing/proxy/proxy-fallback.service.js +18 -4
  21. package/dist/backend/routing/proxy/proxy-friendly-response.js +115 -0
  22. package/dist/backend/routing/proxy/proxy-rate-limiter.js +2 -2
  23. package/dist/backend/routing/proxy/proxy-response-handler.js +25 -5
  24. package/dist/backend/routing/proxy/proxy.controller.js +23 -8
  25. package/dist/backend/routing/proxy/proxy.module.js +4 -0
  26. package/dist/backend/routing/proxy/proxy.service.js +30 -82
  27. package/dist/backend/routing/proxy/thought-signature-cache.js +45 -0
  28. package/dist/backend/routing/resolve/resolve.service.js +2 -1
  29. package/dist/backend/routing/routing-core/provider-key.service.js +12 -2
  30. package/dist/backend/routing/routing-core/tier-auto-assign.service.js +9 -4
  31. package/dist/openclaw.plugin.json +1 -1
  32. package/openclaw.plugin.json +1 -1
  33. package/package.json +1 -1
  34. package/public/assets/{Account-CDKYuc8Y.js → Account-mbrfK614.js} +1 -1
  35. package/public/assets/{AuthBadge-CN6tvI2S.js → AuthBadge-zJLfTd7g.js} +1 -1
  36. package/public/assets/{CopyButton-BrMDM22y.js → CopyButton-CZjeoxZT.js} +1 -1
  37. package/public/assets/{Help-BCxDh7o6.js → Help-Bfmf4yw8.js} +1 -1
  38. package/public/assets/Limits-D6l8xhUA.js +1 -0
  39. package/public/assets/{Login-DBUKUcpO.js → Login-Dn1aZjXw.js} +1 -1
  40. package/public/assets/{MessageLog-DB_ZGtRS.js → MessageLog-CWiKoRpB.js} +1 -1
  41. package/public/assets/{ModelPrices-CsTVSmEf.js → ModelPrices-CbrlrT8V.js} +1 -1
  42. package/public/assets/{Overview-WPFffGwu.js → Overview-Bpuw4RLJ.js} +1 -1
  43. package/public/assets/{Pagination-CQk_Ps2L.js → Pagination-Ctujg2Tx.js} +1 -1
  44. package/public/assets/{Register-BPUSuyhr.js → Register-CIAm21m5.js} +1 -1
  45. package/public/assets/{ResetPassword-DbYikLII.js → ResetPassword-Bir2RHWf.js} +1 -1
  46. package/public/assets/{Routing-CqkpJ5K7.js → Routing-Bc7D8_7D.js} +1 -1
  47. package/public/assets/Settings-CciPiVro.js +1 -0
  48. package/public/assets/{SetupStepAddProvider-CYgcYYLH.js → SetupStepAddProvider-CJemlPav.js} +1 -1
  49. package/public/assets/{SocialButtons-Bz_LKRqa.js → SocialButtons-8xBLnR-O.js} +1 -1
  50. package/public/assets/{auth-B7LxODhZ.js → auth-DmX5tAfx.js} +1 -1
  51. package/public/assets/index-BsqwVwB3.css +1 -0
  52. package/public/assets/{index-6zwEk5De.js → index-JMxG9oFW.js} +2 -2
  53. package/public/assets/{model-display-bwKW0oKN.js → model-display-1OeS4GNc.js} +1 -1
  54. package/public/assets/{overview-CsP0GEBQ.js → overview-DIAj72dm.js} +1 -1
  55. package/public/assets/{routing-C7G-3vs6.js → routing-CuE7sS2j.js} +1 -1
  56. package/public/assets/{routing-utils-TY9NvF2y.js → routing-utils-B_Dl8Sof.js} +1 -1
  57. package/public/assets/vendor-pl6Q4jbW.js +1 -0
  58. package/public/index.html +4 -4
  59. package/public/logo-white.svg +81 -79
  60. package/public/assets/Limits-DpYfj80Z.js +0 -1
  61. package/public/assets/Settings-CPrWA4SD.js +0 -1
  62. package/public/assets/index-DHEiPwfc.css +0 -1
  63. package/public/assets/vendor-K8fEFSq9.js +0 -1
@@ -41,6 +41,7 @@ const VERSION_SUFFIX_RE = /-\d{3}$/;
41
41
  const DATE_SUFFIX_RE = /-\d{4}-?\d{2}-?\d{2}$/;
42
42
  const SHORT_DATE_SUFFIX_RE = /-\d{4}$/;
43
43
  const LATEST_SUFFIX = '-latest';
44
+ const GOOGLE_VARIANT_RE = /-(?:preview(?:-\d{2,4}){1,3}|exp-\d{4}|latest)$/;
44
45
  const REASONING_SUFFIX_RE = /-(reasoning|non-reasoning)$/;
45
46
  let ModelsDevSyncService = ModelsDevSyncService_1 = class ModelsDevSyncService {
46
47
  logger = new common_1.Logger(ModelsDevSyncService_1.name);
@@ -111,6 +112,12 @@ let ModelsDevSyncService = ModelsDevSyncService_1 = class ModelsDevSyncService {
111
112
  if (found)
112
113
  return found;
113
114
  }
115
+ const noGoogleVariant = modelId.replace(GOOGLE_VARIANT_RE, '');
116
+ if (noGoogleVariant !== modelId) {
117
+ const found = providerModels.get(noGoogleVariant);
118
+ if (found)
119
+ return found;
120
+ }
114
121
  const noReasoning = modelId.replace(REASONING_SUFFIX_RE, '');
115
122
  if (noReasoning !== modelId) {
116
123
  const found = providerModels.get(noReasoning);
@@ -169,7 +176,7 @@ let ModelsDevSyncService = ModelsDevSyncService_1 = class ModelsDevSyncService {
169
176
  }
170
177
  const outputMods = model.modalities?.output?.map((m) => m.toLowerCase());
171
178
  if (outputMods && outputMods.length > 0) {
172
- return outputMods.every((m) => m === 'text');
179
+ return outputMods.includes('text');
173
180
  }
174
181
  return true;
175
182
  }
@@ -98,7 +98,7 @@ let PricingSyncService = PricingSyncService_1 = class PricingSyncService {
98
98
  }
99
99
  const outputModalities = model.architecture?.output_modalities?.map((m) => m.toLowerCase());
100
100
  if (outputModalities && outputModalities.length > 0) {
101
- return outputModalities.every((m) => m === 'text');
101
+ return outputModalities.includes('text');
102
102
  }
103
103
  return true;
104
104
  }
@@ -123,11 +123,17 @@ let ModelDiscoveryService = ModelDiscoveryService_1 = class ModelDiscoveryServic
123
123
  ...this.enrichModel(model, provider.provider),
124
124
  authType: authType,
125
125
  }));
126
- provider.cached_models = enriched;
126
+ const filtered = enriched.filter((model) => {
127
+ const mdEntry = this.modelsDevSync?.lookupModel(provider.provider, model.id);
128
+ if (mdEntry && mdEntry.toolCall === false)
129
+ return false;
130
+ return true;
131
+ });
132
+ provider.cached_models = filtered;
127
133
  provider.models_fetched_at = new Date().toISOString();
128
134
  await this.providerRepo.save(provider);
129
- this.logger.log(`Discovered ${enriched.length} models for provider ${provider.provider} (agent ${provider.agent_id})`);
130
- return enriched;
135
+ this.logger.log(`Discovered ${filtered.length} models for provider ${provider.provider} (agent ${provider.agent_id})`);
136
+ return filtered;
131
137
  }
132
138
  async discoverAllForAgent(agentId) {
133
139
  const providers = await this.providerRepo.find({
@@ -9,6 +9,7 @@ exports.supplementWithKnownModels = supplementWithKnownModels;
9
9
  const providers_1 = require("../common/constants/providers");
10
10
  const manifest_shared_1 = require("manifest-shared");
11
11
  const anthropic_model_id_1 = require("../common/utils/anthropic-model-id");
12
+ const model_name_normalizer_1 = require("../model-prices/model-name-normalizer");
12
13
  function normalizeProviderModelId(providerId, modelId) {
13
14
  return providerId.toLowerCase() === 'anthropic'
14
15
  ? (0, anthropic_model_id_1.normalizeAnthropicShortModelId)(modelId)
@@ -62,6 +63,12 @@ function lookupWithVariants(pricingSync, prefix, modelId) {
62
63
  const freeResult = pricingSync.lookupPricing(`${prefix}/${modelId}:free`);
63
64
  if (freeResult)
64
65
  return freeResult;
66
+ const noGoogleVariant = modelId.replace(model_name_normalizer_1.GOOGLE_VARIANT_RE, '');
67
+ if (noGoogleVariant !== modelId) {
68
+ const result = pricingSync.lookupPricing(`${prefix}/${noGoogleVariant}`);
69
+ if (result)
70
+ return result;
71
+ }
65
72
  return null;
66
73
  }
67
74
  function buildModelsDevFallback(modelsDevSync, providerId) {
@@ -7,7 +7,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  };
8
8
  var ProviderModelFetcherService_1;
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.ProviderModelFetcherService = exports.PROVIDER_CONFIGS = void 0;
10
+ exports.ProviderModelFetcherService = exports.PROVIDER_CONFIGS = exports.PROVIDER_NON_CHAT = exports.UNIVERSAL_NON_CHAT_RE = void 0;
11
+ exports.filterNonChatModels = filterNonChatModels;
11
12
  const common_1 = require("@nestjs/common");
12
13
  const ollama_1 = require("../common/constants/ollama");
13
14
  const provider_base_url_1 = require("../routing/provider-base-url");
@@ -48,14 +49,34 @@ const parseOpenAI = createModelParser({
48
49
  getId: (entry) => entry.id,
49
50
  getDisplayName: (_entry, id) => id,
50
51
  });
51
- const OPENAI_NON_CHAT_RE = /(?:embed|tts|whisper|dall-e|moderation|davinci|babbage|^text-|audio|realtime|-transcribe|^sora|^gpt-3\.5-turbo-instruct)/i;
52
+ const OPENAI_DATE_SUFFIX_RE = /-\d{4}-\d{2}-\d{2}$/;
52
53
  const OPENAI_RESPONSES_ONLY_RE = /(?:-codex(?!-mini-latest)|^gpt-5[^/]*-pro(?:-|$))/i;
53
- function parseOpenAIChatOnly(body, provider) {
54
- return parseOpenAI(body, provider).filter((m) => !OPENAI_NON_CHAT_RE.test(m.id) && !OPENAI_RESPONSES_ONLY_RE.test(m.id));
54
+ function parseOpenAIDeduped(body, provider) {
55
+ const filtered = parseOpenAI(body, provider).filter((m) => !OPENAI_RESPONSES_ONLY_RE.test(m.id));
56
+ const ids = new Set(filtered.map((m) => m.id));
57
+ return filtered.filter((m) => {
58
+ if (!OPENAI_DATE_SUFFIX_RE.test(m.id))
59
+ return true;
60
+ const alias = m.id.replace(OPENAI_DATE_SUFFIX_RE, '');
61
+ return !ids.has(alias);
62
+ });
55
63
  }
56
- const MISTRAL_NON_CHAT_RE = /(?:^mistral-ocr|embed)/i;
57
- function parseMistralChatOnly(body, provider) {
58
- return parseOpenAI(body, provider).filter((m) => !MISTRAL_NON_CHAT_RE.test(m.id));
64
+ exports.UNIVERSAL_NON_CHAT_RE = /(?:embed|tts|whisper|dall-e|imagen|cogview|wanx|sambert|paraformer|text-embedding|speech-to|voice-|audio-turbo)/i;
65
+ exports.PROVIDER_NON_CHAT = {
66
+ openai: /(?:moderation|davinci|babbage|^text-|realtime|-transcribe|^sora|^gpt-3\.5-turbo-instruct|audio)/i,
67
+ 'openai-subscription': /(?:moderation|davinci|babbage|^text-|realtime|-transcribe|^sora|audio)/i,
68
+ gemini: /(?:^aqs-|nano-banana)/i,
69
+ mistral: /(?:^mistral-ocr)/i,
70
+ };
71
+ function filterNonChatModels(models, configKey) {
72
+ const providerFilter = exports.PROVIDER_NON_CHAT[configKey];
73
+ return models.filter((m) => {
74
+ if (exports.UNIVERSAL_NON_CHAT_RE.test(m.id))
75
+ return false;
76
+ if (providerFilter && providerFilter.test(m.id))
77
+ return false;
78
+ return true;
79
+ });
59
80
  }
60
81
  function bearerHeaders(key) {
61
82
  return { Authorization: `Bearer ${key}` };
@@ -166,7 +187,7 @@ exports.PROVIDER_CONFIGS = {
166
187
  openai: {
167
188
  endpoint: 'https://api.openai.com/v1/models',
168
189
  buildHeaders: bearerHeaders,
169
- parse: parseOpenAIChatOnly,
190
+ parse: parseOpenAIDeduped,
170
191
  },
171
192
  'openai-subscription': {
172
193
  endpoint: 'https://chatgpt.com/backend-api/codex/models?client_version=0.99.0',
@@ -186,7 +207,7 @@ exports.PROVIDER_CONFIGS = {
186
207
  mistral: {
187
208
  endpoint: 'https://api.mistral.ai/v1/models',
188
209
  buildHeaders: bearerHeaders,
189
- parse: parseMistralChatOnly,
210
+ parse: parseOpenAI,
190
211
  },
191
212
  moonshot: {
192
213
  endpoint: 'https://api.moonshot.ai/v1/models',
@@ -313,7 +334,7 @@ let ProviderModelFetcherService = ProviderModelFetcherService_1 = class Provider
313
334
  return [];
314
335
  }
315
336
  const body = await res.json();
316
- return config.parse(body, providerId);
337
+ return filterNonChatModels(config.parse(body, providerId), configKey);
317
338
  }
318
339
  catch (err) {
319
340
  const message = err instanceof Error ? err.message : String(err);
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GOOGLE_VARIANT_RE = void 0;
4
+ exports.stripGoogleVariant = stripGoogleVariant;
3
5
  exports.stripProviderPrefix = stripProviderPrefix;
4
6
  exports.stripDateSuffix = stripDateSuffix;
5
7
  exports.buildAliasMap = buildAliasMap;
@@ -37,6 +39,10 @@ const PROVIDER_PREFIXES = [
37
39
  ];
38
40
  const DATE_SUFFIX_RE = /-\d{4}-?\d{2}-?\d{2}$/;
39
41
  const VERSION_SUFFIX_RE = /-\d{3}$/;
42
+ exports.GOOGLE_VARIANT_RE = /-(?:preview(?:-\d{2,4}){1,3}|exp-\d{4}|latest)$/;
43
+ function stripGoogleVariant(name) {
44
+ return name.replace(exports.GOOGLE_VARIANT_RE, '');
45
+ }
40
46
  function stripProviderPrefix(name) {
41
47
  for (const prefix of PROVIDER_PREFIXES) {
42
48
  if (name.startsWith(prefix)) {
@@ -65,6 +71,10 @@ function buildAliasMap(canonicalNames) {
65
71
  if (bareNoVersion !== bare && !map.has(bareNoVersion)) {
66
72
  map.set(bareNoVersion, name);
67
73
  }
74
+ const bareNoVariant = stripGoogleVariant(bare);
75
+ if (bareNoVariant !== bare && !map.has(bareNoVariant)) {
76
+ map.set(bareNoVariant, name);
77
+ }
68
78
  for (const variant of (0, anthropic_model_id_1.buildAnthropicShortModelIdVariants)(bare)) {
69
79
  if (!map.has(variant)) {
70
80
  map.set(variant, name);
@@ -96,6 +106,12 @@ function resolveModelName(name, aliasMap) {
96
106
  if (fromNoDate)
97
107
  return fromNoDate;
98
108
  }
109
+ const noVariant = stripGoogleVariant(stripped);
110
+ if (noVariant !== stripped) {
111
+ const fromNoVariant = aliasMap.get(noVariant);
112
+ if (fromNoVariant)
113
+ return fromNoVariant;
114
+ }
99
115
  const dotNorm = normalizeDots(stripped);
100
116
  if (dotNorm !== stripped) {
101
117
  const fromDotNorm = aliasMap.get(dotNorm);
@@ -15,6 +15,7 @@ var ModelPricingCacheService_1;
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.ModelPricingCacheService = void 0;
17
17
  const common_1 = require("@nestjs/common");
18
+ const schedule_1 = require("@nestjs/schedule");
18
19
  const model_name_normalizer_1 = require("./model-name-normalizer");
19
20
  const pricing_sync_service_1 = require("../database/pricing-sync.service");
20
21
  const models_dev_sync_service_1 = require("../database/models-dev-sync.service");
@@ -35,6 +36,9 @@ let ModelPricingCacheService = ModelPricingCacheService_1 = class ModelPricingCa
35
36
  async onApplicationBootstrap() {
36
37
  await this.reload();
37
38
  }
39
+ async scheduledReload() {
40
+ await this.reload();
41
+ }
38
42
  async reload() {
39
43
  this.cache.clear();
40
44
  const orCache = this.pricingSync.getAll();
@@ -115,7 +119,12 @@ let ModelPricingCacheService = ModelPricingCacheService_1 = class ModelPricingCa
115
119
  validated: this.resolveValidatedForModelsDev(providerId, model.id),
116
120
  source: 'models.dev',
117
121
  };
118
- this.cache.set(model.id, pricingEntry);
122
+ const existing = this.cache.get(model.id);
123
+ const hasRealPricing = existing && (existing.input_price_per_token ?? 0) > 0;
124
+ const isZeroPricing = (model.inputPricePerToken ?? 0) === 0 && (model.outputPricePerToken ?? 0) === 0;
125
+ if (!hasRealPricing || !isZeroPricing) {
126
+ this.cache.set(model.id, pricingEntry);
127
+ }
119
128
  for (const prefix of registryEntry.openRouterPrefixes) {
120
129
  const prefixedKey = `${prefix}/${model.id}`;
121
130
  if (this.cache.has(prefixedKey)) {
@@ -145,6 +154,12 @@ let ModelPricingCacheService = ModelPricingCacheService_1 = class ModelPricingCa
145
154
  }
146
155
  };
147
156
  exports.ModelPricingCacheService = ModelPricingCacheService;
157
+ __decorate([
158
+ (0, schedule_1.Cron)('0 5 * * *'),
159
+ __metadata("design:type", Function),
160
+ __metadata("design:paramtypes", []),
161
+ __metadata("design:returntype", Promise)
162
+ ], ModelPricingCacheService.prototype, "scheduledReload", null);
148
163
  exports.ModelPricingCacheService = ModelPricingCacheService = ModelPricingCacheService_1 = __decorate([
149
164
  (0, common_1.Injectable)(),
150
165
  __param(1, (0, common_1.Optional)()),
@@ -28,7 +28,11 @@ function ThresholdAlertEmail(props) {
28
28
  const accentColor = isSoft ? '#ea580c' : '#dc2626';
29
29
  const accentBg = isSoft ? '#fff7ed' : '#fef2f2';
30
30
  const accentBorder = isSoft ? '#fed7aa' : '#fecaca';
31
- return ((0, jsx_runtime_1.jsxs)(components_1.Html, { children: [(0, jsx_runtime_1.jsx)(components_1.Head, {}), (0, jsx_runtime_1.jsxs)(components_1.Preview, { children: [agentName, " exceeded ", metricType, " threshold (", formatValue(actualValue, metricType), ")"] }), (0, jsx_runtime_1.jsx)(components_1.Body, { style: body, children: (0, jsx_runtime_1.jsxs)(components_1.Container, { style: container, children: [(0, jsx_runtime_1.jsx)(components_1.Section, { style: logoSection, children: (0, jsx_runtime_1.jsx)(components_1.Img, { src: logoUrl, alt: "Manifest", width: "140", height: "32", style: logoImg }) }), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: card, children: [(0, jsx_runtime_1.jsx)(components_1.Section, { style: alertBadgeContainer, children: (0, jsx_runtime_1.jsx)(components_1.Text, { style: { ...alertBadge, color: accentColor, backgroundColor: accentBg }, children: isSoft ? 'Warning' : 'Threshold exceeded' }) }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: heading, children: [agentName, " exceeded the ", metricType, " limit"] }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: paragraph, children: ["Your agent ", (0, jsx_runtime_1.jsx)("strong", { children: agentName }), " has exceeded the ", (0, jsx_runtime_1.jsx)("strong", { children: metricType }), ' ', "threshold for the current ", (0, jsx_runtime_1.jsx)("strong", { children: period }), " period."] }), isSoft ? ((0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: "Requests are still being processed normally." })) : ((0, jsx_runtime_1.jsx)(components_1.Section, { style: { ...hardLimitBox, backgroundColor: accentBg, borderColor: accentBorder }, children: (0, jsx_runtime_1.jsxs)(components_1.Text, { style: { ...hardLimitText, color: accentColor }, children: ["Requests are now blocked until the next period resets", periodResetDate ? ` on ${formatTimestamp(periodResetDate)}` : '', "."] }) })), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: statsRow, children: [(0, jsx_runtime_1.jsxs)(components_1.Section, { style: statBox, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: statLabel, children: "Threshold" }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: statValue, children: formatValue(threshold, metricType) })] }), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: { ...statBoxAlert, backgroundColor: accentBg, borderColor: accentBorder }, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: statLabel, children: "Actual usage" }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: { ...statValueAlert, color: accentColor }, children: formatValue(actualValue, metricType) })] })] }), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: metaRow, children: [(0, jsx_runtime_1.jsxs)(components_1.Text, { style: metaText, children: ["Period: ", period] }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: metaText, children: ["Triggered: ", formatTimestamp(timestamp)] })] }), (0, jsx_runtime_1.jsx)(components_1.Section, { style: ctaContainer, children: (0, jsx_runtime_1.jsx)(components_1.Button, { style: ctaButton, href: agentUrl, children: "View Agent Dashboard \u2192" }) })] }), (0, jsx_runtime_1.jsx)(components_1.Hr, { style: divider }), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: footer, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: footerNote, children: "You are receiving this because you set up a notification rule in Manifest." }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: footerMuted, children: ["\u00A9 2026 MNFST Inc. All rights reserved.", ' ', (0, jsx_runtime_1.jsx)(components_1.Link, { href: "https://manifest.build", style: footerLink, children: "manifest.build" })] })] })] }) })] }));
31
+ return ((0, jsx_runtime_1.jsxs)(components_1.Html, { children: [(0, jsx_runtime_1.jsx)(components_1.Head, {}), (0, jsx_runtime_1.jsx)(components_1.Preview, { children: isSoft
32
+ ? `${agentName} exceeded ${metricType} threshold (${formatValue(actualValue, metricType)})`
33
+ : `${agentName} has been blocked — ${metricType} limit reached (${formatValue(actualValue, metricType)} / ${formatValue(threshold, metricType)})` }), (0, jsx_runtime_1.jsx)(components_1.Body, { style: body, children: (0, jsx_runtime_1.jsxs)(components_1.Container, { style: container, children: [(0, jsx_runtime_1.jsx)(components_1.Section, { style: logoSection, children: (0, jsx_runtime_1.jsx)(components_1.Img, { src: logoUrl, alt: "Manifest", width: "140", height: "32", style: logoImg }) }), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: card, children: [(0, jsx_runtime_1.jsx)(components_1.Section, { style: alertBadgeContainer, children: (0, jsx_runtime_1.jsx)(components_1.Text, { style: { ...alertBadge, color: accentColor, backgroundColor: accentBg }, children: isSoft ? 'Warning' : 'Agent blocked' }) }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: heading, children: isSoft
34
+ ? `${agentName} exceeded the ${metricType} limit`
35
+ : `${agentName} has been blocked` }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: isSoft ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Your agent ", (0, jsx_runtime_1.jsx)("strong", { children: agentName }), " has exceeded the", ' ', (0, jsx_runtime_1.jsx)("strong", { children: metricType }), " threshold for the current ", (0, jsx_runtime_1.jsx)("strong", { children: period }), ' ', "period."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Your agent ", (0, jsx_runtime_1.jsx)("strong", { children: agentName }), " has reached the", ' ', (0, jsx_runtime_1.jsx)("strong", { children: metricType }), " limit of", ' ', (0, jsx_runtime_1.jsx)("strong", { children: formatValue(threshold, metricType) }), " per", ' ', (0, jsx_runtime_1.jsx)("strong", { children: period }), ". New requests are blocked to protect your budget."] })) }), isSoft ? ((0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: "Requests are still being processed normally." })) : ((0, jsx_runtime_1.jsx)(components_1.Section, { style: { ...hardLimitBox, backgroundColor: accentBg, borderColor: accentBorder }, children: (0, jsx_runtime_1.jsxs)(components_1.Text, { style: { ...hardLimitText, color: accentColor }, children: ["Requests are now blocked until the next period resets", periodResetDate ? ` on ${formatTimestamp(periodResetDate)}` : '', "."] }) })), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: statsRow, children: [(0, jsx_runtime_1.jsxs)(components_1.Section, { style: statBox, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: statLabel, children: "Threshold" }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: statValue, children: formatValue(threshold, metricType) })] }), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: { ...statBoxAlert, backgroundColor: accentBg, borderColor: accentBorder }, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: statLabel, children: isSoft ? 'Actual usage' : 'Current usage' }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: { ...statValueAlert, color: accentColor }, children: formatValue(actualValue, metricType) })] })] }), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: metaRow, children: [(0, jsx_runtime_1.jsxs)(components_1.Text, { style: metaText, children: ["Period: ", period] }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: metaText, children: ["Triggered: ", formatTimestamp(timestamp)] })] }), (0, jsx_runtime_1.jsx)(components_1.Section, { style: ctaContainer, children: (0, jsx_runtime_1.jsx)(components_1.Button, { style: ctaButton, href: agentUrl, children: "View Agent Dashboard \u2192" }) })] }), (0, jsx_runtime_1.jsx)(components_1.Hr, { style: divider }), (0, jsx_runtime_1.jsxs)(components_1.Section, { style: footer, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: footerNote, children: "You are receiving this because you set up a notification rule in Manifest." }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: footerMuted, children: ["\u00A9 2026 MNFST Inc. All rights reserved.", ' ', (0, jsx_runtime_1.jsx)(components_1.Link, { href: "https://manifest.build", style: footerLink, children: "manifest.build" })] })] })] }) })] }));
32
36
  }
33
37
  const brandBg = '#f8f6f1';
34
38
  const brandCardBg = '#ffffff';
@@ -17,6 +17,7 @@ exports.NotificationsController = void 0;
17
17
  const common_1 = require("@nestjs/common");
18
18
  const current_user_decorator_1 = require("../auth/current-user.decorator");
19
19
  const notification_rules_service_1 = require("./services/notification-rules.service");
20
+ const notification_log_service_1 = require("./services/notification-log.service");
20
21
  const email_provider_config_service_1 = require("./services/email-provider-config.service");
21
22
  const notification_cron_service_1 = require("./services/notification-cron.service");
22
23
  const limit_check_service_1 = require("./services/limit-check.service");
@@ -24,12 +25,14 @@ const notification_rule_dto_1 = require("./dto/notification-rule.dto");
24
25
  const set_email_provider_dto_1 = require("./dto/set-email-provider.dto");
25
26
  let NotificationsController = NotificationsController_1 = class NotificationsController {
26
27
  rulesService;
28
+ notificationLog;
27
29
  emailProviderConfigService;
28
30
  cronService;
29
31
  limitCheck;
30
32
  logger = new common_1.Logger(NotificationsController_1.name);
31
- constructor(rulesService, emailProviderConfigService, cronService, limitCheck) {
33
+ constructor(rulesService, notificationLog, emailProviderConfigService, cronService, limitCheck) {
32
34
  this.rulesService = rulesService;
35
+ this.notificationLog = notificationLog;
33
36
  this.emailProviderConfigService = emailProviderConfigService;
34
37
  this.cronService = cronService;
35
38
  this.limitCheck = limitCheck;
@@ -64,6 +67,9 @@ let NotificationsController = NotificationsController_1 = class NotificationsCon
64
67
  const triggered = await this.cronService.checkThresholds(user.id);
65
68
  return { triggered, message: `${triggered} notification(s) triggered` };
66
69
  }
70
+ async getLogs(agentName, user) {
71
+ return this.notificationLog.getLogsForAgent(user.id, agentName);
72
+ }
67
73
  async listRules(agentName, user) {
68
74
  return this.rulesService.listRules(user.id, agentName);
69
75
  }
@@ -149,6 +155,14 @@ __decorate([
149
155
  __metadata("design:paramtypes", [Object]),
150
156
  __metadata("design:returntype", Promise)
151
157
  ], NotificationsController.prototype, "triggerCheck", null);
158
+ __decorate([
159
+ (0, common_1.Get)('logs'),
160
+ __param(0, (0, common_1.Query)('agent_name')),
161
+ __param(1, (0, current_user_decorator_1.CurrentUser)()),
162
+ __metadata("design:type", Function),
163
+ __metadata("design:paramtypes", [String, Object]),
164
+ __metadata("design:returntype", Promise)
165
+ ], NotificationsController.prototype, "getLogs", null);
152
166
  __decorate([
153
167
  (0, common_1.Get)(),
154
168
  __param(0, (0, common_1.Query)('agent_name')),
@@ -185,6 +199,7 @@ __decorate([
185
199
  exports.NotificationsController = NotificationsController = NotificationsController_1 = __decorate([
186
200
  (0, common_1.Controller)('api/v1/notifications'),
187
201
  __metadata("design:paramtypes", [notification_rules_service_1.NotificationRulesService,
202
+ notification_log_service_1.NotificationLogService,
188
203
  email_provider_config_service_1.EmailProviderConfigService,
189
204
  notification_cron_service_1.NotificationCronService,
190
205
  limit_check_service_1.LimitCheckService])
@@ -85,9 +85,18 @@ let LimitCheckService = LimitCheckService_1 = class LimitCheckService {
85
85
  const now = (0, notification_log_service_1.formatNotificationTimestamp)();
86
86
  const providerConfig = await this.emailProviderConfig.getFullConfig(rule.user_id);
87
87
  const email = await this.notificationLog.resolveUserEmail(rule.user_id, providerConfig?.notificationEmail);
88
- let emailSent = false;
88
+ await this.notificationLog.insertLog({
89
+ ruleId: rule.id,
90
+ periodStart,
91
+ periodEnd,
92
+ actualValue: actual,
93
+ thresholdValue: rule.threshold,
94
+ metricType: rule.metric_type,
95
+ agentName: rule.agent_name,
96
+ sentAt: now,
97
+ });
89
98
  if (email) {
90
- emailSent = await this.emailService.sendThresholdAlert(email, {
99
+ await this.emailService.sendThresholdAlert(email, {
91
100
  agentName: rule.agent_name,
92
101
  metricType: rule.metric_type,
93
102
  threshold: rule.threshold,
@@ -99,18 +108,6 @@ let LimitCheckService = LimitCheckService_1 = class LimitCheckService {
99
108
  periodResetDate: (0, period_util_1.computePeriodResetDate)(rule.period),
100
109
  }, providerConfig ?? undefined);
101
110
  }
102
- if (emailSent || !email) {
103
- await this.notificationLog.insertLog({
104
- ruleId: rule.id,
105
- periodStart,
106
- periodEnd,
107
- actualValue: actual,
108
- thresholdValue: rule.threshold,
109
- metricType: rule.metric_type,
110
- agentName: rule.agent_name,
111
- sentAt: now,
112
- });
113
- }
114
111
  }
115
112
  async getCachedRules(tenantId, agentName) {
116
113
  const key = `${tenantId}:${agentName}`;
@@ -102,9 +102,18 @@ let NotificationCronService = NotificationCronService_1 = class NotificationCron
102
102
  const now = (0, notification_log_service_1.formatNotificationTimestamp)();
103
103
  const fullConfig = await this.emailProviderConfigService.getFullConfig(rule.user_id);
104
104
  const email = await this.notificationLog.resolveUserEmail(rule.user_id, fullConfig?.notificationEmail);
105
- let emailSent = false;
105
+ await this.notificationLog.insertLog({
106
+ ruleId: rule.id,
107
+ periodStart,
108
+ periodEnd,
109
+ actualValue: actual,
110
+ thresholdValue: rule.threshold,
111
+ metricType: rule.metric_type,
112
+ agentName: rule.agent_name,
113
+ sentAt: now,
114
+ });
106
115
  if (email) {
107
- emailSent = await this.emailService.sendThresholdAlert(email, {
116
+ const emailSent = await this.emailService.sendThresholdAlert(email, {
108
117
  agentName: rule.agent_name,
109
118
  metricType: rule.metric_type,
110
119
  threshold: rule.threshold,
@@ -114,26 +123,14 @@ let NotificationCronService = NotificationCronService_1 = class NotificationCron
114
123
  agentUrl: `${this.runtime.getAuthBaseUrl()}/agents/${encodeURIComponent(rule.agent_name)}`,
115
124
  alertType: 'soft',
116
125
  }, fullConfig ?? undefined);
126
+ if (!emailSent) {
127
+ this.logger.warn(`Failed to send alert for rule ${rule.id}, will retry next cron run`);
128
+ }
117
129
  }
118
130
  else {
119
131
  this.logger.warn(`No email found for user ${rule.user_id}, skipping alert for rule ${rule.id}`);
120
132
  }
121
- if (emailSent || !email) {
122
- await this.notificationLog.insertLog({
123
- ruleId: rule.id,
124
- periodStart,
125
- periodEnd,
126
- actualValue: actual,
127
- thresholdValue: rule.threshold,
128
- metricType: rule.metric_type,
129
- agentName: rule.agent_name,
130
- sentAt: now,
131
- });
132
- }
133
- else {
134
- this.logger.warn(`Failed to send alert for rule ${rule.id}, will retry next cron run`);
135
- }
136
- return emailSent || !email;
133
+ return true;
137
134
  }
138
135
  };
139
136
  exports.NotificationCronService = NotificationCronService;
@@ -29,8 +29,9 @@ let NotificationEmailService = NotificationEmailService_1 = class NotificationEm
29
29
  const element = (0, threshold_alert_1.ThresholdAlertEmail)(props);
30
30
  const html = await (0, render_1.render)(element);
31
31
  const text = await (0, render_1.render)(element, { plainText: true });
32
- const prefix = props.alertType === 'soft' ? 'Warning' : 'Alert';
33
- const subject = `${prefix}: ${props.agentName} exceeded ${props.metricType} threshold`;
32
+ const subject = props.alertType === 'soft'
33
+ ? `Warning: ${props.agentName} exceeded ${props.metricType} threshold`
34
+ : `Blocked: ${props.agentName} reached ${props.metricType} limit`;
34
35
  if (providerConfig) {
35
36
  const defaultFrom = this.fromEmail;
36
37
  const from = providerConfig.domain
@@ -51,6 +51,15 @@ let NotificationLogService = class NotificationLogService {
51
51
  params.sentAt,
52
52
  ]);
53
53
  }
54
+ async getLogsForAgent(userId, agentName) {
55
+ return this.ds.query(this.sql(`SELECT nl.id, nl.sent_at, nl.actual_value, nl.threshold_value,
56
+ nl.metric_type, nl.period_start, nl.period_end, nl.agent_name
57
+ FROM notification_logs nl
58
+ JOIN notification_rules nr ON nr.id = nl.rule_id
59
+ WHERE nr.user_id = $1 AND nl.agent_name = $2
60
+ ORDER BY nl.sent_at DESC
61
+ LIMIT 50`), [userId, agentName]);
62
+ }
54
63
  async resolveUserEmail(userId, notificationEmail) {
55
64
  if (notificationEmail)
56
65
  return notificationEmail;
@@ -34,7 +34,7 @@ function extractSystemBlocks(messages) {
34
34
  }
35
35
  function toContentBlocks(content) {
36
36
  if (typeof content === 'string')
37
- return [{ type: 'text', text: content }];
37
+ return content ? [{ type: 'text', text: content }] : [];
38
38
  if (Array.isArray(content)) {
39
39
  return content
40
40
  .filter((b) => b.type === 'text' && typeof b.text === 'string')
@@ -98,8 +98,6 @@ function toAnthropicRequest(body, _model, options) {
98
98
  messages: converted,
99
99
  max_tokens: body.max_tokens || 4096,
100
100
  };
101
- if (shouldCache)
102
- result.cache_control = { type: 'ephemeral' };
103
101
  if (systemBlocks.length > 0)
104
102
  result.system = systemBlocks;
105
103
  const tools = convertTools(body.tools);