- Cap maximum cooldown for rate limit handling in account unavailability and single-model chat flows

- Dynamic custom model fetching for model selection
This commit is contained in:
decolua 2026-04-24 16:14:18 +07:00
parent c42c0146ab
commit cca615eaff
19 changed files with 108 additions and 60 deletions

View file

@ -3,8 +3,8 @@
import { useState, useMemo, useEffect } from "react";
import PropTypes from "prop-types";
import Modal from "./Modal";
import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, getProviderAlias } from "@/shared/constants/providers";
// Provider order: OAuth first, then Free Tier, then API Key (matches dashboard/providers)
const PROVIDER_ORDER = [
@ -29,6 +29,7 @@ export default function ModelSelectModal({
const [searchQuery, setSearchQuery] = useState("");
const [combos, setCombos] = useState([]);
const [providerNodes, setProviderNodes] = useState([]);
const [customModels, setCustomModels] = useState([]);
const fetchCombos = async () => {
try {
@ -62,6 +63,22 @@ export default function ModelSelectModal({
if (isOpen) fetchProviderNodes();
}, [isOpen]);
const fetchCustomModels = async () => {
try {
const res = await fetch("/api/models/custom");
if (!res.ok) throw new Error(`Failed to fetch custom models: ${res.status}`);
const data = await res.json();
setCustomModels(data.models || []);
} catch (error) {
console.error("Error fetching custom models:", error);
setCustomModels([]);
}
};
useEffect(() => {
if (isOpen) fetchCustomModels();
}, [isOpen]);
const allProviders = useMemo(() => ({ ...OAUTH_PROVIDERS, ...FREE_PROVIDERS, ...FREE_TIER_PROVIDERS, ...APIKEY_PROVIDERS }), []);
// Group models by provider with priority order
@ -85,7 +102,7 @@ export default function ModelSelectModal({
});
sortedProviderIds.forEach((providerId) => {
const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
const alias = getProviderAlias(providerId);
const providerInfo = allProviders[providerId] || { name: providerId, color: "#666" };
const isCustomProvider = isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId);
@ -151,7 +168,7 @@ export default function ModelSelectModal({
// Custom models: if no hardcoded models (e.g. openrouter), show all aliases for this provider
// Otherwise only show aliases where aliasName === modelId ("Add Model" button pattern)
const hasHardcoded = hardcodedModels.length > 0;
const customModels = Object.entries(modelAliases)
const customAliasModels = Object.entries(modelAliases)
.filter(([aliasName, fullModel]) =>
fullModel.startsWith(`${alias}/`) &&
(hasHardcoded ? aliasName === fullModel.replace(`${alias}/`, "") : true) &&
@ -162,9 +179,16 @@ export default function ModelSelectModal({
return { id: modelId, name: aliasName, value: fullModel, isCustom: true };
});
// Custom models registered via /api/models/custom (provider "Add Model" button)
const customAliasIds = new Set(customAliasModels.map((m) => m.id));
const customRegisteredModels = customModels
.filter((m) => m.providerAlias === alias && !hardcodedIds.has(m.id) && !customAliasIds.has(m.id))
.map((m) => ({ id: m.id, name: m.name || m.id, value: `${alias}/${m.id}`, isCustom: true }));
const allModels = [
...hardcodedModels.map((m) => ({ id: m.id, name: m.name, value: `${alias}/${m.id}` })),
...customModels,
...customAliasModels,
...customRegisteredModels,
];
if (allModels.length > 0) {
@ -179,7 +203,7 @@ export default function ModelSelectModal({
});
return groups;
}, [activeProviders, modelAliases, allProviders, providerNodes]);
}, [activeProviders, modelAliases, allProviders, providerNodes, customModels]);
// Filter combos by search query
const filteredCombos = useMemo(() => {
@ -304,7 +328,7 @@ export default function ModelSelectModal({
const isPlaceholder = model.isPlaceholder;
return (
<button
key={model.id}
key={model.value}
onClick={() => handleSelect(model)}
title={isPlaceholder ? "Select to pre-fill, then edit model ID in the input" : undefined}
className={`