Files
9router/open-sse/config/providerModels.js
KiMelody e1115e2839 feat(opencode-go): route by request format via transports + per-model guard
opencode-go hard-coded targetFormat: claude per model, so every client
format was force-routed to /messages (Codex/OpenAI clients paid a lossy
Responses->OpenAI->Claude double translation). Declare the existing
upstream multi-endpoint transports [openai, claude, openai-responses]
and guard per model via registry supportedFormats: kimi/glm/mimo only
support /chat/completions, minimax/qwen add /messages, deepseek adds
/responses. Undeclared models keep the upstream default.

Drop the bespoke OpenCodeGoExecutor (its shared _lastModel cache could
cross auth headers between concurrent requests); DefaultExecutor already
consumes runtimeTransport and injects reasoning content.
2026-08-14 16:52:37 +07:00

119 lines
4.9 KiB
JavaScript

import { PROVIDERS } from "./providers.js";
import REGISTRY from "../providers/registry/index.js";
// PROVIDER_MODELS now built from providers/registry (transport + models co-located)
import { PROVIDER_MODELS } from "../providers/index.js";
import { modelQuotaFamily, modelStrip, modelTargetFormat, modelSupportedFormats, normalizeModelId } from "../providers/models/schema.js";
import { CODEX_REVIEW_SUFFIX } from "../providers/models/helpers.js";
export { PROVIDER_MODELS };
// Helper functions
export function getProviderModels(aliasOrId) {
return PROVIDER_MODELS[aliasOrId] || [];
}
export function getDefaultModel(aliasOrId) {
const models = PROVIDER_MODELS[aliasOrId];
return models?.[0]?.id || null;
}
// Providers whose registry uses dots in version numbers (e.g. "claude-sonnet-4.5").
// For these, we tolerate clients sending dashes ("claude-sonnet-4-5") by normalizing
// digit-hyphen-digit to digit-dot-digit before lookup. Other providers are left untouched.
const DOT_VERSION_PROVIDERS = new Set(["kr", "kiro"]);
// Find a registry entry by id. For Kiro models, tolerates dash/dot version separators
// ("claude-sonnet-4-5" ~= "claude-sonnet-4.5"). Other providers use exact match only.
function findModel(models, modelId, aliasOrId) {
if (!models) return undefined;
const found = models.find(m => m.id === modelId);
if (found) return found;
if (!DOT_VERSION_PROVIDERS.has(aliasOrId)) return undefined;
const normalized = normalizeModelId(modelId);
if (normalized === modelId) return undefined;
return models.find(m => m.id === normalized);
}
export function isValidModel(aliasOrId, modelId, passthroughProviders = new Set()) {
if (passthroughProviders.has(aliasOrId)) return true;
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return false;
return !!findModel(models, modelId, aliasOrId);
}
export function findModelName(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return modelId;
const found = findModel(models, modelId, aliasOrId);
return found?.name || modelId;
}
export function getModelTargetFormat(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return null;
return modelTargetFormat(findModel(models, modelId, aliasOrId));
}
// Declared upstream formats for a model (registry `supportedFormats`). Drives the
// per-model guard on the sourceFormat-matched transport; null when undeclared.
export function getModelSupportedFormats(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return null;
return modelSupportedFormats(findModel(models, modelId, aliasOrId));
}
export function getModelType(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return null;
const found = findModel(models, modelId, aliasOrId);
return found?.kind || found?.type || null;
}
export function getModelUpstreamId(aliasOrId, modelId) {
// Split off thinking suffix "(level)" so lookup hits the base id; re-append it to
// the result so downstream applyThinking still sees the suffix (body.model is stripped separately).
const sufMatch = typeof modelId === "string" ? modelId.match(/\([^()]+\)\s*$/) : null;
const suffix = sufMatch ? sufMatch[0] : "";
const baseId = suffix ? modelId.slice(0, sufMatch.index).trim() : modelId;
const models = PROVIDER_MODELS[aliasOrId];
const found = findModel(models, baseId, aliasOrId);
const resolvedId = found?.upstreamModelId || found?.id;
if (resolvedId) {
const presetMatch = resolvedId.match(/\([^()]+\)\s*$/);
const presetSuffix = presetMatch?.[0] || "";
const resolvedBase = presetSuffix ? resolvedId.slice(0, presetMatch.index).trim() : resolvedId;
return resolvedBase + (suffix || presetSuffix);
}
if (aliasOrId === "cx" && typeof baseId === "string" && baseId.endsWith(CODEX_REVIEW_SUFFIX)) {
return baseId.slice(0, -CODEX_REVIEW_SUFFIX.length) + suffix;
}
return baseId + suffix;
}
export function getModelQuotaFamily(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
return modelQuotaFamily(findModel(models, modelId, aliasOrId));
}
// OAuth short aliases — derived from registry `alias` (single source). everything else: alias = id.
// vertex/vertex-partner keep alias=id (kept via the `|| id` fallback in consumers).
export const OAUTH_ALIASES = Object.fromEntries(
REGISTRY.filter(r => r.alias && r.alias !== r.id).map(r => [r.id, r.alias])
);
// Derived from PROVIDERS — no need to maintain manually
export const PROVIDER_ID_TO_ALIAS = Object.fromEntries(
Object.keys(PROVIDERS).map(id => [id, OAUTH_ALIASES[id] || id])
);
export function getModelsByProviderId(providerId) {
const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
return PROVIDER_MODELS[alias] || [];
}
// Get strip list for a model entry (explicit opt-in only)
// Returns array of content types to strip, e.g. ["image", "audio"]
export function getModelStrip(alias, modelId) {
return modelStrip(findModel(PROVIDER_MODELS[alias], modelId, alias));
}