Skip to content

starit/themed-extension

Themed LLM Secure Proxy

A Chrome Extension (Manifest V3) that lets any webpage call an LLM API while keeping the API key completely secure. The key is stored only in the extension's background service worker and is never exposed to:

  • The webpage JavaScript runtime
  • The webpage network panel
  • Any injected or content script

All HTTP requests to the LLM provider are executed exclusively in the extension's service worker.

Features

  • Manifest V3 with service worker background
  • TypeScript + Vite + CRXJS
  • 16 providers (including Custom OpenAI-compatible): OpenAI, OpenRouter, Groq, DeepSeek, Together, Mistral, Moonshot, Azure OpenAI, Fireworks, Anyscale, Perplexity, SiliconFlow, Zhipu, MiniMax, Lingyi, Custom
  • Streaming via Port-based messaging with AbortController support
  • Rate limiting — 60 requests/minute per tab
  • AI Context Provider — store structured data locally (themes, prompts, JSON) and inject it into LLM requests as system messages; webpages can request read access with user approval
  • Schema system — group context entries by structural type; pages can register schemas via API
  • Secure confirmation popupsaveContext() opens a native extension popup so users grant permission inside the extension boundary

Build & Install

pnpm install
pnpm build        # production build → dist/
pnpm dev          # watch mode
pnpm test         # run unit tests (Vitest)
pnpm test:watch   # Vitest watch mode

Load the extension:

  1. Open Chrome → chrome://extensions/
  2. Enable Developer mode
  3. Click Load unpacked → select the dist/ folder

Configure

  1. Click the extension icon in the toolbar → Options
  2. Select provider and enter your API key
  3. Optionally set a custom base URL and model
  4. Save

Webpage API

All methods are available on window.ThemedLLM. The object is frozen and non-configurable — page code cannot replace or delete it.

LLM Requests

// Non-streaming — returns full response as a string
const reply = await window.ThemedLLM.chat([
  { role: 'user', content: 'Hello!' }
]);

// Streaming — calls onToken for each token as it arrives
await window.ThemedLLM.chatStream(
  [{ role: 'user', content: 'Count to 5' }],
  (token) => process.stdout.write(token)
);

// With options
await window.ThemedLLM.chat(messages, {
  temperature: 0.8,
  max_tokens: 1024,
  signal: abortController.signal,   // cancellation support
  contextKeys: ['my_prompt', 'theme_data']  // inject stored context entries
});

Context Entries

Context entries are key-value pairs stored locally in the extension. When a request includes contextKeys, the matching entries are injected as system messages before the user's messages. Webpages can read keys or content only after the user approves access.

// Save a context entry — opens a native confirm popup for user approval
await window.ThemedLLM.saveContext('my_prompt', 'You are a helpful assistant.');

// Save with a schema tag
await window.ThemedLLM.saveContext('dark_theme', JSON.stringify(themeObj), {
  schemaId: '@my-app/theme'
});

// List all stored entry keys
const keys = await window.ThemedLLM.listContextKeys();

// Filter by schema
const themeKeys = await window.ThemedLLM.listContextKeys({ schemaId: '@my-app/theme' });

// Read back the content of an entry (prompts for permission)
const content = await window.ThemedLLM.getContext('my_prompt');

Note: saveContext() rejects duplicate keys unless you pass { overwrite: true }. You can also edit entries in Options.

Context keys: 1–100 characters; Unicode is allowed. Control characters, line breaks, bidi overrides, zero-width space, and BOM are rejected; __proto__, constructor, and prototype are reserved.

Schemas

Schemas group context entries (e.g. all themes of one shape). Schema IDs use a namespaced pattern such as @my-app/theme. Optional fields may mark keys as required: true — then any saveContext(..., { schemaId }) must supply JSON object content that includes those keys with the declared type (string / number / boolean / object). Fields without required are documentation-only.

// Register a schema (idempotent — safe to call on every page load)
await window.ThemedLLM.registerSchema({
  id: '@my-app/theme',
  name: 'App Theme',
  description: 'Color and typography tokens',
  autoInject: true,   // all entries of this schema are injected into every chat() call
  fields: [
    { key: 'colors.primary', type: 'string', required: true, description: 'Primary brand color' },
    { key: 'fonts.body', type: 'string', description: 'Body font family' },
  ]
});

// List all registered schemas
const schemas = await window.ThemedLLM.listSchemas();
// → [{ id: '@my-app/theme', name: 'App Theme', description: '...' }]

Note: Pages can only create or update schemas with source: 'page'. Schemas created manually in the Options page (source: 'user') cannot be overwritten by page code.

autoInject: true — when set, all entries belonging to this schema are automatically injected as system messages in every chat() / chatStream() call, without needing to pass contextKeys explicitly. Explicit contextKeys always take priority; auto-injected entries fill up to the 20-key cap. The toggle can also be set from the Options page.

Query Current Config

const llm = await window.ThemedLLM.getCurrentLLM();
// → { provider: 'openai', model: 'gpt-4o-mini' }  or  null if not configured

Architecture

Page (window.ThemedLLM)
  → postMessage
Content Script  (ISOLATED world — bridge only)
  → chrome.runtime Port / sendMessage
Background Service Worker  (API key lives here)
  → fetch → LLM provider HTTPS API
Layer API Key Role
Injected script Never Exposes window.ThemedLLM, no chrome APIs
Content script Never Bridges postMessage ↔ chrome.runtime
Background SW At request time Storage read, all HTTP requests
Options page Write-only (masked) Config + context management

For a full technical reference including message protocol, error codes, and data flow diagrams, see docs/ARCHITECTURE.md.

Demo

Themed.js integration demo — registers a schema, saves theme entries, uses the extension as an LLM backend with no API key on the page:

pnpm example:themed

Open http://localhost:5174. This demo is a local web page; the extension must be loaded from dist/ and configured in Options for it to work. You can also open the built-in Demo page from the Options footer.

Compatibility

Chrome 120+

About

A Chrome Extension (Manifest V3) that allows any webpage to call an LLM API while keeping the API key secure.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors