From c6feb43179a96eadeabb9668eb3b3297ddf9fa81 Mon Sep 17 00:00:00 2001 From: Jason Tang Date: Tue, 17 Mar 2026 16:58:12 -0500 Subject: [PATCH 1/3] fix: centralize stale model IDs into single CLAUDE_MODELS constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardcoded model IDs (claude-sonnet-4-20250514, claude-opus-4-20250514) were scattered across 7+ files as fallbacks. These were stale after the CLI update to v2.1.73 which resolves aliases to the new model IDs (claude-sonnet-4-6, claude-opus-4-6). - Add src/lib/model-ids.ts with CLAUDE_MODELS + DEFAULT_MODEL_ID (client-safe, no fs/db imports — avoids Next.js bundling errors) - provider-resolver.ts: import + re-export from model-ids.ts, replace hardcoded envModels and toAiSdkConfig fallback - model-context.ts: derive context window map from CLAUDE_MODELS (import from model-ids.ts to stay client-safe) - skills/search/route.ts: derive MODEL_MAP from CLAUDE_MODELS - checkin-processor.ts, onboarding-processor.ts, media/jobs/plan/route.ts: use DEFAULT_MODEL_ID for last-resort fallback One place to update when new models release. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/media/jobs/plan/route.ts | 4 ++-- src/app/api/skills/search/route.ts | 11 +++++------ src/lib/checkin-processor.ts | 4 ++-- src/lib/model-context.ts | 12 ++++++------ src/lib/model-ids.ts | 17 +++++++++++++++++ src/lib/onboarding-processor.ts | 4 ++-- src/lib/provider-resolver.ts | 16 ++++++++++------ 7 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 src/lib/model-ids.ts diff --git a/src/app/api/media/jobs/plan/route.ts b/src/app/api/media/jobs/plan/route.ts index a9c61ebf..cb833c54 100644 --- a/src/app/api/media/jobs/plan/route.ts +++ b/src/app/api/media/jobs/plan/route.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server'; import { streamTextFromProvider } from '@/lib/text-generator'; -import { resolveProvider } from '@/lib/provider-resolver'; +import { resolveProvider, DEFAULT_MODEL_ID } from '@/lib/provider-resolver'; import fs from 'fs'; import type { PlanMediaJobRequest } from '@/types'; @@ -62,7 +62,7 @@ export async function POST(request: NextRequest) { }); // Preserve 'env' semantics (see onboarding route for rationale) const providerId = resolved.provider?.id || 'env'; - const modelId = resolved.upstreamModel || resolved.model || session?.model || 'claude-sonnet-4-20250514'; + const modelId = resolved.upstreamModel || resolved.model || session?.model || DEFAULT_MODEL_ID; // Read document content let docContent = body.docContent || ''; diff --git a/src/app/api/skills/search/route.ts b/src/app/api/skills/search/route.ts index f49e79d2..3ef2cc9a 100644 --- a/src/app/api/skills/search/route.ts +++ b/src/app/api/skills/search/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { getActiveProvider, getSetting } from '@/lib/db'; +import { CLAUDE_MODELS } from '@/lib/provider-resolver'; interface SkillInfo { name: string; @@ -12,12 +13,10 @@ interface SearchRequest { model?: string; } -// Model alias -> full model ID -const MODEL_MAP: Record = { - sonnet: 'claude-sonnet-4-20250514', - opus: 'claude-opus-4-20250514', - haiku: 'claude-haiku-4-20250414', -}; +// Model alias -> full model ID (derived from central CLAUDE_MODELS) +const MODEL_MAP: Record = Object.fromEntries( + Object.entries(CLAUDE_MODELS).map(([alias, m]) => [alias, m.id]) +); interface ApiConfig { supported: boolean; diff --git a/src/lib/checkin-processor.ts b/src/lib/checkin-processor.ts index 0dee1eac..79542ca8 100644 --- a/src/lib/checkin-processor.ts +++ b/src/lib/checkin-processor.ts @@ -6,7 +6,7 @@ import fs from 'fs'; import path from 'path'; import { getSetting, getSession } from '@/lib/db'; -import { resolveProvider } from '@/lib/provider-resolver'; +import { resolveProvider, DEFAULT_MODEL_ID } from '@/lib/provider-resolver'; import { loadState, saveState, writeDailyMemory } from '@/lib/assistant-workspace'; import { getLocalDateString } from '@/lib/utils'; import { generateTextFromProvider } from '@/lib/text-generator'; @@ -70,7 +70,7 @@ export async function processCheckin( sessionModel: session?.model || undefined, }); const providerId = resolved.provider?.id || 'env'; - const model = resolved.upstreamModel || resolved.model || getSetting('default_model') || 'claude-sonnet-4-20250514'; + const model = resolved.upstreamModel || resolved.model || getSetting('default_model') || DEFAULT_MODEL_ID; const dailyMemoryPrompt = `You maintain daily memory entries for an AI assistant. Given the user's daily check-in answers, generate a daily memory entry for ${today}. diff --git a/src/lib/model-context.ts b/src/lib/model-context.ts index 828c0c5a..cfcb360f 100644 --- a/src/lib/model-context.ts +++ b/src/lib/model-context.ts @@ -1,10 +1,10 @@ +import { CLAUDE_MODELS } from './model-ids'; + export const MODEL_CONTEXT_WINDOWS: Record = { - 'sonnet': 200000, - 'opus': 200000, - 'haiku': 200000, - 'claude-sonnet-4-20250514': 200000, - 'claude-opus-4-20250514': 200000, - 'claude-haiku-4-5-20251001': 200000, + // Short aliases + ...Object.fromEntries(Object.entries(CLAUDE_MODELS).map(([alias, m]) => [alias, m.contextWindow])), + // Full model IDs + ...Object.fromEntries(Object.values(CLAUDE_MODELS).map(m => [m.id, m.contextWindow])), }; export function getContextWindow(model: string): number | null { diff --git a/src/lib/model-ids.ts b/src/lib/model-ids.ts new file mode 100644 index 00000000..48d36d92 --- /dev/null +++ b/src/lib/model-ids.ts @@ -0,0 +1,17 @@ +/** + * Canonical Claude model definitions — single source of truth. + * + * This file has ZERO server-side imports (no fs, no db) so it can be + * safely imported from both server code and client-side React hooks. + * + * Update these when Anthropic releases new model generations. + */ + +export const CLAUDE_MODELS = { + sonnet: { id: 'claude-sonnet-4-6', displayName: 'Sonnet 4.6', contextWindow: 200000 }, + opus: { id: 'claude-opus-4-6', displayName: 'Opus 4.6', contextWindow: 200000 }, + haiku: { id: 'claude-haiku-4-5-20251001', displayName: 'Haiku 4.5', contextWindow: 200000 }, +} as const; + +/** Default model ID used as a last-resort fallback */ +export const DEFAULT_MODEL_ID = CLAUDE_MODELS.sonnet.id; diff --git a/src/lib/onboarding-processor.ts b/src/lib/onboarding-processor.ts index 5dfb81c2..8df1661c 100644 --- a/src/lib/onboarding-processor.ts +++ b/src/lib/onboarding-processor.ts @@ -6,7 +6,7 @@ import fs from 'fs'; import path from 'path'; import { getSetting, getSession } from '@/lib/db'; -import { resolveProvider } from '@/lib/provider-resolver'; +import { resolveProvider, DEFAULT_MODEL_ID } from '@/lib/provider-resolver'; import { loadState, saveState, ensureDailyDir, generateRootDocs } from '@/lib/assistant-workspace'; import { getLocalDateString } from '@/lib/utils'; import { generateTextFromProvider } from '@/lib/text-generator'; @@ -74,7 +74,7 @@ export async function processOnboarding( sessionModel: session?.model || undefined, }); const providerId = resolved.provider?.id || 'env'; - const model = resolved.upstreamModel || resolved.model || getSetting('default_model') || 'claude-sonnet-4-20250514'; + const model = resolved.upstreamModel || resolved.model || getSetting('default_model') || DEFAULT_MODEL_ID; const soulPrompt = `Based on the following user onboarding answers, generate a concise "soul.md" file that defines an AI assistant's personality, communication style, and behavioral rules. Write in second person ("You are..."). Keep it under 2000 characters. Use markdown headers and bullet points.\n\n${qaText}`; diff --git a/src/lib/provider-resolver.ts b/src/lib/provider-resolver.ts index d45d8d9f..27dbe112 100644 --- a/src/lib/provider-resolver.ts +++ b/src/lib/provider-resolver.ts @@ -25,6 +25,10 @@ import { getModelsForProvider, } from './db'; +// Canonical model definitions live in model-ids.ts (client-safe, no fs/db imports). +import { CLAUDE_MODELS, DEFAULT_MODEL_ID } from './model-ids'; +export { CLAUDE_MODELS, DEFAULT_MODEL_ID }; + // ── Resolution result ─────────────────────────────────────────── export interface ResolvedProvider { @@ -289,7 +293,7 @@ export function toAiSdkConfig( const catalogEntry = resolved.availableModels.find(m => m.modelId === modelOverride); modelId = catalogEntry?.upstreamModelId || modelOverride; } else { - modelId = resolved.upstreamModel || resolved.model || 'claude-sonnet-4-20250514'; + modelId = resolved.upstreamModel || resolved.model || DEFAULT_MODEL_ID; } const provider = resolved.provider; const protocol = resolved.protocol; @@ -465,11 +469,11 @@ function buildResolution( // Env mode uses short aliases (sonnet/opus/haiku) in the UI. // Map them to full Anthropic model IDs so toAiSdkConfig can resolve correctly. - const envModels: CatalogModel[] = [ - { modelId: 'sonnet', upstreamModelId: 'claude-sonnet-4-20250514', displayName: 'Sonnet 4.6' }, - { modelId: 'opus', upstreamModelId: 'claude-opus-4-20250514', displayName: 'Opus 4.6' }, - { modelId: 'haiku', upstreamModelId: 'claude-haiku-4-5-20251001', displayName: 'Haiku 4.5' }, - ]; + const envModels: CatalogModel[] = Object.entries(CLAUDE_MODELS).map(([alias, m]) => ({ + modelId: alias, + upstreamModelId: m.id, + displayName: m.displayName, + })); // Resolve upstream model from the alias table const catalogEntry = model ? envModels.find(m => m.modelId === model) : undefined; From 7e12b48781dee60b421ee057a279c9a29260d2ab Mon Sep 17 00:00:00 2001 From: Jason Tang Date: Tue, 17 Mar 2026 16:58:22 -0500 Subject: [PATCH 2/3] fix: prefer requested_model over SDK-resolved model in status bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chat/page.tsx used statusData.model (the SDK's resolved wire ID like claude-opus-4-20250514) instead of statusData.requested_model (the alias the user selected). This caused the "Connected (...)" status to flash a stale/misleading model ID. useSSEStream.ts already had the correct priority — this aligns chat/page.tsx to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/chat/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index dd881bd5..5a62ffbd 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -473,7 +473,7 @@ export default function NewChatPage() { try { const statusData = JSON.parse(event.data); if (statusData.session_id) { - setStatusText(`Connected (${statusData.model || 'claude'})`); + setStatusText(`Connected (${statusData.requested_model || statusData.model || 'claude'})`); setTimeout(() => setStatusText(undefined), 2000); } else if (statusData.notification) { setStatusText(statusData.message || statusData.title || undefined); From 72b529b31674f782a782e0edefa897a09319e8a2 Mon Sep 17 00:00:00 2001 From: Jason Tang Date: Tue, 17 Mar 2026 16:58:31 -0500 Subject: [PATCH 3/3] fix: prevent model selector label degradation after first conversation After the first conversation, captureCapabilities() caches the SDK's supportedModels() which have different displayNames ("Sonnet", "Opus 4") than our catalog ("Sonnet 4.6", "Opus 4.6"). The /api/providers/models route was using m.displayName directly from the SDK cache, overriding the correct labels. Fix: prefer CLAUDE_MODELS[alias].displayName when available, fall back to SDK's displayName for unknown/future models. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/providers/models/route.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/api/providers/models/route.ts b/src/app/api/providers/models/route.ts index 740d97e3..ec454719 100644 --- a/src/app/api/providers/models/route.ts +++ b/src/app/api/providers/models/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import { getAllProviders, getDefaultProviderId, setDefaultProviderId, getProvider, getModelsForProvider, getSetting } from '@/lib/db'; import { getContextWindow } from '@/lib/model-context'; +import { CLAUDE_MODELS } from '@/lib/model-ids'; import { getDefaultModelsForProvider, inferProtocolFromLegacy, findPresetForLegacy } from '@/lib/provider-catalog'; import type { Protocol } from '@/lib/provider-catalog'; import type { ErrorResponse, ProviderModelGroup } from '@/types'; @@ -73,7 +74,7 @@ export async function GET() { const cw = getContextWindow(m.value); return { value: m.value, - label: m.displayName, + label: CLAUDE_MODELS[m.value as keyof typeof CLAUDE_MODELS]?.displayName ?? m.displayName, description: m.description, supportsEffort: m.supportsEffort, supportedEffortLevels: m.supportedEffortLevels,