mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-22 06:42:19 +03:00
Compare commits
2 Commits
fix/13927-
...
fix/13832-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84f74e1f81 | ||
|
|
bb13494981 |
@@ -0,0 +1 @@
|
||||
- **fix(sse):** when a gateway API key's `allowed_connections` / quota scope hides every connection of a provider, chat now answers `403` naming that scope instead of the generic `No active credentials for provider: X` — which was indistinguishable from "never configured" even though `/test` and `/sync-models` kept working on the same connection ([#13832](https://github.com/diegosouzapw/OmniRoute/issues/13832))
|
||||
@@ -825,6 +825,23 @@ export function handleNoCredentials(
|
||||
status === "credits_exhausted" ? HTTP_STATUS.PAYMENT_REQUIRED : HTTP_STATUS.UNAUTHORIZED;
|
||||
return errorResponse(httpStatus, message);
|
||||
}
|
||||
if (credentials?.blockedByKeyPolicy) {
|
||||
// #13832: the provider HAS active connections — they were filtered out by the
|
||||
// gateway API key's connection allowlist (`allowed_connections`) or its quota
|
||||
// scope, so the pool arrived empty and the generic "No active credentials"
|
||||
// below was indistinguishable from "this provider was never configured". That
|
||||
// cost the reporter a full investigation: their key passed `/test` and synced
|
||||
// 82 models (both address the connection by id and never consult the key's
|
||||
// scope), while chat kept failing. The classic shape is a key minted before
|
||||
// the provider existed, which is why older providers keep working on it.
|
||||
// 403, not 401: the credential is fine, this principal is not allowed to use it.
|
||||
const count = credentials.blockedCount || 1;
|
||||
const message =
|
||||
`[${provider}] ${count} connection(s) exist but are excluded by this API key's ` +
|
||||
`connection allowlist / quota scope — add them to the key in the dashboard, or use a key without that scope`;
|
||||
log.warn("AUTH", message);
|
||||
return errorResponse(HTTP_STATUS.FORBIDDEN, message);
|
||||
}
|
||||
if (!excludeConnectionId) {
|
||||
// Ported from upstream decolua/9router#336 (Ibrahim Ryan): surface as 404
|
||||
// NOT_FOUND instead of 400 BAD_REQUEST so combo routing can fall through to
|
||||
|
||||
@@ -1389,9 +1389,16 @@ export async function getProviderCredentials(
|
||||
let allConnections = (allConnectionsResults.filter(Array.isArray).flat() as unknown[])
|
||||
.map(toProviderConnection)
|
||||
.filter((conn) => conn.id.length > 0);
|
||||
// #13832: remember how many connections the provider really has BEFORE the
|
||||
// key-policy filter, so an empty pool can say which gate emptied it. Without
|
||||
// this the caller only ever saw "No active credentials for provider: X",
|
||||
// identical to "never configured" — while /test and /sync-models kept working,
|
||||
// because they address a connection by id and never consult the key's scope.
|
||||
const connectionsBeforeKeyPolicy = allConnections.length;
|
||||
if (allowedConnections && allowedConnections.length > 0) {
|
||||
allConnections = allConnections.filter((conn) => allowedConnections.includes(conn.id));
|
||||
}
|
||||
const blockedByKeyPolicyCount = connectionsBeforeKeyPolicy - allConnections.length;
|
||||
if (forcedConnectionId) {
|
||||
allConnections = allConnections.filter((conn) => conn.id === forcedConnectionId);
|
||||
}
|
||||
@@ -1461,6 +1468,16 @@ export async function getProviderCredentials(
|
||||
return geminiEnvCredentials;
|
||||
}
|
||||
invalidateManagedLease(options, "CONNECTION_INELIGIBLE");
|
||||
if (blockedByKeyPolicyCount > 0) {
|
||||
// #13832: the pool is empty only because the calling key's allowlist /
|
||||
// quota scope removed every connection. Say so instead of returning the
|
||||
// bare null that becomes "No active credentials for provider: X".
|
||||
log.warn(
|
||||
"AUTH",
|
||||
`${provider} | ${blockedByKeyPolicyCount} connection(s) hidden by the API key's allowed_connections/quota scope`
|
||||
);
|
||||
return { blockedByKeyPolicy: true, blockedCount: blockedByKeyPolicyCount };
|
||||
}
|
||||
log.debug("AUTH", `No credentials for ${provider}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -778,3 +778,32 @@ test("resolveModelOrError returns model_not_found error for unrecognised bare mo
|
||||
assert.match(json.error.message, /Unable to determine provider/i);
|
||||
assert.match(json.error.message, /completely-unknown-model-xyz/i);
|
||||
});
|
||||
|
||||
test("handleNoCredentials names the API key's connection allowlist as the reason (#13832)", async () => {
|
||||
// #13832: connections for the provider exist and are active, but the gateway API
|
||||
// key's allowed_connections / quota scope filtered every one of them out, so the
|
||||
// pool arrived empty. The old generic "No active credentials for provider: nvidia"
|
||||
// is indistinguishable from "never configured" — the reporter had a key that
|
||||
// passed /test and synced 82 models, and no message ever mentioned the allowlist.
|
||||
const blocked = handleNoCredentials(
|
||||
{ blockedByKeyPolicy: true, blockedCount: 2 },
|
||||
null,
|
||||
"nvidia",
|
||||
"nvidia/nemotron",
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
/* isCombo */ false
|
||||
);
|
||||
|
||||
assert.equal(blocked.status, 403);
|
||||
const blockedJson = (await blocked.json()) as { error?: { message?: string } };
|
||||
const message = blockedJson.error?.message ?? "";
|
||||
assert.match(message, /nvidia/);
|
||||
assert.match(message, /2 connection\(s\)/);
|
||||
assert.match(
|
||||
message,
|
||||
/allowlist|quota scope/i,
|
||||
"the operator must be told WHICH gate hid the connections"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user