fix(noauth): expose only usable model aliases (#3345)

Integrated into release/v3.8.14 — noauth usable-alias filtering + registry alias plumbing (veo-free).
This commit is contained in:
Paijo
2026-06-07 12:16:47 +07:00
committed by GitHub
parent 2271689d19
commit 1a4d2fc4be
8 changed files with 168 additions and 50 deletions

View File

@@ -14,6 +14,7 @@ export interface BaseModel {
export interface BaseProvider<M extends BaseModel = BaseModel> {
id: string;
alias?: string;
baseUrl: string;
authType: string; // "apikey" | "oauth" | "none"
authHeader: string; // "bearer" | "key" | "token" | "xi-api-key" | "x-api-key" | "none"
@@ -32,10 +33,13 @@ export function parseModelFromRegistry<P extends BaseProvider>(
if (!modelStr) return { provider: null, model: null };
// Try each provider prefix
for (const [providerId] of Object.entries(registry)) {
for (const [providerId, config] of Object.entries(registry)) {
if (modelStr.startsWith(providerId + "/")) {
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
}
if (config.alias && modelStr.startsWith(config.alias + "/")) {
return { provider: providerId, model: modelStr.slice(config.alias.length + 1) };
}
}
// No provider prefix — try to find the model in every provider
@@ -62,12 +66,17 @@ export function getAllModelsFromRegistry<P extends BaseProvider>(
for (const [providerId, config] of Object.entries(registry)) {
const extraFields = extra ? extra(providerId, config) : {};
for (const model of config.models) {
models.push({
id: `${providerId}/${model.id}`,
name: model.name,
provider: providerId,
...extraFields,
});
const entries = [providerId, config.alias].filter(
(prefix): prefix is string => typeof prefix === "string" && prefix.length > 0
);
for (const prefix of entries) {
models.push({
id: `${prefix}/${model.id}`,
name: model.name,
provider: providerId,
...extraFields,
});
}
}
}

View File

@@ -16,6 +16,7 @@ interface VideoModel {
interface VideoProvider {
id: string;
alias?: string;
baseUrl: string;
statusUrl?: string;
authType: string;
@@ -140,6 +141,19 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
models: [{ id: "animatediff-webui", name: "AnimateDiff (WebUI)" }],
},
"veoaifree-web": {
id: "veoaifree-web",
alias: "veo-free",
baseUrl: "https://veoaifree.com/wp-admin/admin-ajax.php",
authType: "none",
authHeader: "none",
format: "veoaifree-web",
models: [
{ id: "veo", name: "VEO 3.1" },
{ id: "seedance", name: "Seedance" },
],
},
runwayml: {
id: "runwayml",
baseUrl: "https://api.dev.runwayml.com/v1",

View File

@@ -17,6 +17,7 @@
import { getVideoProvider, parseVideoModel } from "../config/videoRegistry.ts";
import { kieExecutor } from "../executors/kie.ts";
import { getExecutor } from "../executors/index.ts";
import { isJsonObject, parseKieResultJson } from "../utils/kieTask.ts";
import {
buildRunwayApiUrl,
@@ -74,6 +75,11 @@ export async function handleVideoGeneration({ body, credentials, log }) {
if (providerConfig.format === "haiper-video") {
return handleHaiperVideoGeneration({ model, provider, providerConfig, body, credentials, log });
}
if (providerConfig.format === "veoaifree-web") {
return handleVeoAiFreeVideoGeneration({ model, provider, body, credentials, log });
}
if (providerConfig.format === "leonardo-video") {
return handleLeonardoVideoGeneration({
model,
@@ -96,6 +102,48 @@ export async function handleVideoGeneration({ body, credentials, log }) {
* Handle ComfyUI video generation
* Submits an AnimateDiff or SVD workflow, polls for completion, fetches output video
*/
async function handleVeoAiFreeVideoGeneration({ model, provider, body, credentials, log }) {
const executor = getExecutor(provider);
if (!executor) {
return { success: false, status: 400, error: `Unknown video provider: ${provider}` };
}
const prompt = String(body.prompt ?? "");
const systemParts = [];
if (body.size) systemParts.push(`aspect_ratio: ${body.size}`);
if (body.aspect_ratio) systemParts.push(`aspect_ratio: ${body.aspect_ratio}`);
const response = await executor.execute({
model,
body: {
...body,
model: `${provider}/${model}`,
messages: [
...(systemParts.length > 0 ? [{ role: "system", content: systemParts.join("\n") }] : []),
{ role: "user", content: prompt },
],
},
stream: false,
credentials: credentials || { connectionId: "noauth" },
signal: null,
log,
});
const upstreamResponse = response instanceof Response ? response : response.response;
if (!upstreamResponse.ok) {
return {
success: false,
status: upstreamResponse.status || 502,
error: await upstreamResponse.text().catch(() => "Video provider error"),
};
}
return {
success: true,
data: await upstreamResponse.json(),
};
}
async function handleComfyUIVideoGeneration({ model, provider, providerConfig, body, log }) {
const startTime = Date.now();
const [width, height] = (body.size || "512x512").split("x").map(Number);

View File

@@ -427,13 +427,21 @@ export async function getUnifiedModelsResponse(
return providerModels.find((model) => model?.id === modelId) || null;
};
const prefixRoutesToProvider = (prefix: string, providerId: string) => {
const parsed = parseModel(`${prefix}/__omniroute_probe__`);
return parsed.provider === providerId;
};
const getProviderPrefixes = (providerId: string, rawProvider: string) => {
const prefixes = new Set<string>([providerId, rawProvider, providerIdToAlias[providerId]]);
for (const [alias, mappedProviderId] of Object.entries(aliasToProviderId)) {
if (mappedProviderId === providerId) prefixes.add(alias);
}
return [...prefixes].filter(
(prefix): prefix is string => typeof prefix === "string" && prefix.length > 0
(prefix): prefix is string =>
typeof prefix === "string" &&
prefix.length > 0 &&
prefixRoutesToProvider(prefix, providerId)
);
};
@@ -724,7 +732,10 @@ export async function getUnifiedModelsResponse(
// Add provider-id prefix in addition to short alias (ex: kiro/model + kr/model).
// This improves compatibility for clients that expect full provider names.
if (canonicalProviderId !== alias) {
if (
canonicalProviderId !== alias &&
prefixRoutesToProvider(canonicalProviderId, canonicalProviderId)
) {
const providerIdModel = `${canonicalProviderId}/${model.id}`;
const providerVisionFields =
getVisionCapabilityFields(providerIdModel) || getVisionCapabilityFields(model.id);

View File

@@ -724,6 +724,46 @@ async function selectSessionAffinityConnection(
*/
const SYNTHETIC_NOAUTH_CONNECTION_ID = "noauth";
function buildSyntheticNoAuthCredentials(): {
apiKey: null;
accessToken: null;
refreshToken: null;
expiresAt: null;
projectId: null;
copilotToken: null;
providerSpecificData: Record<string, never>;
connectionId: typeof SYNTHETIC_NOAUTH_CONNECTION_ID;
testStatus: "active";
lastError: null;
lastErrorType: null;
lastErrorSource: null;
errorCode: null;
rateLimitedUntil: null;
maxConcurrent: null;
allRateLimited?: never;
allExpired?: never;
retryAfter?: never;
retryAfterHuman?: never;
} {
return {
apiKey: null,
accessToken: null,
refreshToken: null,
expiresAt: null,
projectId: null,
copilotToken: null,
providerSpecificData: {},
connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID,
testStatus: "active",
lastError: null,
lastErrorType: null,
lastErrorSource: null,
errorCode: null,
rateLimitedUntil: null,
maxConcurrent: null,
};
}
function normalizeExcludedConnectionIds(
excludeConnectionId: string | null,
extraExcludedConnectionIds: string[] | null | undefined
@@ -903,23 +943,7 @@ export async function getProviderCredentials(
if (excludedForNoAuth.has(SYNTHETIC_NOAUTH_CONNECTION_ID)) {
return null;
}
return {
apiKey: null,
accessToken: null,
refreshToken: null,
expiresAt: null,
projectId: null,
copilotToken: null,
providerSpecificData: {},
connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID,
testStatus: "active",
lastError: null,
lastErrorType: null,
lastErrorSource: null,
errorCode: null,
rateLimitedUntil: null,
maxConcurrent: null,
};
return buildSyntheticNoAuthCredentials();
}
const allowSuppressedConnections = options.allowSuppressedConnections === true;
@@ -1031,23 +1055,7 @@ export async function getProviderCredentials(
if (excludedConnectionIds.has(SYNTHETIC_NOAUTH_CONNECTION_ID)) {
return null;
}
return {
apiKey: null,
accessToken: null,
refreshToken: null,
expiresAt: null,
projectId: null,
copilotToken: null,
providerSpecificData: {},
connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID,
testStatus: "active",
lastError: null,
lastErrorType: null,
lastErrorSource: null,
errorCode: null,
rateLimitedUntil: null,
maxConcurrent: null,
};
return buildSyntheticNoAuthCredentials();
}
log.warn("AUTH", `No credentials for ${provider}`);
return null;
@@ -1218,6 +1226,13 @@ export async function getProviderCredentials(
cooldownModel: allBlockedByModelCooldown ? requestedModel : null,
};
}
if (resolvedId === "opencode-zen") {
if (excludedConnectionIds.has(SYNTHETIC_NOAUTH_CONNECTION_ID)) {
return null;
}
return buildSyntheticNoAuthCredentials();
}
log.warn("AUTH", `${provider} | all ${connections.length} accounts unavailable`);
return null;
}

View File

@@ -19,6 +19,7 @@ process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const { getProviderCredentials } = await import("../../src/sse/services/auth.ts");
const { createProviderConnection } = await import("../../src/lib/db/providers.ts");
test.after(() => {
core.resetDbInstance();
@@ -33,11 +34,7 @@ test("#2962 opencode-zen with no connection falls back to anonymous no-auth cred
"noauth",
"should be synthetic no-auth credentials"
);
assert.equal(
(creds as { apiKey?: unknown }).apiKey,
null,
"anonymous access carries no api key"
);
assert.equal((creds as { apiKey?: unknown }).apiKey, null, "anonymous access carries no api key");
});
test("#2962 a normal api-key provider with no connection still returns null (no over-broadening)", async () => {
@@ -46,3 +43,19 @@ test("#2962 a normal api-key provider with no connection still returns null (no
const connectionId = (creds as { connectionId?: string } | null)?.connectionId;
assert.notEqual(connectionId, "noauth", "openai must not get anonymous no-auth credentials");
});
test("#2962 opencode-zen falls back to no-auth when saved key rows are unusable", async () => {
await createProviderConnection({
provider: "opencode-zen",
authType: "apikey",
name: "expired-test-key",
apiKey: "oa_test_expired",
isActive: true,
testStatus: "expired",
});
const creds = await getProviderCredentials("opencode-zen");
assert.ok(creds, "opencode-zen should still resolve to anonymous no-auth credentials");
assert.equal((creds as { connectionId?: string }).connectionId, "noauth");
assert.equal((creds as { apiKey?: unknown }).apiKey, null);
});

View File

@@ -1434,7 +1434,12 @@ test("v1 models catalog includes noAuth provider models when no DB connections e
// opencode (noAuth) models must surface even with zero connection rows.
// The registry defines models under alias "oc" (e.g. "oc/big-pickle").
assert.ok(
ids.some((id) => id.startsWith("oc/") || id.startsWith("opencode/")),
`Expected at least one oc/* or opencode/* model in /v1/models but got none. IDs sample: ${ids.slice(0, 10).join(", ")}`
ids.some((id) => id.startsWith("oc/")),
`Expected at least one oc/* model in /v1/models but got none. IDs sample: ${ids.slice(0, 10).join(", ")}`
);
assert.equal(
ids.some((id) => id.startsWith("opencode/")),
false,
"catalog must not return opencode/* noAuth aliases because opencode/ routes to opencode-zen"
);
});

View File

@@ -194,6 +194,7 @@ test("parseVideoModel: works via video registry", async () => {
const { parseVideoModel } = await import("../../open-sse/config/videoRegistry.ts");
const result = parseVideoModel("comfyui/animatediff");
assert.deepEqual(result, { provider: "comfyui", model: "animatediff" });
assert.deepEqual(parseVideoModel("veo-free/veo"), { provider: "veoaifree-web", model: "veo" });
});
test("parseMusicModel: works via music registry", async () => {
@@ -210,6 +211,8 @@ test("getAllVideoModels: returns video models with provider prefix", async () =>
assert.ok(models.some((m) => m.id === "kie/sora-2-pro-image-to-video"));
assert.ok(models.some((m) => m.id === "comfyui/animatediff"));
assert.ok(models.some((m) => m.id === "runwayml/gen4.5"));
assert.ok(models.some((m) => m.id === "veoaifree-web/veo"));
assert.ok(models.some((m) => m.id === "veo-free/veo"));
});
test("getAllMusicModels: returns music models with provider prefix", async () => {