fix(auto-combo): include no-auth OpenCode Free (#3189)

Integrated into release/v3.8.11
This commit is contained in:
Wilson
2026-06-05 00:50:08 -03:00
committed by GitHub
parent ee62c4c38b
commit ccc4425744
3 changed files with 113 additions and 26 deletions

View File

@@ -62,6 +62,12 @@ OAuth resilience & observability release: spaced/sequential quota sync for OAuth
---
## [3.8.10] — Unreleased
_Development cycle in progress — entries are added as work merges into `release/v3.8.10` and finalized by the release flow._
---
## [3.8.9] — 2026-06-03
### ✨ New Features

View File

@@ -5,6 +5,7 @@ import { AutoVariant } from "./autoPrefix";
import { getProviderConnections } from "@/lib/db/providers";
import { getProviderRegistry } from "./providerRegistryAccessor";
import type { ConnectionFields } from "@/lib/db/encryption";
import { NOAUTH_PROVIDERS } from "@/shared/constants/providers";
import { defaultLogger as log } from "@omniroute/open-sse/utils/logger";
/** Minimal connection shape needed for virtual auto-combo factory */
@@ -16,6 +17,12 @@ interface VirtualFactoryConn extends ConnectionFields {
tokenExpiresAt?: number | string | null;
}
type NoAuthProviderDefinition = {
id?: string;
alias?: string;
noAuth?: boolean;
};
export interface VirtualAutoComboCandidate {
provider: string;
connectionId: string;
@@ -81,6 +88,53 @@ function hasUsableOAuthToken(conn: VirtualFactoryConn): boolean {
return expiryMs === null || expiryMs > Date.now();
}
const SYNTHETIC_NOAUTH_CONNECTION_ID = "noauth";
const ZERO_CONFIG_NOAUTH_CHAT_PROVIDERS = new Set(["opencode"]);
function getFirstRegistryModelId(providerInfo: { models?: Array<{ id?: string }> } | undefined) {
const firstModel = Array.isArray(providerInfo?.models) ? providerInfo.models[0] : undefined;
return typeof firstModel?.id === "string" && firstModel.id.trim().length > 0
? firstModel.id
: undefined;
}
function getNoAuthCandidates(excludedProviders: Set<string>): VirtualAutoComboCandidate[] {
const registry = getProviderRegistry();
const candidates: VirtualAutoComboCandidate[] = [];
for (const providerDef of Object.values(NOAUTH_PROVIDERS) as NoAuthProviderDefinition[]) {
if (providerDef?.noAuth !== true) continue;
const providerId = providerDef.id;
if (!providerId || excludedProviders.has(providerId)) continue;
if (!ZERO_CONFIG_NOAUTH_CHAT_PROVIDERS.has(providerId)) continue;
const providerInfo = registry[providerId];
const modelId = getFirstRegistryModelId(providerInfo);
if (!modelId) continue;
// No-auth providers do not have provider_connections rows. Use the same
// synthetic connection id returned by getProviderCredentials() so the
// downstream combo path can still carry a stable target/account identity.
// For OpenCode Free specifically, route through its alias (oc/...) because
// opencode/... is a compatibility alias for the opencode-zen API-key tier.
const registryAlias =
typeof providerInfo?.alias === "string" && providerInfo.alias.trim().length > 0
? providerInfo.alias
: null;
const routingPrefix = providerDef.alias || registryAlias || providerId;
candidates.push({
provider: providerId,
connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID,
model: modelId,
modelStr: `${routingPrefix}/${modelId}`,
costPer1MTokens: 0,
});
}
return candidates;
}
/**
* Creates a virtual AutoCombo configuration dynamically based on connected providers and a specified variant.
* This combo is not persisted in the DB.
@@ -95,7 +149,32 @@ export async function createVirtualAutoCombo(
return hasApiKey || hasUsableOAuthToken(conn);
});
if (validConnections.length === 0) {
const candidatePool: VirtualAutoComboCandidate[] = [];
for (const conn of validConnections) {
const providerInfo = getProviderRegistry()[conn.provider];
if (!providerInfo) continue; // Skip unknown providers
let modelId: string | undefined = conn.defaultModel;
if (!modelId) {
const firstModel = providerInfo.models[0];
modelId = firstModel?.id;
}
if (!modelId) continue; // Skip providers without a model
candidatePool.push({
provider: conn.provider,
connectionId: conn.id,
model: modelId,
modelStr: `${conn.provider}/${modelId}`,
costPer1MTokens: 0, // Not used in virtual auto-combo (LKGP uses session stickiness)
});
}
candidatePool.push(
...getNoAuthCandidates(new Set(validConnections.map((conn) => conn.provider)))
);
if (candidatePool.length === 0) {
log.warn("AUTO", "No connected providers with valid credentials for virtual auto-combo");
const emptyPool: string[] = [];
const autoConfig = {
@@ -119,27 +198,6 @@ export async function createVirtualAutoCombo(
};
}
const candidatePool: VirtualAutoComboCandidate[] = [];
for (const conn of validConnections) {
const providerInfo = getProviderRegistry()[conn.provider];
if (!providerInfo) continue; // Skip unknown providers
let modelId: string | undefined = conn.defaultModel;
if (!modelId) {
const firstModel = providerInfo.models[0];
modelId = firstModel?.id;
}
if (!modelId) continue; // Skip providers without a model
candidatePool.push({
provider: conn.provider,
connectionId: conn.id,
model: modelId,
modelStr: `${conn.provider}/${modelId}`,
costPer1MTokens: 0, // Not used in virtual auto-combo (LKGP uses session stickiness)
});
}
let weights: ScoringWeights = { ...DEFAULT_WEIGHTS };
let explorationRate = 0.05; // Default exploration rate
let routerStrategy = "lkgp"; // All auto variants use LKGP

View File

@@ -48,12 +48,12 @@ test("createVirtualAutoCombo returns an executable auto combo for API-key connec
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("fast");
assert.equal(combo.strategy, "auto");
assert.equal(combo.models.length, 1);
assert.ok(combo.models.length >= 1);
assert.equal(combo.models[0].kind, "model");
assert.equal(combo.models[0].model, "openai/gpt-4o-mini");
assert.equal(combo.models[0].providerId, "openai");
assert.equal(combo.autoConfig.routerStrategy, "lkgp");
assert.deepEqual(combo.autoConfig.candidatePool, ["openai"]);
assert.ok(combo.autoConfig.candidatePool.includes("openai"));
});
test("createVirtualAutoCombo includes OAuth accessToken connections with real expiry fields", async () => {
@@ -69,7 +69,30 @@ test("createVirtualAutoCombo includes OAuth accessToken connections with real ex
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding");
assert.equal(combo.strategy, "auto");
assert.equal(combo.models.length, 1);
assert.ok(combo.models.length >= 1);
assert.equal(combo.models[0].model, "anthropic/claude-sonnet-4-5");
assert.deepEqual(combo.autoConfig.candidatePool, ["anthropic"]);
assert.ok(combo.autoConfig.candidatePool.includes("anthropic"));
});
test("createVirtualAutoCombo includes no-auth OpenCode Free without provider_connections rows", async () => {
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("fast");
const opencode = combo.models.find((model) => model.providerId === "opencode");
assert.ok(
opencode,
"OpenCode Free should appear in auto/* even when it has no provider_connections row"
);
assert.equal(opencode.connectionId, "noauth");
assert.equal(opencode.model, "oc/big-pickle");
assert.ok(combo.autoConfig.candidatePool.includes("opencode"));
});
test("createVirtualAutoCombo keeps credential-required providers out when disconnected", async () => {
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("fast");
assert.equal(
combo.models.some((model) => model.providerId === "openai"),
false,
"OpenAI should still require a real active connection"
);
});