Skip to content

fix(provider): fall back gracefully when the configured model has no provider#16

Merged
Aayam Bansal (aayambansal) merged 1 commit into
mainfrom
graceful-no-provider
Jul 4, 2026
Merged

fix(provider): fall back gracefully when the configured model has no provider#16
Aayam Bansal (aayambansal) merged 1 commit into
mainfrom
graceful-no-provider

Conversation

@aayambansal

Copy link
Copy Markdown
Member

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:

ProviderModelNotFoundError ... data: { providerID: "anthropic", modelID: "claude-sonnet-4-6", suggestions: [] }
    at getModel (src/provider/provider.ts:1294)

and the web UI shows nothing — the session silently never answers.

Root causes

  1. src/provider/provider.tsdefaultModel() returned parseModel(cfg.model) without checking the provider/model is actually available; lastModel() in src/session/prompt.ts likewise reused the model of an old user message even if its provider is gone.
  2. src/server/routes/session.ts — the /:sessionID/prompt_async route fire-and-forgets SessionPrompt.prompt(...) with no .catch, so any throw became an unhandled rejection: raw stack to the terminal, nothing to the UI.
  3. src/session/prompt.ts — in loop(), Provider.getModel(lastUser.model...) threw without publishing a session error event (unlike the command path, which catches ModelNotFoundError and publishes Session.Event.Error before rethrowing).

Fix

  • backend/cli/src/provider/provider.tsdefaultModel() only honors cfg.model when 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.ts
    • lastModel() validates that the historical model's provider+model are still available before reusing them, falling back to Provider.defaultModel().
    • loop() catches Provider.ModelNotFoundError from getModel, publishes Session.Event.Error with 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-forget ensureTitle call now has a .catch — it was a second source of unhandled ProviderModelNotFoundError rejections.
    • prompt() publishes Session.Event.Error when user-message creation fails (e.g. truly zero providers), so the UI is never silent.
  • backend/cli/src/server/routes/session.ts — the prompt_async fire-and-forget now has a .catch that 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.
  • End-to-end repro in an isolated env (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):

INFO  ... service=session.prompt step=0 sessionID=ses_0d18df634ffe... loop
ERROR ... service=default e=ProviderModelNotFoundError rejection
ERROR ... service=default e=ProviderModelNotFoundError rejection

No session.error event, GET /session/{id}/message returned only the user message — the UI had nothing to show.

After (same env, same requests): zero rejection lines, no raw stack, and the session receives the error:

WARN  ... service=provider providerID=anthropic modelID=claude-sonnet-4-6 configured model is not available, falling back to default selection
ERROR ... service=server sessionID=ses_0d1894987ffe... error=No model providers are available. Add an API key for a provider (`openscience auth login`) or connect a managed account (`openscience connect login`), then choose a model. prompt_async failed

/event stream:

data: {"type":"session.error","properties":{"sessionID":"ses_0d1894987ffe...","error":{"name":"UnknownError","data":{"message":"No model providers are available. Add an API key for a provider (`openscience auth login`) or connect a managed account (`openscience connect login`), then choose a model."}}}}
  • Loop path (explicit unavailable model in the request body, which bypasses the lastModel fallback): GET /session/{id}/message now shows a finalized assistant message with error.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 matching session.error event — and zero unhandled rejections.
  • Stale-session path (session created before the fix with a stored anthropic/... user message): WARN ... last used model is no longer available, falling back to default, graceful error, no crash.

…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.
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openscience Ready Ready Preview, Comment Jul 4, 2026 6:51pm

Request Review

@aayambansal Aayam Bansal (aayambansal) merged commit cc8415f into main Jul 4, 2026
9 checks passed
@aayambansal Aayam Bansal (aayambansal) deleted the graceful-no-provider branch July 4, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant