@byok-sdk/keys 0.3.9 → 0.4.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
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { createHash } from 'crypto';
2
3
  import { spawn } from 'child_process';
3
4
  import path, { dirname } from 'path';
4
- import { createHash } from 'crypto';
5
5
  import { mkdirSync, existsSync, chmodSync } from 'fs';
6
6
  import { createRequire } from 'module';
7
7
  import { isCoreConflictError, contentHash } from '@byok-sdk/core';
@@ -47,6 +47,7 @@ var BYOK_KEYS_ERROR_CODES = {
47
47
  PROVIDER_SECRET_MISSING: "PROVIDER_SECRET_MISSING",
48
48
  PROVIDER_SECRET_NOT_ALLOWED: "PROVIDER_SECRET_NOT_ALLOWED",
49
49
  PROVIDER_SECRET_ROLLBACK_FAILED: "PROVIDER_SECRET_ROLLBACK_FAILED",
50
+ PROVIDER_STORE_SCHEMA_STALE: "PROVIDER_STORE_SCHEMA_STALE",
50
51
  PROVIDER_STORE_UNAVAILABLE: "PROVIDER_STORE_UNAVAILABLE",
51
52
  PROVIDER_TRUTH_INVALID: "PROVIDER_TRUTH_INVALID",
52
53
  PROVIDER_URL_INVALID: "PROVIDER_URL_INVALID",
@@ -56,6 +57,89 @@ var BYOK_KEYS_ERROR_CODES = {
56
57
  SECRET_VALUE_INVALID: "SECRET_VALUE_INVALID"
57
58
  };
58
59
 
60
+ // src/provider-catalog.ts
61
+ function openAiCompatible(display_name, base_url, api_key_env) {
62
+ return {
63
+ display_name,
64
+ base_url,
65
+ adapter: "openai_compatible",
66
+ auth_mode: "bearer",
67
+ api_key_env
68
+ };
69
+ }
70
+ function anthropicMessages(display_name, base_url, api_key_env) {
71
+ return {
72
+ display_name,
73
+ base_url,
74
+ adapter: "anthropic",
75
+ auth_mode: "x_api_key",
76
+ api_key_env
77
+ };
78
+ }
79
+ var MODEL_PROVIDER_VENDORS = {
80
+ "ant-ling": openAiCompatible("Ant Ling", "https://api.ant-ling.com/v1", "ANT_LING_API_KEY"),
81
+ baseten: openAiCompatible("Baseten", "https://inference.baseten.co/v1", "BASETEN_API_KEY"),
82
+ cerebras: openAiCompatible("Cerebras", "https://api.cerebras.ai/v1", "CEREBRAS_API_KEY"),
83
+ deepseek: openAiCompatible("DeepSeek", "https://api.deepseek.com", "DEEPSEEK_API_KEY"),
84
+ groq: openAiCompatible("Groq", "https://api.groq.com/openai/v1", "GROQ_API_KEY"),
85
+ huggingface: openAiCompatible("Hugging Face", "https://router.huggingface.co/v1", "HF_TOKEN"),
86
+ moonshotai: openAiCompatible("Moonshot AI", "https://api.moonshot.ai/v1", "MOONSHOT_API_KEY"),
87
+ "moonshotai-cn": openAiCompatible("Moonshot AI CN", "https://api.moonshot.cn/v1", "MOONSHOT_API_KEY"),
88
+ nvidia: openAiCompatible("NVIDIA", "https://integrate.api.nvidia.com/v1", "NVIDIA_API_KEY"),
89
+ openai: openAiCompatible("OpenAI", "https://api.openai.com/v1", "OPENAI_API_KEY"),
90
+ openrouter: openAiCompatible("OpenRouter", "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"),
91
+ "qwen-token-plan": openAiCompatible(
92
+ "Qwen Token Plan",
93
+ "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
94
+ "QWEN_TOKEN_PLAN_API_KEY"
95
+ ),
96
+ "qwen-token-plan-cn": openAiCompatible(
97
+ "Qwen Token Plan CN",
98
+ "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
99
+ "QWEN_TOKEN_PLAN_CN_API_KEY"
100
+ ),
101
+ together: openAiCompatible("Together", "https://api.together.ai/v1", "TOGETHER_API_KEY"),
102
+ xai: openAiCompatible("xAI", "https://api.x.ai/v1", "XAI_API_KEY"),
103
+ xiaomi: openAiCompatible("Xiaomi", "https://api.xiaomimimo.com/v1", "XIAOMI_API_KEY"),
104
+ "xiaomi-token-plan-ams": openAiCompatible(
105
+ "Xiaomi Token Plan AMS",
106
+ "https://token-plan-ams.xiaomimimo.com/v1",
107
+ "XIAOMI_TOKEN_PLAN_AMS_API_KEY"
108
+ ),
109
+ "xiaomi-token-plan-cn": openAiCompatible(
110
+ "Xiaomi Token Plan CN",
111
+ "https://token-plan-cn.xiaomimimo.com/v1",
112
+ "XIAOMI_TOKEN_PLAN_CN_API_KEY"
113
+ ),
114
+ "xiaomi-token-plan-sgp": openAiCompatible(
115
+ "Xiaomi Token Plan SGP",
116
+ "https://token-plan-sgp.xiaomimimo.com/v1",
117
+ "XIAOMI_TOKEN_PLAN_SGP_API_KEY"
118
+ ),
119
+ zai: openAiCompatible("Z.AI", "https://api.z.ai/api/coding/paas/v4", "ZAI_API_KEY"),
120
+ "zai-coding-cn": openAiCompatible(
121
+ "Z.AI Coding CN",
122
+ "https://open.bigmodel.cn/api/coding/paas/v4",
123
+ "ZAI_CODING_CN_API_KEY"
124
+ ),
125
+ anthropic: anthropicMessages("Anthropic", "https://api.anthropic.com/v1", "ANTHROPIC_API_KEY"),
126
+ fireworks: anthropicMessages("Fireworks", "https://api.fireworks.ai/inference/v1", "FIREWORKS_API_KEY"),
127
+ "kimi-coding": anthropicMessages("Kimi For Coding", "https://api.kimi.com/coding/v1", "KIMI_API_KEY"),
128
+ minimax: anthropicMessages("MiniMax", "https://api.minimax.io/anthropic/v1", "MINIMAX_API_KEY"),
129
+ "minimax-cn": anthropicMessages("MiniMax CN", "https://api.minimaxi.com/anthropic/v1", "MINIMAX_CN_API_KEY"),
130
+ "vercel-ai-gateway": anthropicMessages(
131
+ "Vercel AI Gateway",
132
+ "https://ai-gateway.vercel.sh/v1",
133
+ "AI_GATEWAY_API_KEY"
134
+ )
135
+ };
136
+ var MODEL_PROVIDER_VENDOR_IDS = Object.keys(
137
+ MODEL_PROVIDER_VENDORS
138
+ );
139
+ function modelProviderVendor(kind) {
140
+ return Object.hasOwn(MODEL_PROVIDER_VENDORS, kind) ? MODEL_PROVIDER_VENDORS[kind] : void 0;
141
+ }
142
+
59
143
  // src/url.ts
60
144
  function normalizeProviderUrl(value) {
61
145
  let url;
@@ -99,12 +183,17 @@ function isPrivateNetworkLiteral(hostname) {
99
183
  }
100
184
 
101
185
  // src/provider-profile.ts
102
- var MODEL_PROVIDER_IDS = [
103
- "openai",
104
- "deepseek",
105
- "anthropic",
186
+ var PROVIDER_PROFILE_REF_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/u;
187
+ var ProviderProfileRefSchema = z.string().min(1).max(64).regex(
188
+ PROVIDER_PROFILE_REF_PATTERN,
189
+ "provider profile refs must be lowercase portable identifiers"
190
+ );
191
+ var MODEL_PROVIDER_KINDS = [
192
+ ...MODEL_PROVIDER_VENDOR_IDS,
106
193
  "custom"
107
194
  ];
195
+ var PROVIDER_MODEL_CAPABILITIES = ["image-input"];
196
+ var ProviderModelCapabilitySchema = z.enum(PROVIDER_MODEL_CAPABILITIES);
108
197
  var PROVIDER_AUTH_MODES = ["bearer", "x_api_key", "none"];
109
198
  var MODEL_PROVIDER_ADAPTERS = ["openai_compatible", "anthropic"];
110
199
  function boundedString(field, maximumLength) {
@@ -144,14 +233,23 @@ var ModelProviderProfileSchema = z.object({
144
233
  adapter: z.enum(MODEL_PROVIDER_ADAPTERS),
145
234
  auth_mode: z.enum(PROVIDER_AUTH_MODES),
146
235
  base_url: providerBaseUrl,
236
+ capabilities: z.array(ProviderModelCapabilitySchema).max(8),
147
237
  created_at: isoTimestamp("created_at"),
148
238
  display_name: boundedString("display_name", 100),
149
239
  enabled: z.boolean(),
150
240
  kind: z.literal("model"),
151
241
  model: boundedString("model", 160),
152
- provider_id: z.enum(MODEL_PROVIDER_IDS),
242
+ profile_ref: ProviderProfileRefSchema,
243
+ provider_kind: z.enum(MODEL_PROVIDER_KINDS),
153
244
  updated_at: isoTimestamp("updated_at")
154
245
  }).superRefine((profile, ctx) => {
246
+ if (new Set(profile.capabilities).size !== profile.capabilities.length) {
247
+ ctx.addIssue({
248
+ code: "custom",
249
+ message: "Provider capabilities cannot repeat",
250
+ path: ["capabilities"]
251
+ });
252
+ }
155
253
  if (profile.adapter === "anthropic" && profile.auth_mode !== "x_api_key") {
156
254
  ctx.addIssue({
157
255
  code: "custom",
@@ -166,6 +264,14 @@ var ModelProviderProfileSchema = z.object({
166
264
  path: ["auth_mode"]
167
265
  });
168
266
  }
267
+ const vendor = modelProviderVendor(profile.provider_kind);
268
+ if (vendor !== void 0 && vendor.adapter !== profile.adapter) {
269
+ ctx.addIssue({
270
+ code: "custom",
271
+ message: `Provider kind ${profile.provider_kind} speaks the ${vendor.adapter} adapter`,
272
+ path: ["adapter"]
273
+ });
274
+ }
169
275
  if (Date.parse(profile.updated_at) < Date.parse(profile.created_at)) {
170
276
  ctx.addIssue({
171
277
  code: "custom",
@@ -174,6 +280,47 @@ var ModelProviderProfileSchema = z.object({
174
280
  });
175
281
  }
176
282
  });
283
+ function exactProviderProfileBinding(profileInput, requiredCapabilities = profileInput.capabilities) {
284
+ const profile = parseModelProviderProfile(profileInput);
285
+ const revision = Date.parse(profile.updated_at);
286
+ if (!Number.isSafeInteger(revision) || revision < 0) {
287
+ throw new ByokKeysError(
288
+ "PROVIDER_PROFILE_INVALID",
289
+ "Provider updated_at cannot be represented as a canonical revision"
290
+ );
291
+ }
292
+ const normalizedCapabilities = [...profile.capabilities].sort();
293
+ const canonical = JSON.stringify({
294
+ adapter: profile.adapter,
295
+ auth_mode: profile.auth_mode,
296
+ base_url: profile.base_url,
297
+ capabilities: normalizedCapabilities,
298
+ kind: profile.kind,
299
+ model: profile.model,
300
+ profile_ref: profile.profile_ref,
301
+ provider_kind: profile.provider_kind
302
+ });
303
+ return {
304
+ profileRef: profile.profile_ref,
305
+ profileRevision: String(revision),
306
+ profileHash: `sha256:${createHash("sha256").update(canonical).digest("hex")}`,
307
+ modelId: profile.model,
308
+ requiredCapabilities: [...requiredCapabilities]
309
+ };
310
+ }
311
+ function assertExactProviderProfileBinding(profile, expected) {
312
+ if (new Set(expected.requiredCapabilities).size !== expected.requiredCapabilities.length) {
313
+ throw new Error("provider profile required capabilities must be unique");
314
+ }
315
+ const actual = exactProviderProfileBinding(profile, expected.requiredCapabilities);
316
+ if (actual.profileRef !== expected.profileRef) throw new Error("provider profile ref mismatch");
317
+ if (actual.profileRevision !== expected.profileRevision) throw new Error("provider profile revision mismatch");
318
+ if (actual.profileHash !== expected.profileHash) throw new Error("provider profile hash mismatch");
319
+ if (actual.modelId !== expected.modelId) throw new Error("provider profile model mismatch");
320
+ const supported = new Set(profile.capabilities);
321
+ const unsupported = expected.requiredCapabilities.find((capability) => !supported.has(capability));
322
+ if (unsupported !== void 0) throw new Error(`provider profile does not support required capability ${unsupported}`);
323
+ }
177
324
  function parseModelProviderProfile(value) {
178
325
  const result = ModelProviderProfileSchema.safeParse(value);
179
326
  if (result.success) return result.data;
@@ -527,14 +674,16 @@ function assertSecretNamespace(value) {
527
674
 
528
675
  // src/secret-store.ts
529
676
  var DEFAULT_SECRET_SERVICE_PREFIX = "com.byok.keys";
530
- var MODEL_PROVIDER_SECRET_NAMES = {
531
- anthropic: "model-anthropic-api-key",
532
- custom: "model-custom-api-key",
533
- deepseek: "model-deepseek-api-key",
534
- openai: "model-openai-api-key"
535
- };
536
- function modelProviderSecretName(providerId) {
537
- return MODEL_PROVIDER_SECRET_NAMES[providerId];
677
+ function modelProviderSecretName(profileRef) {
678
+ const parsed = ProviderProfileRefSchema.safeParse(profileRef);
679
+ if (!parsed.success) {
680
+ throw new ByokKeysError(
681
+ "PROVIDER_PROFILE_INVALID",
682
+ "Provider profile ref must be a lowercase portable identifier",
683
+ { cause: parsed.error }
684
+ );
685
+ }
686
+ return assertSecretName(`model-${parsed.data}-api-key`);
538
687
  }
539
688
  function assertSharedSecretValue(secret) {
540
689
  if (secret.length === 0 || /[\u0000\r\n]/u.test(secret)) {
@@ -1143,10 +1292,10 @@ function normalizeSecretScope(scope) {
1143
1292
  }
1144
1293
 
1145
1294
  // src/profile-store.ts
1146
- function providerNotConfigured(providerId) {
1295
+ function providerNotConfigured(profileRef) {
1147
1296
  return new ByokKeysError(
1148
1297
  "PROVIDER_NOT_CONFIGURED",
1149
- `${providerId} model provider is not configured`
1298
+ `${profileRef} model provider is not configured`
1150
1299
  );
1151
1300
  }
1152
1301
  var InMemoryProviderProfileStore = class {
@@ -1154,38 +1303,38 @@ var InMemoryProviderProfileStore = class {
1154
1303
  async close() {
1155
1304
  this.#profiles.clear();
1156
1305
  }
1157
- async delete(providerId) {
1158
- return this.#profiles.delete(providerId);
1306
+ async delete(profileRef) {
1307
+ return this.#profiles.delete(profileRef);
1159
1308
  }
1160
- async get(providerId) {
1161
- return this.#profiles.get(providerId);
1309
+ async get(profileRef) {
1310
+ return this.#profiles.get(profileRef);
1162
1311
  }
1163
1312
  async getEnabled() {
1164
1313
  return [...this.#profiles.values()].find((profile) => profile.enabled);
1165
1314
  }
1166
1315
  async list() {
1167
1316
  return [...this.#profiles.values()].sort(
1168
- (left, right) => left.provider_id.localeCompare(right.provider_id)
1317
+ (left, right) => left.profile_ref.localeCompare(right.profile_ref)
1169
1318
  );
1170
1319
  }
1171
1320
  async save(profile) {
1172
1321
  const validated = parseModelProviderProfile({
1173
1322
  ...profile,
1174
- created_at: this.#profiles.get(profile.provider_id)?.created_at ?? profile.created_at
1323
+ created_at: this.#profiles.get(profile.profile_ref)?.created_at ?? profile.created_at
1175
1324
  });
1176
1325
  if (validated.enabled) {
1177
- for (const [providerId, existing] of this.#profiles) {
1178
- if (providerId !== validated.provider_id && existing.enabled) {
1179
- this.#profiles.set(providerId, { ...existing, enabled: false });
1326
+ for (const [profileRef, existing] of this.#profiles) {
1327
+ if (profileRef !== validated.profile_ref && existing.enabled) {
1328
+ this.#profiles.set(profileRef, { ...existing, enabled: false });
1180
1329
  }
1181
1330
  }
1182
1331
  }
1183
- this.#profiles.set(validated.provider_id, validated);
1332
+ this.#profiles.set(validated.profile_ref, validated);
1184
1333
  return validated;
1185
1334
  }
1186
- async setEnabled(providerId) {
1187
- const existing = this.#profiles.get(providerId);
1188
- if (existing === void 0) throw providerNotConfigured(providerId);
1335
+ async setEnabled(profileRef) {
1336
+ const existing = this.#profiles.get(profileRef);
1337
+ if (existing === void 0) throw providerNotConfigured(profileRef);
1189
1338
  return this.save({ ...existing, enabled: true });
1190
1339
  }
1191
1340
  };
@@ -1261,20 +1410,40 @@ function secureSqliteFilePermissions(databasePath) {
1261
1410
  }
1262
1411
 
1263
1412
  // src/sqlite-profile-store.ts
1413
+ function sqlList(values) {
1414
+ return values.map((value) => `'${value}'`).join(", ");
1415
+ }
1264
1416
  var SCHEMA = `
1265
1417
  CREATE TABLE IF NOT EXISTS provider_profile (
1266
- provider_id TEXT PRIMARY KEY CHECK (provider_id IN ('openai', 'deepseek', 'anthropic', 'custom')),
1267
- kind TEXT NOT NULL CHECK (kind = 'model'),
1268
- adapter TEXT NOT NULL CHECK (adapter IN ('openai_compatible', 'anthropic')),
1269
- display_name TEXT NOT NULL,
1270
- base_url TEXT NOT NULL,
1271
- auth_mode TEXT NOT NULL CHECK (auth_mode IN ('bearer', 'x_api_key', 'none')),
1272
- model TEXT NOT NULL,
1273
- enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
1274
- created_at TEXT NOT NULL,
1275
- updated_at TEXT NOT NULL
1418
+ profile_ref TEXT PRIMARY KEY,
1419
+ provider_kind TEXT NOT NULL CHECK (provider_kind IN (${sqlList(MODEL_PROVIDER_KINDS)})),
1420
+ kind TEXT NOT NULL CHECK (kind = 'model'),
1421
+ adapter TEXT NOT NULL CHECK (adapter IN (${sqlList(MODEL_PROVIDER_ADAPTERS)})),
1422
+ display_name TEXT NOT NULL,
1423
+ base_url TEXT NOT NULL,
1424
+ auth_mode TEXT NOT NULL CHECK (auth_mode IN (${sqlList(PROVIDER_AUTH_MODES)})),
1425
+ model TEXT NOT NULL,
1426
+ capabilities TEXT NOT NULL,
1427
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
1428
+ created_at TEXT NOT NULL,
1429
+ updated_at TEXT NOT NULL
1276
1430
  );
1277
1431
  `;
1432
+ function normalizeTableDdl(sql) {
1433
+ return sql.replace(/\bIF\s+NOT\s+EXISTS\b/giu, "").replace(/\s+/gu, " ").trim().replace(/;$/u, "").trim();
1434
+ }
1435
+ function assertProviderProfileSchemaIsCurrent(database, path2) {
1436
+ const row = database.prepare(
1437
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'provider_profile'"
1438
+ ).get();
1439
+ const stored = row?.sql;
1440
+ if (typeof stored !== "string") return;
1441
+ if (normalizeTableDdl(stored) === normalizeTableDdl(SCHEMA)) return;
1442
+ throw new ByokKeysError(
1443
+ "PROVIDER_STORE_SCHEMA_STALE",
1444
+ `Provider profile store at ${path2} was created by a different @byok-sdk/keys schema; recreate the store file to continue`
1445
+ );
1446
+ }
1278
1447
  var ENABLED_INDEX = `
1279
1448
  CREATE UNIQUE INDEX IF NOT EXISTS provider_profile_one_enabled
1280
1449
  ON provider_profile(kind)
@@ -1291,6 +1460,7 @@ var SqliteProviderProfileStore = class {
1291
1460
  try {
1292
1461
  this.#database.exec(SCHEMA);
1293
1462
  this.#database.exec(ENABLED_INDEX);
1463
+ assertProviderProfileSchemaIsCurrent(this.#database, options.path);
1294
1464
  secureSqliteFilePermissions(options.path);
1295
1465
  } catch (error) {
1296
1466
  closeSqliteDatabaseAfterInitializationFailure(
@@ -1299,6 +1469,16 @@ var SqliteProviderProfileStore = class {
1299
1469
  "SqliteProviderProfileStore initialization failed and its native handle could not be closed"
1300
1470
  );
1301
1471
  }
1472
+ } else {
1473
+ try {
1474
+ assertProviderProfileSchemaIsCurrent(this.#database, options.path);
1475
+ } catch (error) {
1476
+ closeSqliteDatabaseAfterInitializationFailure(
1477
+ this.#database,
1478
+ error,
1479
+ "SqliteProviderProfileStore read-only schema check failed and its native handle could not be closed"
1480
+ );
1481
+ }
1302
1482
  }
1303
1483
  }
1304
1484
  /**
@@ -1312,12 +1492,12 @@ var SqliteProviderProfileStore = class {
1312
1492
  this.#closed = true;
1313
1493
  this.#database.close();
1314
1494
  }
1315
- async delete(providerId) {
1316
- const result = this.#database.prepare("DELETE FROM provider_profile WHERE provider_id = ?").run(providerId);
1495
+ async delete(profileRef) {
1496
+ const result = this.#database.prepare("DELETE FROM provider_profile WHERE profile_ref = ?").run(profileRef);
1317
1497
  return Number(result.changes) === 1;
1318
1498
  }
1319
- async get(providerId) {
1320
- const row = this.#database.prepare("SELECT * FROM provider_profile WHERE provider_id = ?").get(providerId);
1499
+ async get(profileRef) {
1500
+ const row = this.#database.prepare("SELECT * FROM provider_profile WHERE profile_ref = ?").get(profileRef);
1321
1501
  return row === void 0 ? void 0 : parseRow(row);
1322
1502
  }
1323
1503
  async getEnabled() {
@@ -1325,11 +1505,11 @@ var SqliteProviderProfileStore = class {
1325
1505
  return row === void 0 ? void 0 : parseRow(row);
1326
1506
  }
1327
1507
  async list() {
1328
- const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY provider_id ASC").all();
1508
+ const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY profile_ref ASC").all();
1329
1509
  return rows.map(parseRow);
1330
1510
  }
1331
1511
  async save(profile) {
1332
- const existing = await this.get(profile.provider_id);
1512
+ const existing = await this.get(profile.profile_ref);
1333
1513
  const validated = parseModelProviderProfile({
1334
1514
  ...profile,
1335
1515
  created_at: existing?.created_at ?? profile.created_at
@@ -1337,40 +1517,44 @@ var SqliteProviderProfileStore = class {
1337
1517
  this.#transaction(() => {
1338
1518
  if (validated.enabled) {
1339
1519
  this.#database.prepare(
1340
- "UPDATE provider_profile SET enabled = 0 WHERE provider_id <> ?"
1341
- ).run(validated.provider_id);
1520
+ "UPDATE provider_profile SET enabled = 0 WHERE profile_ref <> ?"
1521
+ ).run(validated.profile_ref);
1342
1522
  }
1343
1523
  this.#database.prepare(
1344
1524
  `INSERT INTO provider_profile (
1345
- provider_id, kind, adapter, display_name, base_url,
1346
- auth_mode, model, enabled, created_at, updated_at
1347
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1348
- ON CONFLICT(provider_id) DO UPDATE SET
1525
+ profile_ref, provider_kind, kind, adapter, display_name, base_url,
1526
+ auth_mode, model, capabilities, enabled, created_at, updated_at
1527
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1528
+ ON CONFLICT(profile_ref) DO UPDATE SET
1529
+ provider_kind = excluded.provider_kind,
1349
1530
  adapter = excluded.adapter,
1350
1531
  display_name = excluded.display_name,
1351
1532
  base_url = excluded.base_url,
1352
1533
  auth_mode = excluded.auth_mode,
1353
1534
  model = excluded.model,
1535
+ capabilities = excluded.capabilities,
1354
1536
  enabled = excluded.enabled,
1355
1537
  updated_at = excluded.updated_at`
1356
1538
  ).run(
1357
- validated.provider_id,
1539
+ validated.profile_ref,
1540
+ validated.provider_kind,
1358
1541
  validated.kind,
1359
1542
  validated.adapter,
1360
1543
  validated.display_name,
1361
1544
  validated.base_url,
1362
1545
  validated.auth_mode,
1363
1546
  validated.model,
1547
+ JSON.stringify(validated.capabilities),
1364
1548
  validated.enabled ? 1 : 0,
1365
1549
  validated.created_at,
1366
1550
  validated.updated_at
1367
1551
  );
1368
1552
  });
1369
- return await this.get(validated.provider_id);
1553
+ return await this.get(validated.profile_ref);
1370
1554
  }
1371
- async setEnabled(providerId) {
1372
- const existing = await this.get(providerId);
1373
- if (existing === void 0) throw providerNotConfigured(providerId);
1555
+ async setEnabled(profileRef) {
1556
+ const existing = await this.get(profileRef);
1557
+ if (existing === void 0) throw providerNotConfigured(profileRef);
1374
1558
  return this.save({ ...existing, enabled: true });
1375
1559
  }
1376
1560
  /** `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK`, per `providers.ts:1252-1263`. */
@@ -1386,8 +1570,19 @@ var SqliteProviderProfileStore = class {
1386
1570
  }
1387
1571
  };
1388
1572
  function parseRow(row) {
1573
+ let capabilities;
1574
+ try {
1575
+ capabilities = JSON.parse(row.capabilities);
1576
+ } catch (cause) {
1577
+ throw new ByokKeysError(
1578
+ "PROVIDER_PROFILE_INVALID",
1579
+ "Provider profile capabilities column is not valid JSON",
1580
+ { cause }
1581
+ );
1582
+ }
1389
1583
  return parseModelProviderProfile({
1390
1584
  ...row,
1585
+ capabilities,
1391
1586
  enabled: row.enabled === 1
1392
1587
  });
1393
1588
  }
@@ -1396,14 +1591,17 @@ var PROFILE_KEYS = [
1396
1591
  "adapter",
1397
1592
  "auth_mode",
1398
1593
  "base_url",
1594
+ "capabilities",
1399
1595
  "created_at",
1400
1596
  "display_name",
1401
1597
  "enabled",
1402
1598
  "kind",
1403
1599
  "model",
1404
- "provider_id",
1600
+ "profile_ref",
1601
+ "provider_kind",
1405
1602
  "updated_at"
1406
1603
  ];
1604
+ var MAX_PROVIDER_PROFILES = 32;
1407
1605
  var TruthStoreProviderProfileStore = class {
1408
1606
  #tenant;
1409
1607
  #truth;
@@ -1413,20 +1611,20 @@ var TruthStoreProviderProfileStore = class {
1413
1611
  }
1414
1612
  async close() {
1415
1613
  }
1416
- async delete(providerId) {
1614
+ async delete(profileRef) {
1417
1615
  const current = await this.#load();
1418
- if (!current.profiles.some((profile) => profile.provider_id === providerId)) {
1616
+ if (!current.profiles.some((profile) => profile.profile_ref === profileRef)) {
1419
1617
  return false;
1420
1618
  }
1421
1619
  await this.#write(
1422
- current.profiles.filter((profile) => profile.provider_id !== providerId),
1620
+ current.profiles.filter((profile) => profile.profile_ref !== profileRef),
1423
1621
  current.rev
1424
1622
  );
1425
1623
  return true;
1426
1624
  }
1427
- async get(providerId) {
1625
+ async get(profileRef) {
1428
1626
  return (await this.#load()).profiles.find(
1429
- (profile) => profile.provider_id === providerId
1627
+ (profile) => profile.profile_ref === profileRef
1430
1628
  );
1431
1629
  }
1432
1630
  async getEnabled() {
@@ -1438,28 +1636,28 @@ var TruthStoreProviderProfileStore = class {
1438
1636
  async save(profile) {
1439
1637
  const current = await this.#load();
1440
1638
  const existing = current.profiles.find(
1441
- (candidate) => candidate.provider_id === profile.provider_id
1639
+ (candidate) => candidate.profile_ref === profile.profile_ref
1442
1640
  );
1443
1641
  const validated = parseModelProviderProfile({
1444
1642
  ...profile,
1445
1643
  created_at: existing?.created_at ?? profile.created_at
1446
1644
  });
1447
- const next = current.profiles.filter((candidate) => candidate.provider_id !== validated.provider_id).map(
1645
+ const next = current.profiles.filter((candidate) => candidate.profile_ref !== validated.profile_ref).map(
1448
1646
  (candidate) => validated.enabled && candidate.enabled ? { ...candidate, enabled: false } : candidate
1449
1647
  );
1450
1648
  next.push(validated);
1451
1649
  await this.#write(next, current.rev);
1452
1650
  return validated;
1453
1651
  }
1454
- async setEnabled(providerId) {
1652
+ async setEnabled(profileRef) {
1455
1653
  const current = await this.#load();
1456
1654
  const selected = current.profiles.find(
1457
- (profile) => profile.provider_id === providerId
1655
+ (profile) => profile.profile_ref === profileRef
1458
1656
  );
1459
- if (selected === void 0) throw providerNotConfigured(providerId);
1657
+ if (selected === void 0) throw providerNotConfigured(profileRef);
1460
1658
  const next = current.profiles.map((profile) => ({
1461
1659
  ...profile,
1462
- enabled: profile.provider_id === providerId
1660
+ enabled: profile.profile_ref === profileRef
1463
1661
  }));
1464
1662
  await this.#write(next, current.rev);
1465
1663
  return { ...selected, enabled: true };
@@ -1505,7 +1703,7 @@ var TruthStoreProviderProfileStore = class {
1505
1703
  }
1506
1704
  };
1507
1705
  function encodeRegistry(profiles) {
1508
- const normalized = profiles.map((profile) => parseModelProviderProfile(profile)).sort((left, right) => left.provider_id.localeCompare(right.provider_id));
1706
+ const normalized = profiles.map((profile) => parseModelProviderProfile(profile)).sort((left, right) => left.profile_ref.localeCompare(right.profile_ref));
1509
1707
  assertRegistryInvariants(normalized);
1510
1708
  const snapshot = {
1511
1709
  schema_version: 1,
@@ -1513,12 +1711,14 @@ function encodeRegistry(profiles) {
1513
1711
  adapter: profile.adapter,
1514
1712
  auth_mode: profile.auth_mode,
1515
1713
  base_url: profile.base_url,
1714
+ capabilities: profile.capabilities,
1516
1715
  created_at: profile.created_at,
1517
1716
  display_name: profile.display_name,
1518
1717
  enabled: profile.enabled,
1519
1718
  kind: profile.kind,
1520
1719
  model: profile.model,
1521
- provider_id: profile.provider_id,
1720
+ profile_ref: profile.profile_ref,
1721
+ provider_kind: profile.provider_kind,
1522
1722
  updated_at: profile.updated_at
1523
1723
  }))
1524
1724
  };
@@ -1550,7 +1750,7 @@ function decodeRegistryRecord(record, tenant) {
1550
1750
  if (raw.schema_version !== 1 || !Array.isArray(raw.profiles)) {
1551
1751
  throw invalidTruth("Provider profile TruthStore body has an unsupported schema");
1552
1752
  }
1553
- if (raw.profiles.length > MODEL_PROVIDER_IDS.length) {
1753
+ if (raw.profiles.length > MAX_PROVIDER_PROFILES) {
1554
1754
  throw invalidTruth("Provider profile TruthStore body exceeds the provider registry bound");
1555
1755
  }
1556
1756
  let profiles;
@@ -1577,10 +1777,10 @@ function assertRegistryInvariants(profiles) {
1577
1777
  const seen = /* @__PURE__ */ new Set();
1578
1778
  let enabled = 0;
1579
1779
  for (const profile of profiles) {
1580
- if (seen.has(profile.provider_id)) {
1581
- throw invalidTruth(`Provider profile ${profile.provider_id} appears more than once`);
1780
+ if (seen.has(profile.profile_ref)) {
1781
+ throw invalidTruth(`Provider profile ${profile.profile_ref} appears more than once`);
1582
1782
  }
1583
- seen.add(profile.provider_id);
1783
+ seen.add(profile.profile_ref);
1584
1784
  if (profile.enabled) enabled += 1;
1585
1785
  }
1586
1786
  if (enabled > 1) {
@@ -1630,16 +1830,23 @@ var ProviderRegistry = class {
1630
1830
  * authentication it cannot perform.
1631
1831
  */
1632
1832
  async configure(configuration, secret) {
1633
- const timestamp = this.#now().toISOString();
1634
- const previous = await this.#profiles.get(configuration.provider_id);
1833
+ const previous = await this.#profiles.get(configuration.profile_ref);
1834
+ const observedNow = this.#now().getTime();
1835
+ const previousRevision = previous === void 0 ? void 0 : Date.parse(previous.updated_at);
1836
+ const revision = previousRevision === void 0 ? observedNow : Math.max(observedNow, previousRevision + 1);
1837
+ if (!Number.isSafeInteger(revision) || revision < 0) {
1838
+ throw new ByokKeysError("PROVIDER_PROFILE_INVALID", "Provider clock cannot produce a monotonic profile revision");
1839
+ }
1840
+ const timestamp = new Date(revision).toISOString();
1635
1841
  const profile = parseModelProviderProfile({
1636
1842
  ...configuration,
1843
+ capabilities: [...configuration.capabilities],
1637
1844
  created_at: previous?.created_at ?? timestamp,
1638
1845
  enabled: configuration.enabled ?? true,
1639
1846
  kind: "model",
1640
1847
  updated_at: timestamp
1641
1848
  });
1642
- const secretName = modelProviderSecretName(configuration.provider_id);
1849
+ const secretName = modelProviderSecretName(configuration.profile_ref);
1643
1850
  let previousSecret;
1644
1851
  let secretWritten = false;
1645
1852
  if (configuration.auth_mode === "none" && secret !== void 0) {
@@ -1691,14 +1898,14 @@ var ProviderRegistry = class {
1691
1898
  }
1692
1899
  return this.#status(saved);
1693
1900
  }
1694
- /** Remove a provider's profile and its secret together. */
1695
- async delete(providerId) {
1696
- const removed = await this.#profiles.delete(providerId);
1697
- await this.#secrets.delete(modelProviderSecretName(providerId));
1901
+ /** Remove a profile and its secret together. */
1902
+ async delete(profileRef) {
1903
+ const removed = await this.#profiles.delete(profileRef);
1904
+ await this.#secrets.delete(modelProviderSecretName(profileRef));
1698
1905
  return removed;
1699
1906
  }
1700
- async get(providerId) {
1701
- const profile = await this.#profiles.get(providerId);
1907
+ async get(profileRef) {
1908
+ const profile = await this.#profiles.get(profileRef);
1702
1909
  return profile === void 0 ? void 0 : this.#status(profile);
1703
1910
  }
1704
1911
  async list() {
@@ -1717,27 +1924,32 @@ var ProviderRegistry = class {
1717
1924
  const profile = await this.#profiles.getEnabled();
1718
1925
  if (profile === void 0) return void 0;
1719
1926
  const secret = await this.#secrets.get(
1720
- modelProviderSecretName(profile.provider_id)
1927
+ modelProviderSecretName(profile.profile_ref)
1721
1928
  );
1722
1929
  const options = { fetchImpl: this.#fetch, profile, secret };
1723
1930
  return profile.adapter === "anthropic" ? new AnthropicMessagesClient(options) : new OpenAiCompatibleChatClient(options);
1724
1931
  }
1725
- /** Switch which configured provider is the default. */
1726
- async setDefaultModelProvider(providerId) {
1727
- return this.#status(await this.#profiles.setEnabled(providerId));
1932
+ /** Switch which configured profile is the default. */
1933
+ async setDefaultModelProvider(profileRef) {
1934
+ return this.#status(await this.#profiles.setEnabled(profileRef));
1728
1935
  }
1729
1936
  async #status(profile) {
1937
+ const binding = exactProviderProfileBinding(profile, []);
1730
1938
  return {
1731
1939
  adapter: profile.adapter,
1732
1940
  auth_mode: profile.auth_mode,
1733
1941
  base_url: profile.base_url,
1942
+ capabilities: profile.capabilities,
1734
1943
  created_at: profile.created_at,
1735
1944
  display_name: profile.display_name,
1736
1945
  enabled: profile.enabled,
1737
1946
  model: profile.model,
1738
- provider_id: profile.provider_id,
1947
+ profile_ref: profile.profile_ref,
1948
+ profile_revision: binding.profileRevision,
1949
+ profile_hash: binding.profileHash,
1950
+ provider_kind: profile.provider_kind,
1739
1951
  secret_configured: await this.#secrets.has(
1740
- modelProviderSecretName(profile.provider_id)
1952
+ modelProviderSecretName(profile.profile_ref)
1741
1953
  ),
1742
1954
  updated_at: profile.updated_at
1743
1955
  };
@@ -1746,11 +1958,11 @@ var ProviderRegistry = class {
1746
1958
 
1747
1959
  // src/pi-provider-projection.ts
1748
1960
  var PI_PROJECTED_KEY_ENV = "PI_PROVIDER_API_KEY";
1749
- function piProjectionProviderId(profileProviderId) {
1750
- return `byok-sdk-${profileProviderId}`;
1961
+ function piProjectionProviderId(profileRef) {
1962
+ return `byok-sdk-${profileRef}`;
1751
1963
  }
1752
1964
  function buildPiProviderProjection(profile) {
1753
- const projectedProviderId = piProjectionProviderId(profile.provider_id);
1965
+ const projectedProviderId = piProjectionProviderId(profile.profile_ref);
1754
1966
  return {
1755
1967
  providers: {
1756
1968
  [projectedProviderId]: {
@@ -1761,7 +1973,11 @@ function buildPiProviderProjection(profile) {
1761
1973
  models: [
1762
1974
  {
1763
1975
  id: profile.model,
1764
- name: profile.display_name
1976
+ name: profile.display_name,
1977
+ input: [
1978
+ "text",
1979
+ ...profile.capabilities.includes("image-input") ? ["image"] : []
1980
+ ]
1765
1981
  }
1766
1982
  ]
1767
1983
  }
@@ -1769,6 +1985,6 @@ function buildPiProviderProjection(profile) {
1769
1985
  };
1770
1986
  }
1771
1987
 
1772
- export { AnthropicMessagesClient, BYOK_KEYS_ERROR_CODES, ByokKeysError, DEFAULT_KEYCHAIN_SECRET_STORAGE_PREFIX, DEFAULT_SECRET_ENVELOPE_PREFIX, DEFAULT_SECRET_SERVICE_PREFIX, EnvelopeScopedSecretStore, InMemoryProviderProfileStore, InMemorySecretStore, MODEL_PROVIDER_ADAPTERS, MODEL_PROVIDER_IDS, MODEL_PROVIDER_SECRET_NAMES, MacOsKeychainSecretStore, ModelProviderProfileSchema, OpenAiCompatibleChatClient, PI_PROJECTED_KEY_ENV, PROVIDER_AUTH_MODES, PROVIDER_PROFILE_TRUTH_RECORD_KEY, PROVIDER_RESPONSE_MAX_BYTES, PROVIDER_TIMEOUT_MS, ProviderRegistry, SECRET_NAMESPACE_PATTERN, SECRET_NAME_PATTERN, SqliteProviderProfileStore, TruthStoreProviderProfileStore, WindowsCredentialManagerSecretStore, anthropicMessageText, assertLiveModelResponse, assertSecretName, assertSecretNamespace, assertSharedSecretValue, buildPiProviderProjection, chatCompletionText, classifyModelProviderHttpError, decodeStrictBase64Utf8, fetchWithProviderGuards, isLoopbackHost, isLoopbackProviderUrl, isPrivateNetworkLiteral, isSqliteAvailable, loadSqliteModule, modelApiUrl, modelMessageText, modelProviderSecretName, normalizeProviderUrl, objectValue, openSqliteDatabase, parseBoundedJsonResponse, parseModelProviderProfile, providerHeaders, readModelProviderResponse, requiredProviderSecret, runCommand, scopeSecretStore, secretScopeId, secureSqliteFilePermissions };
1988
+ export { AnthropicMessagesClient, BYOK_KEYS_ERROR_CODES, ByokKeysError, DEFAULT_KEYCHAIN_SECRET_STORAGE_PREFIX, DEFAULT_SECRET_ENVELOPE_PREFIX, DEFAULT_SECRET_SERVICE_PREFIX, EnvelopeScopedSecretStore, InMemoryProviderProfileStore, InMemorySecretStore, MODEL_PROVIDER_ADAPTERS, MODEL_PROVIDER_KINDS, MODEL_PROVIDER_VENDORS, MODEL_PROVIDER_VENDOR_IDS, MacOsKeychainSecretStore, ModelProviderProfileSchema, OpenAiCompatibleChatClient, PI_PROJECTED_KEY_ENV, PROVIDER_AUTH_MODES, PROVIDER_MODEL_CAPABILITIES, PROVIDER_PROFILE_TRUTH_RECORD_KEY, PROVIDER_RESPONSE_MAX_BYTES, PROVIDER_TIMEOUT_MS, ProviderModelCapabilitySchema, ProviderProfileRefSchema, ProviderRegistry, SECRET_NAMESPACE_PATTERN, SECRET_NAME_PATTERN, SqliteProviderProfileStore, TruthStoreProviderProfileStore, WindowsCredentialManagerSecretStore, anthropicMessageText, assertExactProviderProfileBinding, assertLiveModelResponse, assertSecretName, assertSecretNamespace, assertSharedSecretValue, buildPiProviderProjection, chatCompletionText, classifyModelProviderHttpError, decodeStrictBase64Utf8, exactProviderProfileBinding, fetchWithProviderGuards, isLoopbackHost, isLoopbackProviderUrl, isPrivateNetworkLiteral, isSqliteAvailable, loadSqliteModule, modelApiUrl, modelMessageText, modelProviderSecretName, modelProviderVendor, normalizeProviderUrl, objectValue, openSqliteDatabase, parseBoundedJsonResponse, parseModelProviderProfile, providerHeaders, readModelProviderResponse, requiredProviderSecret, runCommand, scopeSecretStore, secretScopeId, secureSqliteFilePermissions };
1773
1989
  //# sourceMappingURL=index.js.map
1774
1990
  //# sourceMappingURL=index.js.map