mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 18:53:37 +00:00
Route Assistant sends around failed providers
This commit is contained in:
@@ -90,11 +90,14 @@ runtime cost control, and shared AI transport surfaces.
|
||||
assistant-authored output. Provider checking is a background diagnostic and
|
||||
must not delay the user's first useful chat turn; a confirmed selected-route
|
||||
provider error must keep typed text and focus without blocking normal chat
|
||||
dispatch. If the operator sends while that warning is still visible, the
|
||||
send path owns the attempt and any retryable provider failure is handled as
|
||||
a normal failed turn with draft restoration. Same-model configured-provider
|
||||
alternatives remain one-click route changes, not required preconditions for
|
||||
sending.
|
||||
dispatch. If a same-model configured-provider alternative is available when
|
||||
the operator sends while that warning is still visible, the drawer must
|
||||
promote that route before dispatching the turn instead of knowingly sending
|
||||
through the broken provider again. When no configured alternative is
|
||||
available, the send path owns the attempt and any retryable provider failure
|
||||
is handled as a normal failed turn with draft restoration. Same-model
|
||||
configured-provider alternatives remain one-click route changes as well as
|
||||
send-time recovery choices, not required preconditions for typing or sending.
|
||||
Follow-up sends during an active Assistant response are chat-runtime queue
|
||||
state by default. The drawer must accept and echo the user's follow-up as a
|
||||
queued user turn without aborting or replacing the active model stream, must
|
||||
|
||||
@@ -818,6 +818,66 @@ describe('AIChat', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('uses a configured-provider route automatically when sending after a selected provider failure', async () => {
|
||||
mockAIAPI.getSettings.mockResolvedValue({
|
||||
model: 'deepseek:deepseek-v4-pro',
|
||||
chat_model: '',
|
||||
control_level: 'read_only',
|
||||
autonomous_mode: false,
|
||||
discovery_enabled: true,
|
||||
configured_providers: ['deepseek', 'openrouter'],
|
||||
});
|
||||
mockAIAPI.getModels.mockResolvedValue({
|
||||
models: [
|
||||
{
|
||||
id: 'deepseek:deepseek-v4-pro',
|
||||
name: 'DeepSeek V4 Pro',
|
||||
provider: 'deepseek',
|
||||
notable: true,
|
||||
},
|
||||
{
|
||||
id: 'openrouter:deepseek/deepseek-v4-pro',
|
||||
name: 'DeepSeek: DeepSeek V4 Pro',
|
||||
provider: 'openrouter',
|
||||
notable: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
mockAIAPI.testProvider.mockResolvedValueOnce({
|
||||
success: false,
|
||||
message: 'Provider connection issue',
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek:deepseek-v4-pro',
|
||||
cause: 'provider_connection',
|
||||
summary: 'Pulse could not maintain a healthy connection to this provider.',
|
||||
recommendation: 'Check provider reachability.',
|
||||
action: 'open_provider_settings',
|
||||
});
|
||||
|
||||
renderChat();
|
||||
|
||||
await screen.findByRole('button', {
|
||||
name: 'Use OpenRouter provider route',
|
||||
});
|
||||
|
||||
const textarea = screen.getByPlaceholderText(
|
||||
'Ask about your infrastructure...',
|
||||
) as HTMLTextAreaElement;
|
||||
fireEvent.input(textarea, { target: { value: 'summarize the cluster' } });
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' });
|
||||
|
||||
expect(mockChat.setModel).toHaveBeenCalledWith('openrouter:deepseek/deepseek-v4-pro');
|
||||
expect(mockChat.sendMessage).toHaveBeenCalledWith(
|
||||
'summarize the cluster',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockChat.setModel.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockChat.sendMessage.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(textarea.value).toBe('');
|
||||
});
|
||||
|
||||
it('rechecks provider readiness from the drawer status banner', async () => {
|
||||
mockAIAPI.getSettings.mockResolvedValue({
|
||||
model: 'deepseek:deepseek-v4-pro',
|
||||
|
||||
@@ -957,6 +957,23 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
switchToModelRoute(alternative.id);
|
||||
};
|
||||
|
||||
const selectProviderReadinessAlternativeForSend = () => {
|
||||
const readiness = providerReadiness();
|
||||
if (readiness.status !== 'error') return null;
|
||||
|
||||
const currentProvider = selectedChatProvider().trim();
|
||||
const failedProvider = readiness.provider.trim();
|
||||
if (failedProvider && currentProvider && failedProvider !== currentProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const alternative = providerReadinessAlternative();
|
||||
if (!alternative) return null;
|
||||
|
||||
selectModel(alternative.id);
|
||||
return alternative;
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
const sessionId = chat.sessionId();
|
||||
const storedModel = getStoredModel(sessionId);
|
||||
@@ -1538,6 +1555,8 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
Boolean(sendOptions.handoffResources?.length) ||
|
||||
Boolean(sendOptions.handoffActions?.length) ||
|
||||
Boolean(sendOptions.handoffMetadata);
|
||||
selectProviderReadinessAlternativeForSend();
|
||||
|
||||
const sendPromise = hasSendOptions
|
||||
? chat.sendMessage(prompt, mentionsForAPI, findingId, sendOptions)
|
||||
: chat.sendMessage(prompt, mentionsForAPI, findingId);
|
||||
|
||||
Reference in New Issue
Block a user