mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 18:03:50 +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
|
|
@ -0,0 +1,82 @@
|
|||
import type { StorageBackend } from "../types.js";
|
||||
|
||||
// Chrome extension API types (optional)
|
||||
declare const chrome: any;
|
||||
|
||||
/**
|
||||
* Storage backend using chrome.storage.local.
|
||||
* Good for: Browser extensions, syncing across devices (with chrome.storage.sync).
|
||||
* Limits: ~10MB for local, ~100KB for sync, async API.
|
||||
*/
|
||||
export class ChromeStorageBackend implements StorageBackend {
|
||||
constructor(private prefix: string = "") {}
|
||||
|
||||
private getKey(key: string): string {
|
||||
return this.prefix ? `${this.prefix}:${key}` : key;
|
||||
}
|
||||
|
||||
async get<T = unknown>(key: string): Promise<T | null> {
|
||||
if (!chrome?.storage?.local) {
|
||||
throw new Error("chrome.storage.local is not available");
|
||||
}
|
||||
|
||||
const fullKey = this.getKey(key);
|
||||
const result = await chrome.storage.local.get([fullKey]);
|
||||
return result[fullKey] !== undefined ? (result[fullKey] as T) : null;
|
||||
}
|
||||
|
||||
async set<T = unknown>(key: string, value: T): Promise<void> {
|
||||
if (!chrome?.storage?.local) {
|
||||
throw new Error("chrome.storage.local is not available");
|
||||
}
|
||||
|
||||
const fullKey = this.getKey(key);
|
||||
await chrome.storage.local.set({ [fullKey]: value });
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
if (!chrome?.storage?.local) {
|
||||
throw new Error("chrome.storage.local is not available");
|
||||
}
|
||||
|
||||
const fullKey = this.getKey(key);
|
||||
await chrome.storage.local.remove(fullKey);
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
if (!chrome?.storage?.local) {
|
||||
throw new Error("chrome.storage.local is not available");
|
||||
}
|
||||
|
||||
const allData = await chrome.storage.local.get(null);
|
||||
const allKeys = Object.keys(allData);
|
||||
const prefixWithColon = this.prefix ? `${this.prefix}:` : "";
|
||||
|
||||
if (this.prefix) {
|
||||
return allKeys
|
||||
.filter((key) => key.startsWith(prefixWithColon))
|
||||
.map((key) => key.substring(prefixWithColon.length));
|
||||
}
|
||||
|
||||
return allKeys;
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
if (!chrome?.storage?.local) {
|
||||
throw new Error("chrome.storage.local is not available");
|
||||
}
|
||||
|
||||
if (this.prefix) {
|
||||
const keysToRemove = await this.keys();
|
||||
const fullKeys = keysToRemove.map((key) => this.getKey(key));
|
||||
await chrome.storage.local.remove(fullKeys);
|
||||
} else {
|
||||
await chrome.storage.local.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
const value = await this.get(key);
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
107
packages/web-ui/src/storage/backends/indexeddb-backend.ts
Normal file
107
packages/web-ui/src/storage/backends/indexeddb-backend.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import type { StorageBackend } from "../types.js";
|
||||
|
||||
/**
|
||||
* Storage backend using IndexedDB.
|
||||
* Good for: Large data, binary blobs, complex queries.
|
||||
* Limits: ~50MB-unlimited (browser dependent), async API, more complex.
|
||||
*/
|
||||
export class IndexedDBBackend implements StorageBackend {
|
||||
private dbPromise: Promise<IDBDatabase> | null = null;
|
||||
|
||||
constructor(
|
||||
private dbName: string,
|
||||
private storeName: string = "keyvalue",
|
||||
) {}
|
||||
|
||||
private async getDB(): Promise<IDBDatabase> {
|
||||
if (this.dbPromise) {
|
||||
return this.dbPromise;
|
||||
}
|
||||
|
||||
this.dbPromise = new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return this.dbPromise;
|
||||
}
|
||||
|
||||
async get<T = unknown>(key: string): Promise<T | null> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(this.storeName, "readonly");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.get(key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
const value = request.result;
|
||||
resolve(value !== undefined ? (value as T) : null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async set<T = unknown>(key: string, value: T): Promise<void> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(this.storeName, "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.put(value, key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(this.storeName, "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(this.storeName, "readonly");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.getAllKeys();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result.map((key) => String(key)));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(this.storeName, "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.clear();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
const value = await this.get(key);
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import type { StorageBackend } from "../types.js";
|
||||
|
||||
/**
|
||||
* Storage backend using browser localStorage.
|
||||
* Good for: Simple settings, small data.
|
||||
* Limits: ~5MB, synchronous API (wrapped in promises), string-only (JSON serialization).
|
||||
*/
|
||||
export class LocalStorageBackend implements StorageBackend {
|
||||
constructor(private prefix: string = "") {}
|
||||
|
||||
private getKey(key: string): string {
|
||||
return this.prefix ? `${this.prefix}:${key}` : key;
|
||||
}
|
||||
|
||||
async get<T = unknown>(key: string): Promise<T | null> {
|
||||
const fullKey = this.getKey(key);
|
||||
const value = localStorage.getItem(fullKey);
|
||||
if (value === null) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
// If JSON parse fails, return as string
|
||||
return value as T;
|
||||
}
|
||||
}
|
||||
|
||||
async set<T = unknown>(key: string, value: T): Promise<void> {
|
||||
const fullKey = this.getKey(key);
|
||||
const serialized = JSON.stringify(value);
|
||||
localStorage.setItem(fullKey, serialized);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const fullKey = this.getKey(key);
|
||||
localStorage.removeItem(fullKey);
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
const allKeys: string[] = [];
|
||||
const prefixWithColon = this.prefix ? `${this.prefix}:` : "";
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key) {
|
||||
if (this.prefix) {
|
||||
if (key.startsWith(prefixWithColon)) {
|
||||
allKeys.push(key.substring(prefixWithColon.length));
|
||||
}
|
||||
} else {
|
||||
allKeys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allKeys;
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
if (this.prefix) {
|
||||
const keysToRemove = await this.keys();
|
||||
for (const key of keysToRemove) {
|
||||
await this.delete(key);
|
||||
}
|
||||
} else {
|
||||
localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
const fullKey = this.getKey(key);
|
||||
return localStorage.getItem(fullKey) !== null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue