@axiom-lattice/pg-stores 4.0.0 → 4.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -25,7 +25,7 @@
25
25
  "@langchain/core": "1.1.30",
26
26
  "pg": "^8.16.3",
27
27
  "uuid": "^9.0.1",
28
- "@axiom-lattice/core": "10.0.0",
28
+ "@axiom-lattice/core": "10.0.1",
29
29
  "@axiom-lattice/protocols": "6.0.0"
30
30
  },
31
31
  "devDependencies": {
@@ -9,7 +9,11 @@ import { Pool } from "pg";
9
9
  import { PGVectorStore } from "@langchain/community/vectorstores/pgvector";
10
10
  import { Document } from "@langchain/core/documents";
11
11
  import { embeddingsLatticeManager } from "@axiom-lattice/core";
12
- import type { VectorStoreProvider, VectorStoreCreateParams } from "@axiom-lattice/protocols";
12
+ import {
13
+ EMBEDDING_DEFAULT_KEY,
14
+ type VectorStoreProvider,
15
+ type VectorStoreCreateParams,
16
+ } from "@axiom-lattice/protocols";
13
17
  import type { DocumentInterface } from "@langchain/core/documents";
14
18
 
15
19
  function safeTableName(name: string): string {
@@ -31,15 +35,22 @@ export class PGVectorStoreProvider implements VectorStoreProvider {
31
35
  async create(params: VectorStoreCreateParams): Promise<any> {
32
36
  const { tableName, embeddingKey } = params;
33
37
  const tenantId = params.tenantId ?? "default";
38
+ const requestedKey = embeddingKey || EMBEDDING_DEFAULT_KEY;
34
39
 
35
40
  // Tenant first, deployment second — the same order every model resolver
36
- // uses. A tenant model key is `${providerId}/${modelId}` and only resolves
37
- // in that tenant's namespace.
41
+ // uses. A concrete key that misses falls back to the tenant's embedding
42
+ // default, then to the deployment registration.
38
43
  const embeddings =
39
44
  (await embeddingsLatticeManager.getEmbeddingsClientWithTenant(
40
45
  tenantId,
41
- embeddingKey
46
+ requestedKey
42
47
  )) ??
48
+ (requestedKey === EMBEDDING_DEFAULT_KEY
49
+ ? undefined
50
+ : await embeddingsLatticeManager.getEmbeddingsClientWithTenant(
51
+ tenantId,
52
+ EMBEDDING_DEFAULT_KEY
53
+ )) ??
43
54
  (await embeddingsLatticeManager.getEmbeddingsClientWithTenant(
44
55
  tenantId,
45
56
  "default"
@@ -48,7 +59,7 @@ export class PGVectorStoreProvider implements VectorStoreProvider {
48
59
  if (!embeddings) {
49
60
  throw new Error(
50
61
  `No embedding model "${embeddingKey}" registered for tenant "${tenantId}", ` +
51
- `and no deployment default. Configure one in the model provider panel.`
62
+ `and no tenant or deployment default. Configure one in the model service panel.`
52
63
  );
53
64
  }
54
65
 
@@ -34,7 +34,9 @@ describe("agent run retention migration", () => {
34
34
  (name) => name === createAgentRunRetentionMigration.name,
35
35
  ),
36
36
  ).toHaveLength(1);
37
- expect(registeredMigrationNames.at(-1)).toBe(
37
+ // Registration order is not the apply order — the manager sorts by version —
38
+ // so assert presence rather than that this is the last line registered.
39
+ expect(registeredMigrationNames).toContain(
38
40
  agentRunProjectTaskRunningIndexMigration.name,
39
41
  );
40
42
  });
@@ -109,6 +109,7 @@ import {
109
109
  addModelProviderModelExtra,
110
110
  addModelProviderModelDefault,
111
111
  addModelProviderModelKind,
112
+ addModelProviderKindDefault,
112
113
  } from "./migrations/model_provider_migrations";
113
114
  import { PostgreSQLCapabilityBundleStore } from "./stores/PostgreSQLCapabilityBundleStore";
114
115
  import { PostgreSQLProjectRoomStore } from "./stores/PostgreSQLProjectRoomStore";
@@ -222,6 +223,7 @@ export async function createPgStoreConfig(connectionString: string) {
222
223
  mm.register(createAgentRunRetentionMigration); // v201
223
224
  mm.register(createAgentRunProjectTaskMigration); // v203
224
225
  mm.register(agentRunProjectTaskRunningIndexMigration); // v204
226
+ mm.register(addModelProviderKindDefault); // v207
225
227
 
226
228
  await mm.migrate();
227
229
 
@@ -256,3 +256,41 @@ export const addModelProviderModelKind: Migration = {
256
256
  `);
257
257
  },
258
258
  };
259
+
260
+ /**
261
+ * One default per tenant **per kind**.
262
+ *
263
+ * v192's index was tenant-wide, so a tenant could have a default chat model
264
+ * *or* a default embedding model, never both. Each kind publishes its own
265
+ * alias key (`default`, `default:embedding`, `default:asr`), so the uniqueness
266
+ * constraint has to be per kind too.
267
+ *
268
+ * Existing rows satisfy the wider index unchanged: before this, at most one
269
+ * row per tenant could be the default.
270
+ *
271
+ * Note the `down` migration can legitimately fail once a tenant has one
272
+ * default per kind — the narrower index cannot represent that state. That is a
273
+ * deliberate one-way step.
274
+ */
275
+ export const addModelProviderKindDefault: Migration = {
276
+ version: 207,
277
+ name: 'add_model_provider_kind_default',
278
+ up: async (client: PoolClient) => {
279
+ await client.query(`
280
+ DROP INDEX IF EXISTS uk_lattice_model_provider_models_tenant_default
281
+ `);
282
+ await client.query(`
283
+ CREATE UNIQUE INDEX IF NOT EXISTS uk_lattice_model_provider_models_tenant_kind_default
284
+ ON lattice_model_provider_models (tenant_id, kind) WHERE is_default
285
+ `);
286
+ },
287
+ down: async (client: PoolClient) => {
288
+ await client.query(`
289
+ DROP INDEX IF EXISTS uk_lattice_model_provider_models_tenant_kind_default
290
+ `);
291
+ await client.query(`
292
+ CREATE UNIQUE INDEX IF NOT EXISTS uk_lattice_model_provider_models_tenant_default
293
+ ON lattice_model_provider_models (tenant_id) WHERE is_default
294
+ `);
295
+ },
296
+ };
@@ -26,6 +26,7 @@ import {
26
26
  addModelProviderModelExtra,
27
27
  addModelProviderModelDefault,
28
28
  addModelProviderModelKind,
29
+ addModelProviderKindDefault,
29
30
  } from '../migrations/model_provider_migrations';
30
31
  import { encrypt, safeDecrypt } from '@axiom-lattice/core';
31
32
 
@@ -148,6 +149,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
148
149
  this.migrationManager.register(addModelProviderModelExtra); // v191
149
150
  this.migrationManager.register(addModelProviderModelDefault); // v192
150
151
  this.migrationManager.register(addModelProviderModelKind); // v193
152
+ this.migrationManager.register(addModelProviderKindDefault); // v207
151
153
 
152
154
  // Auto-migrate by default
153
155
  if (options.autoMigrate !== false) {
@@ -610,30 +612,42 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
610
612
  (model) => model.kind ?? currentKind.get(model.id) ?? "chat"
611
613
  );
612
614
 
613
- // Whether this write is naming a default at all.
615
+ // Which kinds this write names a default for.
614
616
  //
615
- // The unique index spans the *tenant*, while this method writes one
617
+ // The unique index is per *(tenant, kind)*, while this method writes one
616
618
  // provider. Two consequences:
617
619
  //
618
- // 1. A swap (A was the default, B now is) would see two true rows
619
- // mid-statement — PostgreSQL checks a unique index per row, so whether
620
- // it fails would depend on which row the batch happened to process
621
- // first. Clearing tenant-wide *before* the upsert makes order
620
+ // 1. A swap (A was kind K's default, B now is) would see two true rows of
621
+ // kind K mid-statement — PostgreSQL checks a unique index per row, so
622
+ // whether it fails would depend on which row the batch happened to
623
+ // process first. Clearing that kind before the upsert makes order
622
624
  // irrelevant.
623
- // 2. Once cleared, an omitted flag must resolve to `false`, not "keep":
624
- // keeping it would restore the old default alongside the new one.
625
- const namesDefault = rows.some((model) => model.isDefault === true);
626
- if (namesDefault) {
625
+ // 2. Once cleared, an omitted flag must resolve to `false` for the kinds
626
+ // this write names — keeping it would restore the old default
627
+ // alongside the new one. Other kinds keep their stored flag, so a chat
628
+ // write cannot drop the tenant's embedding default.
629
+ const namedDefaultKinds = new Set<ModelKind>();
630
+ rows.forEach((model, index) => {
631
+ if (model.isDefault === true) {
632
+ namedDefaultKinds.add(effectiveKind[index] as ModelKind);
633
+ }
634
+ });
635
+
636
+ for (const kind of namedDefaultKinds) {
627
637
  await client.query(
628
- `UPDATE lattice_model_provider_models SET is_default = FALSE WHERE tenant_id = $1`,
629
- [tenantId]
638
+ `UPDATE lattice_model_provider_models
639
+ SET is_default = FALSE
640
+ WHERE tenant_id = $1 AND kind = $2 AND is_default`,
641
+ [tenantId, kind]
630
642
  );
631
643
  }
632
644
 
633
645
  const effectiveIsDefault = rows.map(
634
- (model) =>
646
+ (model, index) =>
635
647
  model.isDefault ??
636
- (namesDefault ? false : currentIsDefault.get(model.id)) ??
648
+ (namedDefaultKinds.has(effectiveKind[index] as ModelKind)
649
+ ? false
650
+ : currentIsDefault.get(model.id)) ??
637
651
  false
638
652
  );
639
653