@chatpanel/gateway 0.6.64 → 0.6.65
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 +1 -1
- package/src/router.js +59 -0
- package/src/server.js +32 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.65",
|
|
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
|
@@ -112,6 +112,44 @@ export function aggregateModels(cfg) {
|
|
|
112
112
|
return { object: 'list', data };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The models each installed agent can be asked for.
|
|
117
|
+
*
|
|
118
|
+
* A CLI agent is not one model — Claude Code takes opus/sonnet/haiku, others enumerate their
|
|
119
|
+
* own — and listing only the agent id meant a user picked `claude` and got whatever default
|
|
120
|
+
* the CLI had. When that default is newer than the installed CLI, the answer is a version
|
|
121
|
+
* error about a model the user never chose.
|
|
122
|
+
*
|
|
123
|
+
* Asked of the bridge, which is the only thing that knows what each CLI supports, and only
|
|
124
|
+
* for agents that are actually INSTALLED: enumerating models for a CLI that is not there
|
|
125
|
+
* spends a subprocess per agent to describe something unusable.
|
|
126
|
+
*/
|
|
127
|
+
async function bridgeAgentModels(cfg, installed, timeoutMs) {
|
|
128
|
+
const base = String(cfg?.bridge?.url || '').replace(/\/$/, '');
|
|
129
|
+
if (!base || !installed) return new Map();
|
|
130
|
+
const token = readBridgeToken(cfg.bridge?.token);
|
|
131
|
+
const ids = [...installed.entries()].filter(([, ok]) => ok).map(([id]) => id);
|
|
132
|
+
const out = new Map();
|
|
133
|
+
await Promise.all(ids.map(async (id) => {
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch(`${base}/list-models`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'content-type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
|
138
|
+
body: JSON.stringify({ agent: id }),
|
|
139
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) return;
|
|
142
|
+
const body = await res.json();
|
|
143
|
+
const models = (Array.isArray(body?.models) ? body.models : [])
|
|
144
|
+
.map((m) => (typeof m === 'string' ? m : m?.id || m?.name || ''))
|
|
145
|
+
.map((m) => String(m).trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
if (models.length) out.set(id, models.slice(0, 40));
|
|
148
|
+
} catch { /* an agent that will not enumerate still works under its bare id */ }
|
|
149
|
+
}));
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
115
153
|
/** id → installed, from the bridge's own /health. `null` when it could not be asked. */
|
|
116
154
|
async function bridgeAgentAvailability(cfg, timeoutMs) {
|
|
117
155
|
const base = String(cfg?.bridge?.url || '').replace(/\/$/, '');
|
|
@@ -158,6 +196,27 @@ export async function aggregateModelsAsync(cfg, { timeoutMs = 4000 } = {}) {
|
|
|
158
196
|
}
|
|
159
197
|
}
|
|
160
198
|
|
|
199
|
+
// Each installed agent's own models, listed as `agent/model` beside the bare id. The bare
|
|
200
|
+
// id stays and still means "the agent's default", so nothing that already works breaks.
|
|
201
|
+
const agentModels = await bridgeAgentModels(cfg, agentAvailability, timeoutMs);
|
|
202
|
+
for (const [agent, models] of agentModels) {
|
|
203
|
+
const parent = base.data.find((m) => m.id === agent);
|
|
204
|
+
if (!parent) continue;
|
|
205
|
+
for (const model of models) {
|
|
206
|
+
const id = `${agent}/${model}`;
|
|
207
|
+
if (seen.has(id)) continue;
|
|
208
|
+
seen.add(id);
|
|
209
|
+
base.data.push({
|
|
210
|
+
...parent,
|
|
211
|
+
id,
|
|
212
|
+
// `model` is what the picker shows under the agent's heading; the agent stays the
|
|
213
|
+
// provider, so the grouping puts them together without any id parsing.
|
|
214
|
+
model,
|
|
215
|
+
available: true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
161
220
|
const dests = listDestinations(cfg).filter((d) => d.type === 'api' && d.baseUrl);
|
|
162
221
|
await Promise.all(dests.map(async (d) => {
|
|
163
222
|
const ctrl = new AbortController();
|
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.
|
|
58
|
+
export const VERSION = '0.6.65';
|
|
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.
|
|
@@ -204,8 +204,31 @@ function route(pathname, headers, cfg) {
|
|
|
204
204
|
return { kind: 'openai', adapter: openai, redactable: openai.matches(pathname), base: cfg.upstreams?.openai?.baseUrl };
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* An agent id, and the model to run it with.
|
|
209
|
+
*
|
|
210
|
+
* A CLI agent is not one model. Claude Code takes `opus`, `sonnet` or `haiku`; the others
|
|
211
|
+
* have their own lists. Picking `claude` alone leaves the CLI on whatever default it has,
|
|
212
|
+
* which is how a user gets "this version does not support that model" from a tool they never
|
|
213
|
+
* chose a model for. So `claude/opus` names both, and the slash is the only new syntax.
|
|
214
|
+
*
|
|
215
|
+
* A bare agent id still works and still means "the agent's own default" — every client that
|
|
216
|
+
* predates this keeps working, which is the whole reason the model is a SUFFIX rather than a
|
|
217
|
+
* change to the id.
|
|
218
|
+
*/
|
|
219
|
+
export function parseAgentModel(model, cfg) {
|
|
220
|
+
const raw = String(model || '');
|
|
221
|
+
if (KNOWN_AGENTS.has(raw)) return { agent: raw, agentModel: '' };
|
|
222
|
+
const slash = raw.indexOf('/');
|
|
223
|
+
if (slash > 0) {
|
|
224
|
+
const head = raw.slice(0, slash);
|
|
225
|
+
if (KNOWN_AGENTS.has(head)) return { agent: head, agentModel: raw.slice(slash + 1) };
|
|
226
|
+
}
|
|
227
|
+
return { agent: cfg.bridge.agent, agentModel: '' };
|
|
228
|
+
}
|
|
229
|
+
|
|
207
230
|
function pickAgent(model, cfg) {
|
|
208
|
-
return
|
|
231
|
+
return parseAgentModel(model, cfg).agent;
|
|
209
232
|
}
|
|
210
233
|
|
|
211
234
|
// A follow-up request carrying a tool result for a PARKED relay session. Such a
|
|
@@ -393,7 +416,13 @@ async function handleBridge(req, res, { kind, adapter, redactable, pathname, age
|
|
|
393
416
|
const ac = new AbortController();
|
|
394
417
|
req.on('close', () => ac.abort());
|
|
395
418
|
|
|
396
|
-
|
|
419
|
+
// The model half of `claude/opus`, handed to the CLI as its `--model`. Absent for a bare
|
|
420
|
+
// agent id, which leaves the agent on its own default exactly as before.
|
|
421
|
+
const { agentModel } = parseAgentModel(body?.model, cfg);
|
|
422
|
+
const turn = {
|
|
423
|
+
bridgeUrl: cfg.bridge.url, agent, token, messages, system, signal: ac.signal,
|
|
424
|
+
...(agentModel ? { options: { model: agentModel } } : {}),
|
|
425
|
+
};
|
|
397
426
|
|
|
398
427
|
if (!wantStream) {
|
|
399
428
|
try {
|