Skip to content

Commit dc3dc51

Browse files
7418claude
andcommitted
fix: stale default provider root cause + Doctor diagnostic accuracy
Root cause: deleting a provider left a dangling default_provider_id in settings. Resolver fell back to env vars, bypassing the user's configured provider. Doctor diagnosed symptoms (no credentials, env fallback) but couldn't fix the root cause. Direct fixes: - providers/[id]/route.ts: DELETE now clears stale default_provider_id and auto-selects the next available provider - providers/models/route.ts: GET auto-heals stale default on page load (safe — this is an explicit API endpoint, not a diagnostic read path) - provider-resolver.ts: removed auto-heal from resolveProvider() read path to prevent side effects during Doctor diagnostics Doctor improvements: - provider.default-missing: message now explains root cause ("default points to deleted record, resolver falls back to env") - auth.resolved-no-creds: distinguishes "provider has no key" from "fell back to env due to stale default" - set-default-provider repair: now addresses provider.default-missing - apply-provider-to-session: validates providerId exists, auto-fixes stale default if needed - Network probe: only checks api.anthropic.com in env mode, avoids "Anthropic API unreachable" noise when user is on Kimi/GLM etc. file-tree.tsx: removed duplicate tabIndex on outer treeitem div — single tab stop on the trigger row only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6a2c290 commit dc3dc51

File tree

6 files changed

+65
-16
lines changed

6 files changed

+65
-16
lines changed

src/app/api/doctor/repair/route.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,29 @@ export async function POST(request: NextRequest) {
111111

112112
case 'apply-provider-to-session': {
113113
// Apply the default (or specified) provider to a session
114-
const providerId = params?.providerId || getDefaultProviderId();
114+
let providerId = params?.providerId || getDefaultProviderId();
115115
if (!providerId) {
116116
return NextResponse.json(
117117
{ error: 'No default provider configured and no providerId specified' },
118118
{ status: 400 },
119119
);
120120
}
121+
// Validate the provider actually exists — stale default IDs are a known issue
122+
const targetProvider = getProvider(providerId);
123+
if (!targetProvider) {
124+
// Try first available provider instead
125+
const { getAllProviders } = await import('@/lib/db');
126+
const all = getAllProviders();
127+
if (all.length > 0) {
128+
providerId = all[0].id;
129+
setDefaultProviderId(providerId); // also fix the stale default
130+
} else {
131+
return NextResponse.json(
132+
{ error: `Provider "${providerId}" not found and no alternatives available` },
133+
{ status: 404 },
134+
);
135+
}
136+
}
121137
if (params?.sessionId) {
122138
updateSessionProviderId(params.sessionId, providerId);
123139
} else {

src/app/api/providers/[id]/route.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextRequest, NextResponse } from 'next/server';
2-
import { getProvider, updateProvider, deleteProvider } from '@/lib/db';
2+
import { getProvider, updateProvider, deleteProvider, getDefaultProviderId, setDefaultProviderId, getAllProviders } from '@/lib/db';
33
import type { ProviderResponse, ErrorResponse, UpdateProviderRequest, ApiProvider } from '@/types';
44

55
interface RouteContext {
@@ -83,6 +83,18 @@ export async function DELETE(_request: NextRequest, context: RouteContext) {
8383
);
8484
}
8585

86+
// If the deleted provider was the default, clear the stale reference
87+
// and auto-switch to the first remaining provider (if any).
88+
const currentDefault = getDefaultProviderId();
89+
if (currentDefault === id) {
90+
const remaining = getAllProviders();
91+
if (remaining.length > 0) {
92+
setDefaultProviderId(remaining[0].id);
93+
} else {
94+
setDefaultProviderId('');
95+
}
96+
}
97+
8698
return NextResponse.json({ success: true });
8799
} catch (error) {
88100
return NextResponse.json<ErrorResponse>(

src/app/api/providers/models/route.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextResponse } from 'next/server';
2-
import { getAllProviders, getDefaultProviderId, getModelsForProvider, getSetting } from '@/lib/db';
2+
import { getAllProviders, getDefaultProviderId, setDefaultProviderId, getProvider, getModelsForProvider, getSetting } from '@/lib/db';
33
import { getContextWindow } from '@/lib/model-context';
44
import { getDefaultModelsForProvider, inferProtocolFromLegacy, findPresetForLegacy } from '@/lib/provider-catalog';
55
import type { Protocol } from '@/lib/provider-catalog';
@@ -188,8 +188,15 @@ export async function GET() {
188188
});
189189
}
190190

191-
// Determine default provider
192-
const defaultProviderId = getDefaultProviderId() || groups[0].provider_id;
191+
// Determine default provider — auto-heal stale references on read
192+
let defaultProviderId = getDefaultProviderId();
193+
if (defaultProviderId && !getProvider(defaultProviderId)) {
194+
// Stale default (provider was deleted). Fix it now.
195+
const firstValid = groups.find(g => g.provider_id !== 'env');
196+
defaultProviderId = firstValid?.provider_id || '';
197+
setDefaultProviderId(defaultProviderId);
198+
}
199+
defaultProviderId = defaultProviderId || groups[0]?.provider_id || '';
193200

194201
return NextResponse.json({
195202
groups,

src/components/ai-elements/file-tree.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ export const FileTreeFolder = ({
142142
<div
143143
className={cn("", className)}
144144
role="treeitem"
145-
tabIndex={0}
146145
{...props}
147146
>
148147
<CollapsibleTrigger asChild>

src/lib/provider-doctor.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,12 @@ async function runAuthProbe(): Promise<ProbeResult> {
237237
findings.push({
238238
severity: 'warn',
239239
code: 'auth.resolved-no-creds',
240-
message: 'Resolved provider reports no usable credentials',
240+
message: resolved.provider
241+
? `Provider "${resolved.provider.name}" is selected but has no usable credentials`
242+
: 'Resolver fell back to environment variables — no configured provider is active',
241243
detail: resolved.provider
242-
? `Provider "${resolved.provider.name}" (${resolved.protocol})`
243-
: 'Environment mode',
244+
? `Check the API key for "${resolved.provider.name}" in Settings → Providers`
245+
: 'This usually means the default provider was deleted or never set. Check the Provider/Model probe for details.',
244246
});
245247
}
246248
// Check for provider-level auth style conflict
@@ -310,14 +312,17 @@ async function runProviderProbe(): Promise<ProbeResult> {
310312
findings.push({
311313
severity: 'error',
312314
code: 'provider.default-missing',
313-
message: `Default provider ID "${defaultId}" not found in database`,
315+
message: `Default provider points to a deleted record — resolver falls back to environment variables, bypassing your configured provider`,
316+
detail: providers.length > 0
317+
? `${providers.length} valid provider(s) exist but none is selected as default. Click "Fix" to set the first one.`
318+
: 'No providers configured. Add a provider in Settings → Providers.',
314319
});
315320
}
316321
} else if (providers.length > 0) {
317322
findings.push({
318323
severity: 'warn',
319324
code: 'provider.no-default',
320-
message: 'Providers exist but no default is set',
325+
message: 'Providers exist but no default is set — new conversations will use environment variables',
321326
});
322327
}
323328

@@ -503,8 +508,14 @@ async function runNetworkProbe(): Promise<ProbeResult> {
503508
// Collect unique base URLs to check
504509
const urlsToCheck = new Map<string, string>(); // url -> label
505510

506-
// Default Anthropic API
507-
urlsToCheck.set('https://api.anthropic.com', 'Anthropic API');
511+
// Only check Anthropic API if the current resolution actually uses it
512+
// (env mode with no providers, or provider with anthropic base_url).
513+
// Avoid showing "Anthropic API unreachable" noise when user is on Kimi/GLM etc.
514+
const resolved = resolveProvider();
515+
const isEnvMode = !resolved.provider;
516+
if (isEnvMode) {
517+
urlsToCheck.set('https://api.anthropic.com', 'Anthropic API');
518+
}
508519

509520
// Provider-specific URLs
510521
const providers = getAllProviders();
@@ -570,9 +581,9 @@ async function runNetworkProbe(): Promise<ProbeResult> {
570581
const REPAIR_ACTIONS: RepairAction[] = [
571582
{
572583
type: 'set-default-provider',
573-
label: 'Set default provider',
574-
description: 'Configure a default provider so sessions have a clear auth path',
575-
addresses: ['provider.no-default', 'auth.no-credentials'],
584+
label: 'Set first valid provider as default',
585+
description: 'Fix the stale default by pointing to an existing provider',
586+
addresses: ['provider.no-default', 'provider.default-missing', 'auth.no-credentials'],
576587
},
577588
{
578589
type: 'apply-provider-to-session',

src/lib/provider-resolver.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export function resolveProvider(opts: ResolveOptions = {}): ResolvedProvider {
9797
// No provider specified — use global default
9898
const defaultId = getDefaultProviderId();
9999
if (defaultId) provider = getProvider(defaultId);
100+
// Note: stale default (provider deleted but setting remains) is NOT
101+
// auto-healed here — resolver is a read path that may run during
102+
// diagnostics. Auto-heal happens in: DELETE /api/providers/[id],
103+
// POST /api/doctor/repair, and startup migration in chat/page.tsx.
100104
}
101105
// effectiveProviderId === 'env' → provider stays undefined
102106

0 commit comments

Comments
 (0)