mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 12:28:23 +00:00
routstr-core reduces a path to its allowlist key by stripping exactly one optional `v1/` segment, then requires an exact endpoint match. `/v1/messages` and `/messages` therefore reach the same handler, but `/v1/v1/messages` canonicalizes to `v1/messages` and is answered with a 404. A client whose base URL already ends in `/v1` while its transport appends `/v1` itself produces that doubled prefix on every request — the Anthropic SDKs add `/v1/messages`, the OpenAI ones add only their endpoint. Each such request became a paid upstream 404 on every provider in the pool (and, with the SDK's current failure accounting, a cooldown strike against each of them). Collapse the repeated segment before any routing or payment decision. Only the duplicated `v1` is touched: the node owns the endpoint allowlist, so the ingress must not rewrite other spellings into routes it knows nothing about.
40 lines
1.8 KiB
TypeScript
40 lines
1.8 KiB
TypeScript
/**
|
|
* Request-path canonicalization for the proxied path.
|
|
*
|
|
* routstr-core reduces an incoming path to its allowlist key by stripping
|
|
* exactly ONE optional leading `v1/` segment (`_canonical_api_path` in
|
|
* `routstr/proxy.py`), then requires an exact match against its endpoint
|
|
* table. Both `/chat/completions` and `/v1/chat/completions` therefore reach
|
|
* the same handler, and no spelling with the prefix repeated ever does:
|
|
* `/v1/v1/messages` canonicalizes to `v1/messages`, which is not an endpoint,
|
|
* so the node answers `404 Path '/v1/v1/messages' not found`.
|
|
*
|
|
* That second prefix is not hypothetical — it is what an Anthropic-style
|
|
* client produces when its base URL already ends in `/v1`, because the
|
|
* Anthropic SDKs append `/v1/messages` themselves (the OpenAI SDKs append only
|
|
* `/chat/completions` and leave the prefix to the configured base URL). One
|
|
* such client turns every request into a paid upstream 404 on every provider in
|
|
* the pool, so collapse the duplicate here, before any routing or payment
|
|
* decision.
|
|
*/
|
|
|
|
/**
|
|
* Collapse a repeated leading `/v1` segment to a single one.
|
|
*
|
|
* `/v1/v1/messages` and `/v1/v1/v1/messages` both become `/v1/messages`;
|
|
* `/v1/messages`, `/chat/completions`, and every other spelling are returned
|
|
* unchanged. Only the repeated `v1` segment is touched: the node owns the
|
|
* endpoint allowlist, and rewriting anything else here would make this
|
|
* ingress answer for paths it knows nothing about.
|
|
*
|
|
* Never throws and is idempotent: each pass shortens the path, so a path
|
|
* that does not start with a doubled `/v1` is returned as-is.
|
|
*/
|
|
export function collapseDuplicatedV1(pathname: string): string {
|
|
let path = pathname;
|
|
while (path.startsWith("/v1/v1/") || path === "/v1/v1") {
|
|
path = path.slice(3);
|
|
}
|
|
return path;
|
|
}
|