@skaile/forge-settings 0.0.0-stage → 1.0.1
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/dist/module.d.mts +17 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +23 -0
- package/dist/runtime/ai-sdk/index.d.ts +46 -0
- package/dist/runtime/ai-sdk/index.js +31 -0
- package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +21 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue +435 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +21 -0
- package/dist/runtime/editor/index.d.ts +58 -0
- package/dist/runtime/editor/index.js +72 -0
- package/dist/runtime/model/errors.d.ts +32 -0
- package/dist/runtime/model/errors.js +19 -0
- package/dist/runtime/server/credential-policy.d.ts +61 -0
- package/dist/runtime/server/credential-policy.js +65 -0
- package/dist/runtime/server/credential-store.d.ts +184 -0
- package/dist/runtime/server/credential-store.js +234 -0
- package/dist/runtime/server/env-layers.d.ts +62 -0
- package/dist/runtime/server/env-layers.js +183 -0
- package/dist/runtime/server/env.d.ts +30 -0
- package/dist/runtime/server/env.js +15 -0
- package/dist/runtime/server/harness-settings.d.ts +150 -0
- package/dist/runtime/server/harness-settings.js +166 -0
- package/dist/runtime/server/index.d.ts +23 -0
- package/dist/runtime/server/index.js +14 -0
- package/dist/runtime/server/provider-discovery.d.ts +73 -0
- package/dist/runtime/server/provider-discovery.js +77 -0
- package/dist/runtime/server/provider-layers.d.ts +27 -0
- package/dist/runtime/server/provider-layers.js +35 -0
- package/dist/runtime/server/provider-service.d.ts +126 -0
- package/dist/runtime/server/provider-service.js +87 -0
- package/dist/runtime/server/provider-settings.d.ts +78 -0
- package/dist/runtime/server/provider-settings.js +81 -0
- package/dist/runtime/server/provider-write.d.ts +154 -0
- package/dist/runtime/server/provider-write.js +221 -0
- package/dist/runtime/server/settings-defaults.d.ts +26 -0
- package/dist/runtime/server/settings-defaults.js +46 -0
- package/dist/runtime/shared/env-vars.d.ts +55 -0
- package/dist/runtime/shared/env-vars.js +22 -0
- package/dist/runtime/shared/guards.d.ts +2 -0
- package/dist/runtime/shared/guards.js +3 -0
- package/dist/runtime/shared/harness-providers.d.ts +62 -0
- package/dist/runtime/shared/harness-providers.js +38 -0
- package/dist/runtime/shared/harnesses.d.ts +19 -0
- package/dist/runtime/shared/harnesses.js +15 -0
- package/dist/runtime/shared/provider-registry.d.ts +255 -0
- package/dist/runtime/shared/provider-registry.js +112 -0
- package/dist/runtime/shared/settings-types.d.ts +84 -0
- package/dist/runtime/shared/settings-types.js +0 -0
- package/dist/runtime/shims-vue.d.ts +8 -0
- package/dist/runtime/vue/index.d.ts +14 -0
- package/dist/runtime/vue/index.js +6 -0
- package/dist/runtime/vue/providers-panel-model.d.ts +147 -0
- package/dist/runtime/vue/providers-panel-model.js +37 -0
- package/dist/runtime/vue/settings-modal-scope.d.ts +62 -0
- package/dist/runtime/vue/settings-modal-scope.js +2 -0
- package/dist/runtime/vue/useProvidersPanel.d.ts +115 -0
- package/dist/runtime/vue/useProvidersPanel.js +633 -0
- package/dist/types.d.mts +9 -0
- package/package.json +117 -4
- package/src/runtime/ai-sdk/index.ts +88 -0
- package/src/runtime/components/ForgeProvidersPanel.vue +449 -0
- package/src/runtime/editor/index.ts +146 -0
- package/src/runtime/model/errors.ts +52 -0
- package/src/runtime/server/credential-policy.ts +139 -0
- package/src/runtime/server/credential-store.ts +473 -0
- package/src/runtime/server/env-layers.ts +287 -0
- package/src/runtime/server/env.ts +46 -0
- package/src/runtime/server/harness-settings.ts +373 -0
- package/src/runtime/server/index.ts +24 -0
- package/src/runtime/server/provider-discovery.ts +164 -0
- package/src/runtime/server/provider-layers.ts +67 -0
- package/src/runtime/server/provider-service.ts +244 -0
- package/src/runtime/server/provider-settings.ts +192 -0
- package/src/runtime/server/provider-write.ts +433 -0
- package/src/runtime/server/settings-defaults.ts +87 -0
- package/src/runtime/shared/env-vars.ts +82 -0
- package/src/runtime/shared/guards.ts +4 -0
- package/src/runtime/shared/harness-providers.ts +118 -0
- package/src/runtime/shared/harnesses.ts +34 -0
- package/src/runtime/shared/provider-registry.ts +391 -0
- package/src/runtime/shared/settings-types.ts +87 -0
- package/src/runtime/shims-vue.d.ts +8 -0
- package/src/runtime/vue/index.ts +15 -0
- package/src/runtime/vue/providers-panel-model.ts +203 -0
- package/src/runtime/vue/settings-modal-scope.ts +66 -0
- package/src/runtime/vue/useProvidersPanel.ts +930 -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"><id>/<model></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,72 @@
|
|
|
1
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
2
|
+
import { ModelResolutionError, assertEntryAllowed } 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 && !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
|
+
assertEntryAllowed(entry, model.providerId, options.policy);
|
|
38
|
+
const credential = resolveEntryCredential(entry, {
|
|
39
|
+
env,
|
|
40
|
+
...options.policy ? { policy: options.policy } : {}
|
|
41
|
+
});
|
|
42
|
+
if (credential.error && !credential.value) {
|
|
43
|
+
throw new ModelResolutionError(
|
|
44
|
+
"unresolved",
|
|
45
|
+
`provider '${model.providerId}': ${credential.error}`,
|
|
46
|
+
{ providerId: model.providerId }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return createOpenAICompatible({
|
|
50
|
+
name: model.providerId,
|
|
51
|
+
baseURL: entry.base_url,
|
|
52
|
+
...credential.value ? { apiKey: credential.value } : {},
|
|
53
|
+
...entry.headers ? { headers: entry.headers } : {}
|
|
54
|
+
}).chatModel(model.providerModelId);
|
|
55
|
+
}
|
|
56
|
+
export function createEditorModelProvider(options) {
|
|
57
|
+
return {
|
|
58
|
+
async languageModel(ref) {
|
|
59
|
+
try {
|
|
60
|
+
return editorLanguageModel(await options.registry(), await options.env(), ref, options);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof ModelResolutionError && options.onFailure)
|
|
63
|
+
throw options.onFailure(error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async listModels() {
|
|
68
|
+
const profiles = editorModelProfiles(await options.registry(), options);
|
|
69
|
+
return profiles.length > 0 ? profiles : null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CredentialPolicy } from "../server/credential-policy.js";
|
|
2
|
+
import type { AIProviderEntry } from "../shared/provider-registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* A model could not be resolved. Framework-free: the host decides which HTTP
|
|
5
|
+
* status (or tool error) each `code` becomes.
|
|
6
|
+
*
|
|
7
|
+
* `message` never carries a secret. `providerId` names the registry entry the
|
|
8
|
+
* selector routed to, when it got that far.
|
|
9
|
+
*/
|
|
10
|
+
export type ModelResolutionCode =
|
|
11
|
+
/** The registry has error diagnostics, so no selector can be trusted. */
|
|
12
|
+
"registry_invalid"
|
|
13
|
+
/** No registry entry serves the selector. */
|
|
14
|
+
| "unknown_selector"
|
|
15
|
+
/** The host's credential policy refuses the entry; nothing was resolved. */
|
|
16
|
+
| "policy_refused"
|
|
17
|
+
/** The entry's credential, endpoint or provider SDK could not be resolved. */
|
|
18
|
+
| "unresolved";
|
|
19
|
+
export declare class ModelResolutionError extends Error {
|
|
20
|
+
readonly code: ModelResolutionCode;
|
|
21
|
+
readonly providerId?: string;
|
|
22
|
+
constructor(code: ModelResolutionCode, message: string, options?: {
|
|
23
|
+
providerId?: string;
|
|
24
|
+
cause?: unknown;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Throw `policy_refused` when `policy` refuses `entry`; no policy refuses
|
|
29
|
+
* nothing. The one place a model adapter turns a policy refusal into an error,
|
|
30
|
+
* so every adapter words it the same way. Nothing is resolved.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertEntryAllowed(entry: AIProviderEntry, providerId: string, policy?: CredentialPolicy): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { entryPolicyRefusal } from "../server/credential-policy.js";
|
|
2
|
+
export class ModelResolutionError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
providerId;
|
|
5
|
+
constructor(code, message, options = {}) {
|
|
6
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
7
|
+
this.name = "ModelResolutionError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
if (options.providerId !== void 0) this.providerId = options.providerId;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function assertEntryAllowed(entry, providerId, policy) {
|
|
13
|
+
const refusal = entryPolicyRefusal(entry, policy);
|
|
14
|
+
if (refusal) {
|
|
15
|
+
throw new ModelResolutionError("policy_refused", `provider '${providerId}': ${refusal}`, {
|
|
16
|
+
providerId
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|