Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/14905-noauth-combo-pin-allowlist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(auth):** `model: "auto"` on a fresh install no longer skips every OpenCode Free target as `availability` ([#14905](https://github.com/diegosouzapw/OmniRoute/pull/14905)). The auto combo pins keyless targets to the synthetic `noauth` connection and pin-fail-closed turns that into the allowlist `["noauth"]`, which the #9057 guards rejected. An allowlist that explicitly names `noauth` now resolves; keys restricted to real connections are still denied. — thanks @TrippyEngineer
19 changes: 8 additions & 11 deletions src/sse/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { isCommonChatGptWebRetiredProviderId } from "@/shared/constants/chatgptW
import { toNumber } from "@/shared/utils/numeric";
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
import { isRuntimeRetiredProviderId } from "@/shared/constants/providerRetirement";
import { allowlistPermitsSyntheticNoAuth } from "./noAuthAllowlist";
import {
createLazyConnectionView,
toProviderConnection,
Expand Down Expand Up @@ -641,6 +642,7 @@ function compareP2CConnections(
* exclude it (#3061), otherwise it gets re-selected forever.
*/
const SYNTHETIC_NOAUTH_CONNECTION_ID = "noauth";

type AnonymousFallbackProviderDefinition = {
anonymousFallback?: boolean;
noAuth?: boolean;
Expand Down Expand Up @@ -780,11 +782,9 @@ async function maybeSyntheticNoAuthFallback(
allowedConnections: string[] | null = null
) {
if (!providerCanUseSyntheticNoAuthFallback(providerId)) return null;
// #9057: a key pinned to specific connections via allowedConnections must
// NOT receive the synthetic "noauth" connection — the synthetic id is
// never in an explicit allowlist, so returning it would let a restricted
// key reach free providers (OpenCode Free, etc.) that it should not access.
if (Array.isArray(allowedConnections) && allowedConnections.length > 0) return null;
// #9057: a restricted key must NOT reach free providers (OpenCode Free, etc.) through the
// synthetic "noauth" connection unless its allowedConnections names it.
if (!allowlistPermitsSyntheticNoAuth(allowedConnections)) return null;
if (excludedConnectionIds.has(SYNTHETIC_NOAUTH_CONNECTION_ID)) return null;
// #14313: a free-tier refusal just paused this keyless path — do not re-select
// the synthetic noauth connection until the short TTL expires.
Expand Down Expand Up @@ -1217,12 +1217,9 @@ export async function getProviderCredentials(
) {
return optionalKey;
}
// #9057: when allowedConnections is set, the synthetic "noauth" connection
// is never in the explicit allowlist, so we must NOT return it — fall through
// to the normal connection-selection path so the connection allowlist is
// respected (the no-auth provider will be rejected if it has no real connections
// matching the allowlist, or a real connection row will be selected if present).
if (!allowedConnections || allowedConnections.length === 0) {
// #9057: an allowlist that does not name "noauth" falls through to the normal
// connection-selection path so the allowlist is respected.
if (allowlistPermitsSyntheticNoAuth(allowedConnections)) {
// #13483: check model-only lockout before handing back the synthetic
// connection. Without this, a locked model (e.g. 400 model_capacity)
// is retried on every request because the noauth path short-circuits
Expand Down
14 changes: 14 additions & 0 deletions src/sse/services/noAuthAllowlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SYNTHETIC_NOAUTH_CONNECTION_ID } from "@omniroute/open-sse/services/autoCombo/resilienceCandidateFilter.ts";

/**
* #9057 gate for the synthetic keyless connection: allowed when there is no
* allowlist, or when the allowlist names the synthetic id explicitly. The auto
* combo pins keyless targets to "noauth", and pin-fail-closed
* (`implicitPinAllowlist`) turns that pin into the allowlist ["noauth"].
*/
export function allowlistPermitsSyntheticNoAuth(
allowedConnections: string[] | null | undefined
): boolean {
if (!Array.isArray(allowedConnections) || allowedConnections.length === 0) return true;
return allowedConnections.includes(SYNTHETIC_NOAUTH_CONNECTION_ID);
}
20 changes: 20 additions & 0 deletions tests/unit/api-key-policy-noauth-allowed-connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ test("#9057 LAYER1: unrestricted key still gets synthetic credentials for OpenCo
);
});

// The auto combo pins keyless targets to the synthetic connection id, and
// pin-fail-closed turns that pin into the allowlist ["noauth"]. An allowlist
// that explicitly names the synthetic id must still receive it — otherwise
// every OpenCode Free target of a fresh install is skipped as "availability".
test("#9057 LAYER1: allowlist that names the synthetic noauth id still gets credentials", async () => {
const creds = await getProviderCredentials("opencode", null, ["noauth"], "big-pickle");
assert(creds, "a combo pin to the synthetic noauth connection must resolve");
assert.equal((creds as Record<string, unknown>)?.connectionId, "noauth");
});

test("#9057 LAYER1: allowlist mixing a real UUID and noauth still gets synthetic credentials", async () => {
const creds = await getProviderCredentials(
"opencode",
null,
[RESTRICTED_CONNECTION_UUID, "noauth"],
"big-pickle"
);
assert.equal((creds as Record<string, unknown>)?.connectionId, "noauth");
});

test("#9057 LAYER2: isModelAllowedForKey rejects keyless model for disableNonPublicModels key", async () => {
// Create a key with disableNonPublicModels=true
const created = await apiKeysDb.createApiKey("dnp-9057", "machine-dnp");
Expand Down