diff --git a/surfsense_backend/app/services/quality_score.py b/surfsense_backend/app/services/quality_score.py index 1aa3e7eda..2ba23d8a8 100644 --- a/surfsense_backend/app/services/quality_score.py +++ b/surfsense_backend/app/services/quality_score.py @@ -26,8 +26,10 @@ from __future__ import annotations # Tunables (constants, not flags) # --------------------------------------------------------------------------- -# Top-K size for deterministic spread inside the locked tier. -_QUALITY_TOP_K: int = 5 +# Top-K size for deterministic spread inside the locked tier. Auto only +# ever pins one of the K best-scoring candidates, so this is the effective +# "shortlist" size regardless of how large the OpenRouter catalogue is. +_QUALITY_TOP_K: int = 4 # Hard health gate: any cfg whose best non-null uptime is below this % # is excluded from Auto-mode selection entirely. @@ -74,39 +76,20 @@ _TINY_LEGACY_PENALTY_PATTERNS: tuple[str, ...] = ( # --------------------------------------------------------------------------- # OpenRouter-side provider slug (the prefix before ``/`` in the model id). -# Tiers are coarse: frontier labs > strong open / fast-moving labs > -# specialist labs > everything else. +# Deliberately frontier-labs-only: anything not listed falls through to the +# neutral default (15) so smaller labs can't out-prestige the big four and +# crowd into the Auto top-K shortlist. PROVIDER_PRESTIGE_OR: dict[str, int] = { - # Frontier labs "openai": 50, "anthropic": 50, "google": 50, "x-ai": 50, - # Strong open / fast-moving labs - "deepseek": 38, - "qwen": 38, - "meta-llama": 38, - "mistralai": 38, - "cohere": 38, - "nvidia": 38, - "alibaba": 38, - # Specialist / regional / strong second-tier - "microsoft": 28, - "01-ai": 28, - "minimax": 28, - "moonshot": 28, - "z-ai": 28, - "nousresearch": 28, - "ai21": 28, - "perplexity": 28, - # Smaller / niche providers - "liquid": 18, - "cognitivecomputations": 18, - "venice": 18, - "inflection": 18, } # YAML provider field (the upstream API shape the operator selected). +# Frontier-labs-only, mirroring PROVIDER_PRESTIGE_OR; unlisted providers get +# the neutral default (15). ``ollama_chat`` / ``custom`` stay listed because +# they sit *below* the default — removing them would raise their score. PROVIDER_PRESTIGE_YAML: dict[str, int] = { "azure": 50, "openai": 50, @@ -114,16 +97,6 @@ PROVIDER_PRESTIGE_YAML: dict[str, int] = { "gemini": 50, "vertex_ai": 50, "xai": 50, - "mistral": 38, - "deepseek": 38, - "cohere": 38, - "groq": 30, - "together_ai": 28, - "fireworks_ai": 28, - "perplexity": 28, - "bedrock": 28, - "openrouter": 25, - "requesty": 25, "ollama_chat": 12, "custom": 12, } diff --git a/surfsense_web/lib/query-client/query-client.provider.check.tsx b/surfsense_web/lib/query-client/query-client.provider.check.tsx new file mode 100644 index 000000000..9a1beb1dd --- /dev/null +++ b/surfsense_web/lib/query-client/query-client.provider.check.tsx @@ -0,0 +1,40 @@ +/** + * Self-check: the app must run on ONE jotai store. + * + * Regression guard for the "screenshot never sent" bug: wrapping the tree in + * jotai-tanstack-query's QueryClientAtomProvider mounted a second jotai + * store, so hook writes (pending screenshot data URLs, mentions) + * were invisible to the chat stream engine's getDefaultStore() reads and + * user_images arrived empty at the backend. + * + * Run: pnpm exec tsx lib/query-client/query-client.provider.check.tsx + */ +import assert from "node:assert"; +import { getDefaultStore, useStore } from "jotai"; +import { queryClientAtom } from "jotai-tanstack-query"; +import { renderToString } from "react-dom/server"; +import { ReactQueryClientProvider } from "./query-client.provider"; + +let seenStore: unknown; +function Probe() { + seenStore = useStore(); + return null; +} + +renderToString( + + + +); + +assert.strictEqual( + seenStore, + getDefaultStore(), + "jotai hooks inside ReactQueryClientProvider must use the default store " + + "(a nested jotai would hide composer atom writes from the chat engine)" +); +assert.ok( + getDefaultStore().get(queryClientAtom), + "queryClientAtom must be hydrated into the default store" +); +console.log("OK: single jotai store, queryClientAtom hydrated"); diff --git a/surfsense_web/lib/query-client/query-client.provider.tsx b/surfsense_web/lib/query-client/query-client.provider.tsx index 30c6d9767..ac1857876 100644 --- a/surfsense_web/lib/query-client/query-client.provider.tsx +++ b/surfsense_web/lib/query-client/query-client.provider.tsx @@ -1,5 +1,7 @@ "use client"; -import { QueryClientAtomProvider } from "jotai-tanstack-query/react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { getDefaultStore } from "jotai"; +import { queryClientAtom } from "jotai-tanstack-query"; import dynamic from "next/dynamic"; import { queryClient } from "./client"; @@ -8,11 +10,20 @@ const ReactQueryDevtools = dynamic( { ssr: false } ); +// Do NOT use jotai-tanstack-query's QueryClientAtomProvider here: it mounts +// its own jotai , which creates a SECOND jotai store for the React +// tree. Hook writes (e.g. pending screenshot data URLs, @-mentions) then land +// in that provider store while the chat stream engine reads via +// getDefaultStore(), so user_images silently arrived empty at the backend. +// Instead, keep the whole app on the one default store and hydrate the +// query-client atom into it directly. +getDefaultStore().set(queryClientAtom, queryClient); + export function ReactQueryClientProvider({ children }: { children: React.ReactNode }) { return ( - + {children} {process.env.NODE_ENV === "development" && } - + ); }