fix(provider): fall back gracefully when the configured model has no provider#16
Merged
Merged
Conversation
…provider With a saved config model (e.g. anthropic/claude-sonnet-4-6) but no API key and no managed session, sending a prompt crashed the server loop with a raw unhandled ProviderModelNotFoundError stack while the web UI stayed silent. - Provider.defaultModel: only honor cfg.model when its provider and model are actually available; otherwise warn and fall through to priority selection. The no-providers throw now carries an actionable message. - SessionPrompt.lastModel: validate the historical model of the last user message is still available before reusing it; fall back to defaultModel. - SessionPrompt.loop: catch ModelNotFoundError from getModel, publish a session.error event with an actionable message, finalize an assistant message carrying the error, and exit the loop cleanly. The fire-and-forget ensureTitle call is caught too so it can no longer reject unhandled. - SessionPrompt.prompt: publish session.error when user-message creation fails (e.g. zero providers) so the UI is never silent. - prompt_async route: catch the fire-and-forget prompt so nothing becomes an unhandled rejection.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
On a machine with a saved config model (
~/.config/openscience/openscience.json→{"model": "anthropic/claude-sonnet-4-6"}) but no API keys and no managed session, sending a prompt from the web workspace crashes the server loop with a raw unhandled stack in the terminal:and the web UI shows nothing — the session silently never answers.
Root causes
src/provider/provider.ts—defaultModel()returnedparseModel(cfg.model)without checking the provider/model is actually available;lastModel()insrc/session/prompt.tslikewise reused the model of an old user message even if its provider is gone.src/server/routes/session.ts— the/:sessionID/prompt_asyncroute fire-and-forgetsSessionPrompt.prompt(...)with no.catch, so any throw became an unhandled rejection: raw stack to the terminal, nothing to the UI.src/session/prompt.ts— inloop(),Provider.getModel(lastUser.model...)threw without publishing a session error event (unlike the command path, which catchesModelNotFoundErrorand publishesSession.Event.Errorbefore rethrowing).Fix
backend/cli/src/provider/provider.ts—defaultModel()only honorscfg.modelwhen its provider and model exist in the available providers; otherwise it logs a warning and falls through to the existing priority-based selection. The zero-providers throw now carries an actionable message (NO_PROVIDER_HINT). Behavior is unchanged when the configured model is available.backend/cli/src/session/prompt.tslastModel()validates that the historical model's provider+model are still available before reusing them, falling back toProvider.defaultModel().loop()catchesProvider.ModelNotFoundErrorfromgetModel, publishesSession.Event.Errorwith an actionable message ("Model anthropic/claude-sonnet-4-6 is not available. Add an API key for a provider (openscience auth login) or connect a managed account (openscience connect login), then choose a model."), finalizes an assistant message carrying the error (same shape as the processor error path), and exits the loop cleanly. The fire-and-forgetensureTitlecall now has a.catch— it was a second source of unhandledProviderModelNotFoundErrorrejections.prompt()publishesSession.Event.Errorwhen user-message creation fails (e.g. truly zero providers), so the UI is never silent.backend/cli/src/server/routes/session.ts— theprompt_asyncfire-and-forget now has a.catchthat logs and swallows; session-level error publishing happens inside prompt/loop.Verification
bun run typecheck— 6/6 packages pass.bun run --cwd backend/cli test— 820 pass, 0 fail.HOME/XDG_*pointed at a temp dir containing only the config model,ANTHROPIC_API_KEY=/OPENAI_API_KEY=empty,serve --port 412x,POST /session+POST /session/{id}/prompt_async {"parts":[{"type":"text","text":"hi"}]}):Before (dev.log):
No
session.errorevent,GET /session/{id}/messagereturned only the user message — the UI had nothing to show.After (same env, same requests): zero
rejectionlines, no raw stack, and the session receives the error:/eventstream:lastModelfallback):GET /session/{id}/messagenow shows a finalized assistant message witherror.data.message = "Model anthropic/claude-sonnet-4-6 is not available. Add an API key for a provider (openscience auth login) or connect a managed account (openscience connect login), then choose a model."plus the matchingsession.errorevent — and zero unhandled rejections.anthropic/...user message):WARN ... last used model is no longer available, falling back to default, graceful error, no crash.