mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
fix(auto-combo): include no-auth OpenCode Free (#3189)
Integrated into release/v3.8.11
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user