@skaile/forge-settings 1.3.3 → 2.0.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 (88) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +52 -9
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/ai-sdk/index.d.ts +33 -26
  5. package/dist/runtime/ai-sdk/index.js +34 -16
  6. package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +13 -0
  7. package/dist/runtime/components/ForgeProvidersPanel.vue +461 -78
  8. package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +13 -0
  9. package/dist/runtime/editor/index.d.ts +55 -16
  10. package/dist/runtime/editor/index.js +82 -40
  11. package/dist/runtime/model/selector.d.ts +16 -0
  12. package/dist/runtime/model/selector.js +12 -0
  13. package/dist/runtime/server/credential-store.d.ts +16 -4
  14. package/dist/runtime/server/credential-store.js +3 -4
  15. package/dist/runtime/server/env-layers.d.ts +1 -1
  16. package/dist/runtime/server/env-layers.js +3 -3
  17. package/dist/runtime/server/index.d.ts +9 -8
  18. package/dist/runtime/server/index.js +3 -3
  19. package/dist/runtime/server/provider-discovery.d.ts +28 -8
  20. package/dist/runtime/server/provider-discovery.js +57 -33
  21. package/dist/runtime/server/provider-layers.d.ts +1 -1
  22. package/dist/runtime/server/provider-layers.js +3 -5
  23. package/dist/runtime/server/provider-service.d.ts +26 -2
  24. package/dist/runtime/server/provider-service.js +16 -1
  25. package/dist/runtime/server/provider-write.d.ts +26 -10
  26. package/dist/runtime/server/provider-write.js +75 -7
  27. package/dist/runtime/server/voice-provider.d.ts +61 -0
  28. package/dist/runtime/server/voice-provider.js +31 -0
  29. package/dist/runtime/shared/data-zone.d.ts +68 -0
  30. package/dist/runtime/shared/data-zone.js +79 -0
  31. package/dist/runtime/shared/harness-providers.d.ts +42 -3
  32. package/dist/runtime/shared/harness-providers.js +31 -8
  33. package/dist/runtime/shared/harnesses.d.ts +7 -0
  34. package/dist/runtime/shared/harnesses.js +18 -0
  35. package/dist/runtime/shared/provider-accounts.d.ts +215 -0
  36. package/dist/runtime/shared/provider-accounts.js +255 -0
  37. package/dist/runtime/shared/provider-registry.d.ts +19 -182
  38. package/dist/runtime/shared/provider-registry.js +25 -65
  39. package/dist/runtime/vue/index.d.ts +2 -0
  40. package/dist/runtime/vue/index.js +2 -0
  41. package/dist/runtime/vue/providers-panel-model.d.ts +98 -5
  42. package/dist/runtime/vue/providers-panel-model.js +13 -1
  43. package/dist/runtime/vue/useProvidersPanel.d.ts +66 -5
  44. package/dist/runtime/vue/useProvidersPanel.js +417 -29
  45. package/dist/runtime/{server → workspaces}/harness-settings.d.ts +51 -13
  46. package/dist/runtime/{server → workspaces}/harness-settings.js +93 -9
  47. package/dist/runtime/workspaces/index.d.ts +16 -0
  48. package/dist/runtime/workspaces/index.js +5 -0
  49. package/dist/runtime/workspaces/language-model.d.ts +45 -0
  50. package/dist/runtime/workspaces/language-model.js +26 -0
  51. package/dist/runtime/workspaces/model-endpoint.d.ts +38 -0
  52. package/dist/runtime/workspaces/model-endpoint.js +54 -0
  53. package/dist/runtime/workspaces/provider-settings.d.ts +35 -0
  54. package/dist/runtime/{server → workspaces}/provider-settings.js +0 -31
  55. package/dist/runtime/{server → workspaces}/settings-defaults.d.ts +1 -1
  56. package/dist/runtime/{shared → workspaces}/settings-types.d.ts +11 -4
  57. package/package.json +11 -2
  58. package/src/runtime/ai-sdk/index.ts +88 -47
  59. package/src/runtime/components/ForgeProvidersPanel.vue +477 -78
  60. package/src/runtime/editor/index.ts +158 -63
  61. package/src/runtime/model/selector.ts +31 -0
  62. package/src/runtime/server/credential-store.ts +20 -10
  63. package/src/runtime/server/env-layers.ts +4 -4
  64. package/src/runtime/server/index.ts +9 -8
  65. package/src/runtime/server/provider-discovery.ts +114 -48
  66. package/src/runtime/server/provider-layers.ts +6 -6
  67. package/src/runtime/server/provider-service.ts +53 -1
  68. package/src/runtime/server/provider-write.ts +132 -18
  69. package/src/runtime/server/voice-provider.ts +110 -0
  70. package/src/runtime/shared/data-zone.ts +179 -0
  71. package/src/runtime/shared/harness-providers.ts +78 -7
  72. package/src/runtime/shared/harnesses.ts +25 -0
  73. package/src/runtime/shared/provider-accounts.ts +526 -0
  74. package/src/runtime/shared/provider-registry.ts +82 -256
  75. package/src/runtime/vue/index.ts +2 -0
  76. package/src/runtime/vue/providers-panel-model.ts +123 -5
  77. package/src/runtime/vue/useProvidersPanel.ts +589 -37
  78. package/src/runtime/{server → workspaces}/harness-settings.ts +166 -22
  79. package/src/runtime/workspaces/index.ts +17 -0
  80. package/src/runtime/workspaces/language-model.ts +82 -0
  81. package/src/runtime/workspaces/model-endpoint.ts +100 -0
  82. package/src/runtime/workspaces/provider-settings.ts +96 -0
  83. package/src/runtime/{server → workspaces}/settings-defaults.ts +1 -1
  84. package/src/runtime/{shared → workspaces}/settings-types.ts +11 -4
  85. package/dist/runtime/server/provider-settings.d.ts +0 -78
  86. package/src/runtime/server/provider-settings.ts +0 -192
  87. /package/dist/runtime/{server → workspaces}/settings-defaults.js +0 -0
  88. /package/dist/runtime/{shared → workspaces}/settings-types.js +0 -0
@@ -71,10 +71,54 @@
71
71
  variant="subtle"
72
72
  color="neutral"
73
73
  >{{ row.badge }}</UBadge>
74
+ <UBadge
75
+ :data-testid="`provider-zone-${row.id}`"
76
+ size="sm"
77
+ variant="outline"
78
+ :color="row.zone === 'transfer' ? 'warning' : 'neutral'"
79
+ :title="`Data zone: ${row.zoneLabel}`"
80
+ >{{ row.zoneLabel }}</UBadge>
81
+ <button
82
+ v-if="row.modelsChip && row.canFilterModels"
83
+ type="button"
84
+ class="rounded-md border border-surface-border px-1.5 py-0.5 text-[10px] text-foreground-muted hover:border-primary-500"
85
+ :data-testid="`provider-models-chip-${row.id}`"
86
+ @click="openModelsEditor(row)"
87
+ >{{ row.modelsChip }}</button>
88
+ <UBadge
89
+ v-else-if="row.modelsChip"
90
+ :data-testid="`provider-models-chip-${row.id}`"
91
+ size="sm"
92
+ variant="subtle"
93
+ color="neutral"
94
+ >{{ row.modelsChip }}</UBadge>
74
95
  <Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
75
96
  </div>
76
97
  <p class="font-mono text-foreground-subtle truncate" :title="row.baseUrl ?? undefined">{{ rowMeta(row) }}</p>
98
+ <p v-if="row.accountLine" :data-testid="`provider-account-${row.id}`" class="text-foreground-muted">{{ row.accountLine }}</p>
77
99
  <p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
100
+ <p
101
+ v-if="row.blockedNote"
102
+ :data-testid="`provider-blocked-${row.id}`"
103
+ class="text-foreground-subtle italic"
104
+ >{{ row.blockedNote }}</p>
105
+ <p
106
+ v-if="row.expiry"
107
+ :data-testid="`provider-expiry-${row.id}`"
108
+ class="flex items-center gap-1"
109
+ :class="row.expiry.expired || row.expiry.days < 7 ? 'text-amber-600 dark:text-amber-400' : 'text-foreground-subtle'"
110
+ >
111
+ {{ row.expiry.text }}
112
+ <template v-if="row.canRenew">
113
+ ·
114
+ <button
115
+ type="button"
116
+ class="underline hover:text-foreground"
117
+ :data-testid="`provider-renew-${row.id}`"
118
+ @click="openSignIn(row)"
119
+ >Renew</button>
120
+ </template>
121
+ </p>
78
122
  </div>
79
123
  <div class="flex items-center gap-1 shrink-0">
80
124
  <UButton
@@ -145,6 +189,16 @@
145
189
  {{ row.status }}
146
190
  </p>
147
191
 
192
+ <USwitch
193
+ v-if="row.zone === 'eu_option'"
194
+ :model-value="row.euProcessing === true"
195
+ :disabled="!row.canSetEuProcessing || rowBusy === row.id"
196
+ label="EU processing is on for this account"
197
+ size="xs"
198
+ :data-testid="`provider-eu-processing-${row.id}`"
199
+ @update:model-value="setEuProcessing(row, Boolean($event))"
200
+ />
201
+
148
202
  <!-- Inline key editor -->
149
203
  <div
150
204
  v-if="keyEditing === row.id"
@@ -203,6 +257,67 @@
203
257
  </div>
204
258
  </div>
205
259
 
260
+ <!-- Models allowed here -->
261
+ <div
262
+ v-if="modelsEditing === row.id"
263
+ :data-testid="`provider-models-${row.id}`"
264
+ class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
265
+ >
266
+ <p class="font-medium text-foreground">Models allowed here</p>
267
+ <URadioGroup
268
+ :model-value="modelsForm.mode"
269
+ :items="MODELS_MODE_ITEMS"
270
+ orientation="horizontal"
271
+ size="xs"
272
+ :data-testid="`provider-models-mode-${row.id}`"
273
+ @update:model-value="setModelsMode($event as ProvidersPanelModelsMode)"
274
+ />
275
+ <template v-if="modelsForm.mode === 'only'">
276
+ <UInput
277
+ v-model="modelsForm.query"
278
+ placeholder="Search models"
279
+ size="xs"
280
+ :data-testid="`provider-models-search-${row.id}`"
281
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs"
282
+ />
283
+ <p v-if="modelsLoading" class="text-foreground-subtle italic">Listing models…</p>
284
+ <p
285
+ v-if="modelsListMessage"
286
+ :data-testid="`provider-models-message-${row.id}`"
287
+ class="text-foreground-subtle italic"
288
+ >{{ modelsListMessage }}</p>
289
+ <div class="max-h-48 overflow-y-auto space-y-1">
290
+ <UCheckbox
291
+ v-for="modelId in modelChoices"
292
+ :key="modelId"
293
+ :model-value="modelsForm.selected.includes(modelId)"
294
+ :label="modelId"
295
+ size="xs"
296
+ :data-testid="`provider-model-${row.id}-${modelId}`"
297
+ @update:model-value="toggleModel(modelId, Boolean($event))"
298
+ />
299
+ </div>
300
+ <p class="text-foreground-subtle">
301
+ {{ modelsForm.selected.length === 0 ? "No model ticked: this provider will run no model." : `${modelsForm.selected.length} ticked.` }}
302
+ </p>
303
+ </template>
304
+ <p
305
+ v-if="modelsError"
306
+ :data-testid="`provider-models-error-${row.id}`"
307
+ class="text-error-500"
308
+ >{{ modelsError }}</p>
309
+ <div class="flex justify-end gap-1">
310
+ <UButton size="xs" variant="ghost" :data-testid="`provider-models-cancel-${row.id}`" @click="cancelModelsEditor()">Cancel</UButton>
311
+ <UButton
312
+ size="xs"
313
+ color="primary"
314
+ :loading="rowBusy === row.id"
315
+ :data-testid="`provider-models-save-${row.id}`"
316
+ @click="saveModelsEditor()"
317
+ >Save</UButton>
318
+ </div>
319
+ </div>
320
+
206
321
  <p
207
322
  v-for="notice in row.overrideNotices"
208
323
  :key="notice"
@@ -228,7 +343,7 @@
228
343
  :open="dialogVisible"
229
344
  :title="dialogTitle"
230
345
  :ui="{
231
- content: 'bg-surface-card border border-surface-border sm:max-w-[460px]',
346
+ content: 'bg-surface-card border border-surface-border sm:max-w-[520px]',
232
347
  header: 'bg-surface-card border-b border-surface-border text-foreground',
233
348
  body: 'bg-surface-card p-5',
234
349
  footer: 'bg-surface-ground/50 border-t border-surface-border p-3 justify-end',
@@ -237,104 +352,323 @@
237
352
  >
238
353
  <template #body>
239
354
  <div data-testid="provider-dialog" class="space-y-4">
240
- <div class="flex flex-col gap-1">
241
- <label class="text-xs text-foreground-muted font-medium">
242
- Preset <span class="text-error-500">*</span>
243
- </label>
244
- <USelect
245
- :model-value="form.preset"
246
- :items="presetOptions"
247
- placeholder="Choose a preset"
248
- data-testid="provider-preset"
249
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm"
250
- @update:model-value="selectPreset(String($event ?? ''))"
251
- />
355
+ <!-- Step 1: library -->
356
+ <div v-if="dialogStep === 'library'" data-testid="provider-library" class="space-y-3">
357
+ <p class="text-xs text-foreground-muted">Which provider do you want to add?</p>
358
+ <button
359
+ type="button"
360
+ data-testid="provider-library-azure"
361
+ class="w-full text-left p-3 rounded-lg border border-surface-border hover:border-primary-500 hover:bg-primary-500/5 transition-colors"
362
+ @click="chooseLibrary(AZURE_ACCOUNT)"
363
+ >
364
+ <span class="flex items-center gap-2">
365
+ <Icon name="lucide:cloud" class="text-primary-500 shrink-0" />
366
+ <span class="text-sm font-medium text-foreground">Microsoft Azure</span>
367
+ </span>
368
+ <span class="block text-xs text-foreground-muted mt-1">
369
+ Azure OpenAI and Claude on Foundry from one Azure resource, with Entra ID or an API key.
370
+ </span>
371
+ </button>
372
+ <div class="grid grid-cols-2 gap-2 max-h-72 overflow-y-auto">
373
+ <button
374
+ v-for="preset in libraryPresets"
375
+ :key="preset.id"
376
+ type="button"
377
+ :data-testid="`provider-library-${preset.id}`"
378
+ class="text-left p-2 rounded-lg border border-surface-border hover:border-primary-500 hover:bg-primary-500/5 transition-colors"
379
+ @click="chooseLibrary(preset.id)"
380
+ >
381
+ <span class="block text-xs font-medium text-foreground truncate">{{ preset.label }}</span>
382
+ <span class="block text-[10px] text-foreground-subtle">{{ libraryHint(preset) }}</span>
383
+ </button>
384
+ <button
385
+ type="button"
386
+ data-testid="provider-library-custom"
387
+ class="text-left p-2 rounded-lg border border-dashed border-surface-border hover:border-primary-500 transition-colors"
388
+ @click="chooseLibrary(CUSTOM_PRESET)"
389
+ >
390
+ <span class="block text-xs font-medium text-foreground">Custom</span>
391
+ <span class="block text-[10px] text-foreground-subtle">Any endpoint, by base URL</span>
392
+ </button>
393
+ </div>
252
394
  </div>
253
395
 
254
- <div v-if="showSurface" class="flex flex-col gap-1">
255
- <label class="text-xs text-foreground-muted font-medium">
256
- Surface <span class="text-error-500">*</span>
257
- </label>
258
- <USelect
259
- v-model="form.surface"
260
- :items="surfaceOptions"
261
- placeholder="Wire protocol"
262
- data-testid="provider-surface"
263
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm"
264
- />
265
- </div>
396
+ <!-- Step 2: account -->
397
+ <template v-else-if="dialogStep === 'account'">
398
+ <!-- Microsoft Azure account -->
399
+ <template v-if="isAzureAccount">
400
+ <div class="flex flex-col gap-1">
401
+ <label class="text-xs text-foreground-muted font-medium">
402
+ Resource name <span class="text-error-500">*</span>
403
+ </label>
404
+ <UInput
405
+ v-model="azureForm.resource"
406
+ placeholder="contoso-ai"
407
+ data-testid="provider-azure-resource"
408
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
409
+ />
410
+ <p class="text-[10px] text-foreground-subtle">
411
+ The <code class="font-mono">{{ azureForm.resource.trim() || 'name' }}</code> of
412
+ <code class="font-mono">{{ azureForm.resource.trim() || 'name' }}.openai.azure.com</code>; shared by every entry this account creates.
413
+ </p>
414
+ </div>
266
415
 
267
- <div class="flex flex-col gap-1">
268
- <label class="text-xs text-foreground-muted font-medium">
269
- ID <span class="text-error-500">*</span>
270
- </label>
271
- <UInput
272
- v-model="form.id"
273
- :disabled="editing !== null"
274
- placeholder="openrouter"
275
- data-testid="provider-id"
276
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
277
- :class="editing !== null ? 'opacity-60 cursor-not-allowed' : ''"
278
- />
279
- <p v-if="idError" class="text-xs text-error-500">{{ idError }}</p>
280
- <p v-else class="text-[10px] text-foreground-subtle">The left half of a model selector: <code class="font-mono">&lt;id&gt;/&lt;model&gt;</code>.</p>
281
- </div>
416
+ <div class="space-y-2">
417
+ <p class="text-xs text-foreground-muted font-medium">What do you want to use?</p>
418
+ <UCheckbox
419
+ v-model="azureForm.useOpenAI"
420
+ label="Azure OpenAI"
421
+ description="GPT models you deployed on the resource."
422
+ data-testid="provider-azure-openai"
423
+ />
424
+ <div v-if="azureForm.useOpenAI" class="ml-6 space-y-1.5" data-testid="provider-azure-deployments">
425
+ <p class="text-[10px] text-foreground-subtle">Deployment name → model id the pickers show (blank: the deployment name).</p>
426
+ <div
427
+ v-for="(row, index) in azureForm.deployments"
428
+ :key="index"
429
+ class="flex items-center gap-1.5"
430
+ >
431
+ <UInput
432
+ v-model="row.deployment"
433
+ placeholder="gpt-5-eu"
434
+ :aria-label="`Deployment ${index + 1}`"
435
+ :data-testid="`provider-azure-deployment-${index}`"
436
+ class="flex-1 bg-surface-ground border-surface-border text-foreground text-xs font-mono"
437
+ />
438
+ <Icon name="lucide:arrow-right" class="text-foreground-subtle shrink-0" />
439
+ <UInput
440
+ v-model="row.model"
441
+ placeholder="gpt-5"
442
+ :aria-label="`Model id ${index + 1}`"
443
+ :data-testid="`provider-azure-model-${index}`"
444
+ class="flex-1 bg-surface-ground border-surface-border text-foreground text-xs font-mono"
445
+ />
446
+ <UButton
447
+ size="xs"
448
+ variant="ghost"
449
+ :aria-label="`Remove deployment ${index + 1}`"
450
+ :data-testid="`provider-azure-deployment-remove-${index}`"
451
+ @click="removeDeploymentRow(index)"
452
+ >
453
+ <Icon name="lucide:x" class="w-3.5 h-3.5" />
454
+ </UButton>
455
+ </div>
456
+ <UButton size="xs" variant="ghost" data-testid="provider-azure-deployment-add" @click="addDeploymentRow()">
457
+ <Icon name="lucide:plus" class="mr-1.5" />Add deployment
458
+ </UButton>
459
+ <div class="flex flex-col gap-1">
460
+ <label class="text-xs text-foreground-muted font-medium">API version</label>
461
+ <UInput
462
+ v-model="azureForm.apiVersion"
463
+ :disabled="!props.expert"
464
+ data-testid="provider-azure-api-version"
465
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
466
+ />
467
+ <p v-if="!props.expert" class="text-[10px] text-foreground-subtle">Editable in expert mode.</p>
468
+ </div>
469
+ </div>
470
+ <UCheckbox
471
+ v-model="azureForm.useFoundry"
472
+ label="Claude on Foundry"
473
+ description="Anthropic models deployed through Azure AI Foundry on the same resource."
474
+ data-testid="provider-azure-foundry"
475
+ />
476
+ </div>
477
+ </template>
282
478
 
283
- <div class="flex flex-col gap-1">
284
- <label class="text-xs text-foreground-muted font-medium">
285
- Base URL <span v-if="isCustom" class="text-error-500">*</span>
286
- </label>
287
- <UInput
288
- v-model="form.baseUrl"
289
- :placeholder="baseUrlPlaceholder"
290
- data-testid="provider-base-url"
291
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
292
- />
293
- </div>
479
+ <template v-else>
480
+ <div class="flex flex-col gap-1">
481
+ <label class="text-xs text-foreground-muted font-medium">
482
+ Preset <span class="text-error-500">*</span>
483
+ </label>
484
+ <USelect
485
+ :model-value="form.preset"
486
+ :items="presetOptions"
487
+ placeholder="Choose a preset"
488
+ data-testid="provider-preset"
489
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm"
490
+ @update:model-value="selectPreset(String($event ?? ''))"
491
+ />
492
+ </div>
294
493
 
295
- <template v-if="showCredentialFields">
296
- <div class="flex flex-col gap-1">
297
- <label class="text-xs text-foreground-muted font-medium">Credential environment variable</label>
298
- <UInput
299
- v-model="form.envName"
300
- placeholder="PROVIDER_API_KEY"
301
- data-testid="provider-credential-env"
302
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
303
- />
304
- <p class="text-[10px] text-foreground-subtle">
305
- Stored as <code class="font-mono">env:{{ form.envName.trim() || 'NAME' }}</code> — the key itself stays in the environment.
306
- </p>
494
+ <div v-if="showSurface" class="flex flex-col gap-1">
495
+ <label class="text-xs text-foreground-muted font-medium">
496
+ Surface <span class="text-error-500">*</span>
497
+ </label>
498
+ <USelect
499
+ v-model="form.surface"
500
+ :items="surfaceOptions"
501
+ placeholder="Wire protocol"
502
+ data-testid="provider-surface"
503
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm"
504
+ />
505
+ </div>
506
+
507
+ <div class="flex flex-col gap-1">
508
+ <label class="text-xs text-foreground-muted font-medium">
509
+ ID <span class="text-error-500">*</span>
510
+ </label>
511
+ <UInput
512
+ v-model="form.id"
513
+ :disabled="editing !== null"
514
+ placeholder="openrouter"
515
+ data-testid="provider-id"
516
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
517
+ :class="editing !== null ? 'opacity-60 cursor-not-allowed' : ''"
518
+ />
519
+ <p v-if="idError" class="text-xs text-error-500">{{ idError }}</p>
520
+ <p v-else class="text-[10px] text-foreground-subtle">The left half of a model selector: <code class="font-mono">&lt;id&gt;/&lt;model&gt;</code>.</p>
521
+ </div>
522
+
523
+ <div class="flex flex-col gap-1">
524
+ <label class="text-xs text-foreground-muted font-medium">
525
+ Base URL <span v-if="isCustom" class="text-error-500">*</span>
526
+ </label>
527
+ <UInput
528
+ v-model="form.baseUrl"
529
+ :placeholder="baseUrlPlaceholder"
530
+ data-testid="provider-base-url"
531
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
532
+ />
533
+ </div>
534
+
535
+ <div v-for="field in paramFields" :key="field.key" class="flex flex-col gap-1">
536
+ <label class="text-xs text-foreground-muted font-medium">
537
+ {{ field.label }} <span v-if="field.required" class="text-error-500">*</span>
538
+ </label>
539
+ <UInput
540
+ v-model="form.params[field.key]"
541
+ :disabled="!field.editable"
542
+ :data-testid="`provider-param-${field.key}`"
543
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
544
+ />
545
+ <p v-if="!field.editable" class="text-[10px] text-foreground-subtle">Editable in expert mode.</p>
546
+ </div>
547
+ </template>
548
+
549
+ <!-- How do you sign in? -->
550
+ <div v-if="showSignIn" data-testid="provider-signin" class="space-y-1.5">
551
+ <p class="text-xs text-foreground-muted font-medium">How do you sign in?</p>
552
+ <div class="flex flex-wrap gap-1.5" role="radiogroup" aria-label="How do you sign in?">
553
+ <UButton
554
+ v-for="option in signInOptions"
555
+ :key="option.id"
556
+ size="xs"
557
+ role="radio"
558
+ :aria-checked="form.signIn === option.id"
559
+ :variant="form.signIn === option.id ? 'solid' : 'outline'"
560
+ :color="form.signIn === option.id ? 'primary' : 'neutral'"
561
+ :data-testid="`provider-signin-${option.id}`"
562
+ @click="selectSignIn(option.id)"
563
+ >{{ option.label }}</UButton>
564
+ </div>
565
+ <p v-if="signInOption" class="text-[10px] text-foreground-subtle leading-relaxed">{{ signInOption.description }}</p>
307
566
  </div>
308
567
 
309
- <div v-if="canSetValue" class="flex flex-col gap-1">
310
- <label class="text-xs text-foreground-muted font-medium">Set value now</label>
568
+ <template v-if="showCredentialFields">
569
+ <div class="flex flex-col gap-1">
570
+ <label class="text-xs text-foreground-muted font-medium">Credential environment variable</label>
571
+ <UInput
572
+ v-model="form.envName"
573
+ :placeholder="defaultEnvPlaceholder"
574
+ data-testid="provider-credential-env"
575
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
576
+ />
577
+ <p class="text-[10px] text-foreground-subtle">
578
+ Stored as <code class="font-mono">env:{{ form.envName.trim() || defaultEnvPlaceholder }}</code> — the key itself stays in the environment.
579
+ </p>
580
+ </div>
581
+
582
+ <div v-if="canSetValue" class="flex flex-col gap-1">
583
+ <label class="text-xs text-foreground-muted font-medium">Set value now</label>
584
+ <UInput
585
+ v-model="form.secret"
586
+ type="password"
587
+ autocomplete="off"
588
+ placeholder="Optional — written to this scope's .env"
589
+ data-testid="provider-set-value"
590
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
591
+ />
592
+ </div>
593
+ </template>
594
+
595
+ <div v-if="showReferenceField" class="flex flex-col gap-1">
596
+ <label class="text-xs text-foreground-muted font-medium">
597
+ Credential reference <span class="text-error-500">*</span>
598
+ </label>
311
599
  <UInput
312
- v-model="form.secret"
313
- type="password"
314
- autocomplete="off"
315
- placeholder="Optional — written to this scope's .env"
316
- data-testid="provider-set-value"
600
+ v-model="form.credentialRef"
601
+ placeholder="env:NAME · keyring:forge/name · cmd:… · platform:…"
602
+ data-testid="provider-dialog-credential-ref"
317
603
  class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
318
604
  />
319
605
  </div>
320
606
  </template>
321
607
 
608
+ <!-- Step 3: done, the created entries tested -->
609
+ <div v-else data-testid="provider-done" class="space-y-2">
610
+ <p class="text-xs text-foreground-muted">
611
+ {{ createdIds.length === 1 ? 'Added' : `Added ${createdIds.length} providers` }}. Testing the sign-in:
612
+ </p>
613
+ <div
614
+ v-for="id in createdIds"
615
+ :key="id"
616
+ :data-testid="`provider-created-${id}`"
617
+ class="flex items-start gap-2 p-2 rounded-lg border border-surface-border text-xs"
618
+ >
619
+ <span class="font-mono text-foreground shrink-0">{{ id }}</span>
620
+ <span v-if="testBusy === id" class="text-foreground-subtle italic">testing…</span>
621
+ <span
622
+ v-else-if="testResults[id]"
623
+ :data-testid="`provider-created-result-${id}`"
624
+ :class="testResults[id]?.ok ? 'text-success-500' : 'text-error-500'"
625
+ >{{ testResults[id]?.message }}</span>
626
+ <UButton
627
+ v-if="testBusy !== id"
628
+ size="xs"
629
+ variant="ghost"
630
+ class="ml-auto"
631
+ :disabled="testBusy !== null"
632
+ :data-testid="`provider-created-test-${id}`"
633
+ @click="testProvider({ id })"
634
+ >Test again</UButton>
635
+ </div>
636
+ </div>
637
+
322
638
  <p v-if="dialogError" data-testid="provider-dialog-error" class="text-sm text-error-500">{{ dialogError }}</p>
323
639
  </div>
324
640
  </template>
325
641
 
326
642
  <template #footer>
327
643
  <div class="flex gap-2 justify-end w-full">
328
- <UButton size="sm" variant="ghost" @click="dialogVisible = false">Cancel</UButton>
329
644
  <UButton
645
+ v-if="!editing && dialogStep === 'account'"
330
646
  size="sm"
331
- color="primary"
332
- data-testid="provider-save"
333
- :loading="saving"
334
- @click="saveProvider"
647
+ variant="ghost"
648
+ class="mr-auto"
649
+ data-testid="provider-back"
650
+ @click="backToLibrary()"
335
651
  >
336
- <Icon name="lucide:check" class="mr-1.5" />{{ editing ? 'Save' : 'Add' }}
652
+ <Icon name="lucide:arrow-left" class="mr-1.5" />Back
337
653
  </UButton>
654
+ <template v-if="dialogStep === 'done'">
655
+ <UButton size="sm" color="primary" data-testid="provider-done-close" @click="finishDialog()">
656
+ <Icon name="lucide:check" class="mr-1.5" />Done
657
+ </UButton>
658
+ </template>
659
+ <template v-else>
660
+ <UButton size="sm" variant="ghost" @click="dialogVisible = false">Cancel</UButton>
661
+ <UButton
662
+ v-if="dialogStep === 'account'"
663
+ size="sm"
664
+ color="primary"
665
+ data-testid="provider-save"
666
+ :loading="saving"
667
+ @click="saveProvider"
668
+ >
669
+ <Icon name="lucide:check" class="mr-1.5" />{{ editing ? 'Save' : isAzureAccount ? 'Add account' : 'Add' }}
670
+ </UButton>
671
+ </template>
338
672
  </div>
339
673
  </template>
340
674
  </UModal>
@@ -375,8 +709,16 @@
375
709
 
376
710
  <script setup lang="ts">
377
711
  import { computed } from "vue";
712
+ import type { AIProviderDataZoneRule } from "../shared/provider-registry";
378
713
  import type { ProvidersPanelClient } from "../vue/providers-client";
379
- import { providersPanelNote, providersPanelRowMeta as rowMeta } from "../vue/providers-panel-model";
714
+ import {
715
+ AZURE_ACCOUNT,
716
+ CUSTOM_PRESET,
717
+ type ProvidersPanelModelsMode,
718
+ type ProvidersPanelPreset,
719
+ providersPanelNote,
720
+ providersPanelRowMeta as rowMeta,
721
+ } from "../vue/providers-panel-model";
380
722
  import { type ProvidersPanelLayer, useProvidersPanel } from "../vue/useProvidersPanel";
381
723
 
382
724
  const props = withDefaults(
@@ -402,13 +744,40 @@ const props = withDefaults(
402
744
  readOnly?: boolean;
403
745
  /** The line above the list. Absent: the default text for `layer`; a string replaces it; `false` hides it. */
404
746
  note?: string | false;
747
+ /**
748
+ * Expert mode: the sign-in step also offers the raw reference form
749
+ * (`env:` / `keyring:` / `cmd:` / `platform:`), and expert-only
750
+ * parameters (`api_version`) become editable.
751
+ */
752
+ expert?: boolean;
753
+ /**
754
+ * The host's data-zone rule: rows outside it show a muted note naming the
755
+ * rule (`provider-blocked-<id>`). Absent or `any`: none.
756
+ */
757
+ dataZoneRule?: AIProviderDataZoneRule;
405
758
  }>(),
406
759
  // Without a default Vue casts an absent boolean to `false`; the panel tells absent from `false`.
407
- { environmentEnabled: undefined, note: undefined },
760
+ { environmentEnabled: undefined, note: undefined, expert: false },
408
761
  );
409
762
 
410
763
  const noteText = computed(() => providersPanelNote(props.note, props.layer));
411
764
 
765
+ const MODELS_MODE_ITEMS: Array<{ label: string; value: ProvidersPanelModelsMode }> = [
766
+ { label: "All models", value: "all" },
767
+ { label: "Only these", value: "only" },
768
+ ];
769
+
770
+ /** The library card's second line: what the preset serves. */
771
+ function libraryHint(preset: ProvidersPanelPreset): string {
772
+ if (preset.credential.kind === "none") return "Local, no sign-in";
773
+ if (preset.kind === "stt") return "Speech to text";
774
+ if (preset.kind === "tts") return "Text to speech";
775
+ if (preset.kind === "embed") return "Embeddings";
776
+ return "Chat models";
777
+ }
778
+
779
+ const defaultEnvPlaceholder = computed(() => defaultEnvName.value ?? "PROVIDER_API_KEY");
780
+
412
781
  const emit = defineEmits<{
413
782
  /** After every successful `PUT`/`DELETE`. */
414
783
  (event: "changed"): void;
@@ -431,6 +800,24 @@ const {
431
800
  baseUrlPlaceholder,
432
801
  canSetValue,
433
802
  showCredentialFields,
803
+ defaultEnvName,
804
+ dialogStep,
805
+ libraryPresets,
806
+ chooseLibrary,
807
+ backToLibrary,
808
+ isAzureAccount,
809
+ signInOptions,
810
+ signInOption,
811
+ selectSignIn,
812
+ showSignIn,
813
+ showReferenceField,
814
+ paramFields,
815
+ azureForm,
816
+ addDeploymentRow,
817
+ removeDeploymentRow,
818
+ createdIds,
819
+ finishDialog,
820
+ openSignIn,
434
821
  rowBusy,
435
822
  rowError,
436
823
  canTest,
@@ -440,6 +827,18 @@ const {
440
827
  onCredentialSelect,
441
828
  setEnabled,
442
829
  resetPatch,
830
+ setEuProcessing,
831
+ modelsEditing,
832
+ modelsForm,
833
+ modelsLoading,
834
+ modelsListMessage,
835
+ modelChoices,
836
+ modelsError,
837
+ openModelsEditor,
838
+ setModelsMode,
839
+ toggleModel,
840
+ cancelModelsEditor,
841
+ saveModelsEditor,
443
842
  keyEditing,
444
843
  keyForm,
445
844
  keyError,