mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
fix(models): derive model discovery config from registry modelsUrl (#5087)
Integrated into release/v3.8.38 (leva 5)
This commit is contained in:
committed by
GitHub
parent
7256d34a23
commit
57d10ea9f7
@@ -29,6 +29,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
- **fix(sse): don't cool down a healthy connection on a self-inflicted upstream timeout (504)** — when OmniRoute's own deadline elapses (surfaced as `TimeoutError`/`BodyTimeoutError` → 504), the connection is no longer disabled/failed-over, so a slow-but-healthy provider isn't penalised for our timeout. Genuine upstream 5xx/429 still trigger cooldown; antigravity keeps its own policy. (thanks @costaeder)
|
||||
- **fix(sse): robust Anthropic `/v1/messages` streaming — real ping keepalive + client-disconnect guard** — slow first tokens on reasoning models could trip strict clients' idle-read watchdog; the route now keeps the stream warm with a real `event: ping` (Anthropic clients ignore SSE comments) from the very first frame, and a client disconnect (AbortError / controller-closed) no longer counts as a provider failure (no failover/cooldown). (thanks @costaeder)
|
||||
- **fix: preserve model hidden flags (`isHidden`) across model sync** — `replaceCustomModels` pruned the compat-override list to the new custom-model ids, silently wiping the `isHidden` flag of eye-hidden SYNCED models on every periodic sync / import (all hidden models turned back on). The redundant cleanup is removed (per-model removal already handles its own compat cleanup), so eye-hidden models stay hidden across re-sync. (#4389, thanks @herjarsa)
|
||||
- **fix(models): derive model-discovery config from the registry `modelsUrl`** — providers absent from the hardcoded `PROVIDER_MODELS_CONFIG` but carrying a registry `modelsUrl` (e.g. MiniMax) now get an auto-derived Bearer `/v1/models` discovery config, so "discover models" works instead of returning nothing. (thanks @herjarsa)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export const minimax_cnProvider: RegistryEntry = {
|
||||
format: "claude",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.minimaxi.com/anthropic/v1/messages",
|
||||
modelsUrl: "https://api.minimaxi.com/v1/models",
|
||||
urlSuffix: "?beta=true",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
|
||||
@@ -7,6 +7,7 @@ export const minimaxProvider: RegistryEntry = {
|
||||
format: "claude",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.minimax.io/anthropic/v1/messages",
|
||||
modelsUrl: "https://api.minimax.io/v1/models",
|
||||
urlSuffix: "?beta=true",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
|
||||
35
src/app/api/providers/[id]/models/discoveryConfig.ts
Normal file
35
src/app/api/providers/[id]/models/discoveryConfig.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
|
||||
/**
|
||||
* Derive a models-discovery config from the provider's registry `modelsUrl`
|
||||
* when the provider is absent from the hardcoded PROVIDER_MODELS_CONFIG.
|
||||
*
|
||||
* Returns a config object with Bearer auth suitable for fetching an
|
||||
* OpenAI-compatible `/v1/models` endpoint, or `undefined` when the
|
||||
* registry entry has no `modelsUrl`.
|
||||
*/
|
||||
export function deriveConfigFromRegistryModelsUrl(provider: string):
|
||||
| {
|
||||
url: string;
|
||||
method: "GET";
|
||||
headers: Record<string, string>;
|
||||
authHeader?: string;
|
||||
authPrefix?: string;
|
||||
authQuery?: string;
|
||||
body?: unknown;
|
||||
parseResponse: (data: any) => any;
|
||||
}
|
||||
| undefined {
|
||||
const entry = getRegistryEntry(provider);
|
||||
if (typeof entry?.modelsUrl === "string" && entry.modelsUrl.length > 0) {
|
||||
return {
|
||||
url: entry.modelsUrl,
|
||||
method: "GET",
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
parseResponse: (data) => data.data || data.models || [],
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts";
|
||||
import { deriveConfigFromRegistryModelsUrl } from "./discoveryConfig";
|
||||
import { fetchGitHubCopilotModels } from "@omniroute/open-sse/services/githubCopilotModels.ts";
|
||||
import { fetchKiroAvailableModels } from "@omniroute/open-sse/services/kiroModels.ts";
|
||||
import { getAntigravityHeaders } from "@omniroute/open-sse/services/antigravityHeaders.ts";
|
||||
@@ -2390,8 +2391,7 @@ export async function GET(
|
||||
const config =
|
||||
provider in PROVIDER_MODELS_CONFIG
|
||||
? PROVIDER_MODELS_CONFIG[provider as keyof typeof PROVIDER_MODELS_CONFIG]
|
||||
: undefined;
|
||||
|
||||
: deriveConfigFromRegistryModelsUrl(provider);
|
||||
// Static model providers (no remote /models API)
|
||||
// Qwen OAuth Fallback: The Dashscope /models API rejects OAuth tokens with 401
|
||||
if (provider === "qwen" && connection.authType === "oauth") {
|
||||
|
||||
34
tests/unit/derive-discovery-config-5087.test.ts
Normal file
34
tests/unit/derive-discovery-config-5087.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* #5087 — derive model-discovery config from a provider's registry `modelsUrl`.
|
||||
*
|
||||
* When a provider is absent from the hardcoded PROVIDER_MODELS_CONFIG but its
|
||||
* registry entry carries a `modelsUrl`, `deriveConfigFromRegistryModelsUrl`
|
||||
* builds a Bearer `/v1/models` discovery config so the dashboard's "discover
|
||||
* models" path works (e.g. MiniMax). Providers without `modelsUrl` return
|
||||
* `undefined` (caller falls back to its existing undefined handling).
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { deriveConfigFromRegistryModelsUrl } from "../../src/app/api/providers/[id]/models/discoveryConfig.ts";
|
||||
|
||||
test("minimax (registry modelsUrl present) yields a Bearer /v1/models config", () => {
|
||||
const config = deriveConfigFromRegistryModelsUrl("minimax");
|
||||
assert.ok(config, "minimax must yield a config");
|
||||
assert.equal(config.url, "https://api.minimax.io/v1/models");
|
||||
assert.equal(config.method, "GET");
|
||||
assert.equal(config.authHeader, "Authorization");
|
||||
assert.equal(config.authPrefix, "Bearer ");
|
||||
// parseResponse tolerates both {data:[...]} and {models:[...]} shapes.
|
||||
assert.deepEqual(config.parseResponse({ data: [{ id: "abab6.5" }] }), [{ id: "abab6.5" }]);
|
||||
assert.deepEqual(config.parseResponse({ models: [{ id: "m" }] }), [{ id: "m" }]);
|
||||
});
|
||||
|
||||
test("a provider without a registry modelsUrl returns undefined", () => {
|
||||
// `baseten` is a registry provider with no `modelsUrl`.
|
||||
assert.equal(deriveConfigFromRegistryModelsUrl("baseten"), undefined);
|
||||
});
|
||||
|
||||
test("an unknown provider returns undefined", () => {
|
||||
assert.equal(deriveConfigFromRegistryModelsUrl("not-a-real-provider-xyz"), undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user