Enhance token refresh logic and improve MITM server handling

- Introduced a caching mechanism for in-flight token refresh requests to prevent race conditions and reduce unnecessary API calls.
- Added error handling for unrecoverable refresh errors, ensuring that the application can gracefully handle token reuse and invalidation scenarios.
- Updated the MITM server management to handle port 443 conflicts, allowing users to kill processes occupying the port before starting the server.
- Improved user feedback in the MitmServerCard component regarding port conflicts and admin privileges.
- Refactored the ComboList component to streamline the display of media provider combos.

This update aims to enhance the reliability and user experience of the token management and MITM functionalities.
This commit is contained in:
decolua 2026-05-03 22:10:03 +07:00
parent b8e3a46add
commit 4ba546afe7
17 changed files with 680 additions and 229 deletions

View file

@ -95,6 +95,19 @@ export default function ModelSelectModal({
const groupedModels = useMemo(() => {
const groups = {};
// Kinds where the provider IS the model (no per-model selection needed)
const PROVIDER_AS_MODEL_KINDS = new Set(["webSearch", "webFetch"]);
// Kinds that map directly to model.type field
const TYPED_KINDS = new Set(["image", "tts", "stt", "embedding", "imageToText"]);
// For these kinds, providers without hardcoded models can still be picked (provider-as-model fallback)
const ALLOW_PROVIDER_FALLBACK_KINDS = new Set(["tts", "image", "webFetch"]);
// Filter a models[] array by kindFilter (keep only matching m.type)
const filterByKind = (models) => {
if (!kindFilter || !TYPED_KINDS.has(kindFilter)) return models;
return models.filter((m) => m.isPlaceholder || m.type === kindFilter);
};
// Get all active provider IDs from connections (filtered by kindFilter if set)
const activeConnectionIds = filteredActiveProviders.map(p => p.provider);
@ -121,6 +134,17 @@ export default function ModelSelectModal({
const providerInfo = allProviders[providerId] || { name: providerId, color: "#666" };
const isCustomProvider = isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId);
// For provider-as-model kinds (webSearch/webFetch): emit a single entry where value === providerId
if (kindFilter && PROVIDER_AS_MODEL_KINDS.has(kindFilter)) {
groups[providerId] = {
name: providerInfo.name,
alias,
color: providerInfo.color,
models: [{ id: providerId, name: providerInfo.name, value: providerId }],
};
return;
}
if (providerInfo.passthroughModels) {
const aliasModels = Object.entries(modelAliases)
.filter(([, fullModel]) => fullModel.startsWith(`${alias}/`))
@ -130,7 +154,20 @@ export default function ModelSelectModal({
value: fullModel,
}));
if (aliasModels.length > 0) {
// For typed kinds, only include hardcoded typed models (aliases are typically LLM-only and lack type info)
let combined = aliasModels;
if (kindFilter && TYPED_KINDS.has(kindFilter)) {
combined = getModelsByProviderId(providerId)
.filter((m) => m.type === kindFilter)
.map((m) => ({ id: m.id, name: m.name, value: `${alias}/${m.id}`, type: m.type }));
// Fallback: provider-as-model when no hardcoded models match (tts/image/webFetch only)
if (combined.length === 0 && ALLOW_PROVIDER_FALLBACK_KINDS.has(kindFilter)) {
const supports = (providerInfo.serviceKinds || ["llm"]).includes(kindFilter);
if (supports) combined = [{ id: providerId, name: providerInfo.name, value: alias }];
}
}
if (combined.length > 0) {
// Check for custom name from providerNodes (for compatible providers)
const matchedNode = providerNodes.find(node => node.id === providerId);
const displayName = matchedNode?.name || providerInfo.name;
@ -139,10 +176,12 @@ export default function ModelSelectModal({
name: displayName,
alias: alias,
color: providerInfo.color,
models: aliasModels,
models: combined,
};
}
} else if (isCustomProvider) {
// Custom (openai/anthropic-compatible) providers are LLM-only — skip for typed media kinds
if (kindFilter && TYPED_KINDS.has(kindFilter)) return;
// Find connection object to get prefix synchronously without waiting for providerNodes fetch
const connection = activeProviders.find(p => p.provider === providerId);
const matchedNode = providerNodes.find(node => node.id === providerId);
@ -200,11 +239,20 @@ export default function ModelSelectModal({
.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}` })),
let allModels = filterByKind([
...hardcodedModels.map((m) => ({ id: m.id, name: m.name, value: `${alias}/${m.id}`, type: m.type })),
...customAliasModels,
...customRegisteredModels,
];
]);
// Provider-as-model fallback: providers that support the kind but have no hardcoded models
// can still be picked (value = providerAlias). Skips embedding (always needs model).
if (allModels.length === 0 && kindFilter && ALLOW_PROVIDER_FALLBACK_KINDS.has(kindFilter)) {
const supports = (providerInfo.serviceKinds || ["llm"]).includes(kindFilter);
if (supports) {
allModels = [{ id: providerId, name: providerInfo.name, value: alias }];
}
}
if (allModels.length > 0) {
groups[providerId] = {