manifest 5.24.0 → 5.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +1 -0
  2. package/dist/backend/analytics/controllers/agent-analytics.controller.js +7 -1
  3. package/dist/backend/analytics/controllers/agents.controller.js +16 -6
  4. package/dist/backend/common/utils/product-telemetry.js +5 -2
  5. package/dist/backend/common/utils/sql-dialect.js +14 -11
  6. package/dist/backend/database/migrations/1772960000000-PurgeNonCuratedModels.js +4 -0
  7. package/dist/backend/database/seed-models.js +25 -3
  8. package/dist/backend/model-prices/model-name-normalizer.js +22 -0
  9. package/dist/backend/notifications/emails/threshold-alert.js +3 -2
  10. package/dist/backend/otlp/guards/otlp-auth.guard.js +37 -1
  11. package/dist/backend/otlp/otlp.controller.js +0 -3
  12. package/dist/backend/routing/proxy/google-adapter.js +42 -1
  13. package/dist/backend/routing/proxy/proxy.service.js +12 -13
  14. package/dist/backend/routing/resolve.controller.js +8 -2
  15. package/dist/backend/routing/routing.controller.js +2 -1
  16. package/dist/backend/routing/routing.service.js +44 -0
  17. package/dist/index.js +13 -13
  18. package/package.json +1 -1
  19. package/public/assets/{Account-BAroz-2n.js → Account-D79xeKyT.js} +1 -1
  20. package/public/assets/{Limits-_v1HGbea.js → Limits-68Jol4KI.js} +1 -1
  21. package/public/assets/{Login-C6lI_KqN.js → Login-q9JoAR_x.js} +1 -1
  22. package/public/assets/{MessageLog-BfxZUc6f.js → MessageLog-CUR9du7l.js} +1 -1
  23. package/public/assets/{ModelPrices-DlrK7bqu.js → ModelPrices-Dfn6eK1-.js} +1 -1
  24. package/public/assets/Overview-QSa9JMZx.js +1 -0
  25. package/public/assets/{Register-B-nLlx_2.js → Register-DH-OYkxl.js} +1 -1
  26. package/public/assets/{ResetPassword-CJEPkh6G.js → ResetPassword-Bd22sqer.js} +1 -1
  27. package/public/assets/{Routing-D1z4fqN4.js → Routing-C-8PKTEa.js} +1 -1
  28. package/public/assets/{Settings-DBVVmq_T.js → Settings-B7NcLutw.js} +1 -1
  29. package/public/assets/{SocialButtons-BQ4JTq-l.js → SocialButtons-BmEEEVZg.js} +1 -1
  30. package/public/assets/index-DBqZDW9Z.css +1 -0
  31. package/public/assets/{index-BhcU06cr.js → index-DKNLgS0h.js} +2 -2
  32. package/public/assets/{overview-_flXQc4-.js → overview-B6JfuXXv.js} +1 -1
  33. package/public/index.html +2 -2
  34. package/public/assets/Overview-CfezCKlq.js +0 -1
  35. package/public/assets/index-D4x6Xoo9.css +0 -1
package/README.md CHANGED
@@ -57,6 +57,7 @@ Manifest is available in cloud and local versions. While both versions install t
57
57
  - You don't want the telemetry data to move from your computer
58
58
  - You don’t need multi-device access
59
59
  - You don't want to subscribe to a cloud service
60
+ - You are using a local model like Ollama
60
61
 
61
62
  If you don't know which version to choose, start with the **cloud version**.
62
63
 
@@ -21,6 +21,7 @@ const agent_analytics_service_1 = require("../services/agent-analytics.service")
21
21
  const range_query_dto_1 = require("../../common/dto/range-query.dto");
22
22
  const agent_cache_interceptor_1 = require("../../common/interceptors/agent-cache.interceptor");
23
23
  const cache_constants_1 = require("../../common/constants/cache.constants");
24
+ const product_telemetry_1 = require("../../common/utils/product-telemetry");
24
25
  let AgentAnalyticsController = class AgentAnalyticsController {
25
26
  analytics;
26
27
  constructor(analytics) {
@@ -29,7 +30,12 @@ let AgentAnalyticsController = class AgentAnalyticsController {
29
30
  async getUsage(query, req) {
30
31
  const range = query.range ?? '24h';
31
32
  const ctx = req.ingestionContext;
32
- return this.analytics.getUsage(range, ctx);
33
+ const usage = await this.analytics.getUsage(range, ctx);
34
+ return {
35
+ ...usage,
36
+ agentName: ctx.agentName,
37
+ telemetryId: (0, product_telemetry_1.hashForTelemetry)(ctx.userId),
38
+ };
33
39
  }
34
40
  async getCosts(query, req) {
35
41
  const range = query.range ?? '7d';
@@ -14,6 +14,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.AgentsController = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
+ const typeorm_1 = require("typeorm");
17
18
  const cache_manager_1 = require("@nestjs/cache-manager");
18
19
  const config_1 = require("@nestjs/config");
19
20
  const timeseries_queries_service_1 = require("../services/timeseries-queries.service");
@@ -52,12 +53,21 @@ let AgentsController = class AgentsController {
52
53
  throw new common_1.BadRequestException('Agent name produces an empty slug');
53
54
  }
54
55
  const displayName = body.name.trim();
55
- const result = await this.apiKeyGenerator.onboardAgent({
56
- tenantName: user.id,
57
- agentName: slug,
58
- displayName,
59
- email: user.email,
60
- });
56
+ let result;
57
+ try {
58
+ result = await this.apiKeyGenerator.onboardAgent({
59
+ tenantName: user.id,
60
+ agentName: slug,
61
+ displayName,
62
+ email: user.email,
63
+ });
64
+ }
65
+ catch (error) {
66
+ if (error instanceof typeorm_1.QueryFailedError && /unique|duplicate/i.test(error.message)) {
67
+ throw new common_1.ConflictException(`Agent "${slug}" already exists`);
68
+ }
69
+ throw error;
70
+ }
61
71
  (0, product_telemetry_1.trackCloudEvent)('agent_created', user.id, { agent_name: slug });
62
72
  return {
63
73
  agent: { id: result.agentId, name: slug, display_name: displayName },
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getMachineId = getMachineId;
4
4
  exports.trackEvent = trackEvent;
5
+ exports.hashForTelemetry = hashForTelemetry;
5
6
  exports.trackCloudEvent = trackCloudEvent;
6
7
  const crypto_1 = require("crypto");
7
8
  const os_1 = require("os");
@@ -34,13 +35,15 @@ function trackEvent(event, properties) {
34
35
  ...properties,
35
36
  });
36
37
  }
38
+ function hashForTelemetry(id) {
39
+ return (0, crypto_1.createHash)('sha256').update(id).digest('hex').slice(0, 16);
40
+ }
37
41
  function trackCloudEvent(event, tenantId, properties) {
38
42
  if (isOptedOut())
39
43
  return;
40
- const hashedTenant = (0, crypto_1.createHash)('sha256').update(tenantId).digest('hex').slice(0, 16);
41
44
  const version = getPackageVersion();
42
45
  (0, posthog_sender_1.sendToPostHog)(event, {
43
- distinct_id: hashedTenant,
46
+ distinct_id: hashForTelemetry(tenantId),
44
47
  os: (0, os_1.platform)(),
45
48
  os_version: (0, os_1.release)(),
46
49
  node_version: process.versions.node,
@@ -18,13 +18,12 @@ function timestampType() {
18
18
  return process.env['MANIFEST_MODE'] === 'local' ? 'datetime' : 'timestamp';
19
19
  }
20
20
  function timestampDefault() {
21
- return process.env['MANIFEST_MODE'] === 'local'
22
- ? () => 'CURRENT_TIMESTAMP'
23
- : () => 'NOW()';
21
+ return process.env['MANIFEST_MODE'] === 'local' ? () => 'CURRENT_TIMESTAMP' : () => 'NOW()';
24
22
  }
25
23
  function computeCutoff(interval) {
26
24
  const ms = intervalToMs(interval);
27
- return new Date(Date.now() - ms).toISOString();
25
+ const cutoff = new Date(Date.now() - ms);
26
+ return formatLocalIso(cutoff);
28
27
  }
29
28
  function intervalToMs(interval) {
30
29
  const match = interval.match(/^(\d+)\s+(hour|hours|day|days)$/);
@@ -37,12 +36,13 @@ function intervalToMs(interval) {
37
36
  return unit.startsWith('hour') ? n * 3600_000 : n * 86400_000;
38
37
  }
39
38
  function sqlNow() {
40
- return new Date().toISOString();
39
+ return formatLocalIso(new Date());
41
40
  }
42
41
  function sqlHourBucket(col, dialect) {
43
- return dialect === 'sqlite'
44
- ? `strftime('%Y-%m-%dT%H:00:00', ${col})`
45
- : `to_char(date_trunc('hour', ${col}), 'YYYY-MM-DD"T"HH24:MI:SS')`;
42
+ if (dialect === 'sqlite') {
43
+ return `strftime('%Y-%m-%dT%H:00:00', ${col})`;
44
+ }
45
+ return `to_char(date_trunc('hour', ${col}), 'YYYY-MM-DD"T"HH24:MI:SS')`;
46
46
  }
47
47
  function sqlDateBucket(col, dialect) {
48
48
  return dialect === 'sqlite'
@@ -61,8 +61,11 @@ function portableSql(sql, dialect) {
61
61
  return sql.replace(/\$\d+/g, '?');
62
62
  }
63
63
  function sqlCastInterval(paramName, dialect) {
64
- return dialect === 'sqlite'
65
- ? `:${paramName}`
66
- : `CAST(:${paramName} AS interval)`;
64
+ return dialect === 'sqlite' ? `:${paramName}` : `CAST(:${paramName} AS interval)`;
65
+ }
66
+ function formatLocalIso(d) {
67
+ const pad = (n) => String(n).padStart(2, '0');
68
+ return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
69
+ `T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`);
67
70
  }
68
71
  //# sourceMappingURL=sql-dialect.js.map
@@ -3,8 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PurgeNonCuratedModels1772960000000 = void 0;
4
4
  const CURATED_MODELS = [
5
5
  'claude-opus-4-6',
6
+ 'claude-sonnet-4-6',
6
7
  'claude-sonnet-4-5-20250929',
8
+ 'claude-opus-4-5-20251101',
9
+ 'claude-opus-4-1-20250805',
7
10
  'claude-sonnet-4-20250514',
11
+ 'claude-opus-4-20250514',
8
12
  'claude-haiku-4-5-20251001',
9
13
  'gpt-4o',
10
14
  'gpt-4o-mini',
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SEED_MODELS = void 0;
4
4
  exports.SEED_MODELS = [
5
- ['claude-opus-4-6', 'Anthropic', 0.000015, 0.000075, 200000, true, true, 'Claude Opus 4.6'],
5
+ ['claude-opus-4-6', 'Anthropic', 0.000005, 0.000025, 200000, true, true, 'Claude Opus 4.6'],
6
+ ['claude-sonnet-4-6', 'Anthropic', 0.000003, 0.000015, 200000, true, true, 'Claude Sonnet 4.6'],
6
7
  [
7
8
  'claude-sonnet-4-5-20250929',
8
9
  'Anthropic',
@@ -13,6 +14,26 @@ exports.SEED_MODELS = [
13
14
  true,
14
15
  'Claude Sonnet 4.5',
15
16
  ],
17
+ [
18
+ 'claude-opus-4-5-20251101',
19
+ 'Anthropic',
20
+ 0.000015,
21
+ 0.000075,
22
+ 200000,
23
+ true,
24
+ true,
25
+ 'Claude Opus 4.5',
26
+ ],
27
+ [
28
+ 'claude-opus-4-1-20250805',
29
+ 'Anthropic',
30
+ 0.000015,
31
+ 0.000075,
32
+ 200000,
33
+ true,
34
+ true,
35
+ 'Claude Opus 4.1',
36
+ ],
16
37
  [
17
38
  'claude-sonnet-4-20250514',
18
39
  'Anthropic',
@@ -23,6 +44,7 @@ exports.SEED_MODELS = [
23
44
  true,
24
45
  'Claude Sonnet 4',
25
46
  ],
47
+ ['claude-opus-4-20250514', 'Anthropic', 0.000015, 0.000075, 200000, true, true, 'Claude Opus 4'],
26
48
  [
27
49
  'claude-haiku-4-5-20251001',
28
50
  'Anthropic',
@@ -100,8 +122,8 @@ exports.SEED_MODELS = [
100
122
  [
101
123
  'anthropic/claude-opus-4-6',
102
124
  'OpenRouter',
103
- 0.000015,
104
- 0.000075,
125
+ 0.000005,
126
+ 0.000025,
105
127
  200000,
106
128
  true,
107
129
  true,
@@ -3,12 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.stripProviderPrefix = stripProviderPrefix;
4
4
  exports.stripDateSuffix = stripDateSuffix;
5
5
  exports.buildAliasMap = buildAliasMap;
6
+ exports.normalizeDots = normalizeDots;
6
7
  exports.resolveModelName = resolveModelName;
7
8
  const KNOWN_ALIASES = [
8
9
  ['claude-opus-4', 'claude-opus-4-6'],
9
10
  ['claude-sonnet-4.5', 'claude-sonnet-4-5-20250929'],
11
+ ['claude-sonnet-4-5', 'claude-sonnet-4-5-20250929'],
12
+ ['claude-opus-4-5', 'claude-opus-4-5-20251101'],
13
+ ['claude-opus-4-1', 'claude-opus-4-1-20250805'],
14
+ ['claude-sonnet-4-0', 'claude-sonnet-4-20250514'],
15
+ ['claude-opus-4-0', 'claude-opus-4-20250514'],
10
16
  ['claude-sonnet-4', 'claude-sonnet-4-20250514'],
11
17
  ['claude-haiku-4.5', 'claude-haiku-4-5-20251001'],
18
+ ['claude-haiku-4-5', 'claude-haiku-4-5-20251001'],
12
19
  ['deepseek-v3', 'deepseek-chat'],
13
20
  ['deepseek-chat-v3-0324', 'deepseek-chat'],
14
21
  ['deepseek-r1', 'deepseek-reasoner'],
@@ -67,6 +74,9 @@ function buildAliasMap(canonicalNames) {
67
74
  }
68
75
  return map;
69
76
  }
77
+ function normalizeDots(name) {
78
+ return name.replace(/\./g, '-');
79
+ }
70
80
  function resolveModelName(name, aliasMap) {
71
81
  const exact = aliasMap.get(name);
72
82
  if (exact)
@@ -81,6 +91,18 @@ function resolveModelName(name, aliasMap) {
81
91
  if (fromNoDate)
82
92
  return fromNoDate;
83
93
  }
94
+ const dotNorm = normalizeDots(stripped);
95
+ if (dotNorm !== stripped) {
96
+ const fromDotNorm = aliasMap.get(dotNorm);
97
+ if (fromDotNorm)
98
+ return fromDotNorm;
99
+ const dotNormNoDate = stripDateSuffix(dotNorm);
100
+ if (dotNormNoDate !== dotNorm) {
101
+ const fromDotNormNoDate = aliasMap.get(dotNormNoDate);
102
+ if (fromDotNormNoDate)
103
+ return fromDotNormNoDate;
104
+ }
105
+ }
84
106
  return undefined;
85
107
  }
86
108
  //# sourceMappingURL=model-name-normalizer.js.map
@@ -17,9 +17,10 @@ function formatTimestamp(raw) {
17
17
  return `${monthName} ${dayNum}, ${timePart}`;
18
18
  }
19
19
  function formatValue(value, metric) {
20
+ const num = Number(value);
20
21
  if (metric === 'cost')
21
- return `$${value.toFixed(4)}`;
22
- return value.toLocaleString();
22
+ return `$${num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
23
+ return num.toLocaleString(undefined, { maximumFractionDigits: 0 });
23
24
  }
24
25
  function ThresholdAlertEmail(props) {
25
26
  const { agentName, metricType, threshold, actualValue, period, timestamp, agentUrl, logoUrl = 'https://app.manifest.build/manifest-logo.png', alertType = 'hard', periodResetDate, } = props;
@@ -26,6 +26,7 @@ let OtlpAuthGuard = OtlpAuthGuard_1 = class OtlpAuthGuard {
26
26
  keyRepo;
27
27
  logger = new common_1.Logger(OtlpAuthGuard_1.name);
28
28
  cache = new Map();
29
+ devContext = null;
29
30
  CACHE_TTL_MS = 5 * 60 * 1000;
30
31
  MAX_CACHE_SIZE = 10_000;
31
32
  cleanupTimer;
@@ -42,7 +43,9 @@ let OtlpAuthGuard = OtlpAuthGuard_1 = class OtlpAuthGuard {
42
43
  async canActivate(context) {
43
44
  const request = context.switchToHttp().getRequest();
44
45
  const authHeader = request.headers['authorization'];
45
- const isLocal = process.env['MANIFEST_MODE'] === 'local' && LOOPBACK_IPS.has(request.ip ?? '');
46
+ const isLoopback = LOOPBACK_IPS.has(request.ip ?? '');
47
+ const isLocal = process.env['MANIFEST_MODE'] === 'local' && isLoopback;
48
+ const isDevLoopback = process.env['NODE_ENV'] === 'development' && isLoopback;
46
49
  if (!authHeader && isLocal) {
47
50
  request.ingestionContext = {
48
51
  tenantId: local_mode_constants_1.LOCAL_TENANT_ID,
@@ -52,6 +55,13 @@ let OtlpAuthGuard = OtlpAuthGuard_1 = class OtlpAuthGuard {
52
55
  };
53
56
  return true;
54
57
  }
58
+ if (!authHeader && isDevLoopback) {
59
+ const devCtx = await this.resolveDevContext();
60
+ if (devCtx) {
61
+ request.ingestionContext = devCtx;
62
+ return true;
63
+ }
64
+ }
55
65
  if (!authHeader) {
56
66
  this.logger.warn(`OTLP request without auth from ${request.ip}`);
57
67
  throw new common_1.UnauthorizedException('Authorization header required');
@@ -70,6 +80,13 @@ let OtlpAuthGuard = OtlpAuthGuard_1 = class OtlpAuthGuard {
70
80
  };
71
81
  return true;
72
82
  }
83
+ if (isDevLoopback) {
84
+ const devCtx = await this.resolveDevContext();
85
+ if (devCtx) {
86
+ request.ingestionContext = devCtx;
87
+ return true;
88
+ }
89
+ }
73
90
  throw new common_1.UnauthorizedException('Invalid API key format');
74
91
  }
75
92
  const cached = this.cache.get(token);
@@ -127,6 +144,25 @@ let OtlpAuthGuard = OtlpAuthGuard_1 = class OtlpAuthGuard {
127
144
  clearCache() {
128
145
  this.cache.clear();
129
146
  }
147
+ async resolveDevContext() {
148
+ if (this.devContext && this.devContext.expiresAt > Date.now()) {
149
+ return this.devContext.context;
150
+ }
151
+ const keyRecord = await this.keyRepo.findOne({
152
+ where: { is_active: true },
153
+ relations: ['agent', 'tenant'],
154
+ });
155
+ if (!keyRecord)
156
+ return null;
157
+ const ctx = {
158
+ tenantId: keyRecord.tenant_id,
159
+ agentId: keyRecord.agent_id,
160
+ agentName: keyRecord.agent.name,
161
+ userId: keyRecord.tenant.name,
162
+ };
163
+ this.devContext = { context: ctx, expiresAt: Date.now() + this.CACHE_TTL_MS };
164
+ return ctx;
165
+ }
130
166
  evictExpired() {
131
167
  const now = Date.now();
132
168
  for (const [key, entry] of this.cache) {
@@ -91,9 +91,6 @@ let OtlpController = OtlpController_1 = class OtlpController {
91
91
  if (this.seenAgents.has(ctx.agentId))
92
92
  return;
93
93
  this.seenAgents.add(ctx.agentId);
94
- (0, product_telemetry_1.trackCloudEvent)('plugin_registered', ctx.userId, {
95
- source: 'backend',
96
- });
97
94
  (0, product_telemetry_1.trackCloudEvent)('first_telemetry_received', ctx.userId, {
98
95
  agent_id_hash: ctx.agentId.slice(0, 8),
99
96
  });
@@ -4,6 +4,47 @@ exports.toGoogleRequest = toGoogleRequest;
4
4
  exports.fromGoogleResponse = fromGoogleResponse;
5
5
  exports.transformGoogleStreamChunk = transformGoogleStreamChunk;
6
6
  const crypto_1 = require("crypto");
7
+ const UNSUPPORTED_SCHEMA_FIELDS = new Set([
8
+ 'patternProperties',
9
+ 'additionalProperties',
10
+ '$schema',
11
+ '$id',
12
+ '$ref',
13
+ '$defs',
14
+ 'definitions',
15
+ 'allOf',
16
+ 'anyOf',
17
+ 'oneOf',
18
+ 'not',
19
+ 'if',
20
+ 'then',
21
+ 'else',
22
+ 'dependentSchemas',
23
+ 'dependentRequired',
24
+ 'unevaluatedProperties',
25
+ 'unevaluatedItems',
26
+ 'contentMediaType',
27
+ 'contentEncoding',
28
+ 'examples',
29
+ 'default',
30
+ 'const',
31
+ 'title',
32
+ ]);
33
+ function sanitizeSchema(schema, isPropertiesMap = false) {
34
+ if (schema === null || schema === undefined || typeof schema !== 'object') {
35
+ return schema;
36
+ }
37
+ if (Array.isArray(schema)) {
38
+ return schema.map((item) => sanitizeSchema(item));
39
+ }
40
+ const result = {};
41
+ for (const [key, value] of Object.entries(schema)) {
42
+ if (!isPropertiesMap && UNSUPPORTED_SCHEMA_FIELDS.has(key))
43
+ continue;
44
+ result[key] = sanitizeSchema(value, key === 'properties');
45
+ }
46
+ return result;
47
+ }
7
48
  function mapRole(role) {
8
49
  if (role === 'assistant')
9
50
  return 'model';
@@ -64,7 +105,7 @@ function convertTools(tools) {
64
105
  return {
65
106
  name: fn.name,
66
107
  description: fn.description,
67
- parameters: fn.parameters,
108
+ parameters: fn.parameters ? sanitizeSchema(fn.parameters) : undefined,
68
109
  };
69
110
  })
70
111
  .filter(Boolean);
@@ -173,11 +173,11 @@ let ProxyService = ProxyService_1 = class ProxyService {
173
173
  if (!exceeded)
174
174
  return;
175
175
  const fmt = exceeded.metricType === 'cost'
176
- ? `$${exceeded.actual.toFixed(2)}`
177
- : exceeded.actual.toLocaleString();
176
+ ? `$${Number(exceeded.actual).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
177
+ : Number(exceeded.actual).toLocaleString(undefined, { maximumFractionDigits: 0 });
178
178
  const threshFmt = exceeded.metricType === 'cost'
179
- ? `$${exceeded.threshold.toFixed(2)}`
180
- : exceeded.threshold.toLocaleString();
179
+ ? `$${Number(exceeded.threshold).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
180
+ : Number(exceeded.threshold).toLocaleString(undefined, { maximumFractionDigits: 0 });
181
181
  throw new common_1.HttpException({
182
182
  error: {
183
183
  message: `Limit exceeded: ${exceeded.metricType} usage (${fmt}) exceeds ${threshFmt} per ${exceeded.period}`,
@@ -192,16 +192,15 @@ let ProxyService = ProxyService_1 = class ProxyService {
192
192
  .slice(-SCORING_RECENT_MESSAGES);
193
193
  }
194
194
  detectHeartbeat(scoringMessages) {
195
- return scoringMessages.some((m) => {
196
- if (m.role !== 'user')
197
- return false;
198
- if (typeof m.content === 'string')
199
- return m.content.includes('HEARTBEAT_OK');
200
- if (Array.isArray(m.content)) {
201
- return m.content.some((p) => p.type === 'text' && typeof p.text === 'string' && p.text.includes('HEARTBEAT_OK'));
202
- }
195
+ const lastUser = [...scoringMessages].reverse().find((m) => m.role === 'user');
196
+ if (!lastUser)
203
197
  return false;
204
- });
198
+ if (typeof lastUser.content === 'string')
199
+ return lastUser.content.includes('HEARTBEAT_OK');
200
+ if (Array.isArray(lastUser.content)) {
201
+ return lastUser.content.some((p) => p.type === 'text' && typeof p.text === 'string' && p.text.includes('HEARTBEAT_OK'));
202
+ }
203
+ return false;
205
204
  }
206
205
  async forwardToProvider(provider, apiKey, model, body, stream, sessionKey, signal, authType) {
207
206
  const extraHeaders = {};
@@ -21,6 +21,7 @@ const otlp_auth_guard_1 = require("../otlp/guards/otlp-auth.guard");
21
21
  const resolve_service_1 = require("./resolve.service");
22
22
  const routing_service_1 = require("./routing.service");
23
23
  const resolve_request_dto_1 = require("./dto/resolve-request.dto");
24
+ const product_telemetry_1 = require("../common/utils/product-telemetry");
24
25
  class SubscriptionProviderItem {
25
26
  provider;
26
27
  }
@@ -55,8 +56,13 @@ let ResolveController = class ResolveController {
55
56
  const { agentId, userId } = req.ingestionContext;
56
57
  let registered = 0;
57
58
  for (const item of body.providers) {
58
- await this.routingService.upsertProvider(agentId, userId, item.provider, undefined, 'subscription');
59
- registered++;
59
+ const { isNew } = await this.routingService.registerSubscriptionProvider(agentId, userId, item.provider);
60
+ if (isNew) {
61
+ (0, product_telemetry_1.trackCloudEvent)('routing_provider_connected', userId, {
62
+ provider: `${item.provider} (Subscription)`,
63
+ });
64
+ registered++;
65
+ }
60
66
  }
61
67
  return { registered };
62
68
  }
@@ -62,8 +62,9 @@ let RoutingController = class RoutingController {
62
62
  }
63
63
  const { provider: result, isNew } = await this.routingService.upsertProvider(agent.id, user.id, body.provider, body.apiKey, body.authType);
64
64
  if (isNew) {
65
+ const providerLabel = body.authType === 'subscription' ? `${body.provider} (Subscription)` : body.provider;
65
66
  (0, product_telemetry_1.trackCloudEvent)('routing_provider_connected', user.id, {
66
- provider: body.provider,
67
+ provider: providerLabel,
67
68
  });
68
69
  }
69
70
  return {
@@ -69,6 +69,9 @@ let RoutingService = RoutingService_1 = class RoutingService {
69
69
  existing.is_active = true;
70
70
  existing.updated_at = new Date().toISOString();
71
71
  await this.providerRepo.save(existing);
72
+ if (effectiveAuthType === 'api_key') {
73
+ await this.deactivateSubscriptionForProvider(agentId, provider);
74
+ }
72
75
  await this.autoAssign.recalculate(agentId);
73
76
  this.routingCache.invalidateAgent(agentId);
74
77
  return { provider: existing, isNew: false };
@@ -86,10 +89,51 @@ let RoutingService = RoutingService_1 = class RoutingService {
86
89
  updated_at: new Date().toISOString(),
87
90
  });
88
91
  await this.providerRepo.insert(record);
92
+ if (effectiveAuthType === 'api_key') {
93
+ await this.deactivateSubscriptionForProvider(agentId, provider);
94
+ }
89
95
  await this.autoAssign.recalculate(agentId);
90
96
  this.routingCache.invalidateAgent(agentId);
91
97
  return { provider: record, isNew: true };
92
98
  }
99
+ async registerSubscriptionProvider(agentId, userId, provider) {
100
+ const existing = await this.providerRepo.findOne({
101
+ where: { agent_id: agentId, provider, auth_type: 'subscription' },
102
+ });
103
+ if (existing)
104
+ return { isNew: false };
105
+ const hasApiKey = await this.providerRepo.findOne({
106
+ where: { agent_id: agentId, provider, auth_type: 'api_key', is_active: true },
107
+ });
108
+ if (hasApiKey)
109
+ return { isNew: false };
110
+ const record = Object.assign(new user_provider_entity_1.UserProvider(), {
111
+ id: (0, crypto_1.randomUUID)(),
112
+ user_id: userId,
113
+ agent_id: agentId,
114
+ provider,
115
+ auth_type: 'subscription',
116
+ api_key_encrypted: null,
117
+ key_prefix: null,
118
+ is_active: true,
119
+ connected_at: new Date().toISOString(),
120
+ updated_at: new Date().toISOString(),
121
+ });
122
+ await this.providerRepo.insert(record);
123
+ await this.autoAssign.recalculate(agentId);
124
+ this.routingCache.invalidateAgent(agentId);
125
+ return { isNew: true };
126
+ }
127
+ async deactivateSubscriptionForProvider(agentId, provider) {
128
+ const sub = await this.providerRepo.findOne({
129
+ where: { agent_id: agentId, provider, auth_type: 'subscription', is_active: true },
130
+ });
131
+ if (!sub)
132
+ return;
133
+ sub.is_active = false;
134
+ sub.updated_at = new Date().toISOString();
135
+ await this.providerRepo.save(sub);
136
+ }
93
137
  async removeProvider(agentId, provider, authType) {
94
138
  const where = { agent_id: agentId, provider };
95
139
  if (authType)