Skip to content

feat/openrouter voice - #6

Merged
shettydev merged 7 commits into
mainfrom
feat/openrouter-voice
Jul 9, 2026
Merged

shettydev merged 7 commits into
mainfrom
feat/openrouter-voice

Conversation

@shettydev

Copy link
Copy Markdown
Owner
  • feat(backend): add OpenRouter as a selectable AI provider
  • feat(frontend): add turn-based voice mode (browser STT, LLM, TTS)

What does this PR do?

Related issue

Type of change

  • Bug fix
  • New feature
  • New problem(s)
  • Docs
  • Refactor / chore

Checklist

  • pytest passes locally
  • ruff check . passes locally
  • I added/updated tests where it makes sense
  • If I added a problem, it follows the YAML format in CONTRIBUTING.md and pytest validates it
  • No API keys, secrets, or .env files are committed

shettydev and others added 7 commits July 2, 2026 23:57
Introduce an OpenRouter provider (AI_PROVIDER=openrouter) that routes chat
completions through openrouter.ai using the OpenAI-compatible API, so one
OPENROUTER_API_KEY unlocks many underlying models by slug (Anthropic, OpenAI,
Google, Meta). It mirrors the existing provider contract — streaming chat and
JSON-mode test-case generation — and is dispatched from services/ai.py.

Because the interviewer, tutor, and voice mode all stream through the same
ai.stream_chat path, selecting OpenRouter makes the entire app model-agnostic.

- services/providers/openrouter_provider.py: new provider (base_url, key,
  optional HTTP-Referer / X-Title attribution headers, curated model list)
- services/ai.py: dispatch AI_PROVIDER=openrouter
- config.py: OPENROUTER_* settings and defaults
- .env.example: document the provider and its keys

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a voice interview mode that wraps the existing text pipeline in a
speech loop: mic -> STT -> useInterview.send() -> streamed LLM reply -> TTS.
The LLM turn is not re-implemented — send()/start() run the same streamSSE
path, and a registered voice sink speaks assistant tokens as they arrive, so
voice runs on whichever AI_PROVIDER is configured (OpenAI, Claude, OpenRouter).

STT and TTS use the browser Web Speech API (Chrome/Edge, no extra keys) behind
a thin seam so a premium provider (Deepgram/ElevenLabs) can drop in later.

- lib/voice/stt.ts, tts.ts: Web Speech adapters; TTS is sentence-buffered for
  low latency and strips markdown so it doesn't read backticks aloud
- lib/voice/sink.ts: one-slot registry decoupling the interview store from voice
- store/voice.ts: listening -> thinking -> speaking state machine; mic pauses
  while the interviewer speaks to avoid echo
- store/interview.ts: feed streamed assistant deltas to the sink (no-op in text)
- components/interview/VoiceControls.tsx: status strip, interim transcript, mute
- components/interview/InterviewTopBar.tsx: voice toggle; ChatPanel.tsx: mount

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hardcoded OpenRouter model list with the live public /models
catalog (~300+ models, no API key needed), cached in-process with a 1h TTL and
an offline seed fallback. The UI receives only a curated "popular few"; the full
catalog is searched server-side and returned bounded, so the client never holds
the whole list.

- openrouter_provider.py: cached catalog(), curated models() resolved against
  live data (stale slugs drop off), ranked bounded search_models(), and
  self-healing default_model()/valid_model() that validate against the catalog
  so a retired default (e.g. claude-3.5-sonnet -> claude-sonnet-5) auto-recovers
- openai_provider.py / claude_provider.py: uniform models()/search_models()
  over their small static lists (filtered, no live search)
- ai.py: search_models dispatch + supports_search flag in config_info
- routes/models.py: GET /api/models/search?q=&limit= (login-gated, bounded)
- config.py: OPENROUTER_MODELS_URL

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the static model <select> in engine settings with a searchable
combobox. It shows the curated popular few by default and, for providers that
support it (OpenRouter), runs a debounced server-side search against the live
catalog — so the client only ever holds the popular few plus a bounded result
page, never the full ~300-model list.

- ModelCombobox.tsx: debounced (250ms) search, latest-query-wins race guard,
  keyboard nav (up/down/enter/esc), click-outside close; results are derived
  (empty/filter cases) with only async server results held in state; each row
  shows a context-length / $-per-Mtok subtitle
- SettingsModal.tsx: use the combobox for the model field
- api.ts: searchModels() -> GET /api/models/search
- types.ts: ModelOption context_length/pricing + EngineConfig.supports_search

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prevents a dead scrollbar track and sub-pixel gap by hiding html/body overflow. Also switches to dvh for dynamic viewport height.
- Default to Claude Code CLI (no API key needed), add OpenAI/OpenRouter as options
- Switch npm to pnpm, update setup instructions
- Add resizable panels with remembered widths, narrow-screen tabs
- Mark voice mode as temporarily unavailable, note TTS rework
- Update FAQ for AI models, pricing, data storage
- Fix LinkedIn link
@shettydev shettydev self-assigned this Jul 9, 2026
@shettydev
shettydev merged commit 08e7dfb into main Jul 9, 2026
2 checks passed
@shettydev
shettydev deleted the feat/openrouter-voice branch July 9, 2026 18:29
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