@chatpanel/gateway 0.2.1 → 0.2.2

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.2.1",
3
+ "version": "0.2.2",
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
@@ -41,17 +41,44 @@ export function resolveDestination(model, cfg, kind) {
41
41
  );
42
42
  }
43
43
 
44
- // Aggregate every destination's models for GET /v1/models.
44
+ // Aggregate every destination's models for GET /v1/models. Agents expose their
45
+ // own name as the model; APIs expose ONLY real model ids (never the destination
46
+ // id — that's a provider name, not a model).
45
47
  export function aggregateModels(cfg) {
46
48
  const data = [];
47
49
  const seen = new Set();
50
+ const add = (id, owner) => { if (id && !seen.has(id)) { seen.add(id); data.push({ id, object: 'model', owned_by: owner }); } };
48
51
  for (const d of listDestinations(cfg)) {
49
- const models = (Array.isArray(d.models) && d.models.length) ? d.models : [d.id];
50
- for (const m of models) {
51
- if (!m || seen.has(m)) continue;
52
- seen.add(m);
53
- data.push({ id: m, object: 'model', owned_by: d.type === 'agent' ? 'chatpanel-bridge' : (d.id || 'api') });
54
- }
52
+ if (d.type === 'agent') for (const m of (d.models?.length ? d.models : [d.id])) add(m, 'chatpanel-bridge');
53
+ else for (const m of (d.models || [])) add(m, d.id);
55
54
  }
56
55
  return { object: 'list', data };
57
56
  }
57
+
58
+ // Async variant: also PROXIES each API destination's own /v1/models to discover
59
+ // real model ids (using its saved key). Fail-open per destination.
60
+ export async function aggregateModelsAsync(cfg, { timeoutMs = 4000 } = {}) {
61
+ const base = aggregateModels(cfg);
62
+ const seen = new Set(base.data.map((m) => m.id));
63
+ const dests = listDestinations(cfg).filter((d) => d.type === 'api' && d.baseUrl);
64
+ await Promise.all(dests.map(async (d) => {
65
+ const ctrl = new AbortController();
66
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
67
+ try {
68
+ const headers = { 'content-type': 'application/json' };
69
+ if (d.apiKey) {
70
+ if (d.protocol === 'anthropic') { headers['x-api-key'] = d.apiKey; headers['anthropic-version'] = '2023-06-01'; }
71
+ else headers.authorization = `Bearer ${d.apiKey}`;
72
+ }
73
+ const res = await fetch(`${d.baseUrl.replace(/\/$/, '')}/models`, { headers, signal: ctrl.signal });
74
+ if (!res.ok) return;
75
+ const j = await res.json();
76
+ const list = Array.isArray(j?.data) ? j.data : (Array.isArray(j?.models) ? j.models : []);
77
+ for (const m of list) {
78
+ const id = typeof m === 'string' ? m : m?.id;
79
+ if (id && !seen.has(id)) { seen.add(id); base.data.push({ id, object: 'model', owned_by: d.id }); }
80
+ }
81
+ } catch { /* fail-open */ } finally { clearTimeout(t); }
82
+ }));
83
+ return base;
84
+ }
package/src/server.js CHANGED
@@ -27,12 +27,12 @@ import { shaperFor } from './shape.js';
27
27
  import { startNer } from './ner.js';
28
28
  import { resolvePro, meter, usage } from './freegate.js';
29
29
  import { publicConfig, applyConfigPatch, persistConfig, configPath } from './configstore.js';
30
- import { resolveDestination, aggregateModels } from './router.js';
30
+ import { resolveDestination, aggregateModelsAsync } from './router.js';
31
31
  import * as openai from './openai.js';
32
32
  import * as responses from './responses.js';
33
33
  import * as anthropic from './anthropic.js';
34
34
 
35
- export const VERSION = '0.2.1';
35
+ export const VERSION = '0.2.2';
36
36
 
37
37
  const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
38
38
 
@@ -71,12 +71,12 @@ const STARTED_AT = Date.now();
71
71
  // chat endpoint, and (api backend) which upstream base URL.
72
72
  function route(pathname, headers, cfg) {
73
73
  if (anthropic.matches(pathname) || 'anthropic-version' in headers) {
74
- return { kind: 'anthropic', adapter: anthropic, redactable: anthropic.matches(pathname), base: cfg.upstreams.anthropic.baseUrl };
74
+ return { kind: 'anthropic', adapter: anthropic, redactable: anthropic.matches(pathname), base: cfg.upstreams?.anthropic?.baseUrl };
75
75
  }
76
76
  if (responses.matches(pathname)) {
77
- return { kind: 'responses', adapter: responses, redactable: true, base: cfg.upstreams.openai.baseUrl };
77
+ return { kind: 'responses', adapter: responses, redactable: true, base: cfg.upstreams?.openai?.baseUrl };
78
78
  }
79
- return { kind: 'openai', adapter: openai, redactable: openai.matches(pathname), base: cfg.upstreams.openai.baseUrl };
79
+ return { kind: 'openai', adapter: openai, redactable: openai.matches(pathname), base: cfg.upstreams?.openai?.baseUrl };
80
80
  }
81
81
 
82
82
  function pickAgent(model, cfg) {
@@ -253,7 +253,7 @@ export function createGateway(cfg = loadConfig()) {
253
253
 
254
254
  // Model discovery — aggregate every destination's models.
255
255
  if (req.method === 'GET' && /\/models$/.test(pathname)) {
256
- return sendJson(res, 200, aggregateModels(cfg));
256
+ return sendJson(res, 200, await aggregateModelsAsync(cfg));
257
257
  }
258
258
 
259
259
  const r = route(pathname, req.headers, cfg);