Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions docs/providers-and-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ await pm.save(); // Persist to chrome.storage.local
pm.getActive(); // Get the active provider instance
await pm.setActive('openai'); // Switch active provider
await pm.updateProvider('openai', { model: 'gpt-5' }); // Update config
await pm.duplicateProvider('openai'); // Create openai__duplicate
await pm.removeDuplicateProvider('openai__duplicate'); // Remove it
pm.getAll(); // All provider configs (for Settings UI)
await pm.testProvider('openai'); // Test connection
```
Expand All @@ -362,6 +364,17 @@ state and is separate from `activeProvider`, which is the provider currently
configured. Connection tests report reachability but do not control the Active
flag.

Settings can create one independent duplicate of each configurable endpoint
provider. A duplicate is stored as a normal provider entry with the stable ID
`<source>__duplicate` and a `duplicateOf` reference to the source definition,
so credentials, models, endpoint URLs, compatibility options, export/import,
and active-provider selection continue to use the existing provider schema.
The manager rejects duplicate-of-duplicate, second, orphaned, type-mismatched,
and forged duplicate entries when loading storage. WebBrain Cloud and the
Chromium-only WebGPU runtime are not duplicable because they do not represent
independent user-managed API credentials or endpoints; their cards keep the
Duplicate affordance disabled with an explanatory tooltip.

### Settings Search

The Settings search index includes provider IDs, labels, type/category, model,
Expand All @@ -372,7 +385,7 @@ ties, and the selected provider remains visible across category filters.

### Config Persistence

Configs are stored in `chrome.storage.local` under the `providers` key, merged against defaults. Defaults provide the SHAPE (which provider keys exist); stored configs override per-key values. This allows upgrades that introduce new provider entries to work without users clearing storage.
Configs are stored in `chrome.storage.local` under the `providers` key, merged against defaults. Defaults provide the SHAPE (which provider keys exist); stored configs override per-key values. This allows upgrades that introduce new provider entries to work without users clearing storage. Duplicate entries share this same persistence path and therefore remain portable through Settings config export/import.

Deprecated provider entries (`webbrain`, `openai_subscription`,
`claude_subscription`) are filtered out.
Expand Down Expand Up @@ -417,7 +430,7 @@ this option because its build has no MV3 offscreen document.

### Transcription Provider

Used by Tab Recorder for Whisper transcription. Falls back through configured providers in priority order: OpenAI → Groq → LM Studio → llama.cpp. Blocklist excludes providers known not to host Whisper (Anthropic, Gemini, Mistral, DeepSeek, xAI, Nvidia).
Used by Tab Recorder for Whisper transcription. Falls back through configured providers in priority order: OpenAI → Groq → LM Studio → llama.cpp. Blocklist excludes providers known not to host Whisper (Anthropic, Gemini, Mistral, DeepSeek, xAI, Nvidia, Kimi), including duplicates of those providers.

---

Expand Down
10 changes: 6 additions & 4 deletions src/chrome/src/agent/transcribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ function pickProvider(providers) {
// …then any other OpenAI-compatible provider not on the blocklist.
const iter = providers.entries ? providers.entries() : Object.entries(providers);
for (const [id, p] of iter) {
if (NO_WHISPER.has(id)) continue;
const cfg = p.config || p;
const sourceProviderId = cfg.duplicateOf || id;
if (NO_WHISPER.has(sourceProviderId)) continue;
if (cfg.type !== 'openai') continue;
if (cfg.enabled === false) continue;
if (!cfg.baseUrl) continue;
if (!cfg.apiKey) continue;
return { id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey };
return { id, sourceProviderId, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey };
}
return null;
}
Expand Down Expand Up @@ -139,7 +140,8 @@ export async function transcribeAudio(providers, audioBlob, opts = {}) {
};
}

const model = opts.modelOverride || picked.explicitModel || WHISPER_MODEL_BY_PROVIDER[picked.id] || 'whisper-1';
const sourceProviderId = picked.sourceProviderId || picked.id;
const model = opts.modelOverride || picked.explicitModel || WHISPER_MODEL_BY_PROVIDER[sourceProviderId] || 'whisper-1';
const filename = opts.filename || 'recording.webm';

// Explicit overrides may be imported from an older version as a bare local
Expand Down Expand Up @@ -196,7 +198,7 @@ export async function transcribeAudio(providers, audioBlob, opts = {}) {
res.status === 415 &&
/application\/json|must.*use.*json/i.test(detail);
if (isChatOnlyEndpoint) {
const isLocal = picked.id === 'lmstudio' || picked.id === 'llamacpp';
const isLocal = sourceProviderId === 'lmstudio' || sourceProviderId === 'llamacpp';
return {
ok: false,
error:
Expand Down
6 changes: 6 additions & 0 deletions src/chrome/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -3311,6 +3311,12 @@ async function handleMessage(msg, sender) {
return { ok: true };
}

case 'duplicate_provider':
return await providerManager.duplicateProvider(msg.providerId);

case 'remove_duplicate_provider':
return await providerManager.removeDuplicateProvider(msg.providerId);

case 'ollama_launch_handoff': {
const handoff = normalizeOllamaLaunchHandoff(msg.handoff || {});
await providerManager.updateProvider(handoff.providerId, handoff.config);
Expand Down
Loading
Loading