@chatpanel/gateway 0.6.65 → 0.6.66

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.65",
3
+ "version": "0.6.66",
4
4
  "description": "Local privacy gateway — redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/router.js CHANGED
@@ -86,6 +86,32 @@ function apiShapeOf(d) {
86
86
  return { api: 'openai', endpoints: ['/v1/chat/completions', '/v1/responses'] };
87
87
  }
88
88
 
89
+ /**
90
+ * CAN THIS DESTINATION ANSWER AT ALL — before anyone sends a turn to it.
91
+ *
92
+ * This list is a ROUTING TABLE. It names every model the gateway would forward to, and a
93
+ * provider that lists four hundred can be configured with no key at all — so the list is
94
+ * mostly names that cannot answer. A user picks one, and finds out at 08:00 when a scheduled
95
+ * job comes back `Missing Authentication header` or `upstream fetch failed`, quoting undici
96
+ * about a destination they never knowingly chose.
97
+ *
98
+ * Local endpoints are the exception that must NOT be marked unconfigured: llama.cpp, Ollama
99
+ * and LM Studio take no key, and greying them out would hide the one setup that needs
100
+ * nothing. So this asks only the question it can answer honestly — "is a credential
101
+ * required here, and is one saved" — and says nothing about whether the server is up.
102
+ *
103
+ * Returns `''` when there is nothing to report.
104
+ */
105
+ const LOCAL_HOST = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0|[^/]*\.local)(:|\/|$)/i;
106
+ function unconfiguredReason(d) {
107
+ if (!d || d.type === 'agent') return '';
108
+ const base = String(d.baseUrl || '');
109
+ if (!base) return 'no endpoint URL is set for this provider';
110
+ if (LOCAL_HOST.test(base)) return ''; // a local server needs no key
111
+ if (d.apiKey || d.hasKey) return '';
112
+ return `no API key is saved for ${d.id}`;
113
+ }
114
+
89
115
  export function aggregateModels(cfg) {
90
116
  const data = [];
91
117
  const seen = new Set();
@@ -93,10 +119,15 @@ export function aggregateModels(cfg) {
93
119
  if (!id || seen.has(id)) return;
94
120
  seen.add(id);
95
121
  const shape = apiShapeOf(d);
122
+ const blocked = unconfiguredReason(d);
96
123
  data.push({
97
124
  id,
98
125
  object: 'model',
99
126
  owned_by: owner,
127
+ // `configured` is about SETUP, not liveness: false means a turn sent here is known to
128
+ // fail for a reason the user can fix in Settings. Absent liveness is deliberate — the
129
+ // gateway does not probe every provider to draw a list.
130
+ ...(blocked ? { configured: false, reason: blocked } : {}),
100
131
  // Additive fields an OpenAI client ignores and a ChatPanel client uses to decide how
101
132
  // to call, and to group a picker by provider instead of by a flat list of ids.
102
133
  provider: d.id,
@@ -237,10 +268,12 @@ export async function aggregateModelsAsync(cfg, { timeoutMs = 4000 } = {}) {
237
268
  if (id && !seen.has(id)) {
238
269
  seen.add(id);
239
270
  const shape = apiShapeOf(d);
271
+ const blocked = unconfiguredReason(d);
240
272
  base.data.push({
241
273
  id, object: 'model', owned_by: d.id, provider: d.id,
242
274
  provider_type: d.protocol === 'anthropic' ? 'anthropic' : 'openai',
243
275
  api: shape.api, endpoints: shape.endpoints,
276
+ ...(blocked ? { configured: false, reason: blocked } : {}),
244
277
  });
245
278
  }
246
279
  }
package/src/server.js CHANGED
@@ -55,7 +55,7 @@ import * as openai from './openai.js';
55
55
  import * as responses from './responses.js';
56
56
  import * as anthropic from './anthropic.js';
57
57
 
58
- export const VERSION = '0.6.65';
58
+ export const VERSION = '0.6.66';
59
59
 
60
60
  // WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
61
61
  // store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.