@byok-sdk/keys 0.3.9 → 0.3.10

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';
@@ -99,12 +99,19 @@ function isPrivateNetworkLiteral(hostname) {
99
99
  }
100
100
 
101
101
  // src/provider-profile.ts
102
- var MODEL_PROVIDER_IDS = [
102
+ var PROVIDER_PROFILE_REF_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/u;
103
+ var ProviderProfileRefSchema = z.string().min(1).max(64).regex(
104
+ PROVIDER_PROFILE_REF_PATTERN,
105
+ "provider profile refs must be lowercase portable identifiers"
106
+ );
107
+ var MODEL_PROVIDER_KINDS = [
103
108
  "openai",
104
109
  "deepseek",
105
110
  "anthropic",
106
111
  "custom"
107
112
  ];
113
+ var PROVIDER_MODEL_CAPABILITIES = ["image-input"];
114
+ var ProviderModelCapabilitySchema = z.enum(PROVIDER_MODEL_CAPABILITIES);
108
115
  var PROVIDER_AUTH_MODES = ["bearer", "x_api_key", "none"];
109
116
  var MODEL_PROVIDER_ADAPTERS = ["openai_compatible", "anthropic"];
110
117
  function boundedString(field, maximumLength) {
@@ -144,14 +151,23 @@ var ModelProviderProfileSchema = z.object({
144
151
  adapter: z.enum(MODEL_PROVIDER_ADAPTERS),
145
152
  auth_mode: z.enum(PROVIDER_AUTH_MODES),
146
153
  base_url: providerBaseUrl,
154
+ capabilities: z.array(ProviderModelCapabilitySchema).max(8),
147
155
  created_at: isoTimestamp("created_at"),
148
156
  display_name: boundedString("display_name", 100),
149
157
  enabled: z.boolean(),
150
158
  kind: z.literal("model"),
151
159
  model: boundedString("model", 160),
152
- provider_id: z.enum(MODEL_PROVIDER_IDS),
160
+ profile_ref: ProviderProfileRefSchema,
161
+ provider_kind: z.enum(MODEL_PROVIDER_KINDS),
153
162
  updated_at: isoTimestamp("updated_at")
154
163
  }).superRefine((profile, ctx) => {
164
+ if (new Set(profile.capabilities).size !== profile.capabilities.length) {
165
+ ctx.addIssue({
166
+ code: "custom",
167
+ message: "Provider capabilities cannot repeat",
168
+ path: ["capabilities"]
169
+ });
170
+ }
155
171
  if (profile.adapter === "anthropic" && profile.auth_mode !== "x_api_key") {
156
172
  ctx.addIssue({
157
173
  code: "custom",
@@ -174,6 +190,47 @@ var ModelProviderProfileSchema = z.object({
174
190
  });
175
191
  }
176
192
  });
193
+ function exactProviderProfileBinding(profileInput, requiredCapabilities = profileInput.capabilities) {
194
+ const profile = parseModelProviderProfile(profileInput);
195
+ const revision = Date.parse(profile.updated_at);
196
+ if (!Number.isSafeInteger(revision) || revision < 0) {
197
+ throw new ByokKeysError(
198
+ "PROVIDER_PROFILE_INVALID",
199
+ "Provider updated_at cannot be represented as a canonical revision"
200
+ );
201
+ }
202
+ const normalizedCapabilities = [...profile.capabilities].sort();
203
+ const canonical = JSON.stringify({
204
+ adapter: profile.adapter,
205
+ auth_mode: profile.auth_mode,
206
+ base_url: profile.base_url,
207
+ capabilities: normalizedCapabilities,
208
+ kind: profile.kind,
209
+ model: profile.model,
210
+ profile_ref: profile.profile_ref,
211
+ provider_kind: profile.provider_kind
212
+ });
213
+ return {
214
+ profileRef: profile.profile_ref,
215
+ profileRevision: String(revision),
216
+ profileHash: `sha256:${createHash("sha256").update(canonical).digest("hex")}`,
217
+ modelId: profile.model,
218
+ requiredCapabilities: [...requiredCapabilities]
219
+ };
220
+ }
221
+ function assertExactProviderProfileBinding(profile, expected) {
222
+ if (new Set(expected.requiredCapabilities).size !== expected.requiredCapabilities.length) {
223
+ throw new Error("provider profile required capabilities must be unique");
224
+ }
225
+ const actual = exactProviderProfileBinding(profile, expected.requiredCapabilities);
226
+ if (actual.profileRef !== expected.profileRef) throw new Error("provider profile ref mismatch");
227
+ if (actual.profileRevision !== expected.profileRevision) throw new Error("provider profile revision mismatch");
228
+ if (actual.profileHash !== expected.profileHash) throw new Error("provider profile hash mismatch");
229
+ if (actual.modelId !== expected.modelId) throw new Error("provider profile model mismatch");
230
+ const supported = new Set(profile.capabilities);
231
+ const unsupported = expected.requiredCapabilities.find((capability) => !supported.has(capability));
232
+ if (unsupported !== void 0) throw new Error(`provider profile does not support required capability ${unsupported}`);
233
+ }
177
234
  function parseModelProviderProfile(value) {
178
235
  const result = ModelProviderProfileSchema.safeParse(value);
179
236
  if (result.success) return result.data;
@@ -527,14 +584,16 @@ function assertSecretNamespace(value) {
527
584
 
528
585
  // src/secret-store.ts
529
586
  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];
587
+ function modelProviderSecretName(profileRef) {
588
+ const parsed = ProviderProfileRefSchema.safeParse(profileRef);
589
+ if (!parsed.success) {
590
+ throw new ByokKeysError(
591
+ "PROVIDER_PROFILE_INVALID",
592
+ "Provider profile ref must be a lowercase portable identifier",
593
+ { cause: parsed.error }
594
+ );
595
+ }
596
+ return assertSecretName(`model-${parsed.data}-api-key`);
538
597
  }
539
598
  function assertSharedSecretValue(secret) {
540
599
  if (secret.length === 0 || /[\u0000\r\n]/u.test(secret)) {
@@ -1143,10 +1202,10 @@ function normalizeSecretScope(scope) {
1143
1202
  }
1144
1203
 
1145
1204
  // src/profile-store.ts
1146
- function providerNotConfigured(providerId) {
1205
+ function providerNotConfigured(profileRef) {
1147
1206
  return new ByokKeysError(
1148
1207
  "PROVIDER_NOT_CONFIGURED",
1149
- `${providerId} model provider is not configured`
1208
+ `${profileRef} model provider is not configured`
1150
1209
  );
1151
1210
  }
1152
1211
  var InMemoryProviderProfileStore = class {
@@ -1154,38 +1213,38 @@ var InMemoryProviderProfileStore = class {
1154
1213
  async close() {
1155
1214
  this.#profiles.clear();
1156
1215
  }
1157
- async delete(providerId) {
1158
- return this.#profiles.delete(providerId);
1216
+ async delete(profileRef) {
1217
+ return this.#profiles.delete(profileRef);
1159
1218
  }
1160
- async get(providerId) {
1161
- return this.#profiles.get(providerId);
1219
+ async get(profileRef) {
1220
+ return this.#profiles.get(profileRef);
1162
1221
  }
1163
1222
  async getEnabled() {
1164
1223
  return [...this.#profiles.values()].find((profile) => profile.enabled);
1165
1224
  }
1166
1225
  async list() {
1167
1226
  return [...this.#profiles.values()].sort(
1168
- (left, right) => left.provider_id.localeCompare(right.provider_id)
1227
+ (left, right) => left.profile_ref.localeCompare(right.profile_ref)
1169
1228
  );
1170
1229
  }
1171
1230
  async save(profile) {
1172
1231
  const validated = parseModelProviderProfile({
1173
1232
  ...profile,
1174
- created_at: this.#profiles.get(profile.provider_id)?.created_at ?? profile.created_at
1233
+ created_at: this.#profiles.get(profile.profile_ref)?.created_at ?? profile.created_at
1175
1234
  });
1176
1235
  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 });
1236
+ for (const [profileRef, existing] of this.#profiles) {
1237
+ if (profileRef !== validated.profile_ref && existing.enabled) {
1238
+ this.#profiles.set(profileRef, { ...existing, enabled: false });
1180
1239
  }
1181
1240
  }
1182
1241
  }
1183
- this.#profiles.set(validated.provider_id, validated);
1242
+ this.#profiles.set(validated.profile_ref, validated);
1184
1243
  return validated;
1185
1244
  }
1186
- async setEnabled(providerId) {
1187
- const existing = this.#profiles.get(providerId);
1188
- if (existing === void 0) throw providerNotConfigured(providerId);
1245
+ async setEnabled(profileRef) {
1246
+ const existing = this.#profiles.get(profileRef);
1247
+ if (existing === void 0) throw providerNotConfigured(profileRef);
1189
1248
  return this.save({ ...existing, enabled: true });
1190
1249
  }
1191
1250
  };
@@ -1263,16 +1322,18 @@ function secureSqliteFilePermissions(databasePath) {
1263
1322
  // src/sqlite-profile-store.ts
1264
1323
  var SCHEMA = `
1265
1324
  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
1325
+ profile_ref TEXT PRIMARY KEY,
1326
+ provider_kind TEXT NOT NULL CHECK (provider_kind IN ('openai', 'deepseek', 'anthropic', 'custom')),
1327
+ kind TEXT NOT NULL CHECK (kind = 'model'),
1328
+ adapter TEXT NOT NULL CHECK (adapter IN ('openai_compatible', 'anthropic')),
1329
+ display_name TEXT NOT NULL,
1330
+ base_url TEXT NOT NULL,
1331
+ auth_mode TEXT NOT NULL CHECK (auth_mode IN ('bearer', 'x_api_key', 'none')),
1332
+ model TEXT NOT NULL,
1333
+ capabilities TEXT NOT NULL,
1334
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
1335
+ created_at TEXT NOT NULL,
1336
+ updated_at TEXT NOT NULL
1276
1337
  );
1277
1338
  `;
1278
1339
  var ENABLED_INDEX = `
@@ -1312,12 +1373,12 @@ var SqliteProviderProfileStore = class {
1312
1373
  this.#closed = true;
1313
1374
  this.#database.close();
1314
1375
  }
1315
- async delete(providerId) {
1316
- const result = this.#database.prepare("DELETE FROM provider_profile WHERE provider_id = ?").run(providerId);
1376
+ async delete(profileRef) {
1377
+ const result = this.#database.prepare("DELETE FROM provider_profile WHERE profile_ref = ?").run(profileRef);
1317
1378
  return Number(result.changes) === 1;
1318
1379
  }
1319
- async get(providerId) {
1320
- const row = this.#database.prepare("SELECT * FROM provider_profile WHERE provider_id = ?").get(providerId);
1380
+ async get(profileRef) {
1381
+ const row = this.#database.prepare("SELECT * FROM provider_profile WHERE profile_ref = ?").get(profileRef);
1321
1382
  return row === void 0 ? void 0 : parseRow(row);
1322
1383
  }
1323
1384
  async getEnabled() {
@@ -1325,11 +1386,11 @@ var SqliteProviderProfileStore = class {
1325
1386
  return row === void 0 ? void 0 : parseRow(row);
1326
1387
  }
1327
1388
  async list() {
1328
- const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY provider_id ASC").all();
1389
+ const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY profile_ref ASC").all();
1329
1390
  return rows.map(parseRow);
1330
1391
  }
1331
1392
  async save(profile) {
1332
- const existing = await this.get(profile.provider_id);
1393
+ const existing = await this.get(profile.profile_ref);
1333
1394
  const validated = parseModelProviderProfile({
1334
1395
  ...profile,
1335
1396
  created_at: existing?.created_at ?? profile.created_at
@@ -1337,40 +1398,44 @@ var SqliteProviderProfileStore = class {
1337
1398
  this.#transaction(() => {
1338
1399
  if (validated.enabled) {
1339
1400
  this.#database.prepare(
1340
- "UPDATE provider_profile SET enabled = 0 WHERE provider_id <> ?"
1341
- ).run(validated.provider_id);
1401
+ "UPDATE provider_profile SET enabled = 0 WHERE profile_ref <> ?"
1402
+ ).run(validated.profile_ref);
1342
1403
  }
1343
1404
  this.#database.prepare(
1344
1405
  `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
1406
+ profile_ref, provider_kind, kind, adapter, display_name, base_url,
1407
+ auth_mode, model, capabilities, enabled, created_at, updated_at
1408
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1409
+ ON CONFLICT(profile_ref) DO UPDATE SET
1410
+ provider_kind = excluded.provider_kind,
1349
1411
  adapter = excluded.adapter,
1350
1412
  display_name = excluded.display_name,
1351
1413
  base_url = excluded.base_url,
1352
1414
  auth_mode = excluded.auth_mode,
1353
1415
  model = excluded.model,
1416
+ capabilities = excluded.capabilities,
1354
1417
  enabled = excluded.enabled,
1355
1418
  updated_at = excluded.updated_at`
1356
1419
  ).run(
1357
- validated.provider_id,
1420
+ validated.profile_ref,
1421
+ validated.provider_kind,
1358
1422
  validated.kind,
1359
1423
  validated.adapter,
1360
1424
  validated.display_name,
1361
1425
  validated.base_url,
1362
1426
  validated.auth_mode,
1363
1427
  validated.model,
1428
+ JSON.stringify(validated.capabilities),
1364
1429
  validated.enabled ? 1 : 0,
1365
1430
  validated.created_at,
1366
1431
  validated.updated_at
1367
1432
  );
1368
1433
  });
1369
- return await this.get(validated.provider_id);
1434
+ return await this.get(validated.profile_ref);
1370
1435
  }
1371
- async setEnabled(providerId) {
1372
- const existing = await this.get(providerId);
1373
- if (existing === void 0) throw providerNotConfigured(providerId);
1436
+ async setEnabled(profileRef) {
1437
+ const existing = await this.get(profileRef);
1438
+ if (existing === void 0) throw providerNotConfigured(profileRef);
1374
1439
  return this.save({ ...existing, enabled: true });
1375
1440
  }
1376
1441
  /** `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK`, per `providers.ts:1252-1263`. */
@@ -1386,8 +1451,19 @@ var SqliteProviderProfileStore = class {
1386
1451
  }
1387
1452
  };
1388
1453
  function parseRow(row) {
1454
+ let capabilities;
1455
+ try {
1456
+ capabilities = JSON.parse(row.capabilities);
1457
+ } catch (cause) {
1458
+ throw new ByokKeysError(
1459
+ "PROVIDER_PROFILE_INVALID",
1460
+ "Provider profile capabilities column is not valid JSON",
1461
+ { cause }
1462
+ );
1463
+ }
1389
1464
  return parseModelProviderProfile({
1390
1465
  ...row,
1466
+ capabilities,
1391
1467
  enabled: row.enabled === 1
1392
1468
  });
1393
1469
  }
@@ -1396,14 +1472,17 @@ var PROFILE_KEYS = [
1396
1472
  "adapter",
1397
1473
  "auth_mode",
1398
1474
  "base_url",
1475
+ "capabilities",
1399
1476
  "created_at",
1400
1477
  "display_name",
1401
1478
  "enabled",
1402
1479
  "kind",
1403
1480
  "model",
1404
- "provider_id",
1481
+ "profile_ref",
1482
+ "provider_kind",
1405
1483
  "updated_at"
1406
1484
  ];
1485
+ var MAX_PROVIDER_PROFILES = 32;
1407
1486
  var TruthStoreProviderProfileStore = class {
1408
1487
  #tenant;
1409
1488
  #truth;
@@ -1413,20 +1492,20 @@ var TruthStoreProviderProfileStore = class {
1413
1492
  }
1414
1493
  async close() {
1415
1494
  }
1416
- async delete(providerId) {
1495
+ async delete(profileRef) {
1417
1496
  const current = await this.#load();
1418
- if (!current.profiles.some((profile) => profile.provider_id === providerId)) {
1497
+ if (!current.profiles.some((profile) => profile.profile_ref === profileRef)) {
1419
1498
  return false;
1420
1499
  }
1421
1500
  await this.#write(
1422
- current.profiles.filter((profile) => profile.provider_id !== providerId),
1501
+ current.profiles.filter((profile) => profile.profile_ref !== profileRef),
1423
1502
  current.rev
1424
1503
  );
1425
1504
  return true;
1426
1505
  }
1427
- async get(providerId) {
1506
+ async get(profileRef) {
1428
1507
  return (await this.#load()).profiles.find(
1429
- (profile) => profile.provider_id === providerId
1508
+ (profile) => profile.profile_ref === profileRef
1430
1509
  );
1431
1510
  }
1432
1511
  async getEnabled() {
@@ -1438,28 +1517,28 @@ var TruthStoreProviderProfileStore = class {
1438
1517
  async save(profile) {
1439
1518
  const current = await this.#load();
1440
1519
  const existing = current.profiles.find(
1441
- (candidate) => candidate.provider_id === profile.provider_id
1520
+ (candidate) => candidate.profile_ref === profile.profile_ref
1442
1521
  );
1443
1522
  const validated = parseModelProviderProfile({
1444
1523
  ...profile,
1445
1524
  created_at: existing?.created_at ?? profile.created_at
1446
1525
  });
1447
- const next = current.profiles.filter((candidate) => candidate.provider_id !== validated.provider_id).map(
1526
+ const next = current.profiles.filter((candidate) => candidate.profile_ref !== validated.profile_ref).map(
1448
1527
  (candidate) => validated.enabled && candidate.enabled ? { ...candidate, enabled: false } : candidate
1449
1528
  );
1450
1529
  next.push(validated);
1451
1530
  await this.#write(next, current.rev);
1452
1531
  return validated;
1453
1532
  }
1454
- async setEnabled(providerId) {
1533
+ async setEnabled(profileRef) {
1455
1534
  const current = await this.#load();
1456
1535
  const selected = current.profiles.find(
1457
- (profile) => profile.provider_id === providerId
1536
+ (profile) => profile.profile_ref === profileRef
1458
1537
  );
1459
- if (selected === void 0) throw providerNotConfigured(providerId);
1538
+ if (selected === void 0) throw providerNotConfigured(profileRef);
1460
1539
  const next = current.profiles.map((profile) => ({
1461
1540
  ...profile,
1462
- enabled: profile.provider_id === providerId
1541
+ enabled: profile.profile_ref === profileRef
1463
1542
  }));
1464
1543
  await this.#write(next, current.rev);
1465
1544
  return { ...selected, enabled: true };
@@ -1505,7 +1584,7 @@ var TruthStoreProviderProfileStore = class {
1505
1584
  }
1506
1585
  };
1507
1586
  function encodeRegistry(profiles) {
1508
- const normalized = profiles.map((profile) => parseModelProviderProfile(profile)).sort((left, right) => left.provider_id.localeCompare(right.provider_id));
1587
+ const normalized = profiles.map((profile) => parseModelProviderProfile(profile)).sort((left, right) => left.profile_ref.localeCompare(right.profile_ref));
1509
1588
  assertRegistryInvariants(normalized);
1510
1589
  const snapshot = {
1511
1590
  schema_version: 1,
@@ -1513,12 +1592,14 @@ function encodeRegistry(profiles) {
1513
1592
  adapter: profile.adapter,
1514
1593
  auth_mode: profile.auth_mode,
1515
1594
  base_url: profile.base_url,
1595
+ capabilities: profile.capabilities,
1516
1596
  created_at: profile.created_at,
1517
1597
  display_name: profile.display_name,
1518
1598
  enabled: profile.enabled,
1519
1599
  kind: profile.kind,
1520
1600
  model: profile.model,
1521
- provider_id: profile.provider_id,
1601
+ profile_ref: profile.profile_ref,
1602
+ provider_kind: profile.provider_kind,
1522
1603
  updated_at: profile.updated_at
1523
1604
  }))
1524
1605
  };
@@ -1550,7 +1631,7 @@ function decodeRegistryRecord(record, tenant) {
1550
1631
  if (raw.schema_version !== 1 || !Array.isArray(raw.profiles)) {
1551
1632
  throw invalidTruth("Provider profile TruthStore body has an unsupported schema");
1552
1633
  }
1553
- if (raw.profiles.length > MODEL_PROVIDER_IDS.length) {
1634
+ if (raw.profiles.length > MAX_PROVIDER_PROFILES) {
1554
1635
  throw invalidTruth("Provider profile TruthStore body exceeds the provider registry bound");
1555
1636
  }
1556
1637
  let profiles;
@@ -1577,10 +1658,10 @@ function assertRegistryInvariants(profiles) {
1577
1658
  const seen = /* @__PURE__ */ new Set();
1578
1659
  let enabled = 0;
1579
1660
  for (const profile of profiles) {
1580
- if (seen.has(profile.provider_id)) {
1581
- throw invalidTruth(`Provider profile ${profile.provider_id} appears more than once`);
1661
+ if (seen.has(profile.profile_ref)) {
1662
+ throw invalidTruth(`Provider profile ${profile.profile_ref} appears more than once`);
1582
1663
  }
1583
- seen.add(profile.provider_id);
1664
+ seen.add(profile.profile_ref);
1584
1665
  if (profile.enabled) enabled += 1;
1585
1666
  }
1586
1667
  if (enabled > 1) {
@@ -1630,16 +1711,23 @@ var ProviderRegistry = class {
1630
1711
  * authentication it cannot perform.
1631
1712
  */
1632
1713
  async configure(configuration, secret) {
1633
- const timestamp = this.#now().toISOString();
1634
- const previous = await this.#profiles.get(configuration.provider_id);
1714
+ const previous = await this.#profiles.get(configuration.profile_ref);
1715
+ const observedNow = this.#now().getTime();
1716
+ const previousRevision = previous === void 0 ? void 0 : Date.parse(previous.updated_at);
1717
+ const revision = previousRevision === void 0 ? observedNow : Math.max(observedNow, previousRevision + 1);
1718
+ if (!Number.isSafeInteger(revision) || revision < 0) {
1719
+ throw new ByokKeysError("PROVIDER_PROFILE_INVALID", "Provider clock cannot produce a monotonic profile revision");
1720
+ }
1721
+ const timestamp = new Date(revision).toISOString();
1635
1722
  const profile = parseModelProviderProfile({
1636
1723
  ...configuration,
1724
+ capabilities: [...configuration.capabilities],
1637
1725
  created_at: previous?.created_at ?? timestamp,
1638
1726
  enabled: configuration.enabled ?? true,
1639
1727
  kind: "model",
1640
1728
  updated_at: timestamp
1641
1729
  });
1642
- const secretName = modelProviderSecretName(configuration.provider_id);
1730
+ const secretName = modelProviderSecretName(configuration.profile_ref);
1643
1731
  let previousSecret;
1644
1732
  let secretWritten = false;
1645
1733
  if (configuration.auth_mode === "none" && secret !== void 0) {
@@ -1691,14 +1779,14 @@ var ProviderRegistry = class {
1691
1779
  }
1692
1780
  return this.#status(saved);
1693
1781
  }
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));
1782
+ /** Remove a profile and its secret together. */
1783
+ async delete(profileRef) {
1784
+ const removed = await this.#profiles.delete(profileRef);
1785
+ await this.#secrets.delete(modelProviderSecretName(profileRef));
1698
1786
  return removed;
1699
1787
  }
1700
- async get(providerId) {
1701
- const profile = await this.#profiles.get(providerId);
1788
+ async get(profileRef) {
1789
+ const profile = await this.#profiles.get(profileRef);
1702
1790
  return profile === void 0 ? void 0 : this.#status(profile);
1703
1791
  }
1704
1792
  async list() {
@@ -1717,27 +1805,32 @@ var ProviderRegistry = class {
1717
1805
  const profile = await this.#profiles.getEnabled();
1718
1806
  if (profile === void 0) return void 0;
1719
1807
  const secret = await this.#secrets.get(
1720
- modelProviderSecretName(profile.provider_id)
1808
+ modelProviderSecretName(profile.profile_ref)
1721
1809
  );
1722
1810
  const options = { fetchImpl: this.#fetch, profile, secret };
1723
1811
  return profile.adapter === "anthropic" ? new AnthropicMessagesClient(options) : new OpenAiCompatibleChatClient(options);
1724
1812
  }
1725
- /** Switch which configured provider is the default. */
1726
- async setDefaultModelProvider(providerId) {
1727
- return this.#status(await this.#profiles.setEnabled(providerId));
1813
+ /** Switch which configured profile is the default. */
1814
+ async setDefaultModelProvider(profileRef) {
1815
+ return this.#status(await this.#profiles.setEnabled(profileRef));
1728
1816
  }
1729
1817
  async #status(profile) {
1818
+ const binding = exactProviderProfileBinding(profile, []);
1730
1819
  return {
1731
1820
  adapter: profile.adapter,
1732
1821
  auth_mode: profile.auth_mode,
1733
1822
  base_url: profile.base_url,
1823
+ capabilities: profile.capabilities,
1734
1824
  created_at: profile.created_at,
1735
1825
  display_name: profile.display_name,
1736
1826
  enabled: profile.enabled,
1737
1827
  model: profile.model,
1738
- provider_id: profile.provider_id,
1828
+ profile_ref: profile.profile_ref,
1829
+ profile_revision: binding.profileRevision,
1830
+ profile_hash: binding.profileHash,
1831
+ provider_kind: profile.provider_kind,
1739
1832
  secret_configured: await this.#secrets.has(
1740
- modelProviderSecretName(profile.provider_id)
1833
+ modelProviderSecretName(profile.profile_ref)
1741
1834
  ),
1742
1835
  updated_at: profile.updated_at
1743
1836
  };
@@ -1746,11 +1839,11 @@ var ProviderRegistry = class {
1746
1839
 
1747
1840
  // src/pi-provider-projection.ts
1748
1841
  var PI_PROJECTED_KEY_ENV = "PI_PROVIDER_API_KEY";
1749
- function piProjectionProviderId(profileProviderId) {
1750
- return `byok-sdk-${profileProviderId}`;
1842
+ function piProjectionProviderId(profileRef) {
1843
+ return `byok-sdk-${profileRef}`;
1751
1844
  }
1752
1845
  function buildPiProviderProjection(profile) {
1753
- const projectedProviderId = piProjectionProviderId(profile.provider_id);
1846
+ const projectedProviderId = piProjectionProviderId(profile.profile_ref);
1754
1847
  return {
1755
1848
  providers: {
1756
1849
  [projectedProviderId]: {
@@ -1761,7 +1854,11 @@ function buildPiProviderProjection(profile) {
1761
1854
  models: [
1762
1855
  {
1763
1856
  id: profile.model,
1764
- name: profile.display_name
1857
+ name: profile.display_name,
1858
+ input: [
1859
+ "text",
1860
+ ...profile.capabilities.includes("image-input") ? ["image"] : []
1861
+ ]
1765
1862
  }
1766
1863
  ]
1767
1864
  }
@@ -1769,6 +1866,6 @@ function buildPiProviderProjection(profile) {
1769
1866
  };
1770
1867
  }
1771
1868
 
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 };
1869
+ 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, 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, normalizeProviderUrl, objectValue, openSqliteDatabase, parseBoundedJsonResponse, parseModelProviderProfile, providerHeaders, readModelProviderResponse, requiredProviderSecret, runCommand, scopeSecretStore, secretScopeId, secureSqliteFilePermissions };
1773
1870
  //# sourceMappingURL=index.js.map
1774
1871
  //# sourceMappingURL=index.js.map