mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 01:03:49 +00:00
Add Anthropic prompt caching, pluggable storage, and CORS proxy support
Storage Architecture:
- New pluggable storage system with backends (LocalStorage, ChromeStorage, IndexedDB)
- SettingsRepository for app settings (proxy config, etc.)
- ProviderKeysRepository for API key management
- AppStorage with global accessors (getAppStorage, setAppStorage, initAppStorage)
Transport Refactoring:
- Renamed DirectTransport → ProviderTransport (calls LLM providers with optional CORS proxy)
- Renamed ProxyTransport → AppTransport (uses app server with user auth)
- Updated TransportMode: "direct" → "provider", "proxy" → "app"
CORS Proxy Integration:
- ProviderTransport checks proxy.enabled/proxy.url from storage
- When enabled, modifies model baseUrl to route through proxy: {proxyUrl}/?url={originalBaseUrl}
- ProviderKeyInput test function also honors proxy settings
- Settings dialog with Proxy tab (Switch toggle, URL input, explanatory description)
Anthropic Prompt Caching:
- System prompt cached with cache_control markers (both OAuth and regular API keys)
- Last user message cached to cache conversation history
- Saves 90% on input tokens for cached content (10x cost reduction)
Settings Dialog Improvements:
- Configurable tab system with SettingsTab base class
- ApiKeysTab and ProxyTab as custom elements
- Switch toggle for proxy enable (instead of Checkbox)
- Explanatory paragraphs for each tab
- ApiKeyPromptDialog reuses ProviderKeyInput component
Removed:
- Deprecated ApiKeysDialog (replaced by ProviderKeyInput in SettingsDialog)
- Old storage-adapter and key-store (replaced by new storage architecture)
This commit is contained in:
parent
66f092c0c6
commit
0496651308
31 changed files with 1141 additions and 488 deletions
223
packages/web-ui/src/dialogs/SettingsDialog.ts
Normal file
223
packages/web-ui/src/dialogs/SettingsDialog.ts
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
html,
|
||||
Input,
|
||||
i18n,
|
||||
Label,
|
||||
Switch,
|
||||
type TemplateResult,
|
||||
} from "@mariozechner/mini-lit";
|
||||
import { getProviders } from "@mariozechner/pi-ai";
|
||||
import { LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import "../components/ProviderKeyInput.js";
|
||||
import { getAppStorage } from "../storage/app-storage.js";
|
||||
|
||||
// Base class for settings tabs
|
||||
export abstract class SettingsTab extends LitElement {
|
||||
abstract getTabName(): string;
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// API Keys Tab
|
||||
@customElement("api-keys-tab")
|
||||
export class ApiKeysTab extends SettingsTab {
|
||||
getTabName(): string {
|
||||
return i18n("API Keys");
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const providers = getProviders();
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col gap-6">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
${i18n("Configure API keys for LLM providers. Keys are stored locally in your browser.")}
|
||||
</p>
|
||||
${providers.map((provider) => html`<provider-key-input .provider=${provider}></provider-key-input>`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy Tab
|
||||
@customElement("proxy-tab")
|
||||
export class ProxyTab extends SettingsTab {
|
||||
@state() private proxyEnabled = false;
|
||||
@state() private proxyUrl = "http://localhost:3001";
|
||||
|
||||
override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Load proxy settings when tab is connected
|
||||
try {
|
||||
const storage = getAppStorage();
|
||||
const enabled = await storage.settings.get<boolean>("proxy.enabled");
|
||||
const url = await storage.settings.get<string>("proxy.url");
|
||||
|
||||
if (enabled !== null) this.proxyEnabled = enabled;
|
||||
if (url !== null) this.proxyUrl = url;
|
||||
} catch (error) {
|
||||
console.error("Failed to load proxy settings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveProxySettings() {
|
||||
try {
|
||||
const storage = getAppStorage();
|
||||
await storage.settings.set("proxy.enabled", this.proxyEnabled);
|
||||
await storage.settings.set("proxy.url", this.proxyUrl);
|
||||
} catch (error) {
|
||||
console.error("Failed to save proxy settings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
getTabName(): string {
|
||||
return i18n("Proxy");
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
${i18n("The CORS proxy strips CORS headers from API responses, allowing browser-based apps to make direct calls to LLM providers without CORS restrictions. It forwards requests to providers while removing headers that would otherwise block cross-origin requests.")}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-foreground">${i18n("Use CORS Proxy")}</span>
|
||||
${Switch({
|
||||
checked: this.proxyEnabled,
|
||||
onChange: (checked: boolean) => {
|
||||
this.proxyEnabled = checked;
|
||||
this.saveProxySettings();
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
${Label({ children: i18n("Proxy URL") })}
|
||||
${Input({
|
||||
type: "text",
|
||||
value: this.proxyUrl,
|
||||
disabled: !this.proxyEnabled,
|
||||
onInput: (e) => {
|
||||
this.proxyUrl = (e.target as HTMLInputElement).value;
|
||||
},
|
||||
onChange: () => this.saveProxySettings(),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("settings-dialog")
|
||||
export class SettingsDialog extends LitElement {
|
||||
@property({ type: Array, attribute: false }) tabs: SettingsTab[] = [];
|
||||
@state() private isOpen = false;
|
||||
@state() private activeTabIndex = 0;
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
static async open(tabs: SettingsTab[]) {
|
||||
const dialog = new SettingsDialog();
|
||||
dialog.tabs = tabs;
|
||||
dialog.isOpen = true;
|
||||
document.body.appendChild(dialog);
|
||||
}
|
||||
|
||||
private setActiveTab(index: number) {
|
||||
this.activeTabIndex = index;
|
||||
}
|
||||
|
||||
private renderSidebarItem(tab: SettingsTab, index: number): TemplateResult {
|
||||
const isActive = this.activeTabIndex === index;
|
||||
return html`
|
||||
<button
|
||||
class="w-full text-left px-4 py-3 rounded-md transition-colors ${
|
||||
isActive
|
||||
? "bg-secondary text-foreground font-medium"
|
||||
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground"
|
||||
}"
|
||||
@click=${() => this.setActiveTab(index)}
|
||||
>
|
||||
${tab.getTabName()}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMobileTab(tab: SettingsTab, index: number): TemplateResult {
|
||||
const isActive = this.activeTabIndex === index;
|
||||
return html`
|
||||
<button
|
||||
class="px-3 py-2 text-sm font-medium transition-colors ${
|
||||
isActive ? "border-b-2 border-primary text-foreground" : "text-muted-foreground hover:text-foreground"
|
||||
}"
|
||||
@click=${() => this.setActiveTab(index)}
|
||||
>
|
||||
${tab.getTabName()}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.tabs.length === 0) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return Dialog({
|
||||
isOpen: this.isOpen,
|
||||
onClose: () => {
|
||||
this.isOpen = false;
|
||||
this.remove();
|
||||
},
|
||||
width: "min(1000px, 90vw)",
|
||||
height: "min(800px, 90vh)",
|
||||
children: html`
|
||||
${DialogContent({
|
||||
className: "h-full p-6",
|
||||
children: html`
|
||||
<div class="flex flex-col h-full overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="pb-4 flex-shrink-0">${DialogHeader({ title: i18n("Settings") })}</div>
|
||||
|
||||
<!-- Mobile Tabs -->
|
||||
<div class="md:hidden flex flex-shrink-0 pb-4">
|
||||
${this.tabs.map((tab, index) => this.renderMobileTab(tab, index))}
|
||||
</div>
|
||||
|
||||
<!-- Layout -->
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- Sidebar (desktop only) -->
|
||||
<div class="hidden md:block w-64 flex-shrink-0 space-y-1">
|
||||
${this.tabs.map((tab, index) => this.renderSidebarItem(tab, index))}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto md:pl-6">
|
||||
${this.tabs.map(
|
||||
(tab, index) =>
|
||||
html`<div style="display: ${this.activeTabIndex === index ? "block" : "none"}">${tab}</div>`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="pt-4 flex-shrink-0">
|
||||
<p class="text-xs text-muted-foreground text-center">
|
||||
${i18n("Settings are stored locally in your browser")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})}
|
||||
`,
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue