baseURL ending in /v1 causes /v1/models fetch to hit /v1/v1/models (HTTP 404)
Repro
Settings (legacy single-connection form):
llm-newapi:
baseURL: https://www.dogapi.cc/v1 # ← ending in /v1 is the trigger
apiKeyEnv: DOGAPI_API_KEY
flavor: openai-compatible
catalogMode: v1
Boot the profile and try to enumerate the gateway's models. Catalog fetch throws:
LlmError: newapi model list failed (HTTP 404)
at .../dsh-gateway-provider/lib/catalog.js:84 (fetchV1Models)
Root cause
Trace through the legacy single-connection path:
index.js:262 — baseURL.replace(/\/+$/, "") keeps the trailing /v1 (it only strips trailing slashes, not the version segment).
index.js:211 — catalogBase: proto !== undefined ? (proto.catalogBase ?? (ownBase || null)) : baseURL. For the legacy form proto is undefined, so catalogBase = baseURL = "https://www.dogapi.cc/v1".
lib/catalog.js:132 — discoveryBase() returns catalogBase verbatim: "https://www.dogapi.cc/v1".
lib/catalog.js:81 — fetch(\${baseURL}/v1/models`) → **GET https://www.dogapi.cc/v1/v1/models`** → dogapi.cc returns {"error":{"message":"Invalid URL (GET /v1/v1/models)","type":"invalid_request_error"}} → 404.
In other words, the plugin treats baseURL as a bare host when assembling the SDK base URL (pi-provider.js:50-56 appends /v1 if missing), but treats it as an already-versioned base when assembling the catalog URL (catalog.js always appends /v1/models). Two different conventions for the same field, no overlap handling.
The dual-convention appears intentional for apiBases["openai-completions"] etc. — protocols.js:85 does strip /v\d+$ when deriving catalogBase for the multi-protocol form. The legacy form doesn't go through that derivation, so it leaks a versioned baseURL straight into the catalog path.
Suggested fix
In lib/catalog.js, strip the version segment from discoveryBase() output before the catalog appends its suffixes (mirroring what protocols.js:85 already does for the multi-protocol form):
function stripVersion(baseURL) {
return String(baseURL ?? "").replace(/\/+$/, "").replace(/\/v\d+$/, "");
}
function discoveryBase(connection) {
const base = connection.catalogBase !== undefined ? connection.catalogBase : connection.baseURL;
return typeof base === "string" && base.length > 0 ? stripVersion(base) : null;
}
Same trim should apply to the management-API call (fetchManagementModels, line 104) — otherwise catalogMode: management with a /v1 baseURL hits /v1/api/user/models and 404s the same way.
Workaround for users on 1.0.8 today
Drop /v1 from baseURL:
llm-newapi:
baseURL: https://www.dogapi.cc # ← bare host; pi-provider.js appends /v1 for SDK calls
The plugin's chat path (pi-provider.js:sdkBaseURL) already handles appending /v1 for the OpenAI protocols, so this is the canonical form (matches the public default https://api.newapi.ai at index.js:53).
Environment
dsh 0.1.2-rc.1
dsh-gateway-provider 1.0.8
- dogapi.cc (self-hosted newapi fork):
/v1/models returns 401 with a valid request, /v1/v1/models returns 404
- Node 26.8.1, pnpm 11.22.0, macOS
Separate but related: this is bug #2 in a series
Bug #1 (still open in pi-bridge.js): imports CallId from @deepseek-ai/dsh-llm, renamed to ToolCallId in 0.1.2-rc.1 — see #2 (the issue number is for the other bug, title starts with "pi-bridge.js imports CallId"). Once that's fixed upstream the plugin tree will at least load, and then this /v1/v1/models 404 surfaces immediately as the next regression.
baseURLending in/v1causes/v1/modelsfetch to hit/v1/v1/models(HTTP 404)Repro
Settings (legacy single-connection form):
Boot the profile and try to enumerate the gateway's models. Catalog fetch throws:
Root cause
Trace through the legacy single-connection path:
index.js:262—baseURL.replace(/\/+$/, "")keeps the trailing/v1(it only strips trailing slashes, not the version segment).index.js:211—catalogBase: proto !== undefined ? (proto.catalogBase ?? (ownBase || null)) : baseURL. For the legacy formprotoisundefined, socatalogBase = baseURL = "https://www.dogapi.cc/v1".lib/catalog.js:132—discoveryBase()returnscatalogBaseverbatim:"https://www.dogapi.cc/v1".lib/catalog.js:81—fetch(\${baseURL}/v1/models`)→ **GET https://www.dogapi.cc/v1/v1/models`** → dogapi.cc returns{"error":{"message":"Invalid URL (GET /v1/v1/models)","type":"invalid_request_error"}}→ 404.In other words, the plugin treats
baseURLas a bare host when assembling the SDK base URL (pi-provider.js:50-56appends/v1if missing), but treats it as an already-versioned base when assembling the catalog URL (catalog.jsalways appends/v1/models). Two different conventions for the same field, no overlap handling.The dual-convention appears intentional for
apiBases["openai-completions"]etc. —protocols.js:85does strip/v\d+$when derivingcatalogBasefor the multi-protocol form. The legacy form doesn't go through that derivation, so it leaks a versionedbaseURLstraight into the catalog path.Suggested fix
In
lib/catalog.js, strip the version segment fromdiscoveryBase()output before the catalog appends its suffixes (mirroring whatprotocols.js:85already does for the multi-protocol form):Same trim should apply to the management-API call (
fetchManagementModels, line104) — otherwisecatalogMode: managementwith a/v1baseURL hits/v1/api/user/modelsand 404s the same way.Workaround for users on 1.0.8 today
Drop
/v1frombaseURL:The plugin's chat path (
pi-provider.js:sdkBaseURL) already handles appending/v1for the OpenAI protocols, so this is the canonical form (matches the public defaulthttps://api.newapi.aiatindex.js:53).Environment
dsh0.1.2-rc.1dsh-gateway-provider1.0.8/v1/modelsreturns 401 with a valid request,/v1/v1/modelsreturns 404Separate but related: this is bug #2 in a series
Bug #1 (still open in
pi-bridge.js): importsCallIdfrom@deepseek-ai/dsh-llm, renamed toToolCallIdin 0.1.2-rc.1 — see #2 (the issue number is for the other bug, title starts with "pi-bridge.js importsCallId"). Once that's fixed upstream the plugin tree will at least load, and then this/v1/v1/models404 surfaces immediately as the next regression.