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
53
packages/web-ui/src/storage/app-storage.ts
Normal file
53
packages/web-ui/src/storage/app-storage.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { LocalStorageBackend } from "./backends/local-storage-backend.js";
|
||||
import { ProviderKeysRepository } from "./repositories/provider-keys-repository.js";
|
||||
import { SettingsRepository } from "./repositories/settings-repository.js";
|
||||
import type { AppStorageConfig } from "./types.js";
|
||||
|
||||
/**
|
||||
* High-level storage API aggregating all repositories.
|
||||
* Apps configure backends and use repositories through this interface.
|
||||
*/
|
||||
export class AppStorage {
|
||||
readonly settings: SettingsRepository;
|
||||
readonly providerKeys: ProviderKeysRepository;
|
||||
|
||||
constructor(config: AppStorageConfig = {}) {
|
||||
// Use LocalStorage with prefixes as defaults
|
||||
const settingsBackend = config.settings ?? new LocalStorageBackend("settings");
|
||||
const providerKeysBackend = config.providerKeys ?? new LocalStorageBackend("providerKeys");
|
||||
|
||||
this.settings = new SettingsRepository(settingsBackend);
|
||||
this.providerKeys = new ProviderKeysRepository(providerKeysBackend);
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance management
|
||||
let globalAppStorage: AppStorage | null = null;
|
||||
|
||||
/**
|
||||
* Get the global AppStorage instance.
|
||||
* Throws if not initialized.
|
||||
*/
|
||||
export function getAppStorage(): AppStorage {
|
||||
if (!globalAppStorage) {
|
||||
throw new Error("AppStorage not initialized. Call setAppStorage() first.");
|
||||
}
|
||||
return globalAppStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the global AppStorage instance.
|
||||
*/
|
||||
export function setAppStorage(storage: AppStorage): void {
|
||||
globalAppStorage = storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize AppStorage with default configuration if not already set.
|
||||
*/
|
||||
export function initAppStorage(config: AppStorageConfig = {}): AppStorage {
|
||||
if (!globalAppStorage) {
|
||||
globalAppStorage = new AppStorage(config);
|
||||
}
|
||||
return globalAppStorage;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import type { StorageBackend } from "../types.js";
|
||||
|
||||
/**
|
||||
* Repository for managing provider API keys.
|
||||
* Provides domain-specific methods for key management.
|
||||
*/
|
||||
export class ProviderKeysRepository {
|
||||
constructor(private backend: StorageBackend) {}
|
||||
|
||||
/**
|
||||
* Get the API key for a provider.
|
||||
*/
|
||||
async getKey(provider: string): Promise<string | null> {
|
||||
return this.backend.get<string>(`key:${provider}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the API key for a provider.
|
||||
*/
|
||||
async setKey(provider: string, key: string): Promise<void> {
|
||||
await this.backend.set(`key:${provider}`, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the API key for a provider.
|
||||
*/
|
||||
async removeKey(provider: string): Promise<void> {
|
||||
await this.backend.delete(`key:${provider}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all providers that have keys stored.
|
||||
*/
|
||||
async getProviders(): Promise<string[]> {
|
||||
const allKeys = await this.backend.keys();
|
||||
return allKeys.filter((key) => key.startsWith("key:")).map((key) => key.substring(4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider has a key stored.
|
||||
*/
|
||||
async hasKey(provider: string): Promise<boolean> {
|
||||
return this.backend.has(`key:${provider}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all stored API keys.
|
||||
*/
|
||||
async clearAll(): Promise<void> {
|
||||
const providers = await this.getProviders();
|
||||
for (const provider of providers) {
|
||||
await this.removeKey(provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import type { StorageBackend } from "../types.js";
|
||||
|
||||
/**
|
||||
* Repository for simple application settings (proxy, theme, etc.).
|
||||
* Uses a single backend for all settings.
|
||||
*/
|
||||
export class SettingsRepository {
|
||||
constructor(private backend: StorageBackend) {}
|
||||
|
||||
/**
|
||||
* Get a setting value by key.
|
||||
*/
|
||||
async get<T = unknown>(key: string): Promise<T | null> {
|
||||
return this.backend.get<T>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a setting value.
|
||||
*/
|
||||
async set<T = unknown>(key: string, value: T): Promise<void> {
|
||||
await this.backend.set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a setting.
|
||||
*/
|
||||
async delete(key: string): Promise<void> {
|
||||
await this.backend.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all setting keys.
|
||||
*/
|
||||
async keys(): Promise<string[]> {
|
||||
return this.backend.keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting exists.
|
||||
*/
|
||||
async has(key: string): Promise<boolean> {
|
||||
return this.backend.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all settings.
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
await this.backend.clear();
|
||||
}
|
||||
}
|
||||
48
packages/web-ui/src/storage/types.ts
Normal file
48
packages/web-ui/src/storage/types.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Base interface for all storage backends.
|
||||
* Provides a simple key-value storage abstraction that can be implemented
|
||||
* by localStorage, IndexedDB, chrome.storage, or remote APIs.
|
||||
*/
|
||||
export interface StorageBackend {
|
||||
/**
|
||||
* Get a value by key. Returns null if key doesn't exist.
|
||||
*/
|
||||
get<T = unknown>(key: string): Promise<T | null>;
|
||||
|
||||
/**
|
||||
* Set a value for a key.
|
||||
*/
|
||||
set<T = unknown>(key: string, value: T): Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete a key.
|
||||
*/
|
||||
delete(key: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get all keys.
|
||||
*/
|
||||
keys(): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Clear all data.
|
||||
*/
|
||||
clear(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if a key exists.
|
||||
*/
|
||||
has(key: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring AppStorage.
|
||||
*/
|
||||
export interface AppStorageConfig {
|
||||
/** Backend for simple settings (proxy, theme, etc.) */
|
||||
settings?: StorageBackend;
|
||||
/** Backend for provider API keys */
|
||||
providerKeys?: StorageBackend;
|
||||
/** Backend for sessions (chat history, attachments) */
|
||||
sessions?: StorageBackend;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue