Provider Fallback
React hook for per-capability Chrome Built-in AI ↔ Transformers.js provider resolution with truthful badge provenance.
Provider Fallback Hook
See it in action
Try the Writing Tools blocks for a working demo — the Write / Translate / Summarize / Complete blocks resolve Chrome Built-in AI independently per capability and fall back to Transformers.js behind an explicit download gate, rendering a truthful provider badge derived from this hook.
useProviderFallback
Resolve each capability (Summarizer, Translator, AI-edit, fill-mask) to Chrome Built-in AI or Transformers.js. Detection is independent per capability and reads the browser globals directly, so one capability can be Chrome-AI-served while another falls back in the same session. Every provider package is loaded via a dynamic import() inside the resolution path, so @localmode/react gains no hard dependency on @localmode/chrome-ai or @localmode/transformers — nothing about a provider ships until a capability first resolves. Resolution is lazy and session-cached, and each result carries truthful resolution provenance for a badge.
import { useProviderFallback, providerName } from '@localmode/react';
function SummarizeTab() {
const { resolveSummarizer, resolution, isResolving } = useProviderFallback();
async function run(text: string) {
const { model } = await resolveSummarizer({
chromeStyle: 'key-points',
length: 'medium',
fallbackModelId: 'Xenova/distilbart-cnn-6-6',
});
return summarize({ model, text });
}
return resolution ? <Badge provider={providerName(resolution.provider)} tier={resolution.tier} /> : null;
}Options
Prop
Type
In the browser, inject static loader overrides
The default loadChromeAI / loadTransformers loaders use a Function-constructed import() so @localmode/react keeps no hard dependency on the provider packages. That indirection is opaque to bundlers, so a browser build can't resolve the bare specifier at runtime. When you consume this hook in the browser, pass loaders with literal imports the bundler can see:
useProviderFallback({
loadChromeAI: () => import('@localmode/chrome-ai'),
loadTransformers: () => import('@localmode/transformers'),
});Return Value
Prop
Type
Chrome availability & the download gate
Chrome's built-in AI APIs can exist while their on-device model has not been fetched. In that
state availability() reports 'downloadable', and Chrome refuses to start the download
outside a user activation — so a button is the only way to trigger it.
const {
chromeAvailability,
refreshChromeAvailability,
requestChromeDownload,
chromeDownloadProgress,
downloadingCapability,
} = useProviderFallback({ loadChromeAI, loadTransformers });
// Probing downloads nothing.
useEffect(() => {
void refreshChromeAvailability('summarize', { chromeStyle: 'tldr', length: 'medium' });
}, [refreshChromeAvailability]);
// Must run from a click.
<button onClick={() => void requestChromeDownload('summarize', { chromeStyle: 'tldr', length: 'medium' })}>
Download model
</button>;ChromeAIAvailability is 'available' | 'downloadable' | 'downloading' | 'unavailable' | 'unsupported'
(the last is ours, meaning the API is absent). ChromeCapability is 'summarize' | 'translate' | 'edit'
— fill-mask has no Chrome equivalent. Translator availability is per language pair, so pass
{ source, target } and re-probe whenever the pair changes.
The ui/local-first/chrome-ai-download-gate registry primitive renders this state machine —
button, progress, and the terminal unsupported / cannot-run states — from exactly these fields.
Resolver Params
Each resolver takes exactly the params its capability needs, so the split Write / Translate / Summarize / Complete surfaces each import only one. resolveFillMask takes a plain modelId: string (no Chrome equivalent).
ResolveSummarizerParams
Prop
Type
ResolveTranslatorParams
Prop
Type
ResolveEditEngineParams
Prop
Type
ResolvedModel and provenance
Every resolver returns a ResolvedModel<M> — the concrete model instance plus the provenance a badge derives from — and updates the hook's resolution, so a badge reflects what actually serves requests, never the detection probe alone.
Prop
Type
Standalone detectors and badge helpers
The detection functions are exported standalone (all async, all returning a ProviderId of 'chrome-ai' | 'transformers'). They read the same browser surfaces the @localmode/chrome-ai detectors read, so no provider import is needed to detect.
| Function | Signature | Behavior |
|---|---|---|
detectSummarizerProvider | (params?: ChromeCapabilityParams) => Promise<ProviderId> | Gated on Summarizer.availability() === 'available' — presence alone is not enough, since a merely-downloadable model would throw at create(). Every other state falls back to 'transformers'. |
detectTranslatorProvider | (params?: ChromeCapabilityParams) => Promise<ProviderId> | Gated on Translator.availability() === 'available' for the given language pair (packs download per directed pair). Every other state falls back to 'transformers'. |
probeChromeAvailability | (capability, params?, timeoutMs?) => Promise<ChromeAIAvailability> | Chrome's raw model state. Returns 'unsupported' only when the API is genuinely absent, and rethrows a bad-option TypeError rather than mislabelling the browser. Raced against a 3s deadline (CHROME_AVAILABILITY_TIMEOUT_MS) because Translator.availability() never settles on some builds; a timeout reports 'unavailable'. |
downloadChromeModel | (capability: ChromeCapability, params?: ChromeCapabilityParams, onProgress?: (p: number) => void) => Promise<ChromeAIAvailability> | Triggers Chrome's one-time, browser-wide download. Must be called from a user activation. Resolves to the post-download availability. |
detectPromptProvider | () => Promise<ProviderId> | Gated on LanguageModel.availability() === 'available' — presence alone is not enough, which prevents committing to a merely-downloadable Gemini Nano. Every other state falls back to 'transformers'. The legacy self.ai.languageModel surface (no availability()) trusts presence. |
providerTier | (provider: ProviderId) => ProviderTier | Maps 'chrome-ai' → 'built-in', 'transformers' → 'download'. |
providerName | (provider: ProviderId) => string | Human-readable name: 'Chrome AI' or 'Transformers.js'. |
import { detectPromptProvider, providerName, providerTier } from '@localmode/react';
const provider = await detectPromptProvider(); // 'chrome-ai' only when Gemini Nano is actually 'available'
const label = providerName(provider); // 'Chrome AI' | 'Transformers.js'
const tier = providerTier(provider); // 'built-in' | 'download'For the Chrome Built-in AI provider (Summarizer, Translator, Prompt APIs) see the Chrome AI guide; for the Transformers.js fallback models see the Transformers provider guide.
Blocks
| App | Description | Links |
|---|---|---|
| Writing Tools | Per-capability Chrome AI ⇄ Transformers.js resolution with a truthful provider-fallback badge via useProviderFallback | Live block · Source |