Add OpenRouter as a setup connection with searchable model pickers - #77
Merged
Merged
Conversation
OpenRouter replaces the non-functional Claude subscription card in the first-run wizard and joins the AI connection choices in Settings. Users add an OpenRouter key (encrypted with safeStorage, like the OpenAI key), pick the clip-finding LLM from OpenRouter's live model catalogue in a searchable dropdown with suggested models on top, and pick transcription from OpenRouter-hosted models or local Whisper. - Chat and transcription route to openrouter.ai with the OpenRouter key; OpenAI base URL settings and env vars do not apply on this route. - The catalogue is fetched in the main process and falls back to the suggested models offline. Check key uses /key and makes no model call. - Transcripts with segment timings but no word timings get estimated word timings; no timings at all fails with a clear fix. - The smoke walk captures the OpenRouter wizard screens from a sample catalogue fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRPuZ4E7P2EcTR4c9BzhS
Captions, cut tightening and clip timing need per-word timestamps. Through OpenRouter only the Whisper models (whisper-1, whisper-large-v3, whisper-large-v3-turbo) reliably return them with verbose_json; others reject verbose_json with HTTP 400 (gpt-4o-transcribe, mai-transcribe-1.5, Google Chirp), return no word timings, or only take short or WAV-only clips. - The transcription picker shows local Whisper plus that allowlist, with no custom ids; stored or submitted unsupported ids are refused. - OpenRouter transcription uses the documented JSON body (base64 input_audio, verbose_json, word and segment granularities) instead of multipart, and labels seam-repair WAV files correctly. - Audio goes in five-minute chunks on this route to stay inside OpenRouter's 60-second upstream timeout. - A reply without word timestamps now fails with a fix instead of getting estimated timings; other routes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRPuZ4E7P2EcTR4c9BzhS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRPuZ4E7P2EcTR4c9BzhS
- Check word timestamps after the retry loop, so a model that ignores the timestamp request is billed once, not four times (Bugbot). - Refuse to send a key the current route did not issue: jobs read their key at the start while endpoints are read per request, so switching provider in Settings mid-job could send an OpenAI key to OpenRouter or the reverse. - Name the missing credential for the selected provider in caption and clip-finding errors and in the top-bar prompt (Bugbot). - Model picker tracks the highlighted row by id, so a catalogue arriving while it is open cannot move Enter onto a different model. - Fall back to the suggested models if the catalogue IPC call fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRPuZ4E7P2EcTR4c9BzhS
Owner
Author
|
Independent review summary. A separate read-only reviewer went through this PR and found nothing blocking in normal use. a4f4438 fixes the following:
Deliberately left as they are (these match the existing API route's behaviour, or are follow-ups):
Generated by Claude Code |
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Resolved by another fix: Key guard skips empty credentials
- Already fixed on this branch by fbb4cd3, which reads the route credential live and keeps the guard armed when the new route has no key.
You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit a4f4438. Configure here.
The key/route guard captured the credential at configure time and turned itself off when it was empty, so switching mid-job to a provider with no stored key still sent the job's key there (Bugbot). It now reads the route's current credential on each request, and an empty one matches no key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRPuZ4E7P2EcTR4c9BzhS
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.

What does this change?
Adds OpenRouter as an AI connection in the first-run wizard, replacing the "Claude subscription" card, which didn't do anything. OpenRouter is also a choice under Settings → AI connection.
safeStoragethe same way as the OpenAI key. Check key calls OpenRouter's/keyendpoint, which makes no model request./modelscatalogue. Suggested models sit at the top: GPT-5.4 Mini (the default), Gemini 3.8 Flash, Claude Sonnet 5 and GPT-5.5. Suggestions the live catalogue doesn't carry are hidden. Each row shows price, context size and a Vision tag, and a full custom model id can be typed.openai/whisper-1,openai/whisper-large-v3-turboandopenai/whisper-large-v3. Nothing else is allowed, and a stored or submitted unsupported id is refused.https://openrouter.ai/api/v1with the OpenRouter key. The OpenAI base-URL settings andOPENAI_BASE_URLdon't apply on this route.input_audio,verbose_json, andtimestamp_granularities: ["word","segment"]. Chunks are 5 minutes, down from 20, to stay inside OpenRouter's 60-second upstream timeout. A reply with speech but no word timestamps fails once with a clear message and isn't retried.tests/fixtures/openrouter-catalog.json, prices illustrative) drives the offline smoke walk, which now also captures the OpenRouter wizard screens.Why?
Users asked to pick any LLM and a transcription model through one OpenRouter key, while keeping local transcription as an option.
Transcription is limited to the Whisper models because captions, cut tightening and clip timing need per-word timestamps. Through OpenRouter, only the Whisper models served by OpenAI-compatible upstreams (OpenAI, Groq, Together) reliably return them. Others either reject
verbose_jsonwith HTTP 400 (openai/gpt-4o-transcribe,microsoft/mai-transcribe-1.5, Google Chirp), return no word timings, or accept only short or WAV-only clips (AssemblyAI sync, Meta Muse Voice). The reasoning is recorded next toOPENROUTER_TRANSCRIPTION_MODELSinsrc/shared/openrouter.ts.Release notes
How did you test it?
npm test(723 passing, including the newtests/openrouter.test.ts: catalogue parsing, allowlist filtering, JSON request body, WAV labelling, chunk length, missing-word errors without retries, and the key guard on provider switches)npm run typechecknpm run lintscripts/test-pipeline.tsandscripts/test-resilience.ts(offline), plus a manual check that an 11-minute file splits into three 5-minute chunks with correct keep-windowsAnything to watch out for?
prompt, so chunks are transcribed without the previous chunk's text. The 8-second overlap and seam repair still cover the joins.🤖 Generated with Claude Code
https://claude.ai/code/session_012fRPuZ4E7P2EcTR4c9BzhS