Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions open-sse/config/registryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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,
});
}
}
}

Expand Down
14 changes: 14 additions & 0 deletions open-sse/config/videoRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface VideoModel {

interface VideoProvider {
id: string;
alias?: string;
baseUrl: string;
statusUrl?: string;
authType: string;
Expand Down Expand Up @@ -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",
Expand Down
48 changes: 48 additions & 0 deletions open-sse/handlers/videoGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down
15 changes: 13 additions & 2 deletions src/app/api/v1/models/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
};

Expand Down Expand Up @@ -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);
Expand Down
83 changes: 49 additions & 34 deletions src/sse/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
23 changes: 18 additions & 5 deletions tests/unit/auth-opencode-zen-noauth-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 () => {
Expand All @@ -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);
});
9 changes: 7 additions & 2 deletions tests/unit/models-catalog-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
});
3 changes: 3 additions & 0 deletions tests/unit/registry-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down