@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,244 @@
1
+ /**
2
+ * The provider inventory a forge app shows and selects from.
3
+ *
4
+ * Every fact here comes out of the AI provider registry the session was handed:
5
+ * which providers exist, whether one serves chat or speech, where it points,
6
+ * which credential reference backs it and whether that reference resolves.
7
+ * forge-settings owns no provider list and no provider-type table any more — an
8
+ * entry's `kind` is the discriminator, and the registry is the list.
9
+ *
10
+ * What forge deliberately does not do here:
11
+ *
12
+ * - **Endpoint probing.** "Does this endpoint accept this key" is the
13
+ * runtime's `skaile providers test` / the catalog's entry probe, which
14
+ * knows every surface's shape. {@link testAIProviderCredential} answers the
15
+ * question forge owns: does the credential reference resolve, and where
16
+ * from. That is the failure mode a settings modal must explain.
17
+ * - **Live model discovery.** Model lists come from the entry's own
18
+ * `model_map`/`capabilities`; harness-backed discovery is
19
+ * `listModelsForHarness(harnessId, registry)` in the runtime.
20
+ */
21
+
22
+ import type {
23
+ AIProviderCapabilities,
24
+ AIProviderCredentialKind,
25
+ AIProviderKind,
26
+ AIProviderRegistryView,
27
+ AIProviderSurface,
28
+ AIProviderVoiceParams,
29
+ } from "../shared/provider-registry";
30
+ import {
31
+ entriesOfKind,
32
+ isEntryEnabled,
33
+ isVoiceEntry,
34
+ parseCredentialRef,
35
+ registryEntries,
36
+ } from "../shared/provider-registry";
37
+ import {
38
+ type CredentialSource,
39
+ type CredentialStoreOptions,
40
+ resolveEntryCredential,
41
+ } from "./credential-store";
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Types
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /** One registry entry, as a settings UI needs to see it. */
48
+ export interface ProviderInfo {
49
+ /** Registry id — the left half of a `"<providerId>/<modelId>"` selector. */
50
+ id: string;
51
+ /** Display name: the entry's `label`, else its id. */
52
+ name: string;
53
+ /** What the entry produces. Replaces forge's old llm/stt/tts guesswork. */
54
+ kind: AIProviderKind;
55
+ /** Whether the entry's credential resolved to a value in this process. */
56
+ configured: boolean;
57
+ /** Which reference form answered — `ambient` means a cloud SDK chain signs. */
58
+ source: CredentialSource;
59
+ /** Why the credential did not resolve. Secret-free, safe to render. */
60
+ error?: string;
61
+ /** The entry's `credential.ref`. A reference, never a secret. */
62
+ credentialRef?: string;
63
+ /** How the credential is presented on the wire. */
64
+ credentialKind?: AIProviderCredentialKind;
65
+ /**
66
+ * Whether the entry's reference is an `env:` one. The variable is shared by
67
+ * every user of the server, so a settings UI names it rather than pretending
68
+ * the value behind it is the user's to change.
69
+ */
70
+ hasEnvKey: boolean;
71
+ /** Name of that environment variable, when {@link hasEnvKey}. */
72
+ envKeyName?: string;
73
+ /** Endpoint the entry pins, when it pins one. */
74
+ baseUrl?: string;
75
+ /** Catalog preset the entry inherits from, when it names one. */
76
+ preset?: string;
77
+ /** Wire protocol of an `llm`/`embed` entry (its own or its preset's). */
78
+ surface?: AIProviderSurface;
79
+ /** The entry's `enabled` flag; `false` only in a listing with `includeDisabled`. */
80
+ enabled: boolean;
81
+ /**
82
+ * Voice parameters a `kind: stt|tts` entry declares — Deepgram
83
+ * `{model, language}`, ElevenLabs `{voice_id, output_format}`. A settings UI
84
+ * shows these as the defaults a per-request override replaces; `llm`/`embed`
85
+ * entries never carry them.
86
+ */
87
+ params?: AIProviderVoiceParams;
88
+ /** Frozen by the control plane: a lower layer cannot change or disable it. */
89
+ locked: boolean;
90
+ }
91
+
92
+ /** One selectable model of one provider. */
93
+ export interface ModelInfo {
94
+ /** The selector to store in `settings.model`: `"<providerId>/<modelId>"`. */
95
+ id: string;
96
+ /** Display name — the logical model id. */
97
+ name: string;
98
+ /** Registry id of the entry serving the model. */
99
+ providerId: string;
100
+ /** Logical id: the `model_map` key. */
101
+ modelId: string;
102
+ /** Provider-side id the logical id maps to. */
103
+ providerModelId: string;
104
+ /** Capability facts the entry declares for this model, when any. */
105
+ capabilities?: AIProviderCapabilities;
106
+ }
107
+
108
+ /** Result of resolving one entry's credential on demand. */
109
+ export interface ProviderTestResult {
110
+ /** Whether the credential resolved to a usable value. */
111
+ ok: boolean;
112
+ /** Registry id that was tested. */
113
+ provider: string;
114
+ /** Which reference form answered. */
115
+ source: CredentialSource;
116
+ /** Why it did not resolve. Secret-free. */
117
+ error?: string;
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Inventory
122
+ // ---------------------------------------------------------------------------
123
+
124
+ /** What {@link listAIProviders} resolves credentials against, and which entries it lists. */
125
+ export interface ListAIProvidersOptions extends CredentialStoreOptions {
126
+ /** List `enabled: false` entries too (a registry editor's view). Default `false`. */
127
+ includeDisabled?: boolean;
128
+ }
129
+
130
+ /**
131
+ * Describe every enabled registry entry, in registry order — every entry with
132
+ * `includeDisabled`.
133
+ *
134
+ * Unconfigured entries are included: an entry whose `env:` variable is missing
135
+ * is exactly what the user has to see and fix, and hiding it (the pre-5.0.0
136
+ * behaviour, which filtered on "has a key") turned a typo into an empty list.
137
+ */
138
+ export function listAIProviders(
139
+ registry: AIProviderRegistryView,
140
+ options: ListAIProvidersOptions = {},
141
+ ): ProviderInfo[] {
142
+ const providers: ProviderInfo[] = [];
143
+ for (const [id, entry] of registryEntries(registry)) {
144
+ const enabled = isEntryEnabled(entry);
145
+ if (!enabled && !options.includeDisabled) continue;
146
+ const resolved = resolveEntryCredential(entry, options);
147
+ const ref = entry.credential?.ref;
148
+ const parsed = ref ? parseCredentialRef(ref) : undefined;
149
+ providers.push({
150
+ id,
151
+ name: entry.label?.trim() || id,
152
+ kind: entry.kind,
153
+ // No error means usable: a value resolved, none is needed (`kind: none`),
154
+ // or an ambient cloud chain signs the request.
155
+ configured: resolved.error === undefined,
156
+ source: resolved.source,
157
+ ...(resolved.error ? { error: resolved.error } : {}),
158
+ ...(ref ? { credentialRef: ref } : {}),
159
+ ...(entry.credential ? { credentialKind: entry.credential.kind } : {}),
160
+ hasEnvKey: parsed?.form === "env",
161
+ ...(parsed?.form === "env" ? { envKeyName: parsed.name } : {}),
162
+ ...(entry.base_url ? { baseUrl: entry.base_url } : {}),
163
+ ...(entry.preset ? { preset: entry.preset } : {}),
164
+ ...(!isVoiceEntry(entry) && entry.surface ? { surface: entry.surface } : {}),
165
+ enabled,
166
+ ...(isVoiceEntry(entry) && entry.params ? { params: entry.params } : {}),
167
+ locked: entry.locked === true,
168
+ });
169
+ }
170
+ return providers;
171
+ }
172
+
173
+ /**
174
+ * List the models the registry declares, as `"<providerId>/<modelId>"`
175
+ * selectors.
176
+ *
177
+ * Sourced from each `llm`/`embed` entry's `model_map` (plus any id that only
178
+ * appears in `capabilities`) — the same two tables the runtime's model-reference
179
+ * resolver reads, so a selector built from this list always resolves.
180
+ *
181
+ * @param providerId - Restrict to one entry; omit for every enabled entry.
182
+ */
183
+ export function listAIProviderModels(
184
+ registry: AIProviderRegistryView,
185
+ providerId?: string,
186
+ ): ModelInfo[] {
187
+ const models: ModelInfo[] = [];
188
+ for (const kind of ["llm", "embed"] as const) {
189
+ for (const [id, entry] of entriesOfKind(registry, kind)) {
190
+ if (providerId && id !== providerId) continue;
191
+ const logicalIds = new Set([
192
+ ...Object.keys(entry.model_map ?? {}),
193
+ ...Object.keys(entry.capabilities ?? {}),
194
+ ]);
195
+ for (const modelId of logicalIds) {
196
+ const capabilities = entry.capabilities?.[modelId];
197
+ models.push({
198
+ id: `${id}/${modelId}`,
199
+ name: modelId,
200
+ providerId: id,
201
+ modelId,
202
+ providerModelId: entry.model_map?.[modelId] ?? modelId,
203
+ ...(capabilities ? { capabilities } : {}),
204
+ });
205
+ }
206
+ }
207
+ }
208
+ return models;
209
+ }
210
+
211
+ /**
212
+ * Resolve one entry's credential and report the outcome.
213
+ *
214
+ * This is the check behind a settings modal's per-provider indicator: it proves
215
+ * the reference points at something this server can read, and names the layer
216
+ * that owns it when it does not (`cmd:` in the spawned runtime, `platform:` in
217
+ * the control plane).
218
+ */
219
+ export function testAIProviderCredential(
220
+ providerId: string,
221
+ registry: AIProviderRegistryView,
222
+ options: CredentialStoreOptions = {},
223
+ ): ProviderTestResult {
224
+ const match = registryEntries(registry).find(([id]) => id === providerId);
225
+ if (!match) {
226
+ return {
227
+ ok: false,
228
+ provider: providerId,
229
+ source: "none",
230
+ error: `'${providerId}' is not an entry in this registry`,
231
+ };
232
+ }
233
+ const [, entry] = match;
234
+ if (!isEntryEnabled(entry)) {
235
+ return { ok: false, provider: providerId, source: "none", error: "entry is disabled" };
236
+ }
237
+ const resolved = resolveEntryCredential(entry, options);
238
+ return {
239
+ ok: resolved.error === undefined,
240
+ provider: providerId,
241
+ source: resolved.source,
242
+ ...(resolved.error ? { error: resolved.error } : {}),
243
+ };
244
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Where a forge app's settings meet the AI provider registry.
3
+ *
4
+ * Two jobs:
5
+ *
6
+ * 1. **Migration.** A `settings.json` written before the 5.0.0 registry migration holds
7
+ * `provider` + a free-text `model`, and an `apiKeys` map of secrets. On
8
+ * first load the pair collapses into one `"<providerId>/<modelId>"`
9
+ * selector, and the `apiKeys` map is dropped with a warning — a plaintext
10
+ * key is never auto-written anywhere; the user moves it into `.env`.
11
+ * 2. **Voice selection.** `sttProvider`/`ttsProvider` are registry ids of
12
+ * `kind: stt|tts` entries; the endpoint and the voice parameters live on
13
+ * the entry, and `sttLanguage`/`ttsVoice` are per-request overrides of two
14
+ * of those parameters. Resolving all of that used to be three apps'
15
+ * worth of hardcoded endpoints and voice ids.
16
+ */
17
+
18
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
19
+ import type {
20
+ AIProviderVoiceEntry,
21
+ AIProviderVoiceKind,
22
+ AIProviderVoiceParams,
23
+ AIProviderRegistryView,
24
+ } from "../shared/provider-registry";
25
+ import {
26
+ entriesOfKind,
27
+ selectVoiceEntryId,
28
+ VOICE_OVERRIDE_PARAM,
29
+ } from "../shared/provider-registry";
30
+ import type { SkaileSettings } from "../shared/settings-types";
31
+
32
+ /** What {@link migrateLegacyProviderSettings} did to one settings file. */
33
+ export interface LegacySettingsMigration {
34
+ /** The file that was inspected. */
35
+ path: string;
36
+ /** Whether the file was rewritten. */
37
+ changed: boolean;
38
+ /** The selector `provider` + `model` collapsed into, when that happened. */
39
+ model?: string;
40
+ /** Everything dropped or left untouched, and why. Secret-free. */
41
+ warnings: string[];
42
+ }
43
+
44
+ /**
45
+ * Migrate one legacy `settings.json` in place. Idempotent: a file with no
46
+ * legacy `provider` and no `apiKeys` is left byte-identical.
47
+ *
48
+ * An `apiKeys` map is removed from the file and never stored anywhere else:
49
+ * the registry holds references only, and a plaintext key has no safe
50
+ * automatic destination. Each non-empty entry is reported through `warnings`
51
+ * (without the key) so the user can move it into `.env`.
52
+ *
53
+ * @param settingsPath - Absolute path to a `settings.json`.
54
+ */
55
+ export function migrateLegacyProviderSettings(settingsPath: string): LegacySettingsMigration {
56
+ const result: LegacySettingsMigration = {
57
+ path: settingsPath,
58
+ changed: false,
59
+ warnings: [],
60
+ };
61
+ if (!existsSync(settingsPath)) return result;
62
+
63
+ let settings: Record<string, unknown>;
64
+ try {
65
+ settings = JSON.parse(readFileSync(settingsPath, "utf8")) as Record<string, unknown>;
66
+ } catch (err) {
67
+ result.warnings.push(`${settingsPath} is not valid JSON (${(err as Error).message})`);
68
+ return result;
69
+ }
70
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
71
+ result.warnings.push(`${settingsPath} does not contain a settings object`);
72
+ return result;
73
+ }
74
+
75
+ // ── provider + free-text model → one selector ───────────────────────────
76
+ const legacyProvider = typeof settings.provider === "string" ? settings.provider.trim() : "";
77
+ if (legacyProvider) {
78
+ const model = typeof settings.model === "string" ? settings.model.trim() : "";
79
+ if (model && !model.includes("/")) {
80
+ settings.model = `${legacyProvider}/${model}`;
81
+ result.model = settings.model as string;
82
+ } else if (!model) {
83
+ result.warnings.push(
84
+ `provider '${legacyProvider}' had no model to namespace — the session now resolves ` +
85
+ "its model through the registry's own order",
86
+ );
87
+ }
88
+ delete settings.provider;
89
+ result.changed = true;
90
+ }
91
+
92
+ // ── apiKeys map → dropped, never stored ─────────────────────────────────
93
+ if ("apiKeys" in settings) {
94
+ const apiKeys = settings.apiKeys;
95
+ const hasSecret =
96
+ typeof apiKeys === "object" &&
97
+ apiKeys !== null &&
98
+ Object.values(apiKeys).some((value) => typeof value === "string" && value !== "");
99
+ if (hasSecret) {
100
+ result.warnings.push(
101
+ "apiKeys found in settings: move each key into .env and reference it from providers.yml (skaile providers add)",
102
+ );
103
+ }
104
+ delete settings.apiKeys;
105
+ result.changed = true;
106
+ }
107
+
108
+ if (result.changed) {
109
+ writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
110
+ }
111
+ return result;
112
+ }
113
+
114
+ // ── Voice selection ────────────────────────────────────────────────────────
115
+
116
+ /** Settings fields that name a voice entry and override its parameters. */
117
+ export type VoiceSettings = Pick<
118
+ SkaileSettings,
119
+ "sttProvider" | "ttsProvider" | "sttLanguage" | "ttsVoice"
120
+ >;
121
+
122
+ /** The `kind: stt|tts` entry a request runs against, with its parameters resolved. */
123
+ export interface VoiceProviderSelection {
124
+ /** Registry id of the entry. */
125
+ providerId: string;
126
+ entry: AIProviderVoiceEntry;
127
+ /** Endpoint from the entry; absent when the entry inherits its preset's default. */
128
+ baseUrl?: string;
129
+ /**
130
+ * The entry's `params` with overrides applied — Deepgram `{model, language}`,
131
+ * ElevenLabs `{voice_id, output_format}`. This is the only place a forge app
132
+ * should read voice defaults from.
133
+ */
134
+ params: AIProviderVoiceParams;
135
+ /** True when settings named no usable entry and registry order decided. */
136
+ fallback: boolean;
137
+ /** Non-fatal notes, e.g. settings naming an entry the registry does not have. */
138
+ warnings: string[];
139
+ }
140
+
141
+ /**
142
+ * Resolve which registry entry serves speech-to-text or text-to-speech.
143
+ *
144
+ * Selection is `settings.sttProvider`/`settings.ttsProvider`, falling back to
145
+ * the first enabled entry of that kind in registry order when the setting is
146
+ * absent or names an entry that is gone — a missing setting must not silence an
147
+ * app that has a perfectly good entry configured.
148
+ *
149
+ * Parameter precedence, lowest first: the entry's `params`, then the settings
150
+ * overrides (`sttLanguage` → `language`, `ttsVoice` → `voice_id`), then
151
+ * `overrides` from the request. The per-request override is the contract
152
+ * forge-concept already honoured and the other apps silently dropped.
153
+ *
154
+ * @returns The selection, or `undefined` when the registry has no enabled entry
155
+ * of that kind.
156
+ */
157
+ export function resolveVoiceProvider(
158
+ registry: AIProviderRegistryView,
159
+ kind: AIProviderVoiceKind,
160
+ settings: VoiceSettings = {},
161
+ overrides: AIProviderVoiceParams = {},
162
+ ): VoiceProviderSelection | undefined {
163
+ const candidates = entriesOfKind(registry, kind);
164
+ if (candidates.length === 0) return undefined;
165
+
166
+ const warnings: string[] = [];
167
+ const wanted = (kind === "stt" ? settings.sttProvider : settings.ttsProvider)?.trim();
168
+ const ids = candidates.map(([id]) => id);
169
+ // `candidates` is non-empty, so the shared rule always names one of these ids.
170
+ const providerId = selectVoiceEntryId(ids, wanted) as string;
171
+ const entry = candidates[ids.indexOf(providerId)][1];
172
+ if (wanted && wanted !== providerId) {
173
+ warnings.push(
174
+ `settings name '${wanted}' for ${kind}, which is not an enabled ${kind} entry — ` +
175
+ `using '${providerId}' instead`,
176
+ );
177
+ }
178
+ const fallback = wanted !== providerId;
179
+
180
+ const settingsOverride: AIProviderVoiceParams = {};
181
+ const override = (kind === "stt" ? settings.sttLanguage : settings.ttsVoice)?.trim();
182
+ if (override) settingsOverride[VOICE_OVERRIDE_PARAM[kind]] = override;
183
+
184
+ return {
185
+ providerId,
186
+ entry,
187
+ ...(entry.base_url ? { baseUrl: entry.base_url } : {}),
188
+ params: { ...entry.params, ...settingsOverride, ...overrides },
189
+ fallback,
190
+ warnings,
191
+ };
192
+ }