diff --git a/tools/ui/src/lib/stores/models/props.svelte.ts b/tools/ui/src/lib/stores/models/props.svelte.ts index f7c20513a2..5b531d54a0 100644 --- a/tools/ui/src/lib/stores/models/props.svelte.ts +++ b/tools/ui/src/lib/stores/models/props.svelte.ts @@ -14,12 +14,17 @@ import { MODEL_PROPS_CACHE } from '$lib/constants'; import { FileTypeCategory, ModelModality } from '$lib/enums'; import { PropsService } from '$lib/services/props.service'; // direct imports between stores, not via the barrel, to avoid circular deps +import { backendsStore } from '$lib/stores/backends.svelte'; import { serverStore } from '$lib/stores/server.svelte'; // deep imports, not the '$lib/utils' barrel: it re-exports modules that reach back // into the stores, and going through it here would read a half-built module import { TTLCache } from '$lib/utils/cache-ttl'; import { detectThinkingSupport } from '$lib/utils/chat-template-thinking-detector'; -import { SvelteSet } from 'svelte/reactivity'; +import { rawModelId } from '$lib/utils/model-option-id'; +import { SvelteMap, SvelteSet } from 'svelte/reactivity'; + +/** Grace period before /props is retried for a model after a failed fetch. */ +const PROPS_FETCH_RETRY_COOLDOWN_MS = 30_000; /** * The slice of modelsStore the manager reads. Kept narrow on purpose so it @@ -29,6 +34,8 @@ import { SvelteSet } from 'svelte/reactivity'; export interface ModelPropsHost { /** Model rows the manager mirrors fetched modalities onto. */ activeModels: ModelOption[]; + /** Every enabled backend's models, used to resolve a model's owner. */ + readonly models: ModelOption[]; readonly selectedModelName: string | null; readonly loadedModelIds: string[]; isModelLoaded(modelId: string): boolean; @@ -45,6 +52,8 @@ export class ModelPropsManager { maxEntries: MODEL_PROPS_CACHE.MAX_ENTRIES, ttlMs: MODEL_PROPS_CACHE.TTL_MS }); + /** Last failed /props fetch per model; requests stay on cooldown after it. */ + private failedPropsAt = new SvelteMap(); private fetching = new SvelteSet(); /** @@ -159,10 +168,25 @@ export class ModelPropsManager { if (cached) return cached; + // never ask a server about a model another backend serves; a name that + // exists on two backends resolves to the active one's entry first + const option = this.host.models.find( + (m) => m.model === modelId || m.id === modelId || rawModelId(m.id) === modelId + ); + + if (option?.backendId && option.backendId !== backendsStore.active.id) return null; + if (serverStore.isRouterMode && !this.host.isModelLoaded(modelId)) { return null; } + // a failing model would otherwise be refetched on every reactive update + const failedAt = this.failedPropsAt.get(modelId); + + if (failedAt !== undefined && Date.now() - failedAt < PROPS_FETCH_RETRY_COOLDOWN_MS) { + return null; + } + if (this.fetching.has(modelId)) return null; this.fetching.add(modelId); @@ -172,9 +196,11 @@ export class ModelPropsManager { this.cache.set(modelId, props); this.cacheVersion++; + this.failedPropsAt.delete(modelId); return props; } catch (error) { + this.failedPropsAt.set(modelId, Date.now()); console.warn(`Failed to fetch props for model ${modelId}:`, error); return null;