diff --git a/README.md b/README.md index 3b2fe3a9..36ad3aeb 100644 --- a/README.md +++ b/README.md @@ -1541,6 +1541,8 @@ Models: **Models:** Access 100+ models from all major providers through a single API key. +**Dashboard behavior:** OpenRouter models are managed from **Available Models**. Manual add, import, and auto-sync all update the same list. +
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 80960c72..421b1961 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -686,25 +686,25 @@ Additional processing layers in the translation pipeline: ## Supported API Endpoints -| Endpoint | Format | Handler | -| -------------------------------------------------- | ------------------ | ---------------------------------------------------- | -| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` | -| `POST /v1/messages` | Claude Messages | Same handler (auto-detected) | -| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` | -| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` | -| `GET /v1/embeddings` | Model listing | API route | -| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` | -| `GET /v1/images/generations` | Model listing | API route | -| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | Dedicated per-provider with model validation | -| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | Dedicated per-provider with model validation | -| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | Dedicated per-provider with model validation | -| `POST /v1/messages/count_tokens` | Claude Token Count | API route | -| `GET /v1/models` | OpenAI Models list | API route (chat + embedding + image + custom models) | -| `GET /api/models/catalog` | Catalog | All models grouped by provider + type | -| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API route | -| `GET/PUT/DELETE /api/settings/proxy` | Proxy Config | Network proxy configuration | -| `POST /api/settings/proxy/test` | Proxy Connectivity | Proxy health/connectivity test endpoint | -| `GET/POST/DELETE /api/provider-models` | Custom Models | Custom model management per provider | +| Endpoint | Format | Handler | +| -------------------------------------------------- | ------------------ | ------------------------------------------------------------------- | +| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` | +| `POST /v1/messages` | Claude Messages | Same handler (auto-detected) | +| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` | +| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` | +| `GET /v1/embeddings` | Model listing | API route | +| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` | +| `GET /v1/images/generations` | Model listing | API route | +| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | Dedicated per-provider with model validation | +| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | Dedicated per-provider with model validation | +| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | Dedicated per-provider with model validation | +| `POST /v1/messages/count_tokens` | Claude Token Count | API route | +| `GET /v1/models` | OpenAI Models list | API route (chat + embedding + image + custom models) | +| `GET /api/models/catalog` | Catalog | All models grouped by provider + type | +| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API route | +| `GET/PUT/DELETE /api/settings/proxy` | Proxy Config | Network proxy configuration | +| `POST /api/settings/proxy/test` | Proxy Connectivity | Proxy health/connectivity test endpoint | +| `GET/POST/DELETE /api/provider-models` | Provider Models | Provider model metadata backing custom and managed available models | ## Bypass Handler diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 3f47a4a6..ed35a0ba 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -596,6 +596,11 @@ curl -X POST http://localhost:20128/api/provider-models \ Or use Dashboard: **Providers → [Provider] → Custom Models**. +Notes: + +- OpenRouter and OpenAI/Anthropic-compatible providers are managed from **Available Models** only. Manual add, import, and auto-sync all land in the same available-model list, so there is no separate Custom Models section for those providers. +- The **Custom Models** section is intended for providers that do not expose managed available-model imports. + ### Dedicated Provider Routes Route requests directly to a specific provider with model validation: diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index ca1a3a1f..53c6e7a0 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -36,6 +36,7 @@ import { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey, } from "@/shared/constants/modelCompat"; +import { resolveManagedModelAlias } from "@/shared/utils/providerModelAliases"; type CompatByProtocolMap = Partial< Record< @@ -331,6 +332,10 @@ interface CompatibleModelsSectionProps { providerStorageAlias: string; providerDisplayAlias: string; modelAliases: Record; + fallbackModels?: CompatModelRow[]; + description: string; + inputLabel: string; + inputPlaceholder: string; copied?: string; onCopy: (text: string, key: string) => void; onSetAlias: (modelId: string, alias: string, providerStorageAlias?: string) => Promise; @@ -850,6 +855,7 @@ export default function ProviderDetailPage() { const isOpenAICompatible = isOpenAICompatibleProvider(providerId); const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId); const isCompatible = isOpenAICompatible || isAnthropicCompatible; + const isManagedAvailableModelsProvider = isCompatible || providerId === "openrouter"; const isSearchProvider = providerId.endsWith("-search"); const providerStorageAlias = isCompatible ? providerId : providerAlias; @@ -1469,7 +1475,10 @@ export default function ProviderDetailPage() { logs: [ t("foundModelsStartingImport", { count: newModels.length }), ...(newModels.length < fetchedModels.length - ? [t("skippingExistingModels", { count: fetchedModels.length - newModels.length }) || `Skipping ${fetchedModels.length - newModels.length} existing models`] + ? [ + t("skippingExistingModels", { count: fetchedModels.length - newModels.length }) || + `Skipping ${fetchedModels.length - newModels.length} existing models`, + ] : []), ], })); @@ -1664,6 +1673,14 @@ export default function ProviderDetailPage() { }; const [clearingModels, setClearingModels] = useState(false); + const providerAliasEntries = useMemo( + () => + Object.entries(modelAliases).filter(([, model]) => + (model as string).startsWith(`${providerStorageAlias}/`) + ), + [modelAliases, providerStorageAlias] + ); + const handleClearAllModels = async () => { if (clearingModels) return; if (!confirm(t("clearAllModelsConfirm"))) return; @@ -1675,11 +1692,8 @@ export default function ProviderDetailPage() { ); if (res.ok) { // Also delete all aliases that belong to this provider - const aliasEntries = Object.entries(modelAliases).filter(([, model]) => - (model as string).startsWith(`${providerStorageAlias}/`) - ); await Promise.all( - aliasEntries.map(([alias]) => + providerAliasEntries.map(([alias]) => fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, { method: "DELETE", }).catch(() => {}) @@ -1806,7 +1820,8 @@ export default function ProviderDetailPage() { ); - const clearAllButton = modelMeta.customModels.length > 0 && ( + const clearAllButton = (modelMeta.customModels.length > 0 || + providerAliasEntries.length > 0) && (