@chatpanel/gateway 0.6.45 → 0.6.46

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +20 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.45",
3
+ "version": "0.6.46",
4
4
  "description": "Local privacy gateway \u2014 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/server.js CHANGED
@@ -49,7 +49,7 @@ import * as openai from './openai.js';
49
49
  import * as responses from './responses.js';
50
50
  import * as anthropic from './anthropic.js';
51
51
 
52
- export const VERSION = '0.6.45';
52
+ export const VERSION = '0.6.46';
53
53
 
54
54
  // WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
55
55
  // store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
@@ -429,6 +429,24 @@ async function handleBridge(req, res, { kind, adapter, redactable, pathname, age
429
429
 
430
430
  // ---- backend: api ----------------------------------------------------------
431
431
 
432
+ // Join a destination's base URL to the incoming path WITHOUT doubling the API version.
433
+ //
434
+ // Every OpenAI-compatible provider tells you to paste a base that already ends at the version
435
+ // — https://integrate.api.nvidia.com/v1, https://openrouter.ai/api/v1, https://router.hugging
436
+ // face.co/v1 — and the request arriving here carries the version too (/v1/chat/completions).
437
+ // Concatenating them produced /v1/v1/chat/completions, and what came back was the provider's
438
+ // own "404 page not found". That reads like a broken gateway, or a wrong model, or a dead
439
+ // channel — anything except the mis-joined URL it actually was.
440
+ //
441
+ // Matching on the leading segment rather than hardcoding "v1" so a provider on /v2 or a beta
442
+ // path is joined correctly too.
443
+ export function joinUpstream(base, pathname, search = '') {
444
+ const b = String(base || '').replace(/\/+$/, '');
445
+ const seg = String(pathname || '').split('/')[1];
446
+ if (seg && b.endsWith(`/${seg}`)) return b.slice(0, -(seg.length + 1)) + pathname + search;
447
+ return b + pathname + search;
448
+ }
449
+
432
450
  async function handleApi(req, res, { adapter, kind, pathname, search, base, destKey, destProtocol, harness, trace }, outBody, vault) {
433
451
  let upstream;
434
452
  const up0 = trace ? trace.clock() : 0;
@@ -443,7 +461,7 @@ async function handleApi(req, res, { adapter, kind, pathname, search, base, dest
443
461
  // SSRF guard on the config-supplied upstream: block cloud-metadata + non-http(s)
444
462
  // BEFORE the fetch. Loopback/LAN stay allowed (Ollama/LM Studio/homelab are the
445
463
  // point of a BYO gateway); only the credential-theft pivot is refused.
446
- const upstreamUrl = assertEndpointUrl(base.replace(/\/$/, '') + pathname + search).toString();
464
+ const upstreamUrl = assertEndpointUrl(joinUpstream(base, pathname, search)).toString();
447
465
  upstream = await fetch(upstreamUrl, {
448
466
  method: req.method,
449
467
  headers,