@skaile/forge-settings 0.0.0-stage → 1.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 (83) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +94 -2
  4. package/dist/module.d.mts +17 -0
  5. package/dist/module.json +9 -0
  6. package/dist/module.mjs +23 -0
  7. package/dist/runtime/ai-sdk/index.d.ts +45 -0
  8. package/dist/runtime/ai-sdk/index.js +39 -0
  9. package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +21 -0
  10. package/dist/runtime/components/ForgeProvidersPanel.vue +435 -0
  11. package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +21 -0
  12. package/dist/runtime/editor/index.d.ts +58 -0
  13. package/dist/runtime/editor/index.js +83 -0
  14. package/dist/runtime/model/errors.d.ts +24 -0
  15. package/dist/runtime/model/errors.js +10 -0
  16. package/dist/runtime/server/credential-policy.d.ts +60 -0
  17. package/dist/runtime/server/credential-policy.js +64 -0
  18. package/dist/runtime/server/credential-store.d.ts +184 -0
  19. package/dist/runtime/server/credential-store.js +234 -0
  20. package/dist/runtime/server/env-layers.d.ts +62 -0
  21. package/dist/runtime/server/env-layers.js +185 -0
  22. package/dist/runtime/server/env.d.ts +30 -0
  23. package/dist/runtime/server/env.js +15 -0
  24. package/dist/runtime/server/harness-settings.d.ts +150 -0
  25. package/dist/runtime/server/harness-settings.js +166 -0
  26. package/dist/runtime/server/index.d.ts +23 -0
  27. package/dist/runtime/server/index.js +14 -0
  28. package/dist/runtime/server/provider-discovery.d.ts +73 -0
  29. package/dist/runtime/server/provider-discovery.js +77 -0
  30. package/dist/runtime/server/provider-layers.d.ts +27 -0
  31. package/dist/runtime/server/provider-layers.js +37 -0
  32. package/dist/runtime/server/provider-service.d.ts +126 -0
  33. package/dist/runtime/server/provider-service.js +87 -0
  34. package/dist/runtime/server/provider-settings.d.ts +78 -0
  35. package/dist/runtime/server/provider-settings.js +81 -0
  36. package/dist/runtime/server/provider-write.d.ts +154 -0
  37. package/dist/runtime/server/provider-write.js +223 -0
  38. package/dist/runtime/server/settings-defaults.d.ts +26 -0
  39. package/dist/runtime/server/settings-defaults.js +46 -0
  40. package/dist/runtime/shared/env-vars.d.ts +55 -0
  41. package/dist/runtime/shared/env-vars.js +22 -0
  42. package/dist/runtime/shared/harness-providers.d.ts +62 -0
  43. package/dist/runtime/shared/harness-providers.js +38 -0
  44. package/dist/runtime/shared/harnesses.d.ts +19 -0
  45. package/dist/runtime/shared/harnesses.js +15 -0
  46. package/dist/runtime/shared/provider-registry.d.ts +255 -0
  47. package/dist/runtime/shared/provider-registry.js +112 -0
  48. package/dist/runtime/shared/settings-types.d.ts +84 -0
  49. package/dist/runtime/shared/settings-types.js +0 -0
  50. package/dist/runtime/shims-vue.d.ts +8 -0
  51. package/dist/runtime/vue/index.d.ts +14 -0
  52. package/dist/runtime/vue/index.js +6 -0
  53. package/dist/runtime/vue/settings-modal-scope.d.ts +62 -0
  54. package/dist/runtime/vue/settings-modal-scope.js +2 -0
  55. package/dist/runtime/vue/useProvidersPanel.d.ts +232 -0
  56. package/dist/runtime/vue/useProvidersPanel.js +655 -0
  57. package/dist/types.d.mts +9 -0
  58. package/package.json +117 -4
  59. package/src/runtime/ai-sdk/index.ts +91 -0
  60. package/src/runtime/components/ForgeProvidersPanel.vue +449 -0
  61. package/src/runtime/editor/index.ts +157 -0
  62. package/src/runtime/model/errors.ts +31 -0
  63. package/src/runtime/server/credential-policy.ts +137 -0
  64. package/src/runtime/server/credential-store.ts +473 -0
  65. package/src/runtime/server/env-layers.ts +290 -0
  66. package/src/runtime/server/env.ts +46 -0
  67. package/src/runtime/server/harness-settings.ts +373 -0
  68. package/src/runtime/server/index.ts +24 -0
  69. package/src/runtime/server/provider-discovery.ts +164 -0
  70. package/src/runtime/server/provider-layers.ts +70 -0
  71. package/src/runtime/server/provider-service.ts +244 -0
  72. package/src/runtime/server/provider-settings.ts +192 -0
  73. package/src/runtime/server/provider-write.ts +438 -0
  74. package/src/runtime/server/settings-defaults.ts +87 -0
  75. package/src/runtime/shared/env-vars.ts +82 -0
  76. package/src/runtime/shared/harness-providers.ts +118 -0
  77. package/src/runtime/shared/harnesses.ts +34 -0
  78. package/src/runtime/shared/provider-registry.ts +391 -0
  79. package/src/runtime/shared/settings-types.ts +87 -0
  80. package/src/runtime/shims-vue.d.ts +8 -0
  81. package/src/runtime/vue/index.ts +15 -0
  82. package/src/runtime/vue/settings-modal-scope.ts +66 -0
  83. package/src/runtime/vue/useProvidersPanel.ts +1087 -0
@@ -0,0 +1,435 @@
1
+ <template>
2
+ <div data-testid="providers-panel" class="space-y-4">
3
+ <!-- Detected in environment -->
4
+ <div
5
+ v-if="detected.length"
6
+ data-testid="providers-detected"
7
+ class="p-3 bg-primary-500/10 border border-primary-500/20 rounded-lg space-y-2"
8
+ >
9
+ <div class="flex items-center gap-2">
10
+ <Icon name="lucide:key-round" class="text-primary-500 shrink-0" />
11
+ <p class="text-xs font-medium text-foreground">Detected in environment</p>
12
+ </div>
13
+ <div v-for="preset in detected" :key="preset.id" class="flex items-center gap-2 flex-wrap text-xs">
14
+ <span class="font-medium text-foreground">{{ preset.label }}</span>
15
+ <code class="font-mono text-foreground-muted">{{ preset.credential.env }}</code>
16
+ <UButton
17
+ v-if="!props.readOnly"
18
+ size="xs"
19
+ variant="ghost"
20
+ class="ml-auto"
21
+ :loading="detectedBusy === preset.id"
22
+ :disabled="detectedBusy !== null"
23
+ :data-testid="`provider-detected-add-${preset.id}`"
24
+ @click="addDetected(preset)"
25
+ >
26
+ <Icon name="lucide:plus" class="mr-1.5" />Add
27
+ </UButton>
28
+ </div>
29
+ <p v-if="detectedError" class="text-xs text-error-500">{{ detectedError }}</p>
30
+ </div>
31
+
32
+ <div class="flex items-center gap-2">
33
+ <p class="text-xs text-foreground-muted leading-relaxed flex-1">
34
+ {{ props.layer === "workspace" ? "Entries of this workspace's providers.yml can be changed here; instance entries are inherited." : "The instance registry, $FORGE_HOME/.skaile/providers.yml." }}
35
+ Keys stay in the environment; an entry only references them.
36
+ </p>
37
+ <UButton
38
+ v-if="!props.readOnly"
39
+ size="sm"
40
+ color="primary"
41
+ data-testid="provider-add"
42
+ @click="openAdd()"
43
+ >
44
+ <Icon name="lucide:plus" class="mr-2" />Add provider
45
+ </UButton>
46
+ </div>
47
+
48
+ <p v-if="loadError" class="text-sm text-error-500">{{ loadError }}</p>
49
+ <div v-else-if="loading && rows.length === 0" class="text-xs text-foreground-subtle py-2 text-center">
50
+ Loading providers…
51
+ </div>
52
+ <div v-else-if="rows.length === 0" class="text-xs text-foreground-subtle py-2 text-center">
53
+ This registry has no providers yet.
54
+ </div>
55
+
56
+ <div v-else class="overflow-x-auto rounded-lg border border-surface-border">
57
+ <table class="w-full text-xs">
58
+ <thead class="bg-surface-ground/50 text-foreground-muted">
59
+ <tr class="text-left">
60
+ <th class="px-3 py-2 font-medium">Provider</th>
61
+ <th class="px-3 py-2 font-medium">Preset / surface</th>
62
+ <th class="px-3 py-2 font-medium">Base URL</th>
63
+ <th class="px-3 py-2 font-medium">Credential</th>
64
+ <th class="px-3 py-2 font-medium">Enabled</th>
65
+ <th class="px-3 py-2" />
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ <tr
70
+ v-for="row in rows"
71
+ :key="row.id"
72
+ :data-testid="`provider-row-${row.id}`"
73
+ class="border-t border-surface-border align-top"
74
+ :class="row.enabled ? '' : 'opacity-60'"
75
+ >
76
+ <td class="px-3 py-2">
77
+ <div class="flex items-center gap-1.5 flex-wrap">
78
+ <span class="font-medium text-foreground">{{ row.name }}</span>
79
+ <UBadge
80
+ :data-testid="`provider-badge-${row.id}`"
81
+ size="sm"
82
+ variant="subtle"
83
+ color="neutral"
84
+ >{{ row.badge }}</UBadge>
85
+ <Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
86
+ </div>
87
+ <code v-if="row.name !== row.id" class="font-mono text-foreground-subtle">{{ row.id }}</code>
88
+ <p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
89
+ </td>
90
+ <td class="px-3 py-2 font-mono text-foreground-muted">{{ row.origin }}</td>
91
+ <td class="px-3 py-2 font-mono text-foreground-muted break-all">{{ row.baseUrl ?? "\u2014" }}</td>
92
+ <td class="px-3 py-2 min-w-[15rem] space-y-1">
93
+ <USelect
94
+ v-if="row.needsCredential"
95
+ :model-value="row.credentialValue"
96
+ :items="row.credentialOptions"
97
+ placeholder="Choose a credential"
98
+ :disabled="!row.canEditCredential || rowBusy === row.id"
99
+ :data-testid="`provider-credential-${row.id}`"
100
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
101
+ @update:model-value="onCredentialSelect(row, String($event ?? ''))"
102
+ />
103
+ <p :class="row.configured ? 'text-foreground-subtle' : 'text-amber-600 dark:text-amber-400'" class="italic">
104
+ {{ row.status }}
105
+ </p>
106
+
107
+ <!-- Inline key editor -->
108
+ <div
109
+ v-if="keyEditing === row.id"
110
+ :data-testid="`provider-credential-enter-key-${row.id}`"
111
+ class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
112
+ >
113
+ <UInput
114
+ v-model="keyForm.name"
115
+ placeholder="PROVIDER_API_KEY"
116
+ :data-testid="`provider-key-name-${row.id}`"
117
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
118
+ />
119
+ <UInput
120
+ v-model="keyForm.value"
121
+ type="password"
122
+ autocomplete="off"
123
+ placeholder="Key"
124
+ :data-testid="`provider-key-value-${row.id}`"
125
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
126
+ />
127
+ <p v-if="keyError" class="text-error-500">{{ keyError }}</p>
128
+ <div class="flex justify-end gap-1">
129
+ <UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
130
+ <UButton
131
+ size="xs"
132
+ color="primary"
133
+ :loading="rowBusy === row.id"
134
+ :data-testid="`provider-key-save-${row.id}`"
135
+ @click="saveKeyEditor()"
136
+ >Save</UButton>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Inline reference editor -->
141
+ <div
142
+ v-if="refEditing === row.id"
143
+ :data-testid="`provider-credential-ref-${row.id}`"
144
+ class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
145
+ >
146
+ <UInput
147
+ v-model="refForm.ref"
148
+ placeholder="keyring:forge/name · cmd:… · platform:…"
149
+ :data-testid="`provider-ref-input-${row.id}`"
150
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
151
+ />
152
+ <p v-if="refError" class="text-error-500">{{ refError }}</p>
153
+ <div class="flex justify-end gap-1">
154
+ <UButton size="xs" variant="ghost" :data-testid="`provider-ref-cancel-${row.id}`" @click="cancelRefEditor()">Cancel</UButton>
155
+ <UButton
156
+ size="xs"
157
+ color="primary"
158
+ :loading="rowBusy === row.id"
159
+ :data-testid="`provider-ref-save-${row.id}`"
160
+ @click="saveRefEditor()"
161
+ >Save</UButton>
162
+ </div>
163
+ </div>
164
+
165
+ <p
166
+ v-for="notice in row.overrideNotices"
167
+ :key="notice"
168
+ :data-testid="`provider-override-notice-${row.id}`"
169
+ class="text-foreground-subtle"
170
+ >{{ notice }}</p>
171
+ <p
172
+ v-if="rowError?.id === row.id"
173
+ :data-testid="`provider-row-error-${row.id}`"
174
+ class="text-error-500"
175
+ >{{ rowError.message }}</p>
176
+ </td>
177
+ <td class="px-3 py-2">
178
+ <USwitch
179
+ :model-value="row.enabled"
180
+ :disabled="!row.canToggle || rowBusy === row.id"
181
+ :aria-label="`Enable ${row.id}`"
182
+ :data-testid="`provider-enabled-${row.id}`"
183
+ @update:model-value="setEnabled(row, Boolean($event))"
184
+ />
185
+ </td>
186
+ <td class="px-3 py-2">
187
+ <div class="flex items-center justify-end gap-1">
188
+ <UButton
189
+ v-if="row.canReset"
190
+ size="sm"
191
+ variant="ghost"
192
+ :loading="rowBusy === row.id"
193
+ :data-testid="`provider-reset-${row.id}`"
194
+ @click="resetPatch(row)"
195
+ >
196
+ <Icon name="lucide:undo-2" class="mr-1.5 w-3.5 h-3.5" />Reset
197
+ </UButton>
198
+ <template v-if="row.editable">
199
+ <UButton
200
+ size="sm"
201
+ variant="ghost"
202
+ :aria-label="`Edit ${row.id}`"
203
+ :data-testid="`provider-edit-${row.id}`"
204
+ @click="openEdit(row)"
205
+ >
206
+ <Icon name="lucide:pencil" class="w-3.5 h-3.5" />
207
+ </UButton>
208
+ <UButton
209
+ size="sm"
210
+ variant="ghost"
211
+ color="error"
212
+ :aria-label="`Delete ${row.id}`"
213
+ :data-testid="`provider-delete-${row.id}`"
214
+ @click="requestDelete(row)"
215
+ >
216
+ <Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
217
+ </UButton>
218
+ </template>
219
+ </div>
220
+ </td>
221
+ </tr>
222
+ </tbody>
223
+ </table>
224
+ </div>
225
+
226
+ <!-- Add / edit dialog -->
227
+ <UModal
228
+ :open="dialogVisible"
229
+ :title="dialogTitle"
230
+ :ui="{
231
+ content: 'bg-surface-card border border-surface-border sm:max-w-[460px]',
232
+ header: 'bg-surface-card border-b border-surface-border text-foreground',
233
+ body: 'bg-surface-card p-5',
234
+ footer: 'bg-surface-ground/50 border-t border-surface-border p-3 justify-end'
235
+ }"
236
+ @update:open="dialogVisible = $event"
237
+ >
238
+ <template #body>
239
+ <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
+ />
252
+ </div>
253
+
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>
266
+
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>
282
+
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>
294
+
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>
307
+ </div>
308
+
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>
311
+ <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"
317
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
318
+ />
319
+ </div>
320
+ </template>
321
+
322
+ <p v-if="dialogError" data-testid="provider-dialog-error" class="text-sm text-error-500">{{ dialogError }}</p>
323
+ </div>
324
+ </template>
325
+
326
+ <template #footer>
327
+ <div class="flex gap-2 justify-end w-full">
328
+ <UButton size="sm" variant="ghost" @click="dialogVisible = false">Cancel</UButton>
329
+ <UButton
330
+ size="sm"
331
+ color="primary"
332
+ data-testid="provider-save"
333
+ :loading="saving"
334
+ @click="saveProvider"
335
+ >
336
+ <Icon name="lucide:check" class="mr-1.5" />{{ editing ? "Save" : "Add" }}
337
+ </UButton>
338
+ </div>
339
+ </template>
340
+ </UModal>
341
+
342
+ <!-- Delete confirmation -->
343
+ <UModal
344
+ :open="deleteOpen"
345
+ title="Delete provider"
346
+ :ui="{
347
+ content: 'bg-surface-card border border-surface-border sm:max-w-[22rem]',
348
+ header: 'bg-surface-card border-b border-surface-border text-foreground',
349
+ body: 'bg-surface-card p-5'
350
+ }"
351
+ @update:open="deleteOpen = $event"
352
+ >
353
+ <template #body>
354
+ <p class="text-sm text-foreground-muted">
355
+ Delete <span class="font-medium font-mono">{{ deleteTarget?.id }}</span> from the {{ props.layer }} registry?
356
+ <template v-if="props.layer === 'workspace'"> An instance entry with the same id shows again.</template>
357
+ </p>
358
+ <p v-if="deleteError" class="text-sm text-error-500 mt-2">{{ deleteError }}</p>
359
+ <div class="flex justify-end gap-2 mt-4">
360
+ <UButton size="sm" variant="ghost" @click="deleteOpen = false">Cancel</UButton>
361
+ <UButton
362
+ size="sm"
363
+ color="error"
364
+ data-testid="provider-delete-confirm"
365
+ :loading="deleting"
366
+ @click="confirmDelete"
367
+ >
368
+ Delete
369
+ </UButton>
370
+ </div>
371
+ </template>
372
+ </UModal>
373
+ </div>
374
+ </template>
375
+
376
+ <script setup>
377
+ import { useProvidersPanel } from "../vue/useProvidersPanel";
378
+ const props = defineProps({
379
+ providersUrl: { type: String, required: true },
380
+ providersWriteUrl: { type: String, required: true },
381
+ presetsUrl: { type: String, required: false },
382
+ environmentUrl: { type: String, required: false },
383
+ layer: { type: String, required: true },
384
+ readOnly: { type: Boolean, required: false }
385
+ });
386
+ const emit = defineEmits(["changed"]);
387
+ const {
388
+ rows,
389
+ loading,
390
+ loadError,
391
+ dialogVisible,
392
+ dialogTitle,
393
+ editing,
394
+ form,
395
+ dialogError,
396
+ saving,
397
+ presetOptions,
398
+ surfaceOptions,
399
+ isCustom,
400
+ showSurface,
401
+ baseUrlPlaceholder,
402
+ canSetValue,
403
+ showCredentialFields,
404
+ rowBusy,
405
+ rowError,
406
+ onCredentialSelect,
407
+ setEnabled,
408
+ resetPatch,
409
+ keyEditing,
410
+ keyForm,
411
+ keyError,
412
+ cancelKeyEditor,
413
+ saveKeyEditor,
414
+ refEditing,
415
+ refForm,
416
+ refError,
417
+ cancelRefEditor,
418
+ saveRefEditor,
419
+ idError,
420
+ openAdd,
421
+ openEdit,
422
+ selectPreset,
423
+ saveProvider,
424
+ deleteTarget,
425
+ deleteOpen,
426
+ deleteError,
427
+ deleting,
428
+ requestDelete,
429
+ confirmDelete,
430
+ detected,
431
+ detectedBusy,
432
+ detectedError,
433
+ addDetected
434
+ } = useProvidersPanel(props, emit);
435
+ </script>
@@ -0,0 +1,21 @@
1
+ import { type ProvidersPanelLayer } from "../vue/useProvidersPanel.js";
2
+ type __VLS_Props = {
3
+ /** `GET` merged view for this scope (`/api/providers` | `/api/workspaces/:id/providers`). */
4
+ providersUrl: string;
5
+ /** `PUT|DELETE ${providersWriteUrl}/:id`; `GET ${providersWriteUrl}/detected`. */
6
+ providersWriteUrl: string;
7
+ /** Catalog preset summaries. Default `/api/providers/presets`. */
8
+ presetsUrl?: string;
9
+ /** The scope's `.env` route: hand-entered keys. Omitted, key entry is hidden. */
10
+ environmentUrl?: string;
11
+ /** Edit/Delete are offered only for entries this layer defines. */
12
+ layer: ProvidersPanelLayer;
13
+ /** Disables add, edit and delete. */
14
+ readOnly?: boolean;
15
+ };
16
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
17
+ changed: () => any;
18
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
19
+ onChanged?: (() => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `@skaile/forge-settings/editor` — the editor's `models` port over a provider
3
+ * registry.
4
+ *
5
+ * `@skaile/forge-editor`'s AI routes ask the host's `ModelProvider`
6
+ * (`listModels` / `languageModel`) before their own runtime-config presets.
7
+ * This adapter answers from the host's registry: only `llm` entries on the
8
+ * `openai-completions` surface are offered, because the editor's transport is a
9
+ * chat-completions client (`@ai-sdk/openai-compatible`). A profile's id is the
10
+ * registry selector `<providerId>/<modelId>`, which `languageModel` receives
11
+ * back as `{ preset: id }`.
12
+ *
13
+ * The registry and the credential environment are supplied by the host on every
14
+ * call, so provider edits apply without a restart and nothing reads the ambient
15
+ * process. Failures are typed ({@link ModelResolutionError}); the host maps
16
+ * them to HTTP in `onFailure`.
17
+ *
18
+ * `@skaile/forge-editor` is an optional peer of this entry only, for its
19
+ * `ModelProvider` contract (types). `@ai-sdk/openai-compatible` builds the model.
20
+ */
21
+ import type { ModelProfile, ModelRef } from "@skaile/forge-editor/ai";
22
+ import type { ModelProvider } from "@skaile/forge-editor/server";
23
+ import { ModelResolutionError } from "../model/errors.js";
24
+ import { type CredentialPolicy } from "../server/credential-policy.js";
25
+ import { type AIProviderRegistryView } from "../shared/provider-registry.js";
26
+ export { ModelResolutionError } from "../model/errors.js";
27
+ export type { ModelResolutionCode } from "../model/errors.js";
28
+ /** Policy for an editor model call; see `CredentialPolicy`. */
29
+ export interface EditorModelPolicyOptions {
30
+ /** Which credential references may be resolved; refused entries are not offered. */
31
+ policy?: CredentialPolicy;
32
+ }
33
+ /**
34
+ * The registry's chat-completions models as editor model profiles, in registry
35
+ * order. With a policy, entries it refuses are left out.
36
+ */
37
+ export declare function editorModelProfiles(registry: AIProviderRegistryView, options?: EditorModelPolicyOptions): ModelProfile[];
38
+ /**
39
+ * The language model of selector `ref.preset`, or `null` when the registry does
40
+ * not serve it (a browser-defined `{ endpoint, model, apiKey }` reference is the
41
+ * editor's own to resolve).
42
+ *
43
+ * @throws {ModelResolutionError} `policy_refused` when the policy refuses the
44
+ * entry (checked before its credential is read); `unresolved` when its
45
+ * credential reference does not resolve. Neither message carries a secret.
46
+ */
47
+ export declare function editorLanguageModel(registry: AIProviderRegistryView, env: Record<string, string | undefined>, ref: ModelRef, options?: EditorModelPolicyOptions): import("@ai-sdk/provider").LanguageModelV4 | null;
48
+ /** Options for {@link createEditorModelProvider}. */
49
+ export interface EditorModelProviderOptions extends EditorModelPolicyOptions {
50
+ /** The registry to serve, read on every call. */
51
+ registry: () => AIProviderRegistryView | Promise<AIProviderRegistryView>;
52
+ /** The environment `env:` references resolve against, read on every call. */
53
+ env: () => Record<string, string | undefined> | Promise<Record<string, string | undefined>>;
54
+ /** Turn a typed failure into the host's own error (e.g. its HTTP error). Default: rethrow as is. */
55
+ onFailure?: (error: ModelResolutionError) => Error;
56
+ }
57
+ /** The editor's `models` port over the host's registry. */
58
+ export declare function createEditorModelProvider(options: EditorModelProviderOptions): ModelProvider;
@@ -0,0 +1,83 @@
1
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2
+ import { ModelResolutionError } from "../model/errors.js";
3
+ import { entryPolicyRefusal } from "../server/credential-policy.js";
4
+ import { resolveEntryCredential } from "../server/credential-store.js";
5
+ import { listAIProviderModels } from "../server/provider-service.js";
6
+ import {
7
+ entriesOfKind
8
+ } from "../shared/provider-registry.js";
9
+ export { ModelResolutionError } from "../model/errors.js";
10
+ const EDITOR_SURFACE = "openai-completions";
11
+ function editorEntries(registry) {
12
+ const entries = /* @__PURE__ */ new Map();
13
+ for (const [id, entry] of entriesOfKind(registry, "llm")) {
14
+ if (entry.surface === EDITOR_SURFACE && entry.base_url) entries.set(id, entry);
15
+ }
16
+ return entries;
17
+ }
18
+ export function editorModelProfiles(registry, options = {}) {
19
+ const entries = editorEntries(registry);
20
+ const allowed = (providerId) => {
21
+ const entry = entries.get(providerId);
22
+ return entry !== void 0 && !(options.policy && entryPolicyRefusal(entry, options.policy));
23
+ };
24
+ return listAIProviderModels(registry).filter((model) => allowed(model.providerId)).map((model) => ({
25
+ id: model.id,
26
+ label: `${model.providerId} \xB7 ${model.modelId}`,
27
+ endpoint: entries.get(model.providerId).base_url,
28
+ model: model.providerModelId,
29
+ source: "preset"
30
+ }));
31
+ }
32
+ export function editorLanguageModel(registry, env, ref, options = {}) {
33
+ if (!("preset" in ref)) return null;
34
+ const model = listAIProviderModels(registry).find((candidate) => candidate.id === ref.preset);
35
+ const entry = model && editorEntries(registry).get(model.providerId);
36
+ if (!model || !entry) return null;
37
+ if (options.policy) {
38
+ const refusal = entryPolicyRefusal(entry, options.policy);
39
+ if (refusal) {
40
+ throw new ModelResolutionError(
41
+ "policy_refused",
42
+ `AI provider "${model.providerId}": ${refusal}`,
43
+ {
44
+ providerId: model.providerId
45
+ }
46
+ );
47
+ }
48
+ }
49
+ const credential = resolveEntryCredential(entry, {
50
+ env,
51
+ ...options.policy ? { policy: options.policy } : {}
52
+ });
53
+ if (credential.error && !credential.value) {
54
+ throw new ModelResolutionError(
55
+ "unresolved",
56
+ `AI provider "${model.providerId}": ${credential.error}`,
57
+ { providerId: model.providerId }
58
+ );
59
+ }
60
+ return createOpenAICompatible({
61
+ name: model.providerId,
62
+ baseURL: entry.base_url,
63
+ ...credential.value ? { apiKey: credential.value } : {},
64
+ ...entry.headers ? { headers: entry.headers } : {}
65
+ }).chatModel(model.providerModelId);
66
+ }
67
+ export function createEditorModelProvider(options) {
68
+ return {
69
+ async languageModel(ref) {
70
+ try {
71
+ return editorLanguageModel(await options.registry(), await options.env(), ref, options);
72
+ } catch (error) {
73
+ if (error instanceof ModelResolutionError && options.onFailure)
74
+ throw options.onFailure(error);
75
+ throw error;
76
+ }
77
+ },
78
+ async listModels() {
79
+ const profiles = editorModelProfiles(await options.registry(), options);
80
+ return profiles.length > 0 ? profiles : null;
81
+ }
82
+ };
83
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A model could not be resolved. Framework-free: the host decides which HTTP
3
+ * status (or tool error) each `code` becomes.
4
+ *
5
+ * `message` never carries a secret. `providerId` names the registry entry the
6
+ * selector routed to, when it got that far.
7
+ */
8
+ export type ModelResolutionCode =
9
+ /** The registry has error diagnostics, so no selector can be trusted. */
10
+ "registry_invalid"
11
+ /** No registry entry serves the selector. */
12
+ | "unknown_selector"
13
+ /** The host's credential policy refuses the entry; nothing was resolved. */
14
+ | "policy_refused"
15
+ /** The entry's credential, endpoint or provider SDK could not be resolved. */
16
+ | "unresolved";
17
+ export declare class ModelResolutionError extends Error {
18
+ readonly code: ModelResolutionCode;
19
+ readonly providerId?: string;
20
+ constructor(code: ModelResolutionCode, message: string, options?: {
21
+ providerId?: string;
22
+ cause?: unknown;
23
+ });
24
+ }
@@ -0,0 +1,10 @@
1
+ export class ModelResolutionError extends Error {
2
+ code;
3
+ providerId;
4
+ constructor(code, message, options = {}) {
5
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
6
+ this.name = "ModelResolutionError";
7
+ this.code = code;
8
+ if (options.providerId !== void 0) this.providerId = options.providerId;
9
+ }
10
+ }