mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 21:52:21 +03:00
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:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user