Contain Assistant model catalog failures

This commit is contained in:
rcourtman
2026-06-08 03:20:44 +01:00
parent 2f1ec1fa61
commit 6b64ace474
3 changed files with 57 additions and 1 deletions
@@ -2247,6 +2247,19 @@ provider route-switch metadata without changing the selected route, and labeling
failed-turn or readiness recovery buttons as explicit route/model-route choices
instead of implying automatic route adoption.
Assistant model-catalog failure is selector-local state, not a drawer
initialization failure. The OpenCode reference at fetched `dev` commit
`3867fa2bad0e644166e360e2e99cfe426fe71105`
`packages/opencode/src/cli/error.ts` lines 58-69 formats missing model/catalog
state as an operator-facing model-selection problem with a list-models hint,
while `packages/opencode/src/session/llm.ts` lines 96-104 resolves the
selected provider/model route independently for the stream. Pulse adapts that
by letting Assistant sessions, settings, route health, and the composer finish
opening when `/api/ai/models` fails; the catalog error stays attached to the
model selector and may be refreshed explicitly, but startup must not log or
render a broad Assistant initialization failure, and it must not replace the
selected model route.
Assistant completed-turn chrome is route-owned summary, not raw usage output.
The OpenCode reference at fetched `dev` commit
`3867fa2bad0e644166e360e2e99cfe426fe71105` imports `turnSummaryCommit` in
@@ -4,6 +4,7 @@ import { Show, createSignal } from 'solid-js';
import type { ChatMessage, ModelInfo, ModelRouteRecoveryOption } from '../types';
import type { QueuedFollowUp } from '../hooks/useChat';
import { WORKFLOW_STATUS_PACE_MS } from '../workflowStatusDisplay';
import { logger } from '@/utils/logger';
// ── Hoisted mocks (vi.mock factories reference these) ──────────────────────
@@ -37,6 +38,7 @@ const {
recentModelIds?: string[];
openRequest?: number;
initialSearchQuery?: string;
error?: string;
onManageProviders?: () => void;
onModelSelect?: (modelId: string) => void;
}> = [];
@@ -335,6 +337,7 @@ vi.mock('../ModelSelector', () => ({
recentModelIds?: string[];
openRequest?: number;
initialSearchQuery?: string;
error?: string;
onManageProviders?: () => void;
onModelSelect?: (modelId: string) => void;
}) => {
@@ -347,6 +350,7 @@ vi.mock('../ModelSelector', () => ({
data-open-request={String(props.openRequest || 0)}
data-initial-search={props.initialSearchQuery || ''}
data-recent-models={(props.recentModelIds || []).join('|')}
data-error={props.error || ''}
/>
);
},
@@ -4321,6 +4325,32 @@ describe('AIChat', () => {
});
});
it('keeps Assistant initialized when the model catalog fails on mount', async () => {
const modelCatalogError = new Error('Model catalog unavailable');
mockAIAPI.getModels.mockRejectedValue(modelCatalogError);
renderChat();
await waitFor(() => {
expect(mockAIChatAPI.getStatus).toHaveBeenCalledTimes(1);
expect(mockAIChatAPI.listSessions).toHaveBeenCalledWith({ limit: 30 });
expect(mockAIAPI.getSettings).toHaveBeenCalledTimes(1);
expect(mockAIAPI.getModels).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
expect(screen.getByTestId('model-selector')).toHaveAttribute(
'data-error',
'Model catalog unavailable',
);
});
expect(mockAIAPI.testProvider).toHaveBeenCalledWith('openai', 'gpt-4');
expect(logger.error).not.toHaveBeenCalledWith(
'[AIChat] Failed to initialize:',
modelCatalogError,
);
});
it('does not load sessions, settings, or models when AI is not running', async () => {
mockAIChatAPI.getStatus.mockResolvedValue({ running: false });
renderChat();
@@ -2274,7 +2274,20 @@ export const AIChat: Component<AIChatProps> = (props) => {
setControlLevel('read_only');
return;
}
await Promise.all([refreshSessions(), loadAIRuntimeSettings(), loadAIRuntimeModels()]);
const [sessionsResult, settingsResult, modelsResult] = await Promise.allSettled([
refreshSessions(),
loadAIRuntimeSettings(),
loadAIRuntimeModels(),
]);
if (sessionsResult.status === 'rejected') {
logger.error('[AIChat] Failed to load sessions:', sessionsResult.reason);
}
if (settingsResult.status === 'rejected') {
logger.error('[AIChat] Failed to load AI settings:', settingsResult.reason);
}
if (modelsResult.status === 'rejected') {
logger.debug('[AIChat] Model catalog unavailable during initialization:', modelsResult.reason);
}
} catch (error) {
logger.error('[AIChat] Failed to initialize:', error);
}