mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
Merge main into release/v3.8.8
Back-merge to resolve PR #2930 (release/v3.8.8 -> main) conflicts. Release is a superset of main's features, so all ~44 content conflicts resolved to the release ("ours") version; generated .source/* dropped. Reconciliation: - auth.ts: port #3058 (getProviderSearchPool expands custom provider_nodes prefixes to internal connection ids) — release lacked this main fix. - quota-plan-registry.test.ts: align knownProviders() 6 -> 10 (pre-existing stale assertion vs the registry).
This commit is contained in:
@@ -2512,7 +2512,19 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
};
|
||||
|
||||
const handleAddModel = (model) => {
|
||||
const nextEntry = { model: model.value, weight: 0 };
|
||||
const qualifiedModel = typeof model?.value === "string" ? model.value : "";
|
||||
const parsedModel = parseQualifiedModel(qualifiedModel);
|
||||
const resolvedProviderId =
|
||||
resolveComboBuilderProviderId(model?.providerId, builderProviders) ||
|
||||
resolveComboBuilderProviderId(parsedModel?.providerId, builderProviders) ||
|
||||
(typeof model?.providerId === "string" && model.providerId.trim()) ||
|
||||
parsedModel?.providerId ||
|
||||
null;
|
||||
const nextEntry = {
|
||||
model: qualifiedModel,
|
||||
...(resolvedProviderId ? { providerId: resolvedProviderId } : {}),
|
||||
weight: 0,
|
||||
};
|
||||
if (hasExactModelStepDuplicate(models, nextEntry)) {
|
||||
setBuilderError(
|
||||
getI18nOrFallback(
|
||||
|
||||
@@ -175,7 +175,11 @@ export default function ProviderQuotaWidget({ autoRefreshInterval = 0 }: Provide
|
||||
onClick={refreshAll}
|
||||
disabled={refreshingAll || loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border bg-bg-subtle text-xs font-medium text-text-main disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface transition-colors"
|
||||
title={autoRefreshIntervalMs > 0 ? tr("autoRefreshing", "Auto-refreshing") : tr("refreshAll", "Refresh All")}
|
||||
title={
|
||||
autoRefreshIntervalMs > 0
|
||||
? tr("autoRefreshing", "Auto-refreshing")
|
||||
: tr("refreshAll", "Refresh All")
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[16px] ${refreshingAll ? "animate-spin" : ""}`}
|
||||
@@ -187,7 +191,10 @@ export default function ProviderQuotaWidget({ autoRefreshInterval = 0 }: Provide
|
||||
? tr("refreshing", "Refreshing")
|
||||
: autoRefreshIntervalMs > 0
|
||||
? `${tr("autoRefreshing", "Auto-refreshing")} ${formatAutoRefreshCountdown(
|
||||
Math.max(0, autoRefreshIntervalMs - (autoRefreshClock - lastRefreshAllAtRef.current))
|
||||
Math.max(
|
||||
0,
|
||||
autoRefreshIntervalMs - (autoRefreshClock - lastRefreshAllAtRef.current)
|
||||
)
|
||||
)}`
|
||||
: tr("refreshAll", "Refresh All")}
|
||||
</span>
|
||||
@@ -224,7 +231,7 @@ export default function ProviderQuotaWidget({ autoRefreshInterval = 0 }: Provide
|
||||
className="rounded-lg border border-border bg-surface/40 p-3 flex flex-col gap-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ProviderIcon provider={provider} size={18} />
|
||||
<ProviderIcon providerId={provider} size={18} />
|
||||
<span className="font-medium text-sm truncate">
|
||||
{provider.charAt(0).toUpperCase() + provider.slice(1)}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomUUID, createHash } from "crypto";
|
||||
import {
|
||||
getProviderConnections,
|
||||
getProviderNodes,
|
||||
validateApiKey,
|
||||
updateProviderConnection,
|
||||
getSettings,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
import { looksLikeQuotaExhausted } from "@/shared/utils/classify429";
|
||||
import { getCodexModelScope } from "@omniroute/open-sse/executors/codex.ts";
|
||||
import {
|
||||
getProviderById,
|
||||
getProviderAlias,
|
||||
resolveProviderId,
|
||||
NOAUTH_PROVIDERS,
|
||||
@@ -799,7 +801,7 @@ export { fisherYatesShuffle, getNextFromDeckSync as getNextFromDeck };
|
||||
/**
|
||||
* Resolve provider aliases (e.g., nvidia -> nvidia_nim) for DB lookup
|
||||
*/
|
||||
function getProviderSearchPool(provider: string): string[] {
|
||||
async function getProviderSearchPool(provider: string): Promise<string[]> {
|
||||
const canonicalProvider = resolveProviderId(provider);
|
||||
const canonicalAlias = getProviderAlias(canonicalProvider);
|
||||
|
||||
@@ -810,7 +812,34 @@ function getProviderSearchPool(provider: string): string[] {
|
||||
return ["nvidia_nim", "nvidia"];
|
||||
}
|
||||
|
||||
return Array.from(new Set([provider, canonicalProvider, canonicalAlias].filter(Boolean)));
|
||||
const searchPool = new Set([provider, canonicalProvider, canonicalAlias].filter(Boolean));
|
||||
|
||||
// Built-in providers already resolve through static ids/aliases. Only
|
||||
// compatible/custom providers need provider_nodes expansion back to the
|
||||
// generated internal connection ids. (#3058)
|
||||
if (getProviderById(canonicalProvider)) {
|
||||
return Array.from(searchPool);
|
||||
}
|
||||
|
||||
// Custom provider nodes are referenced by user-facing prefixes in combos
|
||||
// (for example "78code/gpt-5.4"), but live credentials are stored under
|
||||
// internal provider ids like openai-compatible-responses-<uuid>.
|
||||
try {
|
||||
const providerNodes = await getProviderNodes();
|
||||
for (const node of Array.isArray(providerNodes) ? providerNodes : []) {
|
||||
const nodeRecord = asRecord(node);
|
||||
const nodePrefix = typeof nodeRecord.prefix === "string" ? nodeRecord.prefix.trim() : "";
|
||||
const nodeId = typeof nodeRecord.id === "string" ? nodeRecord.id.trim() : "";
|
||||
if (!nodePrefix || !nodeId) continue;
|
||||
if (nodePrefix === provider || nodePrefix === canonicalProvider || nodePrefix === canonicalAlias) {
|
||||
searchPool.add(nodeId);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best-effort alias expansion only.
|
||||
}
|
||||
|
||||
return Array.from(searchPool);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -885,7 +914,7 @@ export async function getProviderCredentials(
|
||||
);
|
||||
|
||||
// Fix #922: Check for aliases (nvidia/nvidia_nim) to ensure credentials are found
|
||||
const providersToSearch = getProviderSearchPool(provider);
|
||||
const providersToSearch = await getProviderSearchPool(provider);
|
||||
const connectionResults = await Promise.all(
|
||||
providersToSearch.map((p) => getProviderConnections({ provider: p, isActive: true }))
|
||||
);
|
||||
|
||||
@@ -149,3 +149,28 @@ test("#2778 matching logic: node with prefix=flymux and id=UUID-id still matches
|
||||
assert.ok(matchByPrefixOrId !== undefined, "Alias-based match must still work after the fix");
|
||||
assert.strictEqual(matchByPrefixOrId?.id, FAKE_UUID_NODE_ID);
|
||||
});
|
||||
|
||||
test("custom provider auth lookup search pool maps alias prefixes to internal provider ids", async () => {
|
||||
const authSrc = fs.readFileSync(path.resolve(__dirname, "../../src/sse/services/auth.ts"), "utf8");
|
||||
|
||||
assert.match(
|
||||
authSrc,
|
||||
/async function getProviderSearchPool\(provider: string\): Promise<string\[]>/,
|
||||
"getProviderSearchPool should be async so it can expand custom provider aliases via provider_nodes"
|
||||
);
|
||||
assert.match(
|
||||
authSrc,
|
||||
/getProviderNodes\(/,
|
||||
"auth lookup should read provider_nodes to map custom prefixes like 78code/micu back to internal provider ids"
|
||||
);
|
||||
assert.match(
|
||||
authSrc,
|
||||
/nodePrefix === provider \|\| nodePrefix === canonicalProvider \|\| nodePrefix === canonicalAlias/,
|
||||
"auth lookup should match provider node prefixes against the requested alias/canonical provider values"
|
||||
);
|
||||
assert.match(
|
||||
authSrc,
|
||||
/searchPool\.add\(nodeId\)/,
|
||||
"auth lookup should add the matched custom provider node id into the credential search pool"
|
||||
);
|
||||
});
|
||||
|
||||
22
tests/unit/provider-quota-widget-icon-prop.test.ts
Normal file
22
tests/unit/provider-quota-widget-icon-prop.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const PROVIDER_QUOTA_WIDGET_PATH = join(ROOT, "src/app/(dashboard)/home/ProviderQuotaWidget.tsx");
|
||||
|
||||
const providerQuotaWidgetSrc = readFileSync(PROVIDER_QUOTA_WIDGET_PATH, "utf8");
|
||||
|
||||
test("ProviderQuotaWidget passes provider IDs using ProviderIcon's providerId prop", () => {
|
||||
assert.ok(
|
||||
providerQuotaWidgetSrc.includes("<ProviderIcon providerId={provider} size={18} />"),
|
||||
"ProviderQuotaWidget must pass provider through ProviderIcon's providerId prop"
|
||||
);
|
||||
assert.equal(
|
||||
providerQuotaWidgetSrc.includes("<ProviderIcon provider={provider}"),
|
||||
false,
|
||||
"ProviderQuotaWidget must not pass an unsupported provider prop to ProviderIcon"
|
||||
);
|
||||
});
|
||||
@@ -64,13 +64,24 @@ test("getKnownPlan('') returns null", () => {
|
||||
assert.equal(getKnownPlan(""), null);
|
||||
});
|
||||
|
||||
test("knownProviders() returns exactly 6 entries", () => {
|
||||
assert.equal(knownProviders().length, 6);
|
||||
test("knownProviders() returns exactly 10 entries", () => {
|
||||
assert.equal(knownProviders().length, 10);
|
||||
});
|
||||
|
||||
test("knownProviders() includes codex/glm/minimax/bailian/kimi/alibaba", () => {
|
||||
test("knownProviders() includes the full registry set", () => {
|
||||
const list = knownProviders() as readonly string[];
|
||||
for (const p of ["codex", "glm", "minimax", "bailian", "kimi", "alibaba"]) {
|
||||
for (const p of [
|
||||
"codex",
|
||||
"claude",
|
||||
"glm",
|
||||
"minimax",
|
||||
"deepseek",
|
||||
"bailian",
|
||||
"kimi",
|
||||
"kimi-coding",
|
||||
"xiaomi-mimo",
|
||||
"alibaba",
|
||||
]) {
|
||||
assert.ok(list.includes(p), `missing ${p}`);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user