Repository navigation
feat/openrouter voice - #6
Merged
Merged
Conversation
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
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 PR do?
Related issue
Type of change
Checklist
pytestpasses locallyruff check .passes locallypytestvalidates it.envfiles are committed