From fb85e4ffe36578b745660fd5635a1b221dd97567 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Sun, 7 Jun 2026 09:25:44 +0700 Subject: [PATCH 1/2] fix(noauth): expose only usable model aliases --- 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 +- 4 files changed, 87 insertions(+), 43 deletions(-) diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 9e1d36ca16f..928d52cc5cd 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 620e35a4125..7dc74cb87ba 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 fd9054d6866..453a31b67aa 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 8d13a7f0104..3d392c7d4a0 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" ); }); From 58cea8ee9517f84cdfe863697892513422802434 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Sun, 7 Jun 2026 09:42:06 +0700 Subject: [PATCH 2/2] fix(noauth): support keyless web provider results --- open-sse/config/registryUtils.ts | 23 ++++++--- open-sse/config/videoRegistry.ts | 14 ++++++ open-sse/handlers/chatCore.ts | 71 +++++++++++++++++----------- open-sse/handlers/videoGeneration.ts | 48 +++++++++++++++++++ tests/unit/registry-utils.test.ts | 3 ++ 5 files changed, 124 insertions(+), 35 deletions(-) diff --git a/open-sse/config/registryUtils.ts b/open-sse/config/registryUtils.ts index 6ca45f1da60..55dc5b2c489 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 b876bb0b6e8..6e65e5adfb7 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/chatCore.ts b/open-sse/handlers/chatCore.ts index b8832a8b322..4ab4a326d3e 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1559,7 +1559,9 @@ export async function handleChatCore({ ), }; } - } catch { /* memoryUsage() never throws */ } + } catch { + /* memoryUsage() never throws */ + } // apiFormat is an optional custom-model marker injected by getModelInfo for // providers whose models can route to /chat/completions or /responses @@ -1598,8 +1600,7 @@ export async function handleChatCore({ comboName: comboName || undefined, }); }); - const traceEnabled = - process.env.OMNIRROUTE_TRACE === "true" || process.env.DEBUG === "true"; + const traceEnabled = process.env.OMNIRROUTE_TRACE === "true" || process.env.DEBUG === "true"; const trace = (label: string, extra?: Record) => { if (!traceEnabled) return; const elapsed = Date.now() - startTime; @@ -1775,10 +1776,7 @@ export async function handleChatCore({ } }; - const persistCodexQuotaState = async ( - headers: Record | null, - status = 0 - ) => { + const persistCodexQuotaState = async (headers: Record | null, status = 0) => { if (provider !== "codex" || !connectionId || !headers) return; try { @@ -3627,14 +3625,19 @@ export async function handleChatCore({ let fallbackCodes: number[] = [429, 500, 502, 503, 504]; try { const allSettings = await getCachedSettings(); - if (typeof allSettings.cliproxyapi_fallback_codes === "string" && allSettings.cliproxyapi_fallback_codes.trim()) { + if ( + typeof allSettings.cliproxyapi_fallback_codes === "string" && + allSettings.cliproxyapi_fallback_codes.trim() + ) { const parsed = allSettings.cliproxyapi_fallback_codes .split(",") .map((s: string) => parseInt(s.trim(), 10)) .filter((n: number) => !isNaN(n)); if (parsed.length > 0) fallbackCodes = parsed; } - } catch { /* use defaults */ } + } catch { + /* use defaults */ + } const isRetryableStatus = (s: number) => fallbackCodes.includes(s) || s === 0; const wrapper = Object.create(nativeExec); @@ -3710,10 +3713,10 @@ export async function handleChatCore({ if (decision.retryAfterSeconds) { headers["Retry-After"] = String(decision.retryAfterSeconds); } - return new Response( - JSON.stringify(buildErrorBody(429, decision.reason)), - { status: 429, headers } - ); + return new Response(JSON.stringify(buildErrorBody(429, decision.reason)), { + status: 429, + headers, + }); } if (decision.kind === "allow" && decision.deprioritize) { @@ -3961,13 +3964,7 @@ export async function handleChatCore({ stage: "sending_to_provider", }); const execCreds = getExecutionCredentials(); - const res = await executeWithUpstreamStartTimeout<{ - response: Response; - url: string; - headers: Record; - transformedBody: unknown; - _executionCredentials?: unknown; - }>({ + const rawExecutorResult = await executeWithUpstreamStartTimeout({ executor, provider, model: modelToCall, @@ -3988,6 +3985,15 @@ export async function handleChatCore({ skipUpstreamRetry, }), }); + const res = + rawExecutorResult instanceof Response + ? { + response: rawExecutorResult, + url: "", + headers: {}, + transformedBody: null, + } + : rawExecutorResult; trace("post_executor", { status: res?.response?.status }); updatePendingRequest(model, provider, connectionId, { stage: "provider_response_started", @@ -4243,7 +4249,11 @@ export async function handleChatCore({ // ── Tier 2: Authoritative per-model/provider token-limit check (provider now resolved) ── if (apiKeyInfo?.id) { try { - const tokenBreach = checkTokenLimits(apiKeyInfo.id, provider || undefined, model || undefined); + const tokenBreach = checkTokenLimits( + apiKeyInfo.id, + provider || undefined, + model || undefined + ); if (tokenBreach) { const scopeLabel = tokenBreach.scopeType === "global" @@ -4311,7 +4321,11 @@ export async function handleChatCore({ // Store rate-limit headers for quota saturation signals try { const { storeRateLimitHeaders } = await import("@/lib/quota/saturationSignals"); - storeRateLimitHeaders(connectionId, provider, providerResponse.headers as Record); + storeRateLimitHeaders( + connectionId, + provider, + providerResponse.headers as Record + ); } catch { // fail-open: saturation signal is best-effort } @@ -5244,7 +5258,8 @@ export async function handleChatCore({ if (apiKeyInfo?.id) { try { const billable = computeBillableTokens(usage); - if (billable > 0) recordTokenUsage(apiKeyInfo.id, provider || "unknown", model || "unknown", billable); + if (billable > 0) + recordTokenUsage(apiKeyInfo.id, provider || "unknown", model || "unknown", billable); } catch { // never block the response on counter recording } @@ -5467,8 +5482,8 @@ export async function handleChatCore({ cost: { tokens: usage && typeof usage === "object" - ? ((usage as Record).prompt_tokens as number ?? 0) + - ((usage as Record).completion_tokens as number ?? 0) + ? (((usage as Record).prompt_tokens as number) ?? 0) + + (((usage as Record).completion_tokens as number) ?? 0) : 0, usd: estimatedCost > 0 ? estimatedCost : 0, requests: 1, @@ -5701,7 +5716,8 @@ export async function handleChatCore({ if (apiKeyInfo?.id && streamStatus === 200) { try { const billable = computeBillableTokens(streamUsage); - if (billable > 0) recordTokenUsage(apiKeyInfo.id, provider || "unknown", model || "unknown", billable); + if (billable > 0) + recordTokenUsage(apiKeyInfo.id, provider || "unknown", model || "unknown", billable); } catch { // never block the stream on counter recording } @@ -5743,8 +5759,7 @@ export async function handleChatCore({ provider: provider ?? "unknown", cost: { tokens: su - ? (Number(su.prompt_tokens ?? 0) || 0) + - (Number(su.completion_tokens ?? 0) || 0) + ? (Number(su.prompt_tokens ?? 0) || 0) + (Number(su.completion_tokens ?? 0) || 0) : 0, usd: 0, // estimatedCost resolved async above; omit to avoid dependency requests: 1, diff --git a/open-sse/handlers/videoGeneration.ts b/open-sse/handlers/videoGeneration.ts index 103b364215c..65890fdd04f 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/tests/unit/registry-utils.test.ts b/tests/unit/registry-utils.test.ts index 781cf8d332e..4f97bdd41d1 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 () => {