@axiom-lattice/pg-stores 3.1.20 → 3.2.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.
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ __export(index_exports, {
37
37
  MenuStore: () => MenuStore,
38
38
  MigrationManager: () => MigrationManager,
39
39
  PGVectorStoreProvider: () => PGVectorStoreProvider,
40
- Pool: () => import_pg30.Pool,
40
+ Pool: () => import_pg31.Pool,
41
41
  PostgreSQLA2AApiKeyStore: () => PostgreSQLA2AApiKeyStore,
42
42
  PostgreSQLAgentWebAppStore: () => PostgreSQLAgentWebAppStore,
43
43
  PostgreSQLAssistantStore: () => PostgreSQLAssistantStore,
@@ -49,6 +49,7 @@ __export(index_exports, {
49
49
  PostgreSQLEvalStore: () => PostgreSQLEvalStore,
50
50
  PostgreSQLMcpServerConfigStore: () => PostgreSQLMcpServerConfigStore,
51
51
  PostgreSQLMetricsServerConfigStore: () => PostgreSQLMetricsServerConfigStore,
52
+ PostgreSQLModelProviderStore: () => PostgreSQLModelProviderStore,
52
53
  PostgreSQLOpenAuditStore: () => PostgreSQLOpenAuditStore,
53
54
  PostgreSQLProjectBotMembershipStore: () => PostgreSQLProjectBotMembershipStore,
54
55
  PostgreSQLProjectMembershipStore: () => PostgreSQLProjectMembershipStore,
@@ -126,10 +127,10 @@ __export(index_exports, {
126
127
  safeParse: () => safeParse
127
128
  });
128
129
  module.exports = __toCommonJS(index_exports);
129
- var import_pg30 = require("pg");
130
+ var import_pg31 = require("pg");
130
131
 
131
132
  // src/createPgStoreConfig.ts
132
- var import_pg27 = require("pg");
133
+ var import_pg28 = require("pg");
133
134
 
134
135
  // src/migrations/migration.ts
135
136
  var MigrationManager = class {
@@ -10079,6 +10080,116 @@ var createUserPushSubscriptions = {
10079
10080
  }
10080
10081
  };
10081
10082
 
10083
+ // src/migrations/model_provider_migrations.ts
10084
+ var createModelProvidersTable = {
10085
+ version: 181,
10086
+ name: "create_model_providers_table",
10087
+ up: async (client) => {
10088
+ await client.query(`
10089
+ CREATE TABLE IF NOT EXISTS lattice_model_providers (
10090
+ id VARCHAR(255) NOT NULL,
10091
+ tenant_id VARCHAR(255) NOT NULL,
10092
+ name VARCHAR(255) NOT NULL,
10093
+ display_name VARCHAR(255),
10094
+ protocol VARCHAR(64) NOT NULL,
10095
+ api_style VARCHAR(32) NOT NULL DEFAULT 'chat-completions',
10096
+ llm_provider VARCHAR(64) NOT NULL,
10097
+ base_url TEXT NOT NULL,
10098
+ api_key_enc TEXT,
10099
+ api_key_hint VARCHAR(16),
10100
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
10101
+ status VARCHAR(32) NOT NULL DEFAULT 'unknown',
10102
+ last_error VARCHAR(255),
10103
+ last_discovered_at TIMESTAMP,
10104
+ created_by VARCHAR(255),
10105
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10106
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10107
+
10108
+ PRIMARY KEY (tenant_id, id),
10109
+ CONSTRAINT uk_lattice_model_providers_tenant_name UNIQUE (tenant_id, name)
10110
+ )
10111
+ `);
10112
+ await client.query(`
10113
+ CREATE INDEX IF NOT EXISTS idx_lattice_model_providers_tenant_id
10114
+ ON lattice_model_providers(tenant_id)
10115
+ `);
10116
+ },
10117
+ down: async (client) => {
10118
+ await client.query("DROP INDEX IF EXISTS idx_lattice_model_providers_tenant_id");
10119
+ await client.query("DROP TABLE IF EXISTS lattice_model_providers");
10120
+ }
10121
+ };
10122
+ var createModelProviderModelsTable = {
10123
+ version: 182,
10124
+ name: "create_model_provider_models_table",
10125
+ up: async (client) => {
10126
+ await client.query(`
10127
+ CREATE TABLE IF NOT EXISTS lattice_model_provider_models (
10128
+ id VARCHAR(255) NOT NULL,
10129
+ tenant_id VARCHAR(255) NOT NULL,
10130
+ provider_id VARCHAR(255) NOT NULL,
10131
+ model_id VARCHAR(512) NOT NULL,
10132
+ display_name VARCHAR(255),
10133
+ owned_by VARCHAR(255),
10134
+ upstream_created TIMESTAMP,
10135
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
10136
+ raw JSONB,
10137
+ discovered_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10138
+
10139
+ PRIMARY KEY (tenant_id, provider_id, model_id)
10140
+ )
10141
+ `);
10142
+ await client.query(`
10143
+ CREATE INDEX IF NOT EXISTS idx_lattice_model_provider_models_tenant_id
10144
+ ON lattice_model_provider_models(tenant_id)
10145
+ `);
10146
+ await client.query(`
10147
+ CREATE INDEX IF NOT EXISTS idx_lattice_model_provider_models_tenant_model
10148
+ ON lattice_model_provider_models(tenant_id, model_id)
10149
+ `);
10150
+ await client.query(`
10151
+ CREATE INDEX IF NOT EXISTS idx_lattice_model_provider_models_tenant_provider
10152
+ ON lattice_model_provider_models(tenant_id, provider_id)
10153
+ `);
10154
+ },
10155
+ down: async (client) => {
10156
+ await client.query("DROP INDEX IF EXISTS idx_lattice_model_provider_models_tenant_provider");
10157
+ await client.query("DROP INDEX IF EXISTS idx_lattice_model_provider_models_tenant_model");
10158
+ await client.query("DROP INDEX IF EXISTS idx_lattice_model_provider_models_tenant_id");
10159
+ await client.query("DROP TABLE IF EXISTS lattice_model_provider_models");
10160
+ }
10161
+ };
10162
+ var addModelProviderApiStyle = {
10163
+ version: 183,
10164
+ name: "add_model_provider_api_style",
10165
+ up: async (client) => {
10166
+ await client.query(`
10167
+ ALTER TABLE lattice_model_providers
10168
+ ADD COLUMN IF NOT EXISTS api_style VARCHAR(32) NOT NULL DEFAULT 'chat-completions'
10169
+ `);
10170
+ },
10171
+ down: async (client) => {
10172
+ await client.query(`
10173
+ ALTER TABLE lattice_model_providers DROP COLUMN IF EXISTS api_style
10174
+ `);
10175
+ }
10176
+ };
10177
+ var addModelProviderModelStale = {
10178
+ version: 184,
10179
+ name: "add_model_provider_model_stale",
10180
+ up: async (client) => {
10181
+ await client.query(`
10182
+ ALTER TABLE lattice_model_provider_models
10183
+ ADD COLUMN IF NOT EXISTS stale BOOLEAN NOT NULL DEFAULT FALSE
10184
+ `);
10185
+ },
10186
+ down: async (client) => {
10187
+ await client.query(`
10188
+ ALTER TABLE lattice_model_provider_models DROP COLUMN IF EXISTS stale
10189
+ `);
10190
+ }
10191
+ };
10192
+
10082
10193
  // src/stores/PostgreSQLCapabilityBundleStore.ts
10083
10194
  var import_crypto4 = require("crypto");
10084
10195
  var import_pg26 = require("pg");
@@ -11040,9 +11151,513 @@ var PostgreSQLUserPushSubscriptionStore = class {
11040
11151
  }
11041
11152
  };
11042
11153
 
11154
+ // src/stores/PostgreSQLModelProviderStore.ts
11155
+ var import_pg27 = require("pg");
11156
+ var import_core7 = require("@axiom-lattice/core");
11157
+ var PostgreSQLModelProviderStore = class {
11158
+ constructor(options) {
11159
+ this.initialized = false;
11160
+ this.ownsPool = true;
11161
+ this.initPromise = null;
11162
+ if (options.pool) {
11163
+ this.pool = options.pool;
11164
+ this.ownsPool = false;
11165
+ this.initialized = true;
11166
+ return;
11167
+ }
11168
+ if (typeof options.poolConfig === "string") {
11169
+ this.pool = new import_pg27.Pool({ connectionString: options.poolConfig });
11170
+ } else if (options.poolConfig) {
11171
+ this.pool = new import_pg27.Pool(options.poolConfig);
11172
+ } else {
11173
+ throw new Error("Either pool or poolConfig must be provided");
11174
+ }
11175
+ this.migrationManager = new MigrationManager(this.pool);
11176
+ this.migrationManager.register(createModelProvidersTable);
11177
+ this.migrationManager.register(createModelProviderModelsTable);
11178
+ if (options.autoMigrate !== false) {
11179
+ this.initialize().catch((error) => {
11180
+ console.error("Failed to initialize PostgreSQLModelProviderStore:", error);
11181
+ throw error;
11182
+ });
11183
+ }
11184
+ }
11185
+ /**
11186
+ * Initialize the store and run migrations
11187
+ * Uses a promise-based lock to prevent concurrent initialization
11188
+ */
11189
+ async initialize() {
11190
+ if (this.initialized) {
11191
+ return;
11192
+ }
11193
+ if (this.initPromise) {
11194
+ return this.initPromise;
11195
+ }
11196
+ this.initPromise = (async () => {
11197
+ try {
11198
+ await this.migrationManager.migrate();
11199
+ this.initialized = true;
11200
+ } finally {
11201
+ this.initPromise = null;
11202
+ }
11203
+ })();
11204
+ return this.initPromise;
11205
+ }
11206
+ /**
11207
+ * Get all provider configurations for a tenant
11208
+ */
11209
+ async listProviders(tenantId) {
11210
+ await this.ensureInitialized();
11211
+ const result = await this.pool.query(
11212
+ `
11213
+ SELECT id, tenant_id, name, display_name, protocol, api_style, llm_provider, base_url,
11214
+ api_key_enc, api_key_hint, enabled, status, last_error,
11215
+ last_discovered_at, created_by, created_at, updated_at
11216
+ FROM lattice_model_providers
11217
+ WHERE tenant_id = $1
11218
+ ORDER BY created_at DESC
11219
+ `,
11220
+ [tenantId]
11221
+ );
11222
+ return result.rows.map((row) => this.mapRowToEntry(row));
11223
+ }
11224
+ /**
11225
+ * Get a provider configuration by ID
11226
+ */
11227
+ async getProviderById(tenantId, id) {
11228
+ await this.ensureInitialized();
11229
+ const result = await this.pool.query(
11230
+ `
11231
+ SELECT id, tenant_id, name, display_name, protocol, api_style, llm_provider, base_url,
11232
+ api_key_enc, api_key_hint, enabled, status, last_error,
11233
+ last_discovered_at, created_by, created_at, updated_at
11234
+ FROM lattice_model_providers
11235
+ WHERE tenant_id = $1 AND id = $2
11236
+ `,
11237
+ [tenantId, id]
11238
+ );
11239
+ if (result.rows.length === 0) {
11240
+ return null;
11241
+ }
11242
+ return this.mapRowToEntry(result.rows[0]);
11243
+ }
11244
+ /**
11245
+ * Get a provider configuration by business name
11246
+ */
11247
+ async getProviderByName(tenantId, name) {
11248
+ await this.ensureInitialized();
11249
+ const result = await this.pool.query(
11250
+ `
11251
+ SELECT id, tenant_id, name, display_name, protocol, api_style, llm_provider, base_url,
11252
+ api_key_enc, api_key_hint, enabled, status, last_error,
11253
+ last_discovered_at, created_by, created_at, updated_at
11254
+ FROM lattice_model_providers
11255
+ WHERE tenant_id = $1 AND name = $2
11256
+ `,
11257
+ [tenantId, name]
11258
+ );
11259
+ if (result.rows.length === 0) {
11260
+ return null;
11261
+ }
11262
+ return this.mapRowToEntry(result.rows[0]);
11263
+ }
11264
+ /**
11265
+ * Create a new provider configuration
11266
+ */
11267
+ async createProvider(tenantId, id, data) {
11268
+ await this.ensureInitialized();
11269
+ const now = /* @__PURE__ */ new Date();
11270
+ const nowString = now.toISOString();
11271
+ const apiKeyEnc = data.apiKey ? (0, import_core7.encrypt)(data.apiKey) : null;
11272
+ const apiKeyHint = data.apiKey ? data.apiKey.slice(-4) : null;
11273
+ await this.pool.query(
11274
+ `
11275
+ INSERT INTO lattice_model_providers (
11276
+ id, tenant_id, name, display_name, protocol, api_style, llm_provider, base_url,
11277
+ api_key_enc, api_key_hint, enabled, status, created_at, updated_at
11278
+ )
11279
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'unknown', $12::timestamp, $13::timestamp)
11280
+ ON CONFLICT (tenant_id, id) DO UPDATE SET
11281
+ name = EXCLUDED.name,
11282
+ display_name = EXCLUDED.display_name,
11283
+ protocol = EXCLUDED.protocol,
11284
+ api_style = EXCLUDED.api_style,
11285
+ llm_provider = EXCLUDED.llm_provider,
11286
+ base_url = EXCLUDED.base_url,
11287
+ api_key_enc = EXCLUDED.api_key_enc,
11288
+ api_key_hint = EXCLUDED.api_key_hint,
11289
+ enabled = EXCLUDED.enabled,
11290
+ updated_at = EXCLUDED.updated_at
11291
+ `,
11292
+ [
11293
+ id,
11294
+ tenantId,
11295
+ data.name,
11296
+ data.displayName || null,
11297
+ data.protocol,
11298
+ data.apiStyle || "chat-completions",
11299
+ // Phase 1 pins the LLM provider: the UI does not offer a choice, and
11300
+ // some initChatModel branches hardcode their own baseURL and would
11301
+ // silently ignore the tenant's (see ADR-138 decision 7b).
11302
+ "openai",
11303
+ data.baseURL,
11304
+ apiKeyEnc,
11305
+ apiKeyHint,
11306
+ data.enabled !== false,
11307
+ nowString,
11308
+ nowString
11309
+ ]
11310
+ );
11311
+ const created = await this.getProviderById(tenantId, id);
11312
+ if (!created) {
11313
+ throw new Error("Failed to create model provider");
11314
+ }
11315
+ return created;
11316
+ }
11317
+ /**
11318
+ * Update an existing provider configuration.
11319
+ * An omitted or empty `apiKey` leaves the stored key untouched.
11320
+ */
11321
+ async updateProvider(tenantId, id, updates) {
11322
+ await this.ensureInitialized();
11323
+ const existing = await this.getProviderById(tenantId, id);
11324
+ if (!existing) {
11325
+ return null;
11326
+ }
11327
+ const updateData = {};
11328
+ if (updates.name !== void 0) {
11329
+ updateData.name = updates.name;
11330
+ }
11331
+ if (updates.displayName !== void 0) {
11332
+ updateData.display_name = updates.displayName || null;
11333
+ }
11334
+ if (updates.protocol !== void 0) {
11335
+ updateData.protocol = updates.protocol;
11336
+ }
11337
+ if (updates.apiStyle !== void 0) {
11338
+ updateData.api_style = updates.apiStyle;
11339
+ }
11340
+ if (updates.baseURL !== void 0) {
11341
+ updateData.base_url = updates.baseURL;
11342
+ }
11343
+ if (updates.clearApiKey === true) {
11344
+ updateData.api_key_enc = null;
11345
+ updateData.api_key_hint = null;
11346
+ } else if (updates.apiKey) {
11347
+ updateData.api_key_enc = (0, import_core7.encrypt)(updates.apiKey);
11348
+ updateData.api_key_hint = updates.apiKey.slice(-4);
11349
+ }
11350
+ if (updates.enabled !== void 0) {
11351
+ updateData.enabled = updates.enabled;
11352
+ }
11353
+ if (Object.keys(updateData).length === 0) {
11354
+ return existing;
11355
+ }
11356
+ updateData.updated_at = (/* @__PURE__ */ new Date()).toISOString();
11357
+ const fields = Object.keys(updateData);
11358
+ const values = Object.values(updateData);
11359
+ values.push(tenantId);
11360
+ values.push(id);
11361
+ const setClauses = fields.map(
11362
+ (field, index) => field === "updated_at" ? `${field} = $${index + 1}::timestamp` : `${field} = $${index + 1}`
11363
+ );
11364
+ const whereTenantIndex = fields.length + 1;
11365
+ const whereIdIndex = fields.length + 2;
11366
+ const sql = `
11367
+ UPDATE lattice_model_providers
11368
+ SET ${setClauses.join(", ")}
11369
+ WHERE tenant_id = $${whereTenantIndex} AND id = $${whereIdIndex}
11370
+ `;
11371
+ await this.pool.query(sql, values);
11372
+ return await this.getProviderById(tenantId, id);
11373
+ }
11374
+ /**
11375
+ * Delete a provider configuration and its model snapshot
11376
+ */
11377
+ async deleteProvider(tenantId, id) {
11378
+ await this.ensureInitialized();
11379
+ await this.deleteModelsByProvider(tenantId, id);
11380
+ const result = await this.pool.query(
11381
+ `
11382
+ DELETE FROM lattice_model_providers
11383
+ WHERE tenant_id = $1 AND id = $2
11384
+ `,
11385
+ [tenantId, id]
11386
+ );
11387
+ return result.rowCount !== null && result.rowCount > 0;
11388
+ }
11389
+ /**
11390
+ * Record the outcome of a discovery attempt
11391
+ */
11392
+ async markProviderStatus(tenantId, id, status, lastError, discoveredAt) {
11393
+ await this.ensureInitialized();
11394
+ await this.pool.query(
11395
+ `
11396
+ UPDATE lattice_model_providers
11397
+ SET status = $3,
11398
+ last_error = $4,
11399
+ last_discovered_at = CASE
11400
+ WHEN $5::timestamp IS NULL THEN last_discovered_at
11401
+ ELSE $5::timestamp
11402
+ END,
11403
+ updated_at = $6::timestamp
11404
+ WHERE tenant_id = $1 AND id = $2
11405
+ `,
11406
+ [
11407
+ tenantId,
11408
+ id,
11409
+ status,
11410
+ status === "ok" ? null : lastError || null,
11411
+ discoveredAt ? discoveredAt.toISOString() : null,
11412
+ (/* @__PURE__ */ new Date()).toISOString()
11413
+ ]
11414
+ );
11415
+ }
11416
+ /**
11417
+ * List snapshot models for a tenant
11418
+ */
11419
+ async listModels(tenantId, providerId) {
11420
+ await this.ensureInitialized();
11421
+ const result = providerId ? await this.pool.query(
11422
+ `
11423
+ SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
11424
+ upstream_created, enabled, stale, discovered_at
11425
+ FROM lattice_model_provider_models
11426
+ WHERE tenant_id = $1 AND provider_id = $2
11427
+ ORDER BY model_id ASC
11428
+ `,
11429
+ [tenantId, providerId]
11430
+ ) : await this.pool.query(
11431
+ `
11432
+ SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
11433
+ upstream_created, enabled, discovered_at
11434
+ FROM lattice_model_provider_models
11435
+ WHERE tenant_id = $1
11436
+ ORDER BY model_id ASC
11437
+ `,
11438
+ [tenantId]
11439
+ );
11440
+ return result.rows.map((row) => this.mapRowToModel(row));
11441
+ }
11442
+ /**
11443
+ * Get a single snapshot model.
11444
+ * Keyed by provider as well as model id, since two providers in the same
11445
+ * tenant may expose the same upstream model id.
11446
+ */
11447
+ async getModel(tenantId, providerId, modelId) {
11448
+ await this.ensureInitialized();
11449
+ const result = await this.pool.query(
11450
+ `
11451
+ SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
11452
+ upstream_created, enabled, discovered_at
11453
+ FROM lattice_model_provider_models
11454
+ WHERE tenant_id = $1 AND provider_id = $2 AND model_id = $3
11455
+ `,
11456
+ [tenantId, providerId, modelId]
11457
+ );
11458
+ if (result.rows.length === 0) {
11459
+ return null;
11460
+ }
11461
+ return this.mapRowToModel(result.rows[0]);
11462
+ }
11463
+ /**
11464
+ * Replace a provider's model snapshot with the result of one discovery.
11465
+ *
11466
+ * Runs in a single transaction. Model ids are scoped per provider, so the
11467
+ * same upstream id may appear under several providers in one tenant — each
11468
+ * is an independent model.
11469
+ *
11470
+ * Models the upstream no longer offers are deleted; models that survive are
11471
+ * upserted *without* touching `enabled`, so the tenant's enable/disable
11472
+ * choice survives a re-discovery. New rows take the column default (enabled).
11473
+ */
11474
+ async replaceProviderModels(tenantId, providerId, models) {
11475
+ await this.ensureInitialized();
11476
+ const client = await this.pool.connect();
11477
+ try {
11478
+ await client.query("BEGIN");
11479
+ const current = await client.query(
11480
+ `
11481
+ SELECT model_id, enabled FROM lattice_model_provider_models
11482
+ WHERE tenant_id = $1 AND provider_id = $2
11483
+ `,
11484
+ [tenantId, providerId]
11485
+ );
11486
+ const currentEnabled = new Map(
11487
+ current.rows.map((row) => [row.model_id, row.enabled])
11488
+ );
11489
+ const deduped = /* @__PURE__ */ new Map();
11490
+ for (const model of models) {
11491
+ deduped.set(model.id, model);
11492
+ }
11493
+ const rows = [...deduped.values()];
11494
+ const effectiveEnabled = rows.map(
11495
+ (model) => model.enabled ?? currentEnabled.get(model.id) ?? true
11496
+ );
11497
+ await client.query(
11498
+ `
11499
+ DELETE FROM lattice_model_provider_models
11500
+ WHERE tenant_id = $1
11501
+ AND provider_id = $2
11502
+ AND model_id <> ALL($3::text[])
11503
+ `,
11504
+ [tenantId, providerId, rows.map((model) => model.id)]
11505
+ );
11506
+ if (models.length > 0) {
11507
+ await client.query(
11508
+ `
11509
+ INSERT INTO lattice_model_provider_models (
11510
+ id, tenant_id, provider_id, model_id, display_name, owned_by,
11511
+ upstream_created, raw, enabled, stale, discovered_at
11512
+ )
11513
+ SELECT * FROM unnest(
11514
+ $1::text[], $2::text[], $3::text[], $4::text[], $5::text[],
11515
+ $6::text[], $7::timestamp[], $8::jsonb[], $9::boolean[], $10::boolean[], $11::timestamp[]
11516
+ ) AS t(
11517
+ id, tenant_id, provider_id, model_id, display_name, owned_by,
11518
+ upstream_created, raw, enabled, stale, discovered_at
11519
+ )
11520
+ ON CONFLICT (tenant_id, provider_id, model_id) DO UPDATE SET
11521
+ display_name = EXCLUDED.display_name,
11522
+ owned_by = EXCLUDED.owned_by,
11523
+ upstream_created = EXCLUDED.upstream_created,
11524
+ raw = EXCLUDED.raw,
11525
+ enabled = EXCLUDED.enabled,
11526
+ stale = EXCLUDED.stale,
11527
+ discovered_at = EXCLUDED.discovered_at
11528
+ `,
11529
+ [
11530
+ rows.map((model) => `${providerId}:${model.id}`),
11531
+ rows.map(() => tenantId),
11532
+ rows.map(() => providerId),
11533
+ rows.map((model) => model.id),
11534
+ rows.map((model) => model.displayName ?? null),
11535
+ rows.map((model) => model.ownedBy ?? null),
11536
+ rows.map(
11537
+ (model) => model.createdAt ? new Date(model.createdAt).toISOString() : null
11538
+ ),
11539
+ rows.map(
11540
+ (model) => model.raw === void 0 ? null : JSON.stringify(model.raw)
11541
+ ),
11542
+ effectiveEnabled,
11543
+ rows.map((model) => model.stale ?? false),
11544
+ rows.map(() => (/* @__PURE__ */ new Date()).toISOString())
11545
+ ]
11546
+ );
11547
+ }
11548
+ await client.query("COMMIT");
11549
+ } catch (error) {
11550
+ await client.query("ROLLBACK");
11551
+ throw error;
11552
+ } finally {
11553
+ client.release();
11554
+ }
11555
+ return models.map((model) => model.id);
11556
+ }
11557
+ /**
11558
+ * Enable or disable a single snapshot model
11559
+ */
11560
+ async setModelEnabled(tenantId, providerId, modelId, enabled) {
11561
+ await this.ensureInitialized();
11562
+ const result = await this.pool.query(
11563
+ `
11564
+ UPDATE lattice_model_provider_models
11565
+ SET enabled = $4
11566
+ WHERE tenant_id = $1 AND provider_id = $2 AND model_id = $3
11567
+ `,
11568
+ [tenantId, providerId, modelId, enabled]
11569
+ );
11570
+ if (result.rowCount === null || result.rowCount === 0) {
11571
+ return null;
11572
+ }
11573
+ return await this.getModel(tenantId, providerId, modelId);
11574
+ }
11575
+ /**
11576
+ * Delete every snapshot model belonging to a provider
11577
+ */
11578
+ async deleteModelsByProvider(tenantId, providerId) {
11579
+ await this.ensureInitialized();
11580
+ const result = await this.pool.query(
11581
+ `
11582
+ DELETE FROM lattice_model_provider_models
11583
+ WHERE tenant_id = $1 AND provider_id = $2
11584
+ `,
11585
+ [tenantId, providerId]
11586
+ );
11587
+ return result.rowCount ?? 0;
11588
+ }
11589
+ /**
11590
+ * Dispose resources and close the connection pool
11591
+ */
11592
+ async dispose() {
11593
+ if (this.ownsPool && this.pool) {
11594
+ await this.pool.end();
11595
+ }
11596
+ }
11597
+ /**
11598
+ * Ensure store is initialized
11599
+ */
11600
+ async ensureInitialized() {
11601
+ if (!this.initialized) {
11602
+ await this.initialize();
11603
+ }
11604
+ }
11605
+ /**
11606
+ * Map provider row to ModelProviderEntry
11607
+ * Automatically decrypts the API key if present
11608
+ */
11609
+ mapRowToEntry(row) {
11610
+ let apiKey;
11611
+ if (row.api_key_enc) {
11612
+ try {
11613
+ apiKey = (0, import_core7.decrypt)(row.api_key_enc);
11614
+ } catch (error) {
11615
+ console.error("Failed to decrypt model provider API key:", error);
11616
+ throw new Error("Failed to decrypt model provider API key");
11617
+ }
11618
+ }
11619
+ return {
11620
+ id: row.id,
11621
+ tenantId: row.tenant_id,
11622
+ name: row.name,
11623
+ displayName: row.display_name || void 0,
11624
+ protocol: row.protocol,
11625
+ apiStyle: row.api_style,
11626
+ llmProvider: row.llm_provider,
11627
+ baseURL: row.base_url,
11628
+ apiKey,
11629
+ apiKeyHint: row.api_key_hint || void 0,
11630
+ enabled: row.enabled,
11631
+ status: row.status,
11632
+ lastError: row.last_error || void 0,
11633
+ lastDiscoveredAt: row.last_discovered_at || void 0,
11634
+ createdBy: row.created_by || void 0,
11635
+ createdAt: row.created_at,
11636
+ updatedAt: row.updated_at
11637
+ };
11638
+ }
11639
+ /**
11640
+ * Map model row to ModelProviderModelEntry
11641
+ */
11642
+ mapRowToModel(row) {
11643
+ return {
11644
+ id: row.id,
11645
+ tenantId: row.tenant_id,
11646
+ providerId: row.provider_id,
11647
+ modelId: row.model_id,
11648
+ displayName: row.display_name || void 0,
11649
+ ownedBy: row.owned_by || void 0,
11650
+ upstreamCreatedAt: row.upstream_created || void 0,
11651
+ enabled: row.enabled,
11652
+ stale: row.stale,
11653
+ discoveredAt: row.discovered_at
11654
+ };
11655
+ }
11656
+ };
11657
+
11043
11658
  // src/createPgStoreConfig.ts
11044
11659
  async function createPgStoreConfig(connectionString) {
11045
- const pool = new import_pg27.Pool({ connectionString });
11660
+ const pool = new import_pg28.Pool({ connectionString });
11046
11661
  const mm = new MigrationManager(pool);
11047
11662
  mm.register(createThreadsTable);
11048
11663
  mm.register(createScheduledTasksTable);
@@ -11110,6 +11725,10 @@ async function createPgStoreConfig(connectionString) {
11110
11725
  mm.register(createProjectRoomReadStates);
11111
11726
  mm.register(createUserPushSubscriptions);
11112
11727
  mm.register(addProjectRoomMessageRetentionIndex);
11728
+ mm.register(createModelProvidersTable);
11729
+ mm.register(createModelProviderModelsTable);
11730
+ mm.register(addModelProviderApiStyle);
11731
+ mm.register(addModelProviderModelStale);
11113
11732
  await mm.migrate();
11114
11733
  const checkpoint = import_langgraph_checkpoint_postgres.PostgresSaver.fromConnString(connectionString);
11115
11734
  checkpoint.setup().catch((err) => {
@@ -11131,6 +11750,7 @@ async function createPgStoreConfig(connectionString) {
11131
11750
  connection: new PostgreSQLConnectionStore(pool),
11132
11751
  metrics: new PostgreSQLMetricsServerConfigStore(opts),
11133
11752
  mcp: new PostgreSQLMcpServerConfigStore(opts),
11753
+ modelProvider: new PostgreSQLModelProviderStore(opts),
11134
11754
  assistant: new PostgreSQLAssistantStore(opts),
11135
11755
  workflowTracking: new PostgreSQLWorkflowTrackingStore(opts),
11136
11756
  threadMessageQueue: new ThreadMessageQueueStore(opts),
@@ -11156,7 +11776,7 @@ async function createPgStoreConfig(connectionString) {
11156
11776
  }
11157
11777
 
11158
11778
  // src/stores/PostgreSQLSkillStore.ts
11159
- var import_pg28 = require("pg");
11779
+ var import_pg29 = require("pg");
11160
11780
  var PostgreSQLSkillStore = class {
11161
11781
  constructor(options) {
11162
11782
  this.initialized = false;
@@ -11169,9 +11789,9 @@ var PostgreSQLSkillStore = class {
11169
11789
  return;
11170
11790
  }
11171
11791
  if (typeof options.poolConfig === "string") {
11172
- this.pool = new import_pg28.Pool({ connectionString: options.poolConfig });
11792
+ this.pool = new import_pg29.Pool({ connectionString: options.poolConfig });
11173
11793
  } else if (options.poolConfig) {
11174
- this.pool = new import_pg28.Pool(options.poolConfig);
11794
+ this.pool = new import_pg29.Pool(options.poolConfig);
11175
11795
  } else {
11176
11796
  throw new Error("Either pool or poolConfig must be provided");
11177
11797
  }
@@ -11472,7 +12092,7 @@ var PostgreSQLSkillStore = class {
11472
12092
  };
11473
12093
 
11474
12094
  // src/stores/ChannelIdentityMappingStore.ts
11475
- var import_pg29 = require("pg");
12095
+ var import_pg30 = require("pg");
11476
12096
  var ChannelIdentityMappingStore = class {
11477
12097
  constructor(options) {
11478
12098
  this.initialized = false;
@@ -11484,7 +12104,7 @@ var ChannelIdentityMappingStore = class {
11484
12104
  this.initialized = true;
11485
12105
  return;
11486
12106
  }
11487
- this.pool = typeof options.poolConfig === "string" ? new import_pg29.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg29.Pool(options.poolConfig) : (() => {
12107
+ this.pool = typeof options.poolConfig === "string" ? new import_pg30.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg30.Pool(options.poolConfig) : (() => {
11488
12108
  throw new Error("Either pool or poolConfig must be provided");
11489
12109
  })();
11490
12110
  this.migrationManager = new MigrationManager(this.pool);
@@ -11723,6 +12343,7 @@ function mapRowToChannelIdentityMapping(row) {
11723
12343
  PostgreSQLEvalStore,
11724
12344
  PostgreSQLMcpServerConfigStore,
11725
12345
  PostgreSQLMetricsServerConfigStore,
12346
+ PostgreSQLModelProviderStore,
11726
12347
  PostgreSQLOpenAuditStore,
11727
12348
  PostgreSQLProjectBotMembershipStore,
11728
12349
  PostgreSQLProjectMembershipStore,