@luckydraw/cumulus 1.0.11 → 1.0.13

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 (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/gateway/adapters/webchat.d.ts +1 -1
  3. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  4. package/dist/gateway/adapters/webchat.js +1 -0
  5. package/dist/gateway/adapters/webchat.js.map +1 -1
  6. package/dist/gateway/config.d.ts +75 -2
  7. package/dist/gateway/config.d.ts.map +1 -1
  8. package/dist/gateway/config.js +101 -5
  9. package/dist/gateway/config.js.map +1 -1
  10. package/dist/gateway/daemon.js +16 -0
  11. package/dist/gateway/daemon.js.map +1 -1
  12. package/dist/gateway/hooks.d.ts +1 -0
  13. package/dist/gateway/hooks.d.ts.map +1 -1
  14. package/dist/gateway/hooks.js +1 -0
  15. package/dist/gateway/hooks.js.map +1 -1
  16. package/dist/gateway/server.d.ts +2 -0
  17. package/dist/gateway/server.d.ts.map +1 -1
  18. package/dist/gateway/server.js +19 -1
  19. package/dist/gateway/server.js.map +1 -1
  20. package/dist/gateway/static/widget.js +356 -54
  21. package/dist/lib/agentic-loop.d.ts +10 -0
  22. package/dist/lib/agentic-loop.d.ts.map +1 -1
  23. package/dist/lib/agentic-loop.js +64 -1
  24. package/dist/lib/agentic-loop.js.map +1 -1
  25. package/dist/lib/content-store.d.ts +82 -3
  26. package/dist/lib/content-store.d.ts.map +1 -1
  27. package/dist/lib/content-store.js +159 -21
  28. package/dist/lib/content-store.js.map +1 -1
  29. package/dist/lib/embeddings.d.ts +14 -0
  30. package/dist/lib/embeddings.d.ts.map +1 -1
  31. package/dist/lib/embeddings.js +1 -1
  32. package/dist/lib/embeddings.js.map +1 -1
  33. package/dist/lib/gateway.d.ts +52 -2
  34. package/dist/lib/gateway.d.ts.map +1 -1
  35. package/dist/lib/gateway.js +120 -32
  36. package/dist/lib/gateway.js.map +1 -1
  37. package/dist/lib/huggingface-provider.d.ts +12 -0
  38. package/dist/lib/huggingface-provider.d.ts.map +1 -1
  39. package/dist/lib/huggingface-provider.js +45 -14
  40. package/dist/lib/huggingface-provider.js.map +1 -1
  41. package/package.json +1 -1
@@ -50,10 +50,40 @@
50
50
  // Claude CLI variants and direct API models have different runtime contracts,
51
51
  // but the UI presents them as one provider-facing catalog. Keep these transforms
52
52
  // pure so their losslessness/default rules can be regression-tested directly.
53
+ // Task 147: a model entry's provider is `custom:<id>` when it runs on a
54
+ // user-registered OpenAI-compatible endpoint. The catalog carries that string
55
+ // verbatim so a round-trip can never silently re-home a model onto one of the
56
+ // three built-in providers.
57
+ function isCustomProvider(provider) {
58
+ return typeof provider === 'string' && provider.indexOf('custom:') === 0;
59
+ }
60
+
61
+ // Derive a registry id from a display name. Must satisfy the SAME shape the
62
+ // gateway validates ([a-z0-9][a-z0-9_-]*), or the provider is dropped on save
63
+ // and its models silently fall back to the Claude path.
64
+ function slugifyProviderId(label) {
65
+ var slug = String(label || '')
66
+ .toLowerCase()
67
+ .replace(/[^a-z0-9_-]+/g, '-')
68
+ .replace(/^-+/, '')
69
+ .replace(/-+$/, '');
70
+ return slug;
71
+ }
72
+
53
73
  function gatewayConfigToModelCatalog(config) {
54
74
  config = config || {};
55
75
  var topModels = Array.isArray(config.models) ? config.models : [];
56
76
  var claudeModels = Array.isArray(config.claudeModels) ? config.claudeModels : [];
77
+ var customProviders = (Array.isArray(config.customProviders) ? config.customProviders : []).map(
78
+ function (provider) {
79
+ return {
80
+ id: String(provider.id || ''),
81
+ label: String(provider.label || provider.id || ''),
82
+ baseUrl: String(provider.baseUrl || ''),
83
+ maskedKey: String(provider.apiKey || ''),
84
+ };
85
+ }
86
+ );
57
87
  var sentinel = topModels.find(function (model) {
58
88
  return model.id === 'claude' || model.provider === 'claude-cli';
59
89
  }) || { id: 'claude', label: 'Claude (CLI)', provider: 'claude-cli' };
@@ -81,7 +111,11 @@
81
111
  catalog.push({
82
112
  id: model.id,
83
113
  label: model.label || model.name || model.id,
84
- provider: model.provider === 'openai' ? 'openai' : 'huggingface',
114
+ provider: isCustomProvider(model.provider)
115
+ ? model.provider
116
+ : model.provider === 'openai'
117
+ ? 'openai'
118
+ : 'huggingface',
85
119
  contextWindow: typeof model.contextWindow === 'number' ? model.contextWindow : undefined,
86
120
  gatewayDefault: globalDefault === model.id,
87
121
  providerDefault: false,
@@ -96,7 +130,11 @@
96
130
  ) {
97
131
  catalog[0].gatewayDefault = true;
98
132
  }
99
- return { catalog: catalog, sentinel: Object.assign({}, sentinel) };
133
+ return {
134
+ catalog: catalog,
135
+ sentinel: Object.assign({}, sentinel),
136
+ customProviders: customProviders,
137
+ };
100
138
  }
101
139
 
102
140
  function modelCatalogToGatewayConfig(state) {
@@ -137,7 +175,11 @@
137
175
  var entry = Object.assign({}, model.source || {});
138
176
  entry.id = model.id;
139
177
  entry.label = model.label || model.id;
140
- entry.provider = model.provider === 'openai' ? 'openai' : 'huggingface';
178
+ entry.provider = isCustomProvider(model.provider)
179
+ ? model.provider
180
+ : model.provider === 'openai'
181
+ ? 'openai'
182
+ : 'huggingface';
141
183
  delete entry.default;
142
184
  if (typeof model.contextWindow === 'number' && model.contextWindow > 0) {
143
185
  entry.contextWindow = model.contextWindow;
@@ -150,13 +192,38 @@
150
192
  models: [sentinel].concat(directModels),
151
193
  claudeModels: claudeModels,
152
194
  model: selected && selected.provider !== 'anthropic' ? selected.id : 'claude',
195
+ // Registry travels without credentials — addCredentialPatch fills those
196
+ // in, so an untouched key round-trips as absent and the server preserves it.
197
+ customProviders: (Array.isArray(state.customProviders) ? state.customProviders : []).map(
198
+ function (provider) {
199
+ return {
200
+ id: provider.id,
201
+ label: provider.label || provider.id,
202
+ baseUrl: provider.baseUrl,
203
+ };
204
+ }
205
+ ),
153
206
  };
154
207
  }
155
208
 
156
- function validateModelCatalog(catalog) {
209
+ // Task 147. `customProviders` is optional: when supplied, a model referencing
210
+ // a provider that is not in the registry is rejected here rather than saved —
211
+ // the gateway resolves an orphaned `custom:<id>` fail-closed to the Claude
212
+ // path, so the model would silently run somewhere else instead of erroring.
213
+ function validateModelCatalog(catalog, customProviders) {
214
+ var known = null;
215
+ if (Array.isArray(customProviders)) {
216
+ known = {};
217
+ customProviders.forEach(function (provider) {
218
+ known['custom:' + provider.id] = true;
219
+ });
220
+ }
157
221
  var seen = { claude: true };
158
222
  for (var i = 0; i < catalog.length; i++) {
159
223
  var model = catalog[i];
224
+ if (known && isCustomProvider(model.provider) && !known[model.provider]) {
225
+ return 'Unknown provider for ' + (model.id || 'model') + ': ' + model.provider;
226
+ }
160
227
  model.id = String(model.id || '').trim();
161
228
  model.label = String(model.label || model.id).trim() || model.id;
162
229
  if (!model.id) return 'Every model needs an ID';
@@ -173,6 +240,23 @@
173
240
  return '';
174
241
  }
175
242
 
243
+ /** Task 147: registry-level validation, mirroring the gateway's own rules. */
244
+ function validateCustomProviderList(customProviders) {
245
+ var seen = {};
246
+ for (var i = 0; i < customProviders.length; i++) {
247
+ var provider = customProviders[i];
248
+ var id = String(provider.id || '').trim();
249
+ var baseUrl = String(provider.baseUrl || '').trim();
250
+ if (!id) return 'Every custom provider needs a name';
251
+ if (seen[id]) return 'Duplicate custom provider: ' + id;
252
+ seen[id] = true;
253
+ if (!/^https?:\/\/\S+$/.test(baseUrl)) {
254
+ return 'Base URL for ' + (provider.label || id) + ' must start with http:// or https://';
255
+ }
256
+ }
257
+ return '';
258
+ }
259
+
176
260
  function addCredentialPatch(payload, credentials) {
177
261
  [
178
262
  { provider: 'openai', key: 'openaiApiKey' },
@@ -184,6 +268,15 @@
184
268
  if (credential.clear) payload[entry.key] = null;
185
269
  else if (value && value.indexOf('•') === -1) payload[entry.key] = value;
186
270
  });
271
+ // Custom providers carry their own credential inside the registry entry
272
+ // (task 147), keyed in the same credential maps by `custom:<id>`. Same rule
273
+ // as the scalars above: null clears, a real value sets, absent preserves.
274
+ (payload.customProviders || []).forEach(function (provider) {
275
+ var credential = credentials['custom:' + provider.id] || {};
276
+ var value = String(credential.value || '').trim();
277
+ if (credential.clear) provider.apiKey = null;
278
+ else if (value && value.indexOf('•') === -1) provider.apiKey = value;
279
+ });
187
280
  return payload;
188
281
  }
189
282
 
@@ -1561,6 +1654,13 @@
1561
1654
  ' display: grid; grid-template-columns: 120px minmax(0,1fr) auto; align-items: center; gap: 10px;',
1562
1655
  ' padding: 11px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
1563
1656
  '}',
1657
+ // Task 147: same band as the credential row, above the model list, so a
1658
+ // custom provider's endpoint reads as part of the provider, not a model.
1659
+ '.cumulus-settings-provider-endpoint {',
1660
+ ' display: grid; grid-template-columns: 120px minmax(0,1fr) auto; align-items: center; gap: 10px;',
1661
+ ' padding: 10px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
1662
+ '}',
1663
+ '.cumulus-settings-provider-endpoint label { color: #9a9da5; font-size: 11.5px; }',
1564
1664
  '.cumulus-settings-credential-note {',
1565
1665
  ' color: #7d808a; font-size: 11.5px; padding: 11px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
1566
1666
  '}',
@@ -1619,6 +1719,8 @@
1619
1719
  ' .cumulus-settings-add-row .cumulus-settings-add-btn { grid-column: 1 / -1; }',
1620
1720
  ' .cumulus-settings-provider-credential { grid-template-columns: 1fr auto; gap: 5px; }',
1621
1721
  ' .cumulus-settings-provider-credential label { grid-column: 1 / -1; }',
1722
+ ' .cumulus-settings-provider-endpoint { grid-template-columns: 1fr auto; gap: 5px; }',
1723
+ ' .cumulus-settings-provider-endpoint label { grid-column: 1 / -1; }',
1622
1724
  ' .cumulus-settings-default-card { flex-direction: column; align-items: stretch; gap: 6px; }',
1623
1725
  ' .cumulus-settings-edit-grid { grid-template-columns: 1fr 1fr; }',
1624
1726
  '}',
@@ -6392,16 +6494,8 @@
6392
6494
  var addProvider = document.createElement('select');
6393
6495
  addProvider.className = 'cumulus-settings-select';
6394
6496
  addProvider.setAttribute('data-testid', 'webchat-gw-add-provider');
6395
- [
6396
- { value: 'anthropic', label: 'Anthropic' },
6397
- { value: 'openai', label: 'OpenAI' },
6398
- { value: 'huggingface', label: 'HuggingFace' },
6399
- ].forEach(function (provider) {
6400
- var option = document.createElement('option');
6401
- option.value = provider.value;
6402
- option.textContent = provider.label;
6403
- addProvider.appendChild(option);
6404
- });
6497
+ // Options are built by renderAddProviderOptions() from providerDefs — the
6498
+ // one list that also drives the cards and the default dropdown (task 147).
6405
6499
  var addId = document.createElement('input');
6406
6500
  addId.type = 'text';
6407
6501
  addId.className = 'cumulus-settings-input';
@@ -6432,6 +6526,51 @@
6432
6526
  addCard.appendChild(addWrap);
6433
6527
  body.appendChild(addCard);
6434
6528
 
6529
+ // ── Custom OpenAI-compatible providers (task 147) ────────────────────────
6530
+ // One mechanism for every third-party host that speaks chat/completions:
6531
+ // Vultr, Together, Groq, Fireworks, vLLM, LM Studio, Ollama. Each carries
6532
+ // its own credential — the OpenAI/HuggingFace keys belong to those hosts.
6533
+ var customCard = document.createElement('div');
6534
+ customCard.className = 'cumulus-settings-add-card';
6535
+ var customTitle = document.createElement('div');
6536
+ customTitle.className = 'cumulus-settings-add-title';
6537
+ customTitle.textContent = 'Add a custom provider';
6538
+ customCard.appendChild(customTitle);
6539
+ var customHint = document.createElement('div');
6540
+ customHint.className = 'cumulus-settings-subtitle';
6541
+ customHint.textContent =
6542
+ 'Any OpenAI-compatible endpoint. Paste the base URL from its docs — /chat/completions is added if missing.';
6543
+ customCard.appendChild(customHint);
6544
+ var customWrap = document.createElement('div');
6545
+ customWrap.className = 'cumulus-settings-add-row';
6546
+ var customLabelInput = document.createElement('input');
6547
+ customLabelInput.type = 'text';
6548
+ customLabelInput.className = 'cumulus-settings-input';
6549
+ customLabelInput.placeholder = 'Name (e.g. Vultr)';
6550
+ customLabelInput.setAttribute('data-testid', 'webchat-gw-addprovider-label');
6551
+ var customUrlInput = document.createElement('input');
6552
+ customUrlInput.type = 'text';
6553
+ customUrlInput.className = 'cumulus-settings-input';
6554
+ customUrlInput.placeholder = 'https://api.example.com/v1';
6555
+ customUrlInput.setAttribute('data-testid', 'webchat-gw-addprovider-url');
6556
+ var customKeyInput = document.createElement('input');
6557
+ customKeyInput.type = 'password';
6558
+ customKeyInput.autocomplete = 'off';
6559
+ customKeyInput.className = 'cumulus-settings-input';
6560
+ customKeyInput.placeholder = 'API key';
6561
+ customKeyInput.setAttribute('data-testid', 'webchat-gw-addprovider-key');
6562
+ var customAddBtn = document.createElement('button');
6563
+ customAddBtn.type = 'button';
6564
+ customAddBtn.textContent = '+ Add';
6565
+ customAddBtn.className = 'cumulus-settings-add-btn';
6566
+ customAddBtn.setAttribute('data-testid', 'webchat-gw-addprovider-btn');
6567
+ customWrap.appendChild(customLabelInput);
6568
+ customWrap.appendChild(customUrlInput);
6569
+ customWrap.appendChild(customKeyInput);
6570
+ customWrap.appendChild(customAddBtn);
6571
+ customCard.appendChild(customWrap);
6572
+ body.appendChild(customCard);
6573
+
6435
6574
  // ── License (task 129) — persist-only: the key lands in gateway.config.json
6436
6575
  // but enforcement re-verifies at startup, so it takes effect on reload (127).
6437
6576
  var licenseLabel = document.createElement('div');
@@ -6494,7 +6633,7 @@
6494
6633
  });
6495
6634
  document.body.appendChild(backdrop);
6496
6635
 
6497
- var gwState = { catalog: [], sentinel: null };
6636
+ var gwState = { catalog: [], sentinel: null, customProviders: [] };
6498
6637
  var credentialInputs = {};
6499
6638
  var credentialValues = { openai: '', huggingface: '', license: '' };
6500
6639
  var credentialClears = { openai: false, huggingface: false, license: false };
@@ -6526,27 +6665,45 @@
6526
6665
  });
6527
6666
  var gwLicenseMasked = '';
6528
6667
  var gwLicenseInfo = null;
6529
- var providerDefs = [
6530
- {
6531
- id: 'anthropic',
6532
- title: 'Anthropic',
6533
- description: 'Claude CLI models · authentication is managed by the Claude CLI',
6534
- },
6535
- {
6536
- id: 'openai',
6537
- title: 'OpenAI',
6538
- description: 'Responses API models with direct tool use',
6539
- credential: 'openaiApiKey',
6540
- credentialTestId: 'webchat-gw-openaikey',
6541
- },
6542
- {
6543
- id: 'huggingface',
6544
- title: 'HuggingFace',
6545
- description: 'OpenAI-compatible models served through the HuggingFace router',
6546
- credential: 'hfApiKey',
6547
- credentialTestId: 'webchat-gw-hfkey',
6548
- },
6549
- ];
6668
+ // Task 147: the three built-in providers are fixed; custom
6669
+ // OpenAI-compatible endpoints are appended from the registry. Cards, the
6670
+ // default dropdown, the add-model select and the credential rows all read
6671
+ // this ONE list, so they cannot disagree about which providers exist.
6672
+ var providerDefs = [];
6673
+ function buildProviderDefs() {
6674
+ var defs = [
6675
+ {
6676
+ id: 'anthropic',
6677
+ title: 'Anthropic',
6678
+ description: 'Claude CLI models · authentication is managed by the Claude CLI',
6679
+ },
6680
+ {
6681
+ id: 'openai',
6682
+ title: 'OpenAI',
6683
+ description: 'Responses API models with direct tool use',
6684
+ credential: 'openaiApiKey',
6685
+ credentialTestId: 'webchat-gw-openaikey',
6686
+ },
6687
+ {
6688
+ id: 'huggingface',
6689
+ title: 'HuggingFace',
6690
+ description: 'OpenAI-compatible models served through the HuggingFace router',
6691
+ credential: 'hfApiKey',
6692
+ credentialTestId: 'webchat-gw-hfkey',
6693
+ },
6694
+ ];
6695
+ (gwState.customProviders || []).forEach(function (provider) {
6696
+ defs.push({
6697
+ id: 'custom:' + provider.id,
6698
+ title: provider.label || provider.id,
6699
+ description: 'Custom OpenAI-compatible endpoint',
6700
+ credential: 'custom',
6701
+ credentialTestId: 'webchat-gw-customkey-' + provider.id,
6702
+ custom: provider,
6703
+ });
6704
+ });
6705
+ return defs;
6706
+ }
6550
6707
 
6551
6708
  function setSettingsStatus(message, kind) {
6552
6709
  gwStatus.textContent = message || '';
@@ -6714,6 +6871,59 @@
6714
6871
  return row;
6715
6872
  }
6716
6873
 
6874
+ // Task 147: a custom provider owns an endpoint and can be removed. Removal
6875
+ // is REFUSED while models still reference it — an orphaned `custom:<id>`
6876
+ // resolves fail-closed to the Claude path, so the model would silently run
6877
+ // somewhere else instead of erroring.
6878
+ function renderCustomProviderControls(card, providerDef) {
6879
+ var row = document.createElement('div');
6880
+ row.className = 'cumulus-settings-provider-endpoint';
6881
+ var label = document.createElement('label');
6882
+ label.textContent = 'Base URL';
6883
+ var input = document.createElement('input');
6884
+ input.type = 'text';
6885
+ input.className = 'cumulus-settings-input';
6886
+ input.value = providerDef.custom.baseUrl || '';
6887
+ input.placeholder = 'https://api.example.com/v1';
6888
+ input.setAttribute('aria-label', providerDef.title + ' base URL');
6889
+ input.setAttribute('data-testid', 'webchat-gw-customurl-' + providerDef.custom.id);
6890
+ input.addEventListener('input', function () {
6891
+ providerDef.custom.baseUrl = input.value;
6892
+ });
6893
+ var remove = document.createElement('button');
6894
+ remove.type = 'button';
6895
+ remove.className = 'cumulus-settings-clear-btn';
6896
+ remove.textContent = 'Remove provider';
6897
+ remove.setAttribute('data-testid', 'webchat-gw-removeprovider-' + providerDef.custom.id);
6898
+ remove.addEventListener('click', function () {
6899
+ var inUse = gwState.catalog.filter(function (model) {
6900
+ return model.provider === providerDef.id;
6901
+ });
6902
+ if (inUse.length) {
6903
+ setSettingsStatus(
6904
+ 'Remove its ' +
6905
+ inUse.length +
6906
+ ' model' +
6907
+ (inUse.length === 1 ? '' : 's') +
6908
+ ' first — they would stop running on this endpoint',
6909
+ 'error'
6910
+ );
6911
+ return;
6912
+ }
6913
+ gwState.customProviders = gwState.customProviders.filter(function (entry) {
6914
+ return entry.id !== providerDef.custom.id;
6915
+ });
6916
+ delete credentialValues[providerDef.id];
6917
+ delete credentialClears[providerDef.id];
6918
+ setSettingsStatus('', '');
6919
+ renderAll();
6920
+ });
6921
+ row.appendChild(label);
6922
+ row.appendChild(input);
6923
+ row.appendChild(remove);
6924
+ card.appendChild(row);
6925
+ }
6926
+
6717
6927
  function renderCredential(card, provider) {
6718
6928
  if (!provider.credential) {
6719
6929
  var note = document.createElement('div');
@@ -6787,6 +6997,7 @@
6787
6997
  head.appendChild(heading);
6788
6998
  head.appendChild(count);
6789
6999
  card.appendChild(head);
7000
+ if (provider.custom) renderCustomProviderControls(card, provider);
6790
7001
  var list = document.createElement('div');
6791
7002
  list.className = 'cumulus-settings-provider-models';
6792
7003
  if (!providerModels.length) {
@@ -6865,7 +7076,27 @@
6865
7076
  defaultSelect.value = current ? current.id : gwState.catalog[0].id;
6866
7077
  }
6867
7078
 
7079
+ // Rebuild the provider list from state before anything reads it, so a
7080
+ // provider added or removed this session reaches the cards, the default
7081
+ // dropdown and the add-model select in the same pass (task 147).
7082
+ function renderAddProviderOptions() {
7083
+ var previous = addProvider.value;
7084
+ addProvider.innerHTML = '';
7085
+ providerDefs.forEach(function (provider) {
7086
+ var option = document.createElement('option');
7087
+ option.value = provider.id;
7088
+ option.textContent = provider.title;
7089
+ addProvider.appendChild(option);
7090
+ });
7091
+ addProvider.value = previous || 'anthropic';
7092
+ if (!addProvider.value) addProvider.value = 'anthropic';
7093
+ addContext.disabled = addProvider.value === 'anthropic';
7094
+ if (addContext.disabled) addContext.value = '';
7095
+ }
7096
+
6868
7097
  function renderAll() {
7098
+ providerDefs = buildProviderDefs();
7099
+ renderAddProviderOptions();
6869
7100
  renderDefaultSelect();
6870
7101
  renderGwModels();
6871
7102
  }
@@ -6874,6 +7105,41 @@
6874
7105
  addContext.disabled = addProvider.value === 'anthropic';
6875
7106
  if (addContext.disabled) addContext.value = '';
6876
7107
  });
7108
+
7109
+ customAddBtn.addEventListener('click', function () {
7110
+ var label = (customLabelInput.value || '').trim();
7111
+ var baseUrl = (customUrlInput.value || '').trim();
7112
+ var id = slugifyProviderId(label);
7113
+ if (!label || !id) {
7114
+ setSettingsStatus('Provider name is required (letters or digits)', 'error');
7115
+ return;
7116
+ }
7117
+ if (!/^https?:\/\/\S+$/.test(baseUrl)) {
7118
+ setSettingsStatus('Base URL must start with http:// or https://', 'error');
7119
+ return;
7120
+ }
7121
+ if (
7122
+ gwState.customProviders.some(function (provider) {
7123
+ return provider.id === id;
7124
+ })
7125
+ ) {
7126
+ setSettingsStatus('A provider named "' + label + '" already exists', 'error');
7127
+ return;
7128
+ }
7129
+ gwState.customProviders.push({ id: id, label: label, baseUrl: baseUrl, maskedKey: '' });
7130
+ // Key is held in the same credential maps as the built-in providers, so
7131
+ // it rides the one save path and the one masking rule.
7132
+ var key = (customKeyInput.value || '').trim();
7133
+ if (key) {
7134
+ credentialValues['custom:' + id] = key;
7135
+ credentialClears['custom:' + id] = false;
7136
+ }
7137
+ customLabelInput.value = '';
7138
+ customUrlInput.value = '';
7139
+ customKeyInput.value = '';
7140
+ setSettingsStatus('', '');
7141
+ renderAll();
7142
+ });
6877
7143
  addBtn.addEventListener('click', function () {
6878
7144
  var id = (addId.value || '').trim();
6879
7145
  var provider = addProvider.value;
@@ -6915,6 +7181,28 @@
6915
7181
  renderAll();
6916
7182
  });
6917
7183
 
7184
+ // One place that turns a GET/PUT /api/config response into editor state,
7185
+ // so the load and save paths cannot drift on credential seeding (Rule #8).
7186
+ function adoptGatewayConfigResponse(response) {
7187
+ gwState = gatewayConfigToModelCatalog(response);
7188
+ credentialValues.openai = response.openaiApiKey || '';
7189
+ credentialValues.huggingface = response.hfApiKey || '';
7190
+ credentialValues.license = response.licenseKey || '';
7191
+ credentialClears.openai = false;
7192
+ credentialClears.huggingface = false;
7193
+ credentialClears.license = false;
7194
+ // Custom-provider keys arrive MASKED inside their registry entries. Seed
7195
+ // them the same way as the scalars: a masked value is ignored on save,
7196
+ // so an untouched key is preserved rather than read as "cleared".
7197
+ gwState.customProviders.forEach(function (provider) {
7198
+ var key = 'custom:' + provider.id;
7199
+ credentialValues[key] = provider.maskedKey || '';
7200
+ credentialClears[key] = false;
7201
+ });
7202
+ gwLicenseMasked = response.licenseKey || '';
7203
+ gwLicenseInfo = response.license || null;
7204
+ }
7205
+
6918
7206
  function refreshOpenModelCatalogs() {
6919
7207
  var refresh = new XMLHttpRequest();
6920
7208
  refresh.open('GET', '/api/models');
@@ -6935,14 +7223,16 @@
6935
7223
  }
6936
7224
 
6937
7225
  saveBtn.addEventListener('click', function () {
6938
- var validationError = validateModelCatalog(gwState.catalog);
7226
+ var validationError =
7227
+ validateCustomProviderList(gwState.customProviders || []) ||
7228
+ validateModelCatalog(gwState.catalog, gwState.customProviders || []);
6939
7229
  if (validationError) {
6940
7230
  setSettingsStatus(validationError, 'error');
6941
7231
  return;
6942
7232
  }
6943
7233
  saveBtn.disabled = true;
6944
7234
  setSettingsStatus('Saving…', '');
6945
- var payload = addCredentialPatch(modelCatalogToGatewayConfig(gwState), {
7235
+ var credentialMap = {
6946
7236
  openai: {
6947
7237
  value: credentialValues.openai,
6948
7238
  clear: credentialClears.openai,
@@ -6955,7 +7245,15 @@
6955
7245
  value: credentialValues.license,
6956
7246
  clear: credentialClears.license,
6957
7247
  },
7248
+ };
7249
+ (gwState.customProviders || []).forEach(function (provider) {
7250
+ var key = 'custom:' + provider.id;
7251
+ credentialMap[key] = {
7252
+ value: credentialValues[key],
7253
+ clear: credentialClears[key],
7254
+ };
6958
7255
  });
7256
+ var payload = addCredentialPatch(modelCatalogToGatewayConfig(gwState), credentialMap);
6959
7257
  var save = new XMLHttpRequest();
6960
7258
  save.open('PUT', '/api/config');
6961
7259
  save.setRequestHeader('Content-Type', 'application/json');
@@ -6966,15 +7264,7 @@
6966
7264
  if (save.status === 200) {
6967
7265
  try {
6968
7266
  var response = JSON.parse(save.responseText);
6969
- gwState = gatewayConfigToModelCatalog(response);
6970
- credentialClears.openai = false;
6971
- credentialClears.huggingface = false;
6972
- credentialClears.license = false;
6973
- credentialValues.openai = response.openaiApiKey || '';
6974
- credentialValues.huggingface = response.hfApiKey || '';
6975
- credentialValues.license = response.licenseKey || '';
6976
- gwLicenseMasked = response.licenseKey || '';
6977
- gwLicenseInfo = response.license || null;
7267
+ adoptGatewayConfigResponse(response);
6978
7268
  renderAll();
6979
7269
  renderLicenseState(gwLicenseMasked, gwLicenseInfo);
6980
7270
  setSettingsStatus(
@@ -6994,6 +7284,10 @@
6994
7284
  save.send(JSON.stringify(payload));
6995
7285
  });
6996
7286
 
7287
+ // First paint before the config arrives: the three built-in providers are
7288
+ // known without the server, so the shell renders immediately.
7289
+ renderAll();
7290
+
6997
7291
  (function loadGatewayConfig() {
6998
7292
  var load = new XMLHttpRequest();
6999
7293
  load.open('GET', '/api/config');
@@ -7003,12 +7297,7 @@
7003
7297
  if (load.status === 200) {
7004
7298
  try {
7005
7299
  var response = JSON.parse(load.responseText);
7006
- gwState = gatewayConfigToModelCatalog(response);
7007
- credentialValues.openai = response.openaiApiKey || '';
7008
- credentialValues.huggingface = response.hfApiKey || '';
7009
- credentialValues.license = response.licenseKey || '';
7010
- gwLicenseMasked = response.licenseKey || '';
7011
- gwLicenseInfo = response.license || null;
7300
+ adoptGatewayConfigResponse(response);
7012
7301
  renderAll();
7013
7302
  renderLicenseState(gwLicenseMasked, gwLicenseInfo);
7014
7303
  } catch (e) {
@@ -7118,9 +7407,18 @@
7118
7407
  // Model id → provider ("huggingface" | "openai" | "cli"), filled by loadModels
7119
7408
  // from /api/models so the breadcrumb names the real provider (task 118).
7120
7409
  var modelProviders = {};
7410
+ // Custom-provider id → display label, from /api/models (task 147). That
7411
+ // endpoint serves id + label only — never the base URL, which is private
7412
+ // infrastructure and it accepts namespace-scoped app keys (097 P7).
7413
+ var customProviderLabels = {};
7121
7414
  function providerLabel(id) {
7122
7415
  if (id === 'claude') return 'Anthropic';
7123
- if (modelProviders[id] === 'openai') return 'OpenAI';
7416
+ var provider = modelProviders[id];
7417
+ if (isCustomProvider(provider)) {
7418
+ var customId = provider.slice('custom:'.length);
7419
+ return customProviderLabels[customId] || customId;
7420
+ }
7421
+ if (provider === 'openai') return 'OpenAI';
7124
7422
  return 'HuggingFace';
7125
7423
  }
7126
7424
  function updateBreadcrumb() {
@@ -7153,6 +7451,10 @@
7153
7451
  var previousModel = preserveSelection ? modelSelect.value : '';
7154
7452
  var previousClaudeModel = preserveSelection ? claudeModelSelect.value : '';
7155
7453
  modelProviders = {};
7454
+ customProviderLabels = {};
7455
+ (response.customProviders || []).forEach(function (provider) {
7456
+ customProviderLabels[provider.id] = provider.label || provider.id;
7457
+ });
7156
7458
  modelSelect.innerHTML = '';
7157
7459
  (response.models || []).forEach(function (model) {
7158
7460
  var option = document.createElement('option');
@@ -122,5 +122,15 @@ export interface ResumeState {
122
122
  /** Turns completed so far. */
123
123
  turns: number;
124
124
  }
125
+ /**
126
+ * Text carried by a response's content blocks.
127
+ *
128
+ * The loop otherwise builds its answer from streamed `text` events, which never
129
+ * include reasoning — providers accumulate that channel separately and only fold it
130
+ * into `content` as a last-resort text block (`huggingface-provider.ts`:
131
+ * `fullText || reasoningText`). Reading the blocks is what makes that fallback
132
+ * reachable at all; before task 150 it was dead code for every agentic turn.
133
+ */
134
+ export declare function textFromContent(content: Anthropic.ContentBlock[] | undefined): string;
125
135
  export declare function runAgenticLoop(options: AgenticLoopOptions): Promise<AgenticLoopResult>;
126
136
  //# sourceMappingURL=agentic-loop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agentic-loop.d.ts","sourceRoot":"","sources":["../../src/lib/agentic-loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agentic-loop.d.ts","sourceRoot":"","sources":["../../src/lib/agentic-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAC;AAG/C,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAe,MAAM,eAAe,CAAC;AAE3F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAK7C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,WAAW,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;IAClG,6CAA6C;IAC7C,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,sFAAsF;IACtF,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACnE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACxE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6GAA6G;IAC7G,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,cAAc,CAAC;IAC3B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC/B,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE;QACL,gBAAgB,EAAE,MAAM,CAAC;QACzB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,mBAAmB,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,uFAAuF;IACvF,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED,oDAAoD;AACpD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,oDAAoD;IACpD,aAAa,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,sFAAsF;IACtF,QAAQ,EAAE,SAAS,CAAC,YAAY,EAAE,CAAC;IACnC,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,SAAS,EAAE,KAAK,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC/B,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH,kFAAkF;IAClF,mBAAmB,EAAE,SAAS,CAAC,oBAAoB,EAAE,CAAC;IACtD,wCAAwC;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,gCAAgC;IAChC,KAAK,EAAE;QACL,gBAAgB,EAAE,MAAM,CAAC;QACzB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,mBAAmB,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAoBD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC,YAAY,EAAE,GAAG,SAAS,GAAG,MAAM,CAMrF;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoiB5F"}