From 1a4d2fc4be1eb94daf5a4b1828437337427ba5bd Mon Sep 17 00:00:00 2001 From: Paijo <14921983+oyi77@users.noreply.github.com> Date: Sun, 7 Jun 2026 12:16:47 +0700 Subject: [PATCH] fix(noauth): expose only usable model aliases (#3345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.14 — noauth usable-alias filtering + registry alias plumbing (veo-free). --- open-sse/config/registryUtils.ts | 23 +++-- open-sse/config/videoRegistry.ts | 14 ++++ open-sse/handlers/videoGeneration.ts | 48 +++++++++++ src/app/api/v1/models/catalog.ts | 15 +++- src/sse/services/auth.ts | 83 +++++++++++-------- .../auth-opencode-zen-noauth-fallback.test.ts | 23 +++-- tests/unit/models-catalog-route.test.ts | 9 +- tests/unit/registry-utils.test.ts | 3 + 8 files changed, 168 insertions(+), 50 deletions(-) diff --git a/open-sse/config/registryUtils.ts b/open-sse/config/registryUtils.ts index 6ca45f1da6..55dc5b2c48 100644 --- a/open-sse/config/registryUtils.ts +++ b/open-sse/config/registryUtils.ts @@ -14,6 +14,7 @@ export interface BaseModel { export interface BaseProvider { 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

( 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

( 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, + }); + } } } diff --git a/open-sse/config/videoRegistry.ts b/open-sse/config/videoRegistry.ts index b876bb0b6e..6e65e5adfb 100644 --- a/open-sse/config/videoRegistry.ts +++ b/open-sse/config/videoRegistry.ts @@ -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 = { 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", diff --git a/open-sse/handlers/videoGeneration.ts b/open-sse/handlers/videoGeneration.ts index 103b364215..65890fdd04 100644 --- a/open-sse/handlers/videoGeneration.ts +++ b/open-sse/handlers/videoGeneration.ts @@ -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); diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 9e1d36ca16..928d52cc5c 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -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([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); diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index 620e35a412..7dc74cb87b 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -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; + 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; } diff --git a/tests/unit/auth-opencode-zen-noauth-fallback.test.ts b/tests/unit/auth-opencode-zen-noauth-fallback.test.ts index fd9054d686..453a31b67a 100644 --- a/tests/unit/auth-opencode-zen-noauth-fallback.test.ts +++ b/tests/unit/auth-opencode-zen-noauth-fallback.test.ts @@ -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); +}); diff --git a/tests/unit/models-catalog-route.test.ts b/tests/unit/models-catalog-route.test.ts index 8d13a7f010..3d392c7d4a 100644 --- a/tests/unit/models-catalog-route.test.ts +++ b/tests/unit/models-catalog-route.test.ts @@ -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" ); }); diff --git a/tests/unit/registry-utils.test.ts b/tests/unit/registry-utils.test.ts index 781cf8d332..4f97bdd41d 100644 --- a/tests/unit/registry-utils.test.ts +++ b/tests/unit/registry-utils.test.ts @@ -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 () => {