diff --git a/CLAUDE.md b/CLAUDE.md index b96b496..df9487d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,7 @@ This boundary is **enforced by Next.js bundling at build time** (importing `pg`/ 1. `OPERATING INSTRUCTIONS` — `DEFAULT_OPERATING_INSTRUCTION` (`src/lib/shared/llm/system-instructions.ts`) 2. `RUNTIME DATE CONTEXT` — current UTC timestamp + user timezone (from `X-User-Timezone` header) -3. **Provider overlay** (`PROVIDER OVERLAY: ALIBABA|MOONSHOTAI`) — keyed by the model's **provider org**, not its nickname (alibaba=Qwen, moonshotai=Kimi). Always applied for a supported model. +3. **Provider overlay** (`PROVIDER OVERLAY: ZAI`) — keyed by the model's **provider org** (zai=GLM). Always applied for the supported model. `resolvePromptProvider()` is param-less and always returns `"zai"`. 4. `IDENTITY AND TONE CONTEXT` — `DEFAULT_SOUL_FALLBACK_INSTRUCTION` (`src/lib/shared/llm/system-instructions.ts`) 5. `AUTH USER CONTEXT` — authenticated user id, name, email @@ -136,16 +136,15 @@ The only tools are the two Tavily web-search tools, and both are registered toge ### Model Registry -All models are defined in `src/lib/shared/llm/models.ts`: +The app runs on a **single model**, defined in `src/lib/shared/llm/models.ts`: -| Key | Model ID | Display Name | -| ---------------------- | ---------------------- | ------------ | -| `ALIBABA_QWEN3_7_MAX` | `alibaba/qwen3.7-max` | Qwen 3.7 Max | -| `MOONSHOTAI_KIMI_K2_6` | `moonshotai/kimi-k2.6` | Kimi K2.6 | +| Key | Model ID | Display Name | +| ------------- | ------------- | ------------ | +| `ZAI_GLM_5_2` | `zai/glm-5.2` | GLM 5.2 | -- `MODEL_SELECTOR_MODELS` — the chat selector subset: Qwen 3.7 Max and Kimi K2.6. +- `DEFAULT_MODEL` (`= AvailableModels.ZAI_GLM_5_2`) is the single model used everywhere. There is **no model-selector UI** and no per-user model persistence — the client always submits `DEFAULT_MODEL`, and the model still flows through requests/threads so the API can validate it. - The agent is text-only: all chat input is plain text (no image, file, or PDF input). -- Adding a model means updating `AvailableModels`, `ModelInfos`, `SUPPORTED_MODELS`, and optionally `MODEL_SELECTOR_MODELS`. `/api/models` filters this registry by configured keys (`getModels()` in `src/lib/actions/api-keys.ts`). +- Adding a model means updating `AvailableModels`, `ModelInfos`, and `SUPPORTED_MODELS` (and re-introducing selector UI if more than one model is ever exposed). `/api/models` filters this registry by configured keys (`getModels()` in `src/lib/actions/api-keys.ts`). ### Thread Storage @@ -240,7 +239,7 @@ src/ # follow-up-questions agent/messages/ # Message rendering (user, assistant, queued, activity timeline) agent/markdown/ # Memoized markdown renderer - agent/prompt-form/ # PromptForm, ModelSelector + agent/prompt-form/ # PromptForm (single model; no selector) app-sidebar.tsx # Sidebar shell (lazy-loads SearchChats + NavThreads) nav-threads.tsx # Thread list + client-side pinning (localStorage) nav-user.tsx # Account menu + sign-out @@ -250,15 +249,14 @@ src/ layout/ # route group layout ui/ # shadcn/ui primitives (base-lyra/stone) + ShikiCode hooks/ - agent/ # use-models (server-seeded models context), - # use-persistent-selected-model (localStorage-backed) + agent/ # use-models (server-seeded models context) lib/ actions/api-keys.ts # getModels() server action brand/colors.ts # App brand colors (used by layout/manifest) editor/highlighter.ts server/ agent-context.ts # buildAgentSystemInstruction - agent-prompt-steering.ts # Provider overlays (Qwen/Kimi tuning) + agent-prompt-steering.ts # Provider overlay (GLM tuning) agent-route.ts # parseAgentStreamRequest, createAgentStreamResponse agent-runtime-config.ts # Runtime constants (no env knobs) auth.ts / auth-session.ts # isAuthConfigured, getRequestSession @@ -280,7 +278,7 @@ src/ agent/messages.ts # AgentStreamEvent, Message, ToolInvocation, run statuses agent/reasoning-privacy.ts # sanitizeReasoningForDisplay agent-request-limits.ts # Message/char limit defaults - llm/models.ts # AvailableModels, ModelInfos, SUPPORTED/SELECTOR/RESEARCH + llm/models.ts # AvailableModels, ModelInfos, SUPPORTED_MODELS, DEFAULT_MODEL llm/system-instructions.ts # DEFAULT_OPERATING_INSTRUCTION, DEFAULT_SOUL_FALLBACK_INSTRUCTION threads.ts # Thread type, sort/normalize/deriveThreadTitle proxy.ts # Next.js middleware (named export `proxy` + `config`) @@ -357,5 +355,5 @@ Request size limits, stream/gateway timeouts, tool-step budgets, and body-size l - The **mock smoke test uses the production server** (`next start`), so build first. - `pnpm.onlyBuiltDependencies` already approves the `sharp` build script — do not run `pnpm approve-builds`. - Don't reintroduce Sentry/PostHog/OpenTelemetry; they were intentionally removed in favor of Vercel Analytics/Speed Insights + structured logs. -- Pinning and selected model are **client-side localStorage** — there are no server columns or APIs for them. +- Pinning is **client-side localStorage** — there is no server column or API for it. There is no model selection or persistence: the app runs on the single `DEFAULT_MODEL` (GLM 5.2). - After signing up via `/api/auth/sign-up/email`, the session cookie is set automatically; no separate sign-in is needed. diff --git a/README.md b/README.md index 6e64cac..b0b186a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Chloei -Chloei is a Next.js 16 chat app backed by Vercel AI Gateway. It currently exposes a curated model selector that defaults to Qwen 3.7 Max and also includes Kimi K2.6, and offers optional Tavily web search and Better Auth email/password authentication with PostgreSQL-backed users and sessions. +Chloei is a Next.js 16 chat app backed by Vercel AI Gateway. It runs on a single model — GLM 5.2 (`zai/glm-5.2`) — and offers optional Tavily web search and Better Auth email/password authentication with PostgreSQL-backed users and sessions. ## Documentation diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index 4122659..7680739 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -5,10 +5,6 @@ import { ModelsProvider } from "@/hooks/agent/use-models" import { getModels } from "@/lib/actions/api-keys" import { isAuthConfigured } from "@/lib/server/auth" import { getCurrentViewer } from "@/lib/server/auth-session" -import { - getModelSelectorModels, - resolveDefaultModelSelectorModel, -} from "@/lib/shared" export const dynamic = "force-dynamic" @@ -24,19 +20,10 @@ export default async function Home() { } const availableModels = getModels() - const modelSelectorModels = getModelSelectorModels(availableModels) - - const resolvedInitialSelectedModel = - modelSelectorModels.length > 0 - ? resolveDefaultModelSelectorModel(modelSelectorModels) - : null return ( - + ) } diff --git a/src/app/api/agent/follow-ups/route.ts b/src/app/api/agent/follow-ups/route.ts index f7f02dd..3c4d9f0 100644 --- a/src/app/api/agent/follow-ups/route.ts +++ b/src/app/api/agent/follow-ups/route.ts @@ -41,7 +41,8 @@ function createEmptyFollowUpResponse(requestId: string) { } function isAvailableModel(model: ModelType): boolean { - return getModels().some((availableModel) => availableModel.id === model) + const availableIds = new Set(getModels().map((m) => m.id)) + return availableIds.has(model) } export async function POST(request: NextRequest) { diff --git a/src/app/api/agent/route.ts b/src/app/api/agent/route.ts index 4d5ac13..df8b284 100644 --- a/src/app/api/agent/route.ts +++ b/src/app/api/agent/route.ts @@ -109,7 +109,7 @@ export async function POST(request: NextRequest) { const requestNow = new Date() const userTimeZone = resolveUserTimeZone(request) const featureFlags = await resolveAgentFeatureFlags() - const promptProvider = resolvePromptProvider(selectedModel) + const promptProvider = resolvePromptProvider() const systemInstruction = buildAgentSystemInstruction( { id: session.user.id, diff --git a/src/components/agent/home/home-content.tsx b/src/components/agent/home/home-content.tsx index 206471d..8eecf85 100644 --- a/src/components/agent/home/home-content.tsx +++ b/src/components/agent/home/home-content.tsx @@ -51,13 +51,7 @@ const Messages = dynamic( } ) -export function HomePageContent({ - initialSelectedModel, - viewer, -}: { - initialSelectedModel?: ModelType | null - viewer: AuthViewer -}) { +export function HomePageContent({ viewer }: { viewer: AuthViewer }) { const [isPending, startTransition] = useTransition() const [isFallbackEnteringConversation, setIsFallbackEnteringConversation] = useState(false) @@ -375,7 +369,6 @@ export function HomePageContent({ onStopStream={handleStopStream} isStreaming={streamingState} dismissKeyboardOnSubmit={isMobile} - initialSelectedModel={initialSelectedModel} transition={{ isPending, startTransition }} viewTransitionName={promptViewTransitionName} /> @@ -426,7 +419,6 @@ export function HomePageContent({ onClearQueuedMessage={clearQueuedSubmission} isStreaming={streamingState} dismissKeyboardOnSubmit={isMobile} - initialSelectedModel={initialSelectedModel} transition={{ isPending, startTransition }} viewTransitionName={promptViewTransitionName} /> diff --git a/src/components/agent/messages/user-message.tsx b/src/components/agent/messages/user-message.tsx index c4ce834..105abbf 100644 --- a/src/components/agent/messages/user-message.tsx +++ b/src/components/agent/messages/user-message.tsx @@ -1,23 +1,14 @@ import { Check, Copy, CornerRightUp, Loader2, X } from "lucide-react" -import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { toast } from "sonner" -import { useModels } from "@/hooks/agent/use-models" import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard" -import { - getModelSelectorModels, - isModelSelectorModel, - isModelType, - type Message, - type ModelType, - resolveDefaultModelSelectorModel, -} from "@/lib/shared" +import { DEFAULT_MODEL, type Message, type ModelType } from "@/lib/shared" import { cn } from "@/lib/utils" import { Button } from "../../ui/button" import { Textarea } from "../../ui/textarea" import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip" -import { ModelSelector } from "../prompt-form/model-selector" import { agentShellFrameClass, agentShellHighlightClass, @@ -45,30 +36,8 @@ export function UserMessage({ newModel: ModelType }) => Promise | void }) { - const { data: availableModels } = useModels() - const modelSelectorModels = useMemo( - () => getModelSelectorModels(availableModels), - [availableModels] - ) - const initialModel = useMemo(() => { - const selectedModel = message.metadata?.selectedModel - if (isModelType(selectedModel) && isModelSelectorModel(selectedModel)) { - return selectedModel - } - - if ( - isModelType(message.llmModel) && - isModelSelectorModel(message.llmModel) - ) { - return message.llmModel - } - - return resolveDefaultModelSelectorModel(modelSelectorModels) - }, [message.llmModel, message.metadata?.selectedModel, modelSelectorModels]) - const [isEditing, setIsEditing] = useState(false) const [editValue, setEditValue] = useState(message.content) - const [selectedModel, setSelectedModel] = useState(initialModel) const [isEditPending, setIsEditPending] = useState(false) const messageContentRef = useRef(null) const textareaRef = useRef(null) @@ -93,23 +62,15 @@ export function UserMessage({ } }, [isEditing]) - const handleSelectModel = useCallback((model: ModelType | null) => { - if (model) { - setSelectedModel(model) - } - }, []) - const handleStartEditing = useCallback(() => { setEditValue(message.content) - setSelectedModel(initialModel) setIsEditing(true) - }, [initialModel, message.content]) + }, [message.content]) const handleStopEditing = useCallback(() => { setIsEditing(false) setEditValue(message.content) - setSelectedModel(initialModel) - }, [message.content, initialModel]) + }, [message.content]) const handleSubmit = useCallback(async () => { const trimmedValue = editValue.trim() @@ -129,7 +90,7 @@ export function UserMessage({ await onEditMessage({ messageId: message.id, newContent: trimmedValue, - newModel: selectedModel, + newModel: DEFAULT_MODEL, }) setIsEditing(false) } catch (error) { @@ -139,7 +100,7 @@ export function UserMessage({ } finally { setIsEditPending(false) } - }, [editValue, handleStopEditing, message.id, onEditMessage, selectedModel]) + }, [editValue, handleStopEditing, message.id, onEditMessage]) const onKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -221,14 +182,7 @@ export function UserMessage({ onKeyDown={onKeyDown} /> -
-
- -
- +
- - - {!isModelSelectorOpen && ( - - Model Selector - - )} - - { - if (!shouldPreventCloseAutoFocusRef.current) { - return true - } - - shouldPreventCloseAutoFocusRef.current = false - return false - }} - className="flex w-fit flex-col gap-0.5 overflow-hidden rounded-none p-0" - > -
- {modelSelectorModels.length > 0 ? ( - modelSelectorModels.map((model) => ( - - )) - ) : ( -
- No models available. Ask an admin to configure a provider API key - on the server. -
- )} -
-
- - ) -} diff --git a/src/components/agent/prompt-form/prompt-form.tsx b/src/components/agent/prompt-form/prompt-form.tsx index c62e0e0..29f092d 100644 --- a/src/components/agent/prompt-form/prompt-form.tsx +++ b/src/components/agent/prompt-form/prompt-form.tsx @@ -17,8 +17,7 @@ import { RefreshGlow } from "@/components/graphics/effects/refresh-glow" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { useModels } from "@/hooks/agent/use-models" -import { usePersistentSelectedModel } from "@/hooks/agent/use-persistent-selected-model" -import { getModelSelectorModels, type ModelType } from "@/lib/shared" +import { DEFAULT_MODEL, type ModelType } from "@/lib/shared" import { cn } from "@/lib/utils" import { QueuedAction } from "../messages/queued-message" @@ -29,7 +28,6 @@ import { agentSurfaceBackgroundClass, agentSurfaceClass, } from "../shared/shell-styles" -import { ModelSelector } from "./model-selector" interface QueuedPromptSubmission { message: string @@ -44,7 +42,6 @@ export function PromptForm({ dismissKeyboardOnSubmit = false, onFocus, onBlur, - initialSelectedModel, dockToBottomOnHome = false, queuedSubmission, onClearQueuedMessage, @@ -59,7 +56,6 @@ export function PromptForm({ dismissKeyboardOnSubmit?: boolean onFocus?: () => void onBlur?: () => void - initialSelectedModel?: ModelType | null dockToBottomOnHome?: boolean queuedSubmission?: QueuedPromptSubmission | null onClearQueuedMessage?: () => void @@ -80,14 +76,7 @@ export function PromptForm({ const textareaRef = useRef(null) const { data: availableModels } = useModels() - const modelSelectorModels = useMemo( - () => getModelSelectorModels(availableModels), - [availableModels] - ) - const { selectedModel, setSelectedModel } = usePersistentSelectedModel( - initialSelectedModel, - modelSelectorModels - ) + const isModelConfigured = availableModels.length > 0 const formStyle = useMemo( () => viewTransitionName @@ -98,22 +87,12 @@ export function PromptForm({ [viewTransitionName] ) - const resolvedSelectedModel = selectedModel - - const handleSelectModel = useCallback( - (model: ModelType | null) => { - setSelectedModel(model) - }, - [setSelectedModel] - ) - const restoreQueuedSubmission = useCallback(() => { if (!queuedSubmission) { return } setMessage(queuedSubmission.message) - setSelectedModel(queuedSubmission.model) onClearQueuedMessage?.() window.requestAnimationFrame(() => { @@ -127,7 +106,7 @@ export function PromptForm({ textarea.setSelectionRange(cursorPosition, cursorPosition) textarea.scrollTop = textarea.scrollHeight }) - }, [onClearQueuedMessage, queuedSubmission, setSelectedModel]) + }, [onClearQueuedMessage, queuedSubmission]) const submitPrompt = useCallback(() => { const nextMessage = message.trim() @@ -137,7 +116,7 @@ export function PromptForm({ return true } - if (!nextMessage || !resolvedSelectedModel || isFormPending) { + if (!nextMessage || !isModelConfigured || isFormPending) { return false } @@ -149,18 +128,18 @@ export function PromptForm({ } } - onSubmit?.(nextMessage, resolvedSelectedModel, isStreaming) + onSubmit?.(nextMessage, DEFAULT_MODEL, isStreaming) setMessage("") return true }, [ dismissKeyboardOnSubmit, isFormPending, + isModelConfigured, isStreaming, message, onStopStream, onSubmit, - resolvedSelectedModel, ]) const handleSubmit = useCallback( @@ -200,7 +179,7 @@ export function PromptForm({ }, [isStreaming, onStopStream]) const isSubmitButtonDisabled = - isFormPending || !resolvedSelectedModel || (!isStreaming && !trimmedMessage) + isFormPending || !isModelConfigured || (!isStreaming && !trimmedMessage) return (
-
-
- -
- +
- {!resolvedSelectedModel && ( + {!isModelConfigured && (

Configure `AI_GATEWAY_API_KEY` on the server to enable model access.

diff --git a/src/hooks/agent/persistent-selected-model-utils.ts b/src/hooks/agent/persistent-selected-model-utils.ts deleted file mode 100644 index 7b8aa2e..0000000 --- a/src/hooks/agent/persistent-selected-model-utils.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - isModelSelectorModel, - isModelType, - type ModelInfo, - type ModelType, -} from "@/lib/shared" - -const STORED_SELECTED_MODEL_VERSION = 1 - -interface StoredSelectedModel { - model: ModelType - source: "user" - version: typeof STORED_SELECTED_MODEL_VERSION -} - -export function serializeStoredSelectedModel( - model: ModelType -): StoredSelectedModel { - return { - model, - source: "user", - version: STORED_SELECTED_MODEL_VERSION, - } -} - -export function parseStoredSelectedModel(value: unknown): ModelType | null { - if (typeof value !== "string") { - return null - } - - if (isModelType(value) && isModelSelectorModel(value)) { - return value - } - - try { - const parsed: unknown = JSON.parse(value) - - if ( - parsed && - typeof parsed === "object" && - "model" in parsed && - "source" in parsed && - "version" in parsed && - parsed.source === "user" && - parsed.version === STORED_SELECTED_MODEL_VERSION && - isModelType(parsed.model) && - isModelSelectorModel(parsed.model) - ) { - return parsed.model - } - } catch { - return null - } - - return null -} - -export function resolvePersistedSelectedModel(params: { - storedModel: ModelType | null - currentModel: ModelType | null - initialSelectedModel: ModelType | null | undefined - availableModels: ModelInfo[] -}): ModelType | null { - const availableModelIds = new Set( - params.availableModels - .map((model) => model.id) - .filter((modelId) => isModelSelectorModel(modelId)) - ) - const fallbackModel = - params.initialSelectedModel && - availableModelIds.has(params.initialSelectedModel) - ? params.initialSelectedModel - : (params.availableModels.find((model) => availableModelIds.has(model.id)) - ?.id ?? null) - - if (params.storedModel && availableModelIds.has(params.storedModel)) { - return params.storedModel - } - - if (params.currentModel && availableModelIds.has(params.currentModel)) { - return params.currentModel - } - - return fallbackModel -} diff --git a/src/hooks/agent/use-persistent-selected-model.tsx b/src/hooks/agent/use-persistent-selected-model.tsx deleted file mode 100644 index 28a55b1..0000000 --- a/src/hooks/agent/use-persistent-selected-model.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client" - -import { useCallback, useEffect, useMemo, useState } from "react" - -import { - MODEL_SELECTOR_STORAGE_KEY, - MODEL_SELECTOR_UPDATED_EVENT, -} from "@/lib/constants" -import { - isModelSelectorModel, - type ModelInfo, - type ModelType, -} from "@/lib/shared" - -import { - parseStoredSelectedModel, - resolvePersistedSelectedModel, - serializeStoredSelectedModel, -} from "./persistent-selected-model-utils" - -function readStoredSelectedModel(): ModelType | null { - if (typeof window === "undefined") { - return null - } - - const value = window.localStorage.getItem(MODEL_SELECTOR_STORAGE_KEY) - return parseStoredSelectedModel(value) -} - -function writeStoredSelectedModel(model: ModelType | null) { - if (typeof window === "undefined") { - return - } - - if (model) { - window.localStorage.setItem( - MODEL_SELECTOR_STORAGE_KEY, - JSON.stringify(serializeStoredSelectedModel(model)) - ) - } else { - window.localStorage.removeItem(MODEL_SELECTOR_STORAGE_KEY) - } - - window.dispatchEvent(new CustomEvent(MODEL_SELECTOR_UPDATED_EVENT)) -} - -export function usePersistentSelectedModel( - initialSelectedModel: ModelType | null | undefined, - availableModels: ModelInfo[] -) { - const availableModelIds = useMemo( - () => - new Set( - availableModels - .map((model) => model.id) - .filter((modelId) => isModelSelectorModel(modelId)) - ), - [availableModels] - ) - - const fallbackModel = resolvePersistedSelectedModel({ - storedModel: null, - currentModel: null, - initialSelectedModel, - availableModels, - }) - - const [selectedModel, setSelectedModel] = useState( - initialSelectedModel ?? null - ) - - useEffect(() => { - const syncSelectedModel = () => { - const storedModel = readStoredSelectedModel() - const nextSelectedModel = resolvePersistedSelectedModel({ - storedModel, - currentModel: selectedModel, - initialSelectedModel, - availableModels, - }) - - if (nextSelectedModel === selectedModel) { - return - } - - setSelectedModel(nextSelectedModel) - } - - syncSelectedModel() - - const handleStorage = (event: StorageEvent) => { - if (event.key && event.key !== MODEL_SELECTOR_STORAGE_KEY) { - return - } - - syncSelectedModel() - } - - const handleModelUpdate = () => { - syncSelectedModel() - } - - window.addEventListener("storage", handleStorage) - window.addEventListener(MODEL_SELECTOR_UPDATED_EVENT, handleModelUpdate) - - return () => { - window.removeEventListener("storage", handleStorage) - window.removeEventListener( - MODEL_SELECTOR_UPDATED_EVENT, - handleModelUpdate - ) - } - }, [availableModels, fallbackModel, initialSelectedModel, selectedModel]) - - const persistSelectedModel = useCallback((model: ModelType | null) => { - setSelectedModel(model) - writeStoredSelectedModel(model) - }, []) - - const resolvedSelectedModel = - selectedModel && availableModelIds.has(selectedModel) - ? selectedModel - : fallbackModel - - return { - selectedModel: resolvedSelectedModel, - setSelectedModel: persistSelectedModel, - } -} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 7395456..ac62a87 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,5 +1,2 @@ -export const MODEL_SELECTOR_STORAGE_KEY = "model-selector-state" -export const MODEL_SELECTOR_UPDATED_EVENT = "model-selector-updated" - export const ASSISTANT_EMPTY_RESPONSE_FALLBACK = "Sorry, I couldn't generate a response from that input. Please retry." diff --git a/src/lib/server/agent-prompt-steering.ts b/src/lib/server/agent-prompt-steering.ts index cbbfcc4..6411afb 100644 --- a/src/lib/server/agent-prompt-steering.ts +++ b/src/lib/server/agent-prompt-steering.ts @@ -1,6 +1,4 @@ -import type { ModelType } from "@/lib/shared" - -export type PromptProvider = "alibaba" | "moonshotai" | "zai" +export type PromptProvider = "zai" interface PromptSteeringBlock { label: string @@ -13,22 +11,6 @@ interface CreatePromptSteeringBlocksParams { } const PROVIDER_OVERLAYS: Record = { - alibaba: ` -Use Qwen reasoning mode efficiently. -- Take advantage of the long context window: skim and cite earlier turns before re-asking the user for information already present. -- Prefer direct execution and verification over speculative narration. -- On format-sensitive tasks, do a literal final-format check before finishing. -- Treat hard word, line, and sentence caps as hard caps. Count the final output when close to the limit. -- After tool use, synthesize the result and stop. Do not replay raw tool traces. -`.trim(), - moonshotai: ` -Use Kimi reasoning mode efficiently. -- Take advantage of the long context window: skim and cite earlier turns before re-asking the user for information already present. -- Prefer direct execution and verification over speculative narration. -- On format-sensitive tasks, do a literal final-format check before finishing. -- Treat hard word, line, and sentence caps as hard caps. Count the final output when close to the limit. -- After tool use, synthesize the result and stop. Do not replay raw tool traces. -`.trim(), zai: ` Use GLM reasoning mode efficiently. - Take advantage of the long context window: skim and cite earlier turns before re-asking the user for information already present. @@ -39,20 +21,8 @@ Use GLM reasoning mode efficiently. `.trim(), } -export function resolvePromptProvider(model: ModelType): PromptProvider { - if (model.startsWith("alibaba/")) { - return "alibaba" - } - - if (model.startsWith("moonshotai/")) { - return "moonshotai" - } - - if (model.startsWith("zai/")) { - return "zai" - } - - throw new Error(`Unsupported model provider for model: ${model}`) +export function resolvePromptProvider(): PromptProvider { + return "zai" } export function createPromptSteeringBlocks( diff --git a/src/lib/server/agent-route.ts b/src/lib/server/agent-route.ts index 5a84fa2..98065f0 100644 --- a/src/lib/server/agent-route.ts +++ b/src/lib/server/agent-route.ts @@ -9,10 +9,9 @@ import type { AgentFeatureFlags } from "@/lib/server/integration-flags" import { type AgentStreamEvent, ALL_MODELS, - MODEL_SELECTOR_MODELS, + DEFAULT_MODEL, type ModelInfo, type ModelType, - resolveDefaultModelSelectorModel, } from "@/lib/shared" import { @@ -238,7 +237,8 @@ function isAvailableModel( models: readonly Pick[], targetModel: ModelType ): boolean { - return models.some((model) => model.id === targetModel) + const availableIds = new Set(models.map((model) => model.id)) + return availableIds.has(targetModel) } function getTotalMessageChars( @@ -314,14 +314,7 @@ export function parseAgentStreamRequest( }) } - const availableModelIds = new Set( - params.availableModels.map((model) => model.id) - ) - const chatModels = MODEL_SELECTOR_MODELS.flatMap((modelId) => - availableModelIds.has(modelId) ? [{ id: modelId }] : [] - ) - const selectedModelCandidate = - parsed.data.model ?? resolveDefaultModelSelectorModel(chatModels) + const selectedModelCandidate = parsed.data.model ?? DEFAULT_MODEL if (!isSupportedModel(selectedModelCandidate)) { return createJsonErrorResponse({ @@ -332,7 +325,7 @@ export function parseAgentStreamRequest( }) } - if (!isAvailableModel(chatModels, selectedModelCandidate)) { + if (!isAvailableModel(params.availableModels, selectedModelCandidate)) { return createJsonErrorResponse({ requestId: params.requestId, error: "Unsupported model selected.", diff --git a/src/lib/server/llm/agent-runtime-synthesis-gating.ts b/src/lib/server/llm/agent-runtime-synthesis-gating.ts index bc53307..6ea25db 100644 --- a/src/lib/server/llm/agent-runtime-synthesis-gating.ts +++ b/src/lib/server/llm/agent-runtime-synthesis-gating.ts @@ -16,9 +16,9 @@ export function shouldNudgeMidBudgetSynthesis( if (toolMaxSteps <= 3) { return false } - // Kick in at one-third of budget. The failing 10-K tasks (e.g. Kimi K2.6) - // tend to stop naturally with empty text after only 5-8 tool calls - // (~steps 4-7 of 20); half-budget fires too late to reach them. + // Kick in at one-third of budget. The failing 10-K tasks tend to stop + // naturally with empty text after only 5-8 tool calls (~steps 4-7 of 20); + // half-budget fires too late to reach them. const threshold = Math.max(2, Math.floor(toolMaxSteps / 3)) return stepNumber >= threshold && stepNumber < toolMaxSteps - 1 } diff --git a/src/lib/shared/llm/models.ts b/src/lib/shared/llm/models.ts index 7ca635c..30d5648 100644 --- a/src/lib/shared/llm/models.ts +++ b/src/lib/shared/llm/models.ts @@ -1,6 +1,4 @@ export const AvailableModels = { - ALIBABA_QWEN3_7_MAX: "alibaba/qwen3.7-max", - MOONSHOTAI_KIMI_K2_6: "moonshotai/kimi-k2.6", ZAI_GLM_5_2: "zai/glm-5.2", } as const @@ -18,56 +16,14 @@ export interface ModelInfo { name: string } -export const SUPPORTED_MODELS = [ - AvailableModels.ZAI_GLM_5_2, - AvailableModels.ALIBABA_QWEN3_7_MAX, - AvailableModels.MOONSHOTAI_KIMI_K2_6, -] as const +export const SUPPORTED_MODELS = [AvailableModels.ZAI_GLM_5_2] as const export const ALL_MODELS = [...SUPPORTED_MODELS] as const -export const MODEL_SELECTOR_MODELS = [ - AvailableModels.ZAI_GLM_5_2, - AvailableModels.ALIBABA_QWEN3_7_MAX, - AvailableModels.MOONSHOTAI_KIMI_K2_6, -] as const - -const MODEL_SELECTOR_MODEL_SET: ReadonlySet = new Set( - MODEL_SELECTOR_MODELS -) - -export function isModelSelectorModel(value: unknown): value is ModelType { - return ( - typeof value === "string" && - MODEL_SELECTOR_MODEL_SET.has(value as ModelType) - ) -} - -export function getModelSelectorModels( - models: readonly ModelInfo[] -): ModelInfo[] { - const modelById = new Map(models.map((model) => [model.id, model])) - return MODEL_SELECTOR_MODELS.flatMap((modelId) => { - const model = modelById.get(modelId) - return model ? [model] : [] - }) -} - -export function resolveDefaultModelSelectorModel( - models: readonly Pick[] -): ModelType { - return models[0]?.id ?? MODEL_SELECTOR_MODELS[0] -} +/** The single model the app runs on. */ +export const DEFAULT_MODEL: ModelType = AvailableModels.ZAI_GLM_5_2 export const ModelInfos: Record = { - [AvailableModels.ALIBABA_QWEN3_7_MAX]: { - id: AvailableModels.ALIBABA_QWEN3_7_MAX, - name: "Qwen 3.7 Max", - }, - [AvailableModels.MOONSHOTAI_KIMI_K2_6]: { - id: AvailableModels.MOONSHOTAI_KIMI_K2_6, - name: "Kimi K2.6", - }, [AvailableModels.ZAI_GLM_5_2]: { id: AvailableModels.ZAI_GLM_5_2, name: "GLM 5.2", diff --git a/tests/agent-context.test.mjs b/tests/agent-context.test.mjs index 91bbf40..229a97c 100644 --- a/tests/agent-context.test.mjs +++ b/tests/agent-context.test.mjs @@ -103,13 +103,13 @@ test("system prompt never includes a deep research block", () => { test("provider overlay respects its toggle", () => { const withOverlay = buildAgentSystemInstruction(viewer, { ...baseContext, - provider: "moonshotai", + provider: "zai", }) - assert.ok(withOverlay.includes("PROVIDER OVERLAY: MOONSHOTAI")) + assert.ok(withOverlay.includes("PROVIDER OVERLAY: ZAI")) const disabled = buildAgentSystemInstruction( viewer, - { ...baseContext, provider: "moonshotai" }, + { ...baseContext, provider: "zai" }, { providerOverlaysEnabled: false } ) assert.ok(!disabled.includes("PROVIDER OVERLAY")) diff --git a/tests/agent-follow-ups-route-behavior.test.mjs b/tests/agent-follow-ups-route-behavior.test.mjs index 7cb26a3..5dd6278 100644 --- a/tests/agent-follow-ups-route-behavior.test.mjs +++ b/tests/agent-follow-ups-route-behavior.test.mjs @@ -37,7 +37,7 @@ function createRequest(overrides = {}) { signal: AbortSignal.timeout(30_000), json: async () => ({ assistantMessageId: "assistant-1", - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", threadId: "thread-1", messages: [ { role: "user", content: "Explain love" }, diff --git a/tests/agent-follow-ups.test.mjs b/tests/agent-follow-ups.test.mjs index f89e9db..ef66dba 100644 --- a/tests/agent-follow-ups.test.mjs +++ b/tests/agent-follow-ups.test.mjs @@ -107,7 +107,7 @@ test("follow-up generation uses GPT-5.1 Instant and tags the source model", asyn return { output: { questions: [ - "How does Kimi handle the repair-after-conflict point?", + "How does a couple handle the repair-after-conflict point?", "What tradeoff matters most for commitment over time?", "How would care over time change the recommendation?", ], @@ -127,7 +127,7 @@ test("follow-up generation uses GPT-5.1 Instant and tags the source model", asyn "Love includes intimacy, commitment, repair after conflict, and care over time.", }, ], - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", userId: "user-1", }) @@ -135,7 +135,7 @@ test("follow-up generation uses GPT-5.1 Instant and tags the source model", asyn assert.deepEqual(recordedParams?.providerOptions?.gateway?.tags, [ "feature:follow_up_questions", "generation_model:openai/gpt-5.1-instant", - "source_model:moonshotai/kimi-k2.6", + "source_model:zai/glm-5.2", ]) assert.equal(followUps.length, 3) }) diff --git a/tests/agent-helper-behavior.test.mjs b/tests/agent-helper-behavior.test.mjs index a85e467..ef6b026 100644 --- a/tests/agent-helper-behavior.test.mjs +++ b/tests/agent-helper-behavior.test.mjs @@ -122,35 +122,16 @@ test("agent helper validates total size, last-message role, and default model su }, ], }, - availableModels: [{ id: "moonshotai/kimi-k2.6" }], + availableModels: [{ id: "zai/glm-5.2" }], requestId: "request-default-mode", }) assert(!(defaultModeResult instanceof Response)) - assert.equal(defaultModeResult.selectedModel, "moonshotai/kimi-k2.6") - - const defaultModeWithQwenResult = parseAgentStreamRequest({ - body: { - messages: [ - { - role: "user", - content: "Use the default model.", - }, - ], - }, - availableModels: [ - { id: "alibaba/qwen3.7-max" }, - { id: "moonshotai/kimi-k2.6" }, - ], - requestId: "request-default-mode-qwen", - }) - - assert(!(defaultModeWithQwenResult instanceof Response)) - assert.equal(defaultModeWithQwenResult.selectedModel, "alibaba/qwen3.7-max") + assert.equal(defaultModeResult.selectedModel, "zai/glm-5.2") const unavailableModelResult = parseAgentStreamRequest({ body: { - model: "alibaba/qwen3.7-max", + model: "zai/glm-5.2", messages: [ { role: "user", @@ -158,7 +139,7 @@ test("agent helper validates total size, last-message role, and default model su }, ], }, - availableModels: [{ id: "moonshotai/kimi-k2.6" }], + availableModels: [], requestId: "request-unavailable-model", }) @@ -180,7 +161,7 @@ test("agent helper validates total size, last-message role, and default model su }, ], }, - availableModels: [{ id: "moonshotai/kimi-k2.6" }], + availableModels: [{ id: "zai/glm-5.2" }], requestId: "request-unknown-field", }) @@ -199,7 +180,7 @@ test("agent helper validates total size, last-message role, and default model su content: "hello", })), }, - availableModels: [{ id: "moonshotai/kimi-k2.6" }], + availableModels: [{ id: "zai/glm-5.2" }], requestId: "request-too-many", }) @@ -220,7 +201,7 @@ test("agent helper validates total size, last-message role, and default model su }, ], }, - availableModels: [{ id: "moonshotai/kimi-k2.6" }], + availableModels: [{ id: "zai/glm-5.2" }], requestId: "request-message-too-large", }) @@ -239,7 +220,7 @@ test("agent helper validates total size, last-message role, and default model su content: `${String(index).padStart(2, "0")}${"x".repeat(10_998)}`, })), }, - availableModels: [{ id: "moonshotai/kimi-k2.6" }], + availableModels: [{ id: "zai/glm-5.2" }], requestId: "request-1", }) @@ -260,7 +241,7 @@ test("agent helper validates total size, last-message role, and default model su }, ], }, - availableModels: [{ id: "moonshotai/kimi-k2.6" }], + availableModels: [{ id: "zai/glm-5.2" }], requestId: "request-2", }) @@ -308,7 +289,7 @@ test("agent helper streams fallback output when the model yields no content", as request: createRequest(), requestId: "request-1", timeoutMs: 30_000, - selectedModel: "moonshotai/kimi-k2.6", + selectedModel: "zai/glm-5.2", aiGatewayApiKey: "ai-gateway-key", tavilyApiKey: "tavily-key", messages: [{ role: "user", content: "Hello" }], @@ -358,7 +339,7 @@ test("agent helper marks tool-backed partial output incomplete when a tool call request: createRequest(), requestId: "request-unresolved-tool", timeoutMs: 30_000, - selectedModel: "moonshotai/kimi-k2.6", + selectedModel: "zai/glm-5.2", aiGatewayApiKey: "ai-gateway-key", tavilyApiKey: "tavily-key", messages: [{ role: "user", content: "Search latest docs" }], @@ -413,7 +394,7 @@ test("agent helper does not add an incomplete fallback when a meaningful answer request: createRequest(), requestId: "request-tool-error", timeoutMs: 30_000, - selectedModel: "moonshotai/kimi-k2.6", + selectedModel: "zai/glm-5.2", aiGatewayApiKey: "ai-gateway-key", tavilyApiKey: "tavily-key", messages: [{ role: "user", content: "Search latest docs" }], @@ -460,7 +441,7 @@ test("agent helper turns upstream body timeouts into visible timeout output", as request: createRequest(), requestId: "request-body-timeout", timeoutMs: 30_000, - selectedModel: "moonshotai/kimi-k2.6", + selectedModel: "zai/glm-5.2", aiGatewayApiKey: "ai-gateway-key", messages: [{ role: "user", content: "Latest AI news" }], systemInstruction: "system", @@ -506,7 +487,7 @@ test("agent helper returns an auth-key fallback when provider auth fails", async request: createRequest(), requestId: "request-2", timeoutMs: 30_000, - selectedModel: "moonshotai/kimi-k2.6", + selectedModel: "zai/glm-5.2", aiGatewayApiKey: "ai-gateway-key", messages: [{ role: "user", content: "Hello" }], systemInstruction: "system", diff --git a/tests/agent-route-behavior.test.mjs b/tests/agent-route-behavior.test.mjs index 06d642c..1aa87d0 100644 --- a/tests/agent-route-behavior.test.mjs +++ b/tests/agent-route-behavior.test.mjs @@ -109,8 +109,8 @@ beforeEach(() => { }, }, agentPromptSteering: { - resolvePromptProvider(model) { - return `provider:${model}` + resolvePromptProvider() { + return "zai" }, }, agentRoute: { @@ -126,7 +126,7 @@ beforeEach(() => { parsedRequest: { messages: body.messages, }, - selectedModel: "moonshotai/kimi-k2.6", + selectedModel: "zai/glm-5.2", } }, createAgentStreamResponse(params) { @@ -269,7 +269,7 @@ test("agent route passes the resolved prompt context into stream creation", asyn context: { now: recorded.buildInstructionCalls[0].context.now, userTimeZone: "America/Chicago", - provider: "provider:moonshotai/kimi-k2.6", + provider: "zai", }, }) assert.deepEqual(recorded.streamCalls[0]?.messages, [ diff --git a/tests/agent-session-state.test.mjs b/tests/agent-session-state.test.mjs index 5219318..c87fb76 100644 --- a/tests/agent-session-state.test.mjs +++ b/tests/agent-session-state.test.mjs @@ -70,7 +70,7 @@ test("assistant session state builds assistant messages from stream accumulators id: "assistant-1", createdAt: "2026-04-30T12:00:00.000Z", accumulator, - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", isStreaming: true, }) @@ -93,14 +93,14 @@ test("assistant session state omits empty structured fields and upserts by id", id: "assistant-1", createdAt: "2026-04-30T12:00:00.000Z", accumulator: createAccumulator({ content: "Partial" }), - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", isStreaming: true, }) const finalMessage = createAssistantMessageFromAccumulator({ id: "assistant-1", createdAt: "2026-04-30T12:00:00.000Z", accumulator: createAccumulator({ content: "Final" }), - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", isStreaming: false, }) @@ -117,7 +117,7 @@ test("assistant session state attaches follow-up questions without changing cont id: "assistant-1", createdAt: "2026-04-30T12:00:00.000Z", accumulator: createAccumulator({ content: "Final answer." }), - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", isStreaming: false, }) const updatedMessages = attachFollowUpQuestionsToMessage( @@ -148,7 +148,7 @@ test("assistant session state tracks pending follow-up questions", () => { id: "assistant-1", createdAt: "2026-04-30T12:00:00.000Z", accumulator: createAccumulator({ content: "Final answer." }), - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", isStreaming: false, }) diff --git a/tests/agent-system-prompt.test.mjs b/tests/agent-system-prompt.test.mjs index d16d591..7326f37 100644 --- a/tests/agent-system-prompt.test.mjs +++ b/tests/agent-system-prompt.test.mjs @@ -50,15 +50,13 @@ test("agent system prompt composes trusted blocks in priority order", () => { { now: new Date("2026-05-03T12:34:56.000Z"), userTimeZone: "America/Chicago", - provider: "alibaba", + provider: "zai", } ) const operatingIndex = prompt.indexOf("--- BEGIN OPERATING INSTRUCTIONS ---") const dateIndex = prompt.indexOf("--- BEGIN RUNTIME DATE CONTEXT ---") - const providerIndex = prompt.indexOf( - "--- BEGIN PROVIDER OVERLAY: ALIBABA ---" - ) + const providerIndex = prompt.indexOf("--- BEGIN PROVIDER OVERLAY: ZAI ---") const identityIndex = prompt.indexOf( "--- BEGIN IDENTITY AND TONE CONTEXT ---" ) @@ -89,7 +87,7 @@ test("agent system prompt composes trusted blocks in priority order", () => { assert.match(prompt, /Current UTC timestamp: 2026-05-03T12:34:56.000Z/) assert.match(prompt, /User time zone: America\/Chicago/) - assert.match(prompt, /Use Qwen reasoning mode efficiently/) + assert.match(prompt, /Use GLM reasoning mode efficiently/) assert.match(prompt, /Email: user@example.com/) assert(prompt.includes(DEFAULT_SOUL_FALLBACK_INSTRUCTION)) assert.equal(prompt.includes("SOUL.md"), false) @@ -104,14 +102,12 @@ test("agent system prompt places the identity block after provider steering", () }, { now: new Date("2026-05-03T12:34:56.000Z"), - provider: "moonshotai", + provider: "zai", } ) const operatingIndex = prompt.indexOf("--- BEGIN OPERATING INSTRUCTIONS ---") - const providerIndex = prompt.indexOf( - "--- BEGIN PROVIDER OVERLAY: MOONSHOTAI ---" - ) + const providerIndex = prompt.indexOf("--- BEGIN PROVIDER OVERLAY: ZAI ---") const identityIndex = prompt.indexOf( "--- BEGIN IDENTITY AND TONE CONTEXT ---" ) diff --git a/tests/assistant-activity-timeline.test.mjs b/tests/assistant-activity-timeline.test.mjs index ac4d540..4faf906 100644 --- a/tests/assistant-activity-timeline.test.mjs +++ b/tests/assistant-activity-timeline.test.mjs @@ -45,7 +45,7 @@ test("normalizeAssistantActivityTimeline preserves streamed event order", () => id: "assistant-1", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { activityTimeline: [ @@ -112,7 +112,7 @@ test("normalizeAssistantActivityTimeline repairs legacy reasoning spacing from a id: "assistant-legacy-spacing", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { reasoning: @@ -162,7 +162,7 @@ test("normalizeAssistantActivityTimeline repairs spacing around non-BMP characte id: "assistant-legacy-non-bmp", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { reasoning: `Review ${rocket}financial data for MSFT.`, @@ -187,7 +187,7 @@ test("normalizeAssistantActivityTimeline skips redacted entries before aggregate id: "assistant-redacted-repair", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { reasoning: "Visible repaired text for MSFT.", @@ -220,7 +220,7 @@ test("normalizeAssistantActivityTimeline sanitizes private prompt terminology", id: "assistant-private-prompt", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { activityTimeline: [ @@ -247,7 +247,7 @@ test("normalizeAssistantActivityTimeline appends missing sources after legacy fa id: "assistant-2", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { reasoning: "Look up results", @@ -297,7 +297,7 @@ test("normalizeAssistantActivityTimeline hides tool errors superseded by a later id: "assistant-recovered-tool", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { activityTimeline: [ @@ -341,7 +341,7 @@ test("normalizeAssistantActivityTimeline keeps tool errors with a different oper id: "assistant-unresolved-tool", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { activityTimeline: [ @@ -383,7 +383,7 @@ test("normalizeAssistantActivityTimeline keeps tool errors for different input q id: "assistant-distinct-tools", role: "assistant", content: "", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-20T12:00:00.000Z", metadata: { activityTimeline: [ diff --git a/tests/follow-up-questions.test.mjs b/tests/follow-up-questions.test.mjs index ac0ff14..e10992e 100644 --- a/tests/follow-up-questions.test.mjs +++ b/tests/follow-up-questions.test.mjs @@ -25,7 +25,7 @@ const { const { AvailableModels } = await import(modelsUrl) const { ASSISTANT_EMPTY_RESPONSE_FALLBACK } = await import(constantsUrl) -const MODEL = AvailableModels.MOONSHOTAI_KIMI_K2_6 +const MODEL = AvailableModels.ZAI_GLM_5_2 function assistantMessage(overrides = {}) { const { metadata = {}, ...rest } = overrides diff --git a/tests/gateway-search-tools.test.mjs b/tests/gateway-search-tools.test.mjs index 3199be2..97fd50e 100644 --- a/tests/gateway-search-tools.test.mjs +++ b/tests/gateway-search-tools.test.mjs @@ -2,24 +2,13 @@ import assert from "node:assert/strict" import { readFile } from "node:fs/promises" import path from "node:path" import test from "node:test" -import { fileURLToPath, pathToFileURL } from "node:url" - -import "./register-ts-path-hooks.mjs" +import { fileURLToPath } from "node:url" const cwd = fileURLToPath(new URL("..", import.meta.url)) const tavilyToolsPath = path.join( cwd, "src/lib/server/llm/ai-sdk-tavily-tools.ts" ) -const persistentSelectedModelUrl = pathToFileURL( - path.join(cwd, "src/hooks/agent/persistent-selected-model-utils.ts") -).href - -const { - parseStoredSelectedModel, - resolvePersistedSelectedModel, - serializeStoredSelectedModel, -} = await import(persistentSelectedModelUrl) test("tavily search tool results derive source links", async () => { const source = await readFile(tavilyToolsPath, "utf8") @@ -50,36 +39,3 @@ test("inline citation instructions avoid separate sources sections", async () => "Expected source-backed answers to rely on inline citations and Activity instead of a footer." ) }) - -test("stale and fallback-only model ids fall back to GLM 5.2", () => { - assert.equal(parseStoredSelectedModel("qwen/qwen3.6-plus"), null) - assert.equal( - parseStoredSelectedModel( - JSON.stringify(serializeStoredSelectedModel("openai/gpt-5.5")) - ), - null - ) - - assert.equal( - resolvePersistedSelectedModel({ - storedModel: null, - currentModel: null, - initialSelectedModel: null, - availableModels: [ - { - id: "zai/glm-5.2", - name: "GLM 5.2", - }, - { - id: "alibaba/qwen3.7-max", - name: "Qwen 3.7 Max", - }, - { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - }, - ], - }), - "zai/glm-5.2" - ) -}) diff --git a/tests/home-agent-utils.test.mjs b/tests/home-agent-utils.test.mjs index d7da8d8..c4efc80 100644 --- a/tests/home-agent-utils.test.mjs +++ b/tests/home-agent-utils.test.mjs @@ -76,8 +76,8 @@ test("appended user messages record the selected model", () => { const messages = appendUserMessage( [], "Research Apple supply chain risk.", - "moonshotai/kimi-k2.6" + "zai/glm-5.2" ) - assert.equal(messages[0]?.metadata?.selectedModel, "moonshotai/kimi-k2.6") + assert.equal(messages[0]?.metadata?.selectedModel, "zai/glm-5.2") }) diff --git a/tests/model-registry.test.mjs b/tests/model-registry.test.mjs index 060d3ee..3eed25f 100644 --- a/tests/model-registry.test.mjs +++ b/tests/model-registry.test.mjs @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url" const cwd = fileURLToPath(new URL("..", import.meta.url)) const modelsPath = path.join(cwd, "src/lib/shared/llm/models.ts") -test("shared model registry includes the curated gateway models", async () => { +test("shared model registry exposes GLM 5.2 as the only model", async () => { const source = await readFile(modelsPath, "utf8") assert.doesNotMatch( @@ -18,20 +18,14 @@ test("shared model registry includes the curated gateway models", async () => { assert.doesNotMatch( source, - /google\/gemini-3\.1-pro-preview|xiaomi\/mimo-v2\.5-pro|GOOGLE_GEMINI_3_1_PRO_PREVIEW|XIAOMI_MIMO_V2_5_PRO|Gemini 3\.1 Pro Preview|MiMo V2\.5 Pro/, - "Expected Gemini 3.1 Pro Preview and MiMo V2.5 Pro to be fully removed from the shared model registry." + /alibaba\/qwen3\.7-max|moonshotai\/kimi-k2\.6|ALIBABA_QWEN3_7_MAX|MOONSHOTAI_KIMI_K2_6|Qwen 3\.7 Max|Kimi K2\.6/, + "Expected Qwen 3.7 Max and Kimi K2.6 to be fully removed from the shared model registry." ) - assert.match( - source, - /ALIBABA_QWEN3_7_MAX:\s*"alibaba\/qwen3\.7-max"/, - "Expected AvailableModels to include ALIBABA_QWEN3_7_MAX." - ) - - assert.match( + assert.doesNotMatch( source, - /MOONSHOTAI_KIMI_K2_6:\s*"moonshotai\/kimi-k2\.6"/, - "Expected AvailableModels to include MOONSHOTAI_KIMI_K2_6." + /MODEL_SELECTOR_MODELS|isModelSelectorModel|getModelSelectorModels|resolveDefaultModelSelectorModel/, + "Expected all model-selector helpers to be removed now that GLM 5.2 is the only model." ) assert.match( @@ -42,26 +36,14 @@ test("shared model registry includes the curated gateway models", async () => { assert.match( source.replace(/\s+/g, " "), - /SUPPORTED_MODELS = \[ AvailableModels\.ZAI_GLM_5_2, AvailableModels\.ALIBABA_QWEN3_7_MAX, AvailableModels\.MOONSHOTAI_KIMI_K2_6, \] as const/, - "Expected SUPPORTED_MODELS to list GLM 5.2, Qwen 3.7 Max, and Kimi K2.6." - ) - - assert.match( - source.replace(/\s+/g, " "), - /MODEL_SELECTOR_MODELS = \[ AvailableModels\.ZAI_GLM_5_2, AvailableModels\.ALIBABA_QWEN3_7_MAX, AvailableModels\.MOONSHOTAI_KIMI_K2_6, \] as const/, - "Expected the chat model selector to default to GLM 5.2, then Qwen 3.7 Max and Kimi K2.6." - ) - - assert.match( - source, - /\[AvailableModels\.ALIBABA_QWEN3_7_MAX\]:\s*\{[\s\S]*name:\s*"Qwen 3\.7 Max"/, - "Expected ModelInfos to define display metadata for ALIBABA_QWEN3_7_MAX." + /SUPPORTED_MODELS = \[AvailableModels\.ZAI_GLM_5_2\] as const/, + "Expected SUPPORTED_MODELS to list only GLM 5.2." ) assert.match( source, - /\[AvailableModels\.MOONSHOTAI_KIMI_K2_6\]:\s*\{[\s\S]*name:\s*"Kimi K2\.6"/, - "Expected ModelInfos to define display metadata for MOONSHOTAI_KIMI_K2_6." + /DEFAULT_MODEL: ModelType = AvailableModels\.ZAI_GLM_5_2/, + "Expected DEFAULT_MODEL to be GLM 5.2." ) assert.match( diff --git a/tests/prompt-form-contract.test.mjs b/tests/prompt-form-contract.test.mjs index 758f686..a34f9f0 100644 --- a/tests/prompt-form-contract.test.mjs +++ b/tests/prompt-form-contract.test.mjs @@ -10,7 +10,7 @@ const promptFormPath = path.join( "src/components/agent/prompt-form/prompt-form.tsx" ) -test("prompt form drops the tools popover and research mode controls", async () => { +test("prompt form drops the tools popover, research mode, and model selector controls", async () => { const source = await readFile(promptFormPath, "utf8") assert.doesNotMatch( @@ -18,9 +18,9 @@ test("prompt form drops the tools popover and research mode controls", async () /runMode|usePersistentRunMode|Research|Telescope|Popover|setIsToolsOpen/, "Expected PromptForm to drop the Tools popover and Research mode controls." ) - assert.match( + assert.doesNotMatch( source, - / { const normalizedThread = prepareThreadForPersistence({ id: "thread-persist", - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", messages: [ createMessage({ createdAt: "2026-04-15T09:59:00.000Z", diff --git a/tests/thread-payload-contract.test.mjs b/tests/thread-payload-contract.test.mjs index 2b7a9a6..5a1e06d 100644 --- a/tests/thread-payload-contract.test.mjs +++ b/tests/thread-payload-contract.test.mjs @@ -37,7 +37,7 @@ test("thread payload sanitizes private prompt terminology in reasoning", () => { id: "message-1", role: "assistant", content: "Done.", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-26T00:00:00.000Z", metadata: { reasoning: "Use SOUL.md and the system prompt.", @@ -76,7 +76,7 @@ test("thread payload truncates sanitized activity reasoning to the schema limit" id: "message-1", role: "assistant", content: "Done.", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-26T00:00:00.000Z", metadata: { activityTimeline: [ @@ -126,16 +126,16 @@ test("thread store delegates parsing and persistence shaping to the payload help test("thread payload drops legacy run-mode metadata from stored threads", () => { const parsed = parseThreadPayload({ id: "thread-1", - model: "alibaba/qwen3.7-max", + model: "zai/glm-5.2", messages: [ { id: "message-1", role: "user", content: "Research this.", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-26T00:00:00.000Z", metadata: { - selectedModel: "moonshotai/kimi-k2.6", + selectedModel: "zai/glm-5.2", runMode: "research", }, }, @@ -144,9 +144,6 @@ test("thread payload drops legacy run-mode metadata from stored threads", () => updatedAt: "2026-04-26T00:00:01.000Z", }) - assert.equal( - parsed.messages[0]?.metadata?.selectedModel, - "moonshotai/kimi-k2.6" - ) + assert.equal(parsed.messages[0]?.metadata?.selectedModel, "zai/glm-5.2") assert.equal(parsed.messages[0]?.metadata?.runMode, undefined) }) diff --git a/tests/threads-route-behavior.test.mjs b/tests/threads-route-behavior.test.mjs index d5b555a..ae689ac 100644 --- a/tests/threads-route-behavior.test.mjs +++ b/tests/threads-route-behavior.test.mjs @@ -201,7 +201,7 @@ test("threads GET returns a full thread when an id is requested", async () => { id: "message-1", role: "user", content: "Open the thread", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-15T10:00:00.000Z", }, ], diff --git a/tests/threads-store-behavior.test.mjs b/tests/threads-store-behavior.test.mjs index ceded72..fbfbaec 100644 --- a/tests/threads-store-behavior.test.mjs +++ b/tests/threads-store-behavior.test.mjs @@ -34,7 +34,7 @@ function createStoredMessage(overrides = {}) { id: "message-1", role: "user", content: "Stored thread message", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-15T10:00:00.000Z", ...overrides, } @@ -43,7 +43,7 @@ function createStoredMessage(overrides = {}) { function createStoredRow(overrides = {}) { return { id: "thread-1", - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", messages: [createStoredMessage()], createdAt: "2026-04-15T10:00:00.000Z", updatedAt: "2026-04-15T10:05:00.000Z", @@ -140,7 +140,7 @@ test("listThreadSummariesForUser avoids selecting message payloads", async () => { id: "summary-thread", title: "Stored summary", - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", createdAt: "2026-04-15T10:00:00.000Z", updatedAt: "2026-04-15T10:05:00.000Z", }, @@ -156,7 +156,7 @@ test("listThreadSummariesForUser avoids selecting message payloads", async () => { id: "summary-thread", title: "Stored summary", - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", createdAt: "2026-04-15T10:00:00.000Z", updatedAt: "2026-04-15T10:05:00.000Z", }, @@ -177,7 +177,7 @@ test("listThreadSummariesForUser skips invalid summary rows", async () => { { id: "invalid-summary", title: "Stored summary", - model: "moonshotai/kimi-k2.6", + model: "zai/glm-5.2", createdAt: "not-a-date", updatedAt: "2026-04-15T10:05:00.000Z", }, @@ -254,7 +254,7 @@ test("upsertThreadForUser normalizes the persisted thread and shapes SQL values" id: "message-upsert", role: "user", content: " Derive my title from the first message ", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-15T09:59:00.000Z", }, ], @@ -285,7 +285,7 @@ test("upsertThreadForUser normalizes the persisted thread and shapes SQL values" id: "message-upsert", role: "user", content: " Derive my title from the first message ", - llmModel: "moonshotai/kimi-k2.6", + llmModel: "zai/glm-5.2", createdAt: "2026-04-15T09:59:00.000Z", }, ])