@skaile/forge-settings 1.3.3 → 1.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -0
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +13 -0
  5. package/dist/runtime/components/ForgeProvidersPanel.vue +461 -78
  6. package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +13 -0
  7. package/dist/runtime/server/harness-settings.d.ts +50 -12
  8. package/dist/runtime/server/harness-settings.js +93 -9
  9. package/dist/runtime/server/index.d.ts +2 -0
  10. package/dist/runtime/server/index.js +2 -0
  11. package/dist/runtime/server/provider-service.d.ts +26 -2
  12. package/dist/runtime/server/provider-service.js +16 -1
  13. package/dist/runtime/server/provider-write.d.ts +25 -9
  14. package/dist/runtime/server/provider-write.js +72 -4
  15. package/dist/runtime/shared/data-zone.d.ts +66 -0
  16. package/dist/runtime/shared/data-zone.js +79 -0
  17. package/dist/runtime/shared/harness-providers.d.ts +41 -2
  18. package/dist/runtime/shared/harness-providers.js +31 -8
  19. package/dist/runtime/shared/harnesses.d.ts +7 -0
  20. package/dist/runtime/shared/harnesses.js +18 -0
  21. package/dist/runtime/shared/provider-accounts.d.ts +215 -0
  22. package/dist/runtime/shared/provider-accounts.js +255 -0
  23. package/dist/runtime/shared/provider-registry.d.ts +38 -0
  24. package/dist/runtime/shared/provider-registry.js +16 -0
  25. package/dist/runtime/shared/settings-types.d.ts +7 -0
  26. package/dist/runtime/vue/index.d.ts +2 -0
  27. package/dist/runtime/vue/index.js +2 -0
  28. package/dist/runtime/vue/providers-panel-model.d.ts +98 -5
  29. package/dist/runtime/vue/providers-panel-model.js +13 -1
  30. package/dist/runtime/vue/useProvidersPanel.d.ts +66 -5
  31. package/dist/runtime/vue/useProvidersPanel.js +417 -29
  32. package/package.json +2 -2
  33. package/src/runtime/components/ForgeProvidersPanel.vue +477 -78
  34. package/src/runtime/server/harness-settings.ts +165 -21
  35. package/src/runtime/server/index.ts +2 -0
  36. package/src/runtime/server/provider-service.ts +53 -1
  37. package/src/runtime/server/provider-write.ts +129 -15
  38. package/src/runtime/shared/data-zone.ts +172 -0
  39. package/src/runtime/shared/harness-providers.ts +77 -6
  40. package/src/runtime/shared/harnesses.ts +25 -0
  41. package/src/runtime/shared/provider-accounts.ts +526 -0
  42. package/src/runtime/shared/provider-registry.ts +58 -0
  43. package/src/runtime/shared/settings-types.ts +7 -0
  44. package/src/runtime/vue/index.ts +2 -0
  45. package/src/runtime/vue/providers-panel-model.ts +123 -5
  46. package/src/runtime/vue/useProvidersPanel.ts +586 -37
@@ -5,9 +5,27 @@ import {
5
5
  ref,
6
6
  watch
7
7
  } from "vue";
8
+ import { DATA_ZONE_LABELS, dataZoneRefusal, entryDataZone } from "../shared/data-zone.js";
8
9
  import {
9
10
  validateEnvVarName
10
11
  } from "../shared/env-vars.js";
12
+ import { isPlainObject } from "../shared/guards.js";
13
+ import {
14
+ AZURE_OPENAI_DEFAULT_API_VERSION,
15
+ AZURE_OPENAI_PRESET,
16
+ azureAccountEntries,
17
+ credentialExpiry,
18
+ defaultParamValue,
19
+ EXPERT_PARAMS,
20
+ FOUNDRY_PRESET,
21
+ missingRequiredParams,
22
+ PROVIDER_PARAM_LABELS,
23
+ presetParamKeys,
24
+ presetSignInOptions,
25
+ providerAccountLine,
26
+ signInCredential,
27
+ signInOptionOf
28
+ } from "../shared/provider-accounts.js";
11
29
  import {
12
30
  AI_PROVIDER_SURFACES,
13
31
  validateCredentialRef
@@ -16,6 +34,7 @@ import {
16
34
  urlProvidersClient
17
35
  } from "./providers-client.js";
18
36
  import {
37
+ AZURE_ACCOUNT,
19
38
  CREDENTIAL_ENTER_KEY,
20
39
  CREDENTIAL_OTHER_REF,
21
40
  CUSTOM_PRESET,
@@ -26,9 +45,18 @@ import {
26
45
  nonEnvSourceLabel,
27
46
  OVERRIDES_INSTANCE_NOTICE,
28
47
  PROVIDER_ID_PATTERN,
48
+ providerModelChoices,
49
+ providerModelsChip,
29
50
  requestErrorMessage
30
51
  } from "./providers-panel-model.js";
31
- export { CREDENTIAL_ENTER_KEY, CREDENTIAL_OTHER_REF, CUSTOM_PRESET } from "./providers-panel-model.js";
52
+ export {
53
+ AZURE_ACCOUNT,
54
+ CREDENTIAL_ENTER_KEY,
55
+ CREDENTIAL_OTHER_REF,
56
+ CUSTOM_PRESET,
57
+ providerModelChoices,
58
+ providerModelsChip
59
+ } from "./providers-panel-model.js";
32
60
  export function useProvidersPanel(props, emit) {
33
61
  const providers = ref([]);
34
62
  const presets = ref([]);
@@ -173,11 +201,18 @@ export function useProvidersPanel(props, emit) {
173
201
  if (entry.locked) notices.push("Locked by the control plane.");
174
202
  return notices;
175
203
  }
204
+ function blockedNoteOf(zone, euProcessing) {
205
+ const rule = props.dataZoneRule;
206
+ if (!rule || rule === "any") return null;
207
+ return dataZoneRefusal(rule, zone, euProcessing) ?? null;
208
+ }
176
209
  function buildRow(entry) {
177
210
  const state = stateOf(entry);
178
211
  const writable = !props.readOnly && !entry.locked;
179
212
  const needsCredential = entry.credentialKind !== "none";
180
213
  const envName = envNameOfRef(entry.credentialRef);
214
+ const zone = entry.dataZone ?? entryDataZone({ preset: entry.preset });
215
+ const isModelEntry = entry.kind === "llm" || entry.kind === "embed";
181
216
  return {
182
217
  ...entry,
183
218
  state,
@@ -192,25 +227,52 @@ export function useProvidersPanel(props, emit) {
192
227
  credentialOptions: needsCredential ? credentialOptionsOf(entry) : [],
193
228
  origin: entry.preset ?? entry.surface ?? entry.kind,
194
229
  status: statusOf(entry, needsCredential),
195
- overrideNotices: overrideNoticesOf(entry)
230
+ overrideNotices: overrideNoticesOf(entry),
231
+ accountLine: providerAccountLine(entry, presets.value),
232
+ expiry: needsCredential ? credentialExpiry(entry.credentialExpiresAt) : null,
233
+ canRenew: needsCredential && writable,
234
+ zone,
235
+ zoneLabel: DATA_ZONE_LABELS[zone],
236
+ canSetEuProcessing: zone === "eu_option" && writable && state === "own",
237
+ blockedNote: blockedNoteOf(zone, entry.euProcessing),
238
+ modelsChip: isModelEntry ? providerModelsChip(entry.allowedModels) : null,
239
+ canFilterModels: isModelEntry && writable && state === "own"
196
240
  };
197
241
  }
198
242
  const rows = computed(() => providers.value.map(buildRow));
199
243
  const dialogVisible = ref(false);
200
244
  const editing = ref(null);
201
- const form = reactive({ ...EMPTY_FORM });
245
+ const form = reactive({ ...EMPTY_FORM, params: {} });
202
246
  const dialogError = ref(null);
203
247
  const saving = ref(false);
248
+ const dialogStep = ref("account");
249
+ const createdIds = ref([]);
250
+ const azureForm = reactive(emptyAzureForm());
251
+ function emptyAzureForm() {
252
+ return {
253
+ resource: "",
254
+ useOpenAI: true,
255
+ useFoundry: false,
256
+ deployments: [{ deployment: "", model: "" }],
257
+ apiVersion: AZURE_OPENAI_DEFAULT_API_VERSION
258
+ };
259
+ }
204
260
  const presetOptions = computed(() => [
205
261
  ...presets.value.map((preset) => ({ label: preset.label, value: preset.id })),
206
262
  { label: "Custom", value: CUSTOM_PRESET }
207
263
  ]);
264
+ const libraryPresets = computed(
265
+ () => presets.value.filter(
266
+ (preset) => preset.id !== AZURE_OPENAI_PRESET && preset.id !== FOUNDRY_PRESET
267
+ )
268
+ );
208
269
  const surfaceOptions = AI_PROVIDER_SURFACES.map((surface) => ({
209
270
  label: surface,
210
271
  value: surface
211
272
  }));
212
273
  const selectedPreset = computed(() => presets.value.find((preset) => preset.id === form.preset));
213
274
  const isCustom = computed(() => form.preset === CUSTOM_PRESET);
275
+ const isAzureAccount = computed(() => form.preset === AZURE_ACCOUNT);
214
276
  const customKind = computed(() => editing.value?.kind ?? "llm");
215
277
  const showSurface = computed(
216
278
  () => isCustom.value && (customKind.value === "llm" || customKind.value === "embed")
@@ -219,38 +281,135 @@ export function useProvidersPanel(props, emit) {
219
281
  () => selectedPreset.value?.base_url ?? "https://api.example.com/v1"
220
282
  );
221
283
  const canSetValue = computed(() => environmentOn.value);
222
- const showCredentialFields = computed(() => editing.value === null);
284
+ const azureSignInPreset = computed(() => {
285
+ const openai = presets.value.find((p) => p.id === AZURE_OPENAI_PRESET);
286
+ const foundry = presets.value.find((p) => p.id === FOUNDRY_PRESET);
287
+ const chosen = !azureForm.useOpenAI && azureForm.useFoundry ? foundry : openai;
288
+ return chosen ?? openai ?? foundry ?? {
289
+ credential: { kind: "api_key_header", env: "AZURE_OPENAI_API_KEY" },
290
+ transport: "azure-openai"
291
+ };
292
+ });
293
+ const signInOptions = computed(() => {
294
+ const expert = props.expert === true;
295
+ if (isAzureAccount.value) return presetSignInOptions(azureSignInPreset.value, { expert });
296
+ if (!form.preset) return [];
297
+ return presetSignInOptions(isCustom.value ? null : selectedPreset.value ?? null, { expert });
298
+ });
299
+ const signInOption = computed(() => signInOptions.value.find((o) => o.id === form.signIn));
300
+ const storedSignIn = computed(() => {
301
+ const row = editing.value;
302
+ if (!row) return void 0;
303
+ const kind = row.credentialKind ?? selectedPreset.value?.credential.kind;
304
+ return signInOptionOf({ kind, ref: row.credentialRef }, signInOptions.value);
305
+ });
306
+ const showSignIn = computed(() => {
307
+ const row = editing.value;
308
+ if (!row) return signInOptions.value.length > 0;
309
+ const option = storedSignIn.value;
310
+ const kind = row.credentialKind ?? selectedPreset.value?.credential.kind;
311
+ if (!option || kind && option.credentialKind !== kind) return false;
312
+ const ref2 = row.credentialRef;
313
+ if (!ref2) return option.fields !== "reference";
314
+ return option.fields === (envNameOfRef(ref2) ? "key" : "reference");
315
+ });
316
+ const showCredentialFields = computed(
317
+ () => showSignIn.value && signInOption.value?.fields === "key"
318
+ );
319
+ const showReferenceField = computed(
320
+ () => showSignIn.value && signInOption.value?.fields === "reference"
321
+ );
322
+ const defaultEnvName = computed(
323
+ () => isAzureAccount.value ? azureSignInPreset.value.credential.env : selectedPreset.value?.credential.env
324
+ );
325
+ const paramFields = computed(() => {
326
+ if (isCustom.value || isAzureAccount.value) return [];
327
+ const preset = selectedPreset.value;
328
+ const kept = editing.value && editing.value.preset === form.preset ? editing.value : null;
329
+ const required = new Set(preset?.required_params ?? []);
330
+ return presetParamKeys(preset, kept?.transportParams).map((key) => ({
331
+ key,
332
+ label: PROVIDER_PARAM_LABELS[key] ?? key,
333
+ required: required.has(key),
334
+ editable: props.expert === true || !EXPERT_PARAMS.includes(key)
335
+ }));
336
+ });
223
337
  const idError = computed(() => {
224
338
  const id = form.id.trim();
225
339
  return id && !PROVIDER_ID_PATTERN.test(id) ? "Use lower-case letters, digits, '-' and '_', starting with a letter or digit." : null;
226
340
  });
227
- const dialogTitle = computed(
228
- () => editing.value ? `Edit provider \u2014 ${editing.value.id}` : "Add provider"
229
- );
341
+ const dialogTitle = computed(() => {
342
+ if (editing.value) return `Edit provider \u2014 ${editing.value.id}`;
343
+ if (dialogStep.value === "done") return "Provider added";
344
+ if (dialogStep.value === "account" && isAzureAccount.value) return "Add Microsoft Azure";
345
+ return "Add provider";
346
+ });
230
347
  function resetDialog(values, row) {
231
348
  Object.assign(form, EMPTY_FORM, values);
349
+ form.params = { ...values.params ?? {} };
350
+ Object.assign(azureForm, emptyAzureForm());
232
351
  editing.value = row;
233
352
  dialogError.value = null;
353
+ createdIds.value = [];
354
+ dialogStep.value = "account";
234
355
  dialogVisible.value = true;
235
356
  }
357
+ function settleSignIn() {
358
+ if (!signInOptions.value.some((o) => o.id === form.signIn)) {
359
+ form.signIn = signInOptions.value[0]?.id ?? "";
360
+ }
361
+ }
236
362
  function openAdd(presetId) {
237
363
  if (props.readOnly) return;
238
364
  resetDialog({}, null);
239
- if (presetId) selectPreset(presetId);
365
+ if (presetId) chooseLibrary(presetId);
366
+ else dialogStep.value = "library";
367
+ }
368
+ function chooseLibrary(value) {
369
+ if (props.readOnly || editing.value) return;
370
+ dialogError.value = null;
371
+ if (value === AZURE_ACCOUNT) {
372
+ form.preset = AZURE_ACCOUNT;
373
+ form.id = "";
374
+ form.envName = "";
375
+ form.params = {};
376
+ } else {
377
+ if (form.preset === AZURE_ACCOUNT) form.preset = "";
378
+ selectPreset(value);
379
+ }
380
+ settleSignIn();
381
+ dialogStep.value = "account";
382
+ }
383
+ function backToLibrary() {
384
+ if (editing.value) return;
385
+ dialogError.value = null;
386
+ dialogStep.value = "library";
387
+ }
388
+ function selectSignIn(id) {
389
+ if (signInOptions.value.some((o) => o.id === id)) form.signIn = id;
240
390
  }
241
391
  function openEdit(row) {
242
392
  if (!isEditable(row)) return;
243
393
  const preset = presets.value.find((p) => p.id === row.preset);
394
+ const ref2 = row.credentialRef;
395
+ const params = {};
396
+ for (const key of presetParamKeys(preset, row.transportParams)) {
397
+ params[key] = row.transportParams?.[key] ?? defaultParamValue(preset?.id, key);
398
+ }
244
399
  resetDialog(
245
400
  {
246
401
  preset: preset ? preset.id : CUSTOM_PRESET,
247
402
  id: row.id,
248
403
  surface: row.surface ?? "",
249
404
  // The preset's own endpoint shows as the placeholder, not as a pinned value.
250
- baseUrl: row.baseUrl && row.baseUrl !== preset?.base_url ? row.baseUrl : ""
405
+ baseUrl: row.baseUrl && row.baseUrl !== preset?.base_url ? row.baseUrl : "",
406
+ envName: envNameOfRef(ref2) ?? "",
407
+ credentialRef: ref2 && !envNameOfRef(ref2) ? ref2 : "",
408
+ params
251
409
  },
252
410
  row
253
411
  );
412
+ form.signIn = storedSignIn.value?.id ?? signInOptions.value[0]?.id ?? "";
254
413
  }
255
414
  function selectPreset(value) {
256
415
  const previous = selectedPreset.value;
@@ -260,6 +419,41 @@ export function useProvidersPanel(props, emit) {
260
419
  if (form.envName === "" || form.envName === previous?.credential.env)
261
420
  form.envName = next?.credential.env ?? "";
262
421
  if (value === CUSTOM_PRESET && form.surface === "") form.surface = previous?.surface ?? "";
422
+ const params = {};
423
+ for (const key of presetParamKeys(next)) {
424
+ const typed = form.params[key]?.trim();
425
+ params[key] = typed && typed !== defaultParamValue(previous?.id, key) ? typed : defaultParamValue(next?.id, key);
426
+ }
427
+ form.params = params;
428
+ settleSignIn();
429
+ }
430
+ function addDeploymentRow() {
431
+ azureForm.deployments.push({ deployment: "", model: "" });
432
+ }
433
+ function removeDeploymentRow(index) {
434
+ azureForm.deployments.splice(index, 1);
435
+ if (azureForm.deployments.length === 0) addDeploymentRow();
436
+ }
437
+ function dialogCredential() {
438
+ const option = signInOption.value;
439
+ if (!option) return { ok: false, error: "Choose how you sign in." };
440
+ return signInCredential(
441
+ option,
442
+ { envName: form.envName, ref: form.credentialRef },
443
+ { defaultEnv: defaultEnvName.value }
444
+ );
445
+ }
446
+ function keyFieldsRefusal() {
447
+ if (!showCredentialFields.value) return null;
448
+ const envName = form.envName.trim();
449
+ if (envName) {
450
+ const refusal = validateEnvVarName(envName);
451
+ if (refusal) return refusal;
452
+ }
453
+ if (form.secret && !envName && !defaultEnvName.value) {
454
+ return "Name the environment variable the value is stored in.";
455
+ }
456
+ return null;
263
457
  }
264
458
  function formRefusal() {
265
459
  if (!form.preset) return "Choose a preset or Custom.";
@@ -267,13 +461,17 @@ export function useProvidersPanel(props, emit) {
267
461
  if (idError.value) return idError.value;
268
462
  if (showSurface.value && !form.surface) return "Choose the surface a custom provider speaks.";
269
463
  if (isCustom.value && !form.baseUrl.trim()) return "A custom provider needs a base URL.";
270
- if (showCredentialFields.value) {
271
- const envName = form.envName.trim();
272
- if (envName) {
273
- const refusal = validateEnvVarName(envName);
274
- if (refusal) return refusal;
464
+ if (!isCustom.value && selectedPreset.value) {
465
+ const missing = missingRequiredParams(selectedPreset.value, form.params)[0];
466
+ if (missing) {
467
+ return `Enter the ${(PROVIDER_PARAM_LABELS[missing] ?? missing).toLowerCase()} (${missing}).`;
275
468
  }
276
- if (form.secret && !envName) return "Name the environment variable the value is stored in.";
469
+ }
470
+ if (showSignIn.value) {
471
+ const keyRefusal = keyFieldsRefusal();
472
+ if (keyRefusal) return keyRefusal;
473
+ const credential = dialogCredential();
474
+ if (!credential.ok) return credential.error;
277
475
  }
278
476
  return null;
279
477
  }
@@ -291,41 +489,120 @@ export function useProvidersPanel(props, emit) {
291
489
  }
292
490
  const baseUrl = form.baseUrl.trim();
293
491
  if (baseUrl) body.base_url = baseUrl;
294
- const credentialKind = kept?.credentialKind ?? (isCustom.value ? "bearer" : preset?.credential.kind ?? "bearer");
295
- const envName = row ? "" : form.envName.trim();
296
- if (envName) body.credential = { kind: credentialKind, ref: `env:${envName}` };
297
- else if (row?.credentialRef) body.credential = { kind: credentialKind, ref: row.credentialRef };
492
+ const fields = paramFields.value;
493
+ if (fields.length > 0) {
494
+ const params = kept && isPlainObject(body.params) ? { ...body.params } : {};
495
+ for (const { key } of fields) {
496
+ const value = form.params[key]?.trim();
497
+ if (value) params[key] = value;
498
+ else delete params[key];
499
+ }
500
+ body.params = params;
501
+ } else if (!kept) {
502
+ delete body.params;
503
+ }
504
+ if (showSignIn.value) {
505
+ const credential = dialogCredential();
506
+ if (credential.ok && credential.credential) body.credential = credential.credential;
507
+ } else if (row?.credentialRef) {
508
+ const credentialKind = kept?.credentialKind ?? (isCustom.value ? "bearer" : preset?.credential.kind ?? "bearer");
509
+ body.credential = { kind: credentialKind, ref: row.credentialRef };
510
+ } else if (kept?.credentialKind) {
511
+ body.credential = { kind: kept.credentialKind };
512
+ }
298
513
  return body;
299
514
  }
515
+ async function writeDialogSecret() {
516
+ const name = form.envName.trim() || defaultEnvName.value || "";
517
+ if (!showCredentialFields.value || !form.secret || !environmentOn.value || !name) return;
518
+ const update = { set: { [name]: form.secret }, unset: [] };
519
+ await client().updateEnvironment(update);
520
+ form.secret = "";
521
+ }
522
+ async function finishAdd(ids) {
523
+ createdIds.value = ids;
524
+ await afterWrite();
525
+ if (!canTest.value) {
526
+ dialogVisible.value = false;
527
+ return;
528
+ }
529
+ dialogStep.value = "done";
530
+ for (const id of ids) await testProvider({ id });
531
+ }
532
+ async function saveAzureAccount() {
533
+ const keyRefusal = keyFieldsRefusal();
534
+ const credential = dialogCredential();
535
+ const drafts = keyRefusal ? { ok: false, error: keyRefusal } : !credential.ok ? credential : azureAccountEntries({
536
+ resource: azureForm.resource,
537
+ credential: credential.credential,
538
+ openai: azureForm.useOpenAI ? { deployments: azureForm.deployments, apiVersion: azureForm.apiVersion } : void 0,
539
+ foundry: azureForm.useFoundry ? { deployments: [] } : void 0,
540
+ takenIds: providers.value.map((entry) => entry.id)
541
+ });
542
+ if (!drafts.ok) {
543
+ dialogError.value = drafts.error;
544
+ return;
545
+ }
546
+ saving.value = true;
547
+ dialogError.value = null;
548
+ const created = [];
549
+ try {
550
+ await writeDialogSecret();
551
+ for (const draft of drafts.entries) {
552
+ await client().upsert(draft.id, draft.body);
553
+ created.push(draft.id);
554
+ }
555
+ await finishAdd(created);
556
+ } catch (err) {
557
+ const message = requestErrorMessage(err, "Could not add the Azure account.");
558
+ dialogError.value = created.length > 0 ? `${message} (created: ${created.join(", ")})` : message;
559
+ if (created.length > 0) await afterWrite();
560
+ } finally {
561
+ saving.value = false;
562
+ }
563
+ }
300
564
  async function saveProvider() {
301
565
  if (props.readOnly) return;
566
+ if (isAzureAccount.value && !editing.value) {
567
+ await saveAzureAccount();
568
+ return;
569
+ }
302
570
  const refusal = formRefusal();
303
571
  if (refusal) {
304
572
  dialogError.value = refusal;
305
573
  return;
306
574
  }
307
575
  const id = form.id.trim();
576
+ const adding = editing.value === null;
308
577
  saving.value = true;
309
578
  dialogError.value = null;
310
579
  try {
311
- if (showCredentialFields.value && form.secret && environmentOn.value) {
312
- const update = {
313
- set: { [form.envName.trim()]: form.secret },
314
- unset: []
315
- };
316
- await client().updateEnvironment(update);
317
- form.secret = "";
318
- }
580
+ await writeDialogSecret();
319
581
  await client().upsert(id, buildBody());
320
- dialogVisible.value = false;
321
582
  editing.value = null;
322
- await afterWrite();
583
+ if (adding) {
584
+ await finishAdd([id]);
585
+ } else {
586
+ dialogVisible.value = false;
587
+ await afterWrite();
588
+ }
323
589
  } catch (err) {
324
590
  dialogError.value = requestErrorMessage(err, "Could not save the provider.");
325
591
  } finally {
326
592
  saving.value = false;
327
593
  }
328
594
  }
595
+ function finishDialog() {
596
+ dialogVisible.value = false;
597
+ createdIds.value = [];
598
+ }
599
+ function openSignIn(row) {
600
+ if (!row.canRenew) return;
601
+ if (isEditable(row)) openEdit(row);
602
+ else if (canEnterKey.value && (!row.credentialRef || envNameOfRef(row.credentialRef))) {
603
+ openKeyEditor(row);
604
+ } else openRefEditor(row);
605
+ }
329
606
  const deleteTarget = ref(null);
330
607
  const deleteError = ref(null);
331
608
  const deleting = ref(false);
@@ -448,6 +725,84 @@ export function useProvidersPanel(props, emit) {
448
725
  `Could not reset ${row.id}.`
449
726
  );
450
727
  }
728
+ async function setEuProcessing(row, on) {
729
+ if (!row.canSetEuProcessing || row.euProcessing === on) return;
730
+ await runRowWrite(
731
+ row.id,
732
+ () => client().upsert(row.id, { ...ownBody(row), eu_processing: on }),
733
+ `Could not change EU processing of ${row.id}.`
734
+ );
735
+ }
736
+ const modelsEditing = ref(null);
737
+ const modelsForm = reactive({ mode: "all", selected: [], query: "" });
738
+ const modelsKnown = ref([]);
739
+ const modelsLoading = ref(false);
740
+ const modelsListMessage = ref(null);
741
+ const modelsError = ref(null);
742
+ const modelChoices = computed(
743
+ () => providerModelChoices(modelsKnown.value, modelsForm.selected, modelsForm.query)
744
+ );
745
+ async function openModelsEditor(row) {
746
+ if (!row.canFilterModels) return;
747
+ modelsEditing.value = row.id;
748
+ modelsForm.mode = row.allowedModels === void 0 ? "all" : "only";
749
+ modelsForm.selected = [...row.allowedModels ?? []];
750
+ modelsForm.query = "";
751
+ modelsError.value = null;
752
+ modelsListMessage.value = null;
753
+ if (row.modelIds && row.modelIds.length > 0) {
754
+ modelsKnown.value = [...row.modelIds];
755
+ return;
756
+ }
757
+ modelsKnown.value = [];
758
+ modelsLoading.value = true;
759
+ try {
760
+ const listed = await client().models(row.id);
761
+ if (modelsEditing.value !== row.id) return;
762
+ modelsKnown.value = listed.models.map((model) => model.id);
763
+ if (listed.models.length === 0) {
764
+ modelsListMessage.value = listed.message ?? "This provider lists no models.";
765
+ }
766
+ } catch (err) {
767
+ if (modelsEditing.value === row.id) {
768
+ modelsListMessage.value = requestErrorMessage(
769
+ err,
770
+ `Could not list the models of ${row.id}.`
771
+ );
772
+ }
773
+ } finally {
774
+ modelsLoading.value = false;
775
+ }
776
+ }
777
+ function setModelsMode(mode) {
778
+ modelsForm.mode = mode;
779
+ }
780
+ function toggleModel(id, on) {
781
+ const has = modelsForm.selected.includes(id);
782
+ if (on && !has) modelsForm.selected = [...modelsForm.selected, id];
783
+ if (!on && has) modelsForm.selected = modelsForm.selected.filter((model) => model !== id);
784
+ }
785
+ function cancelModelsEditor() {
786
+ modelsEditing.value = null;
787
+ modelsError.value = null;
788
+ }
789
+ async function saveModelsEditor() {
790
+ const row = rows.value.find((r) => r.id === modelsEditing.value);
791
+ if (!row?.canFilterModels) return;
792
+ const order = providerModelChoices(modelsKnown.value, modelsForm.selected);
793
+ const allowed = modelsForm.mode === "all" ? null : order.filter((id) => modelsForm.selected.includes(id));
794
+ rowBusy.value = row.id;
795
+ modelsError.value = null;
796
+ try {
797
+ await client().upsert(row.id, { ...ownBody(row), allowed_models: allowed });
798
+ modelsEditing.value = null;
799
+ await afterWrite();
800
+ } catch (err) {
801
+ modelsError.value = requestErrorMessage(err, `Could not save the models of ${row.id}.`);
802
+ } finally {
803
+ rowBusy.value = null;
804
+ }
805
+ }
451
806
  const canTest = computed(() => Boolean(props.client) && !props.readOnly);
452
807
  const testBusy = ref(null);
453
808
  const testResults = reactive({});
@@ -608,6 +963,25 @@ export function useProvidersPanel(props, emit) {
608
963
  openEdit,
609
964
  selectPreset,
610
965
  saveProvider,
966
+ // add flow
967
+ dialogStep,
968
+ libraryPresets,
969
+ chooseLibrary,
970
+ backToLibrary,
971
+ isAzureAccount,
972
+ signInOptions,
973
+ signInOption,
974
+ selectSignIn,
975
+ showSignIn,
976
+ showReferenceField,
977
+ paramFields,
978
+ azureForm,
979
+ addDeploymentRow,
980
+ removeDeploymentRow,
981
+ createdIds,
982
+ finishDialog,
983
+ defaultEnvName,
984
+ openSignIn,
611
985
  // delete
612
986
  deleteTarget,
613
987
  deleteOpen,
@@ -625,6 +999,7 @@ export function useProvidersPanel(props, emit) {
625
999
  setCredentialEnv,
626
1000
  setEnabled,
627
1001
  resetPatch,
1002
+ setEuProcessing,
628
1003
  rowBusy,
629
1004
  rowError,
630
1005
  canTest,
@@ -646,6 +1021,19 @@ export function useProvidersPanel(props, emit) {
646
1021
  openRefEditor,
647
1022
  cancelRefEditor,
648
1023
  saveRefEditor,
1024
+ // models allowed here
1025
+ modelsEditing,
1026
+ modelsForm,
1027
+ modelsKnown,
1028
+ modelsLoading,
1029
+ modelsListMessage,
1030
+ modelChoices,
1031
+ modelsError,
1032
+ openModelsEditor,
1033
+ setModelsMode,
1034
+ toggleModel,
1035
+ cancelModelsEditor,
1036
+ saveModelsEditor,
649
1037
  // environment
650
1038
  envDocument,
651
1039
  envEditable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaile/forge-settings",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Provider and settings operations for Skaile forge apps: provider registry layers, credential store, env layers, model discovery, the providers panel, and editor / AI SDK model adapters.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.9",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@nuxt/kit": "^4.0.0",
58
- "@skaile/workspaces": "^9.0.0",
58
+ "@skaile/workspaces": "^9.1.0",
59
59
  "ofetch": "^1.4.0",
60
60
  "yaml": "^2.9.0"
61
61
  },