Skip to content

Add OpenRouter as a setup connection with searchable model pickers - #77

Merged
JeremySNR merged 5 commits into
mainfrom
ccr-8b709f89-hh58um
Sep 30, 2026
Merged

JeremySNR merged 5 commits into
mainfrom
ccr-8b709f89-hh58um

Conversation

@JeremySNR

@JeremySNR JeremySNR commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

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.

  • Key: OpenRouter API key field, encrypted with safeStorage the same way as the OpenAI key. Check key calls OpenRouter's /key endpoint, which makes no model request.
  • Clip-finding model (LLM): a searchable dropdown built from OpenRouter's public /models catalogue. 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.
  • Transcription: local Whisper (pinned first, with the existing install fields) or one of OpenRouter's three Whisper models: openai/whisper-1, openai/whisper-large-v3-turbo and openai/whisper-large-v3. Nothing else is allowed, and a stored or submitted unsupported id is refused.
  • Routing: with OpenRouter selected, chat and transcription go to https://openrouter.ai/api/v1 with the OpenRouter key. The OpenAI base-URL settings and OPENAI_BASE_URL don't apply on this route.
  • Key guard: every request checks its key against the credential of the currently selected connection. If the provider is switched in Settings while a job runs, the job stops instead of sending one vendor's key to the other's endpoint.
  • Transcription request: uses OpenRouter's documented JSON body: base64 input_audio, verbose_json, and timestamp_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.
  • Offline: if the catalogue can't be fetched, the suggested models stand in. A sample catalogue fixture (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_json with 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 to OPENROUTER_TRANSCRIPTION_MODELS in src/shared/openrouter.ts.

Release notes

  • OpenRouter is now a connection in first-run setup and Settings. Add an OpenRouter key (encrypted with your system keychain), then pick the clip-finding model from OpenRouter's full model list. The list is searchable, suggests models at the top, and shows prices, context size and image support. Transcription can use local Whisper or one of OpenRouter's Whisper models, which are the only OpenRouter transcription models that return the per-word timestamps captions need.
  • OpenRouter transcription sends five-minute audio chunks so each request finishes inside OpenRouter's 60-second limit.
  • The setup wizard no longer shows the unavailable "Claude subscription" card. Claude models can be used through OpenRouter instead.

How did you test it?

  • npm test (723 passing, including the new tests/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 typecheck
  • npm run lint
  • Checked the change in the running app: wizard and Settings screenshots under Xvfb, using the sample catalogue
  • scripts/test-pipeline.ts and scripts/test-resilience.ts (offline), plus a manual check that an 11-minute file splits into three 5-minute chunks with correct keep-windows

Anything to watch out for?

  • Not live-tested. The dev sandbox couldn't reach openrouter.ai, so no real OpenRouter request was made. The request shapes follow OpenRouter's documented schema, but a real-key check of one clip-finding run and one Whisper transcription is still needed.
  • Suggested model ids came from recent web sources. Any that OpenRouter doesn't list are hidden automatically.
  • No context prompt: OpenRouter's transcription schema has no top-level prompt, so chunks are transcribed without the previous chunk's text. The 8-second overlap and seam repair still cover the joins.
  • Preview and export are untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_012fRPuZ4E7P2EcTR4c9BzhS

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

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/main/pipeline/openai.ts
Comment thread src/main/ipc.ts
- 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

Copy link
Copy Markdown
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:

  • Mixed credentials after a mid-job switch (security): jobs read their key once at the start, but endpoints are read on every request. Switching provider in Settings while a job ran could send an OpenAI key to OpenRouter, or the reverse. Requests now check the key against the current route and stop with "The AI connection changed… Start it again."
  • Retried word-timing failure: a missing-word-timestamps reply was retried 4× (same as the Bugbot thread). It's now checked after the retry loop.
  • Wrong credential named in errors: the caption and clip-finding errors and the top-bar prompt now name the credential for the selected provider.
  • Stale highlight in the model picker: it now tracks the highlighted row by id, so a catalogue that arrives while the list is open can't move Enter onto a different model.
  • Catalogue load failure: if the IPC call rejects, the dropdowns fall back to the suggested models.

Deliberately left as they are (these match the existing API route's behaviour, or are follow-ups):

  • Shared localTranscription flag: one flag serves both the API and OpenRouter routes, so changing it on one changes the other. The UI shows the current state.
  • No local-Whisper check at finish: the wizard doesn't require a local Whisper check before finishing OpenRouter + local. The API route behaves the same.
  • No way to clear a stored key: the OpenAI key field works the same way.
  • Report label: source-discovery and editorial reports label the provider api for OpenRouter. Cache keys already include the OpenRouter base URL.
  • Missing tests: there are no unit tests for the settings.ts provider branches, because they would need an Electron safeStorage mock.

Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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.

Comment thread src/main/pipeline/openai.ts
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
@JeremySNR
JeremySNR merged commit 34f4e2e into main Sep 30, 2026
6 checks passed
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.

2 participants