From aea46652bc48c8b5f7df263da1a73e749691a6d2 Mon Sep 17 00:00:00 2001 From: shrijam12 Date: Sat, 7 Feb 2026 16:54:18 +0530 Subject: [PATCH 01/78] feat: Add Ollama provider support - Add OllamaProvider class with OpenAI-compatible API support - Register Ollama in ProviderRegistry with model selection rules - Add Ollama configuration to AppConfig (baseUrl, apiKey, enabled) - Add Ollama to chat_completions_providers_v1.json catalog with 16 popular models - Add ollama.yaml pricing file (free/local models) - Update ProviderName type to include 'ollama' - Add OLLAMA_BASE_URL and OLLAMA_API_KEY to .env.example Ollama runs models locally and exposes an OpenAI-compatible API at http://localhost:11434/v1 by default. Users can configure a custom base URL via OLLAMA_BASE_URL environment variable. --- .env.example | 3 + gateway/src/costs/ollama.yaml | 59 ++++ .../src/domain/providers/ollama-provider.ts | 104 +++++++ .../src/domain/services/provider-registry.ts | 5 +- .../src/infrastructure/config/app-config.ts | 275 +++++++++--------- .../chat_completions_providers_v1.json | 38 +++ shared/types/types.ts | 2 +- 7 files changed, 349 insertions(+), 137 deletions(-) create mode 100644 gateway/src/costs/ollama.yaml create mode 100644 gateway/src/domain/providers/ollama-provider.ts diff --git a/.env.example b/.env.example index 1b31994..9cd322e 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,9 @@ XAI_API_KEY=your_key_here OPENROUTER_API_KEY=your_key_here ZAI_API_KEY=your_key_here GOOGLE_API_KEY=your_key_here +# Ollama (local models — no API key needed, just set the base URL) +# OLLAMA_BASE_URL=http://localhost:11434/v1 +# OLLAMA_API_KEY= # optional, only if behind an auth proxy PORT=3001 # Optional x402 passthrough for OpenRouter access X402_BASE_URL=x402_supported_provider_url diff --git a/gateway/src/costs/ollama.yaml b/gateway/src/costs/ollama.yaml new file mode 100644 index 0000000..2d7bb29 --- /dev/null +++ b/gateway/src/costs/ollama.yaml @@ -0,0 +1,59 @@ +provider: "ollama" +currency: "USD" +unit: "MTok" +models: + # Ollama runs models locally — all costs are zero. + # Users may add custom model entries here if needed. + llama3.3: + input: 0.00 + output: 0.00 + llama3.2: + input: 0.00 + output: 0.00 + llama3.1: + input: 0.00 + output: 0.00 + llama3: + input: 0.00 + output: 0.00 + gemma3: + input: 0.00 + output: 0.00 + gemma2: + input: 0.00 + output: 0.00 + qwen3: + input: 0.00 + output: 0.00 + qwen2.5-coder: + input: 0.00 + output: 0.00 + deepseek-r1: + input: 0.00 + output: 0.00 + deepseek-coder-v2: + input: 0.00 + output: 0.00 + phi4: + input: 0.00 + output: 0.00 + phi3: + input: 0.00 + output: 0.00 + mistral: + input: 0.00 + output: 0.00 + mixtral: + input: 0.00 + output: 0.00 + codellama: + input: 0.00 + output: 0.00 + starcoder2: + input: 0.00 + output: 0.00 +metadata: + last_updated: "2026-02-03" + source: "https://ollama.com" + notes: "Ollama runs models locally. All API costs are zero — hardware costs are borne by the user." + version: "1.0" diff --git a/gateway/src/domain/providers/ollama-provider.ts b/gateway/src/domain/providers/ollama-provider.ts new file mode 100644 index 0000000..23df2cb --- /dev/null +++ b/gateway/src/domain/providers/ollama-provider.ts @@ -0,0 +1,104 @@ +import { BaseProvider } from './base-provider.js'; +import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; +import { getConfig } from '../../infrastructure/config/app-config.js'; + +interface OllamaRequest { + model: string; + messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string; }>; + max_tokens?: number; + temperature?: number; + stream?: boolean; + stop?: string | string[]; +} + +interface OllamaResponse { + id: string; + object: string; + created: number; + model: string; + choices: Array<{ + index: number; + message: { role: string; content: string; }; + finish_reason: string; + }>; + usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} + +export class OllamaProvider extends BaseProvider { + readonly name = 'ollama'; + + // Ollama exposes an OpenAI-compatible API at /v1 + protected get baseUrl(): string { + return getConfig().providers.ollama.baseUrl; + } + + // Ollama doesn't require an API key by default, but we return a + // dummy value so BaseProvider.isConfigured() stays true when the + // base URL is set. Users can optionally supply a real key if they + // put Ollama behind an auth proxy. + protected get apiKey(): string | undefined { + return getConfig().providers.ollama.apiKey || 'ollama'; + } + + /** + * Ollama is considered configured when the user has explicitly + * enabled it by setting OLLAMA_BASE_URL (even without an API key). + */ + isConfigured(): boolean { + return getConfig().providers.ollama.enabled; + } + + protected transformRequest(request: CanonicalRequest): OllamaRequest { + const messages = request.messages.map(msg => ({ + role: msg.role, + content: msg.content + .filter(c => c.type === 'text') + .map(c => c.text) + .join('') + })); + + return { + model: request.model, + messages, + max_tokens: request.maxTokens, + temperature: request.temperature, + stream: request.stream || false, + stop: request.stopSequences + }; + } + + protected transformResponse(response: OllamaResponse): CanonicalResponse { + const choice = response.choices[0]; + + return { + id: response.id, + model: response.model, + created: response.created, + message: { + role: 'assistant', + content: [{ + type: 'text', + text: choice.message.content + }] + }, + finishReason: this.mapFinishReason(choice.finish_reason), + usage: { + inputTokens: response.usage?.prompt_tokens ?? 0, + outputTokens: response.usage?.completion_tokens ?? 0, + totalTokens: response.usage?.total_tokens ?? 0 + } + }; + } + + private mapFinishReason(reason: string): 'stop' | 'length' | 'tool_calls' | 'error' { + switch (reason) { + case 'stop': return 'stop'; + case 'length': return 'length'; + default: return 'stop'; + } + } +} diff --git a/gateway/src/domain/services/provider-registry.ts b/gateway/src/domain/services/provider-registry.ts index 3a2486f..e2a227e 100644 --- a/gateway/src/domain/services/provider-registry.ts +++ b/gateway/src/domain/services/provider-registry.ts @@ -5,6 +5,7 @@ import { OpenRouterProvider } from '../providers/openrouter-provider.js'; import { XAIProvider } from '../providers/xai-provider.js'; import { ZAIProvider } from '../providers/zai-provider.js'; import { GoogleProvider } from '../providers/google-provider.js'; +import { OllamaProvider } from '../providers/ollama-provider.js'; export enum Provider { ANTHROPIC = 'anthropic', @@ -12,7 +13,8 @@ export enum Provider { OPENROUTER = 'openrouter', XAI = 'xAI', ZAI = 'zai', - GOOGLE = 'google' + GOOGLE = 'google', + OLLAMA = 'ollama' } export interface ProviderSelectionRule { @@ -83,6 +85,7 @@ export function createDefaultProviderRegistry(): ProviderRegistry { { id: Provider.XAI, create: () => new XAIProvider(), selectionRules: [{ match: model => model.includes('grok-') || model.includes('grok_beta') }] }, { id: Provider.ZAI, create: () => new ZAIProvider() }, { id: Provider.GOOGLE, create: () => new GoogleProvider(), selectionRules: [{ match: model => model.toLowerCase().includes('gemini') }] }, + { id: Provider.OLLAMA, create: () => new OllamaProvider(), selectionRules: [{ match: model => model.startsWith('ollama/') }] }, ]; return new ProviderRegistry(plugins); diff --git a/gateway/src/infrastructure/config/app-config.ts b/gateway/src/infrastructure/config/app-config.ts index f52f4f9..c256a1c 100644 --- a/gateway/src/infrastructure/config/app-config.ts +++ b/gateway/src/infrastructure/config/app-config.ts @@ -1,43 +1,43 @@ -/** - * Centralized application configuration - * All environment variables are validated and accessed through this class - */ - -export class AppConfig { - // Server configuration - readonly server = { - port: this.getNumber('PORT', 3001), - environment: this.getString('NODE_ENV', 'development'), - isDevelopment: this.getString('NODE_ENV', 'development') === 'development', - isProduction: this.getString('NODE_ENV', 'development') === 'production', - version: this.getOptionalString('npm_package_version') || 'dev', - }; - - // x402 Payment configuration - readonly x402 = { - enabled: this.has('PRIVATE_KEY'), - privateKey: this.getOptionalString('PRIVATE_KEY'), - baseUrl: this.getString('X402_BASE_URL', 'https://x402.ekailabs.xyz'), - - // Helper methods - get chatCompletionsUrl() { - return `${this.baseUrl}/v1/chat/completions`; - }, - get messagesUrl() { - return `${this.baseUrl}/v1/messages`; - }, - }; - - // Provider API Keys - readonly providers = { - anthropic: { - apiKey: this.getOptionalString('ANTHROPIC_API_KEY'), - enabled: this.has('ANTHROPIC_API_KEY'), - }, - openai: { - apiKey: this.getOptionalString('OPENAI_API_KEY'), - enabled: this.has('OPENAI_API_KEY'), - }, +/** + * Centralized application configuration + * All environment variables are validated and accessed through this class + */ + +export class AppConfig { + // Server configuration + readonly server = { + port: this.getNumber('PORT', 3001), + environment: this.getString('NODE_ENV', 'development'), + isDevelopment: this.getString('NODE_ENV', 'development') === 'development', + isProduction: this.getString('NODE_ENV', 'development') === 'production', + version: this.getOptionalString('npm_package_version') || 'dev', + }; + + // x402 Payment configuration + readonly x402 = { + enabled: this.has('PRIVATE_KEY'), + privateKey: this.getOptionalString('PRIVATE_KEY'), + baseUrl: this.getString('X402_BASE_URL', 'https://x402.ekailabs.xyz'), + + // Helper methods + get chatCompletionsUrl() { + return `${this.baseUrl}/v1/chat/completions`; + }, + get messagesUrl() { + return `${this.baseUrl}/v1/messages`; + }, + }; + + // Provider API Keys + readonly providers = { + anthropic: { + apiKey: this.getOptionalString('ANTHROPIC_API_KEY'), + enabled: this.has('ANTHROPIC_API_KEY'), + }, + openai: { + apiKey: this.getOptionalString('OPENAI_API_KEY'), + enabled: this.has('OPENAI_API_KEY'), + }, openrouter: { apiKey: this.getOptionalString('OPENROUTER_API_KEY'), enabled: this.has('OPENROUTER_API_KEY'), @@ -54,21 +54,26 @@ export class AppConfig { apiKey: this.getOptionalString('GOOGLE_API_KEY'), enabled: this.has('GOOGLE_API_KEY'), }, + ollama: { + baseUrl: this.getString('OLLAMA_BASE_URL', 'http://localhost:11434/v1'), + apiKey: this.getOptionalString('OLLAMA_API_KEY'), + enabled: this.has('OLLAMA_BASE_URL'), + }, + }; + + // Telemetry configuration + readonly telemetry = { + enabled: this.getBoolean('ENABLE_TELEMETRY', true), + endpoint: this.getOptionalString('TELEMETRY_ENDPOINT'), + }; + + // OpenRouter-specific configuration + readonly openrouter = { + skipPricingRefresh: this.getBoolean('SKIP_OPENROUTER_PRICING_REFRESH', false), + pricingTimeoutMs: this.getNumber('OPENROUTER_PRICING_TIMEOUT_MS', 4000), + pricingRetries: this.getNumber('OPENROUTER_PRICING_RETRIES', 2), }; - - // Telemetry configuration - readonly telemetry = { - enabled: this.getBoolean('ENABLE_TELEMETRY', true), - endpoint: this.getOptionalString('TELEMETRY_ENDPOINT'), - }; - - // OpenRouter-specific configuration - readonly openrouter = { - skipPricingRefresh: this.getBoolean('SKIP_OPENROUTER_PRICING_REFRESH', false), - pricingTimeoutMs: this.getNumber('OPENROUTER_PRICING_TIMEOUT_MS', 4000), - pricingRetries: this.getNumber('OPENROUTER_PRICING_RETRIES', 2), - }; - + // Feature flags readonly features = { usageTracking: this.getBoolean('ENABLE_USAGE_TRACKING', true), @@ -79,84 +84,84 @@ export class AppConfig { backend: this.getString('MEMORY_BACKEND', 'file'), maxItems: this.getNumber('MEMORY_MAX_ITEMS', 20), } as const; - - // Helper methods - private has(key: string): boolean { - return !!process.env[key]; - } - - private getString(key: string, defaultValue: string): string; - private getString(key: string): string; - private getString(key: string, defaultValue?: string): string { - const value = process.env[key] || defaultValue; - if (value === undefined) { - throw new Error(`Missing required environment variable: ${key}`); - } - return value; - } - - private getOptionalString(key: string): string | undefined { - return process.env[key]; - } - - private getNumber(key: string, defaultValue: number): number { - const value = process.env[key]; - if (!value) return defaultValue; - const num = parseInt(value, 10); - if (isNaN(num)) { - throw new Error(`Invalid number for environment variable ${key}: ${value}`); - } - return num; - } - - private getBoolean(key: string, defaultValue: boolean): boolean { - const value = process.env[key]; - if (!value) return defaultValue; - return value.toLowerCase() === 'true' || value === '1'; - } - - /** - * Validate that at least one authentication method is configured - */ - validate(): void { - const hasApiKeys = Object.values(this.providers).some(p => p.enabled); - const hasX402 = this.x402.enabled; - - if (!hasApiKeys && !hasX402) { - throw new Error( - 'No authentication configured. Set either:\n' + - ' 1. At least one provider API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n' + - ' 2. PRIVATE_KEY for x402 payment mode' - ); - } - } - - /** - * Get human-readable mode description - */ - getMode(): 'x402-only' | 'hybrid' | 'byok' { - const hasApiKeys = Object.values(this.providers).some(p => p.enabled); - const hasX402 = this.x402.enabled; - - if (!hasApiKeys && hasX402) return 'x402-only'; - if (hasApiKeys && hasX402) return 'hybrid'; - return 'byok'; - } -} - -// Singleton instance -let configInstance: AppConfig | null = null; - -export function getConfig(): AppConfig { - if (!configInstance) { - configInstance = new AppConfig(); - configInstance.validate(); - } - return configInstance; -} - -// For testing - reset config -export function resetConfig(): void { - configInstance = null; -} - + + // Helper methods + private has(key: string): boolean { + return !!process.env[key]; + } + + private getString(key: string, defaultValue: string): string; + private getString(key: string): string; + private getString(key: string, defaultValue?: string): string { + const value = process.env[key] || defaultValue; + if (value === undefined) { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; + } + + private getOptionalString(key: string): string | undefined { + return process.env[key]; + } + + private getNumber(key: string, defaultValue: number): number { + const value = process.env[key]; + if (!value) return defaultValue; + const num = parseInt(value, 10); + if (isNaN(num)) { + throw new Error(`Invalid number for environment variable ${key}: ${value}`); + } + return num; + } + + private getBoolean(key: string, defaultValue: boolean): boolean { + const value = process.env[key]; + if (!value) return defaultValue; + return value.toLowerCase() === 'true' || value === '1'; + } + + /** + * Validate that at least one authentication method is configured + */ + validate(): void { + const hasApiKeys = Object.values(this.providers).some(p => p.enabled); + const hasX402 = this.x402.enabled; + + if (!hasApiKeys && !hasX402) { + throw new Error( + 'No authentication configured. Set either:\n' + + ' 1. At least one provider API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n' + + ' 2. PRIVATE_KEY for x402 payment mode' + ); + } + } + + /** + * Get human-readable mode description + */ + getMode(): 'x402-only' | 'hybrid' | 'byok' { + const hasApiKeys = Object.values(this.providers).some(p => p.enabled); + const hasX402 = this.x402.enabled; + + if (!hasApiKeys && hasX402) return 'x402-only'; + if (hasApiKeys && hasX402) return 'hybrid'; + return 'byok'; + } +} + +// Singleton instance +let configInstance: AppConfig | null = null; + +export function getConfig(): AppConfig { + if (!configInstance) { + configInstance = new AppConfig(); + configInstance.validate(); + } + return configInstance; +} + +// For testing - reset config +export function resetConfig(): void { + configInstance = null; +} + diff --git a/model_catalog/chat_completions_providers_v1.json b/model_catalog/chat_completions_providers_v1.json index 8b260cf..b31f2fb 100644 --- a/model_catalog/chat_completions_providers_v1.json +++ b/model_catalog/chat_completions_providers_v1.json @@ -156,6 +156,44 @@ "format": "openai_chat" } } + }, + { + "provider": "ollama", + "models": [ + "llama3.3", + "llama3.2", + "llama3.1", + "llama3", + "gemma3", + "gemma2", + "qwen3", + "qwen2.5-coder", + "deepseek-r1", + "deepseek-coder-v2", + "phi4", + "phi3", + "mistral", + "mixtral", + "codellama", + "starcoder2" + ], + "chat_completions": { + "base_url": "http://localhost:11434/v1/chat/completions", + "auth": { + "env_var": "OLLAMA_API_KEY", + "header": "Authorization", + "scheme": "Bearer" + }, + "supported_client_formats": ["openai"], + "payload_defaults": { + "stream_options": { + "include_usage": true + } + }, + "usage": { + "format": "openai_chat" + } + } } ] } diff --git a/shared/types/types.ts b/shared/types/types.ts index 94985ff..c98df8f 100644 --- a/shared/types/types.ts +++ b/shared/types/types.ts @@ -31,7 +31,7 @@ export interface ChatCompletionResponse { } -export type ProviderName = 'openai' | 'openrouter' | 'anthropic'; +export type ProviderName = 'openai' | 'openrouter' | 'anthropic' | 'ollama'; // Removed conversation types - no conversation storage From 90be7c12ea651d3521e3d3d36ad8f72827ff05d3 Mon Sep 17 00:00:00 2001 From: shrijam12 Date: Mon, 9 Feb 2026 22:11:12 +0530 Subject: [PATCH 02/78] feat: Add Responses API support for Ollama - Added Ollama to responses_providers_v1.json catalog - Created OllamaResponsesPassthrough class implementing Responses API - Registered Ollama in responses-passthrough-registry.ts Ollama supports the OpenResponses API specification at /v1/responses endpoint, providing future-proof support as /chat/completions may be deprecated. --- .../ollama-responses-passthrough.ts | 284 ++++++++++++++++++ .../responses-passthrough-registry.ts | 2 + model_catalog/responses_providers_v1.json | 30 ++ 3 files changed, 316 insertions(+) create mode 100644 gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts diff --git a/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts b/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts new file mode 100644 index 0000000..6acf733 --- /dev/null +++ b/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts @@ -0,0 +1,284 @@ +import { Response as ExpressResponse } from 'express'; +import { logger } from '../utils/logger.js'; +import { AuthenticationError, ProviderError } from '../../shared/errors/index.js'; +import { CONTENT_TYPES } from '../../domain/types/provider.js'; +import { getConfig } from '../config/app-config.js'; +import { ResponsesPassthrough, ResponsesPassthroughConfig } from './responses-passthrough.js'; +import { injectMemoryContext, persistMemory } from '../memory/memory-helper.js'; + +export class OllamaResponsesPassthrough implements ResponsesPassthrough { + constructor(private readonly config: ResponsesPassthroughConfig) {} + + private get baseUrl(): string { + return this.config.baseUrl; + } + + private get apiKey(): string | undefined { + const envVar = this.config.auth?.envVar; + if (envVar) { + const token = process.env[envVar]; + if (token) return token; + } + + // Ollama doesn't require an API key by default (runs locally) + // Return undefined if no key is configured + return getConfig().providers.ollama.apiKey; + } + + private buildAuthHeader(): string | undefined { + const token = this.apiKey; + if (!token) return undefined; // Ollama doesn't require auth by default + + const { auth } = this.config; + if (!auth) { + return `Bearer ${token}`; + } + + if (auth.template) { + return auth.template.replace('{{token}}', token); + } + + if (auth.scheme) { + return `${auth.scheme} ${token}`.trim(); + } + + return token; + } + + private buildHeaders(): Record { + const headers: Record = { + 'Content-Type': 'application/json', + ...this.config.staticHeaders, + }; + + const authHeader = this.buildAuthHeader(); + if (authHeader) { + const headerName = this.config.auth?.header ?? 'Authorization'; + headers[headerName] = authHeader; + } + + return headers; + } + + // Store usage data for tracking + private usage: { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + } | null = null; + + // Buffer to handle multi-chunk SSE events + private eventBuffer: string = ''; + private assistantResponseBuffer: string = ''; + + private async makeRequest(body: any, stream: boolean): Promise { + const response = await fetch(this.baseUrl, { + method: 'POST', + headers: this.buildHeaders(), + body: JSON.stringify({ ...body, stream, store: false }) // Not storing responses + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new ProviderError('ollama', errorText || `HTTP ${response.status}`, response.status, { endpoint: this.baseUrl }); + } + + return response; + } + + private trackUsage(text: string, model: string, clientIp?: string): void { + try { + // Add to buffer to handle multi-chunk events + this.eventBuffer += text; + + // Extract assistant response content from text.delta events + const textDeltaMatch = /"type":"response\.text\.delta"[^}]*"text":"([^"]+)"/g; + let match; + while ((match = textDeltaMatch.exec(text)) !== null) { + this.assistantResponseBuffer += match[1]; + } + + // Look for the exact response.completed event + if (this.eventBuffer.includes('"type":"response.completed"')) { + + // Find the start of the JSON object + const startIndex = this.eventBuffer.indexOf('{"type":"response.completed"'); + if (startIndex === -1) return; + + // Find the end by counting braces + let braceCount = 0; + let endIndex = -1; + + for (let i = startIndex; i < this.eventBuffer.length; i++) { + if (this.eventBuffer[i] === '{') braceCount++; + if (this.eventBuffer[i] === '}') braceCount--; + + if (braceCount === 0) { + endIndex = i; + break; + } + } + + if (endIndex === -1) return; // Incomplete JSON, wait for more chunks + + // Extract the complete JSON + const jsonString = this.eventBuffer.substring(startIndex, endIndex + 1); + + logger.debug('JSON response found', { provider: 'ollama', operation: 'response_parsing', module: 'ollama-responses-passthrough' }); + + try { + const data = JSON.parse(jsonString); + logger.debug('Response parsed successfully', { provider: 'ollama', operation: 'usage_extraction', module: 'ollama-responses-passthrough' }); + + // Extract usage data from response.usage + if (data.response?.usage) { + const usage = data.response.usage; + const totalInputTokens = usage.input_tokens || 0; + const cachedTokens = usage.input_tokens_details?.cached_tokens || 0; + const nonCachedInputTokens = totalInputTokens - cachedTokens; + const outputTokens = usage.output_tokens || 0; + const totalTokens = usage.total_tokens || (totalInputTokens + outputTokens); + const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0; + + logger.debug('Usage tracking from response', { + provider: 'ollama', + model, + totalInputTokens, + nonCachedInputTokens, + cachedTokens, + outputTokens, + totalTokens, + reasoningTokens, + module: 'ollama-responses-passthrough' + }); + + import('../utils/usage-tracker.js').then(({ usageTracker }) => { + usageTracker.trackUsage( + model, + 'ollama', + nonCachedInputTokens, + outputTokens, + cachedTokens, + 0, // cache read tokens + clientIp + ); + }).catch((error) => { + logger.error('Usage tracking failed', error, { provider: 'ollama', operation: 'passthrough', module: 'ollama-responses-passthrough' }); + }); + } else { + logger.warn('No usage data in response', { provider: 'ollama', operation: 'passthrough', module: 'ollama-responses-passthrough' }); + } + } catch (parseError) { + logger.error('JSON parse error', parseError, { provider: 'ollama', operation: 'response_parsing', module: 'ollama-responses-passthrough' }); + } + + // Clear buffer after processing + this.eventBuffer = ''; + } + } catch (error) { + logger.error('Usage tracking failed', error, { provider: 'ollama', operation: 'passthrough', module: 'ollama-responses-passthrough' }); + } + } + + async handleDirectRequest(request: any, res: ExpressResponse, clientIp?: string): Promise { + // Reset usage tracking for new request + this.usage = null; + this.eventBuffer = ''; + this.assistantResponseBuffer = ''; + + injectMemoryContext(request, { + provider: this.config.provider, + defaultUserId: 'default', + extractCurrentUserInputs: req => extractResponsesUserInputs(req), + applyMemoryContext: (req, context) => { + if (req.instructions) { + req.instructions = `${context}\n\n---\n\n${req.instructions}`; + } else { + req.instructions = context; + } + } + }); + + if (request.stream) { + const response = await this.makeRequest(request, true); + + res.writeHead(200, { + 'Content-Type': CONTENT_TYPES.EVENT_STREAM, + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + }); + + // Manual stream processing for usage tracking + const reader = response.body!.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const text = new TextDecoder().decode(value); + setImmediate(() => this.trackUsage(text, request.model, clientIp)); + + res.write(value); + } + res.end(); + + persistMemory(request, this.assistantResponseBuffer, { + provider: this.config.provider, + defaultUserId: 'default', + extractUserContent: req => req.input || '', + metadataBuilder: req => ({ + model: req.model, + provider: this.config.provider, + }), + }); + } else { + const response = await this.makeRequest(request, false); + const json = await response.json(); + + // Track usage for non-streaming requests + if (json.usage) { + const totalInputTokens = json.usage.input_tokens || 0; + const cachedTokens = json.usage.input_tokens_details?.cached_tokens || 0; + const nonCachedInputTokens = totalInputTokens - cachedTokens; + const outputTokens = json.usage.output_tokens || 0; + const totalTokens = json.usage.total_tokens || (totalInputTokens + outputTokens); + const reasoningTokens = json.usage.output_tokens_details?.reasoning_tokens || 0; + + logger.debug('Tracking non-streaming usage', { + provider: 'ollama', + model: request.model, + totalInputTokens, + nonCachedInputTokens, + cachedTokens, + outputTokens, + totalTokens, + reasoningTokens, + module: 'ollama-responses-passthrough' + }); + + import('../utils/usage-tracker.js').then(({ usageTracker }) => { + usageTracker.trackUsage(request.model, 'ollama', nonCachedInputTokens, outputTokens, cachedTokens, 0, clientIp); + }).catch(() => {}); + } + + const assistantResponse = json?.output?.[0]?.content?.[0]?.text || ''; + persistMemory(request, assistantResponse, { + provider: this.config.provider, + defaultUserId: 'default', + extractUserContent: req => req.input || '', + metadataBuilder: req => ({ + model: req.model, + provider: this.config.provider, + }), + }); + + res.json(json); + } + } +} + +function extractResponsesUserInputs(request: any): string[] { + const content = (request.input || '').trim(); + return content ? [content] : []; +} diff --git a/gateway/src/infrastructure/passthrough/responses-passthrough-registry.ts b/gateway/src/infrastructure/passthrough/responses-passthrough-registry.ts index 6d38f0e..d67bc09 100644 --- a/gateway/src/infrastructure/passthrough/responses-passthrough-registry.ts +++ b/gateway/src/infrastructure/passthrough/responses-passthrough-registry.ts @@ -1,5 +1,6 @@ import { ResponsesPassthrough, ResponsesPassthroughConfig } from './responses-passthrough.js'; import { OpenAIResponsesPassthrough } from './openai-responses-passthrough.js'; +import { OllamaResponsesPassthrough } from './ollama-responses-passthrough.js'; import { loadResponsesProviderDefinitions, ResponsesProviderDefinition } from './responses-provider-config.js'; import { logger } from '../utils/logger.js'; @@ -10,6 +11,7 @@ interface ProviderEntry { const passthroughFactories: Record ResponsesPassthrough> = { openai: (config) => new OpenAIResponsesPassthrough(config), + ollama: (config) => new OllamaResponsesPassthrough(config), }; export class ResponsesPassthroughRegistry { diff --git a/model_catalog/responses_providers_v1.json b/model_catalog/responses_providers_v1.json index 71086a7..472f6b8 100644 --- a/model_catalog/responses_providers_v1.json +++ b/model_catalog/responses_providers_v1.json @@ -15,6 +15,36 @@ }, "supported_client_formats": ["openai_responses"] } + }, + { + "provider": "ollama", + "models": [ + "llama3.3", + "llama3.2", + "llama3.1", + "llama3", + "gemma3", + "gemma2", + "qwen3", + "qwen2.5-coder", + "deepseek-r1", + "deepseek-coder-v2", + "phi4", + "phi3", + "mistral", + "mixtral", + "codellama", + "starcoder2" + ], + "responses": { + "base_url": "http://localhost:11434/v1/responses", + "auth": { + "env_var": "OLLAMA_API_KEY", + "header": "Authorization", + "scheme": "Bearer" + }, + "supported_client_formats": ["openai_responses"] + } } ] } From b22d91d267ac12a3f09aa5f204f14e45019f6ece Mon Sep 17 00:00:00 2001 From: shrijam12 Date: Mon, 9 Feb 2026 22:32:29 +0530 Subject: [PATCH 03/78] refactor: Remove comments from Ollama provider files --- .../src/domain/providers/ollama-provider.ts | 9 -- .../ollama-responses-passthrough.ts | 97 ++++++------------- 2 files changed, 31 insertions(+), 75 deletions(-) diff --git a/gateway/src/domain/providers/ollama-provider.ts b/gateway/src/domain/providers/ollama-provider.ts index 23df2cb..ee43e6d 100644 --- a/gateway/src/domain/providers/ollama-provider.ts +++ b/gateway/src/domain/providers/ollama-provider.ts @@ -31,23 +31,14 @@ interface OllamaResponse { export class OllamaProvider extends BaseProvider { readonly name = 'ollama'; - // Ollama exposes an OpenAI-compatible API at /v1 protected get baseUrl(): string { return getConfig().providers.ollama.baseUrl; } - // Ollama doesn't require an API key by default, but we return a - // dummy value so BaseProvider.isConfigured() stays true when the - // base URL is set. Users can optionally supply a real key if they - // put Ollama behind an auth proxy. protected get apiKey(): string | undefined { return getConfig().providers.ollama.apiKey || 'ollama'; } - /** - * Ollama is considered configured when the user has explicitly - * enabled it by setting OLLAMA_BASE_URL (even without an API key). - */ isConfigured(): boolean { return getConfig().providers.ollama.enabled; } diff --git a/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts b/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts index 6acf733..a692b5d 100644 --- a/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts +++ b/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts @@ -1,6 +1,6 @@ import { Response as ExpressResponse } from 'express'; import { logger } from '../utils/logger.js'; -import { AuthenticationError, ProviderError } from '../../shared/errors/index.js'; +import { ProviderError } from '../../shared/errors/index.js'; import { CONTENT_TYPES } from '../../domain/types/provider.js'; import { getConfig } from '../config/app-config.js'; import { ResponsesPassthrough, ResponsesPassthroughConfig } from './responses-passthrough.js'; @@ -10,39 +10,31 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { constructor(private readonly config: ResponsesPassthroughConfig) {} private get baseUrl(): string { - return this.config.baseUrl; - } - - private get apiKey(): string | undefined { - const envVar = this.config.auth?.envVar; - if (envVar) { - const token = process.env[envVar]; - if (token) return token; + if (this.config.baseUrl) { + return this.config.baseUrl; } - - // Ollama doesn't require an API key by default (runs locally) - // Return undefined if no key is configured - return getConfig().providers.ollama.apiKey; + const configBaseUrl = getConfig().providers.ollama.baseUrl; + return configBaseUrl.replace(/\/v1\/?$/, '/v1/responses'); } - private buildAuthHeader(): string | undefined { - const token = this.apiKey; - if (!token) return undefined; // Ollama doesn't require auth by default - + private buildAuthHeader(): string { const { auth } = this.config; if (!auth) { - return `Bearer ${token}`; - } - - if (auth.template) { - return auth.template.replace('{{token}}', token); + return ''; } - if (auth.scheme) { - return `${auth.scheme} ${token}`.trim(); + const envVar = auth.envVar; + if (envVar) { + const token = process.env[envVar]; + if (token) { + if (auth.scheme) { + return `${auth.scheme} ${token}`.trim(); + } + return token; + } } - return token; + return ''; } private buildHeaders(): Record { @@ -51,23 +43,20 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { ...this.config.staticHeaders, }; + const headerName = this.config.auth?.header ?? 'Authorization'; const authHeader = this.buildAuthHeader(); if (authHeader) { - const headerName = this.config.auth?.header ?? 'Authorization'; headers[headerName] = authHeader; } - return headers; } - // Store usage data for tracking private usage: { inputTokens?: number; outputTokens?: number; totalTokens?: number; } | null = null; - // Buffer to handle multi-chunk SSE events private eventBuffer: string = ''; private assistantResponseBuffer: string = ''; @@ -75,7 +64,7 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { const response = await fetch(this.baseUrl, { method: 'POST', headers: this.buildHeaders(), - body: JSON.stringify({ ...body, stream, store: false }) // Not storing responses + body: JSON.stringify({ ...body, stream, store: false }) }); if (!response.ok) { @@ -88,24 +77,18 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { private trackUsage(text: string, model: string, clientIp?: string): void { try { - // Add to buffer to handle multi-chunk events this.eventBuffer += text; - // Extract assistant response content from text.delta events const textDeltaMatch = /"type":"response\.text\.delta"[^}]*"text":"([^"]+)"/g; let match; while ((match = textDeltaMatch.exec(text)) !== null) { this.assistantResponseBuffer += match[1]; } - // Look for the exact response.completed event if (this.eventBuffer.includes('"type":"response.completed"')) { - - // Find the start of the JSON object const startIndex = this.eventBuffer.indexOf('{"type":"response.completed"'); if (startIndex === -1) return; - // Find the end by counting braces let braceCount = 0; let endIndex = -1; @@ -119,9 +102,8 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { } } - if (endIndex === -1) return; // Incomplete JSON, wait for more chunks + if (endIndex === -1) return; - // Extract the complete JSON const jsonString = this.eventBuffer.substring(startIndex, endIndex + 1); logger.debug('JSON response found', { provider: 'ollama', operation: 'response_parsing', module: 'ollama-responses-passthrough' }); @@ -130,25 +112,18 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { const data = JSON.parse(jsonString); logger.debug('Response parsed successfully', { provider: 'ollama', operation: 'usage_extraction', module: 'ollama-responses-passthrough' }); - // Extract usage data from response.usage if (data.response?.usage) { const usage = data.response.usage; - const totalInputTokens = usage.input_tokens || 0; - const cachedTokens = usage.input_tokens_details?.cached_tokens || 0; - const nonCachedInputTokens = totalInputTokens - cachedTokens; + const inputTokens = usage.input_tokens || 0; const outputTokens = usage.output_tokens || 0; - const totalTokens = usage.total_tokens || (totalInputTokens + outputTokens); - const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0; + const totalTokens = usage.total_tokens || (inputTokens + outputTokens); logger.debug('Usage tracking from response', { provider: 'ollama', model, - totalInputTokens, - nonCachedInputTokens, - cachedTokens, + inputTokens, outputTokens, totalTokens, - reasoningTokens, module: 'ollama-responses-passthrough' }); @@ -156,10 +131,10 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { usageTracker.trackUsage( model, 'ollama', - nonCachedInputTokens, + inputTokens, outputTokens, - cachedTokens, - 0, // cache read tokens + 0, + 0, clientIp ); }).catch((error) => { @@ -172,7 +147,6 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { logger.error('JSON parse error', parseError, { provider: 'ollama', operation: 'response_parsing', module: 'ollama-responses-passthrough' }); } - // Clear buffer after processing this.eventBuffer = ''; } } catch (error) { @@ -181,7 +155,6 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { } async handleDirectRequest(request: any, res: ExpressResponse, clientIp?: string): Promise { - // Reset usage tracking for new request this.usage = null; this.eventBuffer = ''; this.assistantResponseBuffer = ''; @@ -209,7 +182,6 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { 'Access-Control-Allow-Origin': '*', }); - // Manual stream processing for usage tracking const reader = response.body!.getReader(); while (true) { @@ -236,33 +208,26 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { const response = await this.makeRequest(request, false); const json = await response.json(); - // Track usage for non-streaming requests if (json.usage) { - const totalInputTokens = json.usage.input_tokens || 0; - const cachedTokens = json.usage.input_tokens_details?.cached_tokens || 0; - const nonCachedInputTokens = totalInputTokens - cachedTokens; + const inputTokens = json.usage.input_tokens || 0; const outputTokens = json.usage.output_tokens || 0; - const totalTokens = json.usage.total_tokens || (totalInputTokens + outputTokens); - const reasoningTokens = json.usage.output_tokens_details?.reasoning_tokens || 0; + const totalTokens = json.usage.total_tokens || (inputTokens + outputTokens); logger.debug('Tracking non-streaming usage', { provider: 'ollama', model: request.model, - totalInputTokens, - nonCachedInputTokens, - cachedTokens, + inputTokens, outputTokens, totalTokens, - reasoningTokens, module: 'ollama-responses-passthrough' }); import('../utils/usage-tracker.js').then(({ usageTracker }) => { - usageTracker.trackUsage(request.model, 'ollama', nonCachedInputTokens, outputTokens, cachedTokens, 0, clientIp); + usageTracker.trackUsage(request.model, 'ollama', inputTokens, outputTokens, 0, 0, clientIp); }).catch(() => {}); } - const assistantResponse = json?.output?.[0]?.content?.[0]?.text || ''; + const assistantResponse = json?.output?.[0]?.content?.[0]?.text || json?.output_text || ''; persistMemory(request, assistantResponse, { provider: this.config.provider, defaultUserId: 'default', From ac9c09223bf64b5943b6c9a2ece351bb14869694 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:44:44 +0700 Subject: [PATCH 04/78] fix the broken link --- docs/ROFL_DEPLOYMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ROFL_DEPLOYMENT.md b/docs/ROFL_DEPLOYMENT.md index 33b151c..d3d8757 100644 --- a/docs/ROFL_DEPLOYMENT.md +++ b/docs/ROFL_DEPLOYMENT.md @@ -135,5 +135,5 @@ export OPENAI_API_KEY="not-needed" # Keys are in TEE ## Resources - [Oasis ROFL Docs](https://docs.oasis.io/rofl/) -- [Oasis CLI Reference](https://docs.oasis.io/cli/) +- [Oasis CLI Reference](https://cli.oasis.io) - [Testnet Faucet](https://faucet.testnet.oasis.io/) From 6e82165a76c7676009cdbb7dfb68599e1cba8505 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:04:50 +0700 Subject: [PATCH 05/78] open router integration simple version --- integrations/openrouter/.env.example | 8 +++ integrations/openrouter/package.json | 21 ++++++ integrations/openrouter/src/config.ts | 10 +++ integrations/openrouter/src/memory.ts | 100 ++++++++++++++++++++++++++ integrations/openrouter/src/proxy.ts | 53 ++++++++++++++ integrations/openrouter/src/server.ts | 52 ++++++++++++++ integrations/openrouter/tsconfig.json | 21 ++++++ package-lock.json | 20 +++++- package.json | 6 +- 9 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 integrations/openrouter/.env.example create mode 100644 integrations/openrouter/package.json create mode 100644 integrations/openrouter/src/config.ts create mode 100644 integrations/openrouter/src/memory.ts create mode 100644 integrations/openrouter/src/proxy.ts create mode 100644 integrations/openrouter/src/server.ts create mode 100644 integrations/openrouter/tsconfig.json diff --git a/integrations/openrouter/.env.example b/integrations/openrouter/.env.example new file mode 100644 index 0000000..cc18dd5 --- /dev/null +++ b/integrations/openrouter/.env.example @@ -0,0 +1,8 @@ +# Required — your OpenRouter API key +OPENROUTER_API_KEY=your_key_here + +# Memory service URL (optional, defaults to http://localhost:4005) +# MEMORY_URL=http://localhost:4005 + +# Server port (optional, defaults to 4010) +# PORT=4010 diff --git a/integrations/openrouter/package.json b/integrations/openrouter/package.json new file mode 100644 index 0000000..05d6061 --- /dev/null +++ b/integrations/openrouter/package.json @@ -0,0 +1,21 @@ +{ + "name": "@ekai/openrouter", + "version": "0.1.0", + "type": "module", + "private": true, + "main": "dist/server.js", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js" + }, + "dependencies": { + "express": "^4.18.2", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "tsx": "^4.7.0", + "typescript": "^5.3.3" + } +} diff --git a/integrations/openrouter/src/config.ts b/integrations/openrouter/src/config.ts new file mode 100644 index 0000000..0a02b16 --- /dev/null +++ b/integrations/openrouter/src/config.ts @@ -0,0 +1,10 @@ +import 'dotenv/config'; + +export const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; +if (!OPENROUTER_API_KEY) { + console.error('OPENROUTER_API_KEY is required'); + process.exit(1); +} + +export const MEMORY_URL = process.env.MEMORY_URL || 'http://localhost:4005'; +export const PORT = parseInt(process.env.PORT || '4010', 10); diff --git a/integrations/openrouter/src/memory.ts b/integrations/openrouter/src/memory.ts new file mode 100644 index 0000000..38c2e51 --- /dev/null +++ b/integrations/openrouter/src/memory.ts @@ -0,0 +1,100 @@ +import { MEMORY_URL } from './config.js'; + +interface QueryResult { + sector: 'episodic' | 'semantic' | 'procedural'; + content: string; + score: number; + details?: { + // semantic + subject?: string; + predicate?: string; + object?: string; + // procedural + trigger?: string; + steps?: string[]; + goal?: string; + }; +} + +interface SearchResponse { + workingMemory: QueryResult[]; + perSector: Record; + profileId: string; +} + +/** + * Fetch memory context from the memory service. + * Returns null on any failure — memory is additive, never blocking. + */ +export async function fetchMemoryContext( + query: string, + profile: string, +): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 3000); + + const res = await fetch(`${MEMORY_URL}/v1/search`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, profile }), + signal: controller.signal, + }); + clearTimeout(timeout); + + if (!res.ok) { + console.warn(`[memory] search returned ${res.status}`); + return null; + } + + const data = (await res.json()) as SearchResponse; + return data.workingMemory?.length ? data.workingMemory : null; + } catch (err: any) { + console.warn(`[memory] search failed: ${err.message}`); + return null; + } +} + +/** + * Format memory results into a system message block, grouped by sector. + */ +export function formatMemoryBlock(results: QueryResult[]): string { + const facts: string[] = []; + const events: string[] = []; + const procedures: string[] = []; + + for (const r of results) { + if (r.sector === 'semantic' && r.details?.subject) { + facts.push(`- ${r.details.subject} ${r.details.predicate} ${r.details.object}`); + } else if (r.sector === 'procedural' && r.details?.trigger) { + const steps = r.details.steps?.join(' → ') || r.content; + procedures.push(`- When ${r.details.trigger}: ${steps}`); + } else { + events.push(`- ${r.content}`); + } + } + + const sections: string[] = []; + if (facts.length) sections.push(`Facts:\n${facts.join('\n')}`); + if (events.length) sections.push(`Events:\n${events.join('\n')}`); + if (procedures.length) sections.push(`Procedures:\n${procedures.join('\n')}`); + + return `\n[Recalled context for this conversation. Use naturally if relevant, ignore if not.]\n\n${sections.join('\n\n')}\n`; +} + +/** + * Inject a memory block into the messages array. + * If messages[0] is a system message, prepend memory before existing content. + * Otherwise, insert a new system message at index 0. + * Memory goes BEFORE developer instructions so the developer prompt takes priority. + */ +export function injectMemory( + messages: Array<{ role: string; content: string }>, + memoryBlock: string, +): void { + if (messages[0]?.role === 'system') { + messages[0].content = memoryBlock + '\n\n' + messages[0].content; + } else { + messages.unshift({ role: 'system', content: memoryBlock }); + } +} diff --git a/integrations/openrouter/src/proxy.ts b/integrations/openrouter/src/proxy.ts new file mode 100644 index 0000000..6e0c3aa --- /dev/null +++ b/integrations/openrouter/src/proxy.ts @@ -0,0 +1,53 @@ +import type { Response } from 'express'; +import { OPENROUTER_API_KEY } from './config.js'; + +const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions'; + +/** + * Proxy a chat completions request to OpenRouter. + * Handles both streaming (SSE) and non-streaming responses. + */ +export async function proxyToOpenRouter(body: any, res: Response): Promise { + const response = await fetch(OPENROUTER_URL, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, + 'Content-Type': 'application/json', + 'HTTP-Referer': 'https://ekailabs.xyz', + 'X-Title': 'Ekai Gateway', + }, + body: JSON.stringify(body), + }); + + if (!response.ok || !body.stream) { + // Non-streaming: forward status + JSON body + const data = await response.text(); + res.status(response.status).set('Content-Type', 'application/json').send(data); + return; + } + + // Streaming: pipe SSE chunks + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + + const reader = response.body?.getReader(); + if (!reader) { + res.end(); + return; + } + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + res.write(value); + } + } catch (err: any) { + console.error(`[proxy] stream error: ${err.message}`); + } finally { + res.end(); + } +} diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts new file mode 100644 index 0000000..81aaec5 --- /dev/null +++ b/integrations/openrouter/src/server.ts @@ -0,0 +1,52 @@ +import express from 'express'; +import { PORT } from './config.js'; +import { fetchMemoryContext, formatMemoryBlock, injectMemory } from './memory.js'; +import { proxyToOpenRouter } from './proxy.js'; + +const app = express(); +app.use(express.json({ limit: '10mb' })); + +app.get('/health', (_req, res) => { + res.json({ status: 'ok' }); +}); + +app.post('/v1/chat/completions', async (req, res) => { + try { + const body = req.body; + const profile = (req.headers['x-memory-profile'] as string) || 'default'; + + // Extract last user message for memory query + const lastUserMsg = [...(body.messages || [])] + .reverse() + .find((m: any) => m.role === 'user'); + const query = + typeof lastUserMsg?.content === 'string' + ? lastUserMsg.content + : Array.isArray(lastUserMsg?.content) + ? lastUserMsg.content + .filter((p: any) => p.type === 'text') + .map((p: any) => p.text) + .join(' ') + : null; + + // Fetch memory context (non-blocking on failure) + if (query) { + const results = await fetchMemoryContext(query, profile); + if (results) { + const block = formatMemoryBlock(results); + injectMemory(body.messages, block); + } + } + + await proxyToOpenRouter(body, res); + } catch (err: any) { + console.error(`[server] unhandled error: ${err.message}`); + if (!res.headersSent) { + res.status(500).json({ error: 'Internal server error' }); + } + } +}); + +app.listen(PORT, () => { + console.log(`@ekai/openrouter listening on port ${PORT}`); +}); diff --git a/integrations/openrouter/tsconfig.json b/integrations/openrouter/tsconfig.json new file mode 100644 index 0000000..b3bdbbc --- /dev/null +++ b/integrations/openrouter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "lib": ["es2021"], + "strict": false, + "skipLibCheck": true, + "noEmitOnError": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/package-lock.json b/package-lock.json index 6b58347..61ad62c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "workspaces": [ "gateway", "ui/dashboard", - "memory" + "memory", + "integrations/openrouter" ], "devDependencies": { "concurrently": "^8.2.2" @@ -69,6 +70,19 @@ "zod": "^3.24.2" } }, + "integrations/openrouter": { + "name": "@ekai/openrouter", + "version": "0.1.0", + "dependencies": { + "dotenv": "^16.3.1", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "tsx": "^4.7.0", + "typescript": "^5.3.3" + } + }, "memory": { "version": "0.1.0", "dependencies": { @@ -201,6 +215,10 @@ "dev": true, "license": "MIT" }, + "node_modules/@ekai/openrouter": { + "resolved": "integrations/openrouter", + "link": true + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", diff --git a/package.json b/package.json index 0725d68..836035a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-beta.1", "description": "Ekai Gateway - Multi-provider AI request routing with dashboard", "private": true, - "workspaces": ["gateway", "ui/dashboard", "memory"], + "workspaces": ["gateway", "ui/dashboard", "memory", "integrations/openrouter"], "scripts": { "dev": "concurrently \"npm run dev --workspace=gateway\" \"npm run dev --workspace=ui/dashboard\"", "dev:core": "concurrently \"npm run dev --workspace=gateway\" \"npm run dev --workspace=ui/dashboard\"", @@ -16,7 +16,9 @@ "dev:ui": "npm run dev --workspace=ui/dashboard", "start:gateway": "npm run start --workspace=gateway", "start:ui": "npx --workspace=ui/dashboard next start -p 3000 -H 0.0.0.0", - "start:memory": "npm run start --workspace=memory" + "start:memory": "npm run start --workspace=memory", + "dev:openrouter": "npm run dev --workspace=@ekai/openrouter", + "dev:memory+openrouter": "concurrently \"npm run start --workspace=memory\" \"npm run dev --workspace=@ekai/openrouter\"" }, "devDependencies": { "concurrently": "^8.2.2" From 39e0b3f95622b6927b82dd24b9bde01dcc2e038e Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:24:57 +0700 Subject: [PATCH 06/78] save realtime memory --- docs/architecture-overview.md | 10 +++ docs/intro.md | 5 +- integrations/openrouter/package.json | 7 +- integrations/openrouter/src/memory.test.ts | 96 ++++++++++++++++++++++ integrations/openrouter/src/memory.ts | 17 ++++ integrations/openrouter/src/server.ts | 8 +- package-lock.json | 3 +- 7 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 integrations/openrouter/src/memory.test.ts diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 3d66567..d87f9c4 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -31,6 +31,16 @@ Returns payment parameters, settles fees on-chain to the x402 facilitator, and f A web dashboard that visualizes request volume, token usage, and cost per provider.\ It connects to the SQLite database populated by the gateway. +### Memory Service (port 4005) + +Provides persistent memory across conversations.\ +When a request arrives at the OpenRouter proxy, two things happen in parallel: + +1. **Recall** — The last user message is sent to `/v1/search`. Matching memories are injected into the system prompt so the LLM can reference prior context. +2. **Ingestion** — The original conversation messages (before memory injection) are sent fire-and-forget to `/v1/ingest` so they become available for future recall. + +Memory is additive and never blocking: if the service is unreachable, requests proceed normally without recall or ingestion. + ### Storage Usage data and request logs are stored in a local SQLite database (by default `data/usage.db`).\ diff --git a/docs/intro.md b/docs/intro.md index d824d3e..21c9731 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -57,8 +57,9 @@ By decoupling your context from any single provider or interface, the Gateway ma - **OpenAI and Anthropic Compatibility:** Works with standard API formats used by most clients - **Self-Hosted Control:** Run locally or in your own environment; no external dependencies - **Usage Analytics:** Built-in dashboard for tracking tokens, requests, and costs -- **Context Portability:** Maintain continuity when switching models or interfaces -- **Cost-Optimized Routing:** Automatically select the most efficient provider for each model +- **Context Portability:** Maintain continuity when switching models or interfaces +- **Memory Loop:** Conversations are automatically ingested and recalled across sessions — no client changes needed +- **Cost-Optimized Routing:** Automatically select the most efficient provider for each model - **Transparent Billing:** Use your own API keys for full visibility and ownership diff --git a/integrations/openrouter/package.json b/integrations/openrouter/package.json index 05d6061..8ce94ad 100644 --- a/integrations/openrouter/package.json +++ b/integrations/openrouter/package.json @@ -10,12 +10,13 @@ "start": "node dist/server.js" }, "dependencies": { - "express": "^4.18.2", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "express": "^4.18.2" }, "devDependencies": { "@types/express": "^4.17.21", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^1.6.1" } } diff --git a/integrations/openrouter/src/memory.test.ts b/integrations/openrouter/src/memory.test.ts new file mode 100644 index 0000000..e2e170b --- /dev/null +++ b/integrations/openrouter/src/memory.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock config before importing memory module +vi.mock('./config.js', () => ({ + MEMORY_URL: 'http://localhost:4005', +})); + +import { fetchMemoryContext, formatMemoryBlock, ingestMessages, injectMemory } from './memory.js'; + +describe('ingestMessages', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()); + vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('POSTs messages and profile to /v1/ingest', async () => { + const mockFetch = vi.mocked(fetch); + mockFetch.mockResolvedValueOnce(new Response('ok', { status: 200 })); + + const messages = [ + { role: 'user', content: 'My dog is named Luna' }, + ]; + + ingestMessages(messages, 'test-profile'); + + // Let the fire-and-forget promise settle + await vi.waitFor(() => { + expect(mockFetch).toHaveBeenCalledOnce(); + }); + + const [url, options] = mockFetch.mock.calls[0]; + expect(url).toBe('http://localhost:4005/v1/ingest'); + expect(options).toMatchObject({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + + const body = JSON.parse(options!.body as string); + expect(body.messages).toEqual(messages); + expect(body.profile).toBe('test-profile'); + }); + + it('logs and swallows fetch errors', async () => { + const mockFetch = vi.mocked(fetch); + mockFetch.mockRejectedValueOnce(new Error('connection refused')); + + ingestMessages([{ role: 'user', content: 'hello' }], 'default'); + + await vi.waitFor(() => { + expect(console.warn).toHaveBeenCalledWith( + '[memory] ingest failed: connection refused', + ); + }); + }); +}); + +describe('injectMemory preserves original messages', () => { + it('mutates the messages array in place', () => { + const messages = [ + { role: 'user', content: 'hello' }, + ]; + const original = messages.map((m) => ({ ...m })); + + injectMemory(messages, 'test'); + + // Original copy is untouched + expect(original[0].content).toBe('hello'); + expect(original).toHaveLength(1); + + // messages array was mutated + expect(messages[0].role).toBe('system'); + expect(messages[0].content).toBe('test'); + expect(messages).toHaveLength(2); + }); + + it('prepends to existing system message', () => { + const messages = [ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: 'hi' }, + ]; + const original = messages.map((m) => ({ ...m })); + + injectMemory(messages, 'facts'); + + // Original copy is untouched + expect(original[0].content).toBe('You are helpful.'); + + // messages[0] was mutated + expect(messages[0].content).toBe('facts\n\nYou are helpful.'); + expect(messages).toHaveLength(2); + }); +}); diff --git a/integrations/openrouter/src/memory.ts b/integrations/openrouter/src/memory.ts index 38c2e51..de7b359 100644 --- a/integrations/openrouter/src/memory.ts +++ b/integrations/openrouter/src/memory.ts @@ -98,3 +98,20 @@ export function injectMemory( messages.unshift({ role: 'system', content: memoryBlock }); } } + +/** + * Fire-and-forget: send messages to the memory service for ingestion. + * Never awaited — failures are logged and swallowed. + */ +export function ingestMessages( + messages: Array<{ role: string; content: string }>, + profile: string, +): void { + fetch(`${MEMORY_URL}/v1/ingest`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages, profile }), + }).catch((err) => { + console.warn(`[memory] ingest failed: ${err.message}`); + }); +} diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts index 81aaec5..5c45dec 100644 --- a/integrations/openrouter/src/server.ts +++ b/integrations/openrouter/src/server.ts @@ -1,6 +1,6 @@ import express from 'express'; import { PORT } from './config.js'; -import { fetchMemoryContext, formatMemoryBlock, injectMemory } from './memory.js'; +import { fetchMemoryContext, formatMemoryBlock, ingestMessages, injectMemory } from './memory.js'; import { proxyToOpenRouter } from './proxy.js'; const app = express(); @@ -29,6 +29,9 @@ app.post('/v1/chat/completions', async (req, res) => { .join(' ') : null; + // Save original messages before memory injection mutates them + const originalMessages = body.messages.map((m: any) => ({ ...m })); + // Fetch memory context (non-blocking on failure) if (query) { const results = await fetchMemoryContext(query, profile); @@ -38,6 +41,9 @@ app.post('/v1/chat/completions', async (req, res) => { } } + // Fire-and-forget: ingest original messages for future recall + ingestMessages(originalMessages, profile); + await proxyToOpenRouter(body, res); } catch (err: any) { console.error(`[server] unhandled error: ${err.message}`); diff --git a/package-lock.json b/package-lock.json index 61ad62c..1066667 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,7 +80,8 @@ "devDependencies": { "@types/express": "^4.17.21", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^1.6.1" } }, "memory": { From 32ac4800989d5a47ac15d7daabeefcbf08d480e7 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:39:27 +0700 Subject: [PATCH 07/78] npm start only services you need --- .env.example | 6 ++++++ README.md | 45 ++++++++++++++++++++++++--------------------- package.json | 11 ++++------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/.env.example b/.env.example index 49d1c29..8a4ef54 100644 --- a/.env.example +++ b/.env.example @@ -20,3 +20,9 @@ PRIVATE_KEY= # Memory service controls (file-based FIFO backend by default) # MEMORY_BACKEND=file # set to "none" to disable # MEMORY_MAX_ITEMS=20 + +# Service toggles (all enabled by default, set to "false" to disable) +# ENABLE_GATEWAY=true +# ENABLE_DASHBOARD=true +# ENABLE_MEMORY=true +# ENABLE_OPENROUTER=true diff --git a/README.md b/README.md index baa9f2d..dd4a729 100644 --- a/README.md +++ b/README.md @@ -124,11 +124,14 @@ codex ``` ekai-gateway/ -├── gateway/ # Backend API and routing -├── ui/ # Dashboard frontend -├── memory/ # Agent memory service -├── shared/ # Shared types and utilities -└── package.json # Root package configuration +├── gateway/ # Backend API and routing +├── ui/dashboard/ # Dashboard frontend (Next.js) +├── memory/ # Agent memory service +├── integrations/ +│ └── openrouter/ # OpenRouter integration service +├── scripts/ +│ └── launcher.js # Unified service launcher +└── package.json # Root workspace configuration ``` ## API Endpoints @@ -190,32 +193,32 @@ The proxy uses **cost-based optimization** to automatically select the cheapest **Multi-client proxy**: Web apps, mobile apps, and scripts share conversations across providers with automatic cost tracking and optimization. -## Production Commands +## Running Services -```bash -npm run build # Build TypeScript for production -npm start # Start gateway, dashboard, and memory service -``` +A unified launcher starts all 4 services by default (gateway, dashboard, memory, openrouter). Disable any service with an env var: -**Individual services (ports configurable via `PORT` and `MEMORY_PORT` in `.env`):** ```bash -npm run start:gateway # Gateway API only (default: 3001) -npm run start:ui # Dashboard UI only (default: 3000) -npm run start:memory # Memory service only (default: 4005) +npm run dev # Development mode — all services with hot-reload +npm start # Production mode — all services from built output ``` -## Development +**Disable individual services** by setting `ENABLE_=false`: ```bash -npm run dev # Start gateway and dashboard in development mode -npm run dev:all # Start gateway, dashboard, and memory service +ENABLE_DASHBOARD=false npm run dev # Skip the dashboard +ENABLE_OPENROUTER=false npm start # Production without openrouter +ENABLE_MEMORY=false ENABLE_DASHBOARD=false npm run dev # Gateway + openrouter only ``` -**Individual services:** +**Individual service scripts** (escape hatches): + ```bash -cd gateway && npm run dev # Gateway only (port 3001) -cd ui/dashboard && npm run dev # Dashboard only (port 3000) -cd memory && npm start # Memory service only (port 4005) +npm run dev:gateway # Gateway only (port 3001) +npm run dev:ui # Dashboard only (port 3000) +npm run dev:openrouter # OpenRouter integration only (port 4006) +npm run start:gateway # Production gateway +npm run start:ui # Production dashboard +npm run start:memory # Memory service (port 4005) ``` ## Contributing diff --git a/package.json b/package.json index 836035a..591ad12 100644 --- a/package.json +++ b/package.json @@ -5,20 +5,17 @@ "private": true, "workspaces": ["gateway", "ui/dashboard", "memory", "integrations/openrouter"], "scripts": { - "dev": "concurrently \"npm run dev --workspace=gateway\" \"npm run dev --workspace=ui/dashboard\"", - "dev:core": "concurrently \"npm run dev --workspace=gateway\" \"npm run dev --workspace=ui/dashboard\"", - "dev:all": "concurrently \"npm run dev --workspace=gateway\" \"npm run dev --workspace=ui/dashboard\" \"npm run start --workspace=memory\"", + "dev": "node scripts/launcher.js --mode dev", + "start": "node scripts/launcher.js --mode start", "build": "npm install --workspaces && npm run build --workspaces --if-present", - "start": "concurrently \"npm run start:gateway\" \"npm run start:ui\" \"npm run start:memory\"", "clean": "npm run clean --workspaces --if-present && rm -rf node_modules", "test": "npm run test --workspace=gateway", "dev:gateway": "npm run dev --workspace=gateway", "dev:ui": "npm run dev --workspace=ui/dashboard", + "dev:openrouter": "npm run dev --workspace=@ekai/openrouter", "start:gateway": "npm run start --workspace=gateway", "start:ui": "npx --workspace=ui/dashboard next start -p 3000 -H 0.0.0.0", - "start:memory": "npm run start --workspace=memory", - "dev:openrouter": "npm run dev --workspace=@ekai/openrouter", - "dev:memory+openrouter": "concurrently \"npm run start --workspace=memory\" \"npm run dev --workspace=@ekai/openrouter\"" + "start:memory": "npm run start --workspace=memory" }, "devDependencies": { "concurrently": "^8.2.2" From 3558a8a1b60a28f424b5c2117dbb85b27f7a4936 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:10:22 +0700 Subject: [PATCH 08/78] updated build/run scripts --- .env.example | 7 +++++-- integrations/openrouter/src/config.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 8a4ef54..d19e4eb 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,11 @@ GOOGLE_API_KEY=your_key_here # Ollama (local models — no API key needed, just set the base URL) # OLLAMA_BASE_URL=http://localhost:11434/v1 # OLLAMA_API_KEY= # optional, only if behind an auth proxy -PORT=3001 -MEMORY_PORT=4005 +# Service ports +PORT=3001 # Gateway +# DASHBOARD_PORT=3000 # Dashboard +MEMORY_PORT=4005 # Memory +# OPENROUTER_PORT=4010 # OpenRouter integration # Optional x402 passthrough for OpenRouter access X402_BASE_URL=x402_supported_provider_url PRIVATE_KEY= diff --git a/integrations/openrouter/src/config.ts b/integrations/openrouter/src/config.ts index 0a02b16..bc0b546 100644 --- a/integrations/openrouter/src/config.ts +++ b/integrations/openrouter/src/config.ts @@ -7,4 +7,4 @@ if (!OPENROUTER_API_KEY) { } export const MEMORY_URL = process.env.MEMORY_URL || 'http://localhost:4005'; -export const PORT = parseInt(process.env.PORT || '4010', 10); +export const PORT = parseInt(process.env.OPENROUTER_PORT || '4010', 10); From 886b3edd3f3a1c4166229475be26b933a12f7395 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 15 Feb 2026 03:00:27 +0700 Subject: [PATCH 09/78] updated env file --- integrations/openrouter/src/config.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/integrations/openrouter/src/config.ts b/integrations/openrouter/src/config.ts index bc0b546..9ea3c51 100644 --- a/integrations/openrouter/src/config.ts +++ b/integrations/openrouter/src/config.ts @@ -1,4 +1,9 @@ -import 'dotenv/config'; +import dotenv from 'dotenv'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +dotenv.config({ path: resolve(__dirname, '../../../.env') }); export const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; if (!OPENROUTER_API_KEY) { From 65bdcdc40a8b1464dab6e5f8a1114d6935927e4b Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 15 Feb 2026 03:03:30 +0700 Subject: [PATCH 10/78] fixed config --- integrations/openrouter/src/config.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/integrations/openrouter/src/config.ts b/integrations/openrouter/src/config.ts index 9ea3c51..7676569 100644 --- a/integrations/openrouter/src/config.ts +++ b/integrations/openrouter/src/config.ts @@ -1,9 +1,20 @@ import dotenv from 'dotenv'; -import { resolve, dirname } from 'path'; +import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; +import { existsSync } from 'fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); -dotenv.config({ path: resolve(__dirname, '../../../.env') }); + +function findProjectRoot(startPath: string): string { + let cur = startPath; + while (cur !== dirname(cur)) { + if (existsSync(join(cur, '.env.example')) && existsSync(join(cur, 'package.json'))) return cur; + cur = dirname(cur); + } + return process.cwd(); +} + +dotenv.config({ path: join(findProjectRoot(__dirname), '.env') }); export const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; if (!OPENROUTER_API_KEY) { From c325ccf2250c28ec819d03b9ea8cfc08b62cd5e6 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 15 Feb 2026 03:07:22 +0700 Subject: [PATCH 11/78] add scripts/launcher.js (was untracked due to *.js gitignore) Co-Authored-By: Claude Opus 4.6 --- scripts/launcher.js | 92 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 scripts/launcher.js diff --git a/scripts/launcher.js b/scripts/launcher.js new file mode 100644 index 0000000..4b4745d --- /dev/null +++ b/scripts/launcher.js @@ -0,0 +1,92 @@ +const { execSync } = require("child_process"); +const { readFileSync } = require("fs"); +const { resolve } = require("path"); + +// Load .env from project root (don't override existing env vars) +try { + const envPath = resolve(__dirname, "..", ".env"); + const lines = readFileSync(envPath, "utf8").split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq === -1) continue; + const key = trimmed.slice(0, eq).trim(); + const val = trimmed.slice(eq + 1).trim(); + if (!(key in process.env)) process.env[key] = val; + } +} catch {} + +// Resolve ports from env (each service owns its own port var) +const gatewayPort = process.env.PORT || "3001"; +const dashboardPort = process.env.DASHBOARD_PORT || "3000"; +const memoryPort = process.env.MEMORY_PORT || "4005"; +const openrouterPort = process.env.OPENROUTER_PORT || "4010"; + +const SERVICES = { + gateway: { + dev: `PORT=${gatewayPort} npm run dev -w gateway`, + start: `PORT=${gatewayPort} npm run start -w gateway`, + label: "gateway", + color: "blue", + port: gatewayPort, + }, + dashboard: { + dev: `npx -w ui/dashboard next dev --turbopack -p ${dashboardPort}`, + start: `npx -w ui/dashboard next start -p ${dashboardPort} -H 0.0.0.0`, + label: "dashboard", + color: "magenta", + port: dashboardPort, + }, + memory: { + dev: `MEMORY_PORT=${memoryPort} npm run start -w memory`, + start: `MEMORY_PORT=${memoryPort} npm run start -w memory`, + label: "memory", + color: "green", + port: memoryPort, + }, + openrouter: { + dev: `OPENROUTER_PORT=${openrouterPort} npm run dev -w @ekai/openrouter`, + start: `OPENROUTER_PORT=${openrouterPort} npm run start -w @ekai/openrouter`, + label: "openrouter", + color: "yellow", + port: openrouterPort, + }, +}; + +const mode = process.argv.includes("--mode") + ? process.argv[process.argv.indexOf("--mode") + 1] + : "dev"; + +if (!["dev", "start"].includes(mode)) { + console.error(`Unknown mode "${mode}". Use --mode dev or --mode start`); + process.exit(1); +} + +const isDisabled = (v) => v === "false" || v === "0"; + +const enabled = Object.entries(SERVICES).filter( + ([name]) => !isDisabled(process.env[`ENABLE_${name.toUpperCase()}`]) +); + +if (enabled.length === 0) { + console.error("All services disabled — nothing to start."); + process.exit(1); +} + +const commands = enabled.map(([, svc]) => `"${svc[mode]}"`).join(" "); +const names = enabled.map(([, svc]) => svc.label).join(","); +const colors = enabled.map(([, svc]) => svc.color).join(","); + +const summary = enabled + .map(([, svc]) => `${svc.label}(:${svc.port})`) + .join(" "); +console.log(`\n Starting [${mode}]: ${summary}\n`); + +const cmd = `npx concurrently --names "${names}" -c "${colors}" ${commands}`; + +try { + execSync(cmd, { stdio: "inherit" }); +} catch { + process.exit(1); +} From caafe2da84b571d95a28c9fb47ab7ac7f05a6f01 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 15 Feb 2026 03:44:18 +0700 Subject: [PATCH 12/78] open router allow user keys --- integrations/openrouter/src/proxy.ts | 5 +++-- integrations/openrouter/src/server.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/integrations/openrouter/src/proxy.ts b/integrations/openrouter/src/proxy.ts index 6e0c3aa..80d71c8 100644 --- a/integrations/openrouter/src/proxy.ts +++ b/integrations/openrouter/src/proxy.ts @@ -7,11 +7,12 @@ const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions'; * Proxy a chat completions request to OpenRouter. * Handles both streaming (SSE) and non-streaming responses. */ -export async function proxyToOpenRouter(body: any, res: Response): Promise { +export async function proxyToOpenRouter(body: any, res: Response, apiKey?: string): Promise { + const key = apiKey || OPENROUTER_API_KEY; const response = await fetch(OPENROUTER_URL, { method: 'POST', headers: { - 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, + 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://ekailabs.xyz', 'X-Title': 'Ekai Gateway', diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts index 5c45dec..7237454 100644 --- a/integrations/openrouter/src/server.ts +++ b/integrations/openrouter/src/server.ts @@ -13,7 +13,11 @@ app.get('/health', (_req, res) => { app.post('/v1/chat/completions', async (req, res) => { try { const body = req.body; - const profile = (req.headers['x-memory-profile'] as string) || 'default'; + // Profile from body.user (PydanticAI openai_user), header, or default + const profile = body.user || (req.headers['x-memory-profile'] as string) || 'default'; + // Pass through client's API key if provided, otherwise proxy.ts falls back to env + const authHeader = req.headers['authorization'] as string | undefined; + const clientKey = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : undefined; // Extract last user message for memory query const lastUserMsg = [...(body.messages || [])] @@ -44,7 +48,7 @@ app.post('/v1/chat/completions', async (req, res) => { // Fire-and-forget: ingest original messages for future recall ingestMessages(originalMessages, profile); - await proxyToOpenRouter(body, res); + await proxyToOpenRouter(body, res, clientKey); } catch (err: any) { console.error(`[server] unhandled error: ${err.message}`); if (!res.headersSent) { From 9592d768e92509e4efc95331f89a49b22021a872 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:02:23 +0700 Subject: [PATCH 13/78] fixed docker file and build issues --- Dockerfile | 15 ++--- gateway/src/costs/openrouter.yaml | 62 ++++++++++++------- .../ollama-responses-passthrough.ts | 40 +----------- scripts/start-docker-fullstack.sh | 4 +- 4 files changed, 54 insertions(+), 67 deletions(-) diff --git a/Dockerfile b/Dockerfile index a581289..a1fc10c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,6 @@ FROM build-base AS gateway-build WORKDIR /app/gateway RUN npm run build - # Ensure schema.sql ships with compiled code - RUN if [ -f src/db/schema.sql ]; then mkdir -p dist/db && cp src/db/schema.sql dist/db/schema.sql; fi # ---------- dashboard build ---------- FROM build-base AS dashboard-build @@ -47,7 +45,7 @@ RUN npm run build FROM node:20-alpine AS dashboard-runtime WORKDIR /app/ui/dashboard COPY ui/dashboard/package.json ui/dashboard/package-lock.json ./ -RUN npm install +RUN npm install --omit=dev ENV NODE_ENV=production # Copy production build output COPY --from=dashboard-build /app/ui/dashboard/.next ./.next @@ -56,21 +54,24 @@ ENV NODE_ENV=production COPY --from=dashboard-build /app/ui/dashboard/next.config.ts ./next.config.ts COPY --from=dashboard-build /app/ui/dashboard/postcss.config.mjs ./postcss.config.mjs EXPOSE 3000 - CMD ["npx", "next", "start", "-p", "3000"] + CMD ["node_modules/.bin/next", "start", "-p", "3000"] # ---------- fullstack runtime ---------- FROM node:20-alpine AS ekai-gateway-runtime WORKDIR /app - + +# bash is needed for wait -n in the entrypoint script +RUN apk add --no-cache bash + # Gateway runtime bits COPY gateway/package.json gateway/package-lock.json ./gateway/ RUN cd gateway && npm install --omit=dev COPY --from=gateway-build /app/gateway/dist ./gateway/dist RUN mkdir -p /app/gateway/data /app/gateway/logs - + # Dashboard runtime bits COPY ui/dashboard/package.json ui/dashboard/package-lock.json ./ui/dashboard/ -RUN cd ui/dashboard && npm install +RUN cd ui/dashboard && npm install --omit=dev COPY --from=dashboard-build /app/ui/dashboard/.next ./ui/dashboard/.next COPY --from=dashboard-build /app/ui/dashboard/public ./ui/dashboard/public COPY --from=dashboard-build /app/ui/dashboard/next.config.ts ./ui/dashboard/next.config.ts diff --git a/gateway/src/costs/openrouter.yaml b/gateway/src/costs/openrouter.yaml index bc2f4b1..ed92375 100644 --- a/gateway/src/costs/openrouter.yaml +++ b/gateway/src/costs/openrouter.yaml @@ -2,6 +2,26 @@ provider: openrouter currency: USD unit: MTok models: + qwen/qwen3.5-plus-02-15: + id: qwen/qwen3.5-plus-02-15 + input: 0.4 + output: 2.4 + original_provider: qwen + qwen3.5-plus-02-15: + id: qwen/qwen3.5-plus-02-15 + input: 0.4 + output: 2.4 + original_provider: qwen + qwen/qwen3.5-397b-a17b: + id: qwen/qwen3.5-397b-a17b + input: 0.6 + output: 3.6 + original_provider: qwen + qwen3.5-397b-a17b: + id: qwen/qwen3.5-397b-a17b + input: 0.6 + output: 3.6 + original_provider: qwen minimax/minimax-m2.5: id: minimax/minimax-m2.5 input: 0.3 @@ -14,13 +34,13 @@ models: original_provider: minimax z-ai/glm-5: id: z-ai/glm-5 - input: 0.8 - output: 2.56 + input: 0.3 + output: 2.55 original_provider: z-ai glm-5: id: z-ai/glm-5 - input: 0.8 - output: 2.56 + input: 0.3 + output: 2.55 original_provider: z-ai qwen/qwen3-max-thinking: id: qwen/qwen3-max-thinking @@ -104,13 +124,13 @@ models: original_provider: arcee-ai moonshotai/kimi-k2.5: id: moonshotai/kimi-k2.5 - input: 0.45 - output: 2.25 + input: 0.23 + output: 3 original_provider: moonshotai kimi-k2.5: id: moonshotai/kimi-k2.5 - input: 0.45 - output: 2.25 + input: 0.23 + output: 3 original_provider: moonshotai upstage/solar-pro-3:free: id: upstage/solar-pro-3:free @@ -1184,13 +1204,13 @@ models: original_provider: nvidia moonshotai/kimi-k2-0905: id: moonshotai/kimi-k2-0905 - input: 0.39 - output: 1.9 + input: 0.4 + output: 2 original_provider: moonshotai kimi-k2-0905: id: moonshotai/kimi-k2-0905 - input: 0.39 - output: 1.9 + input: 0.4 + output: 2 original_provider: moonshotai moonshotai/kimi-k2-0905:exacto: id: moonshotai/kimi-k2-0905:exacto @@ -2352,23 +2372,23 @@ models: input: 0.075 output: 0.3 original_provider: google - anthropic/claude-3.7-sonnet:thinking: - id: anthropic/claude-3.7-sonnet:thinking + anthropic/claude-3.7-sonnet: + id: anthropic/claude-3.7-sonnet input: 3 output: 15 original_provider: anthropic - claude-3.7-sonnet:thinking: - id: anthropic/claude-3.7-sonnet:thinking + claude-3.7-sonnet: + id: anthropic/claude-3.7-sonnet input: 3 output: 15 original_provider: anthropic - anthropic/claude-3.7-sonnet: - id: anthropic/claude-3.7-sonnet + anthropic/claude-3.7-sonnet:thinking: + id: anthropic/claude-3.7-sonnet:thinking input: 3 output: 15 original_provider: anthropic - claude-3.7-sonnet: - id: anthropic/claude-3.7-sonnet + claude-3.7-sonnet:thinking: + id: anthropic/claude-3.7-sonnet:thinking input: 3 output: 15 original_provider: anthropic @@ -3403,7 +3423,7 @@ models: output: 1.5 original_provider: openai metadata: - last_updated: '2026-02-14' + last_updated: '2026-02-16' source: https://openrouter.ai/api/v1/models notes: Auto-refreshed from OpenRouter models API version: auto diff --git a/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts b/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts index a692b5d..074733c 100644 --- a/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts +++ b/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts @@ -4,7 +4,6 @@ import { ProviderError } from '../../shared/errors/index.js'; import { CONTENT_TYPES } from '../../domain/types/provider.js'; import { getConfig } from '../config/app-config.js'; import { ResponsesPassthrough, ResponsesPassthroughConfig } from './responses-passthrough.js'; -import { injectMemoryContext, persistMemory } from '../memory/memory-helper.js'; export class OllamaResponsesPassthrough implements ResponsesPassthrough { constructor(private readonly config: ResponsesPassthroughConfig) {} @@ -159,18 +158,7 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { this.eventBuffer = ''; this.assistantResponseBuffer = ''; - injectMemoryContext(request, { - provider: this.config.provider, - defaultUserId: 'default', - extractCurrentUserInputs: req => extractResponsesUserInputs(req), - applyMemoryContext: (req, context) => { - if (req.instructions) { - req.instructions = `${context}\n\n---\n\n${req.instructions}`; - } else { - req.instructions = context; - } - } - }); + // TODO: Add memory context injection when memory service is implemented if (request.stream) { const response = await this.makeRequest(request, true); @@ -195,15 +183,7 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { } res.end(); - persistMemory(request, this.assistantResponseBuffer, { - provider: this.config.provider, - defaultUserId: 'default', - extractUserContent: req => req.input || '', - metadataBuilder: req => ({ - model: req.model, - provider: this.config.provider, - }), - }); + // TODO: Persist memory when memory service is implemented } else { const response = await this.makeRequest(request, false); const json = await response.json(); @@ -227,23 +207,9 @@ export class OllamaResponsesPassthrough implements ResponsesPassthrough { }).catch(() => {}); } - const assistantResponse = json?.output?.[0]?.content?.[0]?.text || json?.output_text || ''; - persistMemory(request, assistantResponse, { - provider: this.config.provider, - defaultUserId: 'default', - extractUserContent: req => req.input || '', - metadataBuilder: req => ({ - model: req.model, - provider: this.config.provider, - }), - }); + // TODO: Persist memory when memory service is implemented res.json(json); } } } - -function extractResponsesUserInputs(request: any): string[] { - const content = (request.input || '').trim(); - return content ? [content] : []; -} diff --git a/scripts/start-docker-fullstack.sh b/scripts/start-docker-fullstack.sh index 4675f03..02886c4 100755 --- a/scripts/start-docker-fullstack.sh +++ b/scripts/start-docker-fullstack.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -euo pipefail GATEWAY_DIR="/app/gateway" @@ -41,7 +41,7 @@ node dist/gateway/src/index.js & GW_PID=$! cd "$DASHBOARD_DIR" -npx next start -p "$UI_PORT" -H 0.0.0.0 & +node_modules/.bin/next start -p "$UI_PORT" -H 0.0.0.0 & UI_PID=$! wait -n "$GW_PID" "$UI_PID" From 9ab6080f658ed452d9104d688227984f1503d66c Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:32:21 +0700 Subject: [PATCH 14/78] fixed docker image setup --- .env.example | 3 +++ Dockerfile | 4 ++-- docker-compose.yaml | 3 +++ docs/architecture-overview.md | 2 +- docs/getting-started.md | 21 ++++++++++++++++++++- gateway/package.json | 1 + gateway/src/infrastructure/db/connection.ts | 10 +++++++--- ui/dashboard/next.config.mjs | 6 ++++++ ui/dashboard/next.config.ts | 7 ------- 9 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 ui/dashboard/next.config.mjs delete mode 100644 ui/dashboard/next.config.ts diff --git a/.env.example b/.env.example index d19e4eb..005ea8b 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,9 @@ GOOGLE_API_KEY=your_key_here # Ollama (local models — no API key needed, just set the base URL) # OLLAMA_BASE_URL=http://localhost:11434/v1 # OLLAMA_API_KEY= # optional, only if behind an auth proxy +# Database +# DATABASE_PATH=data/proxy.db # SQLite file path (default: data/proxy.db relative to gateway cwd) + # Service ports PORT=3001 # Gateway # DASHBOARD_PORT=3000 # Dashboard diff --git a/Dockerfile b/Dockerfile index a1fc10c..26ced24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ ENV NODE_ENV=production COPY --from=dashboard-build /app/ui/dashboard/.next ./.next COPY --from=dashboard-build /app/ui/dashboard/public ./public # Copy config files - COPY --from=dashboard-build /app/ui/dashboard/next.config.ts ./next.config.ts + COPY --from=dashboard-build /app/ui/dashboard/next.config.mjs ./next.config.mjs COPY --from=dashboard-build /app/ui/dashboard/postcss.config.mjs ./postcss.config.mjs EXPOSE 3000 CMD ["node_modules/.bin/next", "start", "-p", "3000"] @@ -74,7 +74,7 @@ COPY ui/dashboard/package.json ui/dashboard/package-lock.json ./ui/dashboard/ RUN cd ui/dashboard && npm install --omit=dev COPY --from=dashboard-build /app/ui/dashboard/.next ./ui/dashboard/.next COPY --from=dashboard-build /app/ui/dashboard/public ./ui/dashboard/public - COPY --from=dashboard-build /app/ui/dashboard/next.config.ts ./ui/dashboard/next.config.ts + COPY --from=dashboard-build /app/ui/dashboard/next.config.mjs ./ui/dashboard/next.config.mjs COPY --from=dashboard-build /app/ui/dashboard/postcss.config.mjs ./ui/dashboard/postcss.config.mjs # Entrypoint for running both services diff --git a/docker-compose.yaml b/docker-compose.yaml index a5185f1..1e64551 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,9 +6,12 @@ services: PORT: ${PORT:-3001} UI_PORT: ${UI_PORT:-3000} NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3001} + DATABASE_PATH: /app/gateway/data/proxy.db OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + GOOGLE_API_KEY: ${GOOGLE_API_KEY:-} ports: - "3001:3001" - "3000:3000" diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index d87f9c4..cad4db4 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -43,7 +43,7 @@ Memory is additive and never blocking: if the service is unreachable, requests p ### Storage -Usage data and request logs are stored in a local SQLite database (by default `data/usage.db`).\ +Usage data and request logs are stored in a local SQLite database (by default `data/proxy.db`).\ No external database configuration is required. *** diff --git a/docs/getting-started.md b/docs/getting-started.md index d087c2b..2c56a95 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -75,6 +75,25 @@ docker compose --profile split up --build -d The `split` profile starts the `gateway` and `dashboard` services defined in `docker-compose.yaml`. +### Data persistence + +Docker Compose uses named volumes to persist data across container restarts and recreates: + +| Volume | Container path | Contents | +|---|---|---| +| `gateway_db` | `/app/gateway/data/` | SQLite database (`proxy.db`) and WAL files | +| `gateway_logs` | `/app/gateway/logs/` | Application logs (`gateway.log`) | + +To back up the database: +```bash +docker cp $(docker compose ps -q fullstack):/app/gateway/data/proxy.db ./backup.db +``` + +To remove all data and start fresh: +```bash +docker compose down -v +``` + --- ## Environment Variables @@ -88,7 +107,7 @@ The `split` profile starts the `gateway` and `dashboard` services defined in `do | `GOOGLE_API_KEY` | Key for Google Gemini models | | `PORT` | Gateway API port (default 3001) | | `MEMORY_PORT` | Memory service port (default 4005) | -| `DATABASE_PATH` | SQLite file path (default `data/usage.db`) | +| `DATABASE_PATH` | SQLite file path (default `data/proxy.db`) | The dashboard auto-detects the host from the browser and connects to the gateway and memory service on the same host using their default ports. No URL configuration needed for standard deployments. diff --git a/gateway/package.json b/gateway/package.json index ecb7f93..d790788 100644 --- a/gateway/package.json +++ b/gateway/package.json @@ -2,6 +2,7 @@ "name": "gateway", "version": "0.1.0-beta.1", "description": "Ekai Gateway", + "type": "module", "private": true, "scripts": { "dev": "tsx watch src/index.ts", diff --git a/gateway/src/infrastructure/db/connection.ts b/gateway/src/infrastructure/db/connection.ts index d83240b..a0318ff 100644 --- a/gateway/src/infrastructure/db/connection.ts +++ b/gateway/src/infrastructure/db/connection.ts @@ -1,5 +1,5 @@ import Database from 'better-sqlite3'; -import { readFileSync } from 'fs'; +import { readFileSync, existsSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { logger } from '../utils/logger.js'; @@ -16,8 +16,12 @@ class DatabaseConnection { private initialize() { try { - // Create database file in the same directory as this file - const dbPath = join(__dirname, 'proxy.db'); + // Use DATABASE_PATH env var, or default to data/proxy.db relative to cwd + // In Docker: cwd = /app/gateway/ → /app/gateway/data/proxy.db (matches volume mount) + // In dev: cwd = gateway/ → gateway/data/proxy.db + const dbPath = process.env.DATABASE_PATH || join(process.cwd(), 'data', 'proxy.db'); + const dbDir = dirname(dbPath); + if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true }); this.db = new Database(dbPath); // Enable WAL mode for better concurrency diff --git a/ui/dashboard/next.config.mjs b/ui/dashboard/next.config.mjs new file mode 100644 index 0000000..f3cc306 --- /dev/null +++ b/ui/dashboard/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/ui/dashboard/next.config.ts b/ui/dashboard/next.config.ts deleted file mode 100644 index e9ffa30..0000000 --- a/ui/dashboard/next.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - /* config options here */ -}; - -export default nextConfig; From 557d43f9840ec57270e7dddd69224aa561ba7184 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:24:54 +0700 Subject: [PATCH 15/78] agent centric memory --- gateway/src/costs/openrouter.yaml | 62 +- integrations/openrouter/package.json | 3 +- integrations/openrouter/src/memory.test.ts | 113 +++ integrations/openrouter/src/memory.ts | 17 +- memory/README.md | 369 +++++--- memory/src/providers/extract.ts | 68 +- memory/src/providers/prompt.ts | 84 +- memory/src/scoring.ts | 1 + memory/src/server.ts | 129 +-- memory/src/sqlite-store.ts | 923 ++++++++++++++------- memory/src/types.ts | 56 +- package-lock.json | 423 +++++++++- 12 files changed, 1676 insertions(+), 572 deletions(-) create mode 100644 integrations/openrouter/src/memory.test.ts diff --git a/gateway/src/costs/openrouter.yaml b/gateway/src/costs/openrouter.yaml index bc2f4b1..ed92375 100644 --- a/gateway/src/costs/openrouter.yaml +++ b/gateway/src/costs/openrouter.yaml @@ -2,6 +2,26 @@ provider: openrouter currency: USD unit: MTok models: + qwen/qwen3.5-plus-02-15: + id: qwen/qwen3.5-plus-02-15 + input: 0.4 + output: 2.4 + original_provider: qwen + qwen3.5-plus-02-15: + id: qwen/qwen3.5-plus-02-15 + input: 0.4 + output: 2.4 + original_provider: qwen + qwen/qwen3.5-397b-a17b: + id: qwen/qwen3.5-397b-a17b + input: 0.6 + output: 3.6 + original_provider: qwen + qwen3.5-397b-a17b: + id: qwen/qwen3.5-397b-a17b + input: 0.6 + output: 3.6 + original_provider: qwen minimax/minimax-m2.5: id: minimax/minimax-m2.5 input: 0.3 @@ -14,13 +34,13 @@ models: original_provider: minimax z-ai/glm-5: id: z-ai/glm-5 - input: 0.8 - output: 2.56 + input: 0.3 + output: 2.55 original_provider: z-ai glm-5: id: z-ai/glm-5 - input: 0.8 - output: 2.56 + input: 0.3 + output: 2.55 original_provider: z-ai qwen/qwen3-max-thinking: id: qwen/qwen3-max-thinking @@ -104,13 +124,13 @@ models: original_provider: arcee-ai moonshotai/kimi-k2.5: id: moonshotai/kimi-k2.5 - input: 0.45 - output: 2.25 + input: 0.23 + output: 3 original_provider: moonshotai kimi-k2.5: id: moonshotai/kimi-k2.5 - input: 0.45 - output: 2.25 + input: 0.23 + output: 3 original_provider: moonshotai upstage/solar-pro-3:free: id: upstage/solar-pro-3:free @@ -1184,13 +1204,13 @@ models: original_provider: nvidia moonshotai/kimi-k2-0905: id: moonshotai/kimi-k2-0905 - input: 0.39 - output: 1.9 + input: 0.4 + output: 2 original_provider: moonshotai kimi-k2-0905: id: moonshotai/kimi-k2-0905 - input: 0.39 - output: 1.9 + input: 0.4 + output: 2 original_provider: moonshotai moonshotai/kimi-k2-0905:exacto: id: moonshotai/kimi-k2-0905:exacto @@ -2352,23 +2372,23 @@ models: input: 0.075 output: 0.3 original_provider: google - anthropic/claude-3.7-sonnet:thinking: - id: anthropic/claude-3.7-sonnet:thinking + anthropic/claude-3.7-sonnet: + id: anthropic/claude-3.7-sonnet input: 3 output: 15 original_provider: anthropic - claude-3.7-sonnet:thinking: - id: anthropic/claude-3.7-sonnet:thinking + claude-3.7-sonnet: + id: anthropic/claude-3.7-sonnet input: 3 output: 15 original_provider: anthropic - anthropic/claude-3.7-sonnet: - id: anthropic/claude-3.7-sonnet + anthropic/claude-3.7-sonnet:thinking: + id: anthropic/claude-3.7-sonnet:thinking input: 3 output: 15 original_provider: anthropic - claude-3.7-sonnet: - id: anthropic/claude-3.7-sonnet + claude-3.7-sonnet:thinking: + id: anthropic/claude-3.7-sonnet:thinking input: 3 output: 15 original_provider: anthropic @@ -3403,7 +3423,7 @@ models: output: 1.5 original_provider: openai metadata: - last_updated: '2026-02-14' + last_updated: '2026-02-16' source: https://openrouter.ai/api/v1/models notes: Auto-refreshed from OpenRouter models API version: auto diff --git a/integrations/openrouter/package.json b/integrations/openrouter/package.json index 05d6061..c5649c2 100644 --- a/integrations/openrouter/package.json +++ b/integrations/openrouter/package.json @@ -16,6 +16,7 @@ "devDependencies": { "@types/express": "^4.17.21", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^3.0.0" } } diff --git a/integrations/openrouter/src/memory.test.ts b/integrations/openrouter/src/memory.test.ts new file mode 100644 index 0000000..13dafa1 --- /dev/null +++ b/integrations/openrouter/src/memory.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest'; +import { formatMemoryBlock } from './memory.js'; + +describe('formatMemoryBlock', () => { + it('formats semantic facts under "What I know:"', () => { + const results = [ + { + sector: 'semantic' as const, + content: 'Sha prefers dark mode', + score: 0.9, + details: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + }, + ]; + const block = formatMemoryBlock(results); + expect(block).toContain('What I know:'); + expect(block).toContain('- Sha prefers dark mode'); + expect(block).not.toContain('Facts:'); + }); + + it('formats episodic events under "What I remember:"', () => { + const results = [ + { + sector: 'episodic' as const, + content: 'I discussed project architecture with Sha on Monday', + score: 0.8, + }, + ]; + const block = formatMemoryBlock(results); + expect(block).toContain('What I remember:'); + expect(block).toContain('- I discussed project architecture with Sha on Monday'); + expect(block).not.toContain('Events:'); + }); + + it('formats procedural memories under "How I do things:"', () => { + const results = [ + { + sector: 'procedural' as const, + content: 'deploy to production', + score: 0.7, + details: { + trigger: 'user asks to deploy', + steps: ['run tests', 'build', 'push to main'], + }, + }, + ]; + const block = formatMemoryBlock(results); + expect(block).toContain('How I do things:'); + expect(block).toContain('When user asks to deploy: run tests → build → push to main'); + expect(block).not.toContain('Procedures:'); + }); + + it('formats reflective observations under "My observations:"', () => { + const results = [ + { + sector: 'reflective' as const, + content: 'I tend to give overly detailed answers when a short response would suffice', + score: 0.6, + }, + ]; + const block = formatMemoryBlock(results); + expect(block).toContain('My observations:'); + expect(block).toContain('- I tend to give overly detailed answers when a short response would suffice'); + }); + + it('groups all four sectors correctly', () => { + const results = [ + { + sector: 'semantic' as const, + content: 'TypeScript supports generics', + score: 0.9, + details: { subject: 'TypeScript', predicate: 'supports', object: 'generics' }, + }, + { + sector: 'episodic' as const, + content: 'I helped debug a memory leak yesterday', + score: 0.8, + }, + { + sector: 'procedural' as const, + content: 'run tests', + score: 0.7, + details: { trigger: 'before commit', steps: ['lint', 'test', 'build'] }, + }, + { + sector: 'reflective' as const, + content: 'Sha responds better when I lead with the conclusion', + score: 0.6, + }, + ]; + const block = formatMemoryBlock(results); + expect(block).toContain('What I know:'); + expect(block).toContain('What I remember:'); + expect(block).toContain('How I do things:'); + expect(block).toContain('My observations:'); + expect(block).toContain(''); + expect(block).toContain(''); + }); + + it('omits empty sections', () => { + const results = [ + { + sector: 'reflective' as const, + content: 'I noticed a pattern', + score: 0.5, + }, + ]; + const block = formatMemoryBlock(results); + expect(block).toContain('My observations:'); + expect(block).not.toContain('What I know:'); + expect(block).not.toContain('What I remember:'); + expect(block).not.toContain('How I do things:'); + }); +}); diff --git a/integrations/openrouter/src/memory.ts b/integrations/openrouter/src/memory.ts index 38c2e51..cfcf624 100644 --- a/integrations/openrouter/src/memory.ts +++ b/integrations/openrouter/src/memory.ts @@ -1,7 +1,7 @@ import { MEMORY_URL } from './config.js'; interface QueryResult { - sector: 'episodic' | 'semantic' | 'procedural'; + sector: 'episodic' | 'semantic' | 'procedural' | 'reflective'; content: string; score: number; details?: { @@ -9,6 +9,7 @@ interface QueryResult { subject?: string; predicate?: string; object?: string; + domain?: string; // procedural trigger?: string; steps?: string[]; @@ -29,6 +30,7 @@ interface SearchResponse { export async function fetchMemoryContext( query: string, profile: string, + userId?: string, ): Promise { try { const controller = new AbortController(); @@ -37,7 +39,7 @@ export async function fetchMemoryContext( const res = await fetch(`${MEMORY_URL}/v1/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, profile }), + body: JSON.stringify({ query, profile, userId }), signal: controller.signal, }); clearTimeout(timeout); @@ -57,11 +59,13 @@ export async function fetchMemoryContext( /** * Format memory results into a system message block, grouped by sector. + * Uses agent-voice section names. */ export function formatMemoryBlock(results: QueryResult[]): string { const facts: string[] = []; const events: string[] = []; const procedures: string[] = []; + const observations: string[] = []; for (const r of results) { if (r.sector === 'semantic' && r.details?.subject) { @@ -69,15 +73,18 @@ export function formatMemoryBlock(results: QueryResult[]): string { } else if (r.sector === 'procedural' && r.details?.trigger) { const steps = r.details.steps?.join(' → ') || r.content; procedures.push(`- When ${r.details.trigger}: ${steps}`); + } else if (r.sector === 'reflective') { + observations.push(`- ${r.content}`); } else { events.push(`- ${r.content}`); } } const sections: string[] = []; - if (facts.length) sections.push(`Facts:\n${facts.join('\n')}`); - if (events.length) sections.push(`Events:\n${events.join('\n')}`); - if (procedures.length) sections.push(`Procedures:\n${procedures.join('\n')}`); + if (facts.length) sections.push(`What I know:\n${facts.join('\n')}`); + if (events.length) sections.push(`What I remember:\n${events.join('\n')}`); + if (procedures.length) sections.push(`How I do things:\n${procedures.join('\n')}`); + if (observations.length) sections.push(`My observations:\n${observations.join('\n')}`); return `\n[Recalled context for this conversation. Use naturally if relevant, ignore if not.]\n\n${sections.join('\n\n')}\n`; } diff --git a/memory/README.md b/memory/README.md index ba75b3c..67119a1 100644 --- a/memory/README.md +++ b/memory/README.md @@ -1,155 +1,274 @@ # Memory Service (Ekai) -Neuroscience-inspired, sectorized memory kernel. Runs as a standalone service (default port 4005) and is currently opt-in. +Neuroscience-inspired, agent-centric memory kernel. Sectorized storage with PBWM gating — the agent reflects on conversations and decides what to learn. Memory is first-person, not a passive database about users. -## Quickstart (standalone) +## Quickstart ```bash npm install -w memory npm run build -w memory -npm start -w memory +npm start -w memory # :4005 ``` Env (root `.env` or `memory/.env`): -- `GOOGLE_API_KEY` (required for Gemini extract/embeds) -- Optional: `GEMINI_EXTRACT_MODEL` (default `gemini-2.5-flash`) -- Optional: `GEMINI_EMBED_MODEL` (default `gemini-embedding-001`) -- Optional: `MEMORY_PORT` (default `4005`) -- Optional: `MEMORY_DB_PATH` (default `./memory.db`) -- Optional: `MEMORY_CORS_ORIGIN` +| Variable | Default | Required | +|----------|---------|----------| +| `GOOGLE_API_KEY` | — | Yes | +| `GEMINI_EXTRACT_MODEL` | `gemini-2.5-flash` | No | +| `GEMINI_EMBED_MODEL` | `gemini-embedding-001` | No | +| `MEMORY_PORT` | `4005` | No | +| `MEMORY_DB_PATH` | `./memory.db` | No | +| `MEMORY_CORS_ORIGIN` | `*` | No | -## API (v0) +## How It Works -- `POST /v1/ingest` — ingest an experience - Body: - ```json - { +```mermaid +graph TB + classDef input fill:#eceff1,stroke:#546e7a,stroke-width:2px + classDef process fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + classDef sector fill:#fff3e0,stroke:#f57c00,stroke-width:2px + classDef store fill:#fce4ec,stroke:#c2185b,stroke-width:2px + classDef engine fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px + classDef output fill:#e8f5e9,stroke:#388e3c,stroke-width:2px + + IN["POST /v1/ingest
messages + userId"]:::input + EXT["Agent Reflection (LLM)
first-person, multi-fact"]:::process + + EP["Episodic"]:::sector + SE["Semantic[]
+ domain"]:::sector + PR["Procedural"]:::sector + RE["Reflective"]:::sector + + EMB["Embed"]:::process + CON["Consolidate"]:::process + DB["SQLite"]:::store + AU["agent_users"]:::store + + SEARCH["POST /v1/search
query + userId"]:::input + SCOPE["user_scope filter"]:::engine + PBWM["PBWM Gate"]:::engine + WM["Working Memory (cap 8)"]:::engine + OUT["Response"]:::output + SUM["GET /v1/summary"]:::input + + IN --> EXT + EXT --> EP & SE & PR & RE + SE --> CON + EP & CON & PR & RE --> EMB --> DB + IN -.-> AU + + SEARCH --> SCOPE --> PBWM --> WM --> OUT + DB --> SCOPE + SUM --> DB +``` + +### Four Sectors + +| Sector | What it stores | Example | +|--------|---------------|---------| +| **Episodic** | Events, conversations | "I discussed architecture with Sha on Monday" | +| **Semantic** | Facts as triples + domain | `Sha / prefers / dark mode` (domain: `user`) | +| **Procedural** | Multi-step workflows | When deploying: test -> build -> push | +| **Reflective** | Agent self-observations | "I tend to overcomplicate solutions" | + +### Domain & User Scoping + +```mermaid +graph LR + classDef usr fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + classDef wld fill:#fff3e0,stroke:#f57c00,stroke-width:2px + classDef slf fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px + + U["user domain
scoped to userId
Sha prefers dark mode"]:::usr + W["world domain
visible to all
TypeScript supports generics"]:::wld + S["self domain
visible to all
I use GPT-4 for extraction"]:::slf +``` + +When `userId` is passed: `user`-domain facts are only visible to that user. `world`/`self` facts are shared. + +### Attribution + +Every memory tracks its origin: `origin_type` (conversation/document/api), `origin_actor` (who), `origin_ref` (source reference). + +### Consolidation + +Semantic triples go through consolidation per triple: +- **Merge** — same fact exists -> strengthen it +- **Supersede** — different value for same slot -> close old, insert new +- **Insert** — new fact + +Predicate matching uses embeddings (>=0.9 cosine): "cofounded" ~ "is co-founder of" -> same slot. + +## API + +### `POST /v1/ingest` + +Ingest a conversation. Full conversation (user + assistant) goes to the LLM for agent-centric reflection. + +```json +{ "messages": [ - { "role": "user", "content": "..." }, - { "role": "assistant", "content": "..." } + { "role": "user", "content": "I prefer dark mode and use TypeScript" }, + { "role": "assistant", "content": "Noted!" } ], - "reasoning": "optional", - "feedback": { - "type": "success|failure", - "value": 0 - }, - "metadata": {} - } - ``` - Requires at least one user message. `reasoning`, `feedback`, and `metadata` are optional and currently not used in extraction/scoring (feedback is not yet applied; retrieval_count drives expected_value). - -- `POST /v1/ingest/documents` — ingest markdown files from a directory - Body: - ```json - { "path": "/path/to/markdown/folder", "profile": "project-x" } - ``` - Reads all `.md` files recursively, chunks by markdown headings, extracts memories via LLM, and stores with deduplication + source attribution. Re-ingesting the same folder is safe — duplicates are skipped. - Response: - ```json - { "ingested": 5, "chunks": 18, "stored": 31, "skipped": 4, "errors": [], "profile": "project-x" } - ``` - -- `POST /v1/search` — body `{ "query": "..." }` → returns `{ workingMemory, perSector }` with PBWM gating. - -- `GET /v1/summary` — per-sector counts + recent items (includes procedural details). - -- `DELETE /v1/memory/:id` — delete one; `DELETE /v1/memory` — delete all. - -- `GET /health` - -## Data model (SQLite) - -- `memory` table for episodic: - `id, sector, content, embedding, created_at, last_accessed, event_start, event_end, source`. -- `procedural_memory` table for structured procedures: - `trigger, goal, context, result, steps[], embedding, timestamps, source`. -- `retrieval_count` tracks how often a memory enters working memory; used in PBWM expected_value. -- `semantic_memory` (graph-lite facts): `subject, predicate, object, valid_from, valid_to, strength, embedding, metadata, source`. -- `source` column (`TEXT DEFAULT NULL`) stores the relative file path for document-ingested memories (e.g., `notes/architecture.md`). NULL for chat-ingested memories. - -## Semantic Consolidation - -When ingesting semantic facts, the system applies consolidation logic using **semantic similarity** for predicate matching: - -1. **Find existing facts**: Query all active facts for the same subject -2. **Semantic predicate matching**: Use embeddings to find predicates with ≥0.9 cosine similarity - - "is co-founder of" ≈ "cofounded" ≈ "founded" → treated as same slot -3. **Consolidation action**: - - **Merge**: Same object exists → strengthen it (increment `strength`) - - **Supersede**: Different object for similar predicate → close old fact (`valid_to = now`), insert new - - **Insert**: No matching predicate → insert new with `strength = 1.0` - -This approach: -- Handles natural language variation in predicates (synonyms, paraphrases) -- Preserves history while avoiding duplicate facts -- Superseded facts remain queryable for temporal reasoning - -## Retrieval - -- Query is embedded per sector. -- Candidates with cosine `< 0.2` are dropped. -- PBWM-inspired gate (prefrontal–basal ganglia model) scores the rest: - - ``` - x = 1.0 * relevance + 0.4 * expected_value + 0.05 * control - 0.02 * noise - gate_score = sigmoid(x) - ``` -- We use retrieval_count (log-normalized) for `expected_value` and keep `control = 0.3` for now; small Gaussian noise is applied. -- Candidates with `gate_score > 0.5` are kept, then top-k per sector are merged and capped to a working-memory size of 8. - -## Architecture (v0) + "profile": "my-agent", + "userId": "sha" +} +``` +```json +{ "stored": 3, "ids": ["...", "...", "..."], "profile": "my-agent" } +``` -```mermaid -graph TB - classDef inputStyle fill:#eceff1,stroke:#546e7a,stroke-width:2px,color:#37474f - classDef processStyle fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1 - classDef sectorStyle fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#e65100 - classDef storageStyle fill:#fce4ec,stroke:#c2185b,stroke-width:2px,color:#880e4f - classDef engineStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c - classDef outputStyle fill:#e8f5e9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 +### `POST /v1/search` + +Search with PBWM gating. Pass `userId` for user-scoped retrieval. + +```json +{ "query": "what does Sha prefer?", "profile": "my-agent", "userId": "sha" } +``` +```json +{ + "workingMemory": [ + { "sector": "semantic", "content": "Sha prefers dark mode", "score": 0.87, "details": { "subject": "Sha", "predicate": "prefers", "object": "dark mode", "domain": "user" } } + ], + "perSector": { "episodic": [], "semantic": [...], "procedural": [], "reflective": [] }, + "profileId": "my-agent" +} +``` - EXP["Experience Ingest
messages + reasoning/feedback"]:::inputStyle - EXTRACT["Extractor (Gemini)
episodic / semantic / procedural"]:::processStyle +### `GET /v1/summary` - EPISODIC["Episodic"]:::sectorStyle - SEMANTIC["Semantic"]:::sectorStyle - PROCEDURAL["Procedural
structured: trigger / goal / steps"]:::sectorStyle +Per-sector counts + recent memories. - EMBED["Embedder (Gemini)
text-embedding-004"]:::processStyle +``` +GET /v1/summary?profile=my-agent&limit=20 +``` +```json +{ + "summary": [ + { "sector": "episodic", "count": 3, "lastCreatedAt": 1700000000 }, + { "sector": "semantic", "count": 12, "lastCreatedAt": 1700100000 }, + { "sector": "procedural", "count": 1, "lastCreatedAt": 1700050000 }, + { "sector": "reflective", "count": 2, "lastCreatedAt": 1700090000 } + ], + "recent": [{ "id": "...", "sector": "semantic", "preview": "dark mode", "details": {...} }], + "profile": "my-agent" +} +``` + +### `POST /v1/ingest/documents` + +Ingest markdown files from a directory with deduplication. + +```json +{ "path": "/path/to/docs", "profile": "project-x" } +``` + +### `GET /v1/users` + +List all users the agent has interacted with. + +``` +GET /v1/users?profile=my-agent +``` +```json +{ + "users": [{ "userId": "sha", "firstSeen": 1700000000, "lastSeen": 1700100000, "interactionCount": 5 }] +} +``` - STORE["(SQLite)
memory table (event_start/end)
procedural_memory table"]:::storageStyle - FACTGRAPH["Semantic Facts
subject/predicate/object graph"]:::storageStyle - STEPGRAPH["Action DAG
ordered steps"]:::storageStyle +### `GET /v1/users/:id/memories` - QUERY["Search Query"]:::inputStyle - QEMBED["Query Embeds
per sector"]:::processStyle - CANDIDATES["Candidates
(cosine ≥ 0.2)"]:::engineStyle - PBWM["PBWM Gate
sigmoid(1.0*rel + 0.4*exp + 0.05*ctrl - 0.02*noise)"]:::engineStyle - WM["Working Memory
top-k per sector → cap 8"]:::engineStyle +Get all memories scoped to a specific user. - OUTPUT["Recall Response
(workingMemory + perSector)"]:::outputStyle - UI["Dashboard Memory Vault
summary + recent + delete"]:::outputStyle +### All Endpoints - EXP --> EXTRACT - EXTRACT --> EPISODIC - EXTRACT --> SEMANTIC - EXTRACT --> PROCEDURAL +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/v1/ingest` | Ingest conversation | +| POST | `/v1/ingest/documents` | Ingest markdown directory | +| POST | `/v1/search` | Search with PBWM gating | +| GET | `/v1/summary` | Sector counts + recent | +| GET | `/v1/users` | List agent's users | +| GET | `/v1/users/:id/memories` | User-scoped memories | +| GET | `/v1/profiles` | List profiles | +| PUT | `/v1/memory/:id` | Update a memory | +| DELETE | `/v1/memory/:id` | Delete one memory | +| DELETE | `/v1/memory` | Delete all for profile | +| DELETE | `/v1/profiles/:slug` | Delete profile + memories | +| GET | `/v1/graph/triples` | Query semantic triples | +| GET | `/v1/graph/neighbors` | Entity neighbors | +| GET | `/v1/graph/paths` | Paths between entities | +| GET | `/v1/graph/visualization` | Graph visualization data | +| DELETE | `/v1/graph/triple/:id` | Delete a triple | +| GET | `/health` | Health check | - EPISODIC --> EMBED - SEMANTIC --> FACTGRAPH - FACTGRAPH --> EMBED - PROCEDURAL --> STEPGRAPH - STEPGRAPH --> EMBED +All endpoints support `profile` query/body param. + +## Retrieval Pipeline + +```mermaid +graph LR + classDef i fill:#eceff1,stroke:#546e7a,stroke-width:2px + classDef p fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + classDef e fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px + classDef o fill:#e8f5e9,stroke:#388e3c,stroke-width:2px + + Q["Query + userId"]:::i + EMB["4 embeddings
(per sector)"]:::p + F["cosine >= 0.2
+ user_scope"]:::e + G["PBWM gate
sigmoid(1.0r + 0.4e + 0.05c - 0.02n)"]:::e + W["Working Memory
top-4/sector, cap 8"]:::o + + Q --> EMB --> F --> G --> W +``` + +Sector weights: episodic `1.0`, semantic `1.0`, procedural `1.0`, reflective `0.8`. + +## Data Model + +```mermaid +erDiagram + profiles ||--o{ memory : has + profiles ||--o{ semantic_memory : has + profiles ||--o{ procedural_memory : has + profiles ||--o{ reflective_memory : has + profiles ||--o{ agent_users : has + + memory { text id PK; text sector; text content; text user_scope; text origin_type } + semantic_memory { text id PK; text subject; text predicate; text object; text domain; text user_scope; real strength } + procedural_memory { text id PK; text trigger; json steps; text user_scope; text origin_type } + reflective_memory { text id PK; text observation; text origin_type; text origin_actor } + agent_users { text agent_id PK; text user_id PK; int interaction_count } + profiles { text slug PK; int created_at } +``` + +All tables share: `embedding`, `created_at`, `last_accessed`, `profile_id`, `source`, `origin_type`, `origin_actor`, `origin_ref`. Schema auto-upgrades on startup. + +## Integration + +When used via `@ekai/openrouter`, memories are injected as: + +``` + +What I know: +- Sha prefers dark mode - EMBED --> STORE +What I remember: +- I discussed architecture with Sha on Monday - QUERY --> QEMBED --> CANDIDATES --> PBWM --> WM --> OUTPUT - STORE --> CANDIDATES +How I do things: +- When deploying: test -> build -> push - OUTPUT --> UI +My observations: +- Sha responds better when I lead with the conclusion + ``` -## Notes / Limitations +## Notes -- Only Gemini provider is wired (provider abstraction is pending). OpenAI would need wiring. +- Supports Gemini and OpenAI-compatible APIs for extraction/embedding +- `user_scope` is opt-in — no `userId` = all memories returned +- Schema migrations are additive — existing DBs auto-upgrade, no manual steps +- Reflective weight `0.8` is a tuning knob diff --git a/memory/src/providers/extract.ts b/memory/src/providers/extract.ts index 289271d..7943fdf 100644 --- a/memory/src/providers/extract.ts +++ b/memory/src/providers/extract.ts @@ -1,7 +1,59 @@ -import type { IngestComponents } from '../types.js'; +import type { IngestComponents, SemanticTripleInput, ReflectiveInput } from '../types.js'; import { EXTRACT_PROMPT } from './prompt.js'; import { buildUrl, getApiKey, getModel, resolveProvider } from './registry.js'; +/** + * Normalize semantic output: single object → array, filter invalid triples. + */ +function normalizeSemantic(raw: any): SemanticTripleInput[] { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : [raw]; + return arr.filter( + (t: any) => + t && + typeof t === 'object' && + typeof t.subject === 'string' && t.subject.trim() && + typeof t.predicate === 'string' && t.predicate.trim() && + typeof t.object === 'string' && t.object.trim(), + ).map((t: any) => ({ + subject: t.subject.trim(), + predicate: t.predicate.trim(), + object: t.object.trim(), + domain: ['user', 'world', 'self'].includes(t.domain) ? t.domain : 'world', + })); +} + +/** + * Normalize reflective output: string → array of ReflectiveInput, filter empty. + */ +function normalizeReflective(raw: any): ReflectiveInput[] { + if (!raw) return []; + if (typeof raw === 'string') { + return raw.trim() ? [{ observation: raw.trim() }] : []; + } + const arr = Array.isArray(raw) ? raw : [raw]; + return arr.filter( + (r: any) => + r && + typeof r === 'object' && + typeof r.observation === 'string' && r.observation.trim(), + ).map((r: any) => ({ + observation: r.observation.trim(), + })); +} + +function parseResponse(parsed: any): IngestComponents { + const semantic = normalizeSemantic(parsed.semantic); + const reflective = normalizeReflective(parsed.reflective); + + return { + episodic: typeof parsed.episodic === 'string' ? parsed.episodic : '', + semantic: semantic.length ? semantic : [], + procedural: parsed.procedural ?? '', + reflective: reflective.length ? reflective : [], + }; +} + export async function extract(text: string): Promise { const cfg = resolveProvider('extract'); const apiKey = getApiKey(cfg); @@ -23,12 +75,7 @@ export async function extract(text: string): Promise { } const json = (await resp.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> }; const content = json.candidates?.[0]?.content?.parts?.[0]?.text ?? '{}'; - const parsed = JSON.parse(content) as any; - return { - episodic: parsed.episodic ?? '', - semantic: parsed.semantic ?? '', - procedural: parsed.procedural ?? '', - }; + return parseResponse(JSON.parse(content)); } const resp = await fetch(url, { @@ -50,10 +97,5 @@ export async function extract(text: string): Promise { } const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> }; const content = json.choices[0]?.message?.content ?? '{}'; - const parsed = JSON.parse(content) as any; - return { - episodic: parsed.episodic ?? '', - semantic: parsed.semantic ?? '', - procedural: parsed.procedural ?? '', - }; + return parseResponse(JSON.parse(content)); } diff --git a/memory/src/providers/prompt.ts b/memory/src/providers/prompt.ts index a05c698..c9ae467 100644 --- a/memory/src/providers/prompt.ts +++ b/memory/src/providers/prompt.ts @@ -1,38 +1,62 @@ -export const EXTRACT_PROMPT = `You are a cognitive memory extractor. If the user's message is worthwhile to be stored as long-term information, rewrite the user's message into three distinct memory types. -Always rewrite "I" as "User". Do NOT copy the sentence verbatim; transform it into the correct memory format. +export const EXTRACT_PROMPT = `You are an AI agent reflecting on a conversation. Analyze what was said and extract what you learned into distinct memory types. + +Write from your own perspective as the agent. Use specific names and entities — never use generic labels like "User". Return ONLY valid JSON with these keys: { - "episodic": "", // past events/experiences with time/place; uncertain or one-off statements go here - "semantic": { - "subject": "", - "predicate": "", - "object": "" - }, // Context-free, stable facts for the knowledge graph (including personal facts about User) - "procedural": { // multi-step actions or instructions - "trigger": "", // condition/event that starts the process - "goal": "", // objective of the workflow - "steps": [], // ordered steps - "result": "", // expected outcome - "context": "" // optional conditions/prereqs - } + "episodic": "", + "semantic": [ + { + "subject": "", + "predicate": "", + "object": "", + "domain": "user|world|self" + } + ], + "procedural": { + "trigger": "", + "goal": "", + "steps": [], + "result": "", + "context": "" + }, + "reflective": [ + { + "observation": "" + } + ] } RULES: -- If a field does not apply, return "" (or empty object {} for semantic/procedural). +- If a field does not apply, return "" for episodic, [] for semantic/reflective, {} for procedural. - Do NOT repeat information across fields. -- Episodic = event with time context, place, or uncertain/one-off claims. -- Semantic = stable, context-free facts that can be structured as subject-predicate-object: - * Personal facts about User MUST go in semantic: - - Identity: names, job titles, roles, affiliations - - Relationships: family, friends, colleagues, connections - - Attributes: dietary restrictions, allergies, health conditions, fitness routines - - Preferences as facts: "User is vegetarian", "User prefers remote work" - - Likes/dislikes as facts: "User prefers dark-mode", "User dislikes verbose errors" - - Career: job titles, skills, education, career goals (as facts, not aspirations) - - Location: where User lives, works, frequently visits - - General knowledge: definitions, facts about entities, relationships, properties - * MUST have all three fields (subject, predicate, object) populated if used - * If time-bounded/uncertain/temporary, leave empty {} and use episodic instead. -- Procedural = must be a multi-step workflow or process; if not, leave empty {}. + +EPISODIC — events with time context, place, or uncertain/one-off claims: + - First-person: "I discussed X with Sha", not "User discussed X" + - Include temporal and situational context when present + +SEMANTIC — stable, context-free facts as subject-predicate-object triples: + - Return an ARRAY of triples. Extract ALL distinct facts from the conversation. + - Each triple MUST have subject, predicate, object, and domain. + - Domain classification: + * "user" — facts about the person I'm talking to (preferences, identity, relationships, attributes) + * "world" — general knowledge, facts about external entities, definitions + * "self" — facts about me as an agent (my capabilities, my configuration, my limitations) + - Use the person's name as subject when known, otherwise use their role (e.g. "developer", "customer") + - Examples: + * {"subject": "Sha", "predicate": "prefers", "object": "dark mode", "domain": "user"} + * {"subject": "TypeScript", "predicate": "supports", "object": "type inference", "domain": "world"} + * {"subject": "I", "predicate": "am configured with", "object": "GPT-4 for extraction", "domain": "self"} + +PROCEDURAL — multi-step workflows or processes: + - Must be a genuine multi-step process; if not, leave empty {}. + +REFLECTIVE — meta-cognitive observations about my own behavior or patterns: + - Return an ARRAY of observations. + - Things I notice about how I'm performing, patterns in my interactions, lessons learned. + - Examples: + * {"observation": "I tend to give overly detailed answers when a short response would suffice"} + * {"observation": "Sha responds better when I lead with the conclusion before the reasoning"} + - Only include genuine insights, not restating conversation content. + - NEVER output anything outside the JSON.`; diff --git a/memory/src/scoring.ts b/memory/src/scoring.ts index a13c327..dd37bbf 100644 --- a/memory/src/scoring.ts +++ b/memory/src/scoring.ts @@ -5,6 +5,7 @@ const DEFAULT_SECTOR_WEIGHTS: Record = { episodic: 1, semantic: 1, procedural: 1, + reflective: 0.8, }; const RETRIEVAL_SOFTCAP = 10; // for normalization const RELEVANCE_WEIGHT = 1.0; diff --git a/memory/src/server.ts b/memory/src/server.ts index f72cc82..aa5f475 100644 --- a/memory/src/server.ts +++ b/memory/src/server.ts @@ -48,7 +48,7 @@ async function main() { } }); - app.delete('/v1/profiles/:slug', (req, res) => { + const handleDeleteProfile: express.RequestHandler = (req, res) => { try { const { slug } = req.params; const normalizedProfile = normalizeProfileSlug(slug); @@ -63,39 +63,19 @@ async function main() { } res.status(500).json({ error: err.message ?? 'delete profile failed' }); } - }); - - // Backward/compatibility alias (singular path) - app.delete('/v1/profile/:slug', (req, res) => { - try { - const { slug } = req.params; - const normalizedProfile = normalizeProfileSlug(slug); - const deleted = store.deleteProfile(normalizedProfile); - res.json({ deleted, profile: normalizedProfile }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - if (err?.message === 'cannot_delete_default_profile') { - return res.status(400).json({ error: 'default_profile_protected' }); - } - res.status(500).json({ error: err.message ?? 'delete profile failed' }); - } - }); + }; + app.delete('/v1/profiles/:slug', handleDeleteProfile); app.post('/v1/ingest', async (req, res) => { - const { messages, reasoning, feedback, metadata, profile, profileId } = req.body as { + const { messages, profile, userId } = req.body as { messages?: Array<{ role: 'user' | 'assistant' | string; content: string }>; - reasoning?: string; - feedback?: { type?: 'success' | 'failure'; value?: number; [key: string]: any }; - metadata?: Record; profile?: string; - profileId?: string; + userId?: string; }; let normalizedProfile: string; try { - normalizedProfile = normalizeProfileSlug(profile ?? profileId); + normalizedProfile = normalizeProfileSlug(profile); } catch (err: any) { if (err?.message === 'invalid_profile') { return res.status(400).json({ error: 'invalid_profile' }); @@ -111,12 +91,16 @@ async function main() { return res.status(400).json({ error: 'at least one user message with content is required' }); } - const sourceText = userMessages.map((m) => m.content.trim()).join('\n\n'); + // Pass full conversation (user + assistant) to extraction for agent-centric reflection + const allMessages = messages.filter((m) => m.content?.trim()); + const sourceText = allMessages + .map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content.trim()}`) + .join('\n\n'); + let finalComponents: IngestComponents | undefined; try { finalComponents = await extract(sourceText); - // TODO: incorporate reasoning/feedback into sector construction instead of ignoring them } catch (err: any) { return res.status(500).json({ error: err.message ?? 'extraction failed' }); } @@ -125,7 +109,10 @@ async function main() { return res.status(400).json({ error: 'unable to extract components from messages' }); } try { - const rows = await store.ingest(finalComponents, normalizedProfile); + const rows = await store.ingest(finalComponents, normalizedProfile, { + origin: { originType: 'conversation', originActor: userId }, + userId, + }); res.json({ stored: rows.length, ids: rows.map((r) => r.id), profile: normalizedProfile }); } catch (err: any) { res.status(500).json({ error: err.message ?? 'ingest failed' }); @@ -133,10 +120,9 @@ async function main() { }); app.post('/v1/ingest/documents', async (req, res) => { - const { path: docPath, profile, profileId } = req.body as { + const { path: docPath, profile } = req.body as { path?: string; profile?: string; - profileId?: string; }; if (!docPath || !docPath.trim()) { @@ -145,7 +131,7 @@ async function main() { let normalizedProfile: string; try { - normalizedProfile = normalizeProfileSlug(profile ?? profileId); + normalizedProfile = normalizeProfileSlug(profile); } catch (err: any) { if (err?.message === 'invalid_profile') { return res.status(400).json({ error: 'invalid_profile' }); @@ -172,7 +158,7 @@ async function main() { app.get('/v1/summary', (req, res) => { try { const limit = Number(req.query.limit) || 50; - const profile = (req.query.profile as string) ?? (req.query.profileId as string); + const profile = req.query.profile as string; const normalizedProfile = normalizeProfileSlug(profile); const summary = store.getSectorSummary(normalizedProfile); const recent = store.getRecent(normalizedProfile, limit).map((r) => ({ @@ -197,8 +183,8 @@ async function main() { app.put('/v1/memory/:id', async (req, res) => { try { const { id } = req.params; - const { content, sector, profile, profileId } = req.body as { content?: string; sector?: string; profile?: string; profileId?: string }; - + const { content, sector, profile } = req.body as { content?: string; sector?: string; profile?: string }; + if (!id) return res.status(400).json({ error: 'id_required' }); if (!content || !content.trim()) { return res.status(400).json({ error: 'content_required' }); @@ -206,7 +192,7 @@ async function main() { let normalizedProfile: string; try { - normalizedProfile = normalizeProfileSlug(profile ?? profileId); + normalizedProfile = normalizeProfileSlug(profile); } catch (err: any) { if (err?.message === 'invalid_profile') { return res.status(400).json({ error: 'invalid_profile' }); @@ -227,7 +213,7 @@ async function main() { app.delete('/v1/memory/:id', (req, res) => { try { const { id } = req.params; - const profile = (req.query.profile as string) ?? (req.query.profileId as string); + const profile = req.query.profile as string; if (!id) return res.status(400).json({ error: 'id_required' }); let normalizedProfile: string; try { @@ -253,7 +239,7 @@ async function main() { app.delete('/v1/memory', (req, res) => { try { - const profile = (req.query.profile as string) ?? (req.query.profileId as string); + const profile = req.query.profile as string; const normalizedProfile = normalizeProfileSlug(profile); const deleted = store.deleteAll(normalizedProfile); res.json({ deleted, profile: normalizedProfile }); @@ -269,7 +255,7 @@ async function main() { app.delete('/v1/graph/triple/:id', (req, res) => { try { const { id } = req.params; - const profile = (req.query.profile as string) ?? (req.query.profileId as string); + const profile = req.query.profile as string; if (!id) return res.status(400).json({ error: 'id_required' }); const deleted = store.deleteSemanticById(id, profile); @@ -287,12 +273,12 @@ async function main() { }); app.post('/v1/search', async (req, res) => { - const { query, profile, profileId } = req.body as { query?: string; profile?: string; profileId?: string }; + const { query, profile, userId } = req.body as { query?: string; profile?: string; userId?: string }; if (!query || !query.trim()) { return res.status(400).json({ error: 'query is required' }); } try { - const result = await store.query(query, profile ?? profileId); + const result = await store.query(query, profile, userId); res.json(result); } catch (err: any) { if (err?.message === 'invalid_profile') { @@ -302,10 +288,55 @@ async function main() { } }); + // --- Agent Users --- + + app.get('/v1/users', (req, res) => { + try { + const profile = req.query.profile as string; + const normalizedProfile = normalizeProfileSlug(profile); + const users = store.getAgentUsers(normalizedProfile); + res.json({ users, profile: normalizedProfile }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'failed to fetch users' }); + } + }); + + app.get('/v1/users/:id/memories', (req, res) => { + try { + const { id: userId } = req.params; + const profile = req.query.profile as string; + const limit = Number(req.query.limit) || 50; + const normalizedProfile = normalizeProfileSlug(profile); + + if (!userId) { + return res.status(400).json({ error: 'user_id_required' }); + } + + const memories = store.getMemoriesForUser(normalizedProfile, userId, limit).map((r) => ({ + id: r.id, + sector: r.sector, + profile: r.profileId, + createdAt: r.createdAt, + lastAccessed: r.lastAccessed, + preview: r.content, + details: (r as any).details, + })); + res.json({ memories, userId, profile: normalizedProfile }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'failed to fetch user memories' }); + } + }); + // Graph traversal endpoints app.get('/v1/graph/triples', (req, res) => { try { - const { entity, direction, maxResults, predicate, profile, profileId } = req.query; + const { entity, direction, maxResults, predicate, profile } = req.query; if (!entity || typeof entity !== 'string') { return res.status(400).json({ error: 'entity query parameter is required' }); } @@ -313,7 +344,7 @@ async function main() { const options: any = { maxResults: maxResults ? Number(maxResults) : 100, predicateFilter: predicate as string | undefined, - profile: (profile as string) ?? (profileId as string), + profile: profile as string, }; let triples; @@ -336,12 +367,12 @@ async function main() { app.get('/v1/graph/neighbors', (req, res) => { try { - const { entity, profile, profileId } = req.query; + const { entity, profile } = req.query; if (!entity || typeof entity !== 'string') { return res.status(400).json({ error: 'entity query parameter is required' }); } - const neighbors = Array.from(store.graph.findNeighbors(entity, { profile: (profile as string) ?? (profileId as string) })); + const neighbors = Array.from(store.graph.findNeighbors(entity, { profile: profile as string })); res.json({ entity, neighbors, count: neighbors.length }); } catch (err: any) { if (err?.message === 'invalid_profile') { @@ -353,14 +384,14 @@ async function main() { app.get('/v1/graph/paths', (req, res) => { try { - const { from, to, maxDepth, profile, profileId } = req.query; + const { from, to, maxDepth, profile } = req.query; if (!from || typeof from !== 'string' || !to || typeof to !== 'string') { return res.status(400).json({ error: 'from and to query parameters are required' }); } const paths = store.graph.findPaths(from, to, { maxDepth: maxDepth ? Number(maxDepth) : 3, - profile: (profile as string) ?? (profileId as string), + profile: profile as string, }); res.json({ from, to, paths, count: paths.length }); @@ -374,8 +405,8 @@ async function main() { app.get('/v1/graph/visualization', (req, res) => { try { - const { entity, maxDepth, maxNodes, profile, profileId, includeHistory } = req.query; - const profileValue = (profile as string) ?? (profileId as string); + const { entity, maxDepth, maxNodes, profile, includeHistory } = req.query; + const profileValue = profile as string; const normalizedProfile = normalizeProfileSlug(profileValue); const centerEntity = (entity as string) || null; const depth = maxDepth ? Number(maxDepth) : 2; diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 0a611bf..c42ecc7 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -7,9 +7,11 @@ import type { MemoryRecord, ProceduralMemoryRecord, SemanticMemoryRecord, + ReflectiveMemoryRecord, QueryResult, SectorName, - ConsolidationAction, + SemanticTripleInput, + ReflectiveInput, } from './types.js'; import { determineConsolidationAction } from './consolidation.js'; import { PBWM_SECTOR_WEIGHTS, scoreRowPBWM } from './scoring.js'; @@ -17,12 +19,7 @@ import { cosineSimilarity, DEFAULT_PROFILE, normalizeProfileSlug } from './utils import { filterAndCapWorkingMemory } from './wm.js'; import { SemanticGraphTraversal } from './semantic-graph.js'; -const SECTORS: SectorName[] = ['episodic', 'semantic', 'procedural']; -const DEFAULT_WEIGHTS: Record = { - episodic: 1, - semantic: 1, - procedural: 1, -}; +const SECTORS: SectorName[] = ['episodic', 'semantic', 'procedural', 'reflective']; const PER_SECTOR_K = 4; const WORKING_MEMORY_CAP = 8; const SECTOR_SCAN_LIMIT = 200; // simple scan instead of ANN for v0 @@ -47,125 +44,193 @@ export class SqliteMemoryStore { this.ensureProfileExists(profileId); const createdAt = this.now(); const rows: MemoryRecord[] = []; - const normalized = normalizeComponents(components); const source = options?.source; const dedup = options?.deduplicate === true; + const origin = options?.origin; + const userId = options?.userId; - for (const [sector, content] of Object.entries(normalized) as Array<[SectorName, string | any]>) { - // Skip empty content - if (!content) continue; - if (typeof content === 'string' && !content.trim()) continue; - // For procedural, check trigger; for semantic, check if object has valid fields - if (sector === 'procedural' && typeof content === 'object' && !content.trigger?.trim()) continue; - if (sector === 'semantic' && typeof content === 'object') { - // Skip if all semantic fields are empty - if (!content.subject?.trim() && !content.predicate?.trim() && !content.object?.trim()) continue; + // Upsert into agent_users when userId is provided + if (userId) { + this.upsertAgentUser(profileId, userId); + } + + // --- Episodic --- + const episodic = components.episodic; + if (episodic && typeof episodic === 'string' && episodic.trim()) { + const embedding = await this.embed(episodic, 'episodic'); + + if (dedup) { + const existingDup = this.findDuplicateMemory(embedding, 'episodic', profileId, 0.9); + if (existingDup) { + if (source && !existingDup.source) { + this.setMemorySource(existingDup.id, source); + } + rows.push({ + id: existingDup.id, + sector: 'episodic', + content: existingDup.content, + embedding, + profileId, + createdAt: existingDup.createdAt, + lastAccessed: existingDup.lastAccessed, + source: existingDup.source ?? source, + }); + } else { + const row = this.buildEpisodicRow(episodic, embedding, profileId, createdAt, source, origin, userId); + this.insertRow(row); + rows.push(row); + } + } else { + const row = this.buildEpisodicRow(episodic, embedding, profileId, createdAt, source, origin, userId); + this.insertRow(row); + rows.push(row); } + } - // Prepare content for embedding and storage - let textToEmbed = ''; - let procRow: ProceduralMemoryRecord | undefined; - let semanticRow: SemanticMemoryRecord | undefined; - - if (sector === 'procedural') { - if (typeof content === 'string') { - textToEmbed = content; - procRow = { - id: randomUUID(), - trigger: content, + // --- Semantic (array of triples) --- + const semanticInput = components.semantic; + const triples = this.normalizeSemanticInput(semanticInput); + + for (const triple of triples) { + const textToEmbed = `${triple.subject} ${triple.predicate} ${triple.object}`; + const embedding = await this.embed(textToEmbed, 'semantic'); + const domain = triple.domain ?? 'world'; + const userScope = domain === 'user' ? (userId ?? null) : null; + + const semanticRow: SemanticMemoryRecord = { + id: randomUUID(), + subject: triple.subject, + predicate: triple.predicate, + object: triple.object, + profileId, + embedding, + validFrom: createdAt, + validTo: null, + createdAt, + updatedAt: createdAt, + strength: 1.0, + source, + domain, + originType: origin?.originType, + originActor: origin?.originActor ?? userId, + originRef: origin?.originRef, + userScope, + }; + + // Consolidation + const allFactsForSubject = this.findActiveFactsForSubject(triple.subject, profileId); + const matchingFacts = await this.findSemanticallyMatchingFacts( + triple.predicate, + allFactsForSubject, + 0.9, + ); + const action = determineConsolidationAction( + { subject: triple.subject, predicate: triple.predicate, object: triple.object }, + matchingFacts, + ); + + switch (action.type) { + case 'merge': + if (dedup) { + if (source) this.setSemanticSource(action.targetId, source); + } else { + this.strengthenFact(action.targetId); + } + rows.push({ + id: action.targetId, + sector: 'semantic', + content: triple.object, + embedding, profileId, - goal: '', - context: '', - result: '', - steps: [content], - embedding: [], // Will be set later createdAt, lastAccessed: createdAt, + eventStart: null, + eventEnd: null, source, - }; - } else { - textToEmbed = content.trigger; - procRow = { - id: randomUUID(), - trigger: content.trigger, + }); + break; + + case 'supersede': + this.supersedeFact(action.targetId); + // Fall through to insert + + case 'insert': + this.insertSemanticRow(semanticRow); + rows.push({ + id: semanticRow.id, + sector: 'semantic', + content: triple.object, + embedding, profileId, - goal: content.goal ?? '', - context: content.context ?? '', - result: content.result ?? '', - steps: Array.isArray(content.steps) ? content.steps : [], - embedding: [], // Will be set later createdAt, lastAccessed: createdAt, + eventStart: null, + eventEnd: null, source, - }; - } - } else { - textToEmbed = content as string; - if (sector === 'semantic') { - if (typeof content === 'string') { - // fallback: wrap string into a generic fact - semanticRow = { - id: randomUUID(), - subject: 'User', - predicate: 'statement', - object: textToEmbed, - profileId, - embedding: [], - validFrom: createdAt, - validTo: null, - createdAt, - updatedAt: createdAt, - strength: 1.0, - source, - }; - } else { - // Ensure all three fields are populated for semantic triples - const subject = content.subject?.trim() || 'User'; - const predicate = content.predicate?.trim() || 'hasProperty'; - const object = content.object?.trim(); - - // Skip if object is missing (required for valid triple) - if (!object) { - continue; - } - - semanticRow = { - id: randomUUID(), - subject, - predicate, - object, - profileId, - embedding: [], - validFrom: createdAt, - validTo: null, - createdAt, - updatedAt: createdAt, - strength: 1.0, - source, - }; - // Embed the full triple to capture semantic relationships, not just the object - textToEmbed = `${subject} ${predicate} ${object}`; - } - } + }); + break; } + } - const embedding = await this.embed(textToEmbed, sector); + // --- Procedural --- + const procInput = components.procedural; + if (procInput) { + let textToEmbed = ''; + let procRow: ProceduralMemoryRecord | undefined; - if (sector === 'procedural' && procRow) { + if (typeof procInput === 'string' && procInput.trim()) { + textToEmbed = procInput; + procRow = { + id: randomUUID(), + trigger: procInput, + profileId, + goal: '', + context: '', + result: '', + steps: [procInput], + embedding: [], + createdAt, + lastAccessed: createdAt, + source, + originType: origin?.originType, + originActor: origin?.originActor ?? userId, + originRef: origin?.originRef, + userScope: userId ?? null, + }; + } else if (typeof procInput === 'object' && procInput.trigger?.trim()) { + textToEmbed = procInput.trigger; + procRow = { + id: randomUUID(), + trigger: procInput.trigger, + profileId, + goal: procInput.goal ?? '', + context: procInput.context ?? '', + result: procInput.result ?? '', + steps: Array.isArray(procInput.steps) ? procInput.steps : [], + embedding: [], + createdAt, + lastAccessed: createdAt, + source, + originType: origin?.originType, + originActor: origin?.originActor ?? userId, + originRef: origin?.originRef, + userScope: userId ?? null, + }; + } + + if (textToEmbed && procRow) { + const embedding = await this.embed(textToEmbed, 'procedural'); procRow.embedding = embedding; - procRow.profileId = profileId; - // Dedup: check for near-duplicate triggers if (dedup) { const existingDup = this.findDuplicateProcedural(embedding, profileId, 0.9); if (existingDup) { - // Optionally add source attribution to existing record if (source && !existingDup.source) { this.setProceduralSource(existingDup.id, source); } rows.push({ id: existingDup.id, - sector, + sector: 'procedural', content: existingDup.trigger, embedding, profileId, @@ -173,132 +238,73 @@ export class SqliteMemoryStore { lastAccessed: existingDup.lastAccessed, source: existingDup.source ?? source, }); - continue; - } - } - - this.insertProceduralRow(procRow); - rows.push({ - id: procRow.id, - sector, - content: procRow.trigger, - embedding, - profileId, - createdAt, - lastAccessed: createdAt, - source, - }); - } else if (sector === 'semantic' && semanticRow) { - semanticRow.embedding = embedding; - semanticRow.profileId = profileId; - - // Consolidation: find semantically similar predicates (0.9 threshold) - const allFactsForSubject = this.findActiveFactsForSubject( - semanticRow.subject, - profileId - ); - const matchingFacts = await this.findSemanticallyMatchingFacts( - semanticRow.predicate, - allFactsForSubject, - 0.9 // Semantic similarity threshold - ); - const action = determineConsolidationAction( - { subject: semanticRow.subject, predicate: semanticRow.predicate, object: semanticRow.object }, - matchingFacts - ); - - switch (action.type) { - case 'merge': - if (dedup) { - // Document ingestion: skip strength bump, just add source attribution - if (source) { - this.setSemanticSource(action.targetId, source); - } - } else { - // Chat ingestion: strengthen as before - this.strengthenFact(action.targetId); - } - rows.push({ - id: action.targetId, - sector, - content: semanticRow.object, - embedding, - profileId, - createdAt, - lastAccessed: createdAt, - eventStart: null, - eventEnd: null, - source, - }); - break; - - case 'supersede': - // Different object: close old fact, then insert new - this.supersedeFact(action.targetId); - // Fall through to insert - - case 'insert': - // Insert new semantic triple - this.insertSemanticRow(semanticRow); + } else { + this.insertProceduralRow(procRow); rows.push({ - id: semanticRow.id, - sector, - content: semanticRow.object, + id: procRow.id, + sector: 'procedural', + content: procRow.trigger, embedding, profileId, createdAt, lastAccessed: createdAt, - eventStart: null, - eventEnd: null, source, }); - break; - } - } else { - // Episodic - if (dedup) { - const existingDup = this.findDuplicateMemory(embedding, sector as SectorName, profileId, 0.9); - if (existingDup) { - // Optionally add source attribution to existing record - if (source && !existingDup.source) { - this.setMemorySource(existingDup.id, source); - } - rows.push({ - id: existingDup.id, - sector: sector as SectorName, - content: existingDup.content, - embedding, - profileId, - createdAt: existingDup.createdAt, - lastAccessed: existingDup.lastAccessed, - source: existingDup.source ?? source, - }); - continue; } + } else { + this.insertProceduralRow(procRow); + rows.push({ + id: procRow.id, + sector: 'procedural', + content: procRow.trigger, + embedding, + profileId, + createdAt, + lastAccessed: createdAt, + source, + }); } - - const row: MemoryRecord = { - id: randomUUID(), - sector, - content: textToEmbed, - embedding, - profileId, - createdAt, - lastAccessed: createdAt, - eventStart: sector === 'episodic' ? createdAt : null, - eventEnd: null, - source, - }; - this.insertRow(row); - rows.push(row); } } + + // --- Reflective --- + const reflectiveInput = components.reflective; + const reflections = this.normalizeReflectiveInput(reflectiveInput); + + for (const ref of reflections) { + const embedding = await this.embed(ref.observation, 'reflective'); + const refRow: ReflectiveMemoryRecord = { + id: randomUUID(), + observation: ref.observation, + profileId, + embedding, + createdAt, + lastAccessed: createdAt, + source, + originType: origin?.originType, + originActor: origin?.originActor ?? userId, + originRef: origin?.originRef, + }; + this.insertReflectiveRow(refRow); + rows.push({ + id: refRow.id, + sector: 'reflective', + content: ref.observation, + embedding, + profileId, + createdAt, + lastAccessed: createdAt, + source, + }); + } + return rows; } async query( queryText: string, profile?: string, + userId?: string, ): Promise<{ workingMemory: QueryResult[]; perSector: Record; profileId: string }> { const profileId = normalizeProfileSlug(profile); this.ensureProfileExists(profileId); @@ -311,45 +317,11 @@ export class SqliteMemoryStore { episodic: [], semantic: [], procedural: [], + reflective: [], }; for (const sector of SECTORS) { - const candidates = - sector === 'procedural' - ? this.getProceduralRows(profileId, SECTOR_SCAN_LIMIT).map((r) => ({ - id: r.id, - sector: 'procedural' as SectorName, - content: r.trigger, - embedding: r.embedding, - profileId: r.profileId, - createdAt: r.createdAt, - lastAccessed: r.lastAccessed, - details: { - trigger: r.trigger, - goal: r.goal, - context: r.context, - result: r.result, - steps: r.steps, - }, - })) - : sector === 'semantic' - ? this.getSemanticRows(profileId, SECTOR_SCAN_LIMIT).map((r) => ({ - id: r.id, - sector: 'semantic' as SectorName, - content: `${r.subject} ${r.predicate} ${r.object}`, - embedding: r.embedding ?? [], - profileId: r.profileId, - createdAt: r.createdAt, - lastAccessed: r.updatedAt, - details: { - subject: r.subject, - predicate: r.predicate, - object: r.object, - validFrom: r.validFrom, - validTo: r.validTo, - }, - })) - : this.getRowsForSector(sector, profileId, SECTOR_SCAN_LIMIT); + const candidates = this.getCandidatesForSector(sector, profileId, userId); const scored = candidates .filter((row) => cosineSimilarity(queryEmbeddings[sector], row.embedding) >= 0.2) .map((row) => scoreRowPBWM(row, queryEmbeddings[sector], PBWM_SECTOR_WEIGHTS[sector])) @@ -364,7 +336,7 @@ export class SqliteMemoryStore { details: (row as any).details, })); perSectorResults[sector] = scored; - this.touchRows(scored.map((r) => r.id)); + this.touchRows(scored.map((r) => r.id), sector); } const workingMemory = filterAndCapWorkingMemory(perSectorResults, WORKING_MEMORY_CAP); @@ -400,6 +372,14 @@ export class SqliteMemoryStore { ) .get({ profileId }) as { sector: SectorName; count: number; lastCreatedAt: number | null }; + const reflectiveRow = this.db + .prepare( + `select 'reflective' as sector, count(*) as count, max(created_at) as lastCreatedAt + from reflective_memory + where profile_id = @profileId`, + ) + .get({ profileId }) as { sector: SectorName; count: number; lastCreatedAt: number | null }; + const defaults = SECTORS.map((s) => ({ sector: s, count: 0, @@ -409,6 +389,7 @@ export class SqliteMemoryStore { const map = new Map(rows.map((r) => [r.sector, r])); if (proceduralRow) map.set('procedural', proceduralRow); if (semanticRow) map.set('semantic', semanticRow); + if (reflectiveRow) map.set('reflective', reflectiveRow); return defaults.map((d) => map.get(d.sector) ?? d); } @@ -428,11 +409,17 @@ export class SqliteMemoryStore { where profile_id = @profileId union all select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, - json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'metadata', metadata) as details, + json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'metadata', metadata, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount from semantic_memory where profile_id = @profileId and (valid_to is null or valid_to > @now) + union all + select id, 'reflective' as sector, observation as content, embedding, created_at as createdAt, last_accessed as lastAccessed, + '{}' as details, + null as eventStart, null as eventEnd, 0 as retrievalCount + from reflective_memory + where profile_id = @profileId order by createdAt desc limit @limit`, ) @@ -447,7 +434,7 @@ export class SqliteMemoryStore { eventStart: row.eventStart ?? null, eventEnd: row.eventEnd ?? null, }; - + // Ensure steps is always an array if it exists if (parsed.details && parsed.details.steps) { if (typeof parsed.details.steps === 'string') { @@ -461,18 +448,184 @@ export class SqliteMemoryStore { parsed.details.steps = []; } } - + return parsed; }); } + // --- Agent Users --- + + upsertAgentUser(agentId: string, userId: string): void { + const now = this.now(); + this.db + .prepare( + `INSERT INTO agent_users (agent_id, user_id, first_seen, last_seen, interaction_count) + VALUES (@agentId, @userId, @now, @now, 1) + ON CONFLICT(agent_id, user_id) DO UPDATE SET + last_seen = @now, + interaction_count = interaction_count + 1`, + ) + .run({ agentId, userId, now }); + } + + getAgentUsers(agentId: string): Array<{ userId: string; firstSeen: number; lastSeen: number; interactionCount: number }> { + return this.db + .prepare( + `SELECT user_id as userId, first_seen as firstSeen, last_seen as lastSeen, interaction_count as interactionCount + FROM agent_users + WHERE agent_id = @agentId + ORDER BY last_seen DESC`, + ) + .all({ agentId }) as Array<{ userId: string; firstSeen: number; lastSeen: number; interactionCount: number }>; + } + + getMemoriesForUser(profile: string, userId: string, limit: number = 50): (MemoryRecord & { details?: any })[] { + const profileId = normalizeProfileSlug(profile); + this.ensureProfileExists(profileId); + const rows = this.db + .prepare( + `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount + from memory + where profile_id = @profileId and user_scope = @userId + union all + select id, 'procedural' as sector, trigger as content, embedding, created_at as createdAt, last_accessed as lastAccessed, + json_object('trigger', trigger, 'goal', goal, 'context', context, 'result', result, 'steps', json(steps)) as details, + null as eventStart, null as eventEnd, 0 as retrievalCount + from procedural_memory + where profile_id = @profileId and user_scope = @userId + union all + select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, + json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'domain', domain) as details, + null as eventStart, null as eventEnd, 0 as retrievalCount + from semantic_memory + where profile_id = @profileId and user_scope = @userId + and (valid_to is null or valid_to > @now) + union all + select id, 'reflective' as sector, observation as content, embedding, created_at as createdAt, last_accessed as lastAccessed, + '{}' as details, + null as eventStart, null as eventEnd, 0 as retrievalCount + from reflective_memory + where profile_id = @profileId and origin_actor = @userId + order by createdAt desc + limit @limit`, + ) + .all({ profileId, userId, limit, now: this.now() }) as Array & { details: string }>; + + return rows.map((row) => ({ + ...row, + profileId, + embedding: JSON.parse((row as any).embedding) as number[], + details: row.details ? JSON.parse(row.details) : undefined, + eventStart: row.eventStart ?? null, + eventEnd: row.eventEnd ?? null, + })); + } + + // --- Reflective --- + + insertReflectiveRow(row: ReflectiveMemoryRecord): void { + this.db + .prepare( + `INSERT INTO reflective_memory ( + id, observation, embedding, created_at, last_accessed, profile_id, source, + origin_type, origin_actor, origin_ref + ) VALUES ( + @id, @observation, json(@embedding), @createdAt, @lastAccessed, @profileId, @source, + @originType, @originActor, @originRef + )`, + ) + .run({ + id: row.id, + observation: row.observation, + embedding: JSON.stringify(row.embedding), + createdAt: row.createdAt, + lastAccessed: row.lastAccessed, + profileId: row.profileId ?? DEFAULT_PROFILE, + source: row.source ?? null, + originType: row.originType ?? null, + originActor: row.originActor ?? null, + originRef: row.originRef ?? null, + }); + } + + getReflectiveRows(profileId: string, limit: number): ReflectiveMemoryRecord[] { + const rows = this.db + .prepare( + `SELECT id, observation, embedding, created_at as createdAt, last_accessed as lastAccessed, + profile_id as profileId, source, origin_type as originType, origin_actor as originActor, origin_ref as originRef + FROM reflective_memory + WHERE profile_id = @profileId + ORDER BY last_accessed DESC + LIMIT @limit`, + ) + .all({ profileId, limit }) as any[]; + + return rows.map((row: any) => ({ + ...row, + embedding: JSON.parse(row.embedding) as number[], + })); + } + + private buildEpisodicRow( + content: string, + embedding: number[], + profileId: string, + createdAt: number, + source?: string, + origin?: { originType?: string; originActor?: string; originRef?: string }, + userId?: string, + ): MemoryRecord { + return { + id: randomUUID(), + sector: 'episodic' as SectorName, + content, + embedding, + profileId, + createdAt, + lastAccessed: createdAt, + eventStart: createdAt, + eventEnd: null, + source, + originType: origin?.originType, + originActor: origin?.originActor ?? userId, + originRef: origin?.originRef, + userScope: userId ?? null, + }; + } + + private normalizeSemanticInput(input: IngestComponents['semantic']): SemanticTripleInput[] { + if (!input) return []; + if (Array.isArray(input)) { + return input.filter((t) => t.subject?.trim() && t.predicate?.trim() && t.object?.trim()); + } + // Single triple object + if (input.subject?.trim() && input.predicate?.trim() && input.object?.trim()) { + return [input]; + } + return []; + } + + private normalizeReflectiveInput(input: IngestComponents['reflective']): ReflectiveInput[] { + if (!input) return []; + if (Array.isArray(input)) { + return input.filter((r) => r.observation?.trim()); + } + // Single object + if (input.observation?.trim()) { + return [input]; + } + return []; + } + private insertRow(row: MemoryRecord) { this.db .prepare( `insert into memory ( - id, sector, content, embedding, created_at, last_accessed, event_start, event_end, retrieval_count, profile_id, source + id, sector, content, embedding, created_at, last_accessed, event_start, event_end, retrieval_count, profile_id, source, + origin_type, origin_actor, origin_ref, user_scope ) values ( - @id, @sector, @content, json(@embedding), @createdAt, @lastAccessed, @eventStart, @eventEnd, @retrievalCount, @profileId, @source + @id, @sector, @content, json(@embedding), @createdAt, @lastAccessed, @eventStart, @eventEnd, @retrievalCount, @profileId, @source, + @originType, @originActor, @originRef, @userScope )`, ) .run({ @@ -487,6 +640,10 @@ export class SqliteMemoryStore { retrievalCount: (row as any).retrievalCount ?? DEFAULT_RETRIEVAL_COUNT, profileId: row.profileId ?? DEFAULT_PROFILE, source: row.source ?? null, + originType: row.originType ?? null, + originActor: row.originActor ?? null, + originRef: row.originRef ?? null, + userScope: row.userScope ?? null, }); } @@ -494,9 +651,11 @@ export class SqliteMemoryStore { this.db .prepare( `insert into procedural_memory ( - id, trigger, goal, context, result, steps, embedding, created_at, last_accessed, profile_id, source + id, trigger, goal, context, result, steps, embedding, created_at, last_accessed, profile_id, source, + origin_type, origin_actor, origin_ref, user_scope ) values ( - @id, @trigger, @goal, @context, @result, json(@steps), json(@embedding), @createdAt, @lastAccessed, @profileId, @source + @id, @trigger, @goal, @context, @result, json(@steps), json(@embedding), @createdAt, @lastAccessed, @profileId, @source, + @originType, @originActor, @originRef, @userScope )`, ) .run({ @@ -511,6 +670,10 @@ export class SqliteMemoryStore { lastAccessed: row.lastAccessed, profileId: row.profileId ?? DEFAULT_PROFILE, source: row.source ?? null, + originType: row.originType ?? null, + originActor: row.originActor ?? null, + originRef: row.originRef ?? null, + userScope: row.userScope ?? null, }); } @@ -518,9 +681,11 @@ export class SqliteMemoryStore { this.db .prepare( `insert into semantic_memory ( - id, subject, predicate, object, valid_from, valid_to, created_at, updated_at, embedding, metadata, profile_id, strength, source + id, subject, predicate, object, valid_from, valid_to, created_at, updated_at, embedding, metadata, profile_id, strength, source, + domain, origin_type, origin_actor, origin_ref, user_scope ) values ( - @id, @subject, @predicate, @object, @validFrom, @validTo, @createdAt, @updatedAt, json(@embedding), json(@metadata), @profileId, @strength, @source + @id, @subject, @predicate, @object, @validFrom, @validTo, @createdAt, @updatedAt, json(@embedding), json(@metadata), @profileId, @strength, @source, + @domain, @originType, @originActor, @originRef, @userScope )`, ) .run({ @@ -537,6 +702,11 @@ export class SqliteMemoryStore { profileId: row.profileId ?? DEFAULT_PROFILE, strength: row.strength ?? 1.0, source: row.source ?? null, + domain: row.domain ?? null, + originType: row.originType ?? null, + originActor: row.originActor ?? null, + originRef: row.originRef ?? null, + userScope: row.userScope ?? null, }); } @@ -587,17 +757,55 @@ export class SqliteMemoryStore { this.db.prepare('UPDATE semantic_memory SET source = @source WHERE id = @id').run({ id, source }); } - private getRowsForSector(sector: SectorName, profileId: string, limit: number): MemoryRecord[] { - const rows = this.db - .prepare( - `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, - event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, profile_id as profileId, source - from memory - where sector = @sector and profile_id = @profileId - order by last_accessed desc - limit @limit`, - ) - .all({ sector, limit, profileId }) as Array>; + private getCandidatesForSector(sector: SectorName, profileId: string, userId?: string): MemoryRecord[] { + switch (sector) { + case 'procedural': + return this.getProceduralRows(profileId, SECTOR_SCAN_LIMIT, userId).map((r) => ({ + id: r.id, + sector: 'procedural' as SectorName, + content: r.trigger, + embedding: r.embedding, + profileId: r.profileId, + createdAt: r.createdAt, + lastAccessed: r.lastAccessed, + details: { trigger: r.trigger, goal: r.goal, context: r.context, result: r.result, steps: r.steps }, + } as any)); + case 'semantic': + return this.getSemanticRows(profileId, SECTOR_SCAN_LIMIT, userId).map((r) => ({ + id: r.id, + sector: 'semantic' as SectorName, + content: `${r.subject} ${r.predicate} ${r.object}`, + embedding: r.embedding ?? [], + profileId: r.profileId, + createdAt: r.createdAt, + lastAccessed: r.updatedAt, + details: { subject: r.subject, predicate: r.predicate, object: r.object, validFrom: r.validFrom, validTo: r.validTo, domain: r.domain }, + } as any)); + case 'reflective': + return this.getReflectiveRows(profileId, SECTOR_SCAN_LIMIT).map((r) => ({ + id: r.id, + sector: 'reflective' as SectorName, + content: r.observation, + embedding: r.embedding, + profileId: r.profileId, + createdAt: r.createdAt, + lastAccessed: r.lastAccessed, + })); + default: + return this.getRowsForSector(sector, profileId, SECTOR_SCAN_LIMIT, userId); + } + } + + private getRowsForSector(sector: SectorName, profileId: string, limit: number, userId?: string): MemoryRecord[] { + const userFilter = userId ? 'and (user_scope is null or user_scope = @userId)' : ''; + const rows = this.db.prepare( + `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, + event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, profile_id as profileId, source + from memory + where sector = @sector and profile_id = @profileId ${userFilter} + order by last_accessed desc + limit @limit`, + ).all({ sector, limit, profileId, userId }) as Array>; return rows.map((row) => ({ ...row, @@ -605,39 +813,38 @@ export class SqliteMemoryStore { })); } - private getSemanticRows(profileId: string, limit: number): SemanticMemoryRecord[] { + private getSemanticRows(profileId: string, limit: number, userId?: string): SemanticMemoryRecord[] { const now = this.now(); - const rows = this.db - .prepare( - `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, - created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId, - strength - from semantic_memory - where profile_id = @profileId - and (valid_to is null or valid_to > @now) - order by strength desc, updated_at desc - limit @limit`, - ) - .all({ limit, profileId, now }) as any[]; - - return rows.map((row) => ({ + const userFilter = userId ? 'and (user_scope is null or user_scope = @userId)' : ''; + const rows = this.db.prepare( + `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, + created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId, + strength, domain + from semantic_memory + where profile_id = @profileId + and (valid_to is null or valid_to > @now) + ${userFilter} + order by strength desc, updated_at desc + limit @limit`, + ).all({ limit, profileId, now, userId }) as any[]; + + return rows.map((row: any) => ({ ...row, - embedding: JSON.parse((row as any).embedding) as number[], - metadata: row.metadata ? JSON.parse((row as any).metadata) : undefined, + embedding: JSON.parse(row.embedding) as number[], + metadata: row.metadata ? JSON.parse(row.metadata) : undefined, strength: row.strength ?? 1.0, })); } - private getProceduralRows(profileId: string, limit: number): ProceduralMemoryRecord[] { - const rows = this.db - .prepare( - `select id, trigger, goal, context, result, steps, embedding, created_at as createdAt, last_accessed as lastAccessed, profile_id as profileId, source - from procedural_memory - where profile_id = @profileId - order by last_accessed desc - limit @limit`, - ) - .all({ limit, profileId }) as Array>; + private getProceduralRows(profileId: string, limit: number, userId?: string): ProceduralMemoryRecord[] { + const userFilter = userId ? 'and (user_scope is null or user_scope = @userId)' : ''; + const rows = this.db.prepare( + `select id, trigger, goal, context, result, steps, embedding, created_at as createdAt, last_accessed as lastAccessed, profile_id as profileId, source + from procedural_memory + where profile_id = @profileId ${userFilter} + order by last_accessed desc + limit @limit`, + ).all({ limit, profileId, userId }) as Array>; return rows.map((row) => ({ ...row, @@ -646,16 +853,22 @@ export class SqliteMemoryStore { })); } - private touchRows(ids: string[]) { + private touchRows(ids: string[], sector?: SectorName) { if (!ids.length) return; const placeholders = ids.map(() => '?').join(','); + const now = this.now(); + + // Each sector lives in its own table + const table = + sector === 'procedural' ? 'procedural_memory' + : sector === 'semantic' ? 'semantic_memory' + : sector === 'reflective' ? 'reflective_memory' + : 'memory'; + const timeCol = sector === 'semantic' ? 'updated_at' : 'last_accessed'; + this.db - .prepare( - `update memory - set last_accessed = ? - where id in (${placeholders})`, - ) - .run(this.now(), ...ids); + .prepare(`update ${table} set ${timeCol} = ? where id in (${placeholders})`) + .run(now, ...ids); } private prepareSchema() { @@ -715,11 +928,46 @@ export class SqliteMemoryStore { .run(); this.db.prepare('create index if not exists idx_semantic_subject_pred on semantic_memory(subject, predicate)').run(); this.db.prepare('create index if not exists idx_semantic_object on semantic_memory(object)').run(); + + // New tables + this.db + .prepare( + `create table if not exists reflective_memory ( + id text primary key, + observation text not null, + embedding json not null, + created_at integer not null, + last_accessed integer not null, + profile_id text not null default '${DEFAULT_PROFILE}', + source text, + origin_type text, + origin_actor text, + origin_ref text + )`, + ) + .run(); + this.db.prepare('create index if not exists idx_reflective_profile on reflective_memory(profile_id, last_accessed)').run(); + + this.db + .prepare( + `create table if not exists agent_users ( + agent_id text not null, + user_id text not null, + first_seen integer not null, + last_seen integer not null, + interaction_count integer not null default 1, + primary key (agent_id, user_id) + )`, + ) + .run(); + this.db.prepare('create index if not exists idx_agent_users_agent on agent_users(agent_id)').run(); + this.ensureProfileColumns(); this.ensureProfileIndexes(); this.ensureProfilesTable(); this.ensureStrengthColumn(); this.ensureSourceColumn(); + this.ensureAttributionColumns(); } /** @@ -751,6 +999,43 @@ export class SqliteMemoryStore { } } + /** + * Add attribution and user_scope columns to all memory tables. + * origin_type, origin_actor, origin_ref track where a memory came from. + * user_scope limits visibility to a specific user. + * domain on semantic_memory classifies facts. + */ + private ensureAttributionColumns() { + const tablesWithUserScope = ['memory', 'procedural_memory', 'semantic_memory']; + for (const table of tablesWithUserScope) { + const columns = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>; + if (!columns.some((c) => c.name === 'origin_type')) { + this.db.prepare(`ALTER TABLE ${table} ADD COLUMN origin_type TEXT DEFAULT NULL`).run(); + } + if (!columns.some((c) => c.name === 'origin_actor')) { + this.db.prepare(`ALTER TABLE ${table} ADD COLUMN origin_actor TEXT DEFAULT NULL`).run(); + } + if (!columns.some((c) => c.name === 'origin_ref')) { + this.db.prepare(`ALTER TABLE ${table} ADD COLUMN origin_ref TEXT DEFAULT NULL`).run(); + } + if (!columns.some((c) => c.name === 'user_scope')) { + this.db.prepare(`ALTER TABLE ${table} ADD COLUMN user_scope TEXT DEFAULT NULL`).run(); + } + } + + // Domain column on semantic_memory only + const semCols = this.db.prepare('PRAGMA table_info(semantic_memory)').all() as Array<{ name: string }>; + if (!semCols.some((c) => c.name === 'domain')) { + this.db.prepare("ALTER TABLE semantic_memory ADD COLUMN domain TEXT DEFAULT NULL").run(); + } + + // Indexes for user_scope filtering + this.db.prepare('CREATE INDEX IF NOT EXISTS idx_memory_user_scope ON memory(user_scope)').run(); + this.db.prepare('CREATE INDEX IF NOT EXISTS idx_proc_user_scope ON procedural_memory(user_scope)').run(); + this.db.prepare('CREATE INDEX IF NOT EXISTS idx_semantic_user_scope ON semantic_memory(user_scope)').run(); + this.db.prepare('CREATE INDEX IF NOT EXISTS idx_semantic_domain ON semantic_memory(domain)').run(); + } + /** * Find all active (non-expired) facts for a subject. * Used for semantic similarity matching during consolidation. @@ -764,7 +1049,7 @@ export class SqliteMemoryStore { .prepare( `SELECT id, predicate, object, updated_at as updatedAt FROM semantic_memory - WHERE subject = @subject + WHERE subject = @subject AND profile_id = @profileId AND (valid_to IS NULL OR valid_to > @now) ORDER BY updated_at DESC` @@ -786,22 +1071,22 @@ export class SqliteMemoryStore { // Embed the new predicate const newPredicateEmbedding = await this.embed(newPredicate, 'semantic'); - + // Get unique predicates to embed (avoid duplicate embedding calls) const uniquePredicates = [...new Set(existingFacts.map(f => f.predicate))]; const predicateEmbeddings = new Map(); - + for (const pred of uniquePredicates) { predicateEmbeddings.set(pred, await this.embed(pred, 'semantic')); } // Filter facts by predicate similarity const matchingFacts: Array<{ id: string; object: string; updatedAt: number; similarity: number }> = []; - + for (const fact of existingFacts) { const factPredicateEmbedding = predicateEmbeddings.get(fact.predicate)!; const similarity = cosineSimilarity(newPredicateEmbedding, factPredicateEmbedding); - + if (similarity >= threshold) { matchingFacts.push({ id: fact.id, @@ -828,8 +1113,8 @@ export class SqliteMemoryStore { strengthenFact(id: string, delta: number = 1.0): void { this.db .prepare( - `UPDATE semantic_memory - SET strength = strength + @delta, + `UPDATE semantic_memory + SET strength = strength + @delta, updated_at = @now WHERE id = @id` ) @@ -843,7 +1128,7 @@ export class SqliteMemoryStore { supersedeFact(id: string): void { this.db .prepare( - `UPDATE semantic_memory + `UPDATE semantic_memory SET valid_to = @now, updated_at = @now WHERE id = @id` @@ -922,10 +1207,10 @@ export class SqliteMemoryStore { } const targetSector = sector ?? existing.sector; - + // Regenerate embedding with new content const embedding = await this.embed(content, targetSector); - + // Update the record const stmt = this.db.prepare( 'update memory set content = ?, sector = ?, embedding = json(?), last_accessed = ? where id = ? and profile_id = ?' @@ -938,7 +1223,7 @@ export class SqliteMemoryStore { id, profileId, ); - + return res.changes > 0; } @@ -948,7 +1233,8 @@ export class SqliteMemoryStore { const res1 = this.db.prepare('delete from memory where id = ? and profile_id = ?').run(id, profileId); const res2 = this.db.prepare('delete from procedural_memory where id = ? and profile_id = ?').run(id, profileId); const res3 = this.db.prepare('delete from semantic_memory where id = ? and profile_id = ?').run(id, profileId); - return (res1.changes ?? 0) + (res2.changes ?? 0) + (res3.changes ?? 0); + const res4 = this.db.prepare('delete from reflective_memory where id = ? and profile_id = ?').run(id, profileId); + return (res1.changes ?? 0) + (res2.changes ?? 0) + (res3.changes ?? 0) + (res4.changes ?? 0); } deleteAll(profile?: string): number { @@ -957,7 +1243,8 @@ export class SqliteMemoryStore { const res1 = this.db.prepare('delete from memory where profile_id = ?').run(profileId); const res2 = this.db.prepare('delete from procedural_memory where profile_id = ?').run(profileId); const res3 = this.db.prepare('delete from semantic_memory where profile_id = ?').run(profileId); - return (res1.changes ?? 0) + (res2.changes ?? 0) + (res3.changes ?? 0); + const res4 = this.db.prepare('delete from reflective_memory where profile_id = ?').run(profileId); + return (res1.changes ?? 0) + (res2.changes ?? 0) + (res3.changes ?? 0) + (res4.changes ?? 0); } deleteProfile(profile?: string): number { @@ -969,6 +1256,8 @@ export class SqliteMemoryStore { const deletedCount = this.deleteAll(profileId); // Delete the profile itself from the profiles table const res = this.db.prepare('delete from profiles where slug = ?').run(profileId); + // Delete agent_users entries for this profile + this.db.prepare('delete from agent_users where agent_id = ?').run(profileId); return deletedCount; } @@ -997,11 +1286,3 @@ export class SqliteMemoryStore { .run(this.now(), ...ids); } } - -function normalizeComponents(input: IngestComponents): Record { - return { - episodic: input.episodic ?? '', - semantic: input.semantic ?? '', - procedural: input.procedural ?? '', - }; -} diff --git a/memory/src/types.ts b/memory/src/types.ts index a41a4a9..f628cc5 100644 --- a/memory/src/types.ts +++ b/memory/src/types.ts @@ -1,12 +1,27 @@ -export type SectorName = 'episodic' | 'semantic' | 'procedural'; +export type SectorName = 'episodic' | 'semantic' | 'procedural' | 'reflective'; + +export type SemanticDomain = 'user' | 'world' | 'self'; + +export interface SemanticTripleInput { + subject: string; + predicate: string; + object: string; + domain?: SemanticDomain; +} + +export interface ReflectiveInput { + observation: string; +} + +export interface MemoryOrigin { + originType: 'conversation' | 'document' | 'api' | 'reflection'; + originActor?: string; // userId or system identifier + originRef?: string; // conversation id, document path, etc. +} export interface IngestComponents { episodic?: string; - semantic?: string | { - subject: string; - predicate: string; - object: string; - }; + semantic?: SemanticTripleInput | SemanticTripleInput[]; procedural?: string | { trigger: string; goal?: string; @@ -14,6 +29,7 @@ export interface IngestComponents { result?: string; context?: string; }; + reflective?: ReflectiveInput | ReflectiveInput[]; } export interface MemoryRecord { @@ -27,6 +43,10 @@ export interface MemoryRecord { eventStart?: number | null; eventEnd?: number | null; source?: string; + originType?: string; + originActor?: string; + originRef?: string; + userScope?: string | null; } export interface ProceduralMemoryRecord { @@ -41,6 +61,10 @@ export interface ProceduralMemoryRecord { createdAt: number; lastAccessed: number; source?: string; + originType?: string; + originActor?: string; + originRef?: string; + userScope?: string | null; } export interface SemanticMemoryRecord { @@ -57,6 +81,24 @@ export interface SemanticMemoryRecord { strength: number; // Evidence count from consolidation (starts at 1.0) source?: string; metadata?: Record; + domain?: SemanticDomain; + originType?: string; + originActor?: string; + originRef?: string; + userScope?: string | null; +} + +export interface ReflectiveMemoryRecord { + id: string; + observation: string; + profileId: string; + embedding: number[]; + createdAt: number; + lastAccessed: number; + source?: string; + originType?: string; + originActor?: string; + originRef?: string; } /** @@ -97,4 +139,6 @@ export interface GraphPath { export interface IngestOptions { source?: string; deduplicate?: boolean; + origin?: MemoryOrigin; + userId?: string; } diff --git a/package-lock.json b/package-lock.json index 61ad62c..7f8f09b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,7 +80,303 @@ "devDependencies": { "@types/express": "^4.17.21", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^3.0.0" + } + }, + "integrations/openrouter/node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "integrations/openrouter/node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "integrations/openrouter/node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "integrations/openrouter/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "integrations/openrouter/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "integrations/openrouter/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "integrations/openrouter/node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "integrations/openrouter/node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "integrations/openrouter/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "integrations/openrouter/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "integrations/openrouter/node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "integrations/openrouter/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "integrations/openrouter/node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "integrations/openrouter/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "integrations/openrouter/node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "integrations/openrouter/node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "integrations/openrouter/node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "integrations/openrouter/node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "integrations/openrouter/node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, "memory": { @@ -2544,6 +2840,27 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/chai/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2830,6 +3147,13 @@ "integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3581,6 +3905,69 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker/node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vitest/runner": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", @@ -5322,6 +5709,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5977,6 +6371,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -10437,6 +10841,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -10492,6 +10903,16 @@ "node": ">=14.0.0" } }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tinyspy": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", From ab9434fd31bab92d262895772e65f8b5eceea688 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:37:35 +0700 Subject: [PATCH 16/78] clean up and openrouter integration --- integrations/openrouter/src/memory.test.ts | 30 +++++++++++++++++++++- integrations/openrouter/src/memory.ts | 20 +++++++++------ integrations/openrouter/src/server.ts | 2 +- memory/src/providers/prompt.ts | 2 +- memory/src/sqlite-store.ts | 10 ++++---- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/integrations/openrouter/src/memory.test.ts b/integrations/openrouter/src/memory.test.ts index 13dafa1..558d5d9 100644 --- a/integrations/openrouter/src/memory.test.ts +++ b/integrations/openrouter/src/memory.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { formatMemoryBlock } from './memory.js'; +import { formatMemoryBlock, injectMemory } from './memory.js'; describe('formatMemoryBlock', () => { it('formats semantic facts under "What I know:"', () => { @@ -111,3 +111,31 @@ describe('formatMemoryBlock', () => { expect(block).not.toContain('How I do things:'); }); }); + +describe('injectMemory', () => { + it('does not mutate the original messages array', () => { + const messages = [{ role: 'user', content: 'hello' }]; + const result = injectMemory(messages, 'test'); + expect(messages).toHaveLength(1); + expect(result).toHaveLength(2); + expect(result[0].role).toBe('system'); + }); + + it('prepends memory before existing system message content', () => { + const messages = [ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: 'hi' }, + ]; + const result = injectMemory(messages, 'facts'); + expect(result).toHaveLength(2); + expect(result[0].content).toBe('facts\n\nYou are helpful.'); + // Original untouched + expect(messages[0].content).toBe('You are helpful.'); + }); + + it('returns original array unchanged if memoryBlock is empty', () => { + const messages = [{ role: 'user', content: 'hello' }]; + const result = injectMemory(messages, ''); + expect(result).toBe(messages); + }); +}); diff --git a/integrations/openrouter/src/memory.ts b/integrations/openrouter/src/memory.ts index cfcf624..b4a4541 100644 --- a/integrations/openrouter/src/memory.ts +++ b/integrations/openrouter/src/memory.ts @@ -90,18 +90,22 @@ export function formatMemoryBlock(results: QueryResult[]): string { } /** - * Inject a memory block into the messages array. - * If messages[0] is a system message, prepend memory before existing content. - * Otherwise, insert a new system message at index 0. - * Memory goes BEFORE developer instructions so the developer prompt takes priority. + * Return a new messages array with memory injected. + * Memory is placed before the developer system prompt so the developer instructions + * appear last and take priority (LLM recency bias). + * Returns the original array unchanged if memoryBlock is empty. */ export function injectMemory( messages: Array<{ role: string; content: string }>, memoryBlock: string, -): void { +): Array<{ role: string; content: string }> { + if (!memoryBlock) return messages; + if (messages[0]?.role === 'system') { - messages[0].content = memoryBlock + '\n\n' + messages[0].content; - } else { - messages.unshift({ role: 'system', content: memoryBlock }); + return [ + { ...messages[0], content: memoryBlock + '\n\n' + messages[0].content }, + ...messages.slice(1), + ]; } + return [{ role: 'system', content: memoryBlock }, ...messages]; } diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts index 81aaec5..9e64135 100644 --- a/integrations/openrouter/src/server.ts +++ b/integrations/openrouter/src/server.ts @@ -34,7 +34,7 @@ app.post('/v1/chat/completions', async (req, res) => { const results = await fetchMemoryContext(query, profile); if (results) { const block = formatMemoryBlock(results); - injectMemory(body.messages, block); + body.messages = injectMemory(body.messages, block); } } diff --git a/memory/src/providers/prompt.ts b/memory/src/providers/prompt.ts index c9ae467..39981ca 100644 --- a/memory/src/providers/prompt.ts +++ b/memory/src/providers/prompt.ts @@ -46,7 +46,7 @@ SEMANTIC — stable, context-free facts as subject-predicate-object triples: - Examples: * {"subject": "Sha", "predicate": "prefers", "object": "dark mode", "domain": "user"} * {"subject": "TypeScript", "predicate": "supports", "object": "type inference", "domain": "world"} - * {"subject": "I", "predicate": "am configured with", "object": "GPT-4 for extraction", "domain": "self"} + * {"subject": "this agent", "predicate": "uses", "object": "GPT-4 for extraction", "domain": "self"} PROCEDURAL — multi-step workflows or processes: - Must be a genuine multi-step process; if not, leave empty {}. diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index c42ecc7..03a735f 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -267,9 +267,9 @@ export class SqliteMemoryStore { } } - // --- Reflective --- + // --- Reflective (requires attribution — skip if no origin) --- const reflectiveInput = components.reflective; - const reflections = this.normalizeReflectiveInput(reflectiveInput); + const reflections = (origin?.originType) ? this.normalizeReflectiveInput(reflectiveInput) : []; for (const ref of reflections) { const embedding = await this.embed(ref.observation, 'reflective'); @@ -281,9 +281,9 @@ export class SqliteMemoryStore { createdAt, lastAccessed: createdAt, source, - originType: origin?.originType, - originActor: origin?.originActor ?? userId, - originRef: origin?.originRef, + originType: origin!.originType, + originActor: origin!.originActor ?? userId, + originRef: origin!.originRef, }; this.insertReflectiveRow(refRow); rows.push({ From d06ebc02738a1d73a8a86df6b7b5672a9c7ebb40 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:52:30 +0700 Subject: [PATCH 17/78] added more memory tests - single user mode --- memory/package.json | 4 +- memory/tests/store.test.ts | 429 +++++++++++++++++++++++++++++++++++++ package-lock.json | 298 +++++++++++++++++++++++++- 3 files changed, 729 insertions(+), 2 deletions(-) create mode 100644 memory/tests/store.test.ts diff --git a/memory/package.json b/memory/package.json index 1819141..c6e8098 100644 --- a/memory/package.json +++ b/memory/package.json @@ -9,6 +9,7 @@ "scripts": { "build": "tsc -p tsconfig.json", "type-check": "tsc --noEmit", + "test": "vitest run", "prestart": "npm run build", "start": "node dist/server.js" }, @@ -22,6 +23,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "vitest": "^3.0.0" } } diff --git a/memory/tests/store.test.ts b/memory/tests/store.test.ts new file mode 100644 index 0000000..c37a537 --- /dev/null +++ b/memory/tests/store.test.ts @@ -0,0 +1,429 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { SqliteMemoryStore } from '../src/sqlite-store.js'; +import type { EmbedFn, SectorName, IngestComponents } from '../src/types.js'; + +// ── Mock embedding strategy ────────────────────────────────────────── +// 8-dim vectors with controlled orthogonal directions. +// Same text always returns the same vector (deterministic). + +const VECTORS: Record = { + 'dark mode': [1, 0, 0, 0, 0, 0, 0, 0], + 'light mode': [0.9, 0.1, 0, 0, 0, 0, 0, 0], // similar to dark mode + 'TypeScript': [0, 1, 0, 0, 0, 0, 0, 0], + 'deploy to prod': [0, 0, 1, 0, 0, 0, 0, 0], + 'prefers': [0, 0, 0, 1, 0, 0, 0, 0], + 'supports': [0, 0, 0, 0, 1, 0, 0, 0], + // Full triple texts used during ingest (subject + predicate + object) + 'Sha prefers dark mode': [0.7, 0, 0, 0.7, 0, 0, 0, 0], + 'Sha prefers light mode': [0.63, 0.07, 0, 0.7, 0, 0, 0, 0], + 'Sha supports TypeScript': [0, 0.7, 0, 0, 0.7, 0, 0, 0], + // Episodic & procedural + 'deployed v2 to staging': [0, 0, 0.9, 0, 0, 0.1, 0, 0], + 'run npm test before pushing': [0, 0, 0, 0, 0, 0, 1, 0], + 'run npm test': [0, 0, 0, 0, 0, 0, 1, 0], + // Reflective + 'user prefers concise answers': [0, 0, 0, 0.5, 0, 0, 0, 0.5], + 'this is a reflection': [0, 0, 0, 0, 0, 0, 0, 1], +}; + +/** Hash text to a deterministic 8-dim vector for unknown strings. */ +function hashToVector(text: string): number[] { + let h = 0; + for (let i = 0; i < text.length; i++) { + h = ((h << 5) - h + text.charCodeAt(i)) | 0; + } + const vec = new Array(8).fill(0); + for (let i = 0; i < 8; i++) { + // Use different bits of the hash for each dimension + vec[i] = ((h >>> (i * 4)) & 0xf) / 15; + } + // Normalize + const mag = Math.sqrt(vec.reduce((s: number, v: number) => s + v * v, 0)) || 1; + return vec.map((v: number) => v / mag); +} + +const mockEmbed: EmbedFn = async (text: string, _sector: SectorName) => { + return VECTORS[text] ?? hashToVector(text); +}; + +// Fixed timestamp for deterministic tests +const NOW = 1700000000000; + +function createStore(now = NOW): SqliteMemoryStore { + return new SqliteMemoryStore({ + dbPath: ':memory:', + embed: mockEmbed, + now: () => now, + }); +} + +// ── Tests ──────────────────────────────────────────────────────────── + +describe('SqliteMemoryStore (single-user mode)', () => { + let store: SqliteMemoryStore; + + beforeEach(() => { + store = createStore(); + }); + + // ─── 1. Ingest all 4 sectors ────────────────────────────────────── + it('ingests all 4 sectors and returns correct sector labels', async () => { + const rows = await store.ingest( + { + episodic: 'deployed v2 to staging', + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + procedural: { + trigger: 'deploy to prod', + goal: 'ship release', + steps: ['build', 'test', 'deploy'], + }, + reflective: { observation: 'user prefers concise answers' }, + }, + undefined, + { origin: { originType: 'conversation', originActor: 'test' } }, + ); + + expect(rows).toHaveLength(4); + const sectors = rows.map((r) => r.sector).sort(); + expect(sectors).toEqual(['episodic', 'procedural', 'reflective', 'semantic']); + }); + + // ─── 2. Query returns results matching dashboard shape ──────────── + it('query returns workingMemory and perSector with expected shapes', async () => { + // Ingest data across all 4 sectors + await store.ingest( + { + episodic: 'deployed v2 to staging', + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + procedural: { + trigger: 'deploy to prod', + goal: 'ship release', + steps: ['build', 'test', 'deploy'], + }, + reflective: { observation: 'user prefers concise answers' }, + }, + undefined, + { origin: { originType: 'conversation', originActor: 'test' } }, + ); + + // Query with text similar to the semantic triple + const result = await store.query('dark mode'); + + // Structure + expect(result).toHaveProperty('workingMemory'); + expect(result).toHaveProperty('perSector'); + expect(result).toHaveProperty('profileId'); + expect(Array.isArray(result.workingMemory)).toBe(true); + + // perSector has all 4 sector keys + for (const sector of ['episodic', 'semantic', 'procedural', 'reflective'] as SectorName[]) { + expect(result.perSector).toHaveProperty(sector); + expect(Array.isArray(result.perSector[sector])).toBe(true); + } + + // Semantic results carry content as "subject predicate object" + const semanticResults = result.perSector.semantic; + if (semanticResults.length > 0) { + const first = semanticResults[0]; + expect(first).toHaveProperty('id'); + expect(first).toHaveProperty('score'); + expect(first).toHaveProperty('similarity'); + expect(first.content).toContain('Sha'); + expect(first.content).toContain('dark mode'); + } + + // Procedural results carry trigger as content + const procResults = result.perSector.procedural; + if (procResults.length > 0) { + const first = procResults[0]; + expect(first).toHaveProperty('id'); + expect(first).toHaveProperty('score'); + expect(first.content).toBe('deploy to prod'); + } + }); + + // ─── 3. Semantic consolidation — merge ──────────────────────────── + it('merges duplicate semantic triples and increases strength', async () => { + // Ingest same triple twice + await store.ingest({ + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + }); + await store.ingest({ + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + }); + + const summary = store.getSectorSummary(); + const semanticCount = summary.find((s) => s.sector === 'semantic')!.count; + // Should only have 1 row because the second ingest merged + expect(semanticCount).toBe(1); + + // Query to verify strength increased + const result = await store.query('Sha prefers dark mode'); + // The fact should be findable + expect(result.perSector.semantic.length).toBeGreaterThanOrEqual(1); + }); + + // ─── 4. Semantic consolidation — supersede ──────────────────────── + it('supersedes semantic facts when object changes', async () => { + // Ingest original preference + await store.ingest({ + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + }); + + // Ingest conflicting preference — should supersede + await store.ingest({ + semantic: { subject: 'Sha', predicate: 'prefers', object: 'light mode' }, + }); + + // There should be 2 semantic rows total (old one closed, new one active) + // But getSectorSummary counts all rows + const summary = store.getSectorSummary(); + const semanticCount = summary.find((s) => s.sector === 'semantic')!.count; + expect(semanticCount).toBe(2); + + // Query should only return the active (light mode) fact + const result = await store.query('Sha prefers light mode'); + const activeFacts = result.perSector.semantic; + // Semantic content is "subject predicate object" — only non-expired facts should come back + const factContents = activeFacts.map((f) => f.content); + expect(factContents.some((c) => c.includes('light mode'))).toBe(true); + expect(factContents.some((c) => c.includes('dark mode') && !c.includes('light mode'))).toBe(false); + }); + + // ─── 5. getSectorSummary (dashboard /v1/summary) ────────────────── + it('getSectorSummary returns correct counts per sector', async () => { + await store.ingest( + { + episodic: 'deployed v2 to staging', + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + procedural: { trigger: 'deploy to prod', steps: ['build', 'deploy'] }, + reflective: { observation: 'user prefers concise answers' }, + }, + undefined, + { origin: { originType: 'conversation', originActor: 'test' } }, + ); + + const summary = store.getSectorSummary(); + + // Should have all 4 sectors + expect(summary).toHaveLength(4); + const bySector = Object.fromEntries(summary.map((s) => [s.sector, s])); + + expect(bySector.episodic.count).toBe(1); + expect(bySector.semantic.count).toBe(1); + expect(bySector.procedural.count).toBe(1); + expect(bySector.reflective.count).toBe(1); + + // Each sector has lastCreatedAt + for (const s of summary) { + expect(s).toHaveProperty('sector'); + expect(s).toHaveProperty('count'); + expect(s).toHaveProperty('lastCreatedAt'); + } + }); + + // ─── 6. getRecent (dashboard /v1/summary recent) ────────────────── + it('getRecent returns items in createdAt desc order with correct shape', async () => { + // Ingest with advancing timestamps + let t = NOW; + const store1 = new SqliteMemoryStore({ + dbPath: ':memory:', + embed: mockEmbed, + now: () => t, + }); + + t = NOW; + await store1.ingest({ + episodic: 'deployed v2 to staging', + }); + + t = NOW + 1000; + await store1.ingest({ + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + }); + + t = NOW + 2000; + await store1.ingest( + { + procedural: { trigger: 'run npm test before pushing', steps: ['npm test'] }, + }, + ); + + t = NOW + 3000; + await store1.ingest( + { + reflective: { observation: 'this is a reflection' }, + }, + undefined, + { origin: { originType: 'conversation', originActor: 'test' } }, + ); + + const recent = store1.getRecent(undefined, 10); + + // Should return all 4 items + expect(recent.length).toBe(4); + + // Descending order by createdAt + for (let i = 0; i < recent.length - 1; i++) { + expect(recent[i].createdAt).toBeGreaterThanOrEqual(recent[i + 1].createdAt); + } + + // Each item has the shape the dashboard expects (MemoryRecentItem) + for (const item of recent) { + expect(item).toHaveProperty('id'); + expect(item).toHaveProperty('sector'); + expect(item).toHaveProperty('createdAt'); + expect(item).toHaveProperty('lastAccessed'); + expect(item).toHaveProperty('content'); + expect(typeof item.id).toBe('string'); + expect(typeof item.sector).toBe('string'); + expect(typeof item.createdAt).toBe('number'); + } + + // Semantic items should have details with subject/predicate/object + const semanticItem = recent.find((r) => r.sector === 'semantic'); + expect(semanticItem).toBeDefined(); + expect(semanticItem!.details).toBeDefined(); + expect(semanticItem!.details.subject).toBe('Sha'); + expect(semanticItem!.details.predicate).toBe('prefers'); + expect(semanticItem!.details.object).toBe('dark mode'); + + // Procedural items should have details with trigger/steps + const procItem = recent.find((r) => r.sector === 'procedural'); + expect(procItem).toBeDefined(); + expect(procItem!.details).toBeDefined(); + expect(procItem!.details.trigger).toBe('run npm test before pushing'); + expect(Array.isArray(procItem!.details.steps)).toBe(true); + }); + + // ─── 7. Reflective requires attribution ─────────────────────────── + it('skips reflective without origin, stores with origin', async () => { + // Without origin → should skip reflective + const rows1 = await store.ingest({ + reflective: { observation: 'this is a reflection' }, + }); + const reflectiveRows1 = rows1.filter((r) => r.sector === 'reflective'); + expect(reflectiveRows1).toHaveLength(0); + + const summary1 = store.getSectorSummary(); + expect(summary1.find((s) => s.sector === 'reflective')!.count).toBe(0); + + // With origin → should store + const rows2 = await store.ingest( + { + reflective: { observation: 'this is a reflection' }, + }, + undefined, + { origin: { originType: 'conversation', originActor: 'test', originRef: 'conv-123' } }, + ); + const reflectiveRows2 = rows2.filter((r) => r.sector === 'reflective'); + expect(reflectiveRows2).toHaveLength(1); + + const summary2 = store.getSectorSummary(); + expect(summary2.find((s) => s.sector === 'reflective')!.count).toBe(1); + }); + + // ─── 8. Empty ingest ────────────────────────────────────────────── + it('returns empty array for empty/undefined components', async () => { + const rows1 = await store.ingest({}); + expect(rows1).toEqual([]); + + const rows2 = await store.ingest({ + episodic: undefined, + semantic: undefined, + procedural: undefined, + reflective: undefined, + }); + expect(rows2).toEqual([]); + + // Empty strings should also produce nothing + const rows3 = await store.ingest({ + episodic: '', + procedural: '', + }); + expect(rows3).toEqual([]); + }); + + // ─── 9. Delete operations (dashboard DELETE endpoints) ──────────── + it('deleteById removes a single memory, deleteAll clears everything', async () => { + const rows = await store.ingest( + { + episodic: 'deployed v2 to staging', + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + procedural: { trigger: 'deploy to prod', steps: ['build', 'deploy'] }, + reflective: { observation: 'user prefers concise answers' }, + }, + undefined, + { origin: { originType: 'conversation', originActor: 'test' } }, + ); + expect(rows).toHaveLength(4); + + // deleteById — remove the episodic row + const episodicRow = rows.find((r) => r.sector === 'episodic')!; + const deleted = store.deleteById(episodicRow.id); + expect(deleted).toBe(1); + + // Verify it's gone + const summaryAfterDelete = store.getSectorSummary(); + expect(summaryAfterDelete.find((s) => s.sector === 'episodic')!.count).toBe(0); + // Others still present + expect(summaryAfterDelete.find((s) => s.sector === 'semantic')!.count).toBe(1); + expect(summaryAfterDelete.find((s) => s.sector === 'procedural')!.count).toBe(1); + expect(summaryAfterDelete.find((s) => s.sector === 'reflective')!.count).toBe(1); + + // deleteAll — clears all 4 sector tables + const totalDeleted = store.deleteAll(); + expect(totalDeleted).toBe(3); // 3 remaining after episodic was deleted + + const summaryAfterDeleteAll = store.getSectorSummary(); + for (const s of summaryAfterDeleteAll) { + expect(s.count).toBe(0); + } + }); + + // ─── 10. Profile isolation (dashboard profile selector) ─────────── + it('isolates memories by profile and getAvailableProfiles returns both', async () => { + // Ingest for profile "agent-a" + await store.ingest( + { + episodic: 'deployed v2 to staging', + semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, + }, + 'agent-a', + ); + + // Ingest for profile "agent-b" + await store.ingest( + { + episodic: 'TypeScript', + semantic: { subject: 'Sha', predicate: 'supports', object: 'TypeScript' }, + }, + 'agent-b', + ); + + // Query agent-a should return only its memories + const resultA = await store.query('dark mode', 'agent-a'); + expect(resultA.profileId).toBe('agent-a'); + // Episodic and semantic should only come from agent-a + for (const sector of ['episodic', 'semantic'] as SectorName[]) { + for (const row of resultA.perSector[sector]) { + expect(row.profileId).toBe('agent-a'); + } + } + + // Query agent-b should return only its memories + const resultB = await store.query('TypeScript', 'agent-b'); + expect(resultB.profileId).toBe('agent-b'); + for (const sector of ['episodic', 'semantic'] as SectorName[]) { + for (const row of resultB.perSector[sector]) { + expect(row.profileId).toBe('agent-b'); + } + } + + // getAvailableProfiles returns both (plus 'default') + const profiles = store.getAvailableProfiles(); + expect(profiles).toContain('agent-a'); + expect(profiles).toContain('agent-b'); + expect(profiles).toContain('default'); + }); +}); diff --git a/package-lock.json b/package-lock.json index 7f8f09b..b7df654 100644 --- a/package-lock.json +++ b/package-lock.json @@ -391,7 +391,303 @@ "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "vitest": "^3.0.0" + } + }, + "memory/node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "memory/node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "memory/node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "memory/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "memory/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "memory/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "memory/node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "memory/node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "memory/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "memory/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "memory/node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "memory/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "memory/node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "memory/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "memory/node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "memory/node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "memory/node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "memory/node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "memory/node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, "node_modules/@adraffy/ens-normalize": { From dc207b817948a0902ac31dcd48da841daff805b1 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:58:13 +0700 Subject: [PATCH 18/78] updated rofl links --- docs/ROFL_DEPLOYMENT.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/ROFL_DEPLOYMENT.md b/docs/ROFL_DEPLOYMENT.md index d3d8757..e90dc5e 100644 --- a/docs/ROFL_DEPLOYMENT.md +++ b/docs/ROFL_DEPLOYMENT.md @@ -8,11 +8,21 @@ Deploy your own private ekai-gateway instance on Oasis Network using ROFL (Runti - **Isolated**: Each user deploys their own instance - **Verifiable**: Code execution can be verified on-chain +--- + +## Demo + +[![ROFL Deployment Demo Video](https://img.youtube.com/vi/hZC1Y_dWdhI/maxresdefault.jpg)](https://www.youtube.com/watch?v=hZC1Y_dWdhI) + +Watch the deployment walkthrough: [ROFL Deployment Demo](https://www.youtube.com/watch?v=hZC1Y_dWdhI) + +--- + ## Prerequisites 1. **Oasis CLI** (v0.18.x+) + Install from [L15 CLI Reference](https://cli.oasis.io), then verify: ```bash - curl -fsSL https://get.oasis.io | bash oasis --version ``` From 2029d86b8bdb5438a6c626606a98c533eee4bce3 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:59:40 +0700 Subject: [PATCH 19/78] updated rofl links --- docs/ROFL_DEPLOYMENT.md | 12 ++- gateway/src/costs/openrouter.yaml | 120 +++++++++++++++--------------- 2 files changed, 71 insertions(+), 61 deletions(-) diff --git a/docs/ROFL_DEPLOYMENT.md b/docs/ROFL_DEPLOYMENT.md index d3d8757..e90dc5e 100644 --- a/docs/ROFL_DEPLOYMENT.md +++ b/docs/ROFL_DEPLOYMENT.md @@ -8,11 +8,21 @@ Deploy your own private ekai-gateway instance on Oasis Network using ROFL (Runti - **Isolated**: Each user deploys their own instance - **Verifiable**: Code execution can be verified on-chain +--- + +## Demo + +[![ROFL Deployment Demo Video](https://img.youtube.com/vi/hZC1Y_dWdhI/maxresdefault.jpg)](https://www.youtube.com/watch?v=hZC1Y_dWdhI) + +Watch the deployment walkthrough: [ROFL Deployment Demo](https://www.youtube.com/watch?v=hZC1Y_dWdhI) + +--- + ## Prerequisites 1. **Oasis CLI** (v0.18.x+) + Install from [L15 CLI Reference](https://cli.oasis.io), then verify: ```bash - curl -fsSL https://get.oasis.io | bash oasis --version ``` diff --git a/gateway/src/costs/openrouter.yaml b/gateway/src/costs/openrouter.yaml index ed92375..e6a9d69 100644 --- a/gateway/src/costs/openrouter.yaml +++ b/gateway/src/costs/openrouter.yaml @@ -2822,23 +2822,23 @@ models: input: 1.2 output: 1.2 original_provider: nvidia - inflection/inflection-3-pi: - id: inflection/inflection-3-pi + inflection/inflection-3-productivity: + id: inflection/inflection-3-productivity input: 2.5 output: 10 original_provider: inflection - inflection-3-pi: - id: inflection/inflection-3-pi + inflection-3-productivity: + id: inflection/inflection-3-productivity input: 2.5 output: 10 original_provider: inflection - inflection/inflection-3-productivity: - id: inflection/inflection-3-productivity + inflection/inflection-3-pi: + id: inflection/inflection-3-pi input: 2.5 output: 10 original_provider: inflection - inflection-3-productivity: - id: inflection/inflection-3-productivity + inflection-3-pi: + id: inflection/inflection-3-pi input: 2.5 output: 10 original_provider: inflection @@ -2852,26 +2852,6 @@ models: input: 0.17 output: 0.43 original_provider: thedrummer - meta-llama/llama-3.2-3b-instruct:free: - id: meta-llama/llama-3.2-3b-instruct:free - input: 0 - output: 0 - original_provider: meta-llama - llama-3.2-3b-instruct:free: - id: meta-llama/llama-3.2-3b-instruct:free - input: 0 - output: 0 - original_provider: meta-llama - meta-llama/llama-3.2-3b-instruct: - id: meta-llama/llama-3.2-3b-instruct - input: 0.02 - output: 0.02 - original_provider: meta-llama - llama-3.2-3b-instruct: - id: meta-llama/llama-3.2-3b-instruct - input: 0.02 - output: 0.02 - original_provider: meta-llama meta-llama/llama-3.2-1b-instruct: id: meta-llama/llama-3.2-1b-instruct input: 0.027 @@ -2892,6 +2872,26 @@ models: input: 0.049 output: 0.049 original_provider: meta-llama + meta-llama/llama-3.2-3b-instruct:free: + id: meta-llama/llama-3.2-3b-instruct:free + input: 0 + output: 0 + original_provider: meta-llama + llama-3.2-3b-instruct:free: + id: meta-llama/llama-3.2-3b-instruct:free + input: 0 + output: 0 + original_provider: meta-llama + meta-llama/llama-3.2-3b-instruct: + id: meta-llama/llama-3.2-3b-instruct + input: 0.02 + output: 0.02 + original_provider: meta-llama + llama-3.2-3b-instruct: + id: meta-llama/llama-3.2-3b-instruct + input: 0.02 + output: 0.02 + original_provider: meta-llama qwen/qwen-2.5-72b-instruct: id: qwen/qwen-2.5-72b-instruct input: 0.12 @@ -3022,15 +3022,15 @@ models: input: 4 output: 4 original_provider: meta-llama - meta-llama/llama-3.1-8b-instruct: - id: meta-llama/llama-3.1-8b-instruct - input: 0.02 - output: 0.05 + meta-llama/llama-3.1-70b-instruct: + id: meta-llama/llama-3.1-70b-instruct + input: 0.4 + output: 0.4 original_provider: meta-llama - llama-3.1-8b-instruct: - id: meta-llama/llama-3.1-8b-instruct - input: 0.02 - output: 0.05 + llama-3.1-70b-instruct: + id: meta-llama/llama-3.1-70b-instruct + input: 0.4 + output: 0.4 original_provider: meta-llama meta-llama/llama-3.1-405b-instruct: id: meta-llama/llama-3.1-405b-instruct @@ -3042,15 +3042,15 @@ models: input: 4 output: 4 original_provider: meta-llama - meta-llama/llama-3.1-70b-instruct: - id: meta-llama/llama-3.1-70b-instruct - input: 0.4 - output: 0.4 + meta-llama/llama-3.1-8b-instruct: + id: meta-llama/llama-3.1-8b-instruct + input: 0.02 + output: 0.05 original_provider: meta-llama - llama-3.1-70b-instruct: - id: meta-llama/llama-3.1-70b-instruct - input: 0.4 - output: 0.4 + llama-3.1-8b-instruct: + id: meta-llama/llama-3.1-8b-instruct + input: 0.02 + output: 0.05 original_provider: meta-llama mistralai/mistral-nemo: id: mistralai/mistral-nemo @@ -3392,15 +3392,15 @@ models: input: 0.06 output: 0.06 original_provider: gryphe - openai/gpt-4-0314: - id: openai/gpt-4-0314 - input: 30 - output: 60 + openai/gpt-3.5-turbo: + id: openai/gpt-3.5-turbo + input: 0.5 + output: 1.5 original_provider: openai - gpt-4-0314: - id: openai/gpt-4-0314 - input: 30 - output: 60 + gpt-3.5-turbo: + id: openai/gpt-3.5-turbo + input: 0.5 + output: 1.5 original_provider: openai openai/gpt-4: id: openai/gpt-4 @@ -3412,15 +3412,15 @@ models: input: 30 output: 60 original_provider: openai - openai/gpt-3.5-turbo: - id: openai/gpt-3.5-turbo - input: 0.5 - output: 1.5 + openai/gpt-4-0314: + id: openai/gpt-4-0314 + input: 30 + output: 60 original_provider: openai - gpt-3.5-turbo: - id: openai/gpt-3.5-turbo - input: 0.5 - output: 1.5 + gpt-4-0314: + id: openai/gpt-4-0314 + input: 30 + output: 60 original_provider: openai metadata: last_updated: '2026-02-16' From 26b8b3ae4250a439cf25c0fea52eefdd849cd602 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:56:12 +0700 Subject: [PATCH 20/78] updated docker scripts --- Dockerfile | 159 ++++++++++++++++++------------ docker-compose.yaml | 11 +++ scripts/launcher.js | 2 +- scripts/start-docker-fullstack.sh | 95 ++++++++++++------ ui/dashboard/next.config.mjs | 8 +- 5 files changed, 180 insertions(+), 95 deletions(-) diff --git a/Dockerfile b/Dockerfile index 26ced24..6bb897c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,89 +1,126 @@ # ---------- build base ---------- - FROM node:20 AS build-base - WORKDIR /app - - # Copy manifests for cache-friendly installs - COPY package.json package-lock.json ./ - COPY gateway/package.json gateway/package-lock.json ./gateway/ - COPY ui/dashboard/package.json ui/dashboard/package-lock.json ./ui/dashboard/ - - # Install deps per project (no workspaces) - RUN npm install - RUN cd gateway && npm install - RUN cd ui/dashboard && npm install - - # Copy the rest of the source - COPY . . - - # ---------- gateway build ---------- - FROM build-base AS gateway-build - WORKDIR /app/gateway - RUN npm run build - +FROM node:20 AS build-base +WORKDIR /app + +# Copy root manifests +COPY package.json package-lock.json ./ + +# Copy per-workspace manifests (lock files may not exist for all) +COPY gateway/package.json gateway/package-lock.json* ./gateway/ +COPY ui/dashboard/package.json ui/dashboard/package-lock.json* ./ui/dashboard/ +COPY memory/package.json ./memory/ +COPY integrations/openrouter/package.json ./integrations/openrouter/ + +# Install all workspace deps from root +RUN npm install --workspaces --include-workspace-root + +# Copy the rest of the source +COPY . . + +# ---------- gateway build ---------- +FROM build-base AS gateway-build +WORKDIR /app/gateway +RUN npm run build + # ---------- dashboard build ---------- FROM build-base AS dashboard-build WORKDIR /app/ui/dashboard -# Accept build arg but default to a placeholder that can be replaced at runtime ARG NEXT_PUBLIC_API_BASE_URL=__API_URL_PLACEHOLDER__ ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} -# For smaller runtime images, you could: RUN npm run build && npm prune --omit=dev RUN npm run build - - # ---------- gateway runtime ---------- - FROM node:20-alpine AS gateway-runtime - WORKDIR /app/gateway - ENV NODE_ENV=production - COPY gateway/package.json gateway/package-lock.json ./ - RUN npm install --omit=dev - COPY --from=gateway-build /app/gateway/dist ./dist - # Optional runtime dirs - RUN mkdir -p /app/gateway/data /app/gateway/logs - EXPOSE 3001 - CMD ["node", "dist/gateway/src/index.js"] - + +# ---------- memory build ---------- +FROM build-base AS memory-build +WORKDIR /app/memory +RUN npm run build + +# ---------- openrouter build ---------- +FROM build-base AS openrouter-build +WORKDIR /app/integrations/openrouter +RUN npm run build + +# ---------- gateway runtime ---------- +FROM node:20-alpine AS gateway-runtime +WORKDIR /app/gateway +ENV NODE_ENV=production +COPY gateway/package.json ./ +RUN npm install --omit=dev +COPY --from=gateway-build /app/gateway/dist ./dist +COPY --from=gateway-build /app/model_catalog ./dist/model_catalog +RUN mkdir -p /app/gateway/data /app/gateway/logs +EXPOSE 3001 +CMD ["node", "dist/gateway/src/index.js"] + # ---------- dashboard runtime ---------- FROM node:20-alpine AS dashboard-runtime WORKDIR /app/ui/dashboard -COPY ui/dashboard/package.json ui/dashboard/package-lock.json ./ +ENV NODE_ENV=production +COPY ui/dashboard/package.json ./ +RUN npm install --omit=dev +COPY --from=dashboard-build /app/ui/dashboard/.next ./.next +COPY --from=dashboard-build /app/ui/dashboard/public ./public +COPY --from=dashboard-build /app/ui/dashboard/next.config.mjs ./next.config.mjs +EXPOSE 3000 +CMD ["node_modules/.bin/next", "start", "-p", "3000"] + +# ---------- memory runtime ---------- +FROM node:20-alpine AS memory-runtime +WORKDIR /app/memory +ENV NODE_ENV=production +COPY memory/package.json ./ RUN npm install --omit=dev +COPY --from=memory-build /app/memory/dist ./dist +RUN mkdir -p /app/memory/data +EXPOSE 4005 +CMD ["node", "dist/server.js"] + +# ---------- openrouter runtime ---------- +FROM node:20-alpine AS openrouter-runtime +WORKDIR /app/integrations/openrouter ENV NODE_ENV=production - # Copy production build output - COPY --from=dashboard-build /app/ui/dashboard/.next ./.next - COPY --from=dashboard-build /app/ui/dashboard/public ./public - # Copy config files - COPY --from=dashboard-build /app/ui/dashboard/next.config.mjs ./next.config.mjs - COPY --from=dashboard-build /app/ui/dashboard/postcss.config.mjs ./postcss.config.mjs - EXPOSE 3000 - CMD ["node_modules/.bin/next", "start", "-p", "3000"] - +COPY integrations/openrouter/package.json ./ +RUN npm install --omit=dev +COPY --from=openrouter-build /app/integrations/openrouter/dist ./dist +EXPOSE 4010 +CMD ["node", "dist/server.js"] + # ---------- fullstack runtime ---------- FROM node:20-alpine AS ekai-gateway-runtime WORKDIR /app -# bash is needed for wait -n in the entrypoint script RUN apk add --no-cache bash - # Gateway runtime bits -COPY gateway/package.json gateway/package-lock.json ./gateway/ +# Gateway +COPY gateway/package.json ./gateway/ RUN cd gateway && npm install --omit=dev COPY --from=gateway-build /app/gateway/dist ./gateway/dist +COPY --from=gateway-build /app/model_catalog ./model_catalog RUN mkdir -p /app/gateway/data /app/gateway/logs - # Dashboard runtime bits -COPY ui/dashboard/package.json ui/dashboard/package-lock.json ./ui/dashboard/ +# Dashboard +COPY ui/dashboard/package.json ./ui/dashboard/ RUN cd ui/dashboard && npm install --omit=dev - COPY --from=dashboard-build /app/ui/dashboard/.next ./ui/dashboard/.next - COPY --from=dashboard-build /app/ui/dashboard/public ./ui/dashboard/public - COPY --from=dashboard-build /app/ui/dashboard/next.config.mjs ./ui/dashboard/next.config.mjs - COPY --from=dashboard-build /app/ui/dashboard/postcss.config.mjs ./ui/dashboard/postcss.config.mjs - -# Entrypoint for running both services +COPY --from=dashboard-build /app/ui/dashboard/.next ./ui/dashboard/.next +COPY --from=dashboard-build /app/ui/dashboard/public ./ui/dashboard/public +COPY --from=dashboard-build /app/ui/dashboard/next.config.mjs ./ui/dashboard/next.config.mjs + +# Memory +COPY memory/package.json ./memory/ +RUN cd memory && npm install --omit=dev +COPY --from=memory-build /app/memory/dist ./memory/dist +RUN mkdir -p /app/memory/data + +# OpenRouter +COPY integrations/openrouter/package.json ./integrations/openrouter/ +RUN cd integrations/openrouter && npm install --omit=dev +COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist + +# Entrypoint COPY scripts/start-docker-fullstack.sh /app/start-docker-fullstack.sh RUN chmod +x /app/start-docker-fullstack.sh ENV NODE_ENV=production - -EXPOSE 3001 3000 -VOLUME ["/app/gateway/data", "/app/gateway/logs"] + +EXPOSE 3001 3000 4005 4010 +VOLUME ["/app/gateway/data", "/app/gateway/logs", "/app/memory/data"] CMD ["/app/start-docker-fullstack.sh"] - diff --git a/docker-compose.yaml b/docker-compose.yaml index 1e64551..6fa85a8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,21 +5,32 @@ services: environment: PORT: ${PORT:-3001} UI_PORT: ${UI_PORT:-3000} + MEMORY_PORT: ${MEMORY_PORT:-4005} + OPENROUTER_PORT: ${OPENROUTER_PORT:-4010} NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3001} DATABASE_PATH: /app/gateway/data/proxy.db + MEMORY_DB_PATH: /app/memory/data/memory.db OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} GOOGLE_API_KEY: ${GOOGLE_API_KEY:-} + ENABLE_GATEWAY: ${ENABLE_GATEWAY:-true} + ENABLE_DASHBOARD: ${ENABLE_DASHBOARD:-true} + ENABLE_MEMORY: ${ENABLE_MEMORY:-true} + ENABLE_OPENROUTER: ${ENABLE_OPENROUTER:-true} ports: - "3001:3001" - "3000:3000" + - "4005:4005" + - "4010:4010" volumes: - gateway_logs:/app/gateway/logs - gateway_db:/app/gateway/data + - memory_db:/app/memory/data restart: unless-stopped volumes: gateway_logs: gateway_db: + memory_db: diff --git a/scripts/launcher.js b/scripts/launcher.js index 4b4745d..bf78c99 100644 --- a/scripts/launcher.js +++ b/scripts/launcher.js @@ -32,7 +32,7 @@ const SERVICES = { port: gatewayPort, }, dashboard: { - dev: `npx -w ui/dashboard next dev --turbopack -p ${dashboardPort}`, + dev: `npx -w ui/dashboard next dev -p ${dashboardPort}`, start: `npx -w ui/dashboard next start -p ${dashboardPort} -H 0.0.0.0`, label: "dashboard", color: "magenta", diff --git a/scripts/start-docker-fullstack.sh b/scripts/start-docker-fullstack.sh index 02886c4..cc53f50 100755 --- a/scripts/start-docker-fullstack.sh +++ b/scripts/start-docker-fullstack.sh @@ -1,47 +1,78 @@ #!/bin/bash set -euo pipefail -GATEWAY_DIR="/app/gateway" -DASHBOARD_DIR="/app/ui/dashboard" - PORT="${PORT:-3001}" UI_PORT="${UI_PORT:-3000}" +MEMORY_PORT="${MEMORY_PORT:-4005}" +OPENROUTER_PORT="${OPENROUTER_PORT:-4010}" -# Runtime API URL detection -if [ -n "${NEXT_PUBLIC_API_BASE_URL:-}" ]; then - # If NEXT_PUBLIC_API_BASE_URL is set at runtime, use it - API_URL="$NEXT_PUBLIC_API_BASE_URL" -else - # Default for local development - API_URL="http://localhost:${PORT}" -fi +# Service toggles (all enabled by default) +ENABLE_GATEWAY="${ENABLE_GATEWAY:-true}" +ENABLE_DASHBOARD="${ENABLE_DASHBOARD:-true}" +ENABLE_MEMORY="${ENABLE_MEMORY:-true}" +ENABLE_OPENROUTER="${ENABLE_OPENROUTER:-true}" -echo "Configuring API URL: $API_URL" +# Debug: Show environment variables +echo "Debug: ENABLE_GATEWAY=$ENABLE_GATEWAY ENABLE_DASHBOARD=$ENABLE_DASHBOARD ENABLE_MEMORY=$ENABLE_MEMORY ENABLE_OPENROUTER=$ENABLE_OPENROUTER" >&2 -# Replace placeholder in Next.js built files -cd "$DASHBOARD_DIR" -if [ "$API_URL" != "__API_URL_PLACEHOLDER__" ]; then - echo "Replacing placeholder with $API_URL in Next.js build files..." - find .next -type f \( -name "*.js" -o -name "*.json" \) -exec sed -i "s|__API_URL_PLACEHOLDER__|$API_URL|g" {} + 2>/dev/null || true -fi +PIDS=() cleanup() { - if [ -n "${GW_PID:-}" ] && kill -0 "$GW_PID" 2>/dev/null; then - kill "$GW_PID" 2>/dev/null || true - fi - if [ -n "${UI_PID:-}" ] && kill -0 "$UI_PID" 2>/dev/null; then - kill "$UI_PID" 2>/dev/null || true - fi + for pid in "${PIDS[@]}"; do + kill "$pid" 2>/dev/null || true + done } - trap cleanup INT TERM -cd "$GATEWAY_DIR" -node dist/gateway/src/index.js & -GW_PID=$! +# Runtime API URL replacement for Next.js +if [ "$ENABLE_DASHBOARD" != "false" ] && [ "$ENABLE_DASHBOARD" != "0" ]; then + API_URL="${NEXT_PUBLIC_API_BASE_URL:-http://localhost:${PORT}}" + echo "Configuring API URL: $API_URL" + cd /app/ui/dashboard + if [ "$API_URL" != "__API_URL_PLACEHOLDER__" ]; then + find .next -type f \( -name "*.js" -o -name "*.json" \) -exec sed -i "s|__API_URL_PLACEHOLDER__|$API_URL|g" {} + 2>/dev/null || true + fi +fi -cd "$DASHBOARD_DIR" -node_modules/.bin/next start -p "$UI_PORT" -H 0.0.0.0 & -UI_PID=$! +# Start services +echo "" +echo " ekai-gateway (docker)" +echo "" + +if [ "$ENABLE_GATEWAY" != "false" ] && [ "$ENABLE_GATEWAY" != "0" ]; then + echo " Starting gateway on :${PORT}" + cd /app/gateway + PORT="$PORT" node dist/gateway/src/index.js & + PIDS+=($!) +fi + +if [ "$ENABLE_DASHBOARD" != "false" ] && [ "$ENABLE_DASHBOARD" != "0" ]; then + echo " Starting dashboard on :${UI_PORT}" + cd /app/ui/dashboard + node_modules/.bin/next start -p "$UI_PORT" -H 0.0.0.0 & + PIDS+=($!) +fi + +if [ "$ENABLE_MEMORY" != "false" ] && [ "$ENABLE_MEMORY" != "0" ]; then + echo " Starting memory on :${MEMORY_PORT}" + cd /app/memory + MEMORY_PORT="$MEMORY_PORT" MEMORY_DB_PATH="${MEMORY_DB_PATH:-/app/memory/data/memory.db}" node dist/server.js & + PIDS+=($!) +fi + +if [ "$ENABLE_OPENROUTER" != "false" ] && [ "$ENABLE_OPENROUTER" != "0" ]; then + echo " Starting openrouter on :${OPENROUTER_PORT}" + cd /app/integrations/openrouter + PORT="$OPENROUTER_PORT" node dist/server.js & + PIDS+=($!) +fi + +echo "" + +if [ ${#PIDS[@]} -eq 0 ]; then + echo " No services enabled." + exit 0 +fi -wait -n "$GW_PID" "$UI_PID" +# Wait for all children to exit (or indefinitely if restart=unless-stopped) +wait diff --git a/ui/dashboard/next.config.mjs b/ui/dashboard/next.config.mjs index f3cc306..974ed44 100644 --- a/ui/dashboard/next.config.mjs +++ b/ui/dashboard/next.config.mjs @@ -1,6 +1,12 @@ +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + /** @type {import('next').NextConfig} */ const nextConfig = { - /* config options here */ + // Monorepo: tell Next.js where the workspace root is so it can resolve packages + outputFileTracingRoot: resolve(__dirname, '../..'), }; export default nextConfig; From a74a87ec7df6bbb8a375da25826a628c9a7ff4c0 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:58:04 +0700 Subject: [PATCH 21/78] Fix docker container exit issue in fullstack entrypoint Changed 'wait -n' to 'wait' to keep the container running indefinitely instead of exiting when the first service exits. This allows all services (gateway, dashboard, memory, openrouter) to continue running. Fixes issue where container would restart repeatedly with exit code 0. --- scripts/start-docker-fullstack.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/start-docker-fullstack.sh b/scripts/start-docker-fullstack.sh index cc53f50..ed5da1b 100755 --- a/scripts/start-docker-fullstack.sh +++ b/scripts/start-docker-fullstack.sh @@ -12,9 +12,6 @@ ENABLE_DASHBOARD="${ENABLE_DASHBOARD:-true}" ENABLE_MEMORY="${ENABLE_MEMORY:-true}" ENABLE_OPENROUTER="${ENABLE_OPENROUTER:-true}" -# Debug: Show environment variables -echo "Debug: ENABLE_GATEWAY=$ENABLE_GATEWAY ENABLE_DASHBOARD=$ENABLE_DASHBOARD ENABLE_MEMORY=$ENABLE_MEMORY ENABLE_OPENROUTER=$ENABLE_OPENROUTER" >&2 - PIDS=() cleanup() { From c1d456f08ee714d07eb7d2f12ecef4448a292ad2 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:00:16 +0700 Subject: [PATCH 22/78] Update README with Docker service configuration and fixes - Document all 4 services and their ports (gateway, dashboard, memory, openrouter) - Add Docker service control via ENABLE_* environment variables - Clarify OpenRouter integration service runs on port 4010 (not 4006) - Add Docker Compose section with service management instructions - Document Docker restart behavior and service lifecycle --- README.md | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dd4a729..61aaa53 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,24 @@ docker run --env-file .env -p 3001:3001 -p 3000:3000 ghcr.io/ekailabs/ekai-gatew Important: The dashboard is initially empty. After setup, send a query using your own client/tool (IDE, app, or API) through the gateway; usage appears once at least one request is processed. **Access Points (default ports, configurable in `.env`):** -- Gateway API: port `3001` (`PORT`) -- Dashboard UI: port `3000` -- Memory Service: port `4005` (`MEMORY_PORT`) -- Detailed setup steps live in `docs/getting-started.md`; check `docs/` for additional guides. +- Gateway API: port `3001` (`PORT`) - OpenAI/Anthropic compatible endpoints +- Dashboard UI: port `3000` (`UI_PORT`) - Usage analytics and cost tracking +- Memory Service: port `4005` (`MEMORY_PORT`) - Agent memory and context storage +- OpenRouter Integration: port `4010` (`OPENROUTER_PORT`) - OpenRouter proxy service The dashboard auto-detects the host and connects to the gateway and memory service on the same host using their configured ports. No extra URL configuration needed. +**Docker Service Configuration:** +All services run in a single Docker container. Control which services start via `.env`: +```bash +ENABLE_GATEWAY=true # API server (default: true) +ENABLE_DASHBOARD=true # Web dashboard (default: true) +ENABLE_MEMORY=true # Memory service (default: true) +ENABLE_OPENROUTER=true # OpenRouter integration (default: true) +``` + +Detailed setup steps live in `docs/getting-started.md`; check `docs/` for additional guides. + ### Build the Image Yourself (optional) If you’re contributing changes or need a custom build: @@ -195,7 +206,9 @@ The proxy uses **cost-based optimization** to automatically select the cheapest ## Running Services -A unified launcher starts all 4 services by default (gateway, dashboard, memory, openrouter). Disable any service with an env var: +### With npm (local development) + +A unified launcher starts all 4 services by default (gateway, dashboard, memory, openrouter): ```bash npm run dev # Development mode — all services with hot-reload @@ -215,12 +228,24 @@ ENABLE_MEMORY=false ENABLE_DASHBOARD=false npm run dev # Gateway + openrouter o ```bash npm run dev:gateway # Gateway only (port 3001) npm run dev:ui # Dashboard only (port 3000) -npm run dev:openrouter # OpenRouter integration only (port 4006) +npm run dev:openrouter # OpenRouter integration only (port 4010) npm run start:gateway # Production gateway npm run start:ui # Production dashboard npm run start:memory # Memory service (port 4005) ``` +### With Docker + +All services run together in a single container. The entrypoint script manages service lifecycle and ensures all enabled services stay running: + +```bash +docker compose up -d # Start all services +docker compose logs -f # View logs from all services +docker compose down # Stop all services +``` + +Services are controlled via `.env` file `ENABLE_*` variables. The Docker container will restart automatically on failure (see `docker-compose.yaml` for `restart: unless-stopped`). + ## Contributing Contributions are highly valued and welcomed! See [CONTRIBUTING.md](./CONTRIBUTING.md) for details. From cbea53a991b1b0fe3b21ca5fd4f7367e59739971 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:43:17 +0700 Subject: [PATCH 23/78] Update YouTube video URL to new demo Changed video URL from hZC1Y_dWdhI to sLg9YmYtg64 in: - README.md - docs/ROFL_DEPLOYMENT.md Uses GitBook-compatible embed syntax. --- README.md | 4 +--- docs/ROFL_DEPLOYMENT.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 61aaa53..b06ce49 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,7 @@ Multi-provider AI proxy with usage dashboard supporting Anthropic, OpenAI, Googl ## 🎥 Demo Video - - Demo Video - +{% embed url="https://www.youtube.com/watch?v=sLg9YmYtg64" %} ## Quick Start (Beta) diff --git a/docs/ROFL_DEPLOYMENT.md b/docs/ROFL_DEPLOYMENT.md index e90dc5e..f777c32 100644 --- a/docs/ROFL_DEPLOYMENT.md +++ b/docs/ROFL_DEPLOYMENT.md @@ -12,9 +12,7 @@ Deploy your own private ekai-gateway instance on Oasis Network using ROFL (Runti ## Demo -[![ROFL Deployment Demo Video](https://img.youtube.com/vi/hZC1Y_dWdhI/maxresdefault.jpg)](https://www.youtube.com/watch?v=hZC1Y_dWdhI) - -Watch the deployment walkthrough: [ROFL Deployment Demo](https://www.youtube.com/watch?v=hZC1Y_dWdhI) +{% embed url="https://www.youtube.com/watch?v=sLg9YmYtg64" %} --- From 52790d243253947c14524cf0dd027a79949b0f3d Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:33:31 +0530 Subject: [PATCH 24/78] fix: add missing reflective sector to dashboard to prevent crash The sectorColors/sectorDescriptions maps and MemorySectorSummary type only had 3 sectors (episodic, semantic, procedural) but the memory service also returns reflective memories, causing an undefined .includes() crash in MemoryStrength. --- ui/dashboard/src/components/memory/MemoryStrength.tsx | 2 +- ui/dashboard/src/components/memory/SectorTooltip.tsx | 6 ++++-- ui/dashboard/src/lib/api.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ui/dashboard/src/components/memory/MemoryStrength.tsx b/ui/dashboard/src/components/memory/MemoryStrength.tsx index a432c35..8ade559 100644 --- a/ui/dashboard/src/components/memory/MemoryStrength.tsx +++ b/ui/dashboard/src/components/memory/MemoryStrength.tsx @@ -39,7 +39,7 @@ export function MemoryStrength({ data }: MemoryStrengthProps) {
diff --git a/ui/dashboard/src/components/memory/SectorTooltip.tsx b/ui/dashboard/src/components/memory/SectorTooltip.tsx index 1e25e76..dfcce87 100644 --- a/ui/dashboard/src/components/memory/SectorTooltip.tsx +++ b/ui/dashboard/src/components/memory/SectorTooltip.tsx @@ -3,16 +3,18 @@ import { useState } from 'react'; import type { MemorySectorSummary } from '@/lib/api'; -const sectorColors: Record = { +const sectorColors: Record = { episodic: 'bg-indigo-100 text-indigo-700 border-indigo-200', semantic: 'bg-emerald-100 text-emerald-700 border-emerald-200', procedural: 'bg-amber-100 text-amber-700 border-amber-200', + reflective: 'bg-rose-100 text-rose-700 border-rose-200', }; -const sectorDescriptions: Record = { +const sectorDescriptions: Record = { episodic: 'Personal events and experiences the system has encountered.', semantic: 'Facts, concepts, and general knowledge extracted from interactions.', procedural: 'Multi-step skills, workflows, and how-to instructions.', + reflective: 'Self-observations and meta-cognitive insights about behavior patterns.', }; const capitalizeSector = (sector: string): string => { diff --git a/ui/dashboard/src/lib/api.ts b/ui/dashboard/src/lib/api.ts index 80f7510..c5b3357 100644 --- a/ui/dashboard/src/lib/api.ts +++ b/ui/dashboard/src/lib/api.ts @@ -76,14 +76,14 @@ export interface BudgetResponse { } export interface MemorySectorSummary { - sector: 'episodic' | 'semantic' | 'procedural'; + sector: 'episodic' | 'semantic' | 'procedural' | 'reflective'; count: number; lastCreatedAt: number | null; } export interface MemoryRecentItem { id: string; - sector: 'episodic' | 'semantic' | 'procedural'; + sector: 'episodic' | 'semantic' | 'procedural' | 'reflective'; createdAt: number; lastAccessed: number; preview: string; From 551d581f99359cccf2d53ef44e576ddd7d414183 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:50:05 +0530 Subject: [PATCH 25/78] docs: update READMEs for embedded memory architecture Memory is now a library (@ekai/memory) embedded in the OpenRouter integration on port 4010, no longer a standalone service on port 4005. Remove ENABLE_MEMORY, start:memory, and port 4005 references; add usage modes (direct import, mountable router, standalone) to memory README. --- README.md | 12 ++++-------- memory/README.md | 45 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b06ce49..0c33eb7 100644 --- a/README.md +++ b/README.md @@ -56,17 +56,15 @@ Important: The dashboard is initially empty. After setup, send a query using you **Access Points (default ports, configurable in `.env`):** - Gateway API: port `3001` (`PORT`) - OpenAI/Anthropic compatible endpoints - Dashboard UI: port `3000` (`UI_PORT`) - Usage analytics and cost tracking -- Memory Service: port `4005` (`MEMORY_PORT`) - Agent memory and context storage -- OpenRouter Integration: port `4010` (`OPENROUTER_PORT`) - OpenRouter proxy service +- OpenRouter Integration: port `4010` (`OPENROUTER_PORT`) - OpenRouter proxy with embedded memory APIs -The dashboard auto-detects the host and connects to the gateway and memory service on the same host using their configured ports. No extra URL configuration needed. +The dashboard auto-detects the host and connects to the gateway on the same host using its configured port. No extra URL configuration needed. **Docker Service Configuration:** All services run in a single Docker container. Control which services start via `.env`: ```bash ENABLE_GATEWAY=true # API server (default: true) ENABLE_DASHBOARD=true # Web dashboard (default: true) -ENABLE_MEMORY=true # Memory service (default: true) ENABLE_OPENROUTER=true # OpenRouter integration (default: true) ``` @@ -135,7 +133,7 @@ codex ekai-gateway/ ├── gateway/ # Backend API and routing ├── ui/dashboard/ # Dashboard frontend (Next.js) -├── memory/ # Agent memory service +├── memory/ # Agent memory library (@ekai/memory) ├── integrations/ │ └── openrouter/ # OpenRouter integration service ├── scripts/ @@ -206,7 +204,7 @@ The proxy uses **cost-based optimization** to automatically select the cheapest ### With npm (local development) -A unified launcher starts all 4 services by default (gateway, dashboard, memory, openrouter): +A unified launcher starts all 3 services by default (gateway, dashboard, openrouter): ```bash npm run dev # Development mode — all services with hot-reload @@ -218,7 +216,6 @@ npm start # Production mode — all services from built output ```bash ENABLE_DASHBOARD=false npm run dev # Skip the dashboard ENABLE_OPENROUTER=false npm start # Production without openrouter -ENABLE_MEMORY=false ENABLE_DASHBOARD=false npm run dev # Gateway + openrouter only ``` **Individual service scripts** (escape hatches): @@ -229,7 +226,6 @@ npm run dev:ui # Dashboard only (port 3000) npm run dev:openrouter # OpenRouter integration only (port 4010) npm run start:gateway # Production gateway npm run start:ui # Production dashboard -npm run start:memory # Memory service (port 4005) ``` ### With Docker diff --git a/memory/README.md b/memory/README.md index 67119a1..d27d5dc 100644 --- a/memory/README.md +++ b/memory/README.md @@ -4,12 +4,15 @@ Neuroscience-inspired, agent-centric memory kernel. Sectorized storage with PBWM ## Quickstart +By default, memory is embedded inside the OpenRouter integration and served on port `4010`. No separate service needed. + ```bash -npm install -w memory -npm run build -w memory -npm start -w memory # :4005 +npm install -w @ekai/memory +npm run build -w @ekai/memory ``` +See [Usage Modes](#usage-modes) below for direct import, mountable router, and standalone options. + Env (root `.env` or `memory/.env`): | Variable | Default | Required | @@ -17,9 +20,39 @@ Env (root `.env` or `memory/.env`): | `GOOGLE_API_KEY` | — | Yes | | `GEMINI_EXTRACT_MODEL` | `gemini-2.5-flash` | No | | `GEMINI_EMBED_MODEL` | `gemini-embedding-001` | No | -| `MEMORY_PORT` | `4005` | No | | `MEMORY_DB_PATH` | `./memory.db` | No | -| `MEMORY_CORS_ORIGIN` | `*` | No | +| `MEMORY_CORS_ORIGIN` | `*` | No (standalone mode only) | + +## Usage Modes + +### 1. Direct import + +Use the memory store and embedding functions directly in your code: + +```ts +import { SqliteMemoryStore, embed } from '@ekai/memory'; +``` + +### 2. Mountable router + +Mount memory endpoints into an existing Express app: + +```ts +import { createMemoryRouter } from '@ekai/memory'; + +const memoryRouter = createMemoryRouter(); +app.use(memoryRouter); +``` + +This is how the OpenRouter integration embeds memory on port `4010`. + +### 3. Standalone server + +Run memory as its own HTTP server (useful for development or isolated deployments): + +```bash +npm run start -w @ekai/memory +``` ## How It Works @@ -204,7 +237,7 @@ Get all memories scoped to a specific user. | DELETE | `/v1/graph/triple/:id` | Delete a triple | | GET | `/health` | Health check | -All endpoints support `profile` query/body param. +All endpoints support `profile` query/body param. In the default deployment, these are served on the OpenRouter port (`4010`). ## Retrieval Pipeline From 5ff79578acca52cf5cb7058c1f27ef7accfb62cf Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:54:38 +0530 Subject: [PATCH 26/78] added memory as a standalone package --- .env.example | 12 +- Dockerfile | 39 +- docker-compose.yaml | 4 +- integrations/openrouter/package.json | 3 + integrations/openrouter/src/config.ts | 2 +- integrations/openrouter/src/memory-client.ts | 81 ++++ integrations/openrouter/src/memory.test.ts | 60 +-- integrations/openrouter/src/memory.ts | 59 --- integrations/openrouter/src/server.ts | 24 +- memory/package.json | 2 +- memory/src/index.ts | 1 + memory/src/router.ts | 477 +++++++++++++++++++ memory/src/server.ts | 465 +----------------- package-lock.json | 12 +- package.json | 3 +- scripts/launcher.js | 8 - scripts/start-docker-fullstack.sh | 13 +- ui/dashboard/src/lib/constants.ts | 2 +- 18 files changed, 627 insertions(+), 640 deletions(-) create mode 100644 integrations/openrouter/src/memory-client.ts create mode 100644 memory/src/router.ts diff --git a/.env.example b/.env.example index 005ea8b..ca59f61 100644 --- a/.env.example +++ b/.env.example @@ -13,8 +13,11 @@ GOOGLE_API_KEY=your_key_here # Service ports PORT=3001 # Gateway # DASHBOARD_PORT=3000 # Dashboard -MEMORY_PORT=4005 # Memory -# OPENROUTER_PORT=4010 # OpenRouter integration +# OPENROUTER_PORT=4010 # OpenRouter integration (memory API served here too) + +# Memory is embedded in the OpenRouter process (no separate service). +# MEMORY_DB_PATH=./memory.db # SQLite path for memory store (used by OpenRouter) + # Optional x402 passthrough for OpenRouter access X402_BASE_URL=x402_supported_provider_url PRIVATE_KEY= @@ -23,12 +26,7 @@ PRIVATE_KEY= # OPENROUTER_PRICING_TIMEOUT_MS=4000 # OPENROUTER_PRICING_RETRIES=2 -# Memory service controls (file-based FIFO backend by default) -# MEMORY_BACKEND=file # set to "none" to disable -# MEMORY_MAX_ITEMS=20 - # Service toggles (all enabled by default, set to "false" to disable) # ENABLE_GATEWAY=true # ENABLE_DASHBOARD=true -# ENABLE_MEMORY=true # ENABLE_OPENROUTER=true diff --git a/Dockerfile b/Dockerfile index 6bb897c..c8826f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,8 +34,10 @@ FROM build-base AS memory-build WORKDIR /app/memory RUN npm run build -# ---------- openrouter build ---------- +# ---------- openrouter build (depends on memory being built first) ---------- FROM build-base AS openrouter-build +WORKDIR /app/memory +RUN npm run build WORKDIR /app/integrations/openrouter RUN npm run build @@ -63,24 +65,23 @@ COPY --from=dashboard-build /app/ui/dashboard/next.config.mjs ./next.config.mjs EXPOSE 3000 CMD ["node_modules/.bin/next", "start", "-p", "3000"] -# ---------- memory runtime ---------- -FROM node:20-alpine AS memory-runtime -WORKDIR /app/memory +# ---------- openrouter runtime (includes embedded memory) ---------- +FROM node:20-alpine AS openrouter-runtime +WORKDIR /app ENV NODE_ENV=production -COPY memory/package.json ./ -RUN npm install --omit=dev -COPY --from=memory-build /app/memory/dist ./dist -RUN mkdir -p /app/memory/data -EXPOSE 4005 -CMD ["node", "dist/server.js"] -# ---------- openrouter runtime ---------- -FROM node:20-alpine AS openrouter-runtime +# Memory package (workspace dependency of openrouter) +COPY memory/package.json ./memory/ +RUN cd memory && npm install --omit=dev +COPY --from=memory-build /app/memory/dist ./memory/dist + +# OpenRouter +COPY integrations/openrouter/package.json ./integrations/openrouter/ +RUN cd integrations/openrouter && npm install --omit=dev +COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist + +RUN mkdir -p /app/memory/data WORKDIR /app/integrations/openrouter -ENV NODE_ENV=production -COPY integrations/openrouter/package.json ./ -RUN npm install --omit=dev -COPY --from=openrouter-build /app/integrations/openrouter/dist ./dist EXPOSE 4010 CMD ["node", "dist/server.js"] @@ -104,13 +105,13 @@ COPY --from=dashboard-build /app/ui/dashboard/.next ./ui/dashboard/.next COPY --from=dashboard-build /app/ui/dashboard/public ./ui/dashboard/public COPY --from=dashboard-build /app/ui/dashboard/next.config.mjs ./ui/dashboard/next.config.mjs -# Memory +# Memory (workspace dependency of openrouter) COPY memory/package.json ./memory/ RUN cd memory && npm install --omit=dev COPY --from=memory-build /app/memory/dist ./memory/dist RUN mkdir -p /app/memory/data -# OpenRouter +# OpenRouter (depends on memory package above) COPY integrations/openrouter/package.json ./integrations/openrouter/ RUN cd integrations/openrouter && npm install --omit=dev COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist @@ -121,6 +122,6 @@ RUN chmod +x /app/start-docker-fullstack.sh ENV NODE_ENV=production -EXPOSE 3001 3000 4005 4010 +EXPOSE 3001 3000 4010 VOLUME ["/app/gateway/data", "/app/gateway/logs", "/app/memory/data"] CMD ["/app/start-docker-fullstack.sh"] diff --git a/docker-compose.yaml b/docker-compose.yaml index 6fa85a8..3f187d3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,9 +5,9 @@ services: environment: PORT: ${PORT:-3001} UI_PORT: ${UI_PORT:-3000} - MEMORY_PORT: ${MEMORY_PORT:-4005} OPENROUTER_PORT: ${OPENROUTER_PORT:-4010} NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3001} + NEXT_PUBLIC_MEMORY_PORT: ${OPENROUTER_PORT:-4010} DATABASE_PATH: /app/gateway/data/proxy.db MEMORY_DB_PATH: /app/memory/data/memory.db OPENAI_API_KEY: ${OPENAI_API_KEY:-} @@ -17,12 +17,10 @@ services: GOOGLE_API_KEY: ${GOOGLE_API_KEY:-} ENABLE_GATEWAY: ${ENABLE_GATEWAY:-true} ENABLE_DASHBOARD: ${ENABLE_DASHBOARD:-true} - ENABLE_MEMORY: ${ENABLE_MEMORY:-true} ENABLE_OPENROUTER: ${ENABLE_OPENROUTER:-true} ports: - "3001:3001" - "3000:3000" - - "4005:4005" - "4010:4010" volumes: - gateway_logs:/app/gateway/logs diff --git a/integrations/openrouter/package.json b/integrations/openrouter/package.json index 006b852..6b29fbc 100644 --- a/integrations/openrouter/package.json +++ b/integrations/openrouter/package.json @@ -10,10 +10,13 @@ "start": "node dist/server.js" }, "dependencies": { + "@ekai/memory": "*", + "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2" }, "devDependencies": { + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "tsx": "^4.7.0", "typescript": "^5.3.3", diff --git a/integrations/openrouter/src/config.ts b/integrations/openrouter/src/config.ts index 7676569..1efc578 100644 --- a/integrations/openrouter/src/config.ts +++ b/integrations/openrouter/src/config.ts @@ -22,5 +22,5 @@ if (!OPENROUTER_API_KEY) { process.exit(1); } -export const MEMORY_URL = process.env.MEMORY_URL || 'http://localhost:4005'; +export const MEMORY_DB_PATH = process.env.MEMORY_DB_PATH ?? './memory.db'; export const PORT = parseInt(process.env.OPENROUTER_PORT || '4010', 10); diff --git a/integrations/openrouter/src/memory-client.ts b/integrations/openrouter/src/memory-client.ts new file mode 100644 index 0000000..5b9ab18 --- /dev/null +++ b/integrations/openrouter/src/memory-client.ts @@ -0,0 +1,81 @@ +import type { SqliteMemoryStore } from '@ekai/memory'; +import { extract } from '@ekai/memory'; + +interface QueryResult { + sector: 'episodic' | 'semantic' | 'procedural' | 'reflective'; + content: string; + score: number; + details?: { + subject?: string; + predicate?: string; + object?: string; + domain?: string; + trigger?: string; + steps?: string[]; + goal?: string; + }; +} + +let store: SqliteMemoryStore | null = null; + +/** + * Initialize the memory store reference. Called once at startup. + */ +export function initMemoryStore(s: SqliteMemoryStore): void { + store = s; +} + +/** + * Fetch memory context by querying the store directly. + * Returns null on any failure — memory is additive, never blocking. + */ +export async function fetchMemoryContext( + query: string, + profile: string, + userId?: string, +): Promise { + if (!store) { + console.warn('[memory] store not initialized'); + return null; + } + try { + const data = await store.query(query, profile, userId); + return data.workingMemory?.length ? data.workingMemory : null; + } catch (err: any) { + console.warn(`[memory] search failed: ${err.message}`); + return null; + } +} + +/** + * Fire-and-forget: extract and ingest messages into the memory store. + * Never awaited — failures are logged and swallowed. + */ +export function ingestMessages( + messages: Array<{ role: string; content: string }>, + profile: string, +): void { + if (!store) { + console.warn('[memory] store not initialized, skipping ingest'); + return; + } + + const allMessages = messages.filter((m) => m.content?.trim()); + const sourceText = allMessages + .map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content.trim()}`) + .join('\n\n'); + + if (!sourceText) return; + + // Fire-and-forget: extract then ingest + extract(sourceText) + .then((components) => { + if (!components || !store) return; + return store.ingest(components, profile, { + origin: { originType: 'conversation' }, + }); + }) + .catch((err) => { + console.warn(`[memory] ingest failed: ${err.message}`); + }); +} diff --git a/integrations/openrouter/src/memory.test.ts b/integrations/openrouter/src/memory.test.ts index eb82bba..2601a9b 100644 --- a/integrations/openrouter/src/memory.test.ts +++ b/integrations/openrouter/src/memory.test.ts @@ -1,11 +1,6 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; -// Mock config before importing memory module -vi.mock('./config.js', () => ({ - MEMORY_URL: 'http://localhost:4005', -})); - -import { fetchMemoryContext, formatMemoryBlock, ingestMessages, injectMemory } from './memory.js'; +import { formatMemoryBlock, injectMemory } from './memory.js'; describe('formatMemoryBlock', () => { it('formats semantic facts under "What I know:"', () => { @@ -118,57 +113,6 @@ describe('formatMemoryBlock', () => { }); }); -describe('ingestMessages', () => { - beforeEach(() => { - vi.stubGlobal('fetch', vi.fn()); - vi.spyOn(console, 'warn').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('POSTs messages and profile to /v1/ingest', async () => { - const mockFetch = vi.mocked(fetch); - mockFetch.mockResolvedValueOnce(new Response('ok', { status: 200 })); - - const messages = [ - { role: 'user', content: 'My dog is named Luna' }, - ]; - - ingestMessages(messages, 'test-profile'); - - // Let the fire-and-forget promise settle - await vi.waitFor(() => { - expect(mockFetch).toHaveBeenCalledOnce(); - }); - - const [url, options] = mockFetch.mock.calls[0]; - expect(url).toBe('http://localhost:4005/v1/ingest'); - expect(options).toMatchObject({ - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - - const body = JSON.parse(options!.body as string); - expect(body.messages).toEqual(messages); - expect(body.profile).toBe('test-profile'); - }); - - it('logs and swallows fetch errors', async () => { - const mockFetch = vi.mocked(fetch); - mockFetch.mockRejectedValueOnce(new Error('connection refused')); - - ingestMessages([{ role: 'user', content: 'hello' }], 'default'); - - await vi.waitFor(() => { - expect(console.warn).toHaveBeenCalledWith( - '[memory] ingest failed: connection refused', - ); - }); - }); -}); - describe('injectMemory', () => { it('does not mutate the original messages array', () => { const messages = [{ role: 'user', content: 'hello' }]; diff --git a/integrations/openrouter/src/memory.ts b/integrations/openrouter/src/memory.ts index 5492a86..5c6e73d 100644 --- a/integrations/openrouter/src/memory.ts +++ b/integrations/openrouter/src/memory.ts @@ -1,5 +1,3 @@ -import { MEMORY_URL } from './config.js'; - interface QueryResult { sector: 'episodic' | 'semantic' | 'procedural' | 'reflective'; content: string; @@ -17,46 +15,6 @@ interface QueryResult { }; } -interface SearchResponse { - workingMemory: QueryResult[]; - perSector: Record; - profileId: string; -} - -/** - * Fetch memory context from the memory service. - * Returns null on any failure — memory is additive, never blocking. - */ -export async function fetchMemoryContext( - query: string, - profile: string, - userId?: string, -): Promise { - try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 3000); - - const res = await fetch(`${MEMORY_URL}/v1/search`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, profile, userId }), - signal: controller.signal, - }); - clearTimeout(timeout); - - if (!res.ok) { - console.warn(`[memory] search returned ${res.status}`); - return null; - } - - const data = (await res.json()) as SearchResponse; - return data.workingMemory?.length ? data.workingMemory : null; - } catch (err: any) { - console.warn(`[memory] search failed: ${err.message}`); - return null; - } -} - /** * Format memory results into a system message block, grouped by sector. * Uses agent-voice section names. @@ -109,20 +67,3 @@ export function injectMemory( } return [{ role: 'system', content: memoryBlock }, ...messages]; } - -/** - * Fire-and-forget: send messages to the memory service for ingestion. - * Never awaited — failures are logged and swallowed. - */ -export function ingestMessages( - messages: Array<{ role: string; content: string }>, - profile: string, -): void { - fetch(`${MEMORY_URL}/v1/ingest`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ messages, profile }), - }).catch((err) => { - console.warn(`[memory] ingest failed: ${err.message}`); - }); -} diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts index 5ca0b43..f1d43ae 100644 --- a/integrations/openrouter/src/server.ts +++ b/integrations/openrouter/src/server.ts @@ -1,11 +1,29 @@ import express from 'express'; -import { PORT } from './config.js'; -import { fetchMemoryContext, formatMemoryBlock, ingestMessages, injectMemory } from './memory.js'; +import cors from 'cors'; +import { SqliteMemoryStore, embed, createMemoryRouter } from '@ekai/memory'; +import { PORT, MEMORY_DB_PATH } from './config.js'; +import { initMemoryStore, fetchMemoryContext, ingestMessages } from './memory-client.js'; +import { formatMemoryBlock, injectMemory } from './memory.js'; import { proxyToOpenRouter } from './proxy.js'; const app = express(); + +const corsOrigins = + process.env.MEMORY_CORS_ORIGIN?.split(',').map((s) => s.trim()).filter(Boolean) ?? '*'; +app.use(cors({ origin: corsOrigins })); +app.options('*', cors({ origin: corsOrigins })); app.use(express.json({ limit: '10mb' })); +// Initialize embedded memory store +const store = new SqliteMemoryStore({ + dbPath: MEMORY_DB_PATH, + embed, +}); +initMemoryStore(store); + +// Mount memory admin routes (dashboard, graph, etc.) +app.use(createMemoryRouter(store)); + app.get('/health', (_req, res) => { res.json({ status: 'ok' }); }); @@ -58,5 +76,5 @@ app.post('/v1/chat/completions', async (req, res) => { }); app.listen(PORT, () => { - console.log(`@ekai/openrouter listening on port ${PORT}`); + console.log(`@ekai/openrouter listening on port ${PORT} (memory embedded, db at ${MEMORY_DB_PATH})`); }); diff --git a/memory/package.json b/memory/package.json index c6e8098..4965289 100644 --- a/memory/package.json +++ b/memory/package.json @@ -1,5 +1,5 @@ { - "name": "memory", + "name": "@ekai/memory", "version": "0.1.0", "description": "Neuroscience-inspired cognitive memory kernel", "private": true, diff --git a/memory/src/index.ts b/memory/src/index.ts index 63b97f9..5ffb933 100644 --- a/memory/src/index.ts +++ b/memory/src/index.ts @@ -7,3 +7,4 @@ export * from './scoring.js'; export * from './wm.js'; export * from './utils.js'; export * from './documents.js'; +export * from './router.js'; diff --git a/memory/src/router.ts b/memory/src/router.ts new file mode 100644 index 0000000..e97ae22 --- /dev/null +++ b/memory/src/router.ts @@ -0,0 +1,477 @@ +import { Router } from 'express'; +import type { Request, Response } from 'express'; +import type { SqliteMemoryStore } from './sqlite-store.js'; +import { extract } from './providers/extract.js'; +import { normalizeProfileSlug } from './utils.js'; +import type { IngestComponents } from './types.js'; +import { ingestDocuments } from './documents.js'; + +/** + * Creates an Express Router with all memory API routes. + * The store is received via closure — no global state needed. + */ +export function createMemoryRouter(store: SqliteMemoryStore): Router { + const router = Router(); + + router.get('/v1/profiles', (_req: Request, res: Response) => { + try { + const profiles = store.getAvailableProfiles(); + res.json({ profiles }); + } catch (err: any) { + res.status(500).json({ error: err.message ?? 'failed to fetch profiles' }); + } + }); + + const handleDeleteProfile = (req: Request, res: Response) => { + try { + const { slug } = req.params; + const normalizedProfile = normalizeProfileSlug(slug); + const deleted = store.deleteProfile(normalizedProfile); + res.json({ deleted, profile: normalizedProfile }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + if (err?.message === 'cannot_delete_default_profile') { + return res.status(400).json({ error: 'default_profile_protected' }); + } + res.status(500).json({ error: err.message ?? 'delete profile failed' }); + } + }; + router.delete('/v1/profiles/:slug', handleDeleteProfile); + + router.post('/v1/ingest', async (req: Request, res: Response) => { + const { messages, profile, userId } = req.body as { + messages?: Array<{ role: 'user' | 'assistant' | string; content: string }>; + profile?: string; + userId?: string; + }; + + let normalizedProfile: string; + try { + normalizedProfile = normalizeProfileSlug(profile); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + return res.status(500).json({ error: 'profile_normalization_failed' }); + } + + if (!messages || !messages.length) { + return res.status(400).json({ error: 'messages is required and must include at least one item' }); + } + const userMessages = messages.filter((m) => m.role === 'user' && m.content?.trim()); + if (!userMessages.length) { + return res.status(400).json({ error: 'at least one user message with content is required' }); + } + + // Pass full conversation (user + assistant) to extraction for agent-centric reflection + const allMessages = messages.filter((m) => m.content?.trim()); + const sourceText = allMessages + .map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content.trim()}`) + .join('\n\n'); + + let finalComponents: IngestComponents | undefined; + + try { + finalComponents = await extract(sourceText); + } catch (err: any) { + return res.status(500).json({ error: err.message ?? 'extraction failed' }); + } + + if (!finalComponents) { + return res.status(400).json({ error: 'unable to extract components from messages' }); + } + try { + const rows = await store.ingest(finalComponents, normalizedProfile, { + origin: { originType: 'conversation', originActor: userId }, + userId, + }); + res.json({ stored: rows.length, ids: rows.map((r) => r.id), profile: normalizedProfile }); + } catch (err: any) { + res.status(500).json({ error: err.message ?? 'ingest failed' }); + } + }); + + router.post('/v1/ingest/documents', async (req: Request, res: Response) => { + const { path: docPath, profile } = req.body as { + path?: string; + profile?: string; + }; + + if (!docPath || !docPath.trim()) { + return res.status(400).json({ error: 'path_required' }); + } + + let normalizedProfile: string; + try { + normalizedProfile = normalizeProfileSlug(profile); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + return res.status(500).json({ error: 'profile_normalization_failed' }); + } + + // Validate path exists + try { + const fs = await import('node:fs/promises'); + await fs.stat(docPath.trim()); + } catch { + return res.status(400).json({ error: 'path_not_found' }); + } + + try { + const result = await ingestDocuments(docPath.trim(), store, normalizedProfile); + res.json(result); + } catch (err: any) { + res.status(500).json({ error: err.message ?? 'document ingestion failed' }); + } + }); + + router.get('/v1/summary', (req: Request, res: Response) => { + try { + const limit = Number(req.query.limit) || 50; + const profile = req.query.profile as string; + const normalizedProfile = normalizeProfileSlug(profile); + const summary = store.getSectorSummary(normalizedProfile); + const recent = store.getRecent(normalizedProfile, limit).map((r) => ({ + id: r.id, + sector: r.sector, + profile: r.profileId, + createdAt: r.createdAt, + lastAccessed: r.lastAccessed, + preview: r.content, + retrievalCount: (r as any).retrievalCount ?? 0, + details: (r as any).details, + })); + res.json({ summary, recent, profile: normalizedProfile }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'summary failed' }); + } + }); + + router.put('/v1/memory/:id', async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { content, sector, profile } = req.body as { content?: string; sector?: string; profile?: string }; + + if (!id) return res.status(400).json({ error: 'id_required' }); + if (!content || !content.trim()) { + return res.status(400).json({ error: 'content_required' }); + } + + let normalizedProfile: string; + try { + normalizedProfile = normalizeProfileSlug(profile); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + return res.status(500).json({ error: 'profile_normalization_failed' }); + } + + const updated = await store.updateById(id, content.trim(), sector as any, normalizedProfile); + if (!updated) { + return res.status(404).json({ error: 'not_found', id }); + } + res.json({ updated: true, id, profile: normalizedProfile }); + } catch (err: any) { + res.status(500).json({ error: err.message ?? 'update failed' }); + } + }); + + router.delete('/v1/memory/:id', (req: Request, res: Response) => { + try { + const { id } = req.params; + const profile = req.query.profile as string; + if (!id) return res.status(400).json({ error: 'id_required' }); + let normalizedProfile: string; + try { + normalizedProfile = normalizeProfileSlug(profile); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + return res.status(500).json({ error: 'profile_normalization_failed' }); + } + const deleted = store.deleteById(id, normalizedProfile); + if (!deleted) { + return res.status(404).json({ error: 'not_found', id }); + } + res.json({ deleted, profile: normalizedProfile }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'delete failed' }); + } + }); + + router.delete('/v1/memory', (req: Request, res: Response) => { + try { + const profile = req.query.profile as string; + const normalizedProfile = normalizeProfileSlug(profile); + const deleted = store.deleteAll(normalizedProfile); + res.json({ deleted, profile: normalizedProfile }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'delete all failed' }); + } + }); + + // Delete a single semantic graph triple by id + router.delete('/v1/graph/triple/:id', (req: Request, res: Response) => { + try { + const { id } = req.params; + const profile = req.query.profile as string; + if (!id) return res.status(400).json({ error: 'id_required' }); + + const deleted = store.deleteSemanticById(id, profile); + if (!deleted) { + return res.status(404).json({ error: 'not_found', id }); + } + + res.json({ deleted }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'triple delete failed' }); + } + }); + + router.post('/v1/search', async (req: Request, res: Response) => { + const { query, profile, userId } = req.body as { query?: string; profile?: string; userId?: string }; + if (!query || !query.trim()) { + return res.status(400).json({ error: 'query is required' }); + } + try { + const result = await store.query(query, profile, userId); + res.json(result); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'query failed' }); + } + }); + + // --- Agent Users --- + + router.get('/v1/users', (req: Request, res: Response) => { + try { + const profile = req.query.profile as string; + const normalizedProfile = normalizeProfileSlug(profile); + const users = store.getAgentUsers(normalizedProfile); + res.json({ users, profile: normalizedProfile }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'failed to fetch users' }); + } + }); + + router.get('/v1/users/:id/memories', (req: Request, res: Response) => { + try { + const { id: userId } = req.params; + const profile = req.query.profile as string; + const limit = Number(req.query.limit) || 50; + const normalizedProfile = normalizeProfileSlug(profile); + + if (!userId) { + return res.status(400).json({ error: 'user_id_required' }); + } + + const memories = store.getMemoriesForUser(normalizedProfile, userId, limit).map((r) => ({ + id: r.id, + sector: r.sector, + profile: r.profileId, + createdAt: r.createdAt, + lastAccessed: r.lastAccessed, + preview: r.content, + details: (r as any).details, + })); + res.json({ memories, userId, profile: normalizedProfile }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'failed to fetch user memories' }); + } + }); + + // Graph traversal endpoints + router.get('/v1/graph/triples', (req: Request, res: Response) => { + try { + const { entity, direction, maxResults, predicate, profile } = req.query; + if (!entity || typeof entity !== 'string') { + return res.status(400).json({ error: 'entity query parameter is required' }); + } + + const options: any = { + maxResults: maxResults ? Number(maxResults) : 100, + predicateFilter: predicate as string | undefined, + profile: profile as string, + }; + + let triples; + if (direction === 'incoming' || direction === 'in') { + triples = store.graph.findTriplesByObject(entity, options); + } else if (direction === 'outgoing' || direction === 'out') { + triples = store.graph.findTriplesBySubject(entity, options); + } else { + triples = store.graph.findConnectedTriples(entity, options); + } + + res.json({ entity, triples, count: triples.length }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'graph query failed' }); + } + }); + + router.get('/v1/graph/neighbors', (req: Request, res: Response) => { + try { + const { entity, profile } = req.query; + if (!entity || typeof entity !== 'string') { + return res.status(400).json({ error: 'entity query parameter is required' }); + } + + const neighbors = Array.from(store.graph.findNeighbors(entity, { profile: profile as string })); + res.json({ entity, neighbors, count: neighbors.length }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'neighbors query failed' }); + } + }); + + router.get('/v1/graph/paths', (req: Request, res: Response) => { + try { + const { from, to, maxDepth, profile } = req.query; + if (!from || typeof from !== 'string' || !to || typeof to !== 'string') { + return res.status(400).json({ error: 'from and to query parameters are required' }); + } + + const paths = store.graph.findPaths(from, to, { + maxDepth: maxDepth ? Number(maxDepth) : 3, + profile: profile as string, + }); + + res.json({ from, to, paths, count: paths.length }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'paths query failed' }); + } + }); + + router.get('/v1/graph/visualization', (req: Request, res: Response) => { + try { + const { entity, maxDepth, maxNodes, profile, includeHistory } = req.query; + const profileValue = profile as string; + const normalizedProfile = normalizeProfileSlug(profileValue); + const centerEntity = (entity as string) || null; + const depth = maxDepth ? Number(maxDepth) : 2; + const nodeLimit = maxNodes ? Number(maxNodes) : 50; + const showHistory = includeHistory === 'true' || includeHistory === '1'; + + // If no center entity, get a sample of semantic triples + if (!centerEntity) { + const allSemantic = store.getRecent(normalizedProfile, 100).filter((r) => r.sector === 'semantic'); + const triples = allSemantic + .slice(0, nodeLimit) + .map((r) => (r as any).details) + .filter((d) => d && d.subject && d.predicate && d.object) + .filter((d) => showHistory || !d.validTo); // Filter out historical if not requested + + const nodes = new Set(); + const edges: Array<{ source: string; target: string; predicate: string; isHistorical?: boolean }> = []; + + for (const triple of triples) { + nodes.add(triple.subject); + nodes.add(triple.object); + edges.push({ + source: triple.subject, + target: triple.object, + predicate: triple.predicate, + isHistorical: triple.validTo != null, + }); + } + + return res.json({ + nodes: Array.from(nodes).map((id) => ({ id, label: id })), + edges, + includeHistory: showHistory, + profile: normalizedProfile, + }); + } + + // Build graph from center entity + const graphOptions = { maxDepth: depth, profile: normalizedProfile, includeInvalidated: showHistory }; + const reachable = store.graph.findReachableEntities(centerEntity, graphOptions); + const nodes = new Set([centerEntity]); + const edges: Array<{ source: string; target: string; predicate: string; isHistorical?: boolean }> = []; + + // Add center entity's connections + const centerTriples = store.graph.findConnectedTriples(centerEntity, { maxResults: 100, profile: normalizedProfile, includeInvalidated: showHistory }); + for (const triple of centerTriples) { + nodes.add(triple.subject); + nodes.add(triple.object); + edges.push({ + source: triple.subject, + target: triple.object, + predicate: triple.predicate, + isHistorical: triple.validTo != null, + }); + } + + // Add connections for reachable entities (up to node limit) + const reachableArray = Array.from(reachable.entries()) + .sort((a, b) => a[1] - b[1]) + .slice(0, nodeLimit - nodes.size); + + for (const [entity, _depth] of reachableArray) { + const entityTriples = store.graph.findTriplesBySubject(entity, { maxResults: 10, profile: normalizedProfile, includeInvalidated: showHistory }); + for (const triple of entityTriples) { + if (nodes.has(triple.subject) || nodes.has(triple.object)) { + nodes.add(triple.subject); + nodes.add(triple.object); + edges.push({ + source: triple.subject, + target: triple.object, + predicate: triple.predicate, + isHistorical: triple.validTo != null, + }); + } + } + } + + res.json({ + center: centerEntity, + nodes: Array.from(nodes).map((id) => ({ id, label: id })), + edges, + includeHistory: showHistory, + profile: normalizedProfile, + }); + } catch (err: any) { + if (err?.message === 'invalid_profile') { + return res.status(400).json({ error: 'invalid_profile' }); + } + res.status(500).json({ error: err.message ?? 'visualization query failed' }); + } + }); + + return router; +} diff --git a/memory/src/server.ts b/memory/src/server.ts index aa5f475..8024bb7 100644 --- a/memory/src/server.ts +++ b/memory/src/server.ts @@ -10,10 +10,7 @@ import express from 'express'; import cors from 'cors'; import { SqliteMemoryStore } from './sqlite-store.js'; import { embed } from './providers/embed.js'; -import { extract } from './providers/extract.js'; -import { normalizeProfileSlug } from './utils.js'; -import type { IngestComponents } from './types.js'; -import { ingestDocuments } from './documents.js'; +import { createMemoryRouter } from './router.js'; const PORT = Number(process.env.MEMORY_PORT ?? 4005); const DB_PATH = process.env.MEMORY_DB_PATH ?? './memory.db'; @@ -39,465 +36,7 @@ async function main() { res.json({ status: 'ok' }); }); - app.get('/v1/profiles', (_req, res) => { - try { - const profiles = store.getAvailableProfiles(); - res.json({ profiles }); - } catch (err: any) { - res.status(500).json({ error: err.message ?? 'failed to fetch profiles' }); - } - }); - - const handleDeleteProfile: express.RequestHandler = (req, res) => { - try { - const { slug } = req.params; - const normalizedProfile = normalizeProfileSlug(slug); - const deleted = store.deleteProfile(normalizedProfile); - res.json({ deleted, profile: normalizedProfile }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - if (err?.message === 'cannot_delete_default_profile') { - return res.status(400).json({ error: 'default_profile_protected' }); - } - res.status(500).json({ error: err.message ?? 'delete profile failed' }); - } - }; - app.delete('/v1/profiles/:slug', handleDeleteProfile); - - app.post('/v1/ingest', async (req, res) => { - const { messages, profile, userId } = req.body as { - messages?: Array<{ role: 'user' | 'assistant' | string; content: string }>; - profile?: string; - userId?: string; - }; - - let normalizedProfile: string; - try { - normalizedProfile = normalizeProfileSlug(profile); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - return res.status(500).json({ error: 'profile_normalization_failed' }); - } - - if (!messages || !messages.length) { - return res.status(400).json({ error: 'messages is required and must include at least one item' }); - } - const userMessages = messages.filter((m) => m.role === 'user' && m.content?.trim()); - if (!userMessages.length) { - return res.status(400).json({ error: 'at least one user message with content is required' }); - } - - // Pass full conversation (user + assistant) to extraction for agent-centric reflection - const allMessages = messages.filter((m) => m.content?.trim()); - const sourceText = allMessages - .map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content.trim()}`) - .join('\n\n'); - - let finalComponents: IngestComponents | undefined; - - try { - finalComponents = await extract(sourceText); - } catch (err: any) { - return res.status(500).json({ error: err.message ?? 'extraction failed' }); - } - - if (!finalComponents) { - return res.status(400).json({ error: 'unable to extract components from messages' }); - } - try { - const rows = await store.ingest(finalComponents, normalizedProfile, { - origin: { originType: 'conversation', originActor: userId }, - userId, - }); - res.json({ stored: rows.length, ids: rows.map((r) => r.id), profile: normalizedProfile }); - } catch (err: any) { - res.status(500).json({ error: err.message ?? 'ingest failed' }); - } - }); - - app.post('/v1/ingest/documents', async (req, res) => { - const { path: docPath, profile } = req.body as { - path?: string; - profile?: string; - }; - - if (!docPath || !docPath.trim()) { - return res.status(400).json({ error: 'path_required' }); - } - - let normalizedProfile: string; - try { - normalizedProfile = normalizeProfileSlug(profile); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - return res.status(500).json({ error: 'profile_normalization_failed' }); - } - - // Validate path exists - try { - const fs = await import('node:fs/promises'); - await fs.stat(docPath.trim()); - } catch { - return res.status(400).json({ error: 'path_not_found' }); - } - - try { - const result = await ingestDocuments(docPath.trim(), store, normalizedProfile); - res.json(result); - } catch (err: any) { - res.status(500).json({ error: err.message ?? 'document ingestion failed' }); - } - }); - - app.get('/v1/summary', (req, res) => { - try { - const limit = Number(req.query.limit) || 50; - const profile = req.query.profile as string; - const normalizedProfile = normalizeProfileSlug(profile); - const summary = store.getSectorSummary(normalizedProfile); - const recent = store.getRecent(normalizedProfile, limit).map((r) => ({ - id: r.id, - sector: r.sector, - profile: r.profileId, - createdAt: r.createdAt, - lastAccessed: r.lastAccessed, - preview: r.content, - retrievalCount: (r as any).retrievalCount ?? 0, - details: (r as any).details, - })); - res.json({ summary, recent, profile: normalizedProfile }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'summary failed' }); - } - }); - - app.put('/v1/memory/:id', async (req, res) => { - try { - const { id } = req.params; - const { content, sector, profile } = req.body as { content?: string; sector?: string; profile?: string }; - - if (!id) return res.status(400).json({ error: 'id_required' }); - if (!content || !content.trim()) { - return res.status(400).json({ error: 'content_required' }); - } - - let normalizedProfile: string; - try { - normalizedProfile = normalizeProfileSlug(profile); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - return res.status(500).json({ error: 'profile_normalization_failed' }); - } - - const updated = await store.updateById(id, content.trim(), sector as any, normalizedProfile); - if (!updated) { - return res.status(404).json({ error: 'not_found', id }); - } - res.json({ updated: true, id, profile: normalizedProfile }); - } catch (err: any) { - res.status(500).json({ error: err.message ?? 'update failed' }); - } - }); - - app.delete('/v1/memory/:id', (req, res) => { - try { - const { id } = req.params; - const profile = req.query.profile as string; - if (!id) return res.status(400).json({ error: 'id_required' }); - let normalizedProfile: string; - try { - normalizedProfile = normalizeProfileSlug(profile); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - return res.status(500).json({ error: 'profile_normalization_failed' }); - } - const deleted = store.deleteById(id, normalizedProfile); - if (!deleted) { - return res.status(404).json({ error: 'not_found', id }); - } - res.json({ deleted, profile: normalizedProfile }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'delete failed' }); - } - }); - - app.delete('/v1/memory', (req, res) => { - try { - const profile = req.query.profile as string; - const normalizedProfile = normalizeProfileSlug(profile); - const deleted = store.deleteAll(normalizedProfile); - res.json({ deleted, profile: normalizedProfile }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'delete all failed' }); - } - }); - - // Delete a single semantic graph triple by id - app.delete('/v1/graph/triple/:id', (req, res) => { - try { - const { id } = req.params; - const profile = req.query.profile as string; - if (!id) return res.status(400).json({ error: 'id_required' }); - - const deleted = store.deleteSemanticById(id, profile); - if (!deleted) { - return res.status(404).json({ error: 'not_found', id }); - } - - res.json({ deleted }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'triple delete failed' }); - } - }); - - app.post('/v1/search', async (req, res) => { - const { query, profile, userId } = req.body as { query?: string; profile?: string; userId?: string }; - if (!query || !query.trim()) { - return res.status(400).json({ error: 'query is required' }); - } - try { - const result = await store.query(query, profile, userId); - res.json(result); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'query failed' }); - } - }); - - // --- Agent Users --- - - app.get('/v1/users', (req, res) => { - try { - const profile = req.query.profile as string; - const normalizedProfile = normalizeProfileSlug(profile); - const users = store.getAgentUsers(normalizedProfile); - res.json({ users, profile: normalizedProfile }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'failed to fetch users' }); - } - }); - - app.get('/v1/users/:id/memories', (req, res) => { - try { - const { id: userId } = req.params; - const profile = req.query.profile as string; - const limit = Number(req.query.limit) || 50; - const normalizedProfile = normalizeProfileSlug(profile); - - if (!userId) { - return res.status(400).json({ error: 'user_id_required' }); - } - - const memories = store.getMemoriesForUser(normalizedProfile, userId, limit).map((r) => ({ - id: r.id, - sector: r.sector, - profile: r.profileId, - createdAt: r.createdAt, - lastAccessed: r.lastAccessed, - preview: r.content, - details: (r as any).details, - })); - res.json({ memories, userId, profile: normalizedProfile }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'failed to fetch user memories' }); - } - }); - - // Graph traversal endpoints - app.get('/v1/graph/triples', (req, res) => { - try { - const { entity, direction, maxResults, predicate, profile } = req.query; - if (!entity || typeof entity !== 'string') { - return res.status(400).json({ error: 'entity query parameter is required' }); - } - - const options: any = { - maxResults: maxResults ? Number(maxResults) : 100, - predicateFilter: predicate as string | undefined, - profile: profile as string, - }; - - let triples; - if (direction === 'incoming' || direction === 'in') { - triples = store.graph.findTriplesByObject(entity, options); - } else if (direction === 'outgoing' || direction === 'out') { - triples = store.graph.findTriplesBySubject(entity, options); - } else { - triples = store.graph.findConnectedTriples(entity, options); - } - - res.json({ entity, triples, count: triples.length }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'graph query failed' }); - } - }); - - app.get('/v1/graph/neighbors', (req, res) => { - try { - const { entity, profile } = req.query; - if (!entity || typeof entity !== 'string') { - return res.status(400).json({ error: 'entity query parameter is required' }); - } - - const neighbors = Array.from(store.graph.findNeighbors(entity, { profile: profile as string })); - res.json({ entity, neighbors, count: neighbors.length }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'neighbors query failed' }); - } - }); - - app.get('/v1/graph/paths', (req, res) => { - try { - const { from, to, maxDepth, profile } = req.query; - if (!from || typeof from !== 'string' || !to || typeof to !== 'string') { - return res.status(400).json({ error: 'from and to query parameters are required' }); - } - - const paths = store.graph.findPaths(from, to, { - maxDepth: maxDepth ? Number(maxDepth) : 3, - profile: profile as string, - }); - - res.json({ from, to, paths, count: paths.length }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'paths query failed' }); - } - }); - - app.get('/v1/graph/visualization', (req, res) => { - try { - const { entity, maxDepth, maxNodes, profile, includeHistory } = req.query; - const profileValue = profile as string; - const normalizedProfile = normalizeProfileSlug(profileValue); - const centerEntity = (entity as string) || null; - const depth = maxDepth ? Number(maxDepth) : 2; - const nodeLimit = maxNodes ? Number(maxNodes) : 50; - const showHistory = includeHistory === 'true' || includeHistory === '1'; - - // If no center entity, get a sample of semantic triples - if (!centerEntity) { - const allSemantic = store.getRecent(normalizedProfile, 100).filter((r) => r.sector === 'semantic'); - const triples = allSemantic - .slice(0, nodeLimit) - .map((r) => (r as any).details) - .filter((d) => d && d.subject && d.predicate && d.object) - .filter((d) => showHistory || !d.validTo); // Filter out historical if not requested - - const nodes = new Set(); - const edges: Array<{ source: string; target: string; predicate: string; isHistorical?: boolean }> = []; - - for (const triple of triples) { - nodes.add(triple.subject); - nodes.add(triple.object); - edges.push({ - source: triple.subject, - target: triple.object, - predicate: triple.predicate, - isHistorical: triple.validTo != null, - }); - } - - return res.json({ - nodes: Array.from(nodes).map((id) => ({ id, label: id })), - edges, - includeHistory: showHistory, - profile: normalizedProfile, - }); - } - - // Build graph from center entity - const graphOptions = { maxDepth: depth, profile: normalizedProfile, includeInvalidated: showHistory }; - const reachable = store.graph.findReachableEntities(centerEntity, graphOptions); - const nodes = new Set([centerEntity]); - const edges: Array<{ source: string; target: string; predicate: string; isHistorical?: boolean }> = []; - - // Add center entity's connections - const centerTriples = store.graph.findConnectedTriples(centerEntity, { maxResults: 100, profile: normalizedProfile, includeInvalidated: showHistory }); - for (const triple of centerTriples) { - nodes.add(triple.subject); - nodes.add(triple.object); - edges.push({ - source: triple.subject, - target: triple.object, - predicate: triple.predicate, - isHistorical: triple.validTo != null, - }); - } - - // Add connections for reachable entities (up to node limit) - const reachableArray = Array.from(reachable.entries()) - .sort((a, b) => a[1] - b[1]) - .slice(0, nodeLimit - nodes.size); - - for (const [entity, _depth] of reachableArray) { - const entityTriples = store.graph.findTriplesBySubject(entity, { maxResults: 10, profile: normalizedProfile, includeInvalidated: showHistory }); - for (const triple of entityTriples) { - if (nodes.has(triple.subject) || nodes.has(triple.object)) { - nodes.add(triple.subject); - nodes.add(triple.object); - edges.push({ - source: triple.subject, - target: triple.object, - predicate: triple.predicate, - isHistorical: triple.validTo != null, - }); - } - } - } - - res.json({ - center: centerEntity, - nodes: Array.from(nodes).map((id) => ({ id, label: id })), - edges, - includeHistory: showHistory, - profile: normalizedProfile, - }); - } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); - } - res.status(500).json({ error: err.message ?? 'visualization query failed' }); - } - }); + app.use(createMemoryRouter(store)); // Fallback 404 with CORS headers app.use((req, res) => { diff --git a/package-lock.json b/package-lock.json index b7df654..e804753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,10 +74,13 @@ "name": "@ekai/openrouter", "version": "0.1.0", "dependencies": { + "@ekai/memory": "*", + "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2" }, "devDependencies": { + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "tsx": "^4.7.0", "typescript": "^5.3.3", @@ -380,6 +383,7 @@ } }, "memory": { + "name": "@ekai/memory", "version": "0.1.0", "dependencies": { "better-sqlite3": "^12.2.0", @@ -807,6 +811,10 @@ "dev": true, "license": "MIT" }, + "node_modules/@ekai/memory": { + "resolved": "memory", + "link": true + }, "node_modules/@ekai/openrouter": { "resolved": "integrations/openrouter", "link": true @@ -8627,10 +8635,6 @@ "node": ">= 0.6" } }, - "node_modules/memory": { - "resolved": "memory", - "link": true - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", diff --git a/package.json b/package.json index 591ad12..16086ca 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,7 @@ "dev:ui": "npm run dev --workspace=ui/dashboard", "dev:openrouter": "npm run dev --workspace=@ekai/openrouter", "start:gateway": "npm run start --workspace=gateway", - "start:ui": "npx --workspace=ui/dashboard next start -p 3000 -H 0.0.0.0", - "start:memory": "npm run start --workspace=memory" + "start:ui": "npx --workspace=ui/dashboard next start -p 3000 -H 0.0.0.0" }, "devDependencies": { "concurrently": "^8.2.2" diff --git a/scripts/launcher.js b/scripts/launcher.js index bf78c99..3488751 100644 --- a/scripts/launcher.js +++ b/scripts/launcher.js @@ -20,7 +20,6 @@ try { // Resolve ports from env (each service owns its own port var) const gatewayPort = process.env.PORT || "3001"; const dashboardPort = process.env.DASHBOARD_PORT || "3000"; -const memoryPort = process.env.MEMORY_PORT || "4005"; const openrouterPort = process.env.OPENROUTER_PORT || "4010"; const SERVICES = { @@ -38,13 +37,6 @@ const SERVICES = { color: "magenta", port: dashboardPort, }, - memory: { - dev: `MEMORY_PORT=${memoryPort} npm run start -w memory`, - start: `MEMORY_PORT=${memoryPort} npm run start -w memory`, - label: "memory", - color: "green", - port: memoryPort, - }, openrouter: { dev: `OPENROUTER_PORT=${openrouterPort} npm run dev -w @ekai/openrouter`, start: `OPENROUTER_PORT=${openrouterPort} npm run start -w @ekai/openrouter`, diff --git a/scripts/start-docker-fullstack.sh b/scripts/start-docker-fullstack.sh index ed5da1b..00ee4ec 100755 --- a/scripts/start-docker-fullstack.sh +++ b/scripts/start-docker-fullstack.sh @@ -3,13 +3,11 @@ set -euo pipefail PORT="${PORT:-3001}" UI_PORT="${UI_PORT:-3000}" -MEMORY_PORT="${MEMORY_PORT:-4005}" OPENROUTER_PORT="${OPENROUTER_PORT:-4010}" # Service toggles (all enabled by default) ENABLE_GATEWAY="${ENABLE_GATEWAY:-true}" ENABLE_DASHBOARD="${ENABLE_DASHBOARD:-true}" -ENABLE_MEMORY="${ENABLE_MEMORY:-true}" ENABLE_OPENROUTER="${ENABLE_OPENROUTER:-true}" PIDS=() @@ -50,17 +48,10 @@ if [ "$ENABLE_DASHBOARD" != "false" ] && [ "$ENABLE_DASHBOARD" != "0" ]; then PIDS+=($!) fi -if [ "$ENABLE_MEMORY" != "false" ] && [ "$ENABLE_MEMORY" != "0" ]; then - echo " Starting memory on :${MEMORY_PORT}" - cd /app/memory - MEMORY_PORT="$MEMORY_PORT" MEMORY_DB_PATH="${MEMORY_DB_PATH:-/app/memory/data/memory.db}" node dist/server.js & - PIDS+=($!) -fi - if [ "$ENABLE_OPENROUTER" != "false" ] && [ "$ENABLE_OPENROUTER" != "0" ]; then - echo " Starting openrouter on :${OPENROUTER_PORT}" + echo " Starting openrouter on :${OPENROUTER_PORT} (memory embedded)" cd /app/integrations/openrouter - PORT="$OPENROUTER_PORT" node dist/server.js & + OPENROUTER_PORT="$OPENROUTER_PORT" MEMORY_DB_PATH="${MEMORY_DB_PATH:-/app/memory/data/memory.db}" node dist/server.js & PIDS+=($!) fi diff --git a/ui/dashboard/src/lib/constants.ts b/ui/dashboard/src/lib/constants.ts index f63d2d8..8126de5 100644 --- a/ui/dashboard/src/lib/constants.ts +++ b/ui/dashboard/src/lib/constants.ts @@ -34,7 +34,7 @@ const buildUrl = (host: string, port: string): string => `${host}:${port}`; const baseHost = getBaseHost(); export const GATEWAY_PORT = process.env.NEXT_PUBLIC_GATEWAY_PORT || '3001'; -export const MEMORY_PORT = process.env.NEXT_PUBLIC_MEMORY_PORT || '4005'; +export const MEMORY_PORT = process.env.NEXT_PUBLIC_MEMORY_PORT || '4010'; export const API_CONFIG = { BASE_URL: buildUrl(baseHost, GATEWAY_PORT), From da8807761753901df049dc3a2e048f1b93f025e5 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:36:59 +0530 Subject: [PATCH 27/78] feat: embed Memory Vault UI into OpenRouter for single-container Cloud Run deploy Static-export the Next.js dashboard and serve it from the OpenRouter Express server so the client gets proxy + memory APIs + Memory Vault on one port. Adds openrouter-cloudrun Dockerfile target and docker-compose cloudrun profile. --- Dockerfile | 31 +++++++++++++++++++++++++ docker-compose.yaml | 17 ++++++++++++++ integrations/openrouter/src/server.ts | 33 +++++++++++++++++++++++++++ ui/dashboard/next.config.mjs | 6 +++++ ui/dashboard/package.json | 1 + ui/dashboard/src/app/memory/page.tsx | 27 +++++++++++++++------- ui/dashboard/src/app/page.tsx | 11 +++++++++ ui/dashboard/src/lib/constants.ts | 7 +++++- 8 files changed, 124 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index c8826f1..a3326c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,6 +41,13 @@ RUN npm run build WORKDIR /app/integrations/openrouter RUN npm run build +# ---------- dashboard embedded build (static export for single-container) ---------- +FROM build-base AS dashboard-embedded-build +WORKDIR /app/ui/dashboard +ENV NEXT_BUILD_MODE=embedded +ENV NEXT_PUBLIC_EMBEDDED_MODE=true +RUN npm run build:embedded + # ---------- gateway runtime ---------- FROM node:20-alpine AS gateway-runtime WORKDIR /app/gateway @@ -85,6 +92,30 @@ WORKDIR /app/integrations/openrouter EXPOSE 4010 CMD ["node", "dist/server.js"] +# ---------- openrouter + dashboard Cloud Run runtime (single container) ---------- +FROM node:20-alpine AS openrouter-cloudrun +WORKDIR /app +ENV NODE_ENV=production + +# Memory package (workspace dependency of openrouter) +COPY memory/package.json ./memory/ +RUN cd memory && npm install --omit=dev +COPY --from=memory-build /app/memory/dist ./memory/dist + +# OpenRouter +COPY integrations/openrouter/package.json ./integrations/openrouter/ +RUN cd integrations/openrouter && npm install --omit=dev +COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist + +# Dashboard static export +COPY --from=dashboard-embedded-build /app/ui/dashboard/out ./dashboard-static + +RUN mkdir -p /app/memory/data +WORKDIR /app/integrations/openrouter +ENV DASHBOARD_STATIC_DIR=/app/dashboard-static +EXPOSE 4010 +CMD ["node", "dist/server.js"] + # ---------- fullstack runtime ---------- FROM node:20-alpine AS ekai-gateway-runtime WORKDIR /app diff --git a/docker-compose.yaml b/docker-compose.yaml index 3f187d3..23d3d44 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -28,6 +28,23 @@ services: - memory_db:/app/memory/data restart: unless-stopped + openrouter-standalone: + build: + context: . + target: openrouter-cloudrun + image: ghcr.io/ekailabs/ekai-openrouter:latest + profiles: [cloudrun] + environment: + PORT: ${OPENROUTER_PORT:-4010} + MEMORY_DB_PATH: /app/memory/data/memory.db + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + DASHBOARD_STATIC_DIR: /app/dashboard-static + ports: + - "4010:4010" + volumes: + - memory_db:/app/memory/data + restart: unless-stopped + volumes: gateway_logs: gateway_db: diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts index f1d43ae..bc11cef 100644 --- a/integrations/openrouter/src/server.ts +++ b/integrations/openrouter/src/server.ts @@ -1,11 +1,16 @@ import express from 'express'; import cors from 'cors'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; import { SqliteMemoryStore, embed, createMemoryRouter } from '@ekai/memory'; import { PORT, MEMORY_DB_PATH } from './config.js'; import { initMemoryStore, fetchMemoryContext, ingestMessages } from './memory-client.js'; import { formatMemoryBlock, injectMemory } from './memory.js'; import { proxyToOpenRouter } from './proxy.js'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const app = express(); const corsOrigins = @@ -75,6 +80,34 @@ app.post('/v1/chat/completions', async (req, res) => { } }); +// ---------- Embedded dashboard (static export) ---------- +const DASHBOARD_DIR = process.env.DASHBOARD_STATIC_DIR + ? path.resolve(process.env.DASHBOARD_STATIC_DIR) + : path.resolve(__dirname, '../../dashboard-static'); + +if (fs.existsSync(DASHBOARD_DIR)) { + // Serve static assets (JS, CSS, images, etc.) + app.use(express.static(DASHBOARD_DIR)); + + // SPA catch-all: serve pre-rendered .html for page routes, fallback to index.html + app.get('*', (req, res) => { + // Try .html first (e.g. /memory → /memory.html) + const htmlFile = path.join(DASHBOARD_DIR, `${req.path}.html`); + if (fs.existsSync(htmlFile)) { + return res.sendFile(htmlFile); + } + // Try /index.html (e.g. /memory/ → /memory/index.html) + const indexFile = path.join(DASHBOARD_DIR, req.path, 'index.html'); + if (fs.existsSync(indexFile)) { + return res.sendFile(indexFile); + } + // Fallback to root index.html + res.sendFile(path.join(DASHBOARD_DIR, 'index.html')); + }); + + console.log(`[dashboard] serving static files from ${DASHBOARD_DIR}`); +} + app.listen(PORT, () => { console.log(`@ekai/openrouter listening on port ${PORT} (memory embedded, db at ${MEMORY_DB_PATH})`); }); diff --git a/ui/dashboard/next.config.mjs b/ui/dashboard/next.config.mjs index 974ed44..fa14f93 100644 --- a/ui/dashboard/next.config.mjs +++ b/ui/dashboard/next.config.mjs @@ -7,6 +7,12 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const nextConfig = { // Monorepo: tell Next.js where the workspace root is so it can resolve packages outputFileTracingRoot: resolve(__dirname, '../..'), + + // Static export for embedded mode (single-container deployment) + ...(process.env.NEXT_BUILD_MODE === 'embedded' && { + output: 'export', + images: { unoptimized: true }, + }), }; export default nextConfig; diff --git a/ui/dashboard/package.json b/ui/dashboard/package.json index 67abd0a..634dfc3 100644 --- a/ui/dashboard/package.json +++ b/ui/dashboard/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev --turbopack", "build": "npm install && next build --turbopack", + "build:embedded": "NEXT_BUILD_MODE=embedded NEXT_PUBLIC_EMBEDDED_MODE=true next build", "type-check": "tsc --noEmit", "start": "next start", "lint": "eslint" diff --git a/ui/dashboard/src/app/memory/page.tsx b/ui/dashboard/src/app/memory/page.tsx index 1ddd6ac..2332d5d 100644 --- a/ui/dashboard/src/app/memory/page.tsx +++ b/ui/dashboard/src/app/memory/page.tsx @@ -17,6 +17,7 @@ import ProfileSelector from '@/components/memory/ProfileSelector'; import ProfileManagement from '@/components/memory/ProfileManagement'; import ProfileStats from '@/components/memory/ProfileStats'; import ProfileBadge from '@/components/memory/ProfileBadge'; +import { MEMORY_PORT } from '@/lib/constants'; export default function MemoryVaultPage() { @@ -36,6 +37,14 @@ export default function MemoryVaultPage() { const [currentProfile, setCurrentProfile] = useState('default'); const [showProfileManagement, setShowProfileManagement] = useState(false); const [profileSwitching, setProfileSwitching] = useState(false); + const [embedded, setEmbedded] = useState(false); + + useEffect(() => { + setEmbedded( + process.env.NEXT_PUBLIC_EMBEDDED_MODE === 'true' || + window.location.port === MEMORY_PORT + ); + }, []); const fetchData = useCallback(async () => { try { @@ -190,14 +199,16 @@ export default function MemoryVaultPage() {
- - - - - + {!embedded && ( + + + + + + )}

Memory Vault

diff --git a/ui/dashboard/src/app/page.tsx b/ui/dashboard/src/app/page.tsx index 30c5142..b72f046 100644 --- a/ui/dashboard/src/app/page.tsx +++ b/ui/dashboard/src/app/page.tsx @@ -15,8 +15,19 @@ import BudgetCard from '@/components/BudgetCard'; import { useBudget } from '@/hooks/useBudget'; import { apiService } from '@/lib/api'; import Link from 'next/link'; +import { MEMORY_PORT } from '@/lib/constants'; export default function Dashboard() { + // Embedded mode: redirect root to Memory Vault + useEffect(() => { + if ( + process.env.NEXT_PUBLIC_EMBEDDED_MODE === 'true' || + window.location.port === MEMORY_PORT + ) { + window.location.replace('/memory'); + } + }, []); + const [dateRange, setDateRange] = useState(null); const [mounted, setMounted] = useState(false); const usageData = useUsageData(dateRange?.from, dateRange?.to); diff --git a/ui/dashboard/src/lib/constants.ts b/ui/dashboard/src/lib/constants.ts index 8126de5..df52325 100644 --- a/ui/dashboard/src/lib/constants.ts +++ b/ui/dashboard/src/lib/constants.ts @@ -36,7 +36,12 @@ const baseHost = getBaseHost(); export const GATEWAY_PORT = process.env.NEXT_PUBLIC_GATEWAY_PORT || '3001'; export const MEMORY_PORT = process.env.NEXT_PUBLIC_MEMORY_PORT || '4010'; +// Embedded mode: UI is served from the same Express server as the API +const isEmbedded = + process.env.NEXT_PUBLIC_EMBEDDED_MODE === 'true' || + (typeof window !== 'undefined' && window.location.port === MEMORY_PORT); + export const API_CONFIG = { BASE_URL: buildUrl(baseHost, GATEWAY_PORT), - MEMORY_URL: buildUrl(baseHost, MEMORY_PORT), + MEMORY_URL: isEmbedded ? '' : buildUrl(baseHost, MEMORY_PORT), } as const; \ No newline at end of file From 20af41f6a57cdfe4264cdee6375ac9907aa93d85 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:51:56 +0530 Subject: [PATCH 28/78] fix: resolve @ekai/memory workspace dep in Docker runtime stages Rewrite the workspace wildcard reference to a file: path before npm install so the registry lookup doesn't 404. --- Dockerfile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index a3326c3..e236c73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -82,9 +82,11 @@ COPY memory/package.json ./memory/ RUN cd memory && npm install --omit=dev COPY --from=memory-build /app/memory/dist ./memory/dist -# OpenRouter +# OpenRouter — rewrite workspace ref to local file path before install COPY integrations/openrouter/package.json ./integrations/openrouter/ -RUN cd integrations/openrouter && npm install --omit=dev +RUN cd integrations/openrouter && \ + sed -i 's|"@ekai/memory": "\*"|"@ekai/memory": "file:../../memory"|' package.json && \ + npm install --omit=dev COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist RUN mkdir -p /app/memory/data @@ -102,9 +104,11 @@ COPY memory/package.json ./memory/ RUN cd memory && npm install --omit=dev COPY --from=memory-build /app/memory/dist ./memory/dist -# OpenRouter +# OpenRouter — rewrite workspace ref to local file path before install COPY integrations/openrouter/package.json ./integrations/openrouter/ -RUN cd integrations/openrouter && npm install --omit=dev +RUN cd integrations/openrouter && \ + sed -i 's|"@ekai/memory": "\*"|"@ekai/memory": "file:../../memory"|' package.json && \ + npm install --omit=dev COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist # Dashboard static export From 751401da6f5f17efdf9d604fe66036f070e28edc Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:01:57 +0530 Subject: [PATCH 29/78] feat(memory): add OpenRouter as a memory provider Allows the client to use their existing OpenRouter API key for memory embeddings and extraction, eliminating the need for a separate Google API key. --- memory/src/providers/registry.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/memory/src/providers/registry.ts b/memory/src/providers/registry.ts index 32724fc..105797c 100644 --- a/memory/src/providers/registry.ts +++ b/memory/src/providers/registry.ts @@ -1,4 +1,4 @@ -type ProviderName = 'gemini' | 'openai'; +type ProviderName = 'gemini' | 'openai' | 'openrouter'; type AuthMode = 'queryKey' | 'bearer'; @@ -41,11 +41,23 @@ const PROVIDERS: Record = { extractModelEnv: 'OPENAI_EXTRACT_MODEL', auth: 'bearer', }, + openrouter: { + name: 'openrouter', + apiKeyEnv: 'OPENROUTER_API_KEY', + baseUrl: 'https://openrouter.ai/api/v1', + embedPath: 'embeddings', + extractPath: 'chat/completions', + defaultEmbedModel: 'openai/text-embedding-3-small', + defaultExtractModel: 'openai/gpt-4o-mini', + embedModelEnv: 'OPENROUTER_EMBED_MODEL', + extractModelEnv: 'OPENROUTER_EXTRACT_MODEL', + auth: 'bearer', + }, }; export function resolveProvider(kind: 'embed' | 'extract'): ProviderConfig { const env = (kind === 'embed' ? process.env.MEMORY_EMBED_PROVIDER : process.env.MEMORY_EXTRACT_PROVIDER)?.toLowerCase() as ProviderName | undefined; - const selected = env === 'openai' ? 'openai' : 'gemini'; + const selected: ProviderName = env === 'openai' ? 'openai' : env === 'openrouter' ? 'openrouter' : 'gemini'; return PROVIDERS[selected]; } From e45bf02aec208b44b9ad6333d019f806e1bc363d Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:14:38 +0530 Subject: [PATCH 30/78] fix(ci): remove npm ci from gateway build script The gateway build script ran `npm ci` which nuked the root-level workspace symlinks (node_modules/@ekai/*), causing the openrouter build to fail resolving @ekai/memory. Root npm ci already handles dependency installation. --- gateway/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/package.json b/gateway/package.json index d790788..28b7bbe 100644 --- a/gateway/package.json +++ b/gateway/package.json @@ -6,7 +6,7 @@ "private": true, "scripts": { "dev": "tsx watch src/index.ts", - "build": "npm ci && tsc && npm run copy:assets", + "build": "tsc && npm run copy:assets", "type-check": "tsc --noEmit", "copy:assets": "copyfiles -u 1 ../model_catalog/*.json dist/ && copyfiles -u 3 src/infrastructure/db/schema.sql dist/gateway/src/infrastructure/db/ && copyfiles -u 2 src/costs/*.yaml dist/gateway/src/costs/", "start": "node dist/gateway/src/index.js", From 65d449772c1e1f5baec0dd5f012b454fb30e316a Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:24:01 +0530 Subject: [PATCH 31/78] quick start instructions --- docs/openrouter-quickstart.md | 70 +++++++ gateway/src/costs/openrouter.yaml | 312 ++++++++++++++---------------- 2 files changed, 211 insertions(+), 171 deletions(-) create mode 100644 docs/openrouter-quickstart.md diff --git a/docs/openrouter-quickstart.md b/docs/openrouter-quickstart.md new file mode 100644 index 0000000..8199751 --- /dev/null +++ b/docs/openrouter-quickstart.md @@ -0,0 +1,70 @@ +# OpenRouter + Memory — Quick Start + +## Build + +```sh +docker build --target openrouter-cloudrun -t ekai-openrouter . +``` + +## Run + +```sh +docker run -d --name ekai \ + -e OPENROUTER_API_KEY=sk-or-... \ + -e MEMORY_EMBED_PROVIDER=openrouter \ + -e MEMORY_EXTRACT_PROVIDER=openrouter \ + -p 4010:4010 \ + ekai-openrouter +``` + +### Environment + +| Variable | Required | Default | +|----------|----------|---------| +| `OPENROUTER_API_KEY` | Yes | — | +| `MEMORY_EMBED_PROVIDER` | Yes | `gemini` | +| `MEMORY_EXTRACT_PROVIDER` | Yes | `gemini` | +| `OPENROUTER_EMBED_MODEL` | No | `openai/text-embedding-3-small` | +| `OPENROUTER_EXTRACT_MODEL` | No | `openai/gpt-4o-mini` | +| `MEMORY_DB_PATH` | No | `./memory.db` | + +## Verify + +```sh +# 1. Health +curl localhost:4010/health + +# 2. Chat — sends a message and triggers memory ingest +curl localhost:4010/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "openai/gpt-4o-mini", + "user": "clay-test", + "messages": [{"role": "user", "content": "I believe futarchy is the future of investing. Prediction markets should replace traditional governance for capital allocation decisions."}] + }' + +# 3. Confirm memory was stored +curl "localhost:4010/v1/summary?profile=clay-test" +``` + +Expected: summary shows counts > 0 in episodic and/or semantic sectors. + +## Using OpenAI directly (alternative) + +If you have an OpenAI API key and prefer to use it for memory extraction/embedding instead of routing through OpenRouter: + +```sh +docker run -d --name ekai \ + -e OPENROUTER_API_KEY=sk-or-... \ + -e OPENAI_API_KEY=sk-... \ + -e MEMORY_EMBED_PROVIDER=openai \ + -e MEMORY_EXTRACT_PROVIDER=openai \ + -p 4010:4010 \ + ekai-openrouter +``` + +This uses OpenAI directly for embeddings (`text-embedding-3-small`) and extraction (`gpt-4o-mini`), while still proxying chat completions through OpenRouter. Override models with `OPENAI_EMBED_MODEL` and `OPENAI_EXTRACT_MODEL`. + +## Dashboard + +Open http://localhost:4010/memory to browse the Memory Vault UI. diff --git a/gateway/src/costs/openrouter.yaml b/gateway/src/costs/openrouter.yaml index e6a9d69..8fcc890 100644 --- a/gateway/src/costs/openrouter.yaml +++ b/gateway/src/costs/openrouter.yaml @@ -2,6 +2,16 @@ provider: openrouter currency: USD unit: MTok models: + anthropic/claude-sonnet-4.6: + id: anthropic/claude-sonnet-4.6 + input: 3 + output: 15 + original_provider: anthropic + claude-sonnet-4.6: + id: anthropic/claude-sonnet-4.6 + input: 3 + output: 15 + original_provider: anthropic qwen/qwen3.5-plus-02-15: id: qwen/qwen3.5-plus-02-15 input: 0.4 @@ -14,23 +24,23 @@ models: original_provider: qwen qwen/qwen3.5-397b-a17b: id: qwen/qwen3.5-397b-a17b - input: 0.6 - output: 3.6 + input: 0.15 + output: 1 original_provider: qwen qwen3.5-397b-a17b: id: qwen/qwen3.5-397b-a17b - input: 0.6 - output: 3.6 + input: 0.15 + output: 1 original_provider: qwen minimax/minimax-m2.5: id: minimax/minimax-m2.5 input: 0.3 - output: 1.2 + output: 1.1 original_provider: minimax minimax-m2.5: id: minimax/minimax-m2.5 input: 0.3 - output: 1.2 + output: 1.1 original_provider: minimax z-ai/glm-5: id: z-ai/glm-5 @@ -74,13 +84,13 @@ models: original_provider: anthropic qwen/qwen3-coder-next: id: qwen/qwen3-coder-next - input: 0.07 - output: 0.3 + input: 0.12 + output: 0.75 original_provider: qwen qwen3-coder-next: id: qwen/qwen3-coder-next - input: 0.07 - output: 0.3 + input: 0.12 + output: 0.75 original_provider: qwen openrouter/free: id: openrouter/free @@ -274,13 +284,13 @@ models: original_provider: minimax z-ai/glm-4.7: id: z-ai/glm-4.7 - input: 0.4 - output: 1.5 + input: 0.38 + output: 1.7 original_provider: z-ai glm-4.7: id: z-ai/glm-4.7 - input: 0.4 - output: 1.5 + input: 0.38 + output: 1.7 original_provider: z-ai google/gemini-3-flash-preview: id: google/gemini-3-flash-preview @@ -374,13 +384,13 @@ models: original_provider: openai mistralai/devstral-2512: id: mistralai/devstral-2512 - input: 0.05 - output: 0.22 + input: 0.4 + output: 2 original_provider: mistralai devstral-2512: id: mistralai/devstral-2512 - input: 0.05 - output: 0.22 + input: 0.4 + output: 2 original_provider: mistralai relace/relace-search: id: relace/relace-search @@ -514,22 +524,22 @@ models: original_provider: arcee-ai deepseek/deepseek-v3.2-speciale: id: deepseek/deepseek-v3.2-speciale - input: 0.27 - output: 0.41 + input: 0.4 + output: 1.2 original_provider: deepseek deepseek-v3.2-speciale: id: deepseek/deepseek-v3.2-speciale - input: 0.27 - output: 0.41 + input: 0.4 + output: 1.2 original_provider: deepseek deepseek/deepseek-v3.2: id: deepseek/deepseek-v3.2 - input: 0.25 + input: 0.26 output: 0.38 original_provider: deepseek deepseek-v3.2: id: deepseek/deepseek-v3.2 - input: 0.25 + input: 0.26 output: 0.38 original_provider: deepseek prime-intellect/intellect-3: @@ -542,16 +552,6 @@ models: input: 0.2 output: 1.1 original_provider: prime-intellect - tngtech/tng-r1t-chimera: - id: tngtech/tng-r1t-chimera - input: 0.25 - output: 0.85 - original_provider: tngtech - tng-r1t-chimera: - id: tngtech/tng-r1t-chimera - input: 0.25 - output: 0.85 - original_provider: tngtech anthropic/claude-opus-4.5: id: anthropic/claude-opus-4.5 input: 5 @@ -935,12 +935,12 @@ models: z-ai/glm-4.6: id: z-ai/glm-4.6 input: 0.35 - output: 1.5 + output: 1.71 original_provider: z-ai glm-4.6: id: z-ai/glm-4.6 input: 0.35 - output: 1.5 + output: 1.71 original_provider: z-ai z-ai/glm-4.6:exacto: id: z-ai/glm-4.6:exacto @@ -992,16 +992,6 @@ models: input: 0.85 output: 1.25 original_provider: relace - google/gemini-2.5-flash-preview-09-2025: - id: google/gemini-2.5-flash-preview-09-2025 - input: 0.3 - output: 2.5 - original_provider: google - gemini-2.5-flash-preview-09-2025: - id: google/gemini-2.5-flash-preview-09-2025 - input: 0.3 - output: 2.5 - original_provider: google google/gemini-2.5-flash-lite-preview-09-2025: id: google/gemini-2.5-flash-lite-preview-09-2025 input: 0.1 @@ -1244,13 +1234,13 @@ models: original_provider: x-ai nousresearch/hermes-4-70b: id: nousresearch/hermes-4-70b - input: 0.11 - output: 0.38 + input: 0.13 + output: 0.4 original_provider: nousresearch hermes-4-70b: id: nousresearch/hermes-4-70b - input: 0.11 - output: 0.38 + input: 0.13 + output: 0.4 original_provider: nousresearch nousresearch/hermes-4-405b: id: nousresearch/hermes-4-405b @@ -1454,23 +1444,23 @@ models: original_provider: qwen qwen/qwen3-30b-a3b-instruct-2507: id: qwen/qwen3-30b-a3b-instruct-2507 - input: 0.08 - output: 0.33 + input: 0.09 + output: 0.3 original_provider: qwen qwen3-30b-a3b-instruct-2507: id: qwen/qwen3-30b-a3b-instruct-2507 - input: 0.08 - output: 0.33 + input: 0.09 + output: 0.3 original_provider: qwen z-ai/glm-4.5: id: z-ai/glm-4.5 - input: 0.35 - output: 1.55 + input: 0.55 + output: 2 original_provider: z-ai glm-4.5: id: z-ai/glm-4.5 - input: 0.35 - output: 1.55 + input: 0.55 + output: 2 original_provider: z-ai z-ai/glm-4.5-air:free: id: z-ai/glm-4.5-air:free @@ -1852,16 +1842,6 @@ models: input: 0.02 output: 0.04 original_provider: google - nousresearch/deephermes-3-mistral-24b-preview: - id: nousresearch/deephermes-3-mistral-24b-preview - input: 0.02 - output: 0.1 - original_provider: nousresearch - deephermes-3-mistral-24b-preview: - id: nousresearch/deephermes-3-mistral-24b-preview - input: 0.02 - output: 0.1 - original_provider: nousresearch mistralai/mistral-medium-3: id: mistralai/mistral-medium-3 input: 0.4 @@ -1964,13 +1944,13 @@ models: original_provider: meta-llama qwen/qwen3-30b-a3b: id: qwen/qwen3-30b-a3b - input: 0.06 - output: 0.22 + input: 0.08 + output: 0.28 original_provider: qwen qwen3-30b-a3b: id: qwen/qwen3-30b-a3b - input: 0.06 - output: 0.22 + input: 0.08 + output: 0.28 original_provider: qwen qwen/qwen3-8b: id: qwen/qwen3-8b @@ -1984,13 +1964,13 @@ models: original_provider: qwen qwen/qwen3-14b: id: qwen/qwen3-14b - input: 0.05 - output: 0.22 + input: 0.06 + output: 0.24 original_provider: qwen qwen3-14b: id: qwen/qwen3-14b - input: 0.05 - output: 0.22 + input: 0.06 + output: 0.24 original_provider: qwen qwen/qwen3-32b: id: qwen/qwen3-32b @@ -2004,13 +1984,13 @@ models: original_provider: qwen qwen/qwen3-235b-a22b: id: qwen/qwen3-235b-a22b - input: 0.3 - output: 1.2 + input: 0.455 + output: 1.82 original_provider: qwen qwen3-235b-a22b: id: qwen/qwen3-235b-a22b - input: 0.3 - output: 1.2 + input: 0.455 + output: 1.82 original_provider: qwen tngtech/deepseek-r1t-chimera: id: tngtech/deepseek-r1t-chimera @@ -2164,13 +2144,13 @@ models: original_provider: meta-llama qwen/qwen2.5-vl-32b-instruct: id: qwen/qwen2.5-vl-32b-instruct - input: 0.05 - output: 0.22 + input: 0.2 + output: 0.6 original_provider: qwen qwen2.5-vl-32b-instruct: id: qwen/qwen2.5-vl-32b-instruct - input: 0.05 - output: 0.22 + input: 0.2 + output: 0.6 original_provider: qwen deepseek/deepseek-chat-v3-0324: id: deepseek/deepseek-chat-v3-0324 @@ -2204,13 +2184,13 @@ models: original_provider: mistralai mistralai/mistral-small-3.1-24b-instruct: id: mistralai/mistral-small-3.1-24b-instruct - input: 0.03 - output: 0.11 + input: 0.35 + output: 0.56 original_provider: mistralai mistral-small-3.1-24b-instruct: id: mistralai/mistral-small-3.1-24b-instruct - input: 0.03 - output: 0.11 + input: 0.35 + output: 0.56 original_provider: mistralai allenai/olmo-2-0325-32b-instruct: id: allenai/olmo-2-0325-32b-instruct @@ -2254,13 +2234,13 @@ models: original_provider: google google/gemma-3-12b-it: id: google/gemma-3-12b-it - input: 0.03 - output: 0.1 + input: 0.04 + output: 0.13 original_provider: google gemma-3-12b-it: id: google/gemma-3-12b-it - input: 0.03 - output: 0.1 + input: 0.04 + output: 0.13 original_provider: google cohere/command-a: id: cohere/command-a @@ -2564,13 +2544,13 @@ models: original_provider: perplexity deepseek/deepseek-r1-distill-llama-70b: id: deepseek/deepseek-r1-distill-llama-70b - input: 0.03 - output: 0.11 + input: 0.7 + output: 0.8 original_provider: deepseek deepseek-r1-distill-llama-70b: id: deepseek/deepseek-r1-distill-llama-70b - input: 0.03 - output: 0.11 + input: 0.7 + output: 0.8 original_provider: deepseek deepseek/deepseek-r1: id: deepseek/deepseek-r1 @@ -2744,13 +2724,13 @@ models: original_provider: mistralai qwen/qwen-2.5-coder-32b-instruct: id: qwen/qwen-2.5-coder-32b-instruct - input: 0.03 - output: 0.11 + input: 0.2 + output: 0.2 original_provider: qwen qwen-2.5-coder-32b-instruct: id: qwen/qwen-2.5-coder-32b-instruct - input: 0.03 - output: 0.11 + input: 0.2 + output: 0.2 original_provider: qwen raifle/sorcererlm-8x22b: id: raifle/sorcererlm-8x22b @@ -2822,23 +2802,23 @@ models: input: 1.2 output: 1.2 original_provider: nvidia - inflection/inflection-3-productivity: - id: inflection/inflection-3-productivity + inflection/inflection-3-pi: + id: inflection/inflection-3-pi input: 2.5 output: 10 original_provider: inflection - inflection-3-productivity: - id: inflection/inflection-3-productivity + inflection-3-pi: + id: inflection/inflection-3-pi input: 2.5 output: 10 original_provider: inflection - inflection/inflection-3-pi: - id: inflection/inflection-3-pi + inflection/inflection-3-productivity: + id: inflection/inflection-3-productivity input: 2.5 output: 10 original_provider: inflection - inflection-3-pi: - id: inflection/inflection-3-pi + inflection-3-productivity: + id: inflection/inflection-3-productivity input: 2.5 output: 10 original_provider: inflection @@ -2852,26 +2832,6 @@ models: input: 0.17 output: 0.43 original_provider: thedrummer - meta-llama/llama-3.2-1b-instruct: - id: meta-llama/llama-3.2-1b-instruct - input: 0.027 - output: 0.2 - original_provider: meta-llama - llama-3.2-1b-instruct: - id: meta-llama/llama-3.2-1b-instruct - input: 0.027 - output: 0.2 - original_provider: meta-llama - meta-llama/llama-3.2-11b-vision-instruct: - id: meta-llama/llama-3.2-11b-vision-instruct - input: 0.049 - output: 0.049 - original_provider: meta-llama - llama-3.2-11b-vision-instruct: - id: meta-llama/llama-3.2-11b-vision-instruct - input: 0.049 - output: 0.049 - original_provider: meta-llama meta-llama/llama-3.2-3b-instruct:free: id: meta-llama/llama-3.2-3b-instruct:free input: 0 @@ -2892,6 +2852,26 @@ models: input: 0.02 output: 0.02 original_provider: meta-llama + meta-llama/llama-3.2-1b-instruct: + id: meta-llama/llama-3.2-1b-instruct + input: 0.027 + output: 0.2 + original_provider: meta-llama + llama-3.2-1b-instruct: + id: meta-llama/llama-3.2-1b-instruct + input: 0.027 + output: 0.2 + original_provider: meta-llama + meta-llama/llama-3.2-11b-vision-instruct: + id: meta-llama/llama-3.2-11b-vision-instruct + input: 0.049 + output: 0.049 + original_provider: meta-llama + llama-3.2-11b-vision-instruct: + id: meta-llama/llama-3.2-11b-vision-instruct + input: 0.049 + output: 0.049 + original_provider: meta-llama qwen/qwen-2.5-72b-instruct: id: qwen/qwen-2.5-72b-instruct input: 0.12 @@ -2982,16 +2962,6 @@ models: input: 1 output: 1 original_provider: nousresearch - openai/chatgpt-4o-latest: - id: openai/chatgpt-4o-latest - input: 5 - output: 15 - original_provider: openai - chatgpt-4o-latest: - id: openai/chatgpt-4o-latest - input: 5 - output: 15 - original_provider: openai sao10k/l3-lunaris-8b: id: sao10k/l3-lunaris-8b input: 0.04 @@ -3022,15 +2992,15 @@ models: input: 4 output: 4 original_provider: meta-llama - meta-llama/llama-3.1-70b-instruct: - id: meta-llama/llama-3.1-70b-instruct - input: 0.4 - output: 0.4 + meta-llama/llama-3.1-8b-instruct: + id: meta-llama/llama-3.1-8b-instruct + input: 0.02 + output: 0.05 original_provider: meta-llama - llama-3.1-70b-instruct: - id: meta-llama/llama-3.1-70b-instruct - input: 0.4 - output: 0.4 + llama-3.1-8b-instruct: + id: meta-llama/llama-3.1-8b-instruct + input: 0.02 + output: 0.05 original_provider: meta-llama meta-llama/llama-3.1-405b-instruct: id: meta-llama/llama-3.1-405b-instruct @@ -3042,15 +3012,15 @@ models: input: 4 output: 4 original_provider: meta-llama - meta-llama/llama-3.1-8b-instruct: - id: meta-llama/llama-3.1-8b-instruct - input: 0.02 - output: 0.05 + meta-llama/llama-3.1-70b-instruct: + id: meta-llama/llama-3.1-70b-instruct + input: 0.4 + output: 0.4 original_provider: meta-llama - llama-3.1-8b-instruct: - id: meta-llama/llama-3.1-8b-instruct - input: 0.02 - output: 0.05 + llama-3.1-70b-instruct: + id: meta-llama/llama-3.1-70b-instruct + input: 0.4 + output: 0.4 original_provider: meta-llama mistralai/mistral-nemo: id: mistralai/mistral-nemo @@ -3392,15 +3362,15 @@ models: input: 0.06 output: 0.06 original_provider: gryphe - openai/gpt-3.5-turbo: - id: openai/gpt-3.5-turbo - input: 0.5 - output: 1.5 + openai/gpt-4-0314: + id: openai/gpt-4-0314 + input: 30 + output: 60 original_provider: openai - gpt-3.5-turbo: - id: openai/gpt-3.5-turbo - input: 0.5 - output: 1.5 + gpt-4-0314: + id: openai/gpt-4-0314 + input: 30 + output: 60 original_provider: openai openai/gpt-4: id: openai/gpt-4 @@ -3412,18 +3382,18 @@ models: input: 30 output: 60 original_provider: openai - openai/gpt-4-0314: - id: openai/gpt-4-0314 - input: 30 - output: 60 + openai/gpt-3.5-turbo: + id: openai/gpt-3.5-turbo + input: 0.5 + output: 1.5 original_provider: openai - gpt-4-0314: - id: openai/gpt-4-0314 - input: 30 - output: 60 + gpt-3.5-turbo: + id: openai/gpt-3.5-turbo + input: 0.5 + output: 1.5 original_provider: openai metadata: - last_updated: '2026-02-16' + last_updated: '2026-02-18' source: https://openrouter.ai/api/v1/models notes: Auto-refreshed from OpenRouter models API version: auto From d845d19af7eba389cfb9ce1766bb1c6252d13a4c Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:29:08 +0530 Subject: [PATCH 32/78] updated instructions to add proxy --- docs/openrouter-quickstart.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/openrouter-quickstart.md b/docs/openrouter-quickstart.md index 8199751..bf22200 100644 --- a/docs/openrouter-quickstart.md +++ b/docs/openrouter-quickstart.md @@ -49,22 +49,6 @@ curl "localhost:4010/v1/summary?profile=clay-test" Expected: summary shows counts > 0 in episodic and/or semantic sectors. -## Using OpenAI directly (alternative) - -If you have an OpenAI API key and prefer to use it for memory extraction/embedding instead of routing through OpenRouter: - -```sh -docker run -d --name ekai \ - -e OPENROUTER_API_KEY=sk-or-... \ - -e OPENAI_API_KEY=sk-... \ - -e MEMORY_EMBED_PROVIDER=openai \ - -e MEMORY_EXTRACT_PROVIDER=openai \ - -p 4010:4010 \ - ekai-openrouter -``` - -This uses OpenAI directly for embeddings (`text-embedding-3-small`) and extraction (`gpt-4o-mini`), while still proxying chat completions through OpenRouter. Override models with `OPENAI_EMBED_MODEL` and `OPENAI_EXTRACT_MODEL`. - ## Dashboard Open http://localhost:4010/memory to browse the Memory Vault UI. From 45406d17497f8e015cb5d1bd44d476b1e45eb473 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:49:48 +0530 Subject: [PATCH 33/78] feat(ci): add GHCR publish workflow for ekai-cloudrun image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename Docker target openrouter-cloudrun → ekai-cloudrun, add a GitHub Actions workflow that builds and pushes the image to ghcr.io on every push to main (path-filtered). Update quickstart docs with pre-built image usage and Cloud Run deploy instructions. Respect Cloud Run PORT env var in openrouter config. --- .github/workflows/deploy-cloudrun.yml | 46 +++++++++++++++++++++++++++ Dockerfile | 2 +- docs/openrouter-quickstart.md | 28 +++++++++++++--- integrations/openrouter/src/config.ts | 2 +- 4 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/deploy-cloudrun.yml diff --git a/.github/workflows/deploy-cloudrun.yml b/.github/workflows/deploy-cloudrun.yml new file mode 100644 index 0000000..1805574 --- /dev/null +++ b/.github/workflows/deploy-cloudrun.yml @@ -0,0 +1,46 @@ +name: Build & Push ekai-cloudrun + +on: + push: + branches: [main] + paths: + - "integrations/openrouter/**" + - "memory/**" + - "ui/dashboard/**" + - "Dockerfile" + - ".github/workflows/deploy-cloudrun.yml" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/ekai-cloudrun + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + target: ekai-cloudrun + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index e236c73..47aaa1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -95,7 +95,7 @@ EXPOSE 4010 CMD ["node", "dist/server.js"] # ---------- openrouter + dashboard Cloud Run runtime (single container) ---------- -FROM node:20-alpine AS openrouter-cloudrun +FROM node:20-alpine AS ekai-cloudrun WORKDIR /app ENV NODE_ENV=production diff --git a/docs/openrouter-quickstart.md b/docs/openrouter-quickstart.md index bf22200..e7e0fdf 100644 --- a/docs/openrouter-quickstart.md +++ b/docs/openrouter-quickstart.md @@ -1,20 +1,38 @@ -# OpenRouter + Memory — Quick Start +# Add Memory to Your OpenRouter Proxy -## Build +## Quick Start (pre-built image) ```sh -docker build --target openrouter-cloudrun -t ekai-openrouter . +docker run -d --name ekai \ + -e OPENROUTER_API_KEY=sk-or-... \ + -e MEMORY_EMBED_PROVIDER=openrouter \ + -e MEMORY_EXTRACT_PROVIDER=openrouter \ + -p 4010:4010 \ + ghcr.io/ekailabs/ekai-cloudrun:latest ``` -## Run +## Build from Source ```sh +docker build --target ekai-cloudrun -t ekai-cloudrun . docker run -d --name ekai \ -e OPENROUTER_API_KEY=sk-or-... \ -e MEMORY_EMBED_PROVIDER=openrouter \ -e MEMORY_EXTRACT_PROVIDER=openrouter \ -p 4010:4010 \ - ekai-openrouter + ekai-cloudrun +``` + +## Deploy to Cloud Run + +```sh +gcloud run deploy ekai-cloudrun \ + --image ghcr.io/ekailabs/ekai-cloudrun:latest \ + --region us-central1 \ + --set-env-vars OPENROUTER_API_KEY=sk-or-... \ + --set-env-vars MEMORY_EMBED_PROVIDER=openrouter \ + --set-env-vars MEMORY_EXTRACT_PROVIDER=openrouter \ + --allow-unauthenticated ``` ### Environment diff --git a/integrations/openrouter/src/config.ts b/integrations/openrouter/src/config.ts index 1efc578..bab9dc4 100644 --- a/integrations/openrouter/src/config.ts +++ b/integrations/openrouter/src/config.ts @@ -23,4 +23,4 @@ if (!OPENROUTER_API_KEY) { } export const MEMORY_DB_PATH = process.env.MEMORY_DB_PATH ?? './memory.db'; -export const PORT = parseInt(process.env.OPENROUTER_PORT || '4010', 10); +export const PORT = parseInt(process.env.OPENROUTER_PORT || process.env.PORT || '4010', 10); From 471ed204e9cced346ba380362895df7ae24bbf14 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:14:25 +0530 Subject: [PATCH 34/78] agent commits line number from instruction --- docs/ROFL_DEPLOYMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ROFL_DEPLOYMENT.md b/docs/ROFL_DEPLOYMENT.md index f777c32..325452b 100644 --- a/docs/ROFL_DEPLOYMENT.md +++ b/docs/ROFL_DEPLOYMENT.md @@ -19,7 +19,7 @@ Deploy your own private ekai-gateway instance on Oasis Network using ROFL (Runti ## Prerequisites 1. **Oasis CLI** (v0.18.x+) - Install from [L15 CLI Reference](https://cli.oasis.io), then verify: + Install from [CLI Reference](https://cli.oasis.io), then verify: ```bash oasis --version ``` From 038414ca6e354ae6512f5b004439f7565c3152bf Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:22:29 +0530 Subject: [PATCH 35/78] remove reflective memory from extraction, ingestion, retrieval, and display Reflective memories (self-observations, meta-cognitive insights) don't add enough value in the current inline flow. This stops producing and using them while leaving existing DB data and schema intact for future background processing. --- integrations/openrouter/src/memory.ts | 4 -- memory/src/providers/extract.ts | 23 +--------- memory/src/providers/prompt.ts | 15 +----- memory/src/scoring.ts | 2 +- memory/src/sqlite-store.ts | 66 +-------------------------- memory/tests/store.test.ts | 63 +++++++------------------ ui/dashboard/src/app/memory/page.tsx | 44 +++++++++++------- 7 files changed, 48 insertions(+), 169 deletions(-) diff --git a/integrations/openrouter/src/memory.ts b/integrations/openrouter/src/memory.ts index 5c6e73d..973bb86 100644 --- a/integrations/openrouter/src/memory.ts +++ b/integrations/openrouter/src/memory.ts @@ -23,7 +23,6 @@ export function formatMemoryBlock(results: QueryResult[]): string { const facts: string[] = []; const events: string[] = []; const procedures: string[] = []; - const observations: string[] = []; for (const r of results) { if (r.sector === 'semantic' && r.details?.subject) { @@ -31,8 +30,6 @@ export function formatMemoryBlock(results: QueryResult[]): string { } else if (r.sector === 'procedural' && r.details?.trigger) { const steps = r.details.steps?.join(' → ') || r.content; procedures.push(`- When ${r.details.trigger}: ${steps}`); - } else if (r.sector === 'reflective') { - observations.push(`- ${r.content}`); } else { events.push(`- ${r.content}`); } @@ -42,7 +39,6 @@ export function formatMemoryBlock(results: QueryResult[]): string { if (facts.length) sections.push(`What I know:\n${facts.join('\n')}`); if (events.length) sections.push(`What I remember:\n${events.join('\n')}`); if (procedures.length) sections.push(`How I do things:\n${procedures.join('\n')}`); - if (observations.length) sections.push(`My observations:\n${observations.join('\n')}`); return `\n[Recalled context for this conversation. Use naturally if relevant, ignore if not.]\n\n${sections.join('\n\n')}\n`; } diff --git a/memory/src/providers/extract.ts b/memory/src/providers/extract.ts index 7943fdf..5407bcd 100644 --- a/memory/src/providers/extract.ts +++ b/memory/src/providers/extract.ts @@ -1,4 +1,4 @@ -import type { IngestComponents, SemanticTripleInput, ReflectiveInput } from '../types.js'; +import type { IngestComponents, SemanticTripleInput } from '../types.js'; import { EXTRACT_PROMPT } from './prompt.js'; import { buildUrl, getApiKey, getModel, resolveProvider } from './registry.js'; @@ -23,34 +23,13 @@ function normalizeSemantic(raw: any): SemanticTripleInput[] { })); } -/** - * Normalize reflective output: string → array of ReflectiveInput, filter empty. - */ -function normalizeReflective(raw: any): ReflectiveInput[] { - if (!raw) return []; - if (typeof raw === 'string') { - return raw.trim() ? [{ observation: raw.trim() }] : []; - } - const arr = Array.isArray(raw) ? raw : [raw]; - return arr.filter( - (r: any) => - r && - typeof r === 'object' && - typeof r.observation === 'string' && r.observation.trim(), - ).map((r: any) => ({ - observation: r.observation.trim(), - })); -} - function parseResponse(parsed: any): IngestComponents { const semantic = normalizeSemantic(parsed.semantic); - const reflective = normalizeReflective(parsed.reflective); return { episodic: typeof parsed.episodic === 'string' ? parsed.episodic : '', semantic: semantic.length ? semantic : [], procedural: parsed.procedural ?? '', - reflective: reflective.length ? reflective : [], }; } diff --git a/memory/src/providers/prompt.ts b/memory/src/providers/prompt.ts index 39981ca..5517378 100644 --- a/memory/src/providers/prompt.ts +++ b/memory/src/providers/prompt.ts @@ -20,15 +20,10 @@ Return ONLY valid JSON with these keys: "result": "", "context": "" }, - "reflective": [ - { - "observation": "" - } - ] } RULES: -- If a field does not apply, return "" for episodic, [] for semantic/reflective, {} for procedural. +- If a field does not apply, return "" for episodic, [] for semantic, {} for procedural. - Do NOT repeat information across fields. EPISODIC — events with time context, place, or uncertain/one-off claims: @@ -51,12 +46,4 @@ SEMANTIC — stable, context-free facts as subject-predicate-object triples: PROCEDURAL — multi-step workflows or processes: - Must be a genuine multi-step process; if not, leave empty {}. -REFLECTIVE — meta-cognitive observations about my own behavior or patterns: - - Return an ARRAY of observations. - - Things I notice about how I'm performing, patterns in my interactions, lessons learned. - - Examples: - * {"observation": "I tend to give overly detailed answers when a short response would suffice"} - * {"observation": "Sha responds better when I lead with the conclusion before the reasoning"} - - Only include genuine insights, not restating conversation content. - - NEVER output anything outside the JSON.`; diff --git a/memory/src/scoring.ts b/memory/src/scoring.ts index dd37bbf..b949beb 100644 --- a/memory/src/scoring.ts +++ b/memory/src/scoring.ts @@ -5,7 +5,7 @@ const DEFAULT_SECTOR_WEIGHTS: Record = { episodic: 1, semantic: 1, procedural: 1, - reflective: 0.8, + }; const RETRIEVAL_SOFTCAP = 10; // for normalization const RELEVANCE_WEIGHT = 1.0; diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 03a735f..13bb14d 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -19,7 +19,7 @@ import { cosineSimilarity, DEFAULT_PROFILE, normalizeProfileSlug } from './utils import { filterAndCapWorkingMemory } from './wm.js'; import { SemanticGraphTraversal } from './semantic-graph.js'; -const SECTORS: SectorName[] = ['episodic', 'semantic', 'procedural', 'reflective']; +const SECTORS: SectorName[] = ['episodic', 'semantic', 'procedural']; const PER_SECTOR_K = 4; const WORKING_MEMORY_CAP = 8; const SECTOR_SCAN_LIMIT = 200; // simple scan instead of ANN for v0 @@ -267,37 +267,6 @@ export class SqliteMemoryStore { } } - // --- Reflective (requires attribution — skip if no origin) --- - const reflectiveInput = components.reflective; - const reflections = (origin?.originType) ? this.normalizeReflectiveInput(reflectiveInput) : []; - - for (const ref of reflections) { - const embedding = await this.embed(ref.observation, 'reflective'); - const refRow: ReflectiveMemoryRecord = { - id: randomUUID(), - observation: ref.observation, - profileId, - embedding, - createdAt, - lastAccessed: createdAt, - source, - originType: origin!.originType, - originActor: origin!.originActor ?? userId, - originRef: origin!.originRef, - }; - this.insertReflectiveRow(refRow); - rows.push({ - id: refRow.id, - sector: 'reflective', - content: ref.observation, - embedding, - profileId, - createdAt, - lastAccessed: createdAt, - source, - }); - } - return rows; } @@ -317,7 +286,7 @@ export class SqliteMemoryStore { episodic: [], semantic: [], procedural: [], - reflective: [], + reflective: [], // kept for type compatibility; not queried }; for (const sector of SECTORS) { @@ -372,14 +341,6 @@ export class SqliteMemoryStore { ) .get({ profileId }) as { sector: SectorName; count: number; lastCreatedAt: number | null }; - const reflectiveRow = this.db - .prepare( - `select 'reflective' as sector, count(*) as count, max(created_at) as lastCreatedAt - from reflective_memory - where profile_id = @profileId`, - ) - .get({ profileId }) as { sector: SectorName; count: number; lastCreatedAt: number | null }; - const defaults = SECTORS.map((s) => ({ sector: s, count: 0, @@ -389,7 +350,6 @@ export class SqliteMemoryStore { const map = new Map(rows.map((r) => [r.sector, r])); if (proceduralRow) map.set('procedural', proceduralRow); if (semanticRow) map.set('semantic', semanticRow); - if (reflectiveRow) map.set('reflective', reflectiveRow); return defaults.map((d) => map.get(d.sector) ?? d); } @@ -414,12 +374,6 @@ export class SqliteMemoryStore { from semantic_memory where profile_id = @profileId and (valid_to is null or valid_to > @now) - union all - select id, 'reflective' as sector, observation as content, embedding, created_at as createdAt, last_accessed as lastAccessed, - '{}' as details, - null as eventStart, null as eventEnd, 0 as retrievalCount - from reflective_memory - where profile_id = @profileId order by createdAt desc limit @limit`, ) @@ -500,12 +454,6 @@ export class SqliteMemoryStore { from semantic_memory where profile_id = @profileId and user_scope = @userId and (valid_to is null or valid_to > @now) - union all - select id, 'reflective' as sector, observation as content, embedding, created_at as createdAt, last_accessed as lastAccessed, - '{}' as details, - null as eventStart, null as eventEnd, 0 as retrievalCount - from reflective_memory - where profile_id = @profileId and origin_actor = @userId order by createdAt desc limit @limit`, ) @@ -781,16 +729,6 @@ export class SqliteMemoryStore { lastAccessed: r.updatedAt, details: { subject: r.subject, predicate: r.predicate, object: r.object, validFrom: r.validFrom, validTo: r.validTo, domain: r.domain }, } as any)); - case 'reflective': - return this.getReflectiveRows(profileId, SECTOR_SCAN_LIMIT).map((r) => ({ - id: r.id, - sector: 'reflective' as SectorName, - content: r.observation, - embedding: r.embedding, - profileId: r.profileId, - createdAt: r.createdAt, - lastAccessed: r.lastAccessed, - })); default: return this.getRowsForSector(sector, profileId, SECTOR_SCAN_LIMIT, userId); } diff --git a/memory/tests/store.test.ts b/memory/tests/store.test.ts index c37a537..30232d8 100644 --- a/memory/tests/store.test.ts +++ b/memory/tests/store.test.ts @@ -66,8 +66,8 @@ describe('SqliteMemoryStore (single-user mode)', () => { store = createStore(); }); - // ─── 1. Ingest all 4 sectors ────────────────────────────────────── - it('ingests all 4 sectors and returns correct sector labels', async () => { + // ─── 1. Ingest active sectors ──────────────────────────────────── + it('ingests episodic, semantic, procedural and skips reflective', async () => { const rows = await store.ingest( { episodic: 'deployed v2 to staging', @@ -83,9 +83,9 @@ describe('SqliteMemoryStore (single-user mode)', () => { { origin: { originType: 'conversation', originActor: 'test' } }, ); - expect(rows).toHaveLength(4); + expect(rows).toHaveLength(3); const sectors = rows.map((r) => r.sector).sort(); - expect(sectors).toEqual(['episodic', 'procedural', 'reflective', 'semantic']); + expect(sectors).toEqual(['episodic', 'procedural', 'semantic']); }); // ─── 2. Query returns results matching dashboard shape ──────────── @@ -197,22 +197,18 @@ describe('SqliteMemoryStore (single-user mode)', () => { episodic: 'deployed v2 to staging', semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, procedural: { trigger: 'deploy to prod', steps: ['build', 'deploy'] }, - reflective: { observation: 'user prefers concise answers' }, }, - undefined, - { origin: { originType: 'conversation', originActor: 'test' } }, ); const summary = store.getSectorSummary(); - // Should have all 4 sectors - expect(summary).toHaveLength(4); + // Should have 3 active sectors (reflective is disabled) + expect(summary).toHaveLength(3); const bySector = Object.fromEntries(summary.map((s) => [s.sector, s])); expect(bySector.episodic.count).toBe(1); expect(bySector.semantic.count).toBe(1); expect(bySector.procedural.count).toBe(1); - expect(bySector.reflective.count).toBe(1); // Each sector has lastCreatedAt for (const s of summary) { @@ -249,19 +245,10 @@ describe('SqliteMemoryStore (single-user mode)', () => { }, ); - t = NOW + 3000; - await store1.ingest( - { - reflective: { observation: 'this is a reflection' }, - }, - undefined, - { origin: { originType: 'conversation', originActor: 'test' } }, - ); - const recent = store1.getRecent(undefined, 10); - // Should return all 4 items - expect(recent.length).toBe(4); + // Should return 3 items (reflective is disabled) + expect(recent.length).toBe(3); // Descending order by createdAt for (let i = 0; i < recent.length - 1; i++) { @@ -296,31 +283,17 @@ describe('SqliteMemoryStore (single-user mode)', () => { expect(Array.isArray(procItem!.details.steps)).toBe(true); }); - // ─── 7. Reflective requires attribution ─────────────────────────── - it('skips reflective without origin, stores with origin', async () => { - // Without origin → should skip reflective - const rows1 = await store.ingest({ - reflective: { observation: 'this is a reflection' }, - }); - const reflectiveRows1 = rows1.filter((r) => r.sector === 'reflective'); - expect(reflectiveRows1).toHaveLength(0); - - const summary1 = store.getSectorSummary(); - expect(summary1.find((s) => s.sector === 'reflective')!.count).toBe(0); - - // With origin → should store - const rows2 = await store.ingest( + // ─── 7. Reflective is always skipped (disabled) ───────────────── + it('skips reflective even with origin', async () => { + const rows = await store.ingest( { reflective: { observation: 'this is a reflection' }, }, undefined, { origin: { originType: 'conversation', originActor: 'test', originRef: 'conv-123' } }, ); - const reflectiveRows2 = rows2.filter((r) => r.sector === 'reflective'); - expect(reflectiveRows2).toHaveLength(1); - - const summary2 = store.getSectorSummary(); - expect(summary2.find((s) => s.sector === 'reflective')!.count).toBe(1); + const reflectiveRows = rows.filter((r) => r.sector === 'reflective'); + expect(reflectiveRows).toHaveLength(0); }); // ─── 8. Empty ingest ────────────────────────────────────────────── @@ -351,12 +324,9 @@ describe('SqliteMemoryStore (single-user mode)', () => { episodic: 'deployed v2 to staging', semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, procedural: { trigger: 'deploy to prod', steps: ['build', 'deploy'] }, - reflective: { observation: 'user prefers concise answers' }, }, - undefined, - { origin: { originType: 'conversation', originActor: 'test' } }, ); - expect(rows).toHaveLength(4); + expect(rows).toHaveLength(3); // deleteById — remove the episodic row const episodicRow = rows.find((r) => r.sector === 'episodic')!; @@ -369,11 +339,10 @@ describe('SqliteMemoryStore (single-user mode)', () => { // Others still present expect(summaryAfterDelete.find((s) => s.sector === 'semantic')!.count).toBe(1); expect(summaryAfterDelete.find((s) => s.sector === 'procedural')!.count).toBe(1); - expect(summaryAfterDelete.find((s) => s.sector === 'reflective')!.count).toBe(1); - // deleteAll — clears all 4 sector tables + // deleteAll — clears all sector tables const totalDeleted = store.deleteAll(); - expect(totalDeleted).toBe(3); // 3 remaining after episodic was deleted + expect(totalDeleted).toBe(2); // 2 remaining after episodic was deleted const summaryAfterDeleteAll = store.getSectorSummary(); for (const s of summaryAfterDeleteAll) { diff --git a/ui/dashboard/src/app/memory/page.tsx b/ui/dashboard/src/app/memory/page.tsx index 2332d5d..a7f161f 100644 --- a/ui/dashboard/src/app/memory/page.tsx +++ b/ui/dashboard/src/app/memory/page.tsx @@ -156,40 +156,50 @@ export default function MemoryVaultPage() { const filteredMemories = useMemo(() => { if (!data?.recent) return []; return data.recent.filter((item) => { - // Exclude semantic memories - they're better visualized in the Knowledge Graph tab - if (item.sector === 'semantic') return false; + // Exclude semantic memories (visualized in Knowledge Graph tab) and reflective memories (disabled) + if (item.sector === 'semantic' || item.sector === 'reflective') return false; const matchesSearch = !searchTerm || item.preview.toLowerCase().includes(searchTerm.toLowerCase()); const matchesSector = filterSector === 'all' || item.sector === filterSector; return matchesSearch && matchesSector; }); }, [data, searchTerm, filterSector]); + // Filter out reflective memories from display data + const displayData = useMemo(() => { + if (!data) return null; + return { + ...data, + summary: data.summary.filter(s => s.sector !== 'reflective'), + recent: data.recent?.filter(r => r.sector !== 'reflective'), + }; + }, [data]); + // Calculate quick stats const quickStats = useMemo(() => { - if (!data) return null; - const total = data.summary.reduce((sum, s) => sum + s.count, 0); - const totalRetrievals = data.recent?.reduce((sum, r) => sum + (r.retrievalCount ?? 0), 0) ?? 0; - const mostRecent = data.recent?.[0]?.createdAt; - const oldest = data.recent?.[data.recent.length - 1]?.createdAt; + if (!displayData) return null; + const total = displayData.summary.reduce((sum, s) => sum + s.count, 0); + const totalRetrievals = displayData.recent?.reduce((sum, r) => sum + (r.retrievalCount ?? 0), 0) ?? 0; + const mostRecent = displayData.recent?.[0]?.createdAt; + const oldest = displayData.recent?.[displayData.recent.length - 1]?.createdAt; return { total, totalRetrievals, - avgRetrievals: data.recent?.length ? Math.round(totalRetrievals / data.recent.length) : 0, + avgRetrievals: displayData.recent?.length ? Math.round(totalRetrievals / displayData.recent.length) : 0, mostRecent, oldest, }; - }, [data]); + }, [displayData]); // Calculate sector counts for ProfileStats const sectorCounts = useMemo(() => { - if (!data) return { episodic: 0, procedural: 0, semantic: 0 }; + if (!displayData) return { episodic: 0, procedural: 0, semantic: 0 }; return { - episodic: data.summary.find(s => s.sector === 'episodic')?.count ?? 0, - procedural: data.summary.find(s => s.sector === 'procedural')?.count ?? 0, - semantic: data.summary.find(s => s.sector === 'semantic')?.count ?? 0, + episodic: displayData.summary.find(s => s.sector === 'episodic')?.count ?? 0, + procedural: displayData.summary.find(s => s.sector === 'procedural')?.count ?? 0, + semantic: displayData.summary.find(s => s.sector === 'semantic')?.count ?? 0, }; - }, [data]); + }, [displayData]); return ( @@ -323,10 +333,10 @@ export default function MemoryVaultPage() { />

- - + +
- +
) : activeTab === 'graph' ? (
From 2c69bb0d77585f8e8123009783d5925573883e0f Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:26:39 +0530 Subject: [PATCH 36/78] default dashboard to first agent profile instead of 'default' --- ui/dashboard/src/app/memory/page.tsx | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/ui/dashboard/src/app/memory/page.tsx b/ui/dashboard/src/app/memory/page.tsx index a7f161f..d7f25b1 100644 --- a/ui/dashboard/src/app/memory/page.tsx +++ b/ui/dashboard/src/app/memory/page.tsx @@ -35,6 +35,7 @@ export default function MemoryVaultPage() { const [editModal, setEditModal] = useState<{ id: string; content: string; sector: string } | null>(null); const [editBusy, setEditBusy] = useState(false); const [currentProfile, setCurrentProfile] = useState('default'); + const [profileResolved, setProfileResolved] = useState(false); const [showProfileManagement, setShowProfileManagement] = useState(false); const [profileSwitching, setProfileSwitching] = useState(false); const [embedded, setEmbedded] = useState(false); @@ -46,7 +47,19 @@ export default function MemoryVaultPage() { ); }, []); + // On mount, pick the first non-default agent profile (or fall back to 'default') + useEffect(() => { + apiService.getProfiles().then(({ profiles }) => { + const agent = profiles.find(p => p !== 'default'); + if (agent) setCurrentProfile(agent); + setProfileResolved(true); + }).catch(() => { + setProfileResolved(true); + }); + }, []); + const fetchData = useCallback(async () => { + if (!profileResolved) return; try { setLoading(true); setError(null); @@ -58,7 +71,7 @@ export default function MemoryVaultPage() { } finally { setLoading(false); } - }, [currentProfile]); + }, [currentProfile, profileResolved]); useEffect(() => { fetchData(); @@ -201,6 +214,14 @@ export default function MemoryVaultPage() { }; }, [displayData]); + // Wait for profile resolution before rendering + if (!profileResolved) { + return ( +
+
Loading profiles...
+
+ ); + } return (
From 3dfac23ee57a733d369c77921a3c9c6674035e4e Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:32:11 +0530 Subject: [PATCH 37/78] fix build issues --- docs/SUMMARY.md | 1 + memory/src/sqlite-store.ts | 2 -- memory/src/types.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 20384b3..e563e7a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -3,6 +3,7 @@ * [Ekai Documentation](intro.md) * [Architecture Overview](architecture-overview.md) * [Getting Started](getting-started.md) +* [OpenRouter Quickstart](openrouter-quickstart.md) * [Usage with Claude Code](USAGE_WITH_CLAUDE_CODE.md) * [Usage with Codex](USAGE_WITH_CODEX.md) * [Get Started with x402](GET_STARTED_WITH_X402.md) diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 13bb14d..e2a71da 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -286,7 +286,6 @@ export class SqliteMemoryStore { episodic: [], semantic: [], procedural: [], - reflective: [], // kept for type compatibility; not queried }; for (const sector of SECTORS) { @@ -800,7 +799,6 @@ export class SqliteMemoryStore { const table = sector === 'procedural' ? 'procedural_memory' : sector === 'semantic' ? 'semantic_memory' - : sector === 'reflective' ? 'reflective_memory' : 'memory'; const timeCol = sector === 'semantic' ? 'updated_at' : 'last_accessed'; diff --git a/memory/src/types.ts b/memory/src/types.ts index f628cc5..fb514ec 100644 --- a/memory/src/types.ts +++ b/memory/src/types.ts @@ -1,4 +1,4 @@ -export type SectorName = 'episodic' | 'semantic' | 'procedural' | 'reflective'; +export type SectorName = 'episodic' | 'semantic' | 'procedural'; export type SemanticDomain = 'user' | 'world' | 'self'; From d95bcd4dabfddd39f3628bbc2432bbed471c308c Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:46:31 +0530 Subject: [PATCH 38/78] fixed docker build --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 47aaa1d..44e2e79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -146,9 +146,11 @@ RUN cd memory && npm install --omit=dev COPY --from=memory-build /app/memory/dist ./memory/dist RUN mkdir -p /app/memory/data -# OpenRouter (depends on memory package above) +# OpenRouter — rewrite workspace ref to local file path before install COPY integrations/openrouter/package.json ./integrations/openrouter/ -RUN cd integrations/openrouter && npm install --omit=dev +RUN cd integrations/openrouter && \ + sed -i 's|"@ekai/memory": "\*"|"@ekai/memory": "file:../../memory"|' package.json && \ + npm install --omit=dev COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist # Entrypoint From e5b84251422597f74a57f7f9325e71cdd4afcd05 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:28:34 +0530 Subject: [PATCH 39/78] make ekai-cloudrun the default Docker build target Move ekai-cloudrun stage to be the last stage in the Dockerfile so `docker build .` without --target defaults to the Cloud Run image. --- Dockerfile | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index 44e2e79..24afde6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,32 +94,6 @@ WORKDIR /app/integrations/openrouter EXPOSE 4010 CMD ["node", "dist/server.js"] -# ---------- openrouter + dashboard Cloud Run runtime (single container) ---------- -FROM node:20-alpine AS ekai-cloudrun -WORKDIR /app -ENV NODE_ENV=production - -# Memory package (workspace dependency of openrouter) -COPY memory/package.json ./memory/ -RUN cd memory && npm install --omit=dev -COPY --from=memory-build /app/memory/dist ./memory/dist - -# OpenRouter — rewrite workspace ref to local file path before install -COPY integrations/openrouter/package.json ./integrations/openrouter/ -RUN cd integrations/openrouter && \ - sed -i 's|"@ekai/memory": "\*"|"@ekai/memory": "file:../../memory"|' package.json && \ - npm install --omit=dev -COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist - -# Dashboard static export -COPY --from=dashboard-embedded-build /app/ui/dashboard/out ./dashboard-static - -RUN mkdir -p /app/memory/data -WORKDIR /app/integrations/openrouter -ENV DASHBOARD_STATIC_DIR=/app/dashboard-static -EXPOSE 4010 -CMD ["node", "dist/server.js"] - # ---------- fullstack runtime ---------- FROM node:20-alpine AS ekai-gateway-runtime WORKDIR /app @@ -162,3 +136,29 @@ ENV NODE_ENV=production EXPOSE 3001 3000 4010 VOLUME ["/app/gateway/data", "/app/gateway/logs", "/app/memory/data"] CMD ["/app/start-docker-fullstack.sh"] + +# ---------- openrouter + dashboard Cloud Run runtime (single container) ---------- +FROM node:20-alpine AS ekai-cloudrun +WORKDIR /app +ENV NODE_ENV=production + +# Memory package (workspace dependency of openrouter) +COPY memory/package.json ./memory/ +RUN cd memory && npm install --omit=dev +COPY --from=memory-build /app/memory/dist ./memory/dist + +# OpenRouter — rewrite workspace ref to local file path before install +COPY integrations/openrouter/package.json ./integrations/openrouter/ +RUN cd integrations/openrouter && \ + sed -i 's|"@ekai/memory": "\*"|"@ekai/memory": "file:../../memory"|' package.json && \ + npm install --omit=dev +COPY --from=openrouter-build /app/integrations/openrouter/dist ./integrations/openrouter/dist + +# Dashboard static export +COPY --from=dashboard-embedded-build /app/ui/dashboard/out ./dashboard-static + +RUN mkdir -p /app/memory/data +WORKDIR /app/integrations/openrouter +ENV DASHBOARD_STATIC_DIR=/app/dashboard-static +EXPOSE 4010 +CMD ["node", "dist/server.js"] From 27490a6b07960bb88edb0b5559ff8ed2636ae07f Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:10:58 +0530 Subject: [PATCH 40/78] Remove model catalogue feature Remove the /v1/models browsing API, dashboard models page, and catalogue-only data files. Provider routing JSON files are retained as they configure passthrough routing. --- gateway/src/app/handlers/models-handler.ts | 31 - .../domain/services/model-catalog-service.ts | 180 -- gateway/src/index.ts | 4 +- model_catalog/model_schema_v1.json | 616 ---- model_catalog/model_template_v1.json | 85 - model_catalog/programming_models_v1.json | 2560 ----------------- model_catalog/validate_models.py | 39 - ui/dashboard/src/app/models/page.tsx | 421 --- ui/dashboard/src/app/page.tsx | 23 - ui/dashboard/src/components/ModelCatalog.tsx | 147 - ui/dashboard/src/hooks/useModels.ts | 103 - ui/dashboard/src/lib/api.ts | 38 - ui/dashboard/src/lib/modelFilters.ts | 19 - 13 files changed, 1 insertion(+), 4265 deletions(-) delete mode 100644 gateway/src/app/handlers/models-handler.ts delete mode 100644 gateway/src/domain/services/model-catalog-service.ts delete mode 100644 model_catalog/model_schema_v1.json delete mode 100644 model_catalog/model_template_v1.json delete mode 100644 model_catalog/programming_models_v1.json delete mode 100644 model_catalog/validate_models.py delete mode 100644 ui/dashboard/src/app/models/page.tsx delete mode 100644 ui/dashboard/src/components/ModelCatalog.tsx delete mode 100644 ui/dashboard/src/hooks/useModels.ts delete mode 100644 ui/dashboard/src/lib/modelFilters.ts diff --git a/gateway/src/app/handlers/models-handler.ts b/gateway/src/app/handlers/models-handler.ts deleted file mode 100644 index 2a5719d..0000000 --- a/gateway/src/app/handlers/models-handler.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Request, Response } from 'express'; -import { modelCatalogService } from '../../domain/services/model-catalog-service.js'; -import { logger } from '../../infrastructure/utils/logger.js'; - -const MAX_LIMIT = 500; - -export const handleModelsRequest = (req: Request, res: Response): void => { - try { - const { provider, endpoint, search } = req.query; - const limit = Math.min(parseInt(String(req.query.limit || '200'), 10) || 200, MAX_LIMIT); - const offset = Math.max(parseInt(String(req.query.offset || '0'), 10) || 0, 0); - - const { total, items } = modelCatalogService.getModels({ - provider: provider ? String(provider) : undefined, - endpoint: endpoint === 'messages' ? 'messages' : endpoint === 'chat_completions' ? 'chat_completions' : endpoint === 'responses' ? 'responses' : undefined, - search: search ? String(search) : undefined, - limit, - offset - }); - - res.json({ - total, - limit, - offset, - items - }); - } catch (error) { - logger.error('Failed to fetch models', error, { module: 'models-handler' }); - res.status(500).json({ error: 'Failed to fetch models' }); - } -}; diff --git a/gateway/src/domain/services/model-catalog-service.ts b/gateway/src/domain/services/model-catalog-service.ts deleted file mode 100644 index 66161ce..0000000 --- a/gateway/src/domain/services/model-catalog-service.ts +++ /dev/null @@ -1,180 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { pricingLoader, ModelPricing } from '../../infrastructure/utils/pricing-loader.js'; -import { ModelUtils } from '../../infrastructure/utils/model-utils.js'; -import { logger } from '../../infrastructure/utils/logger.js'; - -type EndpointType = 'chat_completions' | 'messages' | 'responses'; - -export interface ModelCatalogEntry { - id: string; - provider: string; - endpoint: EndpointType; - pricing: (ModelPricing & { currency: string; unit: string }) | null; - source: string; -} - -interface ProviderModels { - provider: string; - models: string[]; -} - -interface CatalogFile { - filename: string; - endpoint: EndpointType; -} - -const CATALOG_FILES: CatalogFile[] = [ - { filename: 'chat_completions_providers_v1.json', endpoint: 'chat_completions' }, - { filename: 'messages_providers_v1.json', endpoint: 'messages' }, - { filename: 'responses_providers_v1.json', endpoint: 'responses' } -]; - -export interface ModelCatalogFilter { - provider?: string; - endpoint?: EndpointType; - search?: string; - limit?: number; - offset?: number; -} - -export class ModelCatalogService { - private cache: ModelCatalogEntry[] | null = null; - private readonly cacheTtlMs = 5 * 60 * 1000; - private lastLoad = 0; - - getModels(filter: ModelCatalogFilter = {}): { total: number; items: ModelCatalogEntry[] } { - const catalog = this.ensureLoaded(); - - let items = catalog; - - if (filter.provider) { - const provider = filter.provider.toLowerCase(); - items = items.filter(m => m.provider === provider); - } - - if (filter.endpoint) { - items = items.filter(m => m.endpoint === filter.endpoint); - } - - if (filter.search) { - const term = filter.search.toLowerCase(); - items = items.filter(m => m.id.toLowerCase().includes(term)); - } - - const total = items.length; - - const limit = Math.min(Math.max(filter.limit ?? 200, 1), 500); - const offset = Math.max(filter.offset ?? 0, 0); - - const paged = items.slice(offset, offset + limit); - - return { total, items: paged }; - } - - private ensureLoaded(): ModelCatalogEntry[] { - const now = Date.now(); - if (this.cache && now - this.lastLoad < this.cacheTtlMs) { - return this.cache; - } - - try { - const entries: ModelCatalogEntry[] = []; - const pricingMap = pricingLoader.loadAllPricing(); - - for (const catalogFile of CATALOG_FILES) { - const providers = this.readCatalogFile(catalogFile.filename); - providers.forEach(providerEntry => { - const provider = providerEntry.provider.toLowerCase(); - const openRouterModels = provider === 'openrouter' ? this.getOpenRouterModels(pricingMap) : null; - const models = openRouterModels ?? providerEntry.models; - - models.forEach(modelId => { - const normalizedModel = ModelUtils.normalizeModelName(modelId); - const pricingConfig = pricingMap.get(provider); - const modelPricing = pricingConfig?.models[normalizedModel] || pricingConfig?.models[modelId] || null; - - const pricing = modelPricing - ? { - ...modelPricing, - currency: pricingConfig?.currency || 'USD', - unit: pricingConfig?.unit || '1K tokens' - } - : null; - - entries.push({ - id: modelId, - provider, - endpoint: catalogFile.endpoint, - pricing, - source: catalogFile.filename - }); - }); - }); - } - - this.cache = entries; - this.lastLoad = now; - - logger.info('Model catalog loaded', { - entryCount: entries.length, - operation: 'model_catalog_load', - module: 'model-catalog-service' - }); - - return entries; - } catch (error) { - logger.error('Failed to load model catalog', error, { module: 'model-catalog-service' }); - this.cache = []; - this.lastLoad = now; - return this.cache; - } - } - - private getOpenRouterModels(pricingMap: Map): string[] | null { - const pricing = pricingMap.get('openrouter'); - if (!pricing || !pricing.models) return null; - const ids = Object.keys(pricing.models); - return ids.length ? ids : null; - } - - private readCatalogFile(filename: string): ProviderModels[] { - const filePath = this.resolveCatalogPath(filename); - - if (!filePath) { - logger.warn('Catalog file not found', { filename, module: 'model-catalog-service' }); - return []; - } - - try { - const raw = fs.readFileSync(filePath, 'utf-8'); - const parsed = JSON.parse(raw) as { providers?: ProviderModels[] }; - return parsed.providers || []; - } catch (error) { - logger.error('Failed to parse catalog file', error, { filename, module: 'model-catalog-service' }); - return []; - } - } - - private resolveCatalogPath(filename: string): string | null { - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - - const candidates = [ - path.resolve(process.cwd(), 'model_catalog', filename), - path.resolve(__dirname, '../../../model_catalog', filename), - path.resolve(__dirname, '../../../../model_catalog', filename) - ]; - - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - return candidate; - } - } - - return null; - } -} - -export const modelCatalogService = new ModelCatalogService(); diff --git a/gateway/src/index.ts b/gateway/src/index.ts index 863a5f9..74df8f0 100644 --- a/gateway/src/index.ts +++ b/gateway/src/index.ts @@ -30,7 +30,6 @@ import cors from 'cors'; import { handleOpenAIFormatChat, handleAnthropicFormatChat, handleOpenAIResponses } from './app/handlers/chat-handler.js'; import { handleUsageRequest } from './app/handlers/usage-handler.js'; import { handleConfigStatus } from './app/handlers/config-handler.js'; -import { handleModelsRequest } from './app/handlers/models-handler.js'; import { handleGetBudget, handleUpdateBudget } from './app/handlers/budget-handler.js'; import { logger } from './infrastructure/utils/logger.js'; import { requestContext } from './infrastructure/middleware/request-context.js'; @@ -86,8 +85,7 @@ async function bootstrap(): Promise { app.post('/v1/chat/completions', handleOpenAIFormatChat); app.post('/v1/messages', handleAnthropicFormatChat); app.post('/v1/responses', handleOpenAIResponses); - app.get('/v1/models', handleModelsRequest); - app.get('/usage', handleUsageRequest); +app.get('/usage', handleUsageRequest); app.get('/config/status', handleConfigStatus); app.get('/budget', handleGetBudget); app.put('/budget', handleUpdateBudget); diff --git a/model_catalog/model_schema_v1.json b/model_catalog/model_schema_v1.json deleted file mode 100644 index 27e066e..0000000 --- a/model_catalog/model_schema_v1.json +++ /dev/null @@ -1,616 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Transformed Model Schema v2", - "type": "object", - "properties": { - "all_models": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "patternProperties": { - "^(?!all_models$)[a-zA-Z0-9._-]+$": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "type": "string" - } - }, - "common": { - "type": "object", - "properties": { - "modalities": { - "$ref": "#/$defs/modalities" - }, - "capabilities": { - "type": "array", - "items": { - "type": "string" - } - }, - "context_window": { - "type": [ - "integer", - "null" - ], - "minimum": 0 - }, - "max_output_tokens": { - "type": [ - "integer", - "null" - ], - "minimum": 0 - }, - "reasoning": { - "type": [ - "boolean", - "null" - ] - } - }, - "required": [ - "modalities", - "capabilities" - ] - } - }, - "patternProperties": { - "^(openai|openrouter|anthropic|xai)$": { - "type": "object", - "properties": { - "snapshots": { - "type": "array", - "items": { - "type": "string" - } - }, - "training_data_cutoff": { - "type": [ - "string", - "null" - ] - }, - "endpoints": { - "type": "array", - "items": { - "type": "string" - } - }, - "rate_limits": { - "$ref": "#/$defs/rate_limits" - }, - "pricing": { - "$ref": "#/$defs/pricing" - } - }, - "required": [ - "snapshots", - "training_data_cutoff", - "endpoints", - "rate_limits", - "pricing" - ] - } - }, - "required": [ - "providers", - "common" - ] - } - }, - "$defs": { - "modalities": { - "type": "object", - "properties": { - "input": { - "type": "object", - "properties": { - "text": { - "type": "boolean" - }, - "image": { - "type": "boolean" - }, - "audio": { - "type": "boolean" - } - }, - "required": [ - "text", - "image", - "audio" - ] - }, - "output": { - "type": "object", - "properties": { - "text": { - "type": "boolean" - }, - "image": { - "type": "boolean" - }, - "audio": { - "type": "boolean" - } - }, - "required": [ - "text", - "image", - "audio" - ] - } - }, - "required": [ - "input", - "output" - ] - }, - "rate_limits": { - "type": "object", - "properties": { - "tiers": { - "type": "object", - "properties": { - "free": { - "$ref": "#/$defs/rate_tier" - }, - "tier_1": { - "$ref": "#/$defs/rate_tier" - }, - "tier_2": { - "$ref": "#/$defs/rate_tier" - }, - "tier_3": { - "$ref": "#/$defs/rate_tier" - }, - "tier_4": { - "$ref": "#/$defs/rate_tier" - }, - "tier_5": { - "$ref": "#/$defs/rate_tier" - } - } - }, - "generic_limits": { - "type": "object", - "properties": { - "rpm": { - "type": [ - "integer", - "null" - ], - "minimum": 0 - }, - "tpm": { - "type": [ - "integer", - "null" - ], - "minimum": 0 - } - } - } - } - }, - "rate_tier": { - "type": "object", - "properties": { - "rpm": { - "type": [ - "integer", - "null" - ], - "minimum": 0 - }, - "tpm": { - "type": [ - "integer", - "null" - ], - "minimum": 0 - }, - "batch_queue_limit": { - "type": [ - "integer", - "null" - ], - "minimum": 0 - } - } - }, - "pricing": { - "type": "object", - "properties": { - "text_per_MTok": { - "type": "object", - "properties": { - "batch": { - "$ref": "#/$defs/textPricing" - }, - "flex": { - "$ref": "#/$defs/textPricing" - }, - "standard": { - "allOf": [ - { - "$ref": "#/$defs/textPricing" - }, - { - "properties": { - "cache_writes_5m": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "cache_writes_1h": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "cache_hits_refreshes": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - } - ] - }, - "priority": { - "$ref": "#/$defs/textPricing" - } - } - }, - "image_per_MTok": { - "$ref": "#/$defs/textPricing" - }, - "audio_per_MTok": { - "$ref": "#/$defs/textPricing" - }, - "fine_tuning_per_MTok": { - "type": "object", - "properties": { - "batch": { - "type": "object", - "properties": { - "training": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "cached_input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "output": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - }, - "standard": { - "type": "object", - "properties": { - "training": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "cached_input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "output": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - } - } - }, - "embeddings_per_MTok": { - "type": "object", - "properties": { - "cost": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "batch_cost": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - }, - "legacy_models_per_MTok": { - "type": "object", - "properties": { - "batch": { - "type": "object", - "properties": { - "input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "output": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - }, - "standard": { - "type": "object", - "properties": { - "input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "output": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - } - } - }, - "per_image": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "feature_specific": { - "type": "object", - "properties": { - "batch_processing": { - "type": "object", - "properties": { - "input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "output": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - }, - "long_context": { - "type": "object", - "properties": { - "<=200k_input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "<=200k_output": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - ">200k_input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - ">200k_output": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - }, - "tool_use": { - "type": "object", - "properties": { - "auto_none": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "any_tool": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - } - } - }, - "tools": { - "type": "object", - "properties": { - "code_interpreter_per_container": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "file_search_storage_per_GB_per_day": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "file_search_call_per_2k_calls": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "web_search_per_1k_calls": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "web_search_preview_per_1k_calls": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "mcp_connector": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "bash_in_tokens": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "code_execution_per_session_hour": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "computer_use_in_tokens": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "text_editor_in_tokens": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "web_fetch": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "livesearch": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - } - } - }, - "textPricing": { - "type": "object", - "properties": { - "input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "cached_input": { - "type": [ - "number", - "null" - ], - "minimum": 0 - }, - "output": { - "type": [ - "number", - "null" - ], - "minimum": 0 - } - } - } - } -} \ No newline at end of file diff --git a/model_catalog/model_template_v1.json b/model_catalog/model_template_v1.json deleted file mode 100644 index 640ce3d..0000000 --- a/model_catalog/model_template_v1.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "all_models": [ - "MODEL_NAME" - ], - "MODEL_NAME": { - "providers": ["PROVIDER_NAME"], - "common": { - "modalities": { - "input": { "text": false, "image": false, "audio": false }, - "output": { "text": false, "image": false, "audio": false } - }, - "capabilities": [], - "context_window": null, - "max_output_tokens": null, - "reasoning": null - }, - "PROVIDER_NAME": { - "snapshots": [], - "training_data_cutoff": null, - "endpoints": [], - "rate_limits": { - "tiers": { - "free": { "rpm": null, "tpm": null, "batch_queue_limit": null }, - "tier_1": { "rpm": null, "tpm": null, "batch_queue_limit": null }, - "tier_2": { "rpm": null, "tpm": null, "batch_queue_limit": null }, - "tier_3": { "rpm": null, "tpm": null, "batch_queue_limit": null }, - "tier_4": { "rpm": null, "tpm": null, "batch_queue_limit": null }, - "tier_5": { "rpm": null, "tpm": null, "batch_queue_limit": null } - }, - "generic_limits": { "rpm": null, "tpm": null } - }, - "pricing": { - "text_per_MTok": { - "batch": { "input": null, "cached_input": null, "output": null }, - "flex": { "input": null, "cached_input": null, "output": null }, - "standard": { - "input": null, - "cached_input": null, - "output": null, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { "input": null, "cached_input": null, "output": null } - }, - "image_per_MTok": { "input": null, "cached_input": null, "output": null }, - "audio_per_MTok": { "input": null, "cached_input": null, "output": null }, - "fine_tuning_per_MTok": { - "batch": { "training": null, "input": null, "cached_input": null, "output": null }, - "standard": { "training": null, "input": null, "cached_input": null, "output": null } - }, - "embeddings_per_MTok": { "cost": null, "batch_cost": null }, - "legacy_models_per_MTok": { - "batch": { "input": null, "output": null }, - "standard": { "input": null, "output": null } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { "input": null, "output": null }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { "auto_none": null, "any_tool": null } - }, - "tools": { - "code_interpreter": null, - "file_search_storage": null, - "file_search_call": null, - "web_search": null, - "web_search_preview": null, - "mcp_connector": null, - "bash": null, - "code_execution": null, - "computer_use": null, - "text_editor": null, - "web_fetch": null, - "livesearch": null - } - } - } - } -} diff --git a/model_catalog/programming_models_v1.json b/model_catalog/programming_models_v1.json deleted file mode 100644 index 9ea40ce..0000000 --- a/model_catalog/programming_models_v1.json +++ /dev/null @@ -1,2560 +0,0 @@ -{ - "all_models": [ - "gpt-5", - "gpt-5-mini", - "gpt-4.1-mini", - "claude-3.7-sonnet", - "claude-sonnet-4", - "claude-opus-4.1", - "grok-code-fast-1", - "grok-4-0709" - ], - "gpt-5": { - "providers": [ - "openai", - "openrouter" - ], - "common": { - "modalities": { - "input": { - "text": true, - "image": true, - "audio": false - }, - "output": { - "text": true, - "image": false, - "audio": false - } - }, - "capabilities": [ - "streaming", - "function_calling", - "structured_outputs", - "distillation", - "reasoning" - ], - "context_window": 400000, - "max_output_tokens": 128000, - "reasoning": true - }, - "openai": { - "snapshots": [ - "gpt-5-2025-08-07" - ], - "training_data_cutoff": "2024-09-30", - "endpoints": [ - "chat_completions", - "responses", - "batch" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 500, - "tpm": 30000, - "batch_queue_limit": 90000 - }, - "tier_2": { - "rpm": 5000, - "tpm": 450000, - "batch_queue_limit": 1350000 - }, - "tier_3": { - "rpm": 5000, - "tpm": 800000, - "batch_queue_limit": 100000000 - }, - "tier_4": { - "rpm": 10000, - "tpm": 2000000, - "batch_queue_limit": 200000000 - }, - "tier_5": { - "rpm": 15000, - "tpm": 40000000, - "batch_queue_limit": 15000000000 - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": 0.625, - "cached_input": 0.0625, - "output": 5.0 - }, - "flex": { - "input": 0.625, - "cached_input": 0.0625, - "output": 5.0 - }, - "standard": { - "input": 1.25, - "cached_input": 0.125, - "output": 10.0, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": 2.5, - "cached_input": 0.25, - "output": 25.0 - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": 0.03, - "file_search_storage_per_GB_per_day": 0.1, - "file_search_call_per_2k_calls": 2.5, - "web_search_per_1k_calls": 10.0, - "web_search_preview_per_1k_calls": 10.0, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": null - } - } - }, - "openrouter": { - "snapshots": [ - "gpt-5-2025-08-07" - ], - "training_data_cutoff": "2024-09-30", - "endpoints": [ - "chat_completions" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 500, - "tpm": 30000, - "batch_queue_limit": 90000 - }, - "tier_2": { - "rpm": 5000, - "tpm": 450000, - "batch_queue_limit": 1350000 - }, - "tier_3": { - "rpm": 5000, - "tpm": 800000, - "batch_queue_limit": 100000000 - }, - "tier_4": { - "rpm": 10000, - "tpm": 2000000, - "batch_queue_limit": 200000000 - }, - "tier_5": { - "rpm": 15000, - "tpm": 40000000, - "batch_queue_limit": 15000000000 - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": 0.65625, - "cached_input": 0.065625, - "output": 5.25 - }, - "flex": { - "input": 0.65625, - "cached_input": 0.065625, - "output": 5.25 - }, - "standard": { - "input": 1.3125, - "cached_input": 0.13125, - "output": 10.5, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": 2.625, - "cached_input": 0.2625, - "output": 26.25 - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": 0.0315, - "file_search_storage_per_GB_per_day": 0.105, - "file_search_call_per_2k_calls": 2.625, - "web_search_per_1k_calls": 10.5, - "web_search_preview_per_1k_calls": 10.5, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": null - } - } - } - }, - "gpt-5-mini": { - "providers": [ - "openai", - "openrouter" - ], - "common": { - "modalities": { - "input": { - "text": true, - "image": true, - "audio": false - }, - "output": { - "text": true, - "image": false, - "audio": false - } - }, - "capabilities": [ - "streaming", - "structured_outputs", - "function_calling" - ], - "context_window": 400000, - "max_output_tokens": 128000, - "reasoning": true - }, - "openai": { - "snapshots": [ - "gpt-5-mini", - "gpt-5-mini-2025-08-07" - ], - "training_data_cutoff": "2024-05-31", - "endpoints": [ - "chat_completions", - "responses", - "batch" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 500, - "tpm": 500000, - "batch_queue_limit": 5000000 - }, - "tier_2": { - "rpm": 5000, - "tpm": 2000000, - "batch_queue_limit": 20000000 - }, - "tier_3": { - "rpm": 5000, - "tpm": 4000000, - "batch_queue_limit": 40000000 - }, - "tier_4": { - "rpm": 10000, - "tpm": 10000000, - "batch_queue_limit": 1000000000 - }, - "tier_5": { - "rpm": 30000, - "tpm": 180000000, - "batch_queue_limit": 15000000000 - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": 0.125, - "cached_input": 0.0125, - "output": 1.0 - }, - "flex": { - "input": 0.125, - "cached_input": 0.0125, - "output": 1.0 - }, - "standard": { - "input": 0.25, - "cached_input": 0.025, - "output": 2.0, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": 0.45, - "cached_input": 0.045, - "output": 3.6 - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": 0.03, - "file_search_storage_per_GB_per_day": 0.1, - "file_search_call_per_2k_calls": 2.5, - "web_search_per_1k_calls": 10.0, - "web_search_preview_per_1k_calls": 10.0, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": null - } - } - }, - "openrouter": { - "snapshots": [ - "gpt-5-mini", - "gpt-5-mini-2025-08-07" - ], - "training_data_cutoff": "2024-05-31", - "endpoints": [ - "chat_completions" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 500, - "tpm": 500000, - "batch_queue_limit": 5000000 - }, - "tier_2": { - "rpm": 5000, - "tpm": 2000000, - "batch_queue_limit": 20000000 - }, - "tier_3": { - "rpm": 5000, - "tpm": 4000000, - "batch_queue_limit": 40000000 - }, - "tier_4": { - "rpm": 10000, - "tpm": 10000000, - "batch_queue_limit": 1000000000 - }, - "tier_5": { - "rpm": 30000, - "tpm": 180000000, - "batch_queue_limit": 15000000000 - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": 0.13125, - "cached_input": 0.013125, - "output": 1.05 - }, - "flex": { - "input": 0.13125, - "cached_input": 0.013125, - "output": 1.05 - }, - "standard": { - "input": 0.2625, - "cached_input": 0.02625, - "output": 2.1, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": 0.4725, - "cached_input": 0.04725, - "output": 3.78 - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": 0.0315, - "file_search_storage_per_GB_per_day": 0.105, - "file_search_call_per_2k_calls": 2.625, - "web_search_per_1k_calls": 10.5, - "web_search_preview_per_1k_calls": 10.5, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": null - } - } - } - }, - "gpt-4.1-mini": { - "providers": [ - "openai", - "openrouter" - ], - "common": { - "modalities": { - "input": { - "text": true, - "image": true, - "audio": false - }, - "output": { - "text": true, - "image": false, - "audio": false - } - }, - "capabilities": [ - "streaming", - "structured_outputs", - "function_calling", - "fine_tuning", - "predicted_outputs" - ], - "context_window": 1048576, - "max_output_tokens": 32768, - "reasoning": false - }, - "openai": { - "snapshots": [ - "gpt-4.1-mini", - "gpt-4.1-mini-2025-04-14" - ], - "training_data_cutoff": "2024-06-01", - "endpoints": [ - "chat_completions", - "responses", - "assistants", - "batch", - "fine_tuning" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": 3, - "tpm": 40000, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": 5000, - "tpm": 2000000, - "batch_queue_limit": 20000000 - }, - "tier_3": { - "rpm": 5000, - "tpm": 4000000, - "batch_queue_limit": 40000000 - }, - "tier_4": { - "rpm": 10000, - "tpm": 10000000, - "batch_queue_limit": 1000000000 - }, - "tier_5": { - "rpm": 30000, - "tpm": 150000000, - "batch_queue_limit": 15000000000 - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": 0.2, - "cached_input": null, - "output": 0.8 - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 0.4, - "cached_input": 0.1, - "output": 1.6, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": 0.7, - "cached_input": 0.175, - "output": 2.8 - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": 5.0, - "input": 0.4, - "cached_input": 0.1, - "output": 1.6 - }, - "standard": { - "training": 5.0, - "input": 0.8, - "cached_input": 0.2, - "output": 3.2 - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": 0.03, - "file_search_storage_per_GB_per_day": 0.1, - "file_search_call_per_2k_calls": 2.5, - "web_search_per_1k_calls": 10.0, - "web_search_preview_per_1k_calls": 25.0, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": null - } - } - }, - "openrouter": { - "snapshots": [ - "gpt-4.1-mini", - "gpt-4.1-mini-2025-04-14" - ], - "training_data_cutoff": "2024-06-01", - "endpoints": [ - "chat_completions" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": 3, - "tpm": 40000, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": 5000, - "tpm": 2000000, - "batch_queue_limit": 20000000 - }, - "tier_3": { - "rpm": 5000, - "tpm": 4000000, - "batch_queue_limit": 40000000 - }, - "tier_4": { - "rpm": 10000, - "tpm": 10000000, - "batch_queue_limit": 1000000000 - }, - "tier_5": { - "rpm": 30000, - "tpm": 150000000, - "batch_queue_limit": 15000000000 - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": 0.21, - "cached_input": null, - "output": 0.84 - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 0.42, - "cached_input": 0.105, - "output": 1.68, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": 0.735, - "cached_input": 0.18375, - "output": 2.94 - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": 5.25, - "input": 0.42, - "cached_input": 0.105, - "output": 1.68 - }, - "standard": { - "training": 5.25, - "input": 0.84, - "cached_input": 0.21, - "output": 3.36 - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": 0.0315, - "file_search_storage_per_GB_per_day": 0.105, - "file_search_call_per_2k_calls": 2.625, - "web_search_per_1k_calls": 10.5, - "web_search_preview_per_1k_calls": 26.25, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": null - } - } - } - }, - "claude-3.7-sonnet": { - "providers": [ - "anthropic", - "openrouter" - ], - "common": { - "modalities": { - "input": { - "text": true, - "image": true, - "audio": false - }, - "output": { - "text": true, - "image": false, - "audio": false - } - }, - "capabilities": [ - "reasoning", - "vision", - "extended_thinking" - ], - "context_window": 200000, - "max_output_tokens": 64000, - "reasoning": true - }, - "anthropic": { - "snapshots": [ - "claude-3-7-sonnet-20250219", - "claude-3-7-sonnet-latest" - ], - "training_data_cutoff": "2024-11", - "endpoints": [ - "messages", - "models", - "organization" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 50, - "tpm": 20000, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": 1000, - "tpm": 40000, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": 2000, - "tpm": 80000, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": 4000, - "tpm": 200000, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 3.0, - "cached_input": null, - "output": 15.0, - "cache_writes_5m": 3.75, - "cache_writes_1h": 6.0, - "cache_hits_refreshes": 0.3 - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": 1.5, - "output": 7.5 - }, - "long_context": { - "<=200k_input": 3.0, - "<=200k_output": 15.0, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": 346, - "any_tool": 313 - } - }, - "tools": { - "code_interpreter_per_container": 0.05, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": 10.0, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": 245, - "code_execution_per_session_hour": 0.05, - "computer_use_in_tokens": 735, - "text_editor_in_tokens": 700, - "web_fetch": 0, - "livesearch": null - } - } - }, - "openrouter": { - "snapshots": [ - "claude-3-7-sonnet-20250219", - "claude-3-7-sonnet-latest" - ], - "training_data_cutoff": "2024-11", - "endpoints": [ - "models" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 50, - "tpm": 20000, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": 1000, - "tpm": 40000, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": 2000, - "tpm": 80000, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": 4000, - "tpm": 200000, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 3.15, - "cached_input": null, - "output": 15.75, - "cache_writes_5m": 3.9375, - "cache_writes_1h": 6.3, - "cache_hits_refreshes": 0.315 - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": 1.575, - "output": 7.875 - }, - "long_context": { - "<=200k_input": 3.15, - "<=200k_output": 15.75, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": 363.3, - "any_tool": 328.65 - } - }, - "tools": { - "code_interpreter_per_container": 0.0525, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": 10.5, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": 257.25, - "code_execution_per_session_hour": 0.0525, - "computer_use_in_tokens": 771.75, - "text_editor_in_tokens": 735.0, - "web_fetch": 0.0, - "livesearch": null - } - } - } - }, - "claude-sonnet-4": { - "providers": [ - "anthropic", - "openrouter" - ], - "common": { - "modalities": { - "input": { - "text": true, - "image": true, - "audio": false - }, - "output": { - "text": true, - "image": false, - "audio": false - } - }, - "capabilities": [ - "reasoning", - "vision", - "extended_thinking" - ], - "context_window": 200000, - "max_output_tokens": 64000, - "reasoning": true - }, - "anthropic": { - "snapshots": [ - "claude-sonnet-4-20250514", - "claude-sonnet-4-0" - ], - "training_data_cutoff": "2025-03", - "endpoints": [ - "messages", - "models", - "organization" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 50, - "tpm": 30000, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": 1000, - "tpm": 450000, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": 2000, - "tpm": 800000, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": 4000, - "tpm": 2000000, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 3.0, - "cached_input": null, - "output": 15.0, - "cache_writes_5m": 3.75, - "cache_writes_1h": 6.0, - "cache_hits_refreshes": 0.3 - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": 1.5, - "output": 7.5 - }, - "long_context": { - "<=200k_input": 3.0, - "<=200k_output": 15.0, - ">200k_input": 6.0, - ">200k_output": 22.5 - }, - "tool_use": { - "auto_none": 346, - "any_tool": 313 - } - }, - "tools": { - "code_interpreter_per_container": 0.05, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": 10.0, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": 245, - "code_execution_per_session_hour": 0.05, - "computer_use_in_tokens": 735, - "text_editor_in_tokens": 700, - "web_fetch": 0, - "livesearch": null - } - } - }, - "openrouter": { - "snapshots": [ - "claude-sonnet-4-20250514", - "claude-sonnet-4-0" - ], - "training_data_cutoff": "2025-03", - "endpoints": [ - "models" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 50, - "tpm": 30000, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": 1000, - "tpm": 450000, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": 2000, - "tpm": 800000, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": 4000, - "tpm": 2000000, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 3.15, - "cached_input": null, - "output": 15.75, - "cache_writes_5m": 3.9375, - "cache_writes_1h": 6.3, - "cache_hits_refreshes": 0.315 - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": 1.575, - "output": 7.875 - }, - "long_context": { - "<=200k_input": 3.15, - "<=200k_output": 15.75, - ">200k_input": 6.3, - ">200k_output": 23.625 - }, - "tool_use": { - "auto_none": 363.3, - "any_tool": 328.65 - } - }, - "tools": { - "code_interpreter_per_container": 0.0525, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": 10.5, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": 257.25, - "code_execution_per_session_hour": 0.0525, - "computer_use_in_tokens": 771.75, - "text_editor_in_tokens": 735.0, - "web_fetch": 0.0, - "livesearch": null - } - } - } - }, - "claude-opus-4.1": { - "providers": [ - "anthropic", - "openrouter" - ], - "common": { - "modalities": { - "input": { - "text": true, - "image": true, - "audio": false - }, - "output": { - "text": true, - "image": false, - "audio": false - } - }, - "capabilities": [ - "reasoning", - "vision", - "extended_thinking" - ], - "context_window": 200000, - "max_output_tokens": 32000, - "reasoning": true - }, - "anthropic": { - "snapshots": [ - "claude-opus-4-1-20250805", - "claude-opus-4-1" - ], - "training_data_cutoff": "2025-03", - "endpoints": [ - "messages", - "models", - "organization" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 50, - "tpm": 30000, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": 1000, - "tpm": 450000, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": 2000, - "tpm": 800000, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": 4000, - "tpm": 2000000, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 15.0, - "cached_input": null, - "output": 75.0, - "cache_writes_5m": 18.75, - "cache_writes_1h": 30.0, - "cache_hits_refreshes": 1.5 - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": 7.5, - "output": 37.5 - }, - "long_context": { - "<=200k_input": 15.0, - "<=200k_output": 75.0, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": 346, - "any_tool": 313 - } - }, - "tools": { - "code_interpreter_per_container": 0.05, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": 10.0, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": 245, - "code_execution_per_session_hour": 0.05, - "computer_use_in_tokens": 735, - "text_editor_in_tokens": 700, - "web_fetch": 0, - "livesearch": null - } - } - }, - "openrouter": { - "snapshots": [ - "claude-opus-4-1-20250805", - "claude-opus-4-1" - ], - "training_data_cutoff": "2025-03", - "endpoints": [ - "models" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": 50, - "tpm": 30000, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": 1000, - "tpm": 450000, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": 2000, - "tpm": 800000, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": 4000, - "tpm": 2000000, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": null, - "tpm": null - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 15.75, - "cached_input": null, - "output": 78.75, - "cache_writes_5m": 19.6875, - "cache_writes_1h": 31.5, - "cache_hits_refreshes": 1.575 - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": 7.875, - "output": 39.375 - }, - "long_context": { - "<=200k_input": 15.75, - "<=200k_output": 78.75, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": 363.3, - "any_tool": 328.65 - } - }, - "tools": { - "code_interpreter_per_container": 0.0525, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": 10.5, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": 257.25, - "code_execution_per_session_hour": 0.0525, - "computer_use_in_tokens": 771.75, - "text_editor_in_tokens": 735.0, - "web_fetch": 0.0, - "livesearch": null - } - } - } - }, - "grok-code-fast-1": { - "providers": [ - "xai", - "openrouter" - ], - "common": { - "modalities": { - "input": { - "text": true, - "image": false, - "audio": false - }, - "output": { - "text": true, - "image": false, - "audio": false - } - }, - "capabilities": [ - "function_calling", - "structured_outputs", - "reasoning" - ], - "context_window": 256000, - "max_output_tokens": null, - "reasoning": true - }, - "xai": { - "snapshots": [ - "grok-code-fast-1", - "grok-code-fast-1-0825" - ], - "training_data_cutoff": null, - "endpoints": [ - "chat_completions", - "responses" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": 480, - "tpm": 2000000 - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 0.2, - "cached_input": 0.02, - "output": 1.5, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": null, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": null, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": null - } - } - }, - "openrouter": { - "snapshots": [ - "grok-code-fast-1", - "grok-code-fast-1-0825" - ], - "training_data_cutoff": null, - "endpoints": [ - "chat_completions" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": 480, - "tpm": 2000000 - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 0.21, - "cached_input": 0.021, - "output": 1.575, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": null, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": null, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": null - } - } - } - }, - "grok-4-0709": { - "providers": [ - "xai", - "openrouter" - ], - "common": { - "modalities": { - "input": { - "text": true, - "image": false, - "audio": false - }, - "output": { - "text": true, - "image": false, - "audio": false - } - }, - "capabilities": [ - "function_calling", - "structured_outputs", - "reasoning" - ], - "context_window": 256000, - "max_output_tokens": null, - "reasoning": true - }, - "xai": { - "snapshots": [ - "grok-4-0709", - "grok-4", - "grok-4-latest" - ], - "training_data_cutoff": null, - "endpoints": [ - "chat_completions", - "responses" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": 480, - "tpm": 2000000 - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 3.0, - "cached_input": 0.75, - "output": 15.0, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": null, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": null, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": 25.0 - } - } - }, - "openrouter": { - "snapshots": [ - "grok-4-0709", - "grok-4", - "grok-4-latest" - ], - "training_data_cutoff": null, - "endpoints": [ - "chat_completions" - ], - "rate_limits": { - "tiers": { - "free": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_1": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_2": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_3": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_4": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - }, - "tier_5": { - "rpm": null, - "tpm": null, - "batch_queue_limit": null - } - }, - "generic_limits": { - "rpm": 480, - "tpm": 2000000 - } - }, - "pricing": { - "text_per_MTok": { - "batch": { - "input": null, - "cached_input": null, - "output": null - }, - "flex": { - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "input": 3.15, - "cached_input": 0.7875, - "output": 15.75, - "cache_writes_5m": null, - "cache_writes_1h": null, - "cache_hits_refreshes": null - }, - "priority": { - "input": null, - "cached_input": null, - "output": null - } - }, - "image_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "audio_per_MTok": { - "input": null, - "cached_input": null, - "output": null - }, - "fine_tuning_per_MTok": { - "batch": { - "training": null, - "input": null, - "cached_input": null, - "output": null - }, - "standard": { - "training": null, - "input": null, - "cached_input": null, - "output": null - } - }, - "embeddings_per_MTok": { - "cost": null, - "batch_cost": null - }, - "legacy_models_per_MTok": { - "batch": { - "input": null, - "output": null - }, - "standard": { - "input": null, - "output": null - } - }, - "per_image": null, - "feature_specific": { - "batch_processing": { - "input": null, - "output": null - }, - "long_context": { - "<=200k_input": null, - "<=200k_output": null, - ">200k_input": null, - ">200k_output": null - }, - "tool_use": { - "auto_none": null, - "any_tool": null - } - }, - "tools": { - "code_interpreter_per_container": null, - "file_search_storage_per_GB_per_day": null, - "file_search_call_per_2k_calls": null, - "web_search_per_1k_calls": null, - "web_search_preview_per_1k_calls": null, - "mcp_connector": null, - "bash_in_tokens": null, - "code_execution_per_session_hour": null, - "computer_use_in_tokens": null, - "text_editor_in_tokens": null, - "web_fetch": null, - "livesearch": 26.25 - } - } - } - } -} \ No newline at end of file diff --git a/model_catalog/validate_models.py b/model_catalog/validate_models.py deleted file mode 100644 index 79149ec..0000000 --- a/model_catalog/validate_models.py +++ /dev/null @@ -1,39 +0,0 @@ -import json -import jsonschema -from jsonschema import validate - -# Load schema -with open("model_schema_v1.json", "r") as f: - schema = json.load(f) - -# Load data to validate -with open("programming_models_v1.json", "r") as f: - data = json.load(f) - -# Validate entire file -print("Validating entire file...") -try: - validate(instance=data, schema=schema) - print("✅ Whole JSON is valid against schema!") -except jsonschema.exceptions.ValidationError as e: - print("❌ Whole JSON validation error:", e.message) - print("At path:", list(e.path)) - -# Validate each model separately with $defs included -print("\nValidating individual models...") -model_schema_template = schema["patternProperties"]["^(?!all_models$)[a-zA-Z0-9._-]+$"] -for model_name in data.get("all_models", []): - if model_name not in data: - print(f"⚠️ {model_name} not found as a top-level key") - continue - try: - sub_schema = { - "$schema": schema["$schema"], - **model_schema_template, - "$defs": schema["$defs"] - } - validate(instance=data[model_name], schema=sub_schema) - print(f"✅ {model_name} is valid") - except jsonschema.exceptions.ValidationError as e: - print(f"❌ {model_name} failed validation: {e.message}") - print("At path:", list(e.path)) diff --git a/ui/dashboard/src/app/models/page.tsx b/ui/dashboard/src/app/models/page.tsx deleted file mode 100644 index 3414715..0000000 --- a/ui/dashboard/src/app/models/page.tsx +++ /dev/null @@ -1,421 +0,0 @@ -'use client'; - -import { useMemo, useState, useEffect } from 'react'; -import { ModelCatalogEntry } from '@/lib/api'; -import { useModels } from '@/hooks/useModels'; -import ErrorState from '@/components/ui/ErrorState'; -import EmptyState from '@/components/ui/EmptyState'; -import Link from 'next/link'; -import { getProviderName } from '@/lib/utils'; -import { ModelEndpoint } from '@/lib/modelFilters'; - -const ITEMS_PER_PAGE = 10; -const SEARCH_DEBOUNCE_MS = 400; - -export default function ModelsPage() { - const [search, setSearch] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [provider, setProvider] = useState(''); - const [endpoint, setEndpoint] = useState(''); - const [currentPage, setCurrentPage] = useState(1); - - // Debounce search input - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearch(search); - setCurrentPage(1); // Reset to page 1 when search changes - }, SEARCH_DEBOUNCE_MS); - - return () => clearTimeout(timer); - }, [search]); - - const { items, loading, error, refetch } = useModels(); - - const providers = useMemo(() => { - const set = new Set(); - items.forEach(item => set.add(item.provider)); - return Array.from(set).sort(); - }, [items]); - - const filteredItems = useMemo(() => { - const query = debouncedSearch.trim().toLowerCase(); - return items.filter(item => { - const matchesProvider = provider ? item.provider === provider : true; - const matchesEndpoint = endpoint ? item.endpoint === endpoint : true; - const matchesSearch = query - ? item.id.toLowerCase().includes(query) || - getProviderName(item.provider).toLowerCase().includes(query) - : true; - return matchesProvider && matchesEndpoint && matchesSearch; - }); - }, [items, debouncedSearch, provider, endpoint]); - - // Client-side pagination - const totalPages = Math.max(1, Math.ceil(filteredItems.length / ITEMS_PER_PAGE)); - const safeCurrentPage = Math.min(currentPage, totalPages); - const startIndex = (safeCurrentPage - 1) * ITEMS_PER_PAGE; - const endIndex = startIndex + ITEMS_PER_PAGE; - const paginatedItems = filteredItems.slice(startIndex, endIndex); - - // Ensure current page stays within bounds when filtered results shrink - useEffect(() => { - setCurrentPage(prev => Math.min(prev, totalPages)); - }, [totalPages]); - - // Reset to page 1 when filters change - const handleFilterChange = (setter: (value: T) => void) => (value: T) => { - setter(value); - setCurrentPage(1); - }; - - const copy = (id: string) => { - navigator.clipboard.writeText(id).catch(() => undefined); - }; - - return ( -
- {/* Header */} -
-
-
-
- - - - - Back to Dashboard - -

Model Catalog

-

Browse available models across all providers

-
-
-
-
- - {/* Main Content */} -
- {error ? ( - - ) : ( -
- {/* Filters */} -
-
-

Available Models

-

- {filteredItems.length > 0 ? ( - - Showing {startIndex + 1}-{Math.min(endIndex, filteredItems.length)} of {filteredItems.length} models - {loading && ( - - - - - )} - - ) : loading ? ( - - - - - - Loading... - - ) : ( - `No models found` - )} -

-
-
- setSearch(e.target.value)} - className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 min-w-[200px]" - /> - - -
-
- - {/* Table */} - {filteredItems.length === 0 && !loading ? ( - - ) : loading && filteredItems.length === 0 ? ( - - ) : ( -
- {/* Subtle overlay when loading new results */} - {loading && filteredItems.length > 0 && ( -
- - - - -
- )} - - - - - - - - - - - {paginatedItems.map((item, index) => ( - - ))} - -
ModelProviderEndpointPricing (Input / Output per 1M tokens)
-
- )} - - {/* Pagination */} - {filteredItems.length > ITEMS_PER_PAGE && ( -
-
- Page {currentPage} of{' '} - {totalPages} -
- -
- - -
- {/* Always show page 1 if not in first few pages */} - {currentPage > 4 && totalPages > 7 && ( - <> - - {currentPage > 5 && ( - ... - )} - - )} - - {/* Show pages around current page */} - {(() => { - const pages: number[] = []; - let start = Math.max(1, currentPage - 2); - let end = Math.min(totalPages, currentPage + 2); - - // Adjust if we're near the beginning - if (currentPage <= 4) { - start = 1; - end = Math.min(5, totalPages); - } - // Adjust if we're near the end - else if (currentPage >= totalPages - 3) { - start = Math.max(1, totalPages - 4); - end = totalPages; - } - - for (let i = start; i <= end; i++) { - pages.push(i); - } - - return pages.map((pageNum) => ( - - )); - })()} - - {/* Always show last page if not in last few pages */} - {currentPage < totalPages - 3 && totalPages > 7 && ( - <> - {currentPage < totalPages - 4 && ( - ... - )} - - - )} -
- - -
-
- )} -
- )} -
-
- ); -} - -function ModelTableSkeleton() { - return ( -
- - - - - - - - - - - {[...Array(10)].map((_, i) => ( - - - - - - - ))} - -
ModelProviderEndpointPricing (Input / Output per 1M tokens)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); -} - -function ModelRow({ entry, onCopy, isEven }: { entry: ModelCatalogEntry; onCopy: (id: string) => void; isEven: boolean }) { - const [copied, setCopied] = useState(false); - const [showTooltip, setShowTooltip] = useState(false); - - const priceText = - entry.pricing && typeof entry.pricing.input === 'number' && typeof entry.pricing.output === 'number' - ? `$${entry.pricing.input.toFixed(2)} / $${entry.pricing.output.toFixed(2)}` - : '—'; - - const handleCopy = () => { - onCopy(entry.id); - setCopied(true); - setShowTooltip(true); - setTimeout(() => { - setCopied(false); - setShowTooltip(false); - }, 2000); - }; - - return ( - - -
- {entry.id} -
- - - {/* Tooltip */} - {showTooltip && ( -
-
-
Copy model id:
-
{entry.id}
- {/* Arrow */} -
-
-
-
-
- )} -
-
- - - {getProviderName(entry.provider)} - - - - {entry.endpoint} - - - - {priceText} - - - ); -} diff --git a/ui/dashboard/src/app/page.tsx b/ui/dashboard/src/app/page.tsx index b72f046..23f4318 100644 --- a/ui/dashboard/src/app/page.tsx +++ b/ui/dashboard/src/app/page.tsx @@ -14,7 +14,6 @@ import FirstRunModal from '@/components/FirstRunModal'; import BudgetCard from '@/components/BudgetCard'; import { useBudget } from '@/hooks/useBudget'; import { apiService } from '@/lib/api'; -import Link from 'next/link'; import { MEMORY_PORT } from '@/lib/constants'; export default function Dashboard() { @@ -116,28 +115,6 @@ export default function Dashboard() { onRetry={configStatus.refetch} /> - {/* Model Catalog Link */} -
-
-
-
-

Browse Available Models

-

View the full catalog of models across all providers with pricing information

-
- - View Model Catalog - - - - -
-
-
- {/* Budget Control */}
(''); - const [endpoint, setEndpoint] = useState(''); - - const { items, loading, error, refetch, total } = useModels({ - search: search.trim() || undefined, - provider: provider || undefined, - endpoint: endpoint || undefined - }); - - const providers = useMemo(() => { - const set = new Set(); - items.forEach(item => set.add(item.provider)); - return Array.from(set).sort(); - }, [items]); - - const filteredItems = items; // server-side filters already applied - - if (loading) { - return ; - } - - if (error) { - return ; - } - - if (filteredItems.length === 0) { - return ( - - ); - } - - return ( - -
-
-

Model Catalog

-

Browse available models

-

Total available: {total}

-
-
- setSearch(e.target.value)} - className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-900" - /> - - -
-
- -
- - - - - - - - - - - - {filteredItems.map((item) => ( - - ))} - -
ModelProviderEndpointInput / OutputActions
-
-
- ); -} - -function ModelRow({ entry }: { entry: ModelCatalogEntry }) { - const { copied, copy } = useCopy(); - const priceText = - entry.pricing && typeof entry.pricing.input === 'number' && typeof entry.pricing.output === 'number' - ? `${entry.pricing.input.toFixed(4)} / ${entry.pricing.output.toFixed(4)} ${entry.pricing.currency}` - : '—'; - - return ( - - {entry.id} - {getProviderName(entry.provider)} - - - {entry.endpoint} - - - - {priceText} - - - - - - ); -} diff --git a/ui/dashboard/src/hooks/useModels.ts b/ui/dashboard/src/hooks/useModels.ts deleted file mode 100644 index b5f190d..0000000 --- a/ui/dashboard/src/hooks/useModels.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { useEffect, useState, useRef, useMemo } from 'react'; -import { apiService, ModelCatalogEntry, ModelsResponse } from '@/lib/api'; -import { ModelsFilter, normalizeModelsFilter, filterKey } from '@/lib/modelFilters'; - -const PAGE_SIZE = 500; - -async function fetchAllModels(filter: ModelsFilter) { - const baseParams = { - provider: filter.provider, - endpoint: filter.endpoint, - search: filter.search, - limit: PAGE_SIZE, - } as const; - - const firstPage = await apiService.getModels({ - ...baseParams, - offset: 0, - }); - - const combinedItems: ModelCatalogEntry[] = [...firstPage.items]; - let offset = combinedItems.length; - - while (offset < firstPage.total) { - const nextPage = await apiService.getModels({ - ...baseParams, - offset, - }); - - if (!nextPage.items.length) { - break; - } - - combinedItems.push(...nextPage.items); - offset += nextPage.items.length; - } - - return { - ...firstPage, - items: combinedItems, - limit: combinedItems.length, - offset: 0, - total: Math.max(firstPage.total, combinedItems.length), - }; -} - -export const useModels = (filter: ModelsFilter = {}) => { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const isInitialLoadRef = useRef(true); - const dataRef = useRef(null); - - const normalizedFilter = useMemo(() => normalizeModelsFilter(filter), [filter]); - const normalizedKey = useMemo(() => filterKey(normalizedFilter), [normalizedFilter]); - - useEffect(() => { - const fetchModels = async () => { - const hasExistingData = dataRef.current !== null && dataRef.current.items.length > 0; - const isInitial = isInitialLoadRef.current; - - try { - // Only show full loading state on initial load or when we have no data - // For subsequent searches, keep existing data visible (optimistic updates) - if (isInitial || !hasExistingData) { - setLoading(true); - } - setError(null); - const resp = await fetchAllModels(normalizedFilter); - setData(resp); - dataRef.current = resp; - isInitialLoadRef.current = false; - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch models'); - } finally { - setLoading(false); - } - }; - - fetchModels(); - // eslint-disable-next-line react-hooks/exhaustive-deps -- normalizedKey is a stable string representation of normalizedFilter - }, [normalizedKey]); - - return { - data, - items: data?.items || [], - total: data?.total || 0, - loading, - error, - refetch: async () => { - isInitialLoadRef.current = false; - setLoading(true); - try { - const resp = await fetchAllModels(normalizedFilter); - setData(resp); - dataRef.current = resp; - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch models'); - } finally { - setLoading(false); - } - } - }; -}; diff --git a/ui/dashboard/src/lib/api.ts b/ui/dashboard/src/lib/api.ts index c5b3357..40c8fb6 100644 --- a/ui/dashboard/src/lib/api.ts +++ b/ui/dashboard/src/lib/api.ts @@ -45,28 +45,6 @@ export interface ConfigStatusResponse { }; } -export interface ModelCatalogEntry { - id: string; - provider: string; - endpoint: 'chat_completions' | 'messages' | 'responses'; - pricing: { - input: number; - output: number; - cache_write?: number; - cache_read?: number; - currency: string; - unit: string; - } | null; - source: string; -} - -export interface ModelsResponse { - total: number; - limit: number; - offset: number; - items: ModelCatalogEntry[]; -} - export interface BudgetResponse { amountUsd: number | null; alertOnly: boolean; @@ -170,22 +148,6 @@ export const apiService = { return response.json(); }, - async getModels(params?: { provider?: string; endpoint?: 'chat_completions' | 'messages' | 'responses'; search?: string; limit?: number; offset?: number }): Promise { - const searchParams = new URLSearchParams(); - if (params?.provider) searchParams.append('provider', params.provider); - if (params?.endpoint) searchParams.append('endpoint', params.endpoint); - if (params?.search) searchParams.append('search', params.search); - if (params?.limit) searchParams.append('limit', String(params.limit)); - if (params?.offset) searchParams.append('offset', String(params.offset)); - - const url = `${API_BASE_URL}/v1/models${searchParams.toString() ? `?${searchParams.toString()}` : ''}`; - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch models: ${response.statusText}`); - } - return response.json(); - }, - async getBudget(): Promise { const response = await fetch(`${API_BASE_URL}/budget`); if (!response.ok) { diff --git a/ui/dashboard/src/lib/modelFilters.ts b/ui/dashboard/src/lib/modelFilters.ts deleted file mode 100644 index d42cd4b..0000000 --- a/ui/dashboard/src/lib/modelFilters.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type ModelEndpoint = 'chat_completions' | 'messages' | 'responses'; - -export interface ModelsFilter { - provider?: string; - endpoint?: ModelEndpoint; - search?: string; -} - -export function normalizeModelsFilter(filter: ModelsFilter = {}): ModelsFilter { - return { - provider: filter.provider || undefined, - endpoint: filter.endpoint || undefined, - search: filter.search?.trim() || undefined, - }; -} - -export function filterKey(filter: ModelsFilter): string { - return `${filter.provider || ''}|${filter.endpoint || ''}|${filter.search || ''}`; -} From 302313a59c36b1e16676d2c449f7c6c1702561e9 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:39:13 +0530 Subject: [PATCH 41/78] =?UTF-8?q?Remove=20gateway=20service=20=E2=80=94=20?= =?UTF-8?q?route=20everything=20through=20integrations/openrouter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 31 +- docker-compose.yaml | 14 +- gateway/package-lock.json | 5821 ----------------- gateway/package.json | 56 - gateway/src/app/handlers/budget-handler.ts | 62 - gateway/src/app/handlers/chat-handler.ts | 256 - gateway/src/app/handlers/config-handler.ts | 32 - gateway/src/app/handlers/usage-handler.ts | 81 - gateway/src/canonical/types/index.ts | 3 - gateway/src/canonical/types/request.types.ts | 238 - gateway/src/canonical/types/response.types.ts | 188 - .../types/streaming-response.types.ts | 187 - gateway/src/costs/README.md | 84 - gateway/src/costs/anthropic.yaml | 89 - gateway/src/costs/google.yaml | 25 - gateway/src/costs/index.ts | 9 - gateway/src/costs/ollama.yaml | 59 - gateway/src/costs/openai.yaml | 184 - gateway/src/costs/openrouter.yaml | 3399 ---------- gateway/src/costs/templates/base.yaml | 24 - gateway/src/costs/xAI.yaml | 41 - gateway/src/costs/zai.yaml | 57 - .../domain/providers/anthropic-provider.ts | 250 - gateway/src/domain/providers/base-provider.ts | 124 - .../src/domain/providers/google-provider.ts | 205 - .../src/domain/providers/ollama-provider.ts | 95 - .../src/domain/providers/openai-provider.ts | 193 - .../domain/providers/openrouter-provider.ts | 139 - gateway/src/domain/providers/xai-provider.ts | 96 - gateway/src/domain/providers/zai-provider.ts | 30 - gateway/src/domain/services/budget-service.ts | 87 - .../src/domain/services/provider-registry.ts | 92 - .../src/domain/services/provider-service.ts | 125 - gateway/src/domain/types/provider.ts | 35 - gateway/src/index.ts | 139 - .../adapters/anthropic-adapter.ts | 139 - .../infrastructure/adapters/openai-adapter.ts | 118 - .../adapters/openai-responses-adapter.ts | 109 - gateway/src/infrastructure/config.ts | 32 - .../src/infrastructure/config/app-config.ts | 149 - gateway/src/infrastructure/db/connection.ts | 89 - gateway/src/infrastructure/db/index.ts | 7 - gateway/src/infrastructure/db/queries.ts | 192 - gateway/src/infrastructure/db/schema.sql | 40 - .../middleware/error-handler.ts | 58 - .../middleware/request-context.ts | 26 - .../middleware/request-logging.ts | 40 - .../chat-completions-passthrough-registry.ts | 81 - .../chat-completions-passthrough.ts | 436 -- .../chat-completions-provider-config.ts | 153 - .../messages-passthrough-registry.ts | 70 - .../passthrough/messages-passthrough.ts | 546 -- .../passthrough/messages-provider-config.ts | 167 - .../ollama-responses-passthrough.ts | 215 - .../openai-responses-passthrough.ts | 230 - .../responses-passthrough-registry.ts | 65 - .../passthrough/responses-passthrough.ts | 20 - .../passthrough/responses-provider-config.ts | 93 - gateway/src/infrastructure/payments/README.md | 68 - .../infrastructure/payments/x402/client.ts | 119 - .../src/infrastructure/payments/x402/index.ts | 14 - .../infrastructure/payments/x402/wallet.ts | 57 - .../utils/canonical-validator.ts | 117 - .../src/infrastructure/utils/error-handler.ts | 82 - gateway/src/infrastructure/utils/logger.ts | 74 - .../src/infrastructure/utils/model-utils.ts | 90 - .../infrastructure/utils/pricing-loader.ts | 499 -- .../src/infrastructure/utils/usage-tracker.ts | 364 -- .../src/infrastructure/utils/validation.ts | 93 - gateway/src/schemas/request.schema.json | 634 -- gateway/src/schemas/response.schema.json | 371 -- .../schemas/streaming-response.schema.json | 386 -- gateway/src/shared/errors/gateway-errors.ts | 183 - gateway/src/shared/errors/index.ts | 2 - gateway/tests/fixtures/usage-records.json | 53 - .../tests/integration/usage-endpoint.test.ts | 344 - gateway/tests/setup.ts | 52 - .../unit/app/handlers/usage-handler.test.ts | 374 -- .../unit/infrastructure/db/queries.test.ts | 497 -- gateway/tests/utils/database-helpers.ts | 143 - gateway/tests/utils/mock-factories.ts | 243 - gateway/tests/utils/request-helpers.ts | 143 - gateway/tests/utils/test-helpers.ts | 163 - gateway/tsconfig.json | 25 - gateway/vitest.config.ts | 76 - .../chat_completions_providers_v1.json | 199 - model_catalog/messages_providers_v1.json | 93 - model_catalog/responses_providers_v1.json | 50 - package.json | 9 +- scripts/launcher.js | 8 - scripts/start-docker-fullstack.sh | 11 +- 91 files changed, 8 insertions(+), 21253 deletions(-) delete mode 100644 gateway/package-lock.json delete mode 100644 gateway/package.json delete mode 100644 gateway/src/app/handlers/budget-handler.ts delete mode 100644 gateway/src/app/handlers/chat-handler.ts delete mode 100644 gateway/src/app/handlers/config-handler.ts delete mode 100644 gateway/src/app/handlers/usage-handler.ts delete mode 100644 gateway/src/canonical/types/index.ts delete mode 100644 gateway/src/canonical/types/request.types.ts delete mode 100644 gateway/src/canonical/types/response.types.ts delete mode 100644 gateway/src/canonical/types/streaming-response.types.ts delete mode 100644 gateway/src/costs/README.md delete mode 100644 gateway/src/costs/anthropic.yaml delete mode 100644 gateway/src/costs/google.yaml delete mode 100644 gateway/src/costs/index.ts delete mode 100644 gateway/src/costs/ollama.yaml delete mode 100644 gateway/src/costs/openai.yaml delete mode 100644 gateway/src/costs/openrouter.yaml delete mode 100644 gateway/src/costs/templates/base.yaml delete mode 100644 gateway/src/costs/xAI.yaml delete mode 100644 gateway/src/costs/zai.yaml delete mode 100644 gateway/src/domain/providers/anthropic-provider.ts delete mode 100644 gateway/src/domain/providers/base-provider.ts delete mode 100644 gateway/src/domain/providers/google-provider.ts delete mode 100644 gateway/src/domain/providers/ollama-provider.ts delete mode 100644 gateway/src/domain/providers/openai-provider.ts delete mode 100644 gateway/src/domain/providers/openrouter-provider.ts delete mode 100644 gateway/src/domain/providers/xai-provider.ts delete mode 100644 gateway/src/domain/providers/zai-provider.ts delete mode 100644 gateway/src/domain/services/budget-service.ts delete mode 100644 gateway/src/domain/services/provider-registry.ts delete mode 100644 gateway/src/domain/services/provider-service.ts delete mode 100644 gateway/src/domain/types/provider.ts delete mode 100644 gateway/src/index.ts delete mode 100644 gateway/src/infrastructure/adapters/anthropic-adapter.ts delete mode 100644 gateway/src/infrastructure/adapters/openai-adapter.ts delete mode 100644 gateway/src/infrastructure/adapters/openai-responses-adapter.ts delete mode 100644 gateway/src/infrastructure/config.ts delete mode 100644 gateway/src/infrastructure/config/app-config.ts delete mode 100644 gateway/src/infrastructure/db/connection.ts delete mode 100644 gateway/src/infrastructure/db/index.ts delete mode 100644 gateway/src/infrastructure/db/queries.ts delete mode 100644 gateway/src/infrastructure/db/schema.sql delete mode 100644 gateway/src/infrastructure/middleware/error-handler.ts delete mode 100644 gateway/src/infrastructure/middleware/request-context.ts delete mode 100644 gateway/src/infrastructure/middleware/request-logging.ts delete mode 100644 gateway/src/infrastructure/passthrough/chat-completions-passthrough-registry.ts delete mode 100644 gateway/src/infrastructure/passthrough/chat-completions-passthrough.ts delete mode 100644 gateway/src/infrastructure/passthrough/chat-completions-provider-config.ts delete mode 100644 gateway/src/infrastructure/passthrough/messages-passthrough-registry.ts delete mode 100644 gateway/src/infrastructure/passthrough/messages-passthrough.ts delete mode 100644 gateway/src/infrastructure/passthrough/messages-provider-config.ts delete mode 100644 gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts delete mode 100644 gateway/src/infrastructure/passthrough/openai-responses-passthrough.ts delete mode 100644 gateway/src/infrastructure/passthrough/responses-passthrough-registry.ts delete mode 100644 gateway/src/infrastructure/passthrough/responses-passthrough.ts delete mode 100644 gateway/src/infrastructure/passthrough/responses-provider-config.ts delete mode 100644 gateway/src/infrastructure/payments/README.md delete mode 100644 gateway/src/infrastructure/payments/x402/client.ts delete mode 100644 gateway/src/infrastructure/payments/x402/index.ts delete mode 100644 gateway/src/infrastructure/payments/x402/wallet.ts delete mode 100644 gateway/src/infrastructure/utils/canonical-validator.ts delete mode 100644 gateway/src/infrastructure/utils/error-handler.ts delete mode 100644 gateway/src/infrastructure/utils/logger.ts delete mode 100644 gateway/src/infrastructure/utils/model-utils.ts delete mode 100644 gateway/src/infrastructure/utils/pricing-loader.ts delete mode 100644 gateway/src/infrastructure/utils/usage-tracker.ts delete mode 100644 gateway/src/infrastructure/utils/validation.ts delete mode 100644 gateway/src/schemas/request.schema.json delete mode 100644 gateway/src/schemas/response.schema.json delete mode 100644 gateway/src/schemas/streaming-response.schema.json delete mode 100644 gateway/src/shared/errors/gateway-errors.ts delete mode 100644 gateway/src/shared/errors/index.ts delete mode 100644 gateway/tests/fixtures/usage-records.json delete mode 100644 gateway/tests/integration/usage-endpoint.test.ts delete mode 100644 gateway/tests/setup.ts delete mode 100644 gateway/tests/unit/app/handlers/usage-handler.test.ts delete mode 100644 gateway/tests/unit/infrastructure/db/queries.test.ts delete mode 100644 gateway/tests/utils/database-helpers.ts delete mode 100644 gateway/tests/utils/mock-factories.ts delete mode 100644 gateway/tests/utils/request-helpers.ts delete mode 100644 gateway/tests/utils/test-helpers.ts delete mode 100644 gateway/tsconfig.json delete mode 100644 gateway/vitest.config.ts delete mode 100644 model_catalog/chat_completions_providers_v1.json delete mode 100644 model_catalog/messages_providers_v1.json delete mode 100644 model_catalog/responses_providers_v1.json diff --git a/Dockerfile b/Dockerfile index 24afde6..12a8a0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,6 @@ WORKDIR /app COPY package.json package-lock.json ./ # Copy per-workspace manifests (lock files may not exist for all) -COPY gateway/package.json gateway/package-lock.json* ./gateway/ COPY ui/dashboard/package.json ui/dashboard/package-lock.json* ./ui/dashboard/ COPY memory/package.json ./memory/ COPY integrations/openrouter/package.json ./integrations/openrouter/ @@ -17,11 +16,6 @@ RUN npm install --workspaces --include-workspace-root # Copy the rest of the source COPY . . -# ---------- gateway build ---------- -FROM build-base AS gateway-build -WORKDIR /app/gateway -RUN npm run build - # ---------- dashboard build ---------- FROM build-base AS dashboard-build WORKDIR /app/ui/dashboard @@ -48,18 +42,6 @@ ENV NEXT_BUILD_MODE=embedded ENV NEXT_PUBLIC_EMBEDDED_MODE=true RUN npm run build:embedded -# ---------- gateway runtime ---------- -FROM node:20-alpine AS gateway-runtime -WORKDIR /app/gateway -ENV NODE_ENV=production -COPY gateway/package.json ./ -RUN npm install --omit=dev -COPY --from=gateway-build /app/gateway/dist ./dist -COPY --from=gateway-build /app/model_catalog ./dist/model_catalog -RUN mkdir -p /app/gateway/data /app/gateway/logs -EXPOSE 3001 -CMD ["node", "dist/gateway/src/index.js"] - # ---------- dashboard runtime ---------- FROM node:20-alpine AS dashboard-runtime WORKDIR /app/ui/dashboard @@ -94,19 +76,12 @@ WORKDIR /app/integrations/openrouter EXPOSE 4010 CMD ["node", "dist/server.js"] -# ---------- fullstack runtime ---------- +# ---------- fullstack runtime (dashboard + openrouter + memory) ---------- FROM node:20-alpine AS ekai-gateway-runtime WORKDIR /app RUN apk add --no-cache bash -# Gateway -COPY gateway/package.json ./gateway/ -RUN cd gateway && npm install --omit=dev -COPY --from=gateway-build /app/gateway/dist ./gateway/dist -COPY --from=gateway-build /app/model_catalog ./model_catalog -RUN mkdir -p /app/gateway/data /app/gateway/logs - # Dashboard COPY ui/dashboard/package.json ./ui/dashboard/ RUN cd ui/dashboard && npm install --omit=dev @@ -133,8 +108,8 @@ RUN chmod +x /app/start-docker-fullstack.sh ENV NODE_ENV=production -EXPOSE 3001 3000 4010 -VOLUME ["/app/gateway/data", "/app/gateway/logs", "/app/memory/data"] +EXPOSE 3000 4010 +VOLUME ["/app/memory/data"] CMD ["/app/start-docker-fullstack.sh"] # ---------- openrouter + dashboard Cloud Run runtime (single container) ---------- diff --git a/docker-compose.yaml b/docker-compose.yaml index 23d3d44..4f65805 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,28 +3,18 @@ services: build: . image: ghcr.io/ekailabs/ekai-gateway:latest environment: - PORT: ${PORT:-3001} UI_PORT: ${UI_PORT:-3000} OPENROUTER_PORT: ${OPENROUTER_PORT:-4010} - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3001} + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:4010} NEXT_PUBLIC_MEMORY_PORT: ${OPENROUTER_PORT:-4010} - DATABASE_PATH: /app/gateway/data/proxy.db MEMORY_DB_PATH: /app/memory/data/memory.db - OPENAI_API_KEY: ${OPENAI_API_KEY:-} - ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} - XAI_API_KEY: ${XAI_API_KEY:-} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} - GOOGLE_API_KEY: ${GOOGLE_API_KEY:-} - ENABLE_GATEWAY: ${ENABLE_GATEWAY:-true} ENABLE_DASHBOARD: ${ENABLE_DASHBOARD:-true} ENABLE_OPENROUTER: ${ENABLE_OPENROUTER:-true} ports: - - "3001:3001" - "3000:3000" - "4010:4010" volumes: - - gateway_logs:/app/gateway/logs - - gateway_db:/app/gateway/data - memory_db:/app/memory/data restart: unless-stopped @@ -46,6 +36,4 @@ services: restart: unless-stopped volumes: - gateway_logs: - gateway_db: memory_db: diff --git a/gateway/package-lock.json b/gateway/package-lock.json deleted file mode 100644 index 810a252..0000000 --- a/gateway/package-lock.json +++ /dev/null @@ -1,5821 +0,0 @@ -{ - "name": "gateway", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "gateway", - "version": "0.1.0", - "dependencies": { - "@types/js-yaml": "^4.0.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "better-sqlite3": "^12.2.0", - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "js-yaml": "^4.1.0", - "json-schema-to-typescript": "^15.0.4", - "node-fetch": "^3.3.2", - "pino": "^9.4.0", - "pino-http-send": "^0.4.2" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.13", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/node": "^20.10.0", - "@types/supertest": "^2.0.16", - "@vitest/coverage-v8": "^1.0.4", - "@vitest/ui": "^1.0.4", - "supertest": "^6.3.3", - "tsx": "^4.6.0", - "typescript": "^5.3.2", - "vitest": "^1.0.4" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", - "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", - "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", - "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", - "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", - "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", - "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", - "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", - "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", - "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", - "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", - "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", - "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", - "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", - "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", - "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", - "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", - "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", - "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", - "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", - "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", - "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", - "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", - "license": "MIT" - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", - "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/supertest": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", - "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/superagent": "*" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", - "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "1.6.1" - } - }, - "node_modules/@vitest/coverage-v8/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@vitest/coverage-v8/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.6.1.tgz", - "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "fast-glob": "^3.3.2", - "fflate": "^0.8.1", - "flatted": "^3.2.9", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "sirv": "^2.0.4" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "1.6.1" - } - }, - "node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz", - "integrity": "sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clone-response/node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/formidable": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", - "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, - "node_modules/json-schema-to-typescript": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", - "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.5.5", - "@types/json-schema": "^7.0.15", - "@types/lodash": "^4.17.7", - "is-glob": "^4.0.3", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "prettier": "^3.2.5", - "tinyglobby": "^0.2.9" - }, - "bin": { - "json2ts": "dist/src/cli.js" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.77.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", - "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pino": { - "version": "9.9.5", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.9.5.tgz", - "integrity": "sha512-d1s98p8/4TfYhsJ09r/Azt30aYELRi6NNnZtEbqFw6BoGsdPVf5lKNK3kUwH8BmJJfpTLNuicjUQjaMbd93dVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-http-send": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/pino-http-send/-/pino-http-send-0.4.2.tgz", - "integrity": "sha512-g60bQOPDYVnrpifubVNrGYti/Aj1Nv7ADfNbizp/vR+xOi6TfXxn9Ncg5ziN/SmZJ3KdE5o1XAS+DyW5d4RDWg==", - "license": "ISC", - "dependencies": { - "chalk": "^4.1.0", - "got": "^11.5.2", - "pump": "^3.0.0", - "pumpify": "^2.0.1", - "split2": "^3.2.2", - "through2": "^4.0.2", - "typescript": "^4.0.2", - "yargs": "^15.4.1" - }, - "bin": { - "pino-http-send": "dist/bin.js" - } - }, - "node_modules/pino-http-send/node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "license": "ISC", - "dependencies": { - "readable-stream": "^3.0.0" - } - }, - "node_modules/pino-http-send/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "license": "MIT" - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", - "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.1", - "@rollup/rollup-android-arm64": "4.50.1", - "@rollup/rollup-darwin-arm64": "4.50.1", - "@rollup/rollup-darwin-x64": "4.50.1", - "@rollup/rollup-freebsd-arm64": "4.50.1", - "@rollup/rollup-freebsd-x64": "4.50.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", - "@rollup/rollup-linux-arm-musleabihf": "4.50.1", - "@rollup/rollup-linux-arm64-gnu": "4.50.1", - "@rollup/rollup-linux-arm64-musl": "4.50.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", - "@rollup/rollup-linux-ppc64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-musl": "4.50.1", - "@rollup/rollup-linux-s390x-gnu": "4.50.1", - "@rollup/rollup-linux-x64-gnu": "4.50.1", - "@rollup/rollup-linux-x64-musl": "4.50.1", - "@rollup/rollup-openharmony-arm64": "4.50.1", - "@rollup/rollup-win32-arm64-msvc": "4.50.1", - "@rollup/rollup-win32-ia32-msvc": "4.50.1", - "@rollup/rollup-win32-x64-msvc": "4.50.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true, - "license": "MIT" - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/superagent": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", - "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", - "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/superagent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/supertest": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", - "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", - "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.1.2" - }, - "engines": { - "node": ">=6.4.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "license": "MIT", - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "5.4.20", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", - "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/vite-node/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/gateway/package.json b/gateway/package.json deleted file mode 100644 index 28b7bbe..0000000 --- a/gateway/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "gateway", - "version": "0.1.0-beta.1", - "description": "Ekai Gateway", - "type": "module", - "private": true, - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc && npm run copy:assets", - "type-check": "tsc --noEmit", - "copy:assets": "copyfiles -u 1 ../model_catalog/*.json dist/ && copyfiles -u 3 src/infrastructure/db/schema.sql dist/gateway/src/infrastructure/db/ && copyfiles -u 2 src/costs/*.yaml dist/gateway/src/costs/", - "start": "node dist/gateway/src/index.js", - "clean": "rm -rf dist", - "test": "vitest", - "test:ui": "vitest --ui", - "test:run": "vitest run", - "test:watch": "vitest --watch", - "test:coverage": "vitest --coverage", - "test:ci": "vitest run --coverage --reporter=verbose" - }, - "dependencies": { - "@types/js-yaml": "^4.0.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "better-sqlite3": "^12.2.0", - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "js-yaml": "^4.1.0", - "json-schema-to-typescript": "^15.0.4", - "node-fetch": "^3.3.2", - "pino": "^9.4.0", - "viem": "^2.0.0", - "x402-fetch": "^0.3.0" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.13", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/node": "^20.10.0", - "@types/supertest": "^2.0.16", - "@vitest/coverage-v8": "^1.0.4", - "@vitest/ui": "^1.0.4", - "copyfiles": "^2.4.1", - "supertest": "^6.3.3", - "tsx": "^4.6.0", - "typescript": "^5.3.2", - "vitest": "^1.0.4" - }, - "keywords": [ - "ai", - "openai", - "proxy" - ], - "author": "Ekai Labs" -} diff --git a/gateway/src/app/handlers/budget-handler.ts b/gateway/src/app/handlers/budget-handler.ts deleted file mode 100644 index cd3d343..0000000 --- a/gateway/src/app/handlers/budget-handler.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Request, Response } from 'express'; -import { budgetService } from '../../domain/services/budget-service.js'; -import { handleError } from '../../infrastructure/utils/error-handler.js'; -import { logger } from '../../infrastructure/utils/logger.js'; - -const serialize = (status: ReturnType) => ({ - amountUsd: status.limit, - alertOnly: status.alertOnly, - window: status.window, - spentMonthToDate: status.spent, - remaining: status.remaining -}); - -export class BudgetHandler { - getBudget(req: Request, res: Response): void { - try { - const status = budgetService.getBudgetStatus(); - res.json(serialize(status)); - } catch (error) { - handleError(error, res); - } - } - - updateBudget(req: Request, res: Response): void { - try { - const { amountUsd, alertOnly } = req.body ?? {}; - - if (amountUsd !== undefined && amountUsd !== null) { - if (typeof amountUsd !== 'number' || Number.isNaN(amountUsd) || amountUsd < 0) { - res.status(400).json({ error: 'amountUsd must be a non-negative number or null' }); - return; - } - } - - const parsedAmount: number | null = amountUsd === undefined ? null : amountUsd; - const parsedAlertOnly = alertOnly === undefined ? false : Boolean(alertOnly); - - const status = budgetService.upsertBudget(parsedAmount, parsedAlertOnly); - - logger.info('Budget updated via API', { - requestId: req.requestId, - amountUsd: parsedAmount, - alertOnly: parsedAlertOnly, - module: 'budget-handler' - }); - - res.json(serialize(status)); - } catch (error) { - handleError(error, res); - } - } -} - -const budgetHandler = new BudgetHandler(); - -export const handleGetBudget = (req: Request, res: Response): void => { - budgetHandler.getBudget(req, res); -}; - -export const handleUpdateBudget = (req: Request, res: Response): void => { - budgetHandler.updateBudget(req, res); -}; diff --git a/gateway/src/app/handlers/chat-handler.ts b/gateway/src/app/handlers/chat-handler.ts deleted file mode 100644 index 69e3291..0000000 --- a/gateway/src/app/handlers/chat-handler.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { Request, Response } from 'express'; -import { ProviderService } from '../../domain/services/provider-service.js'; -import { OpenAIAdapter } from '../../infrastructure/adapters/openai-adapter.js'; -import { OpenAIResponsesAdapter } from '../../infrastructure/adapters/openai-responses-adapter.js'; -import { AnthropicAdapter } from '../../infrastructure/adapters/anthropic-adapter.js'; -import { handleError } from '../../infrastructure/utils/error-handler.js'; -import { ValidationError, ConfigurationError } from '../../shared/errors/index.js'; -import { logger } from '../../infrastructure/utils/logger.js'; -import { HTTP_STATUS, CONTENT_TYPES } from '../../domain/types/provider.js'; -import { ModelUtils } from '../../infrastructure/utils/model-utils.js'; -import { CanonicalRequest } from 'shared/types/index.js'; -import { createChatCompletionsPassthroughRegistry } from '../../infrastructure/passthrough/chat-completions-passthrough-registry.js'; -import { createMessagesPassthroughRegistry } from '../../infrastructure/passthrough/messages-passthrough-registry.js'; -import { createResponsesPassthroughRegistry } from '../../infrastructure/passthrough/responses-passthrough-registry.js'; -import { budgetService } from '../../domain/services/budget-service.js'; - -type ClientFormat = 'openai' | 'anthropic' | 'openai_responses'; -type ProviderName = string; - -interface StreamingHeaders { - 'Content-Type': string; - 'Cache-Control': string; - 'Connection': string; - 'Access-Control-Allow-Origin': string; -} - -const chatCompletionsPassthroughRegistry = createChatCompletionsPassthroughRegistry(); -const messagesPassthroughRegistry = createMessagesPassthroughRegistry(); -const responsesPassthroughRegistry = createResponsesPassthroughRegistry(); - -export class ChatHandler { - constructor( - private readonly providerService: ProviderService, - private readonly adapters: { - openai: OpenAIAdapter; - anthropic: AnthropicAdapter; - openai_responses: OpenAIResponsesAdapter; - } - ) {} - - async handleChatRequest(req: Request, res: Response, clientFormat: ClientFormat): Promise { - try { - logger.debug('Processing chat request', { requestId: req.requestId, module: 'chat-handler' }); - const originalRequest = req.body; - - // For OpenAI responses, we need to determine if we should use passthrough - // This requires provider selection logic - let providerName: ProviderName; - let canonicalRequest: CanonicalRequest; - - if (clientFormat === 'openai_responses') { - // For responses, we always want to use OpenAI provider (responses API is OpenAI-specific) - // But we still need to check if passthrough should be used - providerName = 'openai'; - canonicalRequest = this.adapters[clientFormat].toCanonical(req.body); - - logger.debug('Processing OpenAI Responses request', { - requestId: req.requestId, - clientFormat, - model: canonicalRequest.model, - streaming: canonicalRequest.stream, - provider: providerName, - module: 'chat-handler' - }); - } else { - // Normal flow for other client formats - const result = this.providerService.getMostOptimalProvider(req.body.model, req.requestId); - if (result.error) { - if (result.error.code === 'NO_PROVIDERS_CONFIGURED') { - throw new ConfigurationError(result.error.message, { code: result.error.code }); - } - throw new ValidationError(result.error.message, { code: result.error.code, model: req.body.model }); - } - - providerName = result.provider; - canonicalRequest = this.adapters[clientFormat].toCanonical(req.body); - } - - // Enforce global monthly budget (disabled when no cap is set) - budgetService.enforceBudget(0, req.requestId); - - // Normalize model name, example: anthropic/claude-3-5-sonnet → claude-3-5-sonnet. - // will need to move it to normalization canonical step in future - if (req.body.model.includes(providerName)) { - req.body.model = ModelUtils.removeProviderPrefix(req.body.model); - } - - // Pass-through scenarios: where clientFormat and providerFormat are the same, we want to take a quick route - // Currently supporting claude code proxy through pass-through, i.e., we skip the canonicalization step - const passThrough = this.shouldUsePassThrough(clientFormat, providerName); - - logger.info('Chat request received', { - requestId: req.requestId, - model: req.body.model, - provider: providerName, - streaming: req.body.stream, - passThrough, - module: 'chat-handler' - }); - - if (passThrough) { - - await this.handlePassThrough(originalRequest, res, clientFormat, providerName); - return; - } - - // For non-passthrough cases, ensure we have canonical request - if (clientFormat !== 'openai_responses') { - canonicalRequest = this.adapters[clientFormat].toCanonical(req.body); - } - - if (canonicalRequest.stream) { - await this.handleStreaming(canonicalRequest, res, clientFormat, providerName, originalRequest, req); - } else { - await this.handleNonStreaming(canonicalRequest, res, clientFormat, providerName, originalRequest, req); - } - } catch (error) { - logger.error('Chat request failed', error, { requestId: req.requestId, module: 'chat-handler' }); - const errorFormat = clientFormat === 'openai_responses' ? 'openai' : clientFormat; - handleError(error, res, errorFormat); - } - } - - - private async handleNonStreaming(canonicalRequest: CanonicalRequest, res: Response, clientFormat: ClientFormat, providerName?: ProviderName, originalRequest?: any, req?: Request): Promise { - if (clientFormat === 'openai_responses') { - const canonicalResponse = await this.providerService.processChatCompletion(canonicalRequest, 'openai' as any, 'openai', originalRequest, req.requestId); - const clientResponse = this.adapters[clientFormat].fromCanonical(canonicalResponse); - res.status(HTTP_STATUS.OK).json(clientResponse); - return; - } - - const canonicalResponse = await this.providerService.processChatCompletion(canonicalRequest, providerName as any, clientFormat, originalRequest, req.requestId); - const clientResponse = this.adapters[clientFormat].fromCanonical(canonicalResponse); - - res.status(HTTP_STATUS.OK).json(clientResponse); - } - - private async handleStreaming(canonicalRequest: CanonicalRequest, res: Response, clientFormat: ClientFormat, providerName?: ProviderName, originalRequest?: any, req?: Request): Promise { - if (clientFormat === 'openai_responses') { - const streamResponse = await this.providerService.processStreamingRequest(canonicalRequest, 'openai' as any, 'openai', originalRequest, req.requestId); - - res.writeHead(HTTP_STATUS.OK, { - 'Content-Type': CONTENT_TYPES.EVENT_STREAM, - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - }); - - if (streamResponse?.body) { - streamResponse.body.pipe(res); - } else { - throw new Error('No stream body received from provider'); - } - return; - } - - const streamResponse = await this.providerService.processStreamingRequest(canonicalRequest, providerName as any, clientFormat, originalRequest, req.requestId); - - this.setStreamingHeaders(res); - - if (streamResponse?.body) { - streamResponse.body.pipe(res); - } else { - throw new Error('No stream body received from provider'); - } - } - - // Pass-through scenarios: where clientFormat and providerFormat are the same, we want to take a quick route - // Currently supporting native provider formats via chat completions/messages passthroughs - private shouldUsePassThrough(clientFormat: ClientFormat, providerName: ProviderName): boolean { - const responsesFormats = responsesPassthroughRegistry.getSupportedClientFormats(providerName); - if (responsesFormats.includes(clientFormat)) { - return true; - } - - const chatConfig = chatCompletionsPassthroughRegistry.getConfig(providerName); - if (chatConfig?.supportedClientFormats.includes(clientFormat)) { - return true; - } - - const messagesConfig = messagesPassthroughRegistry.getConfig(providerName); - return Boolean(messagesConfig?.supportedClientFormats.includes(clientFormat)); - } - - private setStreamingHeaders(res: Response): void { - const headers = { - 'Content-Type': CONTENT_TYPES.TEXT_PLAIN, - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - }; - res.writeHead(HTTP_STATUS.OK, headers); - } - - private async handlePassThrough(originalRequest: any, res: Response, clientFormat: ClientFormat, providerName: ProviderName): Promise { - const responsesPassthrough = responsesPassthroughRegistry.getPassthrough(providerName); - const responsesSupportsFormat = responsesPassthroughRegistry - .getSupportedClientFormats(providerName) - .includes(clientFormat); - - if (responsesPassthrough && responsesSupportsFormat) { - await responsesPassthrough.handleDirectRequest(originalRequest, res); - return; - } - - const chatPassthrough = chatCompletionsPassthroughRegistry.getPassthrough(providerName); - const chatSupportsFormat = chatCompletionsPassthroughRegistry - .getSupportedClientFormats(providerName) - .includes(clientFormat); - - if (chatPassthrough && chatSupportsFormat) { - await chatPassthrough.handleDirectRequest(originalRequest, res); - return; - } - - const passthrough = messagesPassthroughRegistry.getPassthrough(providerName); - const supportsFormat = messagesPassthroughRegistry - .getSupportedClientFormats(providerName) - .includes(clientFormat); - - if (!passthrough || !supportsFormat) { - throw new ConfigurationError(`Passthrough not configured for provider ${providerName}`, { provider: providerName, format: clientFormat }); - } - - await passthrough.handleDirectRequest(originalRequest, res); - } - -} - -// Factory function -export function createChatHandler(): ChatHandler { - const providerService = new ProviderService(); - const adapters = { - openai: new OpenAIAdapter(), - anthropic: new AnthropicAdapter(), - openai_responses: new OpenAIResponsesAdapter() - }; - - return new ChatHandler(providerService, adapters); -} -// Singleton instance -const chatHandler = createChatHandler(); - -// Endpoint functions -export const handleOpenAIFormatChat = async (req: Request, res: Response): Promise => { - await chatHandler.handleChatRequest(req, res, 'openai'); -}; - -export const handleAnthropicFormatChat = async (req: Request, res: Response): Promise => { - await chatHandler.handleChatRequest(req, res, 'anthropic'); -}; - -export const handleOpenAIResponses = async (req: Request, res: Response): Promise => { - await chatHandler.handleChatRequest(req, res, 'openai_responses'); -}; diff --git a/gateway/src/app/handlers/config-handler.ts b/gateway/src/app/handlers/config-handler.ts deleted file mode 100644 index f9c5c26..0000000 --- a/gateway/src/app/handlers/config-handler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Request, Response } from 'express'; -import { getConfig } from '../../infrastructure/config/app-config.js'; -import { logger } from '../../infrastructure/utils/logger.js'; - -export const handleConfigStatus = (req: Request, res: Response): void => { - try { - const config = getConfig(); - - const providers = { - anthropic: config.providers.anthropic.enabled, - openai: config.providers.openai.enabled, - openrouter: config.providers.openrouter.enabled, - xai: config.providers.xai.enabled, - zai: config.providers.zai.enabled, - google: config.providers.google.enabled, - }; - - res.json({ - providers, - mode: config.getMode(), - hasApiKeys: Object.values(providers).some(Boolean), - x402Enabled: config.x402.enabled, - server: { - environment: config.server.environment, - port: config.server.port - } - }); - } catch (error) { - logger.error('Failed to fetch config status', error, { requestId: req.requestId, module: 'config-handler' }); - res.status(500).json({ error: 'Failed to fetch config status' }); - } -}; diff --git a/gateway/src/app/handlers/usage-handler.ts b/gateway/src/app/handlers/usage-handler.ts deleted file mode 100644 index 7091f7a..0000000 --- a/gateway/src/app/handlers/usage-handler.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Request, Response } from 'express'; -import { usageTracker } from '../../infrastructure/utils/usage-tracker.js'; -import { handleError } from '../../infrastructure/utils/error-handler.js'; -import { logger } from '../../infrastructure/utils/logger.js'; - -export class UsageHandler { - async getUsage(req: Request, res: Response): Promise { - try { - // Parse and validate query parameters - const { startTime, endTime, timezone, format } = req.query; - - - // Default to last 7 days if no startTime provided - const defaultStartTime = new Date(); - defaultStartTime.setDate(defaultStartTime.getDate() - 7); - - const start = startTime ? new Date(String(startTime)) : defaultStartTime; - const end = endTime ? new Date(String(endTime)) : new Date(); - const tz = String(timezone || 'UTC'); - - // Validate dates - if (isNaN(start.getTime())) { - res.status(400).json({ error: 'Invalid startTime format. Use RFC3339 (e.g., 2024-01-01T00:00:00Z)' }); - return; - } - if (isNaN(end.getTime())) { - res.status(400).json({ error: 'Invalid endTime format. Use RFC3339 (e.g., 2024-01-01T23:59:59Z)' }); - return; - } - if (start >= end) { - res.status(400).json({ error: 'startTime must be before endTime' }); - return; - } - - // Validate timezone (IANA format) - try { - Intl.DateTimeFormat(undefined, { timeZone: tz }); - } catch { - res.status(400).json({ error: 'Invalid timezone. Use IANA format (e.g., America/New_York, UTC)' }); - return; - } - - logger.info('Fetching usage data', { - requestId: req.requestId, - start: start.toISOString(), - end: end.toISOString(), - timezone: tz, - format, - operation: 'usage_fetch', - module: 'usage-handler' - }); - - // CSV export path - const wantsCsv = String(format || '').toLowerCase() === 'csv'; - if (wantsCsv) { - const records = usageTracker.getUsageRecords(start.toISOString(), end.toISOString()); - - const filename = `usage-${start.toISOString().slice(0, 10)}-${end.toISOString().slice(0, 10)}.csv`; - res.setHeader('Content-Type', 'text/csv'); - res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); - res.send(usageTracker.toCsv(records)); - return; - } - - // Get usage data with date range filtering - const usage = usageTracker.getUsageFromDatabase(start.toISOString(), end.toISOString()); - res.json(usage); - } catch (error) { - logger.error('Failed to fetch usage data', error, { requestId: req.requestId, module: 'usage-handler' }); - handleError(error, res); - } - } -} - -// Singleton instance -const usageHandler = new UsageHandler(); - -// Endpoint function -export const handleUsageRequest = async (req: Request, res: Response): Promise => { - await usageHandler.getUsage(req, res); -}; diff --git a/gateway/src/canonical/types/index.ts b/gateway/src/canonical/types/index.ts deleted file mode 100644 index 143df48..0000000 --- a/gateway/src/canonical/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { CanonicalAIRequestSchema as Request } from './request.types'; -export type { CanonicalAIResponseSchema as Response } from './response.types'; -export type { CanonicalAIStreamingResponseSchema as StreamingResponse } from './streaming-response.types'; diff --git a/gateway/src/canonical/types/request.types.ts b/gateway/src/canonical/types/request.types.ts deleted file mode 100644 index 52cf91e..0000000 --- a/gateway/src/canonical/types/request.types.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* eslint-disable */ - -export type InputContent = TextInput | ImageInput | AudioInput | VideoInput | DocumentInput | ToolResultInput; -export type ToolChoice = - | ('auto' | 'none' | 'any' | 'required') - | { - type: 'function'; - function: { - name: string; - }; - allow_parallel?: boolean; - } - | { - type: 'tool'; - name: string; - allow_parallel?: boolean; - }; -export type ResponseFormat = - | ('text' | 'json' | 'json_object') - | { - type: 'json_schema'; - json_schema: { - name: string; - description?: string; - schema: { - [k: string]: unknown; - }; - strict?: boolean; - }; - }; - -/** - * Universal schema for AI provider requests, superset of all input capabilities - */ -export interface CanonicalAIRequestSchema { - schema_version: '1.0.1'; - model: string; - /** - * @minItems 1 - */ - messages: [Message, ...Message[]]; - system?: string | InputContent[]; - generation?: { - max_tokens?: number; - temperature?: number; - top_p?: number; - top_k?: number; - stop?: string | string[]; - seed?: number; - frequency_penalty?: number; - presence_penalty?: number; - n?: number; - logprobs?: boolean; - top_logprobs?: number; - logit_bias?: { - /** - * This interface was referenced by `undefined`'s JSON-Schema definition - * via the `patternProperty` "^\d+$". - */ - [k: string]: number; - }; - /** - * @maxItems 64 - */ - stop_sequences?: string[]; - }; - tools?: Tool[]; - tool_choice?: ToolChoice; - parallel_tool_calls?: boolean; - functions?: { - name: string; - description?: string; - parameters?: { - [k: string]: unknown; - }; - }[]; - function_call?: - | ('auto' | 'none') - | { - name: string; - }; - response_format?: ResponseFormat; - safety_settings?: SafetySettings[]; - candidate_count?: number; - stream?: boolean; - stream_options?: { - include_usage?: boolean; - }; - service_tier?: 'auto' | 'default' | 'scale' | 'flex' | 'priority' | null; - reasoning_effort?: 'low' | 'medium' | 'high'; - modalities?: ('text' | 'audio')[]; - audio?: { - voice?: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer'; - format?: 'wav' | 'mp3' | 'flac' | 'aac' | 'opus' | 'pcm16'; - }; - prediction?: { - type: 'content'; - content: string | InputContent[]; - }; - tier?: 'priority' | 'standard'; - thinking?: { - enabled?: boolean; - budget?: number; - }; - betas?: string[]; - extra_headers?: { - [k: string]: string; - }; - timeout?: number; - user?: string; - context?: { - previous_response_id?: string; - cache_ref?: string; - provider_state?: { - [k: string]: unknown; - }; - }; - attachments?: { - id: string; - name: string; - content_type?: string; - size?: number; - url?: string; - }[]; - provider_params?: { - openai?: { - [k: string]: unknown; - }; - anthropic?: { - [k: string]: unknown; - }; - gemini?: { - [k: string]: unknown; - }; - }; - meta?: { - user_id?: string; - session_id?: string; - tags?: string[]; - [k: string]: unknown; - }; -} -export interface Message { - role: 'user' | 'assistant' | 'tool'; - content: string | [InputContent, ...InputContent[]]; - name?: string; - tool_call_id?: string; - tool_calls?: { - id: string; - type: 'function'; - function: { - name: string; - arguments: string; - }; - }[]; -} -export interface TextInput { - type: 'text'; - text: string; -} -export interface ImageInput { - type: 'image'; - source: - | { - type: 'base64'; - media_type: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp'; - data: string; - } - | { - type: 'url'; - url: string; - }; -} -export interface AudioInput { - type: 'audio'; - source: - | { - type: 'base64'; - media_type: 'audio/wav' | 'audio/mp3' | 'audio/aac' | 'audio/ogg' | 'audio/flac'; - data: string; - } - | { - type: 'url'; - url: string; - }; -} -export interface VideoInput { - type: 'video'; - source: - | { - type: 'base64'; - media_type: 'video/mp4' | 'video/mpeg' | 'video/quicktime' | 'video/webm'; - data: string; - } - | { - type: 'url'; - url: string; - }; -} -export interface DocumentInput { - type: 'document'; - source: - | { - type: 'base64'; - media_type: 'application/pdf' | 'text/plain' | 'text/html' | 'text/markdown'; - data: string; - } - | { - type: 'url'; - url: string; - }; -} -export interface ToolResultInput { - type: 'tool_result'; - tool_use_id: string; - content: string | InputContent[]; - is_error?: boolean; -} -export interface Tool { - type: 'function'; - function: { - name: string; - description?: string; - parameters?: { - [k: string]: unknown; - }; - strict?: boolean; - }; -} -export interface SafetySettings { - category: - | 'HARM_CATEGORY_HARASSMENT' - | 'HARM_CATEGORY_HATE_SPEECH' - | 'HARM_CATEGORY_SEXUALLY_EXPLICIT' - | 'HARM_CATEGORY_DANGEROUS_CONTENT' - | 'HARM_CATEGORY_UNSPECIFIED'; - threshold: 'BLOCK_NONE' | 'BLOCK_ONLY_HIGH' | 'BLOCK_MEDIUM_AND_ABOVE' | 'BLOCK_LOW_AND_ABOVE'; -} diff --git a/gateway/src/canonical/types/response.types.ts b/gateway/src/canonical/types/response.types.ts deleted file mode 100644 index 9ee98dd..0000000 --- a/gateway/src/canonical/types/response.types.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* eslint-disable */ - -export type OutputContent = - | TextOutput - | ThinkingOutput - | ToolUseOutput - | CodeExecutionOutput - | WebSearchOutput - | CitationOutput; - -/** - * Universal schema for AI provider responses - superset of all output capabilities - */ -export interface CanonicalAIResponseSchema { - schema_version: '1.0.1'; - id: string; - model: string; - created: number; - /** - * @minItems 1 - */ - choices: [Choice, ...Choice[]]; - candidates?: Choice[]; - usage?: Usage; - system_fingerprint?: string; - service_tier_utilized?: 'default' | 'scale' | 'auto' | 'flex' | 'priority'; - object?: string; - type?: string; - role?: string; - stop_reason?: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' | null; - stop_sequence?: string; - provider?: 'openai' | 'anthropic' | 'gemini'; - provider_raw?: { - [k: string]: unknown; - }; - safety_feedback?: { - category: - | 'HARM_CATEGORY_HARASSMENT' - | 'HARM_CATEGORY_HATE_SPEECH' - | 'HARM_CATEGORY_SEXUALLY_EXPLICIT' - | 'HARM_CATEGORY_DANGEROUS_CONTENT'; - probability: 'NEGLIGIBLE' | 'LOW' | 'MEDIUM' | 'HIGH'; - blocked?: boolean; - }[]; - metadata?: { - provider?: string; - original_model?: string; - processing_time?: number; - [k: string]: unknown; - }; -} -export interface Choice { - index: number; - message: Message; - finish_reason?: - | 'stop' - | 'length' - | 'tool_calls' - | 'content_filter' - | 'function_call' - | 'end_turn' - | 'max_tokens' - | 'stop_sequence' - | 'tool_use' - | 'error' - | 'recitation' - | 'safety'; - tool_calls?: { - id: string; - type: 'function'; - function: { - name: string; - arguments: string; - }; - }[]; - logprobs?: { - content?: { - token: string; - logprob: number; - bytes?: number[]; - top_logprobs?: { - token: string; - logprob: number; - bytes?: number[]; - }[]; - }[]; - }; - safety_ratings?: { - category: - | 'HARM_CATEGORY_HARASSMENT' - | 'HARM_CATEGORY_HATE_SPEECH' - | 'HARM_CATEGORY_SEXUALLY_EXPLICIT' - | 'HARM_CATEGORY_DANGEROUS_CONTENT'; - probability: 'NEGLIGIBLE' | 'LOW' | 'MEDIUM' | 'HIGH'; - blocked?: boolean; - }[]; -} -export interface Message { - role: 'assistant'; - /** - * @minItems 1 - */ - content: [OutputContent, ...OutputContent[]]; -} -export interface TextOutput { - type: 'text'; - text: string; - annotations?: { - citations?: Citation[]; - }; -} -export interface Citation { - url: string; - title?: string; - start_index?: number; - end_index?: number; - confidence?: number; -} -export interface ThinkingOutput { - type: 'thinking'; - thinking: string; -} -export interface ToolUseOutput { - type: 'tool_use'; - id: string; - name: string; - input: - | { - [k: string]: unknown; - } - | string - | unknown[] - | null; -} -export interface CodeExecutionOutput { - type: 'code_execution'; - language: string; - code: string; - output?: string; - error?: string; - execution_time?: number; -} -export interface WebSearchOutput { - type: 'web_search'; - query: string; - results: { - url: string; - title: string; - snippet?: string; - relevance_score?: number; - }[]; -} -export interface CitationOutput { - type: 'citation'; - sources: Citation[]; -} -export interface Usage { - prompt_tokens?: number; - completion_tokens?: number; - total_tokens?: number; - input_tokens?: number; - output_tokens?: number; - cached_tokens?: number; - reasoning_tokens?: number; - completion_tokens_details?: { - reasoning_tokens?: number; - audio_tokens?: number; - accepted_prediction_tokens?: number; - rejected_prediction_tokens?: number; - }; - predictions?: { - accepted_tokens?: number; - rejected_tokens?: number; - }; - provider_breakdown?: { - [k: string]: - | number - | string - | { - [k: string]: unknown; - } - | null; - }; - prompt_tokens_details?: { - cached_tokens?: number; - audio_tokens?: number; - }; -} diff --git a/gateway/src/canonical/types/streaming-response.types.ts b/gateway/src/canonical/types/streaming-response.types.ts deleted file mode 100644 index 28d0dce..0000000 --- a/gateway/src/canonical/types/streaming-response.types.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* eslint-disable */ - -/** - * Universal schema for AI provider streaming responses - superset of all streaming capabilities - */ -export type CanonicalAIStreamingResponseSchema = - | { - schema_version: '1.0.1'; - stream_type: 'canonical'; - event: - | { - type: 'message_start'; - id?: string; - model?: string; - } - | { - type: 'content_delta'; - part: 'text' | 'tool_call' | 'thinking'; - value: string; - } - | { - type: 'tool_call'; - name: string; - arguments_json: string; - id?: string; - } - | { - type: 'usage'; - input_tokens?: number; - output_tokens?: number; - prompt_tokens?: number; - completion_tokens?: number; - } - | { - type: 'complete'; - finish_reason: 'stop' | 'length' | 'tool_call' | 'content_filter' | 'safety' | 'unknown'; - } - | { - type: 'error'; - code?: string; - message: string; - }; - provider_raw?: { - [k: string]: unknown; - }; - } - | { - schema_version: '1.0.1'; - stream_type: 'openai'; - id: string; - object: 'chat.completion.chunk'; - created: number; - model: string; - system_fingerprint?: string; - /** - * @minItems 1 - */ - choices: [StreamingChoice, ...StreamingChoice[]]; - usage?: { - prompt_tokens?: number; - completion_tokens?: number; - total_tokens?: number; - }; - } - | { - schema_version: '1.0.1'; - stream_type: 'anthropic'; - event: AnthropicEvent; - }; -export type AnthropicEvent = - | { - type: 'message_start'; - message: { - id: string; - type: 'message'; - role: 'assistant'; - content: unknown[]; - model: string; - stop_reason: null; - stop_sequence: null; - usage: { - input_tokens: number; - output_tokens: number; - }; - }; - } - | { - type: 'content_block_start'; - index: number; - content_block: - | { - type: 'text'; - text: string; - } - | { - type: 'tool_use'; - id: string; - name: string; - input: { - [k: string]: unknown; - }; - }; - } - | { - type: 'content_block_delta'; - index: number; - delta: - | { - type: 'text_delta'; - text: string; - } - | { - type: 'input_json_delta'; - partial_json: string; - } - | { - type: 'citations_delta'; - citations: { - [k: string]: unknown; - }[]; - }; - } - | { - type: 'content_block_stop'; - index: number; - } - | { - type: 'message_delta'; - delta: { - stop_reason?: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use'; - stop_sequence?: string; - }; - usage: { - output_tokens: number; - }; - } - | { - type: 'message_stop'; - } - | { - type: 'ping'; - }; - -export interface StreamingChoice { - index: number; - delta?: { - role?: string; - content?: string; - tool_calls?: { - id?: string; - index?: number; - type?: 'function'; - function?: { - name?: string; - arguments?: string; - }; - }[]; - function_call?: { - name?: string; - arguments?: string; - }; - }; - logprobs?: { - content?: { - token: string; - logprob: number; - bytes?: number[]; - top_logprobs?: { - token: string; - logprob: number; - bytes?: number[]; - }[]; - }[]; - }; - finish_reason?: - | 'stop' - | 'length' - | 'tool_calls' - | 'content_filter' - | 'function_call' - | 'end_turn' - | 'max_tokens' - | 'stop_sequence' - | 'tool_use' - | 'error' - | null; -} diff --git a/gateway/src/costs/README.md b/gateway/src/costs/README.md deleted file mode 100644 index 797e26c..0000000 --- a/gateway/src/costs/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Pricing Configuration - -AI provider pricing configuration files for the proxy. - -## Structure - -``` -costs/ -├── openai.yaml # OpenAI (Standard tier) -├── anthropic.yaml # Anthropic -├── openrouter.yaml # OpenRouter -├── templates/base.yaml # Template for new providers -└── index.ts # Pricing loader -``` - -## Adding a Provider - -1. Copy `templates/base.yaml` to `provider-name.yaml` -2. Fill in the required fields: - -```yaml -provider: "provider_name" -currency: "USD" -unit: "MTok" -models: - model-name: - input: 1.00 - output: 3.00 - # Optional caching fields: - cached_input: 0.10 # OpenAI style - 5m_cache_write: 1.25 # Anthropic style - 1h_cache_write: 2.00 # Anthropic style - cache_read: 0.10 # Anthropic style -metadata: - last_updated: "YYYY-MM-DD" - source: "official_pricing_url" - version: "1.0" -``` - -## Required Fields - -- `provider`: Provider name (lowercase) -- `currency`: Currency code (e.g., "USD") -- `unit`: Token unit (e.g., "MTok" for millions) -- `models`: Model pricing object -- `metadata`: Source info and last updated date - -## Model Pricing Fields - -- `input`: Input token cost per 1M tokens -- `output`: Output token cost per 1M tokens - -**Caching (optional):** -- `cached_input`: OpenAI cached input cost (~10% of input) -- `5m_cache_write`: Anthropic 5min cache write (~125% of input) -- `1h_cache_write`: Anthropic 1hr cache write (~200% of input) -- `cache_read`: Anthropic cache read (~10% of input) - -## Best Practices - -- **Update regularly**: Check official pricing monthly -- **Use official sources**: Link to provider pricing pages in metadata -- **Include caching**: Add cache pricing where supported -- **Test changes**: Restart service and check logs for validation errors - -## Examples - -**OpenAI:** -```yaml -gpt-4o: - input: 2.50 - cached_input: 1.25 - output: 10.00 -``` - -**Anthropic:** -```yaml -claude-3-5-sonnet: - input: 3.00 - output: 15.00 - 5m_cache_write: 3.75 - 1h_cache_write: 6.00 - cache_read: 0.30 -``` diff --git a/gateway/src/costs/anthropic.yaml b/gateway/src/costs/anthropic.yaml deleted file mode 100644 index f04cc6d..0000000 --- a/gateway/src/costs/anthropic.yaml +++ /dev/null @@ -1,89 +0,0 @@ -provider: "anthropic" -currency: "USD" -unit: "MTok" - -models: - # Current flagship models (Claude 4.5 series) - claude-opus-4-5: - input: 5.00 - output: 25.00 - 5m_cache_write: 6.25 - 1h_cache_write: 10.00 - cache_read: 0.50 - - claude-sonnet-4-5: - input: 3.00 - output: 15.00 - 5m_cache_write: 3.75 - 1h_cache_write: 6.00 - cache_read: 0.30 - - claude-haiku-4-5: - input: 1.00 - output: 5.00 - 5m_cache_write: 1.25 - 1h_cache_write: 2.00 - cache_read: 0.10 - - # Legacy models - claude-opus-4-1: - input: 15.00 - output: 75.00 - 5m_cache_write: 18.75 - 1h_cache_write: 30.00 - cache_read: 1.50 - - claude-opus-4: - input: 15.00 - output: 75.00 - 5m_cache_write: 18.75 - 1h_cache_write: 30.00 - cache_read: 1.50 - - claude-sonnet-4: - input: 3.00 - output: 15.00 - 5m_cache_write: 3.75 - 1h_cache_write: 6.00 - cache_read: 0.30 - - claude-sonnet-3-7: - input: 3.00 - output: 15.00 - 5m_cache_write: 3.75 - 1h_cache_write: 6.00 - cache_read: 0.30 - - claude-3-5-haiku: - input: 0.80 - output: 4.00 - 5m_cache_write: 1.00 - 1h_cache_write: 1.60 - cache_read: 0.08 - - claude-3-5-sonnet: - input: 3.00 - output: 15.00 - 5m_cache_write: 3.75 - 1h_cache_write: 6.00 - cache_read: 0.30 - - claude-3-opus: - input: 15.00 - output: 75.00 - 5m_cache_write: 18.75 - 1h_cache_write: 30.00 - cache_read: 1.50 - - claude-3-haiku: - input: 0.25 - output: 1.25 - 5m_cache_write: 0.30 - 1h_cache_write: 0.50 - cache_read: 0.03 - -metadata: - last_updated: "2026-01-28" - source: "https://platform.claude.com/docs/en/about-claude/pricing" - notes: "Prices in USD per 1M tokens" - version: "0.2" diff --git a/gateway/src/costs/google.yaml b/gateway/src/costs/google.yaml deleted file mode 100644 index cabbcfe..0000000 --- a/gateway/src/costs/google.yaml +++ /dev/null @@ -1,25 +0,0 @@ -provider: google -currency: USD -unit: MTok -models: - gemini-2.5-pro: - input: 1.25 - cached_input: 0.125 - output: 10.0 - gemini-2.5-flash: - input: 0.3 - cached_input: 0.03 - output: 2.5 - gemini-2.5-flash-lite: - input: 0.1 - cached_input: 0.025 - output: 0.4 - gemini-3.0-pro-preview: - input: 2.0 - cached_input: 0.2 - output: 12.0 -metadata: - last_updated: "2025-01-15" - source: "https://ai.google.dev/pricing" - notes: "Gemini API pricing in USD per 1M tokens" - version: "1.0" diff --git a/gateway/src/costs/index.ts b/gateway/src/costs/index.ts deleted file mode 100644 index ff8fcfe..0000000 --- a/gateway/src/costs/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Export the pricing loader and types -export { pricingLoader, PricingLoader } from '../infrastructure/utils/pricing-loader.js'; -export type { - PricingConfig, - ModelPricing, - PricingMetadata, - CostCalculation -} from '../infrastructure/utils/pricing-loader.js'; - diff --git a/gateway/src/costs/ollama.yaml b/gateway/src/costs/ollama.yaml deleted file mode 100644 index 2d7bb29..0000000 --- a/gateway/src/costs/ollama.yaml +++ /dev/null @@ -1,59 +0,0 @@ -provider: "ollama" -currency: "USD" -unit: "MTok" -models: - # Ollama runs models locally — all costs are zero. - # Users may add custom model entries here if needed. - llama3.3: - input: 0.00 - output: 0.00 - llama3.2: - input: 0.00 - output: 0.00 - llama3.1: - input: 0.00 - output: 0.00 - llama3: - input: 0.00 - output: 0.00 - gemma3: - input: 0.00 - output: 0.00 - gemma2: - input: 0.00 - output: 0.00 - qwen3: - input: 0.00 - output: 0.00 - qwen2.5-coder: - input: 0.00 - output: 0.00 - deepseek-r1: - input: 0.00 - output: 0.00 - deepseek-coder-v2: - input: 0.00 - output: 0.00 - phi4: - input: 0.00 - output: 0.00 - phi3: - input: 0.00 - output: 0.00 - mistral: - input: 0.00 - output: 0.00 - mixtral: - input: 0.00 - output: 0.00 - codellama: - input: 0.00 - output: 0.00 - starcoder2: - input: 0.00 - output: 0.00 -metadata: - last_updated: "2026-02-03" - source: "https://ollama.com" - notes: "Ollama runs models locally. All API costs are zero — hardware costs are borne by the user." - version: "1.0" diff --git a/gateway/src/costs/openai.yaml b/gateway/src/costs/openai.yaml deleted file mode 100644 index a9cc8c5..0000000 --- a/gateway/src/costs/openai.yaml +++ /dev/null @@ -1,184 +0,0 @@ -provider: "openai" -currency: "USD" -unit: "MTok" -models: - # GPT-5.2 series (latest flagship) - gpt-5.2: - input: 1.75 - cached_input: 0.175 - output: 14.00 - gpt-5.2-pro: - input: 21.00 - cached_input: 2.10 - output: 168.00 - - # GPT-5.1 series - gpt-5.1: - input: 1.25 - cached_input: 0.125 - output: 10.00 - gpt-5.1-codex: - input: 1.25 - cached_input: 0.125 - output: 10.00 - gpt-5: - input: 1.25 - cached_input: 0.125 - output: 10.00 - gpt-5-mini: - input: 0.25 - cached_input: 0.025 - output: 2.00 - gpt-5-nano: - input: 0.05 - cached_input: 0.005 - output: 0.40 - gpt-5-chat-latest: - input: 1.25 - cached_input: 0.125 - output: 10.00 - gpt-5-codex: - input: 1.25 - cached_input: 0.125 - output: 10.00 - - gpt-4.1: - input: 2.00 - cached_input: 0.50 - output: 8.00 - gpt-4.1-mini: - input: 0.40 - cached_input: 0.10 - output: 1.60 - gpt-4.1-nano: - input: 0.10 - cached_input: 0.025 - output: 0.40 - - gpt-4o: - input: 2.50 - cached_input: 1.25 - output: 10.00 - gpt-4o-2024-05-13: - input: 5.00 - output: 15.00 - gpt-4o-mini: - input: 0.15 - cached_input: 0.075 - output: 0.60 - - gpt-realtime: - input: 4.00 - cached_input: 0.40 - output: 16.00 - gpt-4o-realtime-preview: - input: 5.00 - cached_input: 2.50 - output: 20.00 - gpt-4o-mini-realtime-preview: - input: 0.60 - cached_input: 0.30 - output: 2.40 - o1: - input: 15.00 - cached_input: 7.50 - output: 60.00 - o1-pro: - input: 150.00 - output: 600.00 - o3-pro: - input: 20.00 - output: 80.00 - o3: - input: 2.00 - cached_input: 0.50 - output: 8.00 - o3-deep-research: - input: 10.00 - cached_input: 2.50 - output: 40.00 - o4-mini: - input: 1.10 - cached_input: 0.275 - output: 4.40 - o4-mini-deep-research: - input: 2.00 - cached_input: 0.50 - output: 8.00 - o3-mini: - input: 1.10 - cached_input: 0.55 - output: 4.40 - o1-mini: - input: 1.10 - cached_input: 0.55 - output: 4.40 - - codex-mini-latest: - input: 1.50 - cached_input: 0.375 - output: 6.00 - gpt-4o-mini-search-preview: - input: 0.15 - output: 0.60 - gpt-4o-search-preview: - input: 2.50 - output: 10.00 - computer-use-preview: - input: 3.00 - output: 12.00 - gpt-image-1: - input: 5.00 - cached_input: 1.25 - - gpt-3.5-turbo: - input: 0.50 - output: 1.50 - chatgpt-4o-latest: - input: 5.00 - output: 15.00 - gpt-4-turbo-2024-04-09: - input: 10.00 - output: 30.00 - gpt-4-0125-preview: - input: 10.00 - output: 30.00 - gpt-4-1106-preview: - input: 10.00 - output: 30.00 - gpt-4-1106-vision-preview: - input: 10.00 - output: 30.00 - gpt-4-0613: - input: 30.00 - output: 60.00 - gpt-4-0314: - input: 30.00 - output: 60.00 - gpt-4-32k: - input: 60.00 - output: 120.00 - gpt-3.5-turbo-0125: - input: 0.50 - output: 1.50 - gpt-3.5-turbo-1106: - input: 1.00 - output: 2.00 - gpt-3.5-turbo-0613: - input: 1.50 - output: 2.00 - gpt-3.5-0301: - input: 1.50 - output: 2.00 - gpt-3.5-turbo-instruct: - input: 1.50 - output: 2.00 - gpt-3.5-turbo-16k-0613: - input: 3.00 - output: 4.00 -metadata: - last_updated: "2026-01-28" - source: "https://platform.openai.com/docs/pricing" - notes: "Prices in USD per 1M tokens for Standard tier processing. Cached input pricing included where available." - version: "0.2" - tier: "standard" diff --git a/gateway/src/costs/openrouter.yaml b/gateway/src/costs/openrouter.yaml deleted file mode 100644 index 8fcc890..0000000 --- a/gateway/src/costs/openrouter.yaml +++ /dev/null @@ -1,3399 +0,0 @@ -provider: openrouter -currency: USD -unit: MTok -models: - anthropic/claude-sonnet-4.6: - id: anthropic/claude-sonnet-4.6 - input: 3 - output: 15 - original_provider: anthropic - claude-sonnet-4.6: - id: anthropic/claude-sonnet-4.6 - input: 3 - output: 15 - original_provider: anthropic - qwen/qwen3.5-plus-02-15: - id: qwen/qwen3.5-plus-02-15 - input: 0.4 - output: 2.4 - original_provider: qwen - qwen3.5-plus-02-15: - id: qwen/qwen3.5-plus-02-15 - input: 0.4 - output: 2.4 - original_provider: qwen - qwen/qwen3.5-397b-a17b: - id: qwen/qwen3.5-397b-a17b - input: 0.15 - output: 1 - original_provider: qwen - qwen3.5-397b-a17b: - id: qwen/qwen3.5-397b-a17b - input: 0.15 - output: 1 - original_provider: qwen - minimax/minimax-m2.5: - id: minimax/minimax-m2.5 - input: 0.3 - output: 1.1 - original_provider: minimax - minimax-m2.5: - id: minimax/minimax-m2.5 - input: 0.3 - output: 1.1 - original_provider: minimax - z-ai/glm-5: - id: z-ai/glm-5 - input: 0.3 - output: 2.55 - original_provider: z-ai - glm-5: - id: z-ai/glm-5 - input: 0.3 - output: 2.55 - original_provider: z-ai - qwen/qwen3-max-thinking: - id: qwen/qwen3-max-thinking - input: 1.2 - output: 6 - original_provider: qwen - qwen3-max-thinking: - id: qwen/qwen3-max-thinking - input: 1.2 - output: 6 - original_provider: qwen - openrouter/aurora-alpha: - id: openrouter/aurora-alpha - input: 0 - output: 0 - original_provider: openrouter - aurora-alpha: - id: openrouter/aurora-alpha - input: 0 - output: 0 - original_provider: openrouter - anthropic/claude-opus-4.6: - id: anthropic/claude-opus-4.6 - input: 5 - output: 25 - original_provider: anthropic - claude-opus-4.6: - id: anthropic/claude-opus-4.6 - input: 5 - output: 25 - original_provider: anthropic - qwen/qwen3-coder-next: - id: qwen/qwen3-coder-next - input: 0.12 - output: 0.75 - original_provider: qwen - qwen3-coder-next: - id: qwen/qwen3-coder-next - input: 0.12 - output: 0.75 - original_provider: qwen - openrouter/free: - id: openrouter/free - input: 0 - output: 0 - original_provider: openrouter - free: - id: openrouter/free - input: 0 - output: 0 - original_provider: openrouter - stepfun/step-3.5-flash:free: - id: stepfun/step-3.5-flash:free - input: 0 - output: 0 - original_provider: stepfun - step-3.5-flash:free: - id: stepfun/step-3.5-flash:free - input: 0 - output: 0 - original_provider: stepfun - stepfun/step-3.5-flash: - id: stepfun/step-3.5-flash - input: 0.1 - output: 0.3 - original_provider: stepfun - step-3.5-flash: - id: stepfun/step-3.5-flash - input: 0.1 - output: 0.3 - original_provider: stepfun - arcee-ai/trinity-large-preview:free: - id: arcee-ai/trinity-large-preview:free - input: 0 - output: 0 - original_provider: arcee-ai - trinity-large-preview:free: - id: arcee-ai/trinity-large-preview:free - input: 0 - output: 0 - original_provider: arcee-ai - moonshotai/kimi-k2.5: - id: moonshotai/kimi-k2.5 - input: 0.23 - output: 3 - original_provider: moonshotai - kimi-k2.5: - id: moonshotai/kimi-k2.5 - input: 0.23 - output: 3 - original_provider: moonshotai - upstage/solar-pro-3:free: - id: upstage/solar-pro-3:free - input: 0 - output: 0 - original_provider: upstage - solar-pro-3:free: - id: upstage/solar-pro-3:free - input: 0 - output: 0 - original_provider: upstage - minimax/minimax-m2-her: - id: minimax/minimax-m2-her - input: 0.3 - output: 1.2 - original_provider: minimax - minimax-m2-her: - id: minimax/minimax-m2-her - input: 0.3 - output: 1.2 - original_provider: minimax - writer/palmyra-x5: - id: writer/palmyra-x5 - input: 0.6 - output: 6 - original_provider: writer - palmyra-x5: - id: writer/palmyra-x5 - input: 0.6 - output: 6 - original_provider: writer - liquid/lfm-2.5-1.2b-thinking:free: - id: liquid/lfm-2.5-1.2b-thinking:free - input: 0 - output: 0 - original_provider: liquid - lfm-2.5-1.2b-thinking:free: - id: liquid/lfm-2.5-1.2b-thinking:free - input: 0 - output: 0 - original_provider: liquid - liquid/lfm-2.5-1.2b-instruct:free: - id: liquid/lfm-2.5-1.2b-instruct:free - input: 0 - output: 0 - original_provider: liquid - lfm-2.5-1.2b-instruct:free: - id: liquid/lfm-2.5-1.2b-instruct:free - input: 0 - output: 0 - original_provider: liquid - openai/gpt-audio: - id: openai/gpt-audio - input: 2.5 - output: 10 - original_provider: openai - gpt-audio: - id: openai/gpt-audio - input: 2.5 - output: 10 - original_provider: openai - openai/gpt-audio-mini: - id: openai/gpt-audio-mini - input: 0.6 - output: 2.4 - original_provider: openai - gpt-audio-mini: - id: openai/gpt-audio-mini - input: 0.6 - output: 2.4 - original_provider: openai - z-ai/glm-4.7-flash: - id: z-ai/glm-4.7-flash - input: 0.06 - output: 0.4 - original_provider: z-ai - glm-4.7-flash: - id: z-ai/glm-4.7-flash - input: 0.06 - output: 0.4 - original_provider: z-ai - openai/gpt-5.2-codex: - id: openai/gpt-5.2-codex - input: 1.75 - output: 14 - original_provider: openai - gpt-5.2-codex: - id: openai/gpt-5.2-codex - input: 1.75 - output: 14 - original_provider: openai - allenai/molmo-2-8b: - id: allenai/molmo-2-8b - input: 0.2 - output: 0.2 - original_provider: allenai - molmo-2-8b: - id: allenai/molmo-2-8b - input: 0.2 - output: 0.2 - original_provider: allenai - allenai/olmo-3.1-32b-instruct: - id: allenai/olmo-3.1-32b-instruct - input: 0.2 - output: 0.6 - original_provider: allenai - olmo-3.1-32b-instruct: - id: allenai/olmo-3.1-32b-instruct - input: 0.2 - output: 0.6 - original_provider: allenai - bytedance-seed/seed-1.6-flash: - id: bytedance-seed/seed-1.6-flash - input: 0.075 - output: 0.3 - original_provider: bytedance-seed - seed-1.6-flash: - id: bytedance-seed/seed-1.6-flash - input: 0.075 - output: 0.3 - original_provider: bytedance-seed - bytedance-seed/seed-1.6: - id: bytedance-seed/seed-1.6 - input: 0.25 - output: 2 - original_provider: bytedance-seed - seed-1.6: - id: bytedance-seed/seed-1.6 - input: 0.25 - output: 2 - original_provider: bytedance-seed - minimax/minimax-m2.1: - id: minimax/minimax-m2.1 - input: 0.27 - output: 0.95 - original_provider: minimax - minimax-m2.1: - id: minimax/minimax-m2.1 - input: 0.27 - output: 0.95 - original_provider: minimax - z-ai/glm-4.7: - id: z-ai/glm-4.7 - input: 0.38 - output: 1.7 - original_provider: z-ai - glm-4.7: - id: z-ai/glm-4.7 - input: 0.38 - output: 1.7 - original_provider: z-ai - google/gemini-3-flash-preview: - id: google/gemini-3-flash-preview - input: 0.5 - output: 3 - original_provider: google - gemini-3-flash-preview: - id: google/gemini-3-flash-preview - input: 0.5 - output: 3 - original_provider: google - mistralai/mistral-small-creative: - id: mistralai/mistral-small-creative - input: 0.1 - output: 0.3 - original_provider: mistralai - mistral-small-creative: - id: mistralai/mistral-small-creative - input: 0.1 - output: 0.3 - original_provider: mistralai - allenai/olmo-3.1-32b-think: - id: allenai/olmo-3.1-32b-think - input: 0.15 - output: 0.5 - original_provider: allenai - olmo-3.1-32b-think: - id: allenai/olmo-3.1-32b-think - input: 0.15 - output: 0.5 - original_provider: allenai - xiaomi/mimo-v2-flash: - id: xiaomi/mimo-v2-flash - input: 0.09 - output: 0.29 - original_provider: xiaomi - mimo-v2-flash: - id: xiaomi/mimo-v2-flash - input: 0.09 - output: 0.29 - original_provider: xiaomi - nvidia/nemotron-3-nano-30b-a3b:free: - id: nvidia/nemotron-3-nano-30b-a3b:free - input: 0 - output: 0 - original_provider: nvidia - nemotron-3-nano-30b-a3b:free: - id: nvidia/nemotron-3-nano-30b-a3b:free - input: 0 - output: 0 - original_provider: nvidia - nvidia/nemotron-3-nano-30b-a3b: - id: nvidia/nemotron-3-nano-30b-a3b - input: 0.05 - output: 0.2 - original_provider: nvidia - nemotron-3-nano-30b-a3b: - id: nvidia/nemotron-3-nano-30b-a3b - input: 0.05 - output: 0.2 - original_provider: nvidia - openai/gpt-5.2-chat: - id: openai/gpt-5.2-chat - input: 1.75 - output: 14 - original_provider: openai - gpt-5.2-chat: - id: openai/gpt-5.2-chat - input: 1.75 - output: 14 - original_provider: openai - openai/gpt-5.2-pro: - id: openai/gpt-5.2-pro - input: 21 - output: 168 - original_provider: openai - gpt-5.2-pro: - id: openai/gpt-5.2-pro - input: 21 - output: 168 - original_provider: openai - openai/gpt-5.2: - id: openai/gpt-5.2 - input: 1.75 - output: 14 - original_provider: openai - gpt-5.2: - id: openai/gpt-5.2 - input: 1.75 - output: 14 - original_provider: openai - mistralai/devstral-2512: - id: mistralai/devstral-2512 - input: 0.4 - output: 2 - original_provider: mistralai - devstral-2512: - id: mistralai/devstral-2512 - input: 0.4 - output: 2 - original_provider: mistralai - relace/relace-search: - id: relace/relace-search - input: 1 - output: 3 - original_provider: relace - relace-search: - id: relace/relace-search - input: 1 - output: 3 - original_provider: relace - z-ai/glm-4.6v: - id: z-ai/glm-4.6v - input: 0.3 - output: 0.9 - original_provider: z-ai - glm-4.6v: - id: z-ai/glm-4.6v - input: 0.3 - output: 0.9 - original_provider: z-ai - nex-agi/deepseek-v3.1-nex-n1: - id: nex-agi/deepseek-v3.1-nex-n1 - input: 0.27 - output: 1 - original_provider: nex-agi - deepseek-v3.1-nex-n1: - id: nex-agi/deepseek-v3.1-nex-n1 - input: 0.27 - output: 1 - original_provider: nex-agi - essentialai/rnj-1-instruct: - id: essentialai/rnj-1-instruct - input: 0.15 - output: 0.15 - original_provider: essentialai - rnj-1-instruct: - id: essentialai/rnj-1-instruct - input: 0.15 - output: 0.15 - original_provider: essentialai - openrouter/bodybuilder: - id: openrouter/bodybuilder - input: -1000000 - output: -1000000 - original_provider: openrouter - bodybuilder: - id: openrouter/bodybuilder - input: -1000000 - output: -1000000 - original_provider: openrouter - openai/gpt-5.1-codex-max: - id: openai/gpt-5.1-codex-max - input: 1.25 - output: 10 - original_provider: openai - gpt-5.1-codex-max: - id: openai/gpt-5.1-codex-max - input: 1.25 - output: 10 - original_provider: openai - amazon/nova-2-lite-v1: - id: amazon/nova-2-lite-v1 - input: 0.3 - output: 2.5 - original_provider: amazon - nova-2-lite-v1: - id: amazon/nova-2-lite-v1 - input: 0.3 - output: 2.5 - original_provider: amazon - mistralai/ministral-14b-2512: - id: mistralai/ministral-14b-2512 - input: 0.2 - output: 0.2 - original_provider: mistralai - ministral-14b-2512: - id: mistralai/ministral-14b-2512 - input: 0.2 - output: 0.2 - original_provider: mistralai - mistralai/ministral-8b-2512: - id: mistralai/ministral-8b-2512 - input: 0.15 - output: 0.15 - original_provider: mistralai - ministral-8b-2512: - id: mistralai/ministral-8b-2512 - input: 0.15 - output: 0.15 - original_provider: mistralai - mistralai/ministral-3b-2512: - id: mistralai/ministral-3b-2512 - input: 0.1 - output: 0.1 - original_provider: mistralai - ministral-3b-2512: - id: mistralai/ministral-3b-2512 - input: 0.1 - output: 0.1 - original_provider: mistralai - mistralai/mistral-large-2512: - id: mistralai/mistral-large-2512 - input: 0.5 - output: 1.5 - original_provider: mistralai - mistral-large-2512: - id: mistralai/mistral-large-2512 - input: 0.5 - output: 1.5 - original_provider: mistralai - arcee-ai/trinity-mini:free: - id: arcee-ai/trinity-mini:free - input: 0 - output: 0 - original_provider: arcee-ai - trinity-mini:free: - id: arcee-ai/trinity-mini:free - input: 0 - output: 0 - original_provider: arcee-ai - arcee-ai/trinity-mini: - id: arcee-ai/trinity-mini - input: 0.045 - output: 0.15 - original_provider: arcee-ai - trinity-mini: - id: arcee-ai/trinity-mini - input: 0.045 - output: 0.15 - original_provider: arcee-ai - deepseek/deepseek-v3.2-speciale: - id: deepseek/deepseek-v3.2-speciale - input: 0.4 - output: 1.2 - original_provider: deepseek - deepseek-v3.2-speciale: - id: deepseek/deepseek-v3.2-speciale - input: 0.4 - output: 1.2 - original_provider: deepseek - deepseek/deepseek-v3.2: - id: deepseek/deepseek-v3.2 - input: 0.26 - output: 0.38 - original_provider: deepseek - deepseek-v3.2: - id: deepseek/deepseek-v3.2 - input: 0.26 - output: 0.38 - original_provider: deepseek - prime-intellect/intellect-3: - id: prime-intellect/intellect-3 - input: 0.2 - output: 1.1 - original_provider: prime-intellect - intellect-3: - id: prime-intellect/intellect-3 - input: 0.2 - output: 1.1 - original_provider: prime-intellect - anthropic/claude-opus-4.5: - id: anthropic/claude-opus-4.5 - input: 5 - output: 25 - original_provider: anthropic - claude-opus-4.5: - id: anthropic/claude-opus-4.5 - input: 5 - output: 25 - original_provider: anthropic - allenai/olmo-3-32b-think: - id: allenai/olmo-3-32b-think - input: 0.15 - output: 0.5 - original_provider: allenai - olmo-3-32b-think: - id: allenai/olmo-3-32b-think - input: 0.15 - output: 0.5 - original_provider: allenai - allenai/olmo-3-7b-instruct: - id: allenai/olmo-3-7b-instruct - input: 0.1 - output: 0.2 - original_provider: allenai - olmo-3-7b-instruct: - id: allenai/olmo-3-7b-instruct - input: 0.1 - output: 0.2 - original_provider: allenai - allenai/olmo-3-7b-think: - id: allenai/olmo-3-7b-think - input: 0.12 - output: 0.2 - original_provider: allenai - olmo-3-7b-think: - id: allenai/olmo-3-7b-think - input: 0.12 - output: 0.2 - original_provider: allenai - google/gemini-3-pro-image-preview: - id: google/gemini-3-pro-image-preview - input: 2 - output: 12 - original_provider: google - gemini-3-pro-image-preview: - id: google/gemini-3-pro-image-preview - input: 2 - output: 12 - original_provider: google - x-ai/grok-4.1-fast: - id: x-ai/grok-4.1-fast - input: 0.2 - output: 0.5 - original_provider: x-ai - grok-4.1-fast: - id: x-ai/grok-4.1-fast - input: 0.2 - output: 0.5 - original_provider: x-ai - google/gemini-3-pro-preview: - id: google/gemini-3-pro-preview - input: 2 - output: 12 - original_provider: google - gemini-3-pro-preview: - id: google/gemini-3-pro-preview - input: 2 - output: 12 - original_provider: google - deepcogito/cogito-v2.1-671b: - id: deepcogito/cogito-v2.1-671b - input: 1.25 - output: 1.25 - original_provider: deepcogito - cogito-v2.1-671b: - id: deepcogito/cogito-v2.1-671b - input: 1.25 - output: 1.25 - original_provider: deepcogito - openai/gpt-5.1: - id: openai/gpt-5.1 - input: 1.25 - output: 10 - original_provider: openai - gpt-5.1: - id: openai/gpt-5.1 - input: 1.25 - output: 10 - original_provider: openai - openai/gpt-5.1-chat: - id: openai/gpt-5.1-chat - input: 1.25 - output: 10 - original_provider: openai - gpt-5.1-chat: - id: openai/gpt-5.1-chat - input: 1.25 - output: 10 - original_provider: openai - openai/gpt-5.1-codex: - id: openai/gpt-5.1-codex - input: 1.25 - output: 10 - original_provider: openai - gpt-5.1-codex: - id: openai/gpt-5.1-codex - input: 1.25 - output: 10 - original_provider: openai - openai/gpt-5.1-codex-mini: - id: openai/gpt-5.1-codex-mini - input: 0.25 - output: 2 - original_provider: openai - gpt-5.1-codex-mini: - id: openai/gpt-5.1-codex-mini - input: 0.25 - output: 2 - original_provider: openai - kwaipilot/kat-coder-pro: - id: kwaipilot/kat-coder-pro - input: 0.207 - output: 0.828 - original_provider: kwaipilot - kat-coder-pro: - id: kwaipilot/kat-coder-pro - input: 0.207 - output: 0.828 - original_provider: kwaipilot - moonshotai/kimi-k2-thinking: - id: moonshotai/kimi-k2-thinking - input: 0.4 - output: 1.75 - original_provider: moonshotai - kimi-k2-thinking: - id: moonshotai/kimi-k2-thinking - input: 0.4 - output: 1.75 - original_provider: moonshotai - amazon/nova-premier-v1: - id: amazon/nova-premier-v1 - input: 2.5 - output: 12.5 - original_provider: amazon - nova-premier-v1: - id: amazon/nova-premier-v1 - input: 2.5 - output: 12.5 - original_provider: amazon - perplexity/sonar-pro-search: - id: perplexity/sonar-pro-search - input: 3 - output: 15 - original_provider: perplexity - sonar-pro-search: - id: perplexity/sonar-pro-search - input: 3 - output: 15 - original_provider: perplexity - mistralai/voxtral-small-24b-2507: - id: mistralai/voxtral-small-24b-2507 - input: 0.1 - output: 0.3 - original_provider: mistralai - voxtral-small-24b-2507: - id: mistralai/voxtral-small-24b-2507 - input: 0.1 - output: 0.3 - original_provider: mistralai - openai/gpt-oss-safeguard-20b: - id: openai/gpt-oss-safeguard-20b - input: 0.075 - output: 0.3 - original_provider: openai - gpt-oss-safeguard-20b: - id: openai/gpt-oss-safeguard-20b - input: 0.075 - output: 0.3 - original_provider: openai - nvidia/nemotron-nano-12b-v2-vl:free: - id: nvidia/nemotron-nano-12b-v2-vl:free - input: 0 - output: 0 - original_provider: nvidia - nemotron-nano-12b-v2-vl:free: - id: nvidia/nemotron-nano-12b-v2-vl:free - input: 0 - output: 0 - original_provider: nvidia - nvidia/nemotron-nano-12b-v2-vl: - id: nvidia/nemotron-nano-12b-v2-vl - input: 0.2 - output: 0.6 - original_provider: nvidia - nemotron-nano-12b-v2-vl: - id: nvidia/nemotron-nano-12b-v2-vl - input: 0.2 - output: 0.6 - original_provider: nvidia - minimax/minimax-m2: - id: minimax/minimax-m2 - input: 0.255 - output: 1 - original_provider: minimax - minimax-m2: - id: minimax/minimax-m2 - input: 0.255 - output: 1 - original_provider: minimax - qwen/qwen3-vl-32b-instruct: - id: qwen/qwen3-vl-32b-instruct - input: 0.104 - output: 0.416 - original_provider: qwen - qwen3-vl-32b-instruct: - id: qwen/qwen3-vl-32b-instruct - input: 0.104 - output: 0.416 - original_provider: qwen - liquid/lfm2-8b-a1b: - id: liquid/lfm2-8b-a1b - input: 0.01 - output: 0.02 - original_provider: liquid - lfm2-8b-a1b: - id: liquid/lfm2-8b-a1b - input: 0.01 - output: 0.02 - original_provider: liquid - liquid/lfm-2.2-6b: - id: liquid/lfm-2.2-6b - input: 0.01 - output: 0.02 - original_provider: liquid - lfm-2.2-6b: - id: liquid/lfm-2.2-6b - input: 0.01 - output: 0.02 - original_provider: liquid - ibm-granite/granite-4.0-h-micro: - id: ibm-granite/granite-4.0-h-micro - input: 0.017 - output: 0.11 - original_provider: ibm-granite - granite-4.0-h-micro: - id: ibm-granite/granite-4.0-h-micro - input: 0.017 - output: 0.11 - original_provider: ibm-granite - openai/gpt-5-image-mini: - id: openai/gpt-5-image-mini - input: 2.5 - output: 2 - original_provider: openai - gpt-5-image-mini: - id: openai/gpt-5-image-mini - input: 2.5 - output: 2 - original_provider: openai - anthropic/claude-haiku-4.5: - id: anthropic/claude-haiku-4.5 - input: 1 - output: 5 - original_provider: anthropic - claude-haiku-4.5: - id: anthropic/claude-haiku-4.5 - input: 1 - output: 5 - original_provider: anthropic - qwen/qwen3-vl-8b-thinking: - id: qwen/qwen3-vl-8b-thinking - input: 0.117 - output: 1.365 - original_provider: qwen - qwen3-vl-8b-thinking: - id: qwen/qwen3-vl-8b-thinking - input: 0.117 - output: 1.365 - original_provider: qwen - qwen/qwen3-vl-8b-instruct: - id: qwen/qwen3-vl-8b-instruct - input: 0.08 - output: 0.5 - original_provider: qwen - qwen3-vl-8b-instruct: - id: qwen/qwen3-vl-8b-instruct - input: 0.08 - output: 0.5 - original_provider: qwen - openai/gpt-5-image: - id: openai/gpt-5-image - input: 10 - output: 10 - original_provider: openai - gpt-5-image: - id: openai/gpt-5-image - input: 10 - output: 10 - original_provider: openai - openai/o3-deep-research: - id: openai/o3-deep-research - input: 10 - output: 40 - original_provider: openai - o3-deep-research: - id: openai/o3-deep-research - input: 10 - output: 40 - original_provider: openai - openai/o4-mini-deep-research: - id: openai/o4-mini-deep-research - input: 2 - output: 8 - original_provider: openai - o4-mini-deep-research: - id: openai/o4-mini-deep-research - input: 2 - output: 8 - original_provider: openai - nvidia/llama-3.3-nemotron-super-49b-v1.5: - id: nvidia/llama-3.3-nemotron-super-49b-v1.5 - input: 0.1 - output: 0.4 - original_provider: nvidia - llama-3.3-nemotron-super-49b-v1.5: - id: nvidia/llama-3.3-nemotron-super-49b-v1.5 - input: 0.1 - output: 0.4 - original_provider: nvidia - baidu/ernie-4.5-21b-a3b-thinking: - id: baidu/ernie-4.5-21b-a3b-thinking - input: 0.07 - output: 0.28 - original_provider: baidu - ernie-4.5-21b-a3b-thinking: - id: baidu/ernie-4.5-21b-a3b-thinking - input: 0.07 - output: 0.28 - original_provider: baidu - google/gemini-2.5-flash-image: - id: google/gemini-2.5-flash-image - input: 0.3 - output: 2.5 - original_provider: google - gemini-2.5-flash-image: - id: google/gemini-2.5-flash-image - input: 0.3 - output: 2.5 - original_provider: google - qwen/qwen3-vl-30b-a3b-thinking: - id: qwen/qwen3-vl-30b-a3b-thinking - input: 0 - output: 0 - original_provider: qwen - qwen3-vl-30b-a3b-thinking: - id: qwen/qwen3-vl-30b-a3b-thinking - input: 0 - output: 0 - original_provider: qwen - qwen/qwen3-vl-30b-a3b-instruct: - id: qwen/qwen3-vl-30b-a3b-instruct - input: 0.13 - output: 0.52 - original_provider: qwen - qwen3-vl-30b-a3b-instruct: - id: qwen/qwen3-vl-30b-a3b-instruct - input: 0.13 - output: 0.52 - original_provider: qwen - openai/gpt-5-pro: - id: openai/gpt-5-pro - input: 15 - output: 120 - original_provider: openai - gpt-5-pro: - id: openai/gpt-5-pro - input: 15 - output: 120 - original_provider: openai - z-ai/glm-4.6: - id: z-ai/glm-4.6 - input: 0.35 - output: 1.71 - original_provider: z-ai - glm-4.6: - id: z-ai/glm-4.6 - input: 0.35 - output: 1.71 - original_provider: z-ai - z-ai/glm-4.6:exacto: - id: z-ai/glm-4.6:exacto - input: 0.44 - output: 1.76 - original_provider: z-ai - glm-4.6:exacto: - id: z-ai/glm-4.6:exacto - input: 0.44 - output: 1.76 - original_provider: z-ai - anthropic/claude-sonnet-4.5: - id: anthropic/claude-sonnet-4.5 - input: 3 - output: 15 - original_provider: anthropic - claude-sonnet-4.5: - id: anthropic/claude-sonnet-4.5 - input: 3 - output: 15 - original_provider: anthropic - deepseek/deepseek-v3.2-exp: - id: deepseek/deepseek-v3.2-exp - input: 0.27 - output: 0.41 - original_provider: deepseek - deepseek-v3.2-exp: - id: deepseek/deepseek-v3.2-exp - input: 0.27 - output: 0.41 - original_provider: deepseek - thedrummer/cydonia-24b-v4.1: - id: thedrummer/cydonia-24b-v4.1 - input: 0.3 - output: 0.5 - original_provider: thedrummer - cydonia-24b-v4.1: - id: thedrummer/cydonia-24b-v4.1 - input: 0.3 - output: 0.5 - original_provider: thedrummer - relace/relace-apply-3: - id: relace/relace-apply-3 - input: 0.85 - output: 1.25 - original_provider: relace - relace-apply-3: - id: relace/relace-apply-3 - input: 0.85 - output: 1.25 - original_provider: relace - google/gemini-2.5-flash-lite-preview-09-2025: - id: google/gemini-2.5-flash-lite-preview-09-2025 - input: 0.1 - output: 0.4 - original_provider: google - gemini-2.5-flash-lite-preview-09-2025: - id: google/gemini-2.5-flash-lite-preview-09-2025 - input: 0.1 - output: 0.4 - original_provider: google - qwen/qwen3-vl-235b-a22b-thinking: - id: qwen/qwen3-vl-235b-a22b-thinking - input: 0 - output: 0 - original_provider: qwen - qwen3-vl-235b-a22b-thinking: - id: qwen/qwen3-vl-235b-a22b-thinking - input: 0 - output: 0 - original_provider: qwen - qwen/qwen3-vl-235b-a22b-instruct: - id: qwen/qwen3-vl-235b-a22b-instruct - input: 0.2 - output: 0.88 - original_provider: qwen - qwen3-vl-235b-a22b-instruct: - id: qwen/qwen3-vl-235b-a22b-instruct - input: 0.2 - output: 0.88 - original_provider: qwen - qwen/qwen3-max: - id: qwen/qwen3-max - input: 1.2 - output: 6 - original_provider: qwen - qwen3-max: - id: qwen/qwen3-max - input: 1.2 - output: 6 - original_provider: qwen - qwen/qwen3-coder-plus: - id: qwen/qwen3-coder-plus - input: 1 - output: 5 - original_provider: qwen - qwen3-coder-plus: - id: qwen/qwen3-coder-plus - input: 1 - output: 5 - original_provider: qwen - openai/gpt-5-codex: - id: openai/gpt-5-codex - input: 1.25 - output: 10 - original_provider: openai - gpt-5-codex: - id: openai/gpt-5-codex - input: 1.25 - output: 10 - original_provider: openai - deepseek/deepseek-v3.1-terminus:exacto: - id: deepseek/deepseek-v3.1-terminus:exacto - input: 0.21 - output: 0.79 - original_provider: deepseek - deepseek-v3.1-terminus:exacto: - id: deepseek/deepseek-v3.1-terminus:exacto - input: 0.21 - output: 0.79 - original_provider: deepseek - deepseek/deepseek-v3.1-terminus: - id: deepseek/deepseek-v3.1-terminus - input: 0.21 - output: 0.79 - original_provider: deepseek - deepseek-v3.1-terminus: - id: deepseek/deepseek-v3.1-terminus - input: 0.21 - output: 0.79 - original_provider: deepseek - x-ai/grok-4-fast: - id: x-ai/grok-4-fast - input: 0.2 - output: 0.5 - original_provider: x-ai - grok-4-fast: - id: x-ai/grok-4-fast - input: 0.2 - output: 0.5 - original_provider: x-ai - alibaba/tongyi-deepresearch-30b-a3b: - id: alibaba/tongyi-deepresearch-30b-a3b - input: 0.09 - output: 0.45 - original_provider: alibaba - tongyi-deepresearch-30b-a3b: - id: alibaba/tongyi-deepresearch-30b-a3b - input: 0.09 - output: 0.45 - original_provider: alibaba - qwen/qwen3-coder-flash: - id: qwen/qwen3-coder-flash - input: 0.3 - output: 1.5 - original_provider: qwen - qwen3-coder-flash: - id: qwen/qwen3-coder-flash - input: 0.3 - output: 1.5 - original_provider: qwen - opengvlab/internvl3-78b: - id: opengvlab/internvl3-78b - input: 0.15 - output: 0.6 - original_provider: opengvlab - internvl3-78b: - id: opengvlab/internvl3-78b - input: 0.15 - output: 0.6 - original_provider: opengvlab - qwen/qwen3-next-80b-a3b-thinking: - id: qwen/qwen3-next-80b-a3b-thinking - input: 0.15 - output: 1.2 - original_provider: qwen - qwen3-next-80b-a3b-thinking: - id: qwen/qwen3-next-80b-a3b-thinking - input: 0.15 - output: 1.2 - original_provider: qwen - qwen/qwen3-next-80b-a3b-instruct:free: - id: qwen/qwen3-next-80b-a3b-instruct:free - input: 0 - output: 0 - original_provider: qwen - qwen3-next-80b-a3b-instruct:free: - id: qwen/qwen3-next-80b-a3b-instruct:free - input: 0 - output: 0 - original_provider: qwen - qwen/qwen3-next-80b-a3b-instruct: - id: qwen/qwen3-next-80b-a3b-instruct - input: 0.09 - output: 1.1 - original_provider: qwen - qwen3-next-80b-a3b-instruct: - id: qwen/qwen3-next-80b-a3b-instruct - input: 0.09 - output: 1.1 - original_provider: qwen - meituan/longcat-flash-chat: - id: meituan/longcat-flash-chat - input: 0.2 - output: 0.8 - original_provider: meituan - longcat-flash-chat: - id: meituan/longcat-flash-chat - input: 0.2 - output: 0.8 - original_provider: meituan - qwen/qwen-plus-2025-07-28: - id: qwen/qwen-plus-2025-07-28 - input: 0.4 - output: 1.2 - original_provider: qwen - qwen-plus-2025-07-28: - id: qwen/qwen-plus-2025-07-28 - input: 0.4 - output: 1.2 - original_provider: qwen - qwen/qwen-plus-2025-07-28:thinking: - id: qwen/qwen-plus-2025-07-28:thinking - input: 0.4 - output: 1.2 - original_provider: qwen - qwen-plus-2025-07-28:thinking: - id: qwen/qwen-plus-2025-07-28:thinking - input: 0.4 - output: 1.2 - original_provider: qwen - nvidia/nemotron-nano-9b-v2:free: - id: nvidia/nemotron-nano-9b-v2:free - input: 0 - output: 0 - original_provider: nvidia - nemotron-nano-9b-v2:free: - id: nvidia/nemotron-nano-9b-v2:free - input: 0 - output: 0 - original_provider: nvidia - nvidia/nemotron-nano-9b-v2: - id: nvidia/nemotron-nano-9b-v2 - input: 0.04 - output: 0.16 - original_provider: nvidia - nemotron-nano-9b-v2: - id: nvidia/nemotron-nano-9b-v2 - input: 0.04 - output: 0.16 - original_provider: nvidia - moonshotai/kimi-k2-0905: - id: moonshotai/kimi-k2-0905 - input: 0.4 - output: 2 - original_provider: moonshotai - kimi-k2-0905: - id: moonshotai/kimi-k2-0905 - input: 0.4 - output: 2 - original_provider: moonshotai - moonshotai/kimi-k2-0905:exacto: - id: moonshotai/kimi-k2-0905:exacto - input: 0.6 - output: 2.5 - original_provider: moonshotai - kimi-k2-0905:exacto: - id: moonshotai/kimi-k2-0905:exacto - input: 0.6 - output: 2.5 - original_provider: moonshotai - qwen/qwen3-30b-a3b-thinking-2507: - id: qwen/qwen3-30b-a3b-thinking-2507 - input: 0.051 - output: 0.34 - original_provider: qwen - qwen3-30b-a3b-thinking-2507: - id: qwen/qwen3-30b-a3b-thinking-2507 - input: 0.051 - output: 0.34 - original_provider: qwen - x-ai/grok-code-fast-1: - id: x-ai/grok-code-fast-1 - input: 0.2 - output: 1.5 - original_provider: x-ai - grok-code-fast-1: - id: x-ai/grok-code-fast-1 - input: 0.2 - output: 1.5 - original_provider: x-ai - nousresearch/hermes-4-70b: - id: nousresearch/hermes-4-70b - input: 0.13 - output: 0.4 - original_provider: nousresearch - hermes-4-70b: - id: nousresearch/hermes-4-70b - input: 0.13 - output: 0.4 - original_provider: nousresearch - nousresearch/hermes-4-405b: - id: nousresearch/hermes-4-405b - input: 1 - output: 3 - original_provider: nousresearch - hermes-4-405b: - id: nousresearch/hermes-4-405b - input: 1 - output: 3 - original_provider: nousresearch - deepseek/deepseek-chat-v3.1: - id: deepseek/deepseek-chat-v3.1 - input: 0.15 - output: 0.75 - original_provider: deepseek - deepseek-chat-v3.1: - id: deepseek/deepseek-chat-v3.1 - input: 0.15 - output: 0.75 - original_provider: deepseek - openai/gpt-4o-audio-preview: - id: openai/gpt-4o-audio-preview - input: 2.5 - output: 10 - original_provider: openai - gpt-4o-audio-preview: - id: openai/gpt-4o-audio-preview - input: 2.5 - output: 10 - original_provider: openai - mistralai/mistral-medium-3.1: - id: mistralai/mistral-medium-3.1 - input: 0.4 - output: 2 - original_provider: mistralai - mistral-medium-3.1: - id: mistralai/mistral-medium-3.1 - input: 0.4 - output: 2 - original_provider: mistralai - baidu/ernie-4.5-21b-a3b: - id: baidu/ernie-4.5-21b-a3b - input: 0.07 - output: 0.28 - original_provider: baidu - ernie-4.5-21b-a3b: - id: baidu/ernie-4.5-21b-a3b - input: 0.07 - output: 0.28 - original_provider: baidu - baidu/ernie-4.5-vl-28b-a3b: - id: baidu/ernie-4.5-vl-28b-a3b - input: 0.14 - output: 0.56 - original_provider: baidu - ernie-4.5-vl-28b-a3b: - id: baidu/ernie-4.5-vl-28b-a3b - input: 0.14 - output: 0.56 - original_provider: baidu - z-ai/glm-4.5v: - id: z-ai/glm-4.5v - input: 0.6 - output: 1.8 - original_provider: z-ai - glm-4.5v: - id: z-ai/glm-4.5v - input: 0.6 - output: 1.8 - original_provider: z-ai - ai21/jamba-large-1.7: - id: ai21/jamba-large-1.7 - input: 2 - output: 8 - original_provider: ai21 - jamba-large-1.7: - id: ai21/jamba-large-1.7 - input: 2 - output: 8 - original_provider: ai21 - openai/gpt-5-chat: - id: openai/gpt-5-chat - input: 1.25 - output: 10 - original_provider: openai - gpt-5-chat: - id: openai/gpt-5-chat - input: 1.25 - output: 10 - original_provider: openai - openai/gpt-5: - id: openai/gpt-5 - input: 1.25 - output: 10 - original_provider: openai - gpt-5: - id: openai/gpt-5 - input: 1.25 - output: 10 - original_provider: openai - openai/gpt-5-mini: - id: openai/gpt-5-mini - input: 0.25 - output: 2 - original_provider: openai - gpt-5-mini: - id: openai/gpt-5-mini - input: 0.25 - output: 2 - original_provider: openai - openai/gpt-5-nano: - id: openai/gpt-5-nano - input: 0.05 - output: 0.4 - original_provider: openai - gpt-5-nano: - id: openai/gpt-5-nano - input: 0.05 - output: 0.4 - original_provider: openai - openai/gpt-oss-120b:free: - id: openai/gpt-oss-120b:free - input: 0 - output: 0 - original_provider: openai - gpt-oss-120b:free: - id: openai/gpt-oss-120b:free - input: 0 - output: 0 - original_provider: openai - openai/gpt-oss-120b: - id: openai/gpt-oss-120b - input: 0.039 - output: 0.19 - original_provider: openai - gpt-oss-120b: - id: openai/gpt-oss-120b - input: 0.039 - output: 0.19 - original_provider: openai - openai/gpt-oss-120b:exacto: - id: openai/gpt-oss-120b:exacto - input: 0.039 - output: 0.19 - original_provider: openai - gpt-oss-120b:exacto: - id: openai/gpt-oss-120b:exacto - input: 0.039 - output: 0.19 - original_provider: openai - openai/gpt-oss-20b:free: - id: openai/gpt-oss-20b:free - input: 0 - output: 0 - original_provider: openai - gpt-oss-20b:free: - id: openai/gpt-oss-20b:free - input: 0 - output: 0 - original_provider: openai - openai/gpt-oss-20b: - id: openai/gpt-oss-20b - input: 0.03 - output: 0.14 - original_provider: openai - gpt-oss-20b: - id: openai/gpt-oss-20b - input: 0.03 - output: 0.14 - original_provider: openai - anthropic/claude-opus-4.1: - id: anthropic/claude-opus-4.1 - input: 15 - output: 75 - original_provider: anthropic - claude-opus-4.1: - id: anthropic/claude-opus-4.1 - input: 15 - output: 75 - original_provider: anthropic - mistralai/codestral-2508: - id: mistralai/codestral-2508 - input: 0.3 - output: 0.9 - original_provider: mistralai - codestral-2508: - id: mistralai/codestral-2508 - input: 0.3 - output: 0.9 - original_provider: mistralai - qwen/qwen3-coder-30b-a3b-instruct: - id: qwen/qwen3-coder-30b-a3b-instruct - input: 0.07 - output: 0.27 - original_provider: qwen - qwen3-coder-30b-a3b-instruct: - id: qwen/qwen3-coder-30b-a3b-instruct - input: 0.07 - output: 0.27 - original_provider: qwen - qwen/qwen3-30b-a3b-instruct-2507: - id: qwen/qwen3-30b-a3b-instruct-2507 - input: 0.09 - output: 0.3 - original_provider: qwen - qwen3-30b-a3b-instruct-2507: - id: qwen/qwen3-30b-a3b-instruct-2507 - input: 0.09 - output: 0.3 - original_provider: qwen - z-ai/glm-4.5: - id: z-ai/glm-4.5 - input: 0.55 - output: 2 - original_provider: z-ai - glm-4.5: - id: z-ai/glm-4.5 - input: 0.55 - output: 2 - original_provider: z-ai - z-ai/glm-4.5-air:free: - id: z-ai/glm-4.5-air:free - input: 0 - output: 0 - original_provider: z-ai - glm-4.5-air:free: - id: z-ai/glm-4.5-air:free - input: 0 - output: 0 - original_provider: z-ai - z-ai/glm-4.5-air: - id: z-ai/glm-4.5-air - input: 0.13 - output: 0.85 - original_provider: z-ai - glm-4.5-air: - id: z-ai/glm-4.5-air - input: 0.13 - output: 0.85 - original_provider: z-ai - qwen/qwen3-235b-a22b-thinking-2507: - id: qwen/qwen3-235b-a22b-thinking-2507 - input: 0 - output: 0 - original_provider: qwen - qwen3-235b-a22b-thinking-2507: - id: qwen/qwen3-235b-a22b-thinking-2507 - input: 0 - output: 0 - original_provider: qwen - z-ai/glm-4-32b: - id: z-ai/glm-4-32b - input: 0.1 - output: 0.1 - original_provider: z-ai - glm-4-32b: - id: z-ai/glm-4-32b - input: 0.1 - output: 0.1 - original_provider: z-ai - qwen/qwen3-coder:free: - id: qwen/qwen3-coder:free - input: 0 - output: 0 - original_provider: qwen - qwen3-coder:free: - id: qwen/qwen3-coder:free - input: 0 - output: 0 - original_provider: qwen - qwen/qwen3-coder: - id: qwen/qwen3-coder - input: 0.22 - output: 1 - original_provider: qwen - qwen3-coder: - id: qwen/qwen3-coder - input: 0.22 - output: 1 - original_provider: qwen - qwen/qwen3-coder:exacto: - id: qwen/qwen3-coder:exacto - input: 0.22 - output: 1.8 - original_provider: qwen - qwen3-coder:exacto: - id: qwen/qwen3-coder:exacto - input: 0.22 - output: 1.8 - original_provider: qwen - bytedance/ui-tars-1.5-7b: - id: bytedance/ui-tars-1.5-7b - input: 0.1 - output: 0.2 - original_provider: bytedance - ui-tars-1.5-7b: - id: bytedance/ui-tars-1.5-7b - input: 0.1 - output: 0.2 - original_provider: bytedance - google/gemini-2.5-flash-lite: - id: google/gemini-2.5-flash-lite - input: 0.1 - output: 0.4 - original_provider: google - gemini-2.5-flash-lite: - id: google/gemini-2.5-flash-lite - input: 0.1 - output: 0.4 - original_provider: google - qwen/qwen3-235b-a22b-2507: - id: qwen/qwen3-235b-a22b-2507 - input: 0.071 - output: 0.1 - original_provider: qwen - qwen3-235b-a22b-2507: - id: qwen/qwen3-235b-a22b-2507 - input: 0.071 - output: 0.1 - original_provider: qwen - switchpoint/router: - id: switchpoint/router - input: 0.85 - output: 3.4 - original_provider: switchpoint - router: - id: switchpoint/router - input: 0.85 - output: 3.4 - original_provider: switchpoint - moonshotai/kimi-k2: - id: moonshotai/kimi-k2 - input: 0.5 - output: 2.4 - original_provider: moonshotai - kimi-k2: - id: moonshotai/kimi-k2 - input: 0.5 - output: 2.4 - original_provider: moonshotai - mistralai/devstral-medium: - id: mistralai/devstral-medium - input: 0.4 - output: 2 - original_provider: mistralai - devstral-medium: - id: mistralai/devstral-medium - input: 0.4 - output: 2 - original_provider: mistralai - mistralai/devstral-small: - id: mistralai/devstral-small - input: 0.1 - output: 0.3 - original_provider: mistralai - devstral-small: - id: mistralai/devstral-small - input: 0.1 - output: 0.3 - original_provider: mistralai - cognitivecomputations/dolphin-mistral-24b-venice-edition:free: - id: cognitivecomputations/dolphin-mistral-24b-venice-edition:free - input: 0 - output: 0 - original_provider: cognitivecomputations - dolphin-mistral-24b-venice-edition:free: - id: cognitivecomputations/dolphin-mistral-24b-venice-edition:free - input: 0 - output: 0 - original_provider: cognitivecomputations - x-ai/grok-4: - id: x-ai/grok-4 - input: 3 - output: 15 - original_provider: x-ai - grok-4: - id: x-ai/grok-4 - input: 3 - output: 15 - original_provider: x-ai - google/gemma-3n-e2b-it:free: - id: google/gemma-3n-e2b-it:free - input: 0 - output: 0 - original_provider: google - gemma-3n-e2b-it:free: - id: google/gemma-3n-e2b-it:free - input: 0 - output: 0 - original_provider: google - tencent/hunyuan-a13b-instruct: - id: tencent/hunyuan-a13b-instruct - input: 0.14 - output: 0.57 - original_provider: tencent - hunyuan-a13b-instruct: - id: tencent/hunyuan-a13b-instruct - input: 0.14 - output: 0.57 - original_provider: tencent - tngtech/deepseek-r1t2-chimera: - id: tngtech/deepseek-r1t2-chimera - input: 0.25 - output: 0.85 - original_provider: tngtech - deepseek-r1t2-chimera: - id: tngtech/deepseek-r1t2-chimera - input: 0.25 - output: 0.85 - original_provider: tngtech - morph/morph-v3-large: - id: morph/morph-v3-large - input: 0.9 - output: 1.9 - original_provider: morph - morph-v3-large: - id: morph/morph-v3-large - input: 0.9 - output: 1.9 - original_provider: morph - morph/morph-v3-fast: - id: morph/morph-v3-fast - input: 0.8 - output: 1.2 - original_provider: morph - morph-v3-fast: - id: morph/morph-v3-fast - input: 0.8 - output: 1.2 - original_provider: morph - baidu/ernie-4.5-vl-424b-a47b: - id: baidu/ernie-4.5-vl-424b-a47b - input: 0.42 - output: 1.25 - original_provider: baidu - ernie-4.5-vl-424b-a47b: - id: baidu/ernie-4.5-vl-424b-a47b - input: 0.42 - output: 1.25 - original_provider: baidu - baidu/ernie-4.5-300b-a47b: - id: baidu/ernie-4.5-300b-a47b - input: 0.28 - output: 1.1 - original_provider: baidu - ernie-4.5-300b-a47b: - id: baidu/ernie-4.5-300b-a47b - input: 0.28 - output: 1.1 - original_provider: baidu - inception/mercury: - id: inception/mercury - input: 0.25 - output: 1 - original_provider: inception - mercury: - id: inception/mercury - input: 0.25 - output: 1 - original_provider: inception - mistralai/mistral-small-3.2-24b-instruct: - id: mistralai/mistral-small-3.2-24b-instruct - input: 0.06 - output: 0.18 - original_provider: mistralai - mistral-small-3.2-24b-instruct: - id: mistralai/mistral-small-3.2-24b-instruct - input: 0.06 - output: 0.18 - original_provider: mistralai - minimax/minimax-m1: - id: minimax/minimax-m1 - input: 0.4 - output: 2.2 - original_provider: minimax - minimax-m1: - id: minimax/minimax-m1 - input: 0.4 - output: 2.2 - original_provider: minimax - google/gemini-2.5-flash: - id: google/gemini-2.5-flash - input: 0.3 - output: 2.5 - original_provider: google - gemini-2.5-flash: - id: google/gemini-2.5-flash - input: 0.3 - output: 2.5 - original_provider: google - google/gemini-2.5-pro: - id: google/gemini-2.5-pro - input: 1.25 - output: 10 - original_provider: google - gemini-2.5-pro: - id: google/gemini-2.5-pro - input: 1.25 - output: 10 - original_provider: google - openai/o3-pro: - id: openai/o3-pro - input: 20 - output: 80 - original_provider: openai - o3-pro: - id: openai/o3-pro - input: 20 - output: 80 - original_provider: openai - x-ai/grok-3-mini: - id: x-ai/grok-3-mini - input: 0.3 - output: 0.5 - original_provider: x-ai - grok-3-mini: - id: x-ai/grok-3-mini - input: 0.3 - output: 0.5 - original_provider: x-ai - x-ai/grok-3: - id: x-ai/grok-3 - input: 3 - output: 15 - original_provider: x-ai - grok-3: - id: x-ai/grok-3 - input: 3 - output: 15 - original_provider: x-ai - google/gemini-2.5-pro-preview: - id: google/gemini-2.5-pro-preview - input: 1.25 - output: 10 - original_provider: google - gemini-2.5-pro-preview: - id: google/gemini-2.5-pro-preview - input: 1.25 - output: 10 - original_provider: google - deepseek/deepseek-r1-0528:free: - id: deepseek/deepseek-r1-0528:free - input: 0 - output: 0 - original_provider: deepseek - deepseek-r1-0528:free: - id: deepseek/deepseek-r1-0528:free - input: 0 - output: 0 - original_provider: deepseek - deepseek/deepseek-r1-0528: - id: deepseek/deepseek-r1-0528 - input: 0.4 - output: 1.75 - original_provider: deepseek - deepseek-r1-0528: - id: deepseek/deepseek-r1-0528 - input: 0.4 - output: 1.75 - original_provider: deepseek - anthropic/claude-opus-4: - id: anthropic/claude-opus-4 - input: 15 - output: 75 - original_provider: anthropic - claude-opus-4: - id: anthropic/claude-opus-4 - input: 15 - output: 75 - original_provider: anthropic - anthropic/claude-sonnet-4: - id: anthropic/claude-sonnet-4 - input: 3 - output: 15 - original_provider: anthropic - claude-sonnet-4: - id: anthropic/claude-sonnet-4 - input: 3 - output: 15 - original_provider: anthropic - google/gemma-3n-e4b-it:free: - id: google/gemma-3n-e4b-it:free - input: 0 - output: 0 - original_provider: google - gemma-3n-e4b-it:free: - id: google/gemma-3n-e4b-it:free - input: 0 - output: 0 - original_provider: google - google/gemma-3n-e4b-it: - id: google/gemma-3n-e4b-it - input: 0.02 - output: 0.04 - original_provider: google - gemma-3n-e4b-it: - id: google/gemma-3n-e4b-it - input: 0.02 - output: 0.04 - original_provider: google - mistralai/mistral-medium-3: - id: mistralai/mistral-medium-3 - input: 0.4 - output: 2 - original_provider: mistralai - mistral-medium-3: - id: mistralai/mistral-medium-3 - input: 0.4 - output: 2 - original_provider: mistralai - google/gemini-2.5-pro-preview-05-06: - id: google/gemini-2.5-pro-preview-05-06 - input: 1.25 - output: 10 - original_provider: google - gemini-2.5-pro-preview-05-06: - id: google/gemini-2.5-pro-preview-05-06 - input: 1.25 - output: 10 - original_provider: google - arcee-ai/spotlight: - id: arcee-ai/spotlight - input: 0.18 - output: 0.18 - original_provider: arcee-ai - spotlight: - id: arcee-ai/spotlight - input: 0.18 - output: 0.18 - original_provider: arcee-ai - arcee-ai/maestro-reasoning: - id: arcee-ai/maestro-reasoning - input: 0.9 - output: 3.3 - original_provider: arcee-ai - maestro-reasoning: - id: arcee-ai/maestro-reasoning - input: 0.9 - output: 3.3 - original_provider: arcee-ai - arcee-ai/virtuoso-large: - id: arcee-ai/virtuoso-large - input: 0.75 - output: 1.2 - original_provider: arcee-ai - virtuoso-large: - id: arcee-ai/virtuoso-large - input: 0.75 - output: 1.2 - original_provider: arcee-ai - arcee-ai/coder-large: - id: arcee-ai/coder-large - input: 0.5 - output: 0.8 - original_provider: arcee-ai - coder-large: - id: arcee-ai/coder-large - input: 0.5 - output: 0.8 - original_provider: arcee-ai - inception/mercury-coder: - id: inception/mercury-coder - input: 0.25 - output: 1 - original_provider: inception - mercury-coder: - id: inception/mercury-coder - input: 0.25 - output: 1 - original_provider: inception - qwen/qwen3-4b:free: - id: qwen/qwen3-4b:free - input: 0 - output: 0 - original_provider: qwen - qwen3-4b:free: - id: qwen/qwen3-4b:free - input: 0 - output: 0 - original_provider: qwen - qwen/qwen3-4b: - id: qwen/qwen3-4b - input: 0.0715 - output: 0.273 - original_provider: qwen - qwen3-4b: - id: qwen/qwen3-4b - input: 0.0715 - output: 0.273 - original_provider: qwen - meta-llama/llama-guard-4-12b: - id: meta-llama/llama-guard-4-12b - input: 0.18 - output: 0.18 - original_provider: meta-llama - llama-guard-4-12b: - id: meta-llama/llama-guard-4-12b - input: 0.18 - output: 0.18 - original_provider: meta-llama - qwen/qwen3-30b-a3b: - id: qwen/qwen3-30b-a3b - input: 0.08 - output: 0.28 - original_provider: qwen - qwen3-30b-a3b: - id: qwen/qwen3-30b-a3b - input: 0.08 - output: 0.28 - original_provider: qwen - qwen/qwen3-8b: - id: qwen/qwen3-8b - input: 0.05 - output: 0.4 - original_provider: qwen - qwen3-8b: - id: qwen/qwen3-8b - input: 0.05 - output: 0.4 - original_provider: qwen - qwen/qwen3-14b: - id: qwen/qwen3-14b - input: 0.06 - output: 0.24 - original_provider: qwen - qwen3-14b: - id: qwen/qwen3-14b - input: 0.06 - output: 0.24 - original_provider: qwen - qwen/qwen3-32b: - id: qwen/qwen3-32b - input: 0.08 - output: 0.24 - original_provider: qwen - qwen3-32b: - id: qwen/qwen3-32b - input: 0.08 - output: 0.24 - original_provider: qwen - qwen/qwen3-235b-a22b: - id: qwen/qwen3-235b-a22b - input: 0.455 - output: 1.82 - original_provider: qwen - qwen3-235b-a22b: - id: qwen/qwen3-235b-a22b - input: 0.455 - output: 1.82 - original_provider: qwen - tngtech/deepseek-r1t-chimera: - id: tngtech/deepseek-r1t-chimera - input: 0.3 - output: 1.2 - original_provider: tngtech - deepseek-r1t-chimera: - id: tngtech/deepseek-r1t-chimera - input: 0.3 - output: 1.2 - original_provider: tngtech - openai/o4-mini-high: - id: openai/o4-mini-high - input: 1.1 - output: 4.4 - original_provider: openai - o4-mini-high: - id: openai/o4-mini-high - input: 1.1 - output: 4.4 - original_provider: openai - openai/o3: - id: openai/o3 - input: 2 - output: 8 - original_provider: openai - o3: - id: openai/o3 - input: 2 - output: 8 - original_provider: openai - openai/o4-mini: - id: openai/o4-mini - input: 1.1 - output: 4.4 - original_provider: openai - o4-mini: - id: openai/o4-mini - input: 1.1 - output: 4.4 - original_provider: openai - qwen/qwen2.5-coder-7b-instruct: - id: qwen/qwen2.5-coder-7b-instruct - input: 0.03 - output: 0.09 - original_provider: qwen - qwen2.5-coder-7b-instruct: - id: qwen/qwen2.5-coder-7b-instruct - input: 0.03 - output: 0.09 - original_provider: qwen - openai/gpt-4.1: - id: openai/gpt-4.1 - input: 2 - output: 8 - original_provider: openai - gpt-4.1: - id: openai/gpt-4.1 - input: 2 - output: 8 - original_provider: openai - openai/gpt-4.1-mini: - id: openai/gpt-4.1-mini - input: 0.4 - output: 1.6 - original_provider: openai - gpt-4.1-mini: - id: openai/gpt-4.1-mini - input: 0.4 - output: 1.6 - original_provider: openai - openai/gpt-4.1-nano: - id: openai/gpt-4.1-nano - input: 0.1 - output: 0.4 - original_provider: openai - gpt-4.1-nano: - id: openai/gpt-4.1-nano - input: 0.1 - output: 0.4 - original_provider: openai - eleutherai/llemma_7b: - id: eleutherai/llemma_7b - input: 0.8 - output: 1.2 - original_provider: eleutherai - llemma_7b: - id: eleutherai/llemma_7b - input: 0.8 - output: 1.2 - original_provider: eleutherai - alfredpros/codellama-7b-instruct-solidity: - id: alfredpros/codellama-7b-instruct-solidity - input: 0.8 - output: 1.2 - original_provider: alfredpros - codellama-7b-instruct-solidity: - id: alfredpros/codellama-7b-instruct-solidity - input: 0.8 - output: 1.2 - original_provider: alfredpros - x-ai/grok-3-mini-beta: - id: x-ai/grok-3-mini-beta - input: 0.3 - output: 0.5 - original_provider: x-ai - grok-3-mini-beta: - id: x-ai/grok-3-mini-beta - input: 0.3 - output: 0.5 - original_provider: x-ai - x-ai/grok-3-beta: - id: x-ai/grok-3-beta - input: 3 - output: 15 - original_provider: x-ai - grok-3-beta: - id: x-ai/grok-3-beta - input: 3 - output: 15 - original_provider: x-ai - nvidia/llama-3.1-nemotron-ultra-253b-v1: - id: nvidia/llama-3.1-nemotron-ultra-253b-v1 - input: 0.6 - output: 1.8 - original_provider: nvidia - llama-3.1-nemotron-ultra-253b-v1: - id: nvidia/llama-3.1-nemotron-ultra-253b-v1 - input: 0.6 - output: 1.8 - original_provider: nvidia - meta-llama/llama-4-maverick: - id: meta-llama/llama-4-maverick - input: 0.15 - output: 0.6 - original_provider: meta-llama - llama-4-maverick: - id: meta-llama/llama-4-maverick - input: 0.15 - output: 0.6 - original_provider: meta-llama - meta-llama/llama-4-scout: - id: meta-llama/llama-4-scout - input: 0.08 - output: 0.3 - original_provider: meta-llama - llama-4-scout: - id: meta-llama/llama-4-scout - input: 0.08 - output: 0.3 - original_provider: meta-llama - qwen/qwen2.5-vl-32b-instruct: - id: qwen/qwen2.5-vl-32b-instruct - input: 0.2 - output: 0.6 - original_provider: qwen - qwen2.5-vl-32b-instruct: - id: qwen/qwen2.5-vl-32b-instruct - input: 0.2 - output: 0.6 - original_provider: qwen - deepseek/deepseek-chat-v3-0324: - id: deepseek/deepseek-chat-v3-0324 - input: 0.19 - output: 0.87 - original_provider: deepseek - deepseek-chat-v3-0324: - id: deepseek/deepseek-chat-v3-0324 - input: 0.19 - output: 0.87 - original_provider: deepseek - openai/o1-pro: - id: openai/o1-pro - input: 150 - output: 600 - original_provider: openai - o1-pro: - id: openai/o1-pro - input: 150 - output: 600 - original_provider: openai - mistralai/mistral-small-3.1-24b-instruct:free: - id: mistralai/mistral-small-3.1-24b-instruct:free - input: 0 - output: 0 - original_provider: mistralai - mistral-small-3.1-24b-instruct:free: - id: mistralai/mistral-small-3.1-24b-instruct:free - input: 0 - output: 0 - original_provider: mistralai - mistralai/mistral-small-3.1-24b-instruct: - id: mistralai/mistral-small-3.1-24b-instruct - input: 0.35 - output: 0.56 - original_provider: mistralai - mistral-small-3.1-24b-instruct: - id: mistralai/mistral-small-3.1-24b-instruct - input: 0.35 - output: 0.56 - original_provider: mistralai - allenai/olmo-2-0325-32b-instruct: - id: allenai/olmo-2-0325-32b-instruct - input: 0.05 - output: 0.2 - original_provider: allenai - olmo-2-0325-32b-instruct: - id: allenai/olmo-2-0325-32b-instruct - input: 0.05 - output: 0.2 - original_provider: allenai - google/gemma-3-4b-it:free: - id: google/gemma-3-4b-it:free - input: 0 - output: 0 - original_provider: google - gemma-3-4b-it:free: - id: google/gemma-3-4b-it:free - input: 0 - output: 0 - original_provider: google - google/gemma-3-4b-it: - id: google/gemma-3-4b-it - input: 0.01703 - output: 0.068154 - original_provider: google - gemma-3-4b-it: - id: google/gemma-3-4b-it - input: 0.01703 - output: 0.068154 - original_provider: google - google/gemma-3-12b-it:free: - id: google/gemma-3-12b-it:free - input: 0 - output: 0 - original_provider: google - gemma-3-12b-it:free: - id: google/gemma-3-12b-it:free - input: 0 - output: 0 - original_provider: google - google/gemma-3-12b-it: - id: google/gemma-3-12b-it - input: 0.04 - output: 0.13 - original_provider: google - gemma-3-12b-it: - id: google/gemma-3-12b-it - input: 0.04 - output: 0.13 - original_provider: google - cohere/command-a: - id: cohere/command-a - input: 2.5 - output: 10 - original_provider: cohere - command-a: - id: cohere/command-a - input: 2.5 - output: 10 - original_provider: cohere - openai/gpt-4o-mini-search-preview: - id: openai/gpt-4o-mini-search-preview - input: 0.15 - output: 0.6 - original_provider: openai - gpt-4o-mini-search-preview: - id: openai/gpt-4o-mini-search-preview - input: 0.15 - output: 0.6 - original_provider: openai - openai/gpt-4o-search-preview: - id: openai/gpt-4o-search-preview - input: 2.5 - output: 10 - original_provider: openai - gpt-4o-search-preview: - id: openai/gpt-4o-search-preview - input: 2.5 - output: 10 - original_provider: openai - google/gemma-3-27b-it:free: - id: google/gemma-3-27b-it:free - input: 0 - output: 0 - original_provider: google - gemma-3-27b-it:free: - id: google/gemma-3-27b-it:free - input: 0 - output: 0 - original_provider: google - google/gemma-3-27b-it: - id: google/gemma-3-27b-it - input: 0.04 - output: 0.15 - original_provider: google - gemma-3-27b-it: - id: google/gemma-3-27b-it - input: 0.04 - output: 0.15 - original_provider: google - thedrummer/skyfall-36b-v2: - id: thedrummer/skyfall-36b-v2 - input: 0.55 - output: 0.8 - original_provider: thedrummer - skyfall-36b-v2: - id: thedrummer/skyfall-36b-v2 - input: 0.55 - output: 0.8 - original_provider: thedrummer - perplexity/sonar-reasoning-pro: - id: perplexity/sonar-reasoning-pro - input: 2 - output: 8 - original_provider: perplexity - sonar-reasoning-pro: - id: perplexity/sonar-reasoning-pro - input: 2 - output: 8 - original_provider: perplexity - perplexity/sonar-pro: - id: perplexity/sonar-pro - input: 3 - output: 15 - original_provider: perplexity - sonar-pro: - id: perplexity/sonar-pro - input: 3 - output: 15 - original_provider: perplexity - perplexity/sonar-deep-research: - id: perplexity/sonar-deep-research - input: 2 - output: 8 - original_provider: perplexity - sonar-deep-research: - id: perplexity/sonar-deep-research - input: 2 - output: 8 - original_provider: perplexity - qwen/qwq-32b: - id: qwen/qwq-32b - input: 0.15 - output: 0.4 - original_provider: qwen - qwq-32b: - id: qwen/qwq-32b - input: 0.15 - output: 0.4 - original_provider: qwen - google/gemini-2.0-flash-lite-001: - id: google/gemini-2.0-flash-lite-001 - input: 0.075 - output: 0.3 - original_provider: google - gemini-2.0-flash-lite-001: - id: google/gemini-2.0-flash-lite-001 - input: 0.075 - output: 0.3 - original_provider: google - anthropic/claude-3.7-sonnet: - id: anthropic/claude-3.7-sonnet - input: 3 - output: 15 - original_provider: anthropic - claude-3.7-sonnet: - id: anthropic/claude-3.7-sonnet - input: 3 - output: 15 - original_provider: anthropic - anthropic/claude-3.7-sonnet:thinking: - id: anthropic/claude-3.7-sonnet:thinking - input: 3 - output: 15 - original_provider: anthropic - claude-3.7-sonnet:thinking: - id: anthropic/claude-3.7-sonnet:thinking - input: 3 - output: 15 - original_provider: anthropic - mistralai/mistral-saba: - id: mistralai/mistral-saba - input: 0.2 - output: 0.6 - original_provider: mistralai - mistral-saba: - id: mistralai/mistral-saba - input: 0.2 - output: 0.6 - original_provider: mistralai - meta-llama/llama-guard-3-8b: - id: meta-llama/llama-guard-3-8b - input: 0.02 - output: 0.06 - original_provider: meta-llama - llama-guard-3-8b: - id: meta-llama/llama-guard-3-8b - input: 0.02 - output: 0.06 - original_provider: meta-llama - openai/o3-mini-high: - id: openai/o3-mini-high - input: 1.1 - output: 4.4 - original_provider: openai - o3-mini-high: - id: openai/o3-mini-high - input: 1.1 - output: 4.4 - original_provider: openai - google/gemini-2.0-flash-001: - id: google/gemini-2.0-flash-001 - input: 0.1 - output: 0.4 - original_provider: google - gemini-2.0-flash-001: - id: google/gemini-2.0-flash-001 - input: 0.1 - output: 0.4 - original_provider: google - qwen/qwen-vl-plus: - id: qwen/qwen-vl-plus - input: 0.21 - output: 0.63 - original_provider: qwen - qwen-vl-plus: - id: qwen/qwen-vl-plus - input: 0.21 - output: 0.63 - original_provider: qwen - aion-labs/aion-1.0: - id: aion-labs/aion-1.0 - input: 4 - output: 8 - original_provider: aion-labs - aion-1.0: - id: aion-labs/aion-1.0 - input: 4 - output: 8 - original_provider: aion-labs - aion-labs/aion-1.0-mini: - id: aion-labs/aion-1.0-mini - input: 0.7 - output: 1.4 - original_provider: aion-labs - aion-1.0-mini: - id: aion-labs/aion-1.0-mini - input: 0.7 - output: 1.4 - original_provider: aion-labs - aion-labs/aion-rp-llama-3.1-8b: - id: aion-labs/aion-rp-llama-3.1-8b - input: 0.8 - output: 1.6 - original_provider: aion-labs - aion-rp-llama-3.1-8b: - id: aion-labs/aion-rp-llama-3.1-8b - input: 0.8 - output: 1.6 - original_provider: aion-labs - qwen/qwen-vl-max: - id: qwen/qwen-vl-max - input: 0.8 - output: 3.2 - original_provider: qwen - qwen-vl-max: - id: qwen/qwen-vl-max - input: 0.8 - output: 3.2 - original_provider: qwen - qwen/qwen-turbo: - id: qwen/qwen-turbo - input: 0.05 - output: 0.2 - original_provider: qwen - qwen-turbo: - id: qwen/qwen-turbo - input: 0.05 - output: 0.2 - original_provider: qwen - qwen/qwen2.5-vl-72b-instruct: - id: qwen/qwen2.5-vl-72b-instruct - input: 0.15 - output: 0.6 - original_provider: qwen - qwen2.5-vl-72b-instruct: - id: qwen/qwen2.5-vl-72b-instruct - input: 0.15 - output: 0.6 - original_provider: qwen - qwen/qwen-plus: - id: qwen/qwen-plus - input: 0.4 - output: 1.2 - original_provider: qwen - qwen-plus: - id: qwen/qwen-plus - input: 0.4 - output: 1.2 - original_provider: qwen - qwen/qwen-max: - id: qwen/qwen-max - input: 1.6 - output: 6.4 - original_provider: qwen - qwen-max: - id: qwen/qwen-max - input: 1.6 - output: 6.4 - original_provider: qwen - openai/o3-mini: - id: openai/o3-mini - input: 1.1 - output: 4.4 - original_provider: openai - o3-mini: - id: openai/o3-mini - input: 1.1 - output: 4.4 - original_provider: openai - mistralai/mistral-small-24b-instruct-2501: - id: mistralai/mistral-small-24b-instruct-2501 - input: 0.05 - output: 0.08 - original_provider: mistralai - mistral-small-24b-instruct-2501: - id: mistralai/mistral-small-24b-instruct-2501 - input: 0.05 - output: 0.08 - original_provider: mistralai - deepseek/deepseek-r1-distill-qwen-32b: - id: deepseek/deepseek-r1-distill-qwen-32b - input: 0.29 - output: 0.29 - original_provider: deepseek - deepseek-r1-distill-qwen-32b: - id: deepseek/deepseek-r1-distill-qwen-32b - input: 0.29 - output: 0.29 - original_provider: deepseek - perplexity/sonar: - id: perplexity/sonar - input: 1 - output: 1 - original_provider: perplexity - sonar: - id: perplexity/sonar - input: 1 - output: 1 - original_provider: perplexity - deepseek/deepseek-r1-distill-llama-70b: - id: deepseek/deepseek-r1-distill-llama-70b - input: 0.7 - output: 0.8 - original_provider: deepseek - deepseek-r1-distill-llama-70b: - id: deepseek/deepseek-r1-distill-llama-70b - input: 0.7 - output: 0.8 - original_provider: deepseek - deepseek/deepseek-r1: - id: deepseek/deepseek-r1 - input: 0.7 - output: 2.5 - original_provider: deepseek - deepseek-r1: - id: deepseek/deepseek-r1 - input: 0.7 - output: 2.5 - original_provider: deepseek - minimax/minimax-01: - id: minimax/minimax-01 - input: 0.2 - output: 1.1 - original_provider: minimax - minimax-01: - id: minimax/minimax-01 - input: 0.2 - output: 1.1 - original_provider: minimax - microsoft/phi-4: - id: microsoft/phi-4 - input: 0.06 - output: 0.14 - original_provider: microsoft - phi-4: - id: microsoft/phi-4 - input: 0.06 - output: 0.14 - original_provider: microsoft - sao10k/l3.1-70b-hanami-x1: - id: sao10k/l3.1-70b-hanami-x1 - input: 3 - output: 3 - original_provider: sao10k - l3.1-70b-hanami-x1: - id: sao10k/l3.1-70b-hanami-x1 - input: 3 - output: 3 - original_provider: sao10k - deepseek/deepseek-chat: - id: deepseek/deepseek-chat - input: 0.3 - output: 1.2 - original_provider: deepseek - deepseek-chat: - id: deepseek/deepseek-chat - input: 0.3 - output: 1.2 - original_provider: deepseek - sao10k/l3.3-euryale-70b: - id: sao10k/l3.3-euryale-70b - input: 0.65 - output: 0.75 - original_provider: sao10k - l3.3-euryale-70b: - id: sao10k/l3.3-euryale-70b - input: 0.65 - output: 0.75 - original_provider: sao10k - openai/o1: - id: openai/o1 - input: 15 - output: 60 - original_provider: openai - o1: - id: openai/o1 - input: 15 - output: 60 - original_provider: openai - cohere/command-r7b-12-2024: - id: cohere/command-r7b-12-2024 - input: 0.0375 - output: 0.15 - original_provider: cohere - command-r7b-12-2024: - id: cohere/command-r7b-12-2024 - input: 0.0375 - output: 0.15 - original_provider: cohere - meta-llama/llama-3.3-70b-instruct:free: - id: meta-llama/llama-3.3-70b-instruct:free - input: 0 - output: 0 - original_provider: meta-llama - llama-3.3-70b-instruct:free: - id: meta-llama/llama-3.3-70b-instruct:free - input: 0 - output: 0 - original_provider: meta-llama - meta-llama/llama-3.3-70b-instruct: - id: meta-llama/llama-3.3-70b-instruct - input: 0.1 - output: 0.32 - original_provider: meta-llama - llama-3.3-70b-instruct: - id: meta-llama/llama-3.3-70b-instruct - input: 0.1 - output: 0.32 - original_provider: meta-llama - amazon/nova-lite-v1: - id: amazon/nova-lite-v1 - input: 0.06 - output: 0.24 - original_provider: amazon - nova-lite-v1: - id: amazon/nova-lite-v1 - input: 0.06 - output: 0.24 - original_provider: amazon - amazon/nova-micro-v1: - id: amazon/nova-micro-v1 - input: 0.035 - output: 0.14 - original_provider: amazon - nova-micro-v1: - id: amazon/nova-micro-v1 - input: 0.035 - output: 0.14 - original_provider: amazon - amazon/nova-pro-v1: - id: amazon/nova-pro-v1 - input: 0.8 - output: 3.2 - original_provider: amazon - nova-pro-v1: - id: amazon/nova-pro-v1 - input: 0.8 - output: 3.2 - original_provider: amazon - openai/gpt-4o-2024-11-20: - id: openai/gpt-4o-2024-11-20 - input: 2.5 - output: 10 - original_provider: openai - gpt-4o-2024-11-20: - id: openai/gpt-4o-2024-11-20 - input: 2.5 - output: 10 - original_provider: openai - mistralai/mistral-large-2411: - id: mistralai/mistral-large-2411 - input: 2 - output: 6 - original_provider: mistralai - mistral-large-2411: - id: mistralai/mistral-large-2411 - input: 2 - output: 6 - original_provider: mistralai - mistralai/mistral-large-2407: - id: mistralai/mistral-large-2407 - input: 2 - output: 6 - original_provider: mistralai - mistral-large-2407: - id: mistralai/mistral-large-2407 - input: 2 - output: 6 - original_provider: mistralai - mistralai/pixtral-large-2411: - id: mistralai/pixtral-large-2411 - input: 2 - output: 6 - original_provider: mistralai - pixtral-large-2411: - id: mistralai/pixtral-large-2411 - input: 2 - output: 6 - original_provider: mistralai - qwen/qwen-2.5-coder-32b-instruct: - id: qwen/qwen-2.5-coder-32b-instruct - input: 0.2 - output: 0.2 - original_provider: qwen - qwen-2.5-coder-32b-instruct: - id: qwen/qwen-2.5-coder-32b-instruct - input: 0.2 - output: 0.2 - original_provider: qwen - raifle/sorcererlm-8x22b: - id: raifle/sorcererlm-8x22b - input: 4.5 - output: 4.5 - original_provider: raifle - sorcererlm-8x22b: - id: raifle/sorcererlm-8x22b - input: 4.5 - output: 4.5 - original_provider: raifle - thedrummer/unslopnemo-12b: - id: thedrummer/unslopnemo-12b - input: 0.4 - output: 0.4 - original_provider: thedrummer - unslopnemo-12b: - id: thedrummer/unslopnemo-12b - input: 0.4 - output: 0.4 - original_provider: thedrummer - anthropic/claude-3.5-haiku: - id: anthropic/claude-3.5-haiku - input: 0.8 - output: 4 - original_provider: anthropic - claude-3.5-haiku: - id: anthropic/claude-3.5-haiku - input: 0.8 - output: 4 - original_provider: anthropic - anthracite-org/magnum-v4-72b: - id: anthracite-org/magnum-v4-72b - input: 3 - output: 5 - original_provider: anthracite-org - magnum-v4-72b: - id: anthracite-org/magnum-v4-72b - input: 3 - output: 5 - original_provider: anthracite-org - anthropic/claude-3.5-sonnet: - id: anthropic/claude-3.5-sonnet - input: 6 - output: 30 - original_provider: anthropic - claude-3.5-sonnet: - id: anthropic/claude-3.5-sonnet - input: 6 - output: 30 - original_provider: anthropic - qwen/qwen-2.5-7b-instruct: - id: qwen/qwen-2.5-7b-instruct - input: 0.04 - output: 0.1 - original_provider: qwen - qwen-2.5-7b-instruct: - id: qwen/qwen-2.5-7b-instruct - input: 0.04 - output: 0.1 - original_provider: qwen - nvidia/llama-3.1-nemotron-70b-instruct: - id: nvidia/llama-3.1-nemotron-70b-instruct - input: 1.2 - output: 1.2 - original_provider: nvidia - llama-3.1-nemotron-70b-instruct: - id: nvidia/llama-3.1-nemotron-70b-instruct - input: 1.2 - output: 1.2 - original_provider: nvidia - inflection/inflection-3-pi: - id: inflection/inflection-3-pi - input: 2.5 - output: 10 - original_provider: inflection - inflection-3-pi: - id: inflection/inflection-3-pi - input: 2.5 - output: 10 - original_provider: inflection - inflection/inflection-3-productivity: - id: inflection/inflection-3-productivity - input: 2.5 - output: 10 - original_provider: inflection - inflection-3-productivity: - id: inflection/inflection-3-productivity - input: 2.5 - output: 10 - original_provider: inflection - thedrummer/rocinante-12b: - id: thedrummer/rocinante-12b - input: 0.17 - output: 0.43 - original_provider: thedrummer - rocinante-12b: - id: thedrummer/rocinante-12b - input: 0.17 - output: 0.43 - original_provider: thedrummer - meta-llama/llama-3.2-3b-instruct:free: - id: meta-llama/llama-3.2-3b-instruct:free - input: 0 - output: 0 - original_provider: meta-llama - llama-3.2-3b-instruct:free: - id: meta-llama/llama-3.2-3b-instruct:free - input: 0 - output: 0 - original_provider: meta-llama - meta-llama/llama-3.2-3b-instruct: - id: meta-llama/llama-3.2-3b-instruct - input: 0.02 - output: 0.02 - original_provider: meta-llama - llama-3.2-3b-instruct: - id: meta-llama/llama-3.2-3b-instruct - input: 0.02 - output: 0.02 - original_provider: meta-llama - meta-llama/llama-3.2-1b-instruct: - id: meta-llama/llama-3.2-1b-instruct - input: 0.027 - output: 0.2 - original_provider: meta-llama - llama-3.2-1b-instruct: - id: meta-llama/llama-3.2-1b-instruct - input: 0.027 - output: 0.2 - original_provider: meta-llama - meta-llama/llama-3.2-11b-vision-instruct: - id: meta-llama/llama-3.2-11b-vision-instruct - input: 0.049 - output: 0.049 - original_provider: meta-llama - llama-3.2-11b-vision-instruct: - id: meta-llama/llama-3.2-11b-vision-instruct - input: 0.049 - output: 0.049 - original_provider: meta-llama - qwen/qwen-2.5-72b-instruct: - id: qwen/qwen-2.5-72b-instruct - input: 0.12 - output: 0.39 - original_provider: qwen - qwen-2.5-72b-instruct: - id: qwen/qwen-2.5-72b-instruct - input: 0.12 - output: 0.39 - original_provider: qwen - neversleep/llama-3.1-lumimaid-8b: - id: neversleep/llama-3.1-lumimaid-8b - input: 0.09 - output: 0.6 - original_provider: neversleep - llama-3.1-lumimaid-8b: - id: neversleep/llama-3.1-lumimaid-8b - input: 0.09 - output: 0.6 - original_provider: neversleep - cohere/command-r-08-2024: - id: cohere/command-r-08-2024 - input: 0.15 - output: 0.6 - original_provider: cohere - command-r-08-2024: - id: cohere/command-r-08-2024 - input: 0.15 - output: 0.6 - original_provider: cohere - cohere/command-r-plus-08-2024: - id: cohere/command-r-plus-08-2024 - input: 2.5 - output: 10 - original_provider: cohere - command-r-plus-08-2024: - id: cohere/command-r-plus-08-2024 - input: 2.5 - output: 10 - original_provider: cohere - sao10k/l3.1-euryale-70b: - id: sao10k/l3.1-euryale-70b - input: 0.65 - output: 0.75 - original_provider: sao10k - l3.1-euryale-70b: - id: sao10k/l3.1-euryale-70b - input: 0.65 - output: 0.75 - original_provider: sao10k - qwen/qwen-2.5-vl-7b-instruct: - id: qwen/qwen-2.5-vl-7b-instruct - input: 0.2 - output: 0.2 - original_provider: qwen - qwen-2.5-vl-7b-instruct: - id: qwen/qwen-2.5-vl-7b-instruct - input: 0.2 - output: 0.2 - original_provider: qwen - nousresearch/hermes-3-llama-3.1-70b: - id: nousresearch/hermes-3-llama-3.1-70b - input: 0.3 - output: 0.3 - original_provider: nousresearch - hermes-3-llama-3.1-70b: - id: nousresearch/hermes-3-llama-3.1-70b - input: 0.3 - output: 0.3 - original_provider: nousresearch - nousresearch/hermes-3-llama-3.1-405b:free: - id: nousresearch/hermes-3-llama-3.1-405b:free - input: 0 - output: 0 - original_provider: nousresearch - hermes-3-llama-3.1-405b:free: - id: nousresearch/hermes-3-llama-3.1-405b:free - input: 0 - output: 0 - original_provider: nousresearch - nousresearch/hermes-3-llama-3.1-405b: - id: nousresearch/hermes-3-llama-3.1-405b - input: 1 - output: 1 - original_provider: nousresearch - hermes-3-llama-3.1-405b: - id: nousresearch/hermes-3-llama-3.1-405b - input: 1 - output: 1 - original_provider: nousresearch - sao10k/l3-lunaris-8b: - id: sao10k/l3-lunaris-8b - input: 0.04 - output: 0.05 - original_provider: sao10k - l3-lunaris-8b: - id: sao10k/l3-lunaris-8b - input: 0.04 - output: 0.05 - original_provider: sao10k - openai/gpt-4o-2024-08-06: - id: openai/gpt-4o-2024-08-06 - input: 2.5 - output: 10 - original_provider: openai - gpt-4o-2024-08-06: - id: openai/gpt-4o-2024-08-06 - input: 2.5 - output: 10 - original_provider: openai - meta-llama/llama-3.1-405b: - id: meta-llama/llama-3.1-405b - input: 4 - output: 4 - original_provider: meta-llama - llama-3.1-405b: - id: meta-llama/llama-3.1-405b - input: 4 - output: 4 - original_provider: meta-llama - meta-llama/llama-3.1-8b-instruct: - id: meta-llama/llama-3.1-8b-instruct - input: 0.02 - output: 0.05 - original_provider: meta-llama - llama-3.1-8b-instruct: - id: meta-llama/llama-3.1-8b-instruct - input: 0.02 - output: 0.05 - original_provider: meta-llama - meta-llama/llama-3.1-405b-instruct: - id: meta-llama/llama-3.1-405b-instruct - input: 4 - output: 4 - original_provider: meta-llama - llama-3.1-405b-instruct: - id: meta-llama/llama-3.1-405b-instruct - input: 4 - output: 4 - original_provider: meta-llama - meta-llama/llama-3.1-70b-instruct: - id: meta-llama/llama-3.1-70b-instruct - input: 0.4 - output: 0.4 - original_provider: meta-llama - llama-3.1-70b-instruct: - id: meta-llama/llama-3.1-70b-instruct - input: 0.4 - output: 0.4 - original_provider: meta-llama - mistralai/mistral-nemo: - id: mistralai/mistral-nemo - input: 0.02 - output: 0.04 - original_provider: mistralai - mistral-nemo: - id: mistralai/mistral-nemo - input: 0.02 - output: 0.04 - original_provider: mistralai - openai/gpt-4o-mini-2024-07-18: - id: openai/gpt-4o-mini-2024-07-18 - input: 0.15 - output: 0.6 - original_provider: openai - gpt-4o-mini-2024-07-18: - id: openai/gpt-4o-mini-2024-07-18 - input: 0.15 - output: 0.6 - original_provider: openai - openai/gpt-4o-mini: - id: openai/gpt-4o-mini - input: 0.15 - output: 0.6 - original_provider: openai - gpt-4o-mini: - id: openai/gpt-4o-mini - input: 0.15 - output: 0.6 - original_provider: openai - google/gemma-2-27b-it: - id: google/gemma-2-27b-it - input: 0.65 - output: 0.65 - original_provider: google - gemma-2-27b-it: - id: google/gemma-2-27b-it - input: 0.65 - output: 0.65 - original_provider: google - google/gemma-2-9b-it: - id: google/gemma-2-9b-it - input: 0.03 - output: 0.09 - original_provider: google - gemma-2-9b-it: - id: google/gemma-2-9b-it - input: 0.03 - output: 0.09 - original_provider: google - sao10k/l3-euryale-70b: - id: sao10k/l3-euryale-70b - input: 1.48 - output: 1.48 - original_provider: sao10k - l3-euryale-70b: - id: sao10k/l3-euryale-70b - input: 1.48 - output: 1.48 - original_provider: sao10k - nousresearch/hermes-2-pro-llama-3-8b: - id: nousresearch/hermes-2-pro-llama-3-8b - input: 0.14 - output: 0.14 - original_provider: nousresearch - hermes-2-pro-llama-3-8b: - id: nousresearch/hermes-2-pro-llama-3-8b - input: 0.14 - output: 0.14 - original_provider: nousresearch - mistralai/mistral-7b-instruct: - id: mistralai/mistral-7b-instruct - input: 0.2 - output: 0.2 - original_provider: mistralai - mistral-7b-instruct: - id: mistralai/mistral-7b-instruct - input: 0.2 - output: 0.2 - original_provider: mistralai - mistralai/mistral-7b-instruct-v0.3: - id: mistralai/mistral-7b-instruct-v0.3 - input: 0.2 - output: 0.2 - original_provider: mistralai - mistral-7b-instruct-v0.3: - id: mistralai/mistral-7b-instruct-v0.3 - input: 0.2 - output: 0.2 - original_provider: mistralai - meta-llama/llama-guard-2-8b: - id: meta-llama/llama-guard-2-8b - input: 0.2 - output: 0.2 - original_provider: meta-llama - llama-guard-2-8b: - id: meta-llama/llama-guard-2-8b - input: 0.2 - output: 0.2 - original_provider: meta-llama - openai/gpt-4o-2024-05-13: - id: openai/gpt-4o-2024-05-13 - input: 5 - output: 15 - original_provider: openai - gpt-4o-2024-05-13: - id: openai/gpt-4o-2024-05-13 - input: 5 - output: 15 - original_provider: openai - openai/gpt-4o: - id: openai/gpt-4o - input: 2.5 - output: 10 - original_provider: openai - gpt-4o: - id: openai/gpt-4o - input: 2.5 - output: 10 - original_provider: openai - openai/gpt-4o:extended: - id: openai/gpt-4o:extended - input: 6 - output: 18 - original_provider: openai - gpt-4o:extended: - id: openai/gpt-4o:extended - input: 6 - output: 18 - original_provider: openai - meta-llama/llama-3-70b-instruct: - id: meta-llama/llama-3-70b-instruct - input: 0.51 - output: 0.74 - original_provider: meta-llama - llama-3-70b-instruct: - id: meta-llama/llama-3-70b-instruct - input: 0.51 - output: 0.74 - original_provider: meta-llama - meta-llama/llama-3-8b-instruct: - id: meta-llama/llama-3-8b-instruct - input: 0.03 - output: 0.04 - original_provider: meta-llama - llama-3-8b-instruct: - id: meta-llama/llama-3-8b-instruct - input: 0.03 - output: 0.04 - original_provider: meta-llama - mistralai/mixtral-8x22b-instruct: - id: mistralai/mixtral-8x22b-instruct - input: 2 - output: 6 - original_provider: mistralai - mixtral-8x22b-instruct: - id: mistralai/mixtral-8x22b-instruct - input: 2 - output: 6 - original_provider: mistralai - microsoft/wizardlm-2-8x22b: - id: microsoft/wizardlm-2-8x22b - input: 0.62 - output: 0.62 - original_provider: microsoft - wizardlm-2-8x22b: - id: microsoft/wizardlm-2-8x22b - input: 0.62 - output: 0.62 - original_provider: microsoft - openai/gpt-4-turbo: - id: openai/gpt-4-turbo - input: 10 - output: 30 - original_provider: openai - gpt-4-turbo: - id: openai/gpt-4-turbo - input: 10 - output: 30 - original_provider: openai - anthropic/claude-3-haiku: - id: anthropic/claude-3-haiku - input: 0.25 - output: 1.25 - original_provider: anthropic - claude-3-haiku: - id: anthropic/claude-3-haiku - input: 0.25 - output: 1.25 - original_provider: anthropic - mistralai/mistral-large: - id: mistralai/mistral-large - input: 2 - output: 6 - original_provider: mistralai - mistral-large: - id: mistralai/mistral-large - input: 2 - output: 6 - original_provider: mistralai - openai/gpt-3.5-turbo-0613: - id: openai/gpt-3.5-turbo-0613 - input: 1 - output: 2 - original_provider: openai - gpt-3.5-turbo-0613: - id: openai/gpt-3.5-turbo-0613 - input: 1 - output: 2 - original_provider: openai - openai/gpt-4-turbo-preview: - id: openai/gpt-4-turbo-preview - input: 10 - output: 30 - original_provider: openai - gpt-4-turbo-preview: - id: openai/gpt-4-turbo-preview - input: 10 - output: 30 - original_provider: openai - mistralai/mistral-7b-instruct-v0.2: - id: mistralai/mistral-7b-instruct-v0.2 - input: 0.2 - output: 0.2 - original_provider: mistralai - mistral-7b-instruct-v0.2: - id: mistralai/mistral-7b-instruct-v0.2 - input: 0.2 - output: 0.2 - original_provider: mistralai - mistralai/mixtral-8x7b-instruct: - id: mistralai/mixtral-8x7b-instruct - input: 0.54 - output: 0.54 - original_provider: mistralai - mixtral-8x7b-instruct: - id: mistralai/mixtral-8x7b-instruct - input: 0.54 - output: 0.54 - original_provider: mistralai - neversleep/noromaid-20b: - id: neversleep/noromaid-20b - input: 1 - output: 1.75 - original_provider: neversleep - noromaid-20b: - id: neversleep/noromaid-20b - input: 1 - output: 1.75 - original_provider: neversleep - alpindale/goliath-120b: - id: alpindale/goliath-120b - input: 3.75 - output: 7.5 - original_provider: alpindale - goliath-120b: - id: alpindale/goliath-120b - input: 3.75 - output: 7.5 - original_provider: alpindale - openrouter/auto: - id: openrouter/auto - input: -1000000 - output: -1000000 - original_provider: openrouter - auto: - id: openrouter/auto - input: -1000000 - output: -1000000 - original_provider: openrouter - openai/gpt-4-1106-preview: - id: openai/gpt-4-1106-preview - input: 10 - output: 30 - original_provider: openai - gpt-4-1106-preview: - id: openai/gpt-4-1106-preview - input: 10 - output: 30 - original_provider: openai - openai/gpt-3.5-turbo-instruct: - id: openai/gpt-3.5-turbo-instruct - input: 1.5 - output: 2 - original_provider: openai - gpt-3.5-turbo-instruct: - id: openai/gpt-3.5-turbo-instruct - input: 1.5 - output: 2 - original_provider: openai - mistralai/mistral-7b-instruct-v0.1: - id: mistralai/mistral-7b-instruct-v0.1 - input: 0.11 - output: 0.19 - original_provider: mistralai - mistral-7b-instruct-v0.1: - id: mistralai/mistral-7b-instruct-v0.1 - input: 0.11 - output: 0.19 - original_provider: mistralai - openai/gpt-3.5-turbo-16k: - id: openai/gpt-3.5-turbo-16k - input: 3 - output: 4 - original_provider: openai - gpt-3.5-turbo-16k: - id: openai/gpt-3.5-turbo-16k - input: 3 - output: 4 - original_provider: openai - mancer/weaver: - id: mancer/weaver - input: 0.75 - output: 1 - original_provider: mancer - weaver: - id: mancer/weaver - input: 0.75 - output: 1 - original_provider: mancer - undi95/remm-slerp-l2-13b: - id: undi95/remm-slerp-l2-13b - input: 0.45 - output: 0.65 - original_provider: undi95 - remm-slerp-l2-13b: - id: undi95/remm-slerp-l2-13b - input: 0.45 - output: 0.65 - original_provider: undi95 - gryphe/mythomax-l2-13b: - id: gryphe/mythomax-l2-13b - input: 0.06 - output: 0.06 - original_provider: gryphe - mythomax-l2-13b: - id: gryphe/mythomax-l2-13b - input: 0.06 - output: 0.06 - original_provider: gryphe - openai/gpt-4-0314: - id: openai/gpt-4-0314 - input: 30 - output: 60 - original_provider: openai - gpt-4-0314: - id: openai/gpt-4-0314 - input: 30 - output: 60 - original_provider: openai - openai/gpt-4: - id: openai/gpt-4 - input: 30 - output: 60 - original_provider: openai - gpt-4: - id: openai/gpt-4 - input: 30 - output: 60 - original_provider: openai - openai/gpt-3.5-turbo: - id: openai/gpt-3.5-turbo - input: 0.5 - output: 1.5 - original_provider: openai - gpt-3.5-turbo: - id: openai/gpt-3.5-turbo - input: 0.5 - output: 1.5 - original_provider: openai -metadata: - last_updated: '2026-02-18' - source: https://openrouter.ai/api/v1/models - notes: Auto-refreshed from OpenRouter models API - version: auto diff --git a/gateway/src/costs/templates/base.yaml b/gateway/src/costs/templates/base.yaml deleted file mode 100644 index ed69842..0000000 --- a/gateway/src/costs/templates/base.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Base pricing template for new providers -# Copy this file and modify for your provider - -provider: "provider_name" -currency: "USD" -unit: "1MTok" -models: - model-name: - input: 0.00 - output: 0.00 - # Optional fields: - # original_provider: "if_via_aggregator" - # region: "if_regional_pricing" - # tier: "if_tiered_pricing" we only support standard tier pricing currently. - # cached_input: 0.00 - # 5m_cache_write: 0.00 - # 1h_cache_write: 0.00 - # cache_read: 0.00 - -metadata: - last_updated: "YYYY-MM-DD" - source: "pricing_source_url" - notes: "Additional notes about pricing" - version: "1.0" \ No newline at end of file diff --git a/gateway/src/costs/xAI.yaml b/gateway/src/costs/xAI.yaml deleted file mode 100644 index bea4211..0000000 --- a/gateway/src/costs/xAI.yaml +++ /dev/null @@ -1,41 +0,0 @@ -provider: "xAI" -currency: "USD" -unit: "MTok" - -models: - grok-4: - input: 3.00 - cached_input: 0.75 - output: 15.00 - - grok-3: - input: 3.00 - cached_input: 0.75 - output: 15.00 - - grok-3-mini: - input: 0.30 - cached_input: 0.0075 - output: 0.50 - - grok-code-fast-1: - input: 0.20 - cached_input: 0.02 - output: 1.50 - - grok-code-fast: - input: 0.20 - cached_input: 0.02 - output: 1.50 - - grok-code-fast-1-0825: - input: 0.20 - cached_input: 0.02 - output: 1.50 - -metadata: - last_updated: "2024-12-19" - source: "https://docs.x.ai/docs#models" - notes: "xAI Grok model pricing with cache support." - version: "1.0" - contributor: "xAI" \ No newline at end of file diff --git a/gateway/src/costs/zai.yaml b/gateway/src/costs/zai.yaml deleted file mode 100644 index 8a2482e..0000000 --- a/gateway/src/costs/zai.yaml +++ /dev/null @@ -1,57 +0,0 @@ -provider: "zai" -currency: "USD" -unit: "MTok" - -models: - glm-4.6: - input: 0.60 - cache_write: 0.11 - cache_read: 0.00 - output: 2.20 - - glm-4.5: - input: 0.60 - cache_write: 0.11 - cache_read: 0.00 - output: 2.20 - - glm-4.5v: - input: 0.60 - cache_write: 0.11 - cache_read: 0.00 - output: 1.80 - - glm-4.5-x: - input: 2.20 - cache_write: 0.45 - cache_read: 0.00 - output: 8.90 - - glm-4.5-air: - input: 0.20 - cache_write: 0.03 - cache_read: 0.00 - output: 1.10 - - glm-4.5-airx: - input: 1.10 - cache_write: 0.22 - cache_read: 0.00 - output: 4.50 - - glm-4-32b-0414-128k: - input: 0.10 - output: 0.10 - - glm-4.5-flash: - input: 0.00 - cache_write: 0.00 - cache_read: 0.00 - output: 0.00 - -metadata: - last_updated: "2025-01-10" - source: "User-provided Z AI pricing table" - notes: "Cached input storage currently free; set cache_read to 0 until provider publishes rates." - version: "1.0" - contributor: "zai" diff --git a/gateway/src/domain/providers/anthropic-provider.ts b/gateway/src/domain/providers/anthropic-provider.ts deleted file mode 100644 index 6aa7fc8..0000000 --- a/gateway/src/domain/providers/anthropic-provider.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { BaseProvider } from './base-provider.js'; -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { AuthenticationError } from '../../shared/errors/index.js'; -import fetch, { Response } from 'node-fetch'; -import { getConfig } from '../../infrastructure/config/app-config.js'; - -interface AnthropicRequest { - model: string; - max_tokens: number; - messages: Array<{ role: 'user' | 'assistant'; content: string; }>; - system?: string; - temperature?: number; - stream?: boolean; -} - -interface AnthropicResponse { - id: string; - type: 'message'; - role: 'assistant'; - content: Array<{ type: 'text'; text: string; }>; - model: string; - stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence'; - usage: { - input_tokens: number; - cache_creation_input_tokens?: number; - cache_read_input_tokens?: number; - output_tokens: number; - }; -} - -// Constants -const DEFAULT_MAX_TOKENS = 1000; -const ANTHROPIC_VERSION = '2023-06-01'; -const REQUEST_TIMEOUT = 30000; - -export class AnthropicProvider extends BaseProvider { - readonly name = 'anthropic'; - protected readonly baseUrl = 'https://api.anthropic.com/v1'; - protected get apiKey(): string | undefined { - return getConfig().providers.anthropic.apiKey; - } - - isConfigured(): boolean { - const config = getConfig(); - // Anthropic is available via x402 for /v1/messages - if (config.x402.enabled) { - return true; - } - return super.isConfigured(); - } - - private validateApiKey(): void { - if (!this.apiKey) { - throw new AuthenticationError(`${this.name} API key not configured`, { provider: this.name }); - } - } - - private async makeRequest(url: string, body: any, stream: boolean = false): Promise { - this.validateApiKey(); - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); - - try { - const response = await fetch(url, { - method: 'POST', - headers: this.getHeaders(), - body: JSON.stringify({ ...body, stream }), - signal: controller.signal - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new AuthenticationError(errorText || `HTTP ${response.status}`, { provider: this.name, statusCode: response.status }); - } - - return response; - } finally { - clearTimeout(timeoutId); - } - } - - async chatCompletion(request: CanonicalRequest): Promise { - // For non-streaming, collect the stream and parse it - const streamResponse = await this.getStreamingResponse(request); - const text = await streamResponse.text(); - return this.parseStreamToCanonical(text, request); - } - - // Get raw streaming response - async getStreamingResponse(request: CanonicalRequest): Promise { - const transformedRequest = this.transformRequest(request); - const url = `${this.baseUrl}${this.getChatCompletionEndpoint()}`; - - return await this.makeRequest(url, transformedRequest, true); - } - - private parseStreamToCanonical(streamText: string, originalRequest: CanonicalRequest): CanonicalResponse { - let finalMessage = ''; - let usage = { - input_tokens: 0, - output_tokens: 0, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0 - }; - - // Parse Server-Sent Events format - const lines = streamText.split('\n'); - for (const line of lines) { - if (line.startsWith('data: ')) { - const dataStr = line.slice(6); - if (dataStr === '[DONE]') break; - - try { - const data = JSON.parse(dataStr); - if (data.type === 'content_block_delta' && data.delta?.text) { - finalMessage += data.delta.text; - } else if (data.type === 'message_delta' && data.usage) { - usage = { ...usage, ...data.usage }; - } else if (data.type === 'message_start' && data.message?.usage) { - usage = { ...usage, ...data.message.usage }; - } - } catch (e) { - // Skip malformed JSON lines - } - } - } - - // Note: Usage tracking is now handled by BaseProvider - - // Return canonical response - return { - id: `msg-${Date.now()}`, - model: originalRequest.model, - created: Math.floor(Date.now() / 1000), - message: { - role: 'assistant', - content: [{ - type: 'text', - text: finalMessage - }] - }, - finishReason: 'stop', - usage: { - inputTokens: usage.input_tokens, - cacheWriteInputTokens: usage.cache_creation_input_tokens, - cacheReadInputTokens: usage.cache_read_input_tokens, - outputTokens: usage.output_tokens, - totalTokens: usage.input_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0) + usage.output_tokens - } - }; - } - - protected getHeaders(): Record { - this.validateApiKey(); - return { - 'x-api-key': this.apiKey!, - 'Content-Type': 'application/json', - 'anthropic-version': ANTHROPIC_VERSION - }; - } - - protected getChatCompletionEndpoint(): string { - return '/messages'; - } - - protected transformRequest(request: CanonicalRequest): AnthropicRequest { - // Extract system message and regular messages - let systemPrompt: string | undefined; - const messages: Array<{ role: 'user' | 'assistant'; content: string; }> = []; - - for (const message of request.messages) { - if (message.role === 'system') { - // Combine all text content for system message - systemPrompt = message.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join(''); - } else if (message.role === 'user' || message.role === 'assistant') { - // Combine all text content for regular messages - const content = message.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join(''); - - messages.push({ - role: message.role, - content - }); - } - } - - const anthropicRequest: AnthropicRequest = { - model: request.model, - max_tokens: request.maxTokens || DEFAULT_MAX_TOKENS, - messages, - temperature: request.temperature, - stream: request.stream || false - }; - - if (systemPrompt) { - anthropicRequest.system = systemPrompt; - } - - return anthropicRequest; - } - - protected transformResponse(response: AnthropicResponse): CanonicalResponse { - const content = response.content - .filter(item => item.type === 'text') - .map(item => item.text) - .join(''); - - return { - id: response.id, - model: response.model, - created: Math.floor(Date.now() / 1000), - message: { - role: 'assistant', - content: [{ - type: 'text', - text: content - }] - }, - finishReason: this.mapFinishReason(response.stop_reason), - usage: { - inputTokens: response.usage.input_tokens, - cacheWriteInputTokens: response.usage.cache_creation_input_tokens, - cacheReadInputTokens: response.usage.cache_read_input_tokens, - outputTokens: response.usage.output_tokens, - totalTokens: response.usage.input_tokens + (response.usage.cache_creation_input_tokens || 0) + (response.usage.cache_read_input_tokens || 0) + response.usage.output_tokens - } - }; - } - - private mapFinishReason(stopReason: string): 'stop' | 'length' | 'tool_calls' | 'error' { - switch (stopReason) { - case 'end_turn': - return 'stop'; - case 'max_tokens': - return 'length'; - case 'stop_sequence': - return 'stop'; - default: - return 'stop'; - } - } - - -} \ No newline at end of file diff --git a/gateway/src/domain/providers/base-provider.ts b/gateway/src/domain/providers/base-provider.ts deleted file mode 100644 index dcadc6a..0000000 --- a/gateway/src/domain/providers/base-provider.ts +++ /dev/null @@ -1,124 +0,0 @@ -import fetch from 'node-fetch'; -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { ProviderError, AuthenticationError } from '../../shared/errors/index.js'; -import { usageTracker } from '../../infrastructure/utils/usage-tracker.js'; -import { AIProvider, ProviderRequest, ProviderResponse, HTTP_STATUS } from '../types/provider.js'; -import { logger } from '../../infrastructure/utils/logger.js'; - -export abstract class BaseProvider implements AIProvider { - abstract readonly name: string; - protected abstract readonly baseUrl: string; - protected abstract readonly apiKey: string | undefined; - - isConfigured(): boolean { - return !!this.apiKey; - } - - // Template method pattern: allow providers to customize headers - protected getHeaders(): Record { - return { - 'Authorization': `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json' - }; - } - - // Provider-specific transformations with proper types - protected abstract transformRequest(request: CanonicalRequest): ProviderRequest; - protected abstract transformResponse(response: ProviderResponse): CanonicalResponse; - - // Template method pattern: allow providers to customize endpoint - protected getChatCompletionEndpoint(): string { - return '/chat/completions'; - } - - - protected async makeAPIRequest(endpoint: string, options: Record = {}): Promise { - if (!this.apiKey) { - throw new AuthenticationError(`${this.name} API key not configured`, { provider: this.name }); - } - - const url = `${this.baseUrl}${endpoint}`; - const headers = { - ...this.getHeaders(), - ...(options.headers as Record || {}) - }; - - const response = await fetch(url, { - ...options, - headers - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new ProviderError( - this.name, - errorText || `HTTP ${response.status}`, - response.status, - { endpoint, statusText: response.statusText } - ); - } - - return response.json() as Promise; - } - - async chatCompletion(request: CanonicalRequest): Promise { - const transformedRequest = this.transformRequest(request); - const response = await this.makeAPIRequest(this.getChatCompletionEndpoint(), { - method: 'POST', - body: JSON.stringify(transformedRequest) - }); - - const transformedResponse = this.transformResponse(response); - - // Track usage - if (transformedResponse.usage) { - usageTracker.trackUsage( - request.model, - this.name, - transformedResponse.usage.inputTokens, - transformedResponse.usage.outputTokens, - transformedResponse.usage.cacheWriteInputTokens || 0, - transformedResponse.usage.cacheReadInputTokens || 0, - ); - } - - return transformedResponse; - } - - async getStreamingResponse(request: CanonicalRequest): Promise { - const transformedRequest = this.transformRequest(request); - - // Set stream: true for streaming requests - const streamingRequest = { ...transformedRequest, stream: true }; - - if (!this.apiKey) { - throw new AuthenticationError(`${this.name} API key not configured`, { provider: this.name }); - } - - const url = `${this.baseUrl}${this.getChatCompletionEndpoint()}`; - const headers = { - ...this.getHeaders(), - 'Accept': 'text/event-stream', - 'Cache-Control': 'no-cache' - }; - - const response = await fetch(url, { - method: 'POST', - headers, - body: JSON.stringify(streamingRequest) - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new ProviderError( - this.name, - errorText || `HTTP ${response.status}`, - response.status, - { endpoint: this.getChatCompletionEndpoint(), stream: true, statusText: response.statusText } - ); - } - - return response; - } - -} diff --git a/gateway/src/domain/providers/google-provider.ts b/gateway/src/domain/providers/google-provider.ts deleted file mode 100644 index 21c1b1c..0000000 --- a/gateway/src/domain/providers/google-provider.ts +++ /dev/null @@ -1,205 +0,0 @@ -import fetch, { Response as FetchResponse } from 'node-fetch'; -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { BaseProvider } from './base-provider.js'; -import { getConfig } from '../../infrastructure/config/app-config.js'; -import { ModelUtils } from '../../infrastructure/utils/model-utils.js'; -import { ProviderError, AuthenticationError } from '../../shared/errors/index.js'; -import { usageTracker } from '../../infrastructure/utils/usage-tracker.js'; - -interface GoogleContentPart { - text?: string; -} - -interface GoogleContent { - role: string; - parts: GoogleContentPart[]; -} - -interface GoogleResponseCandidate { - content?: GoogleContent; - finishReason?: string; -} - -interface GoogleResponse { - candidates?: GoogleResponseCandidate[]; - usageMetadata?: { - promptTokenCount?: number; - candidatesTokenCount?: number; - totalTokenCount?: number; - }; -} - -export class GoogleProvider extends BaseProvider { - readonly name = 'google'; - protected readonly baseUrl = 'https://generativelanguage.googleapis.com/v1beta'; - private lastRequestedModel: string | null = null; - - protected get apiKey(): string | undefined { - return getConfig().providers.google.apiKey; - } - - isConfigured(): boolean { - return !!this.apiKey; - } - - protected getHeaders(): Record { - if (!this.apiKey) { - throw new AuthenticationError('Google API key not configured', { provider: this.name }); - } - return { - 'Content-Type': 'application/json', - 'x-goog-api-key': this.apiKey - }; - } - - private normalizeModelName(model: string): string { - return ModelUtils.removeProviderPrefix(model); - } - - private buildUrl(model: string, endpoint: 'generateContent' | 'streamGenerateContent'): string { - return `${this.baseUrl}/models/${this.normalizeModelName(model)}:${endpoint}`; - } - - protected transformRequest(request: CanonicalRequest): Record { - const systemMessages = request.messages.filter(msg => msg.role === 'system'); - const conversationMessages = request.messages.filter(msg => msg.role !== 'system'); - - const contents = conversationMessages.map(msg => ({ - role: msg.role === 'assistant' ? 'model' : 'user', - parts: msg.content.map(part => ({ text: part.text })) - })); - - const body: Record = { contents }; - - if (systemMessages.length) { - body.systemInstruction = { - parts: systemMessages.flatMap(msg => msg.content.map(part => ({ text: part.text }))) - }; - } - - const generationConfig: Record = {}; - if (typeof request.temperature === 'number') { - generationConfig.temperature = request.temperature; - } - if (typeof request.topP === 'number') { - generationConfig.topP = request.topP; - } - if (typeof request.maxTokens === 'number') { - generationConfig.maxOutputTokens = request.maxTokens; - } - if (request.stopSequences?.length) { - generationConfig.stopSequences = request.stopSequences; - } - - if (Object.keys(generationConfig).length > 0) { - body.generationConfig = generationConfig; - } - - return body; - } - - private toCanonicalResponse(response: GoogleResponse, requestedModel: string): CanonicalResponse { - const candidate = response.candidates?.[0]; - const parts = candidate?.content?.parts ?? []; - const text = parts - .map(part => part.text ?? '') - .join('') - .trim(); - - const inputTokens = response.usageMetadata?.promptTokenCount ?? 0; - const outputTokens = response.usageMetadata?.candidatesTokenCount ?? 0; - const totalTokens = response.usageMetadata?.totalTokenCount ?? (inputTokens + outputTokens); - - return { - id: `google-${Date.now()}`, - model: requestedModel, - created: Math.floor(Date.now() / 1000), - message: { - role: 'assistant', - content: [{ - type: 'text', - text - }] - }, - finishReason: this.mapFinishReason(candidate?.finishReason), - usage: { - inputTokens, - outputTokens, - totalTokens - } - }; - } - - protected transformResponse(response: Record): CanonicalResponse { - const model = this.lastRequestedModel ?? 'gemini'; - return this.toCanonicalResponse(response as GoogleResponse, model); - } - - private mapFinishReason(reason?: string): 'stop' | 'length' | 'tool_calls' | 'error' { - switch ((reason || '').toUpperCase()) { - case 'STOP': - return 'stop'; - case 'MAX_TOKENS': - return 'length'; - default: - return 'stop'; - } - } - - async chatCompletion(request: CanonicalRequest): Promise { - if (!this.apiKey) { - throw new AuthenticationError('Google API key not configured', { provider: this.name }); - } - - this.lastRequestedModel = request.model; - const url = this.buildUrl(request.model, 'generateContent'); - const body = JSON.stringify(this.transformRequest(request)); - const response = await fetch(url, { - method: 'POST', - headers: this.getHeaders(), - body - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new ProviderError(this.name, errorText || `HTTP ${response.status}`, response.status, { endpoint: url }); - } - - const json = await response.json() as GoogleResponse; - const canonical = this.toCanonicalResponse(json, request.model); - - usageTracker.trackUsage( - request.model, - this.name, - canonical.usage.inputTokens, - canonical.usage.outputTokens, - canonical.usage.cacheWriteInputTokens || 0, - canonical.usage.cacheReadInputTokens || 0 - ); - - return canonical; - } - - async getStreamingResponse(request: CanonicalRequest): Promise { - if (!this.apiKey) { - throw new AuthenticationError('Google API key not configured', { provider: this.name }); - } - - const url = `${this.buildUrl(request.model, 'streamGenerateContent')}?alt=sse`; - const response = await fetch(url, { - method: 'POST', - headers: { - ...this.getHeaders(), - Accept: 'text/event-stream' - }, - body: JSON.stringify(this.transformRequest(request)) - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new ProviderError(this.name, errorText || `HTTP ${response.status}`, response.status, { endpoint: url, stream: true }); - } - - return response; - } -} diff --git a/gateway/src/domain/providers/ollama-provider.ts b/gateway/src/domain/providers/ollama-provider.ts deleted file mode 100644 index ee43e6d..0000000 --- a/gateway/src/domain/providers/ollama-provider.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { BaseProvider } from './base-provider.js'; -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { getConfig } from '../../infrastructure/config/app-config.js'; - -interface OllamaRequest { - model: string; - messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string; }>; - max_tokens?: number; - temperature?: number; - stream?: boolean; - stop?: string | string[]; -} - -interface OllamaResponse { - id: string; - object: string; - created: number; - model: string; - choices: Array<{ - index: number; - message: { role: string; content: string; }; - finish_reason: string; - }>; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - -export class OllamaProvider extends BaseProvider { - readonly name = 'ollama'; - - protected get baseUrl(): string { - return getConfig().providers.ollama.baseUrl; - } - - protected get apiKey(): string | undefined { - return getConfig().providers.ollama.apiKey || 'ollama'; - } - - isConfigured(): boolean { - return getConfig().providers.ollama.enabled; - } - - protected transformRequest(request: CanonicalRequest): OllamaRequest { - const messages = request.messages.map(msg => ({ - role: msg.role, - content: msg.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join('') - })); - - return { - model: request.model, - messages, - max_tokens: request.maxTokens, - temperature: request.temperature, - stream: request.stream || false, - stop: request.stopSequences - }; - } - - protected transformResponse(response: OllamaResponse): CanonicalResponse { - const choice = response.choices[0]; - - return { - id: response.id, - model: response.model, - created: response.created, - message: { - role: 'assistant', - content: [{ - type: 'text', - text: choice.message.content - }] - }, - finishReason: this.mapFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage?.prompt_tokens ?? 0, - outputTokens: response.usage?.completion_tokens ?? 0, - totalTokens: response.usage?.total_tokens ?? 0 - } - }; - } - - private mapFinishReason(reason: string): 'stop' | 'length' | 'tool_calls' | 'error' { - switch (reason) { - case 'stop': return 'stop'; - case 'length': return 'length'; - default: return 'stop'; - } - } -} diff --git a/gateway/src/domain/providers/openai-provider.ts b/gateway/src/domain/providers/openai-provider.ts deleted file mode 100644 index f9a94d8..0000000 --- a/gateway/src/domain/providers/openai-provider.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { BaseProvider } from './base-provider.js'; -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import fetch, { Response } from 'node-fetch'; -import { AuthenticationError, ProviderError } from '../../shared/errors/index.js'; -import { ModelUtils } from '../../infrastructure/utils/model-utils.js'; -import { getConfig } from '../../infrastructure/config/app-config.js'; - -interface OpenAIRequest { - model: string; - messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string; }>; - max_tokens?: number; - max_completion_tokens?: number; - temperature?: number; - stream?: boolean; - stop?: string | string[]; -} - -interface OpenAIResponse { - id: string; - object: string; - created: number; - model: string; - choices: Array<{ - index: number; - message: { role: string; content: string; }; - finish_reason: string; - }>; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} -export class OpenAIProvider extends BaseProvider { - readonly name = 'openai'; - protected readonly baseUrl = 'https://api.openai.com/v1'; - protected get apiKey(): string | undefined { - return getConfig().providers.openai.apiKey; - } - - private isResponsesAPI(request: CanonicalRequest): boolean { - return Boolean((request as any).metadata?.useResponsesAPI); - } - - private transformRequestForResponses(request: CanonicalRequest): any { - // Flatten canonical messages to a simple string input when possible - const lastUser = [...request.messages].reverse().find(m => m.role === 'user'); - const text = lastUser - ? lastUser.content.filter(c => c.type === 'text').map(c => c.text).join('') - : request.messages.map(m => m.content.filter(c => c.type === 'text').map(c => c.text).join('')).join('\n'); - const body: any = { - model: request.model, - input: text, - temperature: request.temperature, - }; - if (request.maxTokens) body.max_output_tokens = request.maxTokens; - if (request.stream) body.stream = true; - return body; - } - - protected transformRequest(request: CanonicalRequest): OpenAIRequest { - const messages = request.messages.map(msg => ({ - role: msg.role, - content: msg.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join('') - })); - - const requestData: OpenAIRequest = { - model: request.model, - messages, - temperature: request.temperature, - stream: request.stream || false, - stop: request.stopSequences - }; - - // Use max_completion_tokens for o1/o3/o4 series models, max_tokens for others - if (request.maxTokens) { - if (ModelUtils.requiresMaxCompletionTokens(request.model)) { - requestData.max_completion_tokens = request.maxTokens; - } else { - requestData.max_tokens = request.maxTokens; - } - } - - return requestData; - } - - private transformResponsesToCanonical(response: any): CanonicalResponse { - const text = response.output_text - || (Array.isArray(response.output) - ? response.output.flatMap((o: any) => (o.content || [])).filter((c: any) => c.type?.includes('text')).map((c: any) => c.text || '').join('') - : ''); - - const inputTokens = response.usage?.input_tokens ?? response.usage?.prompt_tokens ?? 0; - const outputTokens = response.usage?.output_tokens ?? response.usage?.completion_tokens ?? 0; - const created = response.created ?? Math.floor(Date.now() / 1000); - - return { - id: response.id, - model: response.model, - created, - message: { - role: 'assistant', - content: [{ type: 'text', text }] - }, - finishReason: 'stop', - usage: { - inputTokens, - outputTokens, - totalTokens: inputTokens + outputTokens - } - }; - } - - // Add streaming support - async getStreamingResponse(request: CanonicalRequest): Promise { - if (this.isResponsesAPI(request)) { - const transformed = this.transformRequestForResponses(request); - return this.fetchStreaming('/responses', transformed); - } - - const transformedRequest = this.transformRequest(request); - transformedRequest.stream = true; - - if (!this.apiKey) { - throw new AuthenticationError(`${this.name} API key not configured`, { provider: this.name }); - } - - return this.fetchStreaming(this.getChatCompletionEndpoint(), transformedRequest); - } - - private async fetchStreaming(endpoint: string, body: any): Promise { - if (!this.apiKey) { - throw new AuthenticationError(`${this.name} API key not configured`, { provider: this.name }); - } - const url = `${this.baseUrl}${endpoint}`; - const headers = this.getHeaders(); - const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }); - if (!response.ok) { - const errorText = await response.text(); - throw new ProviderError(this.name, errorText || `HTTP ${response.status}`, response.status, { endpoint, statusText: response.statusText }); - } - return response; - } - - - protected transformResponse(response: OpenAIResponse): CanonicalResponse { - const choice = response.choices[0]; - - return { - id: response.id, - model: response.model, - created: response.created, - message: { - role: 'assistant', - content: [{ - type: 'text', - text: choice.message.content - }] - }, - finishReason: this.mapFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage.prompt_tokens, - outputTokens: response.usage.completion_tokens, - totalTokens: response.usage.total_tokens - } - }; - } - - private mapFinishReason(reason: string): 'stop' | 'length' | 'tool_calls' | 'error' { - switch (reason) { - case 'stop': return 'stop'; - case 'length': return 'length'; - case 'function_call': return 'tool_calls'; - case 'tool_calls': return 'tool_calls'; - default: return 'stop'; - } - } - - async chatCompletion(request: CanonicalRequest): Promise { - if (this.isResponsesAPI(request)) { - const body = this.transformRequestForResponses(request); - const resp = await this.makeAPIRequest('/responses', { - method: 'POST', - body: JSON.stringify(body) - }); - return this.transformResponsesToCanonical(resp); - } - return super.chatCompletion(request); - } -} diff --git a/gateway/src/domain/providers/openrouter-provider.ts b/gateway/src/domain/providers/openrouter-provider.ts deleted file mode 100644 index e18bc4c..0000000 --- a/gateway/src/domain/providers/openrouter-provider.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { BaseProvider } from './base-provider.js'; -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { pricingLoader } from '../../infrastructure/utils/pricing-loader.js'; -import { ModelUtils } from '../../infrastructure/utils/model-utils.js'; -import { getConfig } from '../../infrastructure/config/app-config.js'; - -interface OpenRouterRequest { - model: string; - messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string; }>; - max_tokens?: number; - max_completion_tokens?: number; - temperature?: number; - stream?: boolean; - stop?: string | string[]; -} - -interface OpenRouterResponse { - id: string; - object: string; - created: number; - model: string; - choices: Array<{ - index: number; - message: { role: string; content: string; }; - finish_reason: string; - }>; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - -export class OpenRouterProvider extends BaseProvider { - readonly name = 'openrouter'; - protected readonly baseUrl = 'https://openrouter.ai/api/v1'; - protected get apiKey(): string | undefined { - return getConfig().providers.openrouter.apiKey; - } - - isConfigured(): boolean { - const config = getConfig(); - if (config.x402.enabled) { - return true; - } - return super.isConfigured(); - } - - protected getHeaders(): Record { - return { - 'Authorization': `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json', - 'HTTP-Referer': 'https://ekai-proxy', - 'X-Title': 'EKAI Proxy' - }; - } - - protected transformRequest(request: CanonicalRequest): OpenRouterRequest { - const messages = request.messages.map(msg => ({ - role: msg.role, - content: msg.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join('') - })); - - // Get the actual OpenRouter model ID from pricing data - const openRouterModel = this.getOpenRouterModelId(request.model); - - const requestData: OpenRouterRequest = { - model: openRouterModel, - messages, - temperature: request.temperature, - stream: request.stream || false, - stop: request.stopSequences - }; - - // Use max_completion_tokens for o1/o3/o4 series models, max_tokens for others - if (request.maxTokens) { - if (ModelUtils.requiresMaxCompletionTokens(openRouterModel)) { - requestData.max_completion_tokens = request.maxTokens; - } else { - requestData.max_tokens = request.maxTokens; - } - } - - return requestData; - } - - private getOpenRouterModelId(modelName: string): string { - // If model already has provider prefix, use as-is - if (modelName.includes('/')) { - return modelName; - } - - // Look up the model in OpenRouter pricing to get the actual ID - const openRouterPricing = pricingLoader.getModelPricing('openrouter', modelName); - - if (openRouterPricing?.id) { - return openRouterPricing.id; - } - - // Fallback to original model name if no ID found - return modelName; - } - - protected transformResponse(response: OpenRouterResponse): CanonicalResponse { - const choice = response.choices[0]; - - return { - id: response.id, - model: response.model, - created: response.created, - message: { - role: 'assistant', - content: [{ - type: 'text', - text: choice.message.content - }] - }, - finishReason: this.mapFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage.prompt_tokens, - outputTokens: response.usage.completion_tokens, - totalTokens: response.usage.total_tokens - } - }; - } - - private mapFinishReason(reason: string): 'stop' | 'length' | 'tool_calls' | 'error' { - switch (reason) { - case 'stop': return 'stop'; - case 'length': return 'length'; - case 'function_call': return 'tool_calls'; - case 'tool_calls': return 'tool_calls'; - default: return 'stop'; - } - } -} diff --git a/gateway/src/domain/providers/xai-provider.ts b/gateway/src/domain/providers/xai-provider.ts deleted file mode 100644 index df0e0ca..0000000 --- a/gateway/src/domain/providers/xai-provider.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { BaseProvider } from './base-provider.js'; -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { getConfig } from '../../infrastructure/config/app-config.js'; - -interface GrokRequest { - model: string; - messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string; }>; - max_tokens?: number; - temperature?: number; - stream?: boolean; - stop?: string | string[]; -} - -interface GrokResponse { - id: string; - object: string; - created: number; - model: string; - choices: Array<{ - index: number; - message: { role: string; content: string; }; - finish_reason: string; - }>; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - -export class XAIProvider extends BaseProvider { - readonly name = 'xAI'; - protected readonly baseUrl = 'https://api.x.ai/v1'; - protected get apiKey(): string | undefined { - return getConfig().providers.xai.apiKey; - } - - isConfigured(): boolean { - const config = getConfig(); - // xAI is available via x402 for /v1/messages - if (config.x402.enabled) { - return true; - } - return super.isConfigured(); - } - - protected transformRequest(request: CanonicalRequest): GrokRequest { - const messages = request.messages.map(msg => ({ - role: msg.role, - content: msg.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join('') - })); - - return { - model: request.model, - messages, - max_tokens: request.maxTokens, - temperature: request.temperature, - stream: request.stream || false, - stop: request.stopSequences - }; - } - - protected transformResponse(response: GrokResponse): CanonicalResponse { - const choice = response.choices[0]; - - return { - id: response.id, - model: response.model, - created: response.created, - message: { - role: 'assistant', - content: [{ - type: 'text', - text: choice.message.content - }] - }, - finishReason: this.mapFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage.prompt_tokens, - outputTokens: response.usage.completion_tokens, - totalTokens: response.usage.total_tokens - } - }; - } - - private mapFinishReason(reason: string): 'stop' | 'length' | 'tool_calls' | 'error' { - switch (reason) { - case 'stop': return 'stop'; - case 'length': return 'length'; - default: return 'stop'; - } - } -} diff --git a/gateway/src/domain/providers/zai-provider.ts b/gateway/src/domain/providers/zai-provider.ts deleted file mode 100644 index 7cc481c..0000000 --- a/gateway/src/domain/providers/zai-provider.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { Response as NodeFetchResponse } from 'node-fetch'; -import { AIProvider } from '../types/provider.js'; -import { ProviderError } from '../../shared/errors/index.js'; -import { getConfig } from '../../infrastructure/config/app-config.js'; - -/** - * Z AI provider placeholder used to participate in provider selection. - * Requests are expected to go through the messages passthrough pipeline. - */ -export class ZAIProvider implements AIProvider { - readonly name = 'zai'; - - isConfigured(): boolean { - const config = getConfig(); - // ZAI is available via x402 for /v1/messages - if (config.x402.enabled) { - return true; - } - return Boolean(config.providers.zai.apiKey); - } - - async chatCompletion(_request: CanonicalRequest): Promise { - throw new ProviderError('zai', 'Z AI provider supports passthrough-only requests', 501); - } - - async getStreamingResponse(_request: CanonicalRequest): Promise { - throw new ProviderError('zai', 'Z AI provider supports passthrough-only requests', 501); - } -} diff --git a/gateway/src/domain/services/budget-service.ts b/gateway/src/domain/services/budget-service.ts deleted file mode 100644 index bf8999e..0000000 --- a/gateway/src/domain/services/budget-service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { dbQueries } from '../../infrastructure/db/queries.js'; -import { logger } from '../../infrastructure/utils/logger.js'; -import { BudgetExceededError } from '../../shared/errors/gateway-errors.js'; - -export interface BudgetStatus { - limit: number | null; - alertOnly: boolean; - spent: number; - remaining: number | null; - window: 'monthly'; - allowed: boolean; -} - -export class BudgetService { - private getCurrentWindow(): { start: string; end: string } { - const now = new Date(); - const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0)); - return { start: start.toISOString(), end: now.toISOString() }; - } - - getBudgetStatus(estimatedCost: number = 0): BudgetStatus { - const limitRecord = dbQueries.getGlobalSpendLimit(); - const { start, end } = this.getCurrentWindow(); - const spent = dbQueries.getTotalCost(start, end); - - const limit = limitRecord?.amount_usd ?? null; - const alertOnly = Boolean(limitRecord?.alert_only); - const remaining = limit === null ? null : Math.max(0, limit - spent); - const allowed = limit === null ? true : spent + Math.max(estimatedCost, 0) <= limit; - - return { - limit, - alertOnly, - spent, - remaining, - window: 'monthly', - allowed - }; - } - - enforceBudget(estimatedCost: number = 0, requestId?: string): BudgetStatus { - const status = this.getBudgetStatus(estimatedCost); - - if (!status.allowed && !status.alertOnly) { - logger.warn('Request blocked by global budget', { - requestId, - spent: status.spent, - limit: status.limit, - estimatedCost, - window: status.window, - module: 'budget-service' - }); - throw new BudgetExceededError('Global monthly budget exceeded', { - spent: status.spent, - limit: status.limit, - window: status.window - }); - } - - if (!status.allowed && status.alertOnly) { - logger.warn('Global budget exceeded (alert-only)', { - requestId, - spent: status.spent, - limit: status.limit, - estimatedCost, - window: status.window, - module: 'budget-service' - }); - } - - return status; - } - - upsertBudget(amountUsd: number | null, alertOnly: boolean): BudgetStatus { - dbQueries.upsertGlobalSpendLimit(amountUsd, alertOnly); - const status = this.getBudgetStatus(); - logger.info('Global budget updated', { - amountUsd, - alertOnly, - window: status.window, - module: 'budget-service' - }); - return status; - } -} - -export const budgetService = new BudgetService(); diff --git a/gateway/src/domain/services/provider-registry.ts b/gateway/src/domain/services/provider-registry.ts deleted file mode 100644 index e2a227e..0000000 --- a/gateway/src/domain/services/provider-registry.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { AIProvider } from '../types/provider.js'; -import { AnthropicProvider } from '../providers/anthropic-provider.js'; -import { OpenAIProvider } from '../providers/openai-provider.js'; -import { OpenRouterProvider } from '../providers/openrouter-provider.js'; -import { XAIProvider } from '../providers/xai-provider.js'; -import { ZAIProvider } from '../providers/zai-provider.js'; -import { GoogleProvider } from '../providers/google-provider.js'; -import { OllamaProvider } from '../providers/ollama-provider.js'; - -export enum Provider { - ANTHROPIC = 'anthropic', - OPENAI = 'openai', - OPENROUTER = 'openrouter', - XAI = 'xAI', - ZAI = 'zai', - GOOGLE = 'google', - OLLAMA = 'ollama' -} - -export interface ProviderSelectionRule { - match: (modelName: string) => boolean; -} - -export interface ProviderPlugin { - id: Provider; - create: () => AIProvider; - selectionRules?: ProviderSelectionRule[]; -} - -/** - * Central registry for provider creation and selection hints. - * Keeps wiring in one place and reduces per-provider boilerplate. - */ -export class ProviderRegistry { - private readonly instances = new Map(); - - constructor(private readonly plugins: ProviderPlugin[]) {} - - listProviders(): Provider[] { - return this.plugins.map(p => p.id); - } - - getOrCreateProvider(id: Provider): AIProvider { - if (!this.instances.has(id)) { - const plugin = this.plugins.find(p => p.id === id); - if (!plugin) { - throw new Error(`Unknown provider: ${id}`); - } - this.instances.set(id, plugin.create()); - } - - const provider = this.instances.get(id); - if (!provider) { - throw new Error(`Failed to create provider: ${id}`); - } - return provider; - } - - getAvailableProviders(): Provider[] { - return this.listProviders().filter(id => { - const provider = this.getOrCreateProvider(id); - return provider.isConfigured(); - }); - } - - /** - * Return the first preferred provider whose rule matches the model name and is available. - */ - findPreferredProvider(modelName: string, available: Provider[]): Provider | undefined { - for (const plugin of this.plugins) { - if (!plugin.selectionRules || !available.includes(plugin.id)) continue; - if (plugin.selectionRules.some(rule => rule.match(modelName))) { - return plugin.id; - } - } - return undefined; - } -} - -export function createDefaultProviderRegistry(): ProviderRegistry { - const plugins: ProviderPlugin[] = [ - { id: Provider.ANTHROPIC, create: () => new AnthropicProvider() }, - { id: Provider.OPENAI, create: () => new OpenAIProvider() }, - { id: Provider.OPENROUTER, create: () => new OpenRouterProvider() }, - { id: Provider.XAI, create: () => new XAIProvider(), selectionRules: [{ match: model => model.includes('grok-') || model.includes('grok_beta') }] }, - { id: Provider.ZAI, create: () => new ZAIProvider() }, - { id: Provider.GOOGLE, create: () => new GoogleProvider(), selectionRules: [{ match: model => model.toLowerCase().includes('gemini') }] }, - { id: Provider.OLLAMA, create: () => new OllamaProvider(), selectionRules: [{ match: model => model.startsWith('ollama/') }] }, - ]; - - return new ProviderRegistry(plugins); -} diff --git a/gateway/src/domain/services/provider-service.ts b/gateway/src/domain/services/provider-service.ts deleted file mode 100644 index b907368..0000000 --- a/gateway/src/domain/services/provider-service.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { logger } from '../../infrastructure/utils/logger.js'; -import { pricingLoader } from '../../infrastructure/utils/pricing-loader.js'; -import { ModelUtils } from '../../infrastructure/utils/model-utils.js'; -import { - Provider, - ProviderRegistry, - createDefaultProviderRegistry -} from './provider-registry.js'; - -export class ProviderService { - constructor(private readonly registry: ProviderRegistry = createDefaultProviderRegistry()) {} - - getAvailableProviders(): Provider[] { - return this.registry.getAvailableProviders(); - } - - getMostOptimalProvider(modelName: string, requestId?: string): { provider: Provider; error?: never } | { provider?: never; error: { code: string; message: string } } { - const normalizedModel = ModelUtils.normalizeModelName(modelName); - const availableProviders = this.getAvailableProviders(); - - if (availableProviders.length === 0) { - logger.warn('No providers configured', { operation: 'provider_selection', requestId, module: 'provider-service' }); - return { - error: { - code: 'NO_PROVIDERS_CONFIGURED', - message: 'No inference providers are configured. Please add your API keys to the .env file.' - } - }; - } - - // Check for explicit provider matches first via registry rules - const preferred = this.registry.findPreferredProvider(modelName, availableProviders); - if (preferred) { - return { provider: preferred }; - } - - // Find cheapest available provider that supports this model - let cheapestProvider: Provider | null = null; - let lowestCost = Infinity; - - const allPricing = pricingLoader.loadAllPricing(); - - for (const providerName of availableProviders) { - const pricingConfig = allPricing.get(providerName); - const modelPricing = pricingConfig?.models[normalizedModel]; - - if (modelPricing) { - const totalCost = modelPricing.input + modelPricing.output; - if (totalCost < lowestCost) { - lowestCost = totalCost; - cheapestProvider = providerName; - } - } - } - - if (!cheapestProvider) { - logger.warn('No providers found for model', { - operation: 'provider_selection', - module: 'provider-service', - model: normalizedModel, - availableProviders, - requestId - }); - return { - error: { - code: 'MODEL_NOT_SUPPORTED', - message: `Model '${modelName}' is not supported by any available providers. Either try a different model or add more providers to your .env file.` - } - }; - } - - return { provider: cheapestProvider }; - } - - - async processChatCompletion( - request: CanonicalRequest, - providerName: Provider, - clientType?: 'openai' | 'anthropic', - originalRequest?: unknown, - requestId?: string, - ): Promise { - const provider = this.registry.getOrCreateProvider(providerName); - - // Ensure Anthropic models have required suffixes - if (providerName === Provider.ANTHROPIC) { - request.model = ModelUtils.ensureAnthropicSuffix(request.model); - } - - logger.info(`Processing chat completion`, { - provider: providerName, - model: request.model, - streaming: request.stream, - requestId, - module: 'provider-service' - }); - - return provider.chatCompletion(request); - } - - async processStreamingRequest( - request: CanonicalRequest, - providerName: Provider, - clientType?: 'openai' | 'anthropic', - originalRequest?: unknown, - requestId?: string, - ): Promise { - const provider = this.registry.getOrCreateProvider(providerName); - - // Ensure Anthropic models have required suffixes - if (providerName === Provider.ANTHROPIC) { - request.model = ModelUtils.ensureAnthropicSuffix(request.model); - } - - logger.info(`Processing streaming request`, { - provider: providerName, - model: request.model, - requestId, - module: 'provider-service' - }); - - return provider.getStreamingResponse(request); - } -} diff --git a/gateway/src/domain/types/provider.ts b/gateway/src/domain/types/provider.ts deleted file mode 100644 index 8ceac26..0000000 --- a/gateway/src/domain/types/provider.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CanonicalRequest, CanonicalResponse } from 'shared/types/index.js'; -import { Response as NodeFetchResponse } from 'node-fetch'; - -// Proper interface with all required methods -export interface AIProvider { - readonly name: string; - isConfigured(): boolean; - chatCompletion(request: CanonicalRequest): Promise; - getStreamingResponse(request: CanonicalRequest): Promise; -} - -// Type-safe provider transformation interfaces -export interface ProviderRequest { - [key: string]: any; -} - -export interface ProviderResponse { - [key: string]: any; -} - -// HTTP status constants -export const HTTP_STATUS = { - OK: 200, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - NOT_FOUND: 404, - INTERNAL_SERVER_ERROR: 500 -} as const; - -// Content type constants -export const CONTENT_TYPES = { - JSON: 'application/json', - TEXT_PLAIN: 'text/plain', - EVENT_STREAM: 'text/event-stream' -} as const; \ No newline at end of file diff --git a/gateway/src/index.ts b/gateway/src/index.ts deleted file mode 100644 index 74df8f0..0000000 --- a/gateway/src/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -// Load environment variables before importing modules -import dotenv from 'dotenv'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { existsSync } from 'fs'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Find project root by looking for package.json (works in both dev and prod) -function findProjectRoot(startPath: string): string { - let currentPath = startPath; - while (currentPath !== dirname(currentPath)) { - if (existsSync(join(currentPath, 'package.json')) && - existsSync(join(currentPath, '.env.example'))) { - return currentPath; - } - currentPath = dirname(currentPath); - } - // Fallback to process.cwd() if not found - return process.cwd(); -} - -const projectRoot = findProjectRoot(__dirname); -dotenv.config({ path: join(projectRoot, '.env') }); - -// Import application modules -import express from 'express'; -import cors from 'cors'; -import { handleOpenAIFormatChat, handleAnthropicFormatChat, handleOpenAIResponses } from './app/handlers/chat-handler.js'; -import { handleUsageRequest } from './app/handlers/usage-handler.js'; -import { handleConfigStatus } from './app/handlers/config-handler.js'; -import { handleGetBudget, handleUpdateBudget } from './app/handlers/budget-handler.js'; -import { logger } from './infrastructure/utils/logger.js'; -import { requestContext } from './infrastructure/middleware/request-context.js'; -import { requestLogging } from './infrastructure/middleware/request-logging.js'; -import { ProviderService } from './domain/services/provider-service.js'; -import { pricingLoader } from './infrastructure/utils/pricing-loader.js'; -import { getConfig } from './infrastructure/config/app-config.js'; -import { errorHandler } from './infrastructure/middleware/error-handler.js'; - -async function bootstrap(): Promise { - // Initialize and validate config - const config = getConfig(); - - if (config.x402.enabled) { - logger.info('x402 payment gateway enabled', { - x402BaseUrl: config.x402.baseUrl, - mode: config.getMode(), - chatCompletions: 'OpenRouter only', - chatCompletionsUrl: config.x402.chatCompletionsUrl, - messages: 'All providers', - messagesUrl: config.x402.messagesUrl, - module: 'bootstrap' - }); - } - - await pricingLoader.refreshOpenRouterPricing(); - ensureProvidersConfigured(); - - const app = express(); - const PORT = config.server.port; - // Middleware - app.set('trust proxy', true); - app.use(cors()); - app.use(requestContext); - app.use(requestLogging); - app.use(express.json({ limit: '50mb' })); - - // Health check - app.get('/health', (req, res) => { - logger.debug('Health check accessed', { - requestId: req.requestId, - module: 'health-endpoint' - }); - - res.json({ - status: 'ok', - timestamp: new Date().toISOString(), - version: '1.0.0' - }); - }); - - // API Routes - app.post('/v1/chat/completions', handleOpenAIFormatChat); - app.post('/v1/messages', handleAnthropicFormatChat); - app.post('/v1/responses', handleOpenAIResponses); -app.get('/usage', handleUsageRequest); - app.get('/config/status', handleConfigStatus); - app.get('/budget', handleGetBudget); - app.put('/budget', handleUpdateBudget); - - // Error handler MUST be last middleware - app.use(errorHandler); - - // Start server - app.listen(PORT, () => { - logger.info('Ekai Gateway server started', { - port: PORT, - environment: config.server.environment, - mode: config.getMode(), - module: 'server' - }); - }); -} - -function ensureProvidersConfigured(): void { - const config = getConfig(); - const providerService = new ProviderService(); - const availableProviders = providerService.getAvailableProviders(); - - // Config validation already ensures we have at least one auth method - // Just log the mode we're running in - const mode = config.getMode(); - - if (mode === 'x402-only') { - logger.info('Running in x402 payment-only mode (no API keys configured)', { - mode, - module: 'bootstrap' - }); - } else if (mode === 'hybrid') { - logger.info('Running in hybrid mode (API keys + x402 payments)', { - mode, - availableProviders: availableProviders.length, - module: 'bootstrap' - }); - } else { - logger.info('Running in BYOK mode (API keys only, no x402)', { - mode, - availableProviders: availableProviders.length, - module: 'bootstrap' - }); - } -} - -bootstrap().catch(error => { - logger.error('Gateway failed to start', error, { module: 'bootstrap' }); - process.exit(1); -}); diff --git a/gateway/src/infrastructure/adapters/anthropic-adapter.ts b/gateway/src/infrastructure/adapters/anthropic-adapter.ts deleted file mode 100644 index 26cb651..0000000 --- a/gateway/src/infrastructure/adapters/anthropic-adapter.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - CanonicalRequest, - CanonicalResponse, - CanonicalStreamChunk, - CanonicalContent, - CanonicalMessage, - FormatAdapter -} from 'shared/types/canonical.js'; -import { AnthropicMessagesRequest, AnthropicMessagesResponse } from 'shared/types/types.js'; - -export class AnthropicAdapter implements FormatAdapter { - - toCanonical(input: AnthropicMessagesRequest): CanonicalRequest { - const messages: CanonicalMessage[] = []; - - // Handle system message - if (input.system) { - const systemContent = this.normalizeContent(input.system); - messages.push({ - role: 'system', - content: systemContent - }); - } - - // Handle regular messages - input.messages.forEach(msg => { - messages.push({ - role: msg.role, - content: this.normalizeContent(msg.content) - }); - }); - - return { - model: input.model, - messages, - maxTokens: input.max_tokens, - temperature: input.temperature, - stream: input.stream || false, - - // Anthropic-specific features in metadata - metadata: { - promptCaching: input.metadata?.promptCaching, - // Preserve any other fields - ...Object.fromEntries( - Object.entries(input).filter(([key]) => - !['model', 'messages', 'max_tokens', 'system', 'temperature', 'stream'].includes(key) - ) - ) - } - }; - } - - fromCanonical(response: CanonicalResponse): AnthropicMessagesResponse { - // Convert canonical content back to Anthropic format - const content = response.message.content.map(c => { - if (c.type === 'text') { - return { type: 'text', text: c.text || '' }; - } - // Handle other content types as needed - return { type: 'text', text: '' }; - }); - - return { - id: response.id, - type: 'message', - role: 'assistant', - content: content as Array<{ type: "text"; text: string; }>, - model: response.model, - stop_reason: this.mapFinishReason(response.finishReason), - usage: { - input_tokens: response.usage.inputTokens, - output_tokens: response.usage.outputTokens - } - }; - } - - fromCanonicalStream(chunk: CanonicalStreamChunk): string { - // Convert to Anthropic streaming format - if (chunk.delta.content && chunk.delta.content.length > 0) { - const textContent = chunk.delta.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join(''); - - if (textContent) { - const anthropicChunk = { - type: 'content_block_delta', - index: 0, - delta: { - type: 'text_delta', - text: textContent - } - }; - return `data: ${JSON.stringify(anthropicChunk)}\n\n`; - } - } - - // Handle final chunk with usage - if (chunk.finishReason && chunk.usage) { - const finalChunk = { - type: 'message_delta', - delta: {}, - usage: { - output_tokens: chunk.usage.outputTokens || 0 - } - }; - return `data: ${JSON.stringify(finalChunk)}\n\n`; - } - - return ''; - } - - private normalizeContent(content: string | Array<{ type: string; text: string }>): CanonicalContent[] { - if (typeof content === 'string') { - return [{ - type: 'text', - text: content - }]; - } - - if (Array.isArray(content)) { - return content.map(item => ({ - type: 'text', - text: item.text - })); - } - - return [{ type: 'text', text: String(content) }]; - } - - private mapFinishReason(reason: string): 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' { - switch (reason) { - case 'stop': return 'end_turn'; - case 'length': return 'max_tokens'; - case 'tool_calls': return 'tool_use'; - default: return 'end_turn'; - } - } -} \ No newline at end of file diff --git a/gateway/src/infrastructure/adapters/openai-adapter.ts b/gateway/src/infrastructure/adapters/openai-adapter.ts deleted file mode 100644 index da39ec3..0000000 --- a/gateway/src/infrastructure/adapters/openai-adapter.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - CanonicalRequest, - CanonicalResponse, - CanonicalStreamChunk, - CanonicalContent, - CanonicalMessage, - FormatAdapter -} from 'shared/types/canonical.js'; -import { ChatCompletionRequest, ChatCompletionResponse } from 'shared/types/types.js'; - -export class OpenAIAdapter implements FormatAdapter { - - toCanonical(input: ChatCompletionRequest): CanonicalRequest { - const messages: CanonicalMessage[] = input.messages.map(msg => ({ - role: msg.role as 'system' | 'user' | 'assistant', - content: [{ - type: 'text', - text: msg.content - }] as CanonicalContent[] - })); - - return { - model: input.model, - messages, - maxTokens: input.max_tokens, - temperature: input.temperature, - topP: input.top_p, - stopSequences: Array.isArray(input.stop) ? input.stop : input.stop ? [input.stop] : undefined, - stream: input.stream || false, - - // OpenAI-specific features in metadata - metadata: { - presencePenalty: input.presence_penalty, - frequencyPenalty: input.frequency_penalty, - logitBias: input.logit_bias, - userId: input.user, - // Preserve any other fields - ...Object.fromEntries( - Object.entries(input).filter(([key]) => - !['model', 'messages', 'max_tokens', 'temperature', 'top_p', 'stop', 'stream', - 'presence_penalty', 'frequency_penalty', 'logit_bias', 'user'].includes(key) - ) - ) - } - }; - } - - fromCanonical(response: CanonicalResponse): ChatCompletionResponse { - // Extract text content from canonical content array - const textContent = response.message.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join(''); - - return { - id: response.id, - object: 'chat.completion', - created: response.created, - model: response.model, - choices: [{ - index: 0, - message: { - role: response.message.role, - content: textContent - }, - finish_reason: this.mapFinishReason(response.finishReason) - }], - usage: { - prompt_tokens: response.usage.inputTokens, - completion_tokens: response.usage.outputTokens, - total_tokens: response.usage.totalTokens - } - }; - } - - fromCanonicalStream(chunk: CanonicalStreamChunk): string { - const textDelta = chunk.delta.content - ?.filter(c => c.type === 'text') - ?.map(c => c.text) - ?.join('') || ''; - - const openaiChunk = { - id: chunk.id, - object: 'chat.completion.chunk', - created: chunk.created, - model: chunk.model, - choices: [{ - index: 0, - delta: { - role: chunk.delta.role, - content: textDelta || undefined - }, - finish_reason: chunk.finishReason ? this.mapFinishReason(chunk.finishReason) : null - }] - }; - - // Add usage in final chunk - if (chunk.usage && chunk.finishReason) { - (openaiChunk as any).usage = { - prompt_tokens: chunk.usage.inputTokens || 0, - completion_tokens: chunk.usage.outputTokens || 0, - total_tokens: (chunk.usage.inputTokens || 0) + (chunk.usage.outputTokens || 0) - }; - } - - return `data: ${JSON.stringify(openaiChunk)}\n\n`; - } - - private mapFinishReason(reason: string): string { - switch (reason) { - case 'stop': return 'stop'; - case 'length': return 'length'; - case 'tool_calls': return 'tool_calls'; - case 'error': return 'stop'; - default: return 'stop'; - } - } -} \ No newline at end of file diff --git a/gateway/src/infrastructure/adapters/openai-responses-adapter.ts b/gateway/src/infrastructure/adapters/openai-responses-adapter.ts deleted file mode 100644 index 5592590..0000000 --- a/gateway/src/infrastructure/adapters/openai-responses-adapter.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Minimal adapter for OpenAI Responses API request/response shapes -// Converts between Responses API and canonical format used internally. -import { - CanonicalRequest, - CanonicalResponse, - CanonicalStreamChunk, - CanonicalContent, - CanonicalMessage, - FormatAdapter -} from 'shared/types/canonical.js'; - -type ResponsesInput = - | string - | Array<{ role?: 'system' | 'user' | 'assistant'; content: Array<{ type: string; text?: string }> }>; - -interface OpenAIResponsesRequest { - model: string; - input: ResponsesInput; - stream?: boolean; - temperature?: number; - max_output_tokens?: number; - [key: string]: any; -} - -interface OpenAIResponsesResponse { - id: string; - object: 'response'; - created: number; - model: string; - output_text?: string; - // Fallback structures (not exhaustively typed) - output?: Array<{ content: Array<{ type: string; text?: string }> }>; - usage?: { input_tokens?: number; output_tokens?: number; total_tokens?: number }; -} - -export class OpenAIResponsesAdapter implements FormatAdapter { - toCanonical(input: OpenAIResponsesRequest): CanonicalRequest { - const messages: CanonicalMessage[] = []; - - // Normalize input to canonical messages - if (typeof input.input === 'string') { - messages.push({ - role: 'user', - content: [{ type: 'text', text: input.input }] as CanonicalContent[], - }); - } else if (Array.isArray(input.input)) { - for (const item of input.input) { - const role = item.role ?? 'user'; - const parts: CanonicalContent[] = []; - for (const c of item.content || []) { - if (c.type === 'input_text' || c.type === 'text') { - parts.push({ type: 'text', text: c.text || '' }); - } - } - messages.push({ role, content: parts }); - } - } - - return { - model: (input as any).model, - messages, - maxTokens: (input as any).max_output_tokens ?? (input as any).max_tokens, - temperature: input.temperature, - stream: !!input.stream, - metadata: { useResponsesAPI: true } - } as unknown as CanonicalRequest; - } - - fromCanonical(response: CanonicalResponse): OpenAIResponsesResponse { - const textContent = response.message.content - .filter(c => c.type === 'text') - .map(c => c.text) - .join(''); - - return { - id: response.id, - object: 'response', - created: response.created, - model: response.model, - output_text: textContent, - usage: { - input_tokens: response.usage.inputTokens, - output_tokens: response.usage.outputTokens, - total_tokens: response.usage.totalTokens - } - } as OpenAIResponsesResponse; - } - - fromCanonicalStream(chunk: CanonicalStreamChunk): string { - // Map canonical text deltas to Responses API stream-like event lines - const textDelta = chunk.delta.content - ?.filter(c => c.type === 'text') - ?.map(c => c.text) - ?.join('') || ''; - - if (textDelta) { - // Responses API uses SSE events; this is a best-effort mapping - const evt = { type: 'response.output_text.delta', delta: textDelta }; - return `data: ${JSON.stringify(evt)}\n\n`; - } - - if (chunk.finishReason) { - const done = { type: 'response.completed' }; - return `data: ${JSON.stringify(done)}\n\n`; - } - - return ''; - } -} diff --git a/gateway/src/infrastructure/config.ts b/gateway/src/infrastructure/config.ts deleted file mode 100644 index 0f2342f..0000000 --- a/gateway/src/infrastructure/config.ts +++ /dev/null @@ -1,32 +0,0 @@ -import dotenv from 'dotenv'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; -import { existsSync } from 'fs'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Find project root by looking for package.json (works in both dev and prod) -function findProjectRoot(startPath: string): string { - let currentPath = startPath; - while (currentPath !== dirname(currentPath)) { - if (existsSync(join(currentPath, 'package.json')) && - existsSync(join(currentPath, '.env.example'))) { - return currentPath; - } - currentPath = dirname(currentPath); - } - // Fallback to process.cwd() if not found - return process.cwd(); -} - -// Load environment exactly once for any module that imports config -const projectRoot = findProjectRoot(__dirname); -dotenv.config({ path: join(projectRoot, '.env') }); - -// Export normalized configuration values -// NOTE: These are deprecated - use getConfig() from app-config.ts instead -export const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; -export const SERVICE_NAME = 'ekai-gateway'; -export const SERVICE_VERSION = process.env.npm_package_version || 'dev'; - diff --git a/gateway/src/infrastructure/config/app-config.ts b/gateway/src/infrastructure/config/app-config.ts deleted file mode 100644 index 7fa4b73..0000000 --- a/gateway/src/infrastructure/config/app-config.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Centralized application configuration - * All environment variables are validated and accessed through this class - */ - -export class AppConfig { - // Server configuration - readonly server = { - port: this.getNumber('PORT', 3001), - environment: this.getString('NODE_ENV', 'development'), - isDevelopment: this.getString('NODE_ENV', 'development') === 'development', - isProduction: this.getString('NODE_ENV', 'development') === 'production', - version: this.getOptionalString('npm_package_version') || 'dev', - }; - - // x402 Payment configuration - readonly x402 = { - enabled: this.has('PRIVATE_KEY'), - privateKey: this.getOptionalString('PRIVATE_KEY'), - baseUrl: this.getString('X402_BASE_URL', 'https://x402.ekailabs.xyz'), - - // Helper methods - get chatCompletionsUrl() { - return `${this.baseUrl}/v1/chat/completions`; - }, - get messagesUrl() { - return `${this.baseUrl}/v1/messages`; - }, - }; - - // Provider API Keys - readonly providers = { - anthropic: { - apiKey: this.getOptionalString('ANTHROPIC_API_KEY'), - enabled: this.has('ANTHROPIC_API_KEY'), - }, - openai: { - apiKey: this.getOptionalString('OPENAI_API_KEY'), - enabled: this.has('OPENAI_API_KEY'), - }, - openrouter: { - apiKey: this.getOptionalString('OPENROUTER_API_KEY'), - enabled: this.has('OPENROUTER_API_KEY'), - }, - xai: { - apiKey: this.getOptionalString('XAI_API_KEY'), - enabled: this.has('XAI_API_KEY'), - }, - zai: { - apiKey: this.getOptionalString('ZAI_API_KEY'), - enabled: this.has('ZAI_API_KEY'), - }, - google: { - apiKey: this.getOptionalString('GOOGLE_API_KEY'), - enabled: this.has('GOOGLE_API_KEY'), - }, - ollama: { - baseUrl: this.getString('OLLAMA_BASE_URL', 'http://localhost:11434/v1'), - apiKey: this.getOptionalString('OLLAMA_API_KEY'), - enabled: this.has('OLLAMA_BASE_URL'), - }, - }; - - // OpenRouter-specific configuration - readonly openrouter = { - skipPricingRefresh: this.getBoolean('SKIP_OPENROUTER_PRICING_REFRESH', false), - pricingTimeoutMs: this.getNumber('OPENROUTER_PRICING_TIMEOUT_MS', 4000), - pricingRetries: this.getNumber('OPENROUTER_PRICING_RETRIES', 2), - }; - - // Helper methods - private has(key: string): boolean { - return !!process.env[key]; - } - - private getString(key: string, defaultValue: string): string; - private getString(key: string): string; - private getString(key: string, defaultValue?: string): string { - const value = process.env[key] || defaultValue; - if (value === undefined) { - throw new Error(`Missing required environment variable: ${key}`); - } - return value; - } - - private getOptionalString(key: string): string | undefined { - return process.env[key]; - } - - private getNumber(key: string, defaultValue: number): number { - const value = process.env[key]; - if (!value) return defaultValue; - const num = parseInt(value, 10); - if (isNaN(num)) { - throw new Error(`Invalid number for environment variable ${key}: ${value}`); - } - return num; - } - - private getBoolean(key: string, defaultValue: boolean): boolean { - const value = process.env[key]; - if (!value) return defaultValue; - return value.toLowerCase() === 'true' || value === '1'; - } - - /** - * Validate that at least one authentication method is configured - */ - validate(): void { - const hasApiKeys = Object.values(this.providers).some(p => p.enabled); - const hasX402 = this.x402.enabled; - - if (!hasApiKeys && !hasX402) { - throw new Error( - 'No authentication configured. Set either:\n' + - ' 1. At least one provider API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n' + - ' 2. PRIVATE_KEY for x402 payment mode' - ); - } - } - - /** - * Get human-readable mode description - */ - getMode(): 'x402-only' | 'hybrid' | 'byok' { - const hasApiKeys = Object.values(this.providers).some(p => p.enabled); - const hasX402 = this.x402.enabled; - - if (!hasApiKeys && hasX402) return 'x402-only'; - if (hasApiKeys && hasX402) return 'hybrid'; - return 'byok'; - } -} - -// Singleton instance -let configInstance: AppConfig | null = null; - -export function getConfig(): AppConfig { - if (!configInstance) { - configInstance = new AppConfig(); - configInstance.validate(); - } - return configInstance; -} - -// For testing - reset config -export function resetConfig(): void { - configInstance = null; -} diff --git a/gateway/src/infrastructure/db/connection.ts b/gateway/src/infrastructure/db/connection.ts deleted file mode 100644 index a0318ff..0000000 --- a/gateway/src/infrastructure/db/connection.ts +++ /dev/null @@ -1,89 +0,0 @@ -import Database from 'better-sqlite3'; -import { readFileSync, existsSync, mkdirSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { logger } from '../utils/logger.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -class DatabaseConnection { - private db: Database.Database | null = null; - - constructor() { - this.initialize(); - } - - private initialize() { - try { - // Use DATABASE_PATH env var, or default to data/proxy.db relative to cwd - // In Docker: cwd = /app/gateway/ → /app/gateway/data/proxy.db (matches volume mount) - // In dev: cwd = gateway/ → gateway/data/proxy.db - const dbPath = process.env.DATABASE_PATH || join(process.cwd(), 'data', 'proxy.db'); - const dbDir = dirname(dbPath); - if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true }); - this.db = new Database(dbPath); - - // Enable WAL mode for better concurrency - this.db.pragma('journal_mode = WAL'); - - // Create tables from schema - this.createTables(); - - logger.info('Database initialized', { operation: 'db_init', module: 'db-connection' }); - } catch (error) { - logger.error('Database initialization failed', error, { operation: 'db_init', module: 'db-connection' }); - throw error; - } - } - - private createTables() { - if (!this.db) throw new Error('Database not initialized'); - - try { - // Read and execute schema - const schemaPath = join(__dirname, 'schema.sql'); - const schema = readFileSync(schemaPath, 'utf8'); - - // Execute schema (split by semicolon and execute each statement) - const statements = schema.split(';').filter(stmt => stmt.trim()); - statements.forEach(statement => { - if (statement.trim()) { - this.db!.exec(statement); - } - }); - - // Lightweight migration: add payment_method column if missing - const columns = this.db.prepare(`PRAGMA table_info(usage_records)`).all(); - const hasPaymentMethod = columns.some((c: any) => c.name === 'payment_method'); - if (!hasPaymentMethod) { - this.db.exec(`ALTER TABLE usage_records ADD COLUMN payment_method TEXT DEFAULT 'api_key';`); - logger.info('Added payment_method column to usage_records', { module: 'db-connection', operation: 'db_migration' }); - } - - logger.debug('Database tables created', { operation: 'db_schema', module: 'db-connection' }); - } catch (error) { - logger.error('Failed to create tables', error, { operation: 'db_schema', module: 'db-connection' }); - throw error; - } - } - - getDatabase(): Database.Database { - if (!this.db) { - throw new Error('Database not initialized'); - } - return this.db; - } - - close() { - if (this.db) { - this.db.close(); - this.db = null; - logger.info('Database connection closed', { operation: 'db_cleanup', module: 'db-connection' }); - } - } -} - -// Export singleton instance -export const dbConnection = new DatabaseConnection(); -export default dbConnection; diff --git a/gateway/src/infrastructure/db/index.ts b/gateway/src/infrastructure/db/index.ts deleted file mode 100644 index ce7c58b..0000000 --- a/gateway/src/infrastructure/db/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Database exports -export { dbConnection } from './connection.js'; -export { dbQueries, type UsageRecord } from './queries.js'; - -// Re-export for convenience -export { default as connection } from './connection.js'; -export { default as queries } from './queries.js'; diff --git a/gateway/src/infrastructure/db/queries.ts b/gateway/src/infrastructure/db/queries.ts deleted file mode 100644 index 6cf8919..0000000 --- a/gateway/src/infrastructure/db/queries.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { dbConnection } from './connection.js'; - -export interface UsageRecord { - id: number; - request_id: string; - provider: string; - model: string; - timestamp: string; - input_tokens: number; - cache_write_input_tokens: number; - cache_read_input_tokens: number; - output_tokens: number; - total_tokens: number; - input_cost: number; - cache_write_cost: number; - cache_read_cost: number; - output_cost: number; - total_cost: number; - currency: string; - payment_method?: string; - created_at: string; -} - -export interface SpendLimitRecord { - amount_usd: number | null; - alert_only: boolean; - window: string; - scope: string; - updated_at: string; -} - -export class DatabaseQueries { - private db = dbConnection.getDatabase(); - - // Insert a new usage record - insertUsageRecord(record: Omit): number { - const stmt = this.db.prepare(` - INSERT INTO usage_records ( - request_id, provider, model, timestamp, - input_tokens, cache_write_input_tokens, cache_read_input_tokens, output_tokens, total_tokens, - input_cost, cache_write_cost, cache_read_cost, output_cost, total_cost, currency, payment_method - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - const result = stmt.run( - record.request_id, - record.provider, - record.model, - record.timestamp, - record.input_tokens, - record.cache_write_input_tokens, - record.cache_read_input_tokens, - record.output_tokens, - record.total_tokens, - record.input_cost, - record.cache_write_cost, - record.cache_read_cost, - record.output_cost, - record.total_cost, - record.currency, - record.payment_method || 'api_key' - ); - - return result.lastInsertRowid as number; - } - - // Get all usage records (with optional limit, required date range) - getAllUsageRecords(limit: number = 100, startDate: string, endDate: string): UsageRecord[] { - const stmt = this.db.prepare(` - SELECT * FROM usage_records - WHERE timestamp >= ? AND timestamp < ? - ORDER BY timestamp DESC - LIMIT ? - `); - return stmt.all(startDate, endDate, limit) as UsageRecord[]; - } - - // Get usage records by date range - getUsageRecordsByDateRange(startDate: string, endDate: string): UsageRecord[] { - const stmt = this.db.prepare(` - SELECT * FROM usage_records - WHERE timestamp BETWEEN ? AND ? - ORDER BY timestamp DESC - `); - return stmt.all(startDate, endDate) as UsageRecord[]; - } - - // Get total cost (with date range) - getTotalCost(startDate: string, endDate: string): number { - const stmt = this.db.prepare(` - SELECT SUM(total_cost) as total FROM usage_records - WHERE timestamp >= ? AND timestamp < ? - `); - const result = stmt.get(startDate, endDate) as { total: number | null }; - return result.total || 0; - } - - // Get total tokens (with date range) - getTotalTokens(startDate: string, endDate: string): number { - const stmt = this.db.prepare(` - SELECT SUM(total_tokens) as total FROM usage_records - WHERE timestamp >= ? AND timestamp < ? - `); - const result = stmt.get(startDate, endDate) as { total: number | null }; - return result.total || 0; - } - - // Get total requests (with date range) - getTotalRequests(startDate: string, endDate: string): number { - const stmt = this.db.prepare(` - SELECT COUNT(*) as total FROM usage_records - WHERE timestamp >= ? AND timestamp < ? - `); - const result = stmt.get(startDate, endDate) as { total: number }; - return result.total; - } - - // Get cost by provider (with date range) - getCostByProvider(startDate: string, endDate: string): Record { - const stmt = this.db.prepare(` - SELECT provider, SUM(total_cost) as total - FROM usage_records - WHERE timestamp >= ? AND timestamp < ? - GROUP BY provider - `); - const results = stmt.all(startDate, endDate) as Array<{ provider: string; total: number }>; - - const costByProvider: Record = {}; - results.forEach(row => { - costByProvider[row.provider] = row.total; - }); - - return costByProvider; - } - - // Get cost by model (with date range) - getCostByModel(startDate: string, endDate: string): Record { - const stmt = this.db.prepare(` - SELECT model, SUM(total_cost) as total - FROM usage_records - WHERE timestamp >= ? AND timestamp < ? - GROUP BY model - `); - const results = stmt.all(startDate, endDate) as Array<{ model: string; total: number }>; - - const costByModel: Record = {}; - results.forEach(row => { - costByModel[row.model] = row.total; - }); - - return costByModel; - } - - // Get global spend limit (single row) - getGlobalSpendLimit(): SpendLimitRecord | null { - const stmt = this.db.prepare(` - SELECT scope, amount_usd, alert_only, window, updated_at - FROM spend_limits - WHERE id = 1 - LIMIT 1 - `); - const result = stmt.get() as { scope: string; amount_usd: number | null; alert_only: number; window: string; updated_at: string } | undefined; - if (!result) return null; - return { - scope: result.scope, - amount_usd: result.amount_usd, - alert_only: Boolean(result.alert_only), - window: result.window, - updated_at: result.updated_at - }; - } - - // Upsert global spend limit - upsertGlobalSpendLimit(amountUsd: number | null, alertOnly: boolean): void { - const stmt = this.db.prepare(` - INSERT INTO spend_limits (id, scope, amount_usd, alert_only, window, updated_at) - VALUES (1, 'global', ?, ?, 'monthly', CURRENT_TIMESTAMP) - ON CONFLICT(id) DO UPDATE SET - amount_usd = excluded.amount_usd, - alert_only = excluded.alert_only, - window = excluded.window, - updated_at = excluded.updated_at - `); - - stmt.run(amountUsd, alertOnly ? 1 : 0); - } - -} - -// Export singleton instance -export const dbQueries = new DatabaseQueries(); -export default dbQueries; diff --git a/gateway/src/infrastructure/db/schema.sql b/gateway/src/infrastructure/db/schema.sql deleted file mode 100644 index c8fc059..0000000 --- a/gateway/src/infrastructure/db/schema.sql +++ /dev/null @@ -1,40 +0,0 @@ --- AI Proxy Database Schema --- Minimal setup for usage tracking - --- Usage records table -CREATE TABLE IF NOT EXISTS usage_records ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id TEXT UNIQUE NOT NULL, - provider TEXT NOT NULL, - model TEXT NOT NULL, - timestamp DATETIME NOT NULL, - input_tokens INTEGER NOT NULL, - cache_write_input_tokens INTEGER DEFAULT 0, - cache_read_input_tokens INTEGER DEFAULT 0, - output_tokens INTEGER NOT NULL, - total_tokens INTEGER NOT NULL, - input_cost REAL NOT NULL, - cache_write_cost REAL DEFAULT 0, - cache_read_cost REAL DEFAULT 0, - output_cost REAL NOT NULL, - total_cost REAL NOT NULL, - currency TEXT DEFAULT 'USD', - payment_method TEXT DEFAULT 'api_key', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - --- Indexes for fast queries -CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_records(timestamp); -CREATE INDEX IF NOT EXISTS idx_usage_provider ON usage_records(provider); -CREATE INDEX IF NOT EXISTS idx_usage_model ON usage_records(model); -CREATE INDEX IF NOT EXISTS idx_usage_cost ON usage_records(total_cost); - --- Global spend limit (single-row table) -CREATE TABLE IF NOT EXISTS spend_limits ( - id INTEGER PRIMARY KEY CHECK (id = 1), - scope TEXT NOT NULL DEFAULT 'global', - amount_usd REAL, - alert_only INTEGER DEFAULT 0, - window TEXT NOT NULL DEFAULT 'monthly', - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); diff --git a/gateway/src/infrastructure/middleware/error-handler.ts b/gateway/src/infrastructure/middleware/error-handler.ts deleted file mode 100644 index 002d148..0000000 --- a/gateway/src/infrastructure/middleware/error-handler.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { GatewayError, toGatewayError } from '../../shared/errors/index.js'; -import { logger } from '../utils/logger.js'; - -/** - * Centralized error handling middleware - * Converts all errors to consistent format and logs them - */ -export function errorHandler( - error: unknown, - req: Request, - res: Response, - next: NextFunction -): void { - // Convert to GatewayError if not already - const gatewayError = toGatewayError(error); - - // Log error with context - const logContext = { - requestId: (req as any).requestId, - method: req.method, - path: req.path, - errorCode: gatewayError.code, - statusCode: gatewayError.statusCode, - ...gatewayError.context, - module: 'error-handler', - }; - - if (gatewayError.statusCode >= 500) { - logger.error(gatewayError.message, gatewayError, logContext); - } else { - logger.warn(gatewayError.message, logContext); - } - - // Send error response - res.status(gatewayError.statusCode).json({ - error: { - code: gatewayError.code, - message: gatewayError.message, - ...(gatewayError.context && { context: gatewayError.context }), - }, - ...(req.app.get('env') === 'development' && { - stack: gatewayError.stack, - }), - }); -} - -/** - * Async handler wrapper to catch promise rejections - */ -export function asyncHandler( - fn: (req: T, res: Response, next: NextFunction) => Promise -) { - return (req: T, res: Response, next: NextFunction) => { - Promise.resolve(fn(req, res, next)).catch(next); - }; -} - diff --git a/gateway/src/infrastructure/middleware/request-context.ts b/gateway/src/infrastructure/middleware/request-context.ts deleted file mode 100644 index d53a9eb..0000000 --- a/gateway/src/infrastructure/middleware/request-context.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { randomUUID } from 'crypto'; - -declare global { - namespace Express { - interface Request { - requestId: string; - clientIp?: string; - } - } -} - -export function requestContext(req: Request, res: Response, next: NextFunction): void { - req.requestId = randomUUID(); - // Determine client IP considering proxies - const xff = (req.headers['x-forwarded-for'] || '') as string; - // x-forwarded-for may be a comma-separated list, take the first non-empty - const forwarded = xff.split(',').map(s => s.trim()).filter(Boolean)[0]; - const remote = (req.socket && (req.socket as any).remoteAddress) || (req as any).ip || undefined; - req.clientIp = forwarded || remote; - res.setHeader('X-Request-ID', req.requestId); - if (req.clientIp) { - res.setHeader('X-Client-IP', req.clientIp); - } - next(); -} diff --git a/gateway/src/infrastructure/middleware/request-logging.ts b/gateway/src/infrastructure/middleware/request-logging.ts deleted file mode 100644 index f81068f..0000000 --- a/gateway/src/infrastructure/middleware/request-logging.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { logger } from '../utils/logger.js'; - -export const requestLogging = (req: any, res: Response, next: NextFunction): void => { - const startTime = Date.now(); - req.startTime = startTime; - - // Log request start - logger.info('HTTP Request', { - method: req.method, - url: req.url, - userAgent: req.get('User-Agent'), - requestId: req.requestId, - ip: req.clientIp || req.ip || req.socket.remoteAddress, - module: 'http-middleware' - }); - - // Override res.end to capture response - const originalEnd = res.end; - res.end = function(chunk?: any, encoding?: any): Response { - const duration = Date.now() - startTime; - - // Log response - logger.info('HTTP Response', { - method: req.method, - url: req.url, - statusCode: res.statusCode, - duration, - requestId: req.requestId, - ip: req.clientIp || req.ip || req.socket.remoteAddress, - contentLength: res.get('Content-Length'), - module: 'http-middleware' - }); - - // Call original end method - return originalEnd.call(this, chunk, encoding); - }; - - next(); -}; diff --git a/gateway/src/infrastructure/passthrough/chat-completions-passthrough-registry.ts b/gateway/src/infrastructure/passthrough/chat-completions-passthrough-registry.ts deleted file mode 100644 index 895a93a..0000000 --- a/gateway/src/infrastructure/passthrough/chat-completions-passthrough-registry.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { ModelUtils } from '../utils/model-utils.js'; -import { ChatCompletionsPassthrough, ChatCompletionsPassthroughConfig } from './chat-completions-passthrough.js'; -import { loadChatCompletionsProviderDefinitions, ChatCompletionsProviderDefinition } from './chat-completions-provider-config.js'; - -interface ProviderEntry { - definition: ChatCompletionsProviderDefinition; - passthrough?: ChatCompletionsPassthrough; -} - -export class ChatCompletionsPassthroughRegistry { - private readonly providers = new Map(); - private readonly modelToProvider = new Map(); - - constructor(definitions: ChatCompletionsProviderDefinition[]) { - definitions.forEach(def => { - this.providers.set(def.provider, { definition: def }); - - def.models.forEach(modelId => { - const normalized = ModelUtils.removeProviderPrefix(modelId); - this.modelToProvider.set(modelId, def.provider); - this.modelToProvider.set(normalized, def.provider); - this.modelToProvider.set(`${def.provider}/${normalized}`, def.provider); - }); - }); - } - - static fromCatalog(): ChatCompletionsPassthroughRegistry { - const definitions = loadChatCompletionsProviderDefinitions(); - return new ChatCompletionsPassthroughRegistry(definitions); - } - - listProviders(): string[] { - return Array.from(this.providers.keys()); - } - - getSupportedClientFormats(provider: string): string[] { - const entry = this.providers.get(provider); - return entry?.definition.config.supportedClientFormats ?? []; - } - - findProviderByModel(modelName: string): string | undefined { - if (!modelName) return undefined; - const direct = this.modelToProvider.get(modelName); - if (direct) return direct; - - const normalized = ModelUtils.removeProviderPrefix(modelName); - const mapped = this.modelToProvider.get(normalized); - if (mapped) return mapped; - - // Fallback for OpenRouter aggregator: any provider/model slug should route through openrouter passthrough - if (modelName.includes('/')) { - const openRouterEntry = this.providers.get('openrouter'); - if (openRouterEntry) { - return 'openrouter'; - } - } - - return undefined; - } - - getConfig(provider: string): ChatCompletionsPassthroughConfig | undefined { - const entry = this.providers.get(provider); - return entry?.definition.config; - } - - getPassthrough(provider: string): ChatCompletionsPassthrough | undefined { - const entry = this.providers.get(provider); - if (!entry) return undefined; - - if (!entry.passthrough) { - entry.passthrough = new ChatCompletionsPassthrough(entry.definition.config); - this.providers.set(provider, entry); - } - - return entry.passthrough; - } -} - -export function createChatCompletionsPassthroughRegistry(): ChatCompletionsPassthroughRegistry { - return ChatCompletionsPassthroughRegistry.fromCatalog(); -} diff --git a/gateway/src/infrastructure/passthrough/chat-completions-passthrough.ts b/gateway/src/infrastructure/passthrough/chat-completions-passthrough.ts deleted file mode 100644 index 6e2f21a..0000000 --- a/gateway/src/infrastructure/passthrough/chat-completions-passthrough.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { Response as ExpressResponse } from 'express'; -import { AuthenticationError, PaymentError, ProviderError } from '../../shared/errors/index.js'; -import { logger } from '../utils/logger.js'; -import { CONTENT_TYPES, HTTP_STATUS } from '../../domain/types/provider.js'; - -export type ChatUsageFormat = 'openai_chat'; - -export interface ChatCompletionsAuthConfig { - envVar: string; - header: string; - scheme?: string; - template?: string; -} - -export interface ChatCompletionsUsageConfig { - format: ChatUsageFormat; -} - -export interface ChatCompletionsPayloadDefaults { - [key: string]: any; -} - -export interface ChatCompletionsPassthroughConfig { - provider: string; - baseUrl: string; - auth?: ChatCompletionsAuthConfig; - staticHeaders?: Record; - supportedClientFormats: string[]; - payloadDefaults?: ChatCompletionsPayloadDefaults; - usage?: ChatCompletionsUsageConfig; - forceStreamOption?: boolean; -} - -interface OpenAIChatUsage { - prompt_tokens?: number; - completion_tokens?: number; - total_tokens?: number; - prompt_tokens_details?: { - cached_tokens?: number; - }; - completion_tokens_details?: { - reasoning_tokens?: number; - }; -} - -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' && !Array.isArray(value); -} - -function mergeDeep>(target: T, source: Record): T { - const output = { ...target } as Record; - Object.entries(source).forEach(([key, value]) => { - if (isObject(value) && isObject(output[key])) { - output[key] = mergeDeep(output[key] as Record, value as Record); - } else { - output[key] = value; - } - }); - return output as T; -} - -export class ChatCompletionsPassthrough { - private eventBuffer = ''; - private x402FetchFunction: typeof fetch | null = null; - private x402Initialized = false; - private lastX402PaymentAmount: string | undefined = undefined; - constructor(private readonly config: ChatCompletionsPassthroughConfig) { - // Initialize x402 payment wrapper once if needed - this.initializeX402Support(); - } - - private async initializeX402Support(): Promise { - // Only initialize for OpenRouter with x402 URL and PRIVATE_KEY - const { getConfig } = await import('../config/app-config.js'); - const config = getConfig(); - const shouldUseX402 = - this.config.provider === 'openrouter' && - config.x402.enabled && - this.config.baseUrl.includes('x402'); - - if (!shouldUseX402) { - this.x402Initialized = true; - return; - } - - try { - const { getX402Account, createX402Fetch, logPaymentReady } = await import('../payments/x402/index.js'); - const account = getX402Account(); - - if (account) { - this.x402FetchFunction = createX402Fetch(account); - logPaymentReady(account, { - provider: this.config.provider, - baseUrl: this.config.baseUrl, - }); - logger.info('x402 payment support initialized', { - provider: this.config.provider, - walletAddress: account.address, - module: 'chat-completions-passthrough', - }); - } else { - logger.error('x402 wallet initialization failed - getX402Account returned null', { - provider: this.config.provider, - module: 'chat-completions-passthrough', - }); - } - } catch (error) { - logger.error('Failed to initialize x402 payments', error, { - provider: this.config.provider, - errorMessage: error instanceof Error ? error.message : String(error), - module: 'chat-completions-passthrough', - }); - } finally { - this.x402Initialized = true; - } - } - - private get apiKey(): string | undefined { - const auth = this.config.auth; - if (!auth) return undefined; - const token = process.env[auth.envVar]; - if (!token) { - throw new AuthenticationError(`${this.config.provider} API key not configured`, { provider: this.config.provider }); - } - return token; - } - - private buildAuthHeader(): string | undefined { - const auth = this.config.auth; - if (!auth) return undefined; - - const { scheme, template } = auth; - const token = this.apiKey!; - - if (template) { - return template.replace('{{token}}', token); - } - - if (scheme) { - return `${scheme} ${token}`.trim(); - } - - return token; - } - - private buildHeaders(): Record { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (this.config.staticHeaders) { - Object.assign(headers, this.config.staticHeaders); - } - - const authHeader = this.buildAuthHeader(); - if (authHeader && this.config.auth) { - headers[this.config.auth.header] = authHeader; - } - - return headers; - } - - private buildPayload(body: any, stream: boolean): any { - let payload = isObject(body) ? { ...body } : body; - - if (isObject(payload)) { - if (this.config.forceStreamOption !== false) { - payload.stream = stream; - } - - if (this.config.payloadDefaults) { - payload = mergeDeep(payload, this.config.payloadDefaults); - } - } - - return payload; - } - - private async makeRequest(body: any, stream: boolean): Promise { - // Wait for x402 initialization to complete - while (!this.x402Initialized) { - await new Promise(resolve => setTimeout(resolve, 10)); - } - - // Use x402 fetch if available, otherwise standard fetch - const fetchFunction = this.x402FetchFunction || fetch; - const isX402Enabled = this.x402FetchFunction !== null; - - // Log which authentication method is being used for this request - if (isX402Enabled) { - logger.info('Making request with x402 payment (PRIVATE_KEY)', { - provider: this.config.provider, - model: body?.model, - baseUrl: this.config.baseUrl, - stream, - module: 'chat-completions-passthrough', - }); - } else { - logger.info('Making request with API key authentication', { - provider: this.config.provider, - model: body?.model, - baseUrl: this.config.baseUrl, - apiKeyEnvVar: this.config.auth?.envVar, - stream, - module: 'chat-completions-passthrough', - }); - } - - let response: globalThis.Response; - - try { - response = await fetchFunction(this.config.baseUrl, { - method: 'POST', - headers: this.buildHeaders(), - body: JSON.stringify(this.buildPayload(body, stream)), - }); - } catch (error) { - // Catch x402 payment errors (insufficient balance, payment failures, etc.) - if (isX402Enabled) { - logger.error('x402 payment request failed', error, { - provider: this.config.provider, - baseUrl: this.config.baseUrl, - errorMessage: error instanceof Error ? error.message : String(error), - module: 'chat-completions-passthrough', - }); - } - throw error; - } - - // Store fixed x402 cost for chat completions ($0.02 per request) - // TODO: Replace with dynamically calculated amounts from x402 payment response - if (isX402Enabled) { - this.lastX402PaymentAmount = '0.02'; - - const { logPaymentInfo, extractPaymentInfo } = await import('../payments/x402/index.js'); - const paymentInfo = extractPaymentInfo(response); - if (paymentInfo) { - logPaymentInfo(paymentInfo, { - provider: this.config.provider, - model: body?.model, - }); - } - } - - // Handle failed responses - if (!response.ok) { - const errorText = await response.text(); - - // Special handling for 402 with x402 enabled - payment failed - if (response.status === 402 && isX402Enabled) { - logger.error('x402 payment failed', { - provider: this.config.provider, - error: errorText, - module: 'chat-completions-passthrough', - }); - throw new PaymentError( - errorText || 'Insufficient balance or payment error', - { provider: this.config.provider, statusCode: response.status } - ); - } - - // Standard error handling for other failures - throw new ProviderError( - this.config.provider, - errorText || `HTTP ${response.status}`, - response.status, - { endpoint: this.config.baseUrl } - ); - } - - return response; - } - - - private recordOpenAIUsage(usage: OpenAIChatUsage, model: string): void { - const totalInputTokens = usage.prompt_tokens ?? 0; - const cachedPromptTokens = usage.prompt_tokens_details?.cached_tokens ?? 0; - const nonCachedPromptTokens = totalInputTokens - cachedPromptTokens; - const completionTokens = usage.completion_tokens ?? 0; - - logger.debug('Tracking chat completions usage', { - provider: this.config.provider, - model, - totalInputTokens, - nonCachedPromptTokens, - cachedPromptTokens, - completionTokens, - totalTokens: usage.total_tokens, - reasoningTokens: usage.completion_tokens_details?.reasoning_tokens, - x402PaymentAmount: this.lastX402PaymentAmount, - willUseX402Pricing: !!this.lastX402PaymentAmount, - module: 'chat-completions-passthrough', - }); - - import('../utils/usage-tracker.js') - .then(({ usageTracker }) => { - usageTracker.trackUsage( - model, - this.config.provider, - Math.max(nonCachedPromptTokens, 0), - completionTokens, - Math.max(cachedPromptTokens, 0), - 0, - this.lastX402PaymentAmount, // Pass x402 payment amount if available - ); - // Clear payment amount after tracking - this.lastX402PaymentAmount = undefined; - }) - .catch(error => { - logger.error('Usage tracking failed', error, { - provider: this.config.provider, - operation: 'passthrough', - module: 'chat-completions-passthrough', - }); - }); - } - - private recordUsage(usage: unknown, model: string): void { - if (!usage || !this.config.usage?.format) return; - - switch (this.config.usage.format) { - case 'openai_chat': - this.recordOpenAIUsage(usage as OpenAIChatUsage, model); - break; - default: - logger.warn('Unsupported usage format for chat completions passthrough', { - provider: this.config.provider, - format: this.config.usage.format, - module: 'chat-completions-passthrough', - }); - } - } - - private processStreamingChunk(chunk: string, model: string): void { - try { - this.eventBuffer += chunk; - - const events = this.eventBuffer.split(/\r?\n\r?\n/); - this.eventBuffer = events.pop() ?? ''; - - for (const event of events) { - const dataLine = event.split(/\r?\n/).find(line => line.startsWith('data:')); - if (!dataLine) continue; - - const payload = dataLine.replace('data:', '').trim(); - if (!payload || payload === '[DONE]') { - continue; - } - - try { - const parsed = JSON.parse(payload); - - if (parsed?.usage) { - this.recordUsage(parsed.usage, model); - // reset buffer after successful usage capture to avoid duplicate processing - this.eventBuffer = ''; - } - } catch (parseError) { - logger.debug('Skipping incomplete chat streaming chunk', { - provider: this.config.provider, - error: parseError instanceof Error ? parseError.message : String(parseError), - module: 'chat-completions-passthrough', - }); - } - } - - if (this.eventBuffer.includes('[DONE]')) { - this.eventBuffer = ''; - } - } catch (error) { - logger.error('Chat streaming usage tracking failed', error, { - provider: this.config.provider, - operation: 'passthrough', - module: 'chat-completions-passthrough', - }); - } - } - - async handleDirectRequest(request: any, res: ExpressResponse): Promise { - this.eventBuffer = ''; - this.lastX402PaymentAmount = undefined; // Reset payment amount for new request - - if (request?.stream) { - const response = await this.makeRequest(request, true); - - if (!response.ok) { - const bodyText = await response.text(); - const contentType = response.headers.get('content-type') ?? CONTENT_TYPES.JSON; - res.status(response.status).set('Content-Type', contentType).send(bodyText); - return; - } - - res.writeHead(HTTP_STATUS.OK, { - 'Content-Type': CONTENT_TYPES.EVENT_STREAM, - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - }); - - const reader = response.body?.getReader(); - if (!reader) { - throw new ProviderError(this.config.provider, 'No stream body received', 500); - } - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const text = new TextDecoder().decode(value); - setImmediate(() => this.processStreamingChunk(text, request?.model)); - res.write(value); - } - - res.end(); - return; - } - - const response = await this.makeRequest(request, false); - - if (!response.ok) { - const bodyText = await response.text(); - const contentType = response.headers.get('content-type') ?? CONTENT_TYPES.JSON; - res.status(response.status).set('Content-Type', contentType).send(bodyText); - return; - } - - const json = await response.json(); - - if (json?.usage) { - this.recordUsage(json.usage, request?.model); - } - - res.status(HTTP_STATUS.OK).json(json); - } - -} diff --git a/gateway/src/infrastructure/passthrough/chat-completions-provider-config.ts b/gateway/src/infrastructure/passthrough/chat-completions-provider-config.ts deleted file mode 100644 index f15a237..0000000 --- a/gateway/src/infrastructure/passthrough/chat-completions-provider-config.ts +++ /dev/null @@ -1,153 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import { logger } from '../utils/logger.js'; -import { getConfig } from '../config/app-config.js'; -import { - ChatCompletionsPassthroughConfig, - ChatCompletionsAuthConfig, - ChatCompletionsUsageConfig, - ChatCompletionsPayloadDefaults, -} from './chat-completions-passthrough.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const CATALOG_PATH = path.join(__dirname, '../../../../model_catalog/chat_completions_providers_v1.json'); - -interface RawChatCompletionsCatalog { - providers: RawChatCompletionsEntry[]; -} - -interface RawChatCompletionsEntry { - provider: string; - models: string[]; - chat_completions: RawChatCompletionsConfig; -} - -interface RawChatCompletionsConfig { - base_url: string; - auth: RawChatCompletionsAuthConfig; - static_headers?: Record; - supported_client_formats: string[]; - payload_defaults?: ChatCompletionsPayloadDefaults; - usage?: RawChatCompletionsUsageConfig; - force_stream_option?: boolean; -} - -interface RawChatCompletionsAuthConfig { - env_var: string; - header: string; - scheme?: string; - template?: string; -} - -interface RawChatCompletionsUsageConfig { - format: ChatCompletionsUsageConfig['format']; -} - -export interface ChatCompletionsProviderDefinition { - provider: string; - models: string[]; - config: ChatCompletionsPassthroughConfig; -} - -function toAuthConfig(raw: RawChatCompletionsAuthConfig): ChatCompletionsAuthConfig { - return { - envVar: raw.env_var, - header: raw.header, - scheme: raw.scheme, - template: raw.template, - }; -} - -function toUsageConfig(raw?: RawChatCompletionsUsageConfig): ChatCompletionsUsageConfig | undefined { - if (!raw) return undefined; - return { - format: raw.format, - }; -} - -function toPassthroughConfig(raw: RawChatCompletionsConfig, provider: string): ChatCompletionsPassthroughConfig { - return { - provider, - baseUrl: raw.base_url, - auth: toAuthConfig(raw.auth), - staticHeaders: raw.static_headers, - supportedClientFormats: raw.supported_client_formats, - payloadDefaults: raw.payload_defaults, - usage: toUsageConfig(raw.usage), - forceStreamOption: raw.force_stream_option, - }; -} - -export function loadChatCompletionsProviderDefinitions(): ChatCompletionsProviderDefinition[] { - if (!fs.existsSync(CATALOG_PATH)) { - logger.warn('Chat completions providers catalog not found', { - path: CATALOG_PATH, - module: 'chat-completions-provider-config', - }); - return []; - } - - try { - const rawContent = fs.readFileSync(CATALOG_PATH, 'utf-8'); - const parsed = JSON.parse(rawContent) as RawChatCompletionsCatalog; - - if (!Array.isArray(parsed.providers)) { - logger.error('Invalid chat completions providers catalog structure', { - path: CATALOG_PATH, - module: 'chat-completions-provider-config', - }); - return []; - } - - const definitions = parsed.providers.map(entry => ({ - provider: entry.provider, - models: entry.models || [], - config: toPassthroughConfig(entry.chat_completions, entry.provider), - })); - - const config = getConfig(); - if (config.x402.enabled) { - const x402Url = config.x402.chatCompletionsUrl; - - // Apply x402 as fallback: only for providers whose API key is NOT configured - definitions.forEach(definition => { - const providerApiKey = definition.config.auth?.envVar; - const hasProviderKey = providerApiKey && process.env[providerApiKey]; - - if (!hasProviderKey) { - logger.info('Provider API key not found, using x402 payment gateway as fallback', { - provider: definition.provider, - envVar: providerApiKey, - originalUrl: definition.config.baseUrl, - x402Url: x402Url, - module: 'chat-completions-provider-config', - }); - - definition.config = { - ...definition.config, - baseUrl: x402Url, - auth: undefined, // x402 uses payment instead of API keys - }; - } else { - logger.info('Provider API key found, using normal configuration', { - provider: definition.provider, - envVar: providerApiKey, - module: 'chat-completions-provider-config', - }); - } - }); - } - - return definitions; - } catch (error) { - logger.error('Failed to load chat completions providers catalog', error, { - path: CATALOG_PATH, - module: 'chat-completions-provider-config', - }); - return []; - } -} diff --git a/gateway/src/infrastructure/passthrough/messages-passthrough-registry.ts b/gateway/src/infrastructure/passthrough/messages-passthrough-registry.ts deleted file mode 100644 index 9bbaa15..0000000 --- a/gateway/src/infrastructure/passthrough/messages-passthrough-registry.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ModelUtils } from '../utils/model-utils.js'; -import { MessagesPassthrough, MessagesPassthroughConfig } from './messages-passthrough.js'; -import { loadMessagesProviderDefinitions, MessagesProviderDefinition } from './messages-provider-config.js'; - -interface ProviderEntry { - definition: MessagesProviderDefinition; - passthrough?: MessagesPassthrough; -} - -export class MessagesPassthroughRegistry { - private readonly providers = new Map(); - private readonly modelToProvider = new Map(); - - constructor(definitions: MessagesProviderDefinition[]) { - definitions.forEach(def => { - this.providers.set(def.provider, { definition: def }); - - def.models.forEach(modelId => { - const normalized = ModelUtils.removeProviderPrefix(modelId); - this.modelToProvider.set(modelId, def.provider); - this.modelToProvider.set(normalized, def.provider); - this.modelToProvider.set(`${def.provider}/${normalized}`, def.provider); - }); - }); - } - - static fromCatalog(): MessagesPassthroughRegistry { - const definitions = loadMessagesProviderDefinitions(); - return new MessagesPassthroughRegistry(definitions); - } - - listProviders(): string[] { - return Array.from(this.providers.keys()); - } - - getSupportedClientFormats(provider: string): string[] { - const entry = this.providers.get(provider); - return entry?.definition.config.supportedClientFormats ?? []; - } - - findProviderByModel(modelName: string): string | undefined { - if (!modelName) return undefined; - const direct = this.modelToProvider.get(modelName); - if (direct) return direct; - - const normalized = ModelUtils.removeProviderPrefix(modelName); - return this.modelToProvider.get(normalized); - } - - getConfig(provider: string): MessagesPassthroughConfig | undefined { - const entry = this.providers.get(provider); - return entry?.definition.config; - } - - getPassthrough(provider: string): MessagesPassthrough | undefined { - const entry = this.providers.get(provider); - if (!entry) return undefined; - - if (!entry.passthrough) { - entry.passthrough = new MessagesPassthrough(entry.definition.config); - this.providers.set(provider, entry); - } - - return entry.passthrough; - } -} - -export function createMessagesPassthroughRegistry(): MessagesPassthroughRegistry { - return MessagesPassthroughRegistry.fromCatalog(); -} diff --git a/gateway/src/infrastructure/passthrough/messages-passthrough.ts b/gateway/src/infrastructure/passthrough/messages-passthrough.ts deleted file mode 100644 index bede34a..0000000 --- a/gateway/src/infrastructure/passthrough/messages-passthrough.ts +++ /dev/null @@ -1,546 +0,0 @@ -import { Response as ExpressResponse } from 'express'; -import { logger } from '../utils/logger.js'; -import { AuthenticationError, PaymentError, ProviderError } from '../../shared/errors/index.js'; -import { CONTENT_TYPES } from '../../domain/types/provider.js'; -import { ModelUtils } from '../utils/model-utils.js'; - -type UsageFormat = 'anthropic_messages'; - -export interface MessagesAuthConfig { - envVar: string; - header: string; - scheme?: string; - template?: string; -} - -export interface MessagesUsageConfig { - format: UsageFormat; -} - -export interface MessagesModelOptions { - ensureAnthropicSuffix?: boolean; -} - -export interface MessagesPassthroughConfig { - provider: string; - baseUrl: string; - auth?: MessagesAuthConfig; - staticHeaders?: Record; - supportedClientFormats: string[]; - modelOptions?: MessagesModelOptions; - usage?: MessagesUsageConfig; - forceStreamOption?: boolean; - x402Enabled?: boolean; -} - -interface StreamUsageSnapshot { - inputTokens: number; - cacheCreationTokens: number; - cacheReadTokens: number; -} - -export class MessagesPassthrough { - private initialUsage: StreamUsageSnapshot | null = null; - private streamBuffer = ''; - private x402FetchFunction: typeof fetch | null = null; - private x402Initialized = false; - private lastX402PaymentAmount: string | undefined = undefined; - - constructor(private readonly config: MessagesPassthroughConfig) { - // Initialize x402 payment wrapper once if needed - this.initializeX402Support(); - } - - private async initializeX402Support(): Promise { - // Check if x402 is enabled for this provider (set by config loader) - const { getConfig } = await import('../config/app-config.js'); - const config = getConfig(); - const shouldUseX402 = this.config.x402Enabled && config.x402.enabled; - - if (!shouldUseX402) { - this.x402Initialized = true; - return; - } - - try { - const { getX402Account, createX402Fetch, logPaymentReady } = await import('../payments/x402/index.js'); - const account = getX402Account(); - - if (account) { - this.x402FetchFunction = createX402Fetch(account); - logPaymentReady(account, { - provider: this.config.provider, - baseUrl: this.config.baseUrl, - }); - logger.info('x402 payment support initialized', { - provider: this.config.provider, - walletAddress: account.address, - module: 'messages-passthrough', - }); - } else { - logger.error('x402 wallet initialization failed - getX402Account returned null', { - provider: this.config.provider, - module: 'messages-passthrough', - }); - } - } catch (error) { - logger.error('Failed to initialize x402 payments', error, { - provider: this.config.provider, - errorMessage: error instanceof Error ? error.message : String(error), - module: 'messages-passthrough', - }); - } finally { - this.x402Initialized = true; - } - } - - private resolveBaseUrl(): string { - return this.config.baseUrl; - } - - private get apiKey(): string | undefined { - if (!this.config.auth) return undefined; - const envVar = this.config.auth.envVar; - const token = process.env[envVar]; - if (!token) { - throw new AuthenticationError(`${this.config.provider} API key not configured`, { provider: this.config.provider }); - } - return token; - } - - private buildAuthHeader(): string | undefined { - if (!this.config.auth) return undefined; - const { scheme, template } = this.config.auth; - const token = this.apiKey; - - if (!token) return undefined; - - if (template) { - return template.replace('{{token}}', token); - } - - if (scheme) { - return `${scheme} ${token}`.trim(); - } - - return token; - } - - private buildHeaders(): Record { - const headers: Record = { - 'Content-Type': 'application/json', - ...this.config.staticHeaders, - }; - - // Only add auth header if auth is configured (not x402 mode) - if (this.config.auth) { - const authHeader = this.buildAuthHeader(); - if (authHeader) { - headers[this.config.auth.header] = authHeader; - } - } - - return headers; - } - - private applyModelOptions(request: any): void { - const modelOptions = this.config.modelOptions; - if (!modelOptions) return; - - if (modelOptions.ensureAnthropicSuffix && typeof request.model === 'string') { - request.model = ModelUtils.ensureAnthropicSuffix(request.model); - } - } - - /** - * Recursively fix null or missing 'required' fields anywhere in an object. - * xAI rejects required: null AND missing required in schemas, expects required: [] - */ - private sanitizeRequiredFields(obj: unknown): unknown { - if (obj === null || obj === undefined) return obj; - if (Array.isArray(obj)) return obj.map(item => this.sanitizeRequiredFields(item)); - if (typeof obj === 'object') { - const result: Record = {}; - const objRecord = obj as Record; - - for (const [key, value] of Object.entries(objRecord)) { - if (key === 'required' && value === null) { - result[key] = []; - } else { - result[key] = this.sanitizeRequiredFields(value); - } - } - - // If this looks like a JSON schema object (has properties or type:object) but no required, add it - const hasProperties = 'properties' in objRecord; - const isObjectType = objRecord['type'] === 'object'; - const hasRequired = 'required' in result; - - if ((hasProperties || isObjectType) && !hasRequired) { - result['required'] = []; - } - - return result; - } - return obj; - } - - private ensurePayloadBody(body: any, stream: boolean): any { - // Strip fields not supported by Anthropic's public API - // Claude Code sends these but they're only valid for internal/beta endpoints - const { output_config, context_management, ...rest } = body; - - // Sanitize required: null to required: [] for xAI compatibility - const sanitized = this.sanitizeRequiredFields(rest); - - // Debug: check if sanitization worked - const jsonStr = JSON.stringify(sanitized); - const hasRequiredNull = jsonStr.includes('"required":null'); - const hasRequiredNullSpaced = jsonStr.includes('"required": null'); - - // Write full payload to file for inspection (per-provider) - const provider = this.config.provider; - require('fs').writeFileSync(`/tmp/payload-${provider}.json`, jsonStr); - if (hasRequiredNull || hasRequiredNullSpaced) { - require('fs').writeFileSync(`/tmp/bad-payload-${provider}.json`, jsonStr); - } - - const result = this.config.forceStreamOption === false - ? sanitized - : { ...(sanitized as object), stream }; - - // Log the FINAL result that will be stringified - const finalJson = JSON.stringify(result); - const finalHasNull = finalJson.includes('"required":null') || finalJson.includes('"required": null'); - require('fs').writeFileSync(`/tmp/final-${provider}.json`, finalJson); - - logger.info('PAYLOAD SANITIZATION', { - provider, - hasRequiredNull, - hasRequiredNullSpaced, - finalHasNull, - payloadLength: finalJson.length, - module: 'messages-passthrough', - }); - - return result; - } - - private async makeRequest(body: any, stream: boolean): Promise { - // Wait for x402 initialization to complete - while (!this.x402Initialized) { - await new Promise(resolve => setTimeout(resolve, 10)); - } - - // Use x402 fetch if available, otherwise standard fetch - const fetchFunction = this.x402FetchFunction || fetch; - const isX402Enabled = this.x402FetchFunction !== null; - - // Log which authentication method is being used for this request - if (isX402Enabled) { - logger.info('Making request with x402 payment (PRIVATE_KEY)', { - provider: this.config.provider, - model: body?.model, - baseUrl: this.config.baseUrl, - stream, - module: 'messages-passthrough', - }); - } else { - logger.info('Making request with API key authentication', { - provider: this.config.provider, - model: body?.model, - baseUrl: this.config.baseUrl, - apiKeyEnvVar: this.config.auth?.envVar, - stream, - module: 'messages-passthrough', - }); - } - - let response: globalThis.Response; - - try { - // Build payload and do final string-level sanitization - let payloadJson = JSON.stringify(this.ensurePayloadBody(body, stream)); - - // Bulletproof fix: replace ALL variations of required:null - const beforeLen = payloadJson.length; - payloadJson = payloadJson.replace(/"required"\s*:\s*null/g, '"required":[]'); - const fixed = beforeLen !== payloadJson.length; - - // Write final payload to file for debugging - require('fs').writeFileSync(`/tmp/FINAL-${this.config.provider}-${Date.now()}.json`, payloadJson); - - // Double check - does it still have required:null? - const stillHasNull = payloadJson.includes('required') && payloadJson.includes('null'); - logger.info('FINAL PAYLOAD CHECK', { - provider: this.config.provider, - fixed, - stillHasNull, - len: payloadJson.length, - module: 'messages-passthrough' - }); - - response = await fetchFunction(this.resolveBaseUrl(), { - method: 'POST', - headers: this.buildHeaders(), - body: payloadJson, - }); - } catch (error) { - // Catch x402 payment errors (insufficient balance, payment failures, etc.) - if (isX402Enabled) { - logger.error('x402 payment request failed', error, { - provider: this.config.provider, - baseUrl: this.config.baseUrl, - errorMessage: error instanceof Error ? error.message : String(error), - module: 'messages-passthrough', - }); - } - throw error; - } - - // Store fixed x402 cost for messages ($0.01 per request) - // TODO: Replace with dynamically calculated amounts from x402 payment response - if (isX402Enabled) { - this.lastX402PaymentAmount = '0.01'; - - const { logPaymentInfo, extractPaymentInfo } = await import('../payments/x402/index.js'); - const paymentInfo = extractPaymentInfo(response); - if (paymentInfo) { - logPaymentInfo(paymentInfo, { - provider: this.config.provider, - model: body?.model, - }); - } - } - - // Handle failed responses - if (!response.ok) { - const errorText = await response.text(); - - // Special handling for 402 with x402 enabled - payment failed - if (response.status === 402 && isX402Enabled) { - logger.error('x402 payment failed', { - provider: this.config.provider, - error: errorText, - module: 'messages-passthrough', - }); - throw new PaymentError( - errorText || 'Insufficient balance or payment error', - { provider: this.config.provider, statusCode: response.status } - ); - } - - // Standard error handling - throw new ProviderError( - this.config.provider, - errorText || `HTTP ${response.status}`, - response.status, - { endpoint: this.resolveBaseUrl() } - ); - } - - return response; - } - - - private trackUsage(payloadChunk: string, model: string): void { - if (this.config.usage?.format !== 'anthropic_messages') { - return; - } - - try { - this.streamBuffer += payloadChunk; - const events = this.streamBuffer.split(/\n\n/); - this.streamBuffer = events.pop() ?? ''; - - for (const rawEvent of events) { - const dataLines = rawEvent - .split('\n') - .filter(line => line.startsWith('data:')) - .map(line => line.replace(/^data:\s?/, '').trim()) - .filter(Boolean); - - if (!dataLines.length) continue; - - const payload = dataLines.join(''); - if (!payload.startsWith('{')) continue; - - const data = JSON.parse(payload); - - // Don't accumulate from content_block_delta - we'll get the final response from message_delta/message_stop - - if (data.type === 'message_start' && data.message?.usage) { - this.initialUsage = { - inputTokens: data.message.usage.input_tokens || 0, - cacheCreationTokens: data.message.usage.cache_creation_input_tokens || 0, - cacheReadTokens: data.message.usage.cache_read_input_tokens || 0, - }; - - logger.debug('Usage tracking started', { - provider: this.config.provider, - model, - ...this.initialUsage, - module: 'messages-passthrough', - }); - continue; - } - - if (data.type === 'message_delta' || data.type === 'message_stop') { - const usageData = data.usage; - - if (usageData) { - const inputTokens = usageData.input_tokens ?? this.initialUsage?.inputTokens ?? 0; - const cacheCreationTokens = usageData.cache_creation_input_tokens ?? this.initialUsage?.cacheCreationTokens ?? 0; - const cacheReadTokens = usageData.cache_read_input_tokens ?? this.initialUsage?.cacheReadTokens ?? 0; - const outputTokens = usageData.output_tokens ?? 0; - - const usingFallback = !this.initialUsage; - - if (usingFallback) { - logger.warn('Using fallback usage tracking', { - provider: this.config.provider, - reason: 'missed_message_start', - model, - inputTokens, - cacheCreationTokens, - cacheReadTokens, - outputTokens, - module: 'messages-passthrough', - }); - } - - const usageSnapshot = { - inputTokens, - cacheCreationTokens, - cacheReadTokens, - outputTokens, - }; - - logger.debug('Usage tracking completed', { - provider: this.config.provider, - model, - ...usageSnapshot, - x402PaymentAmount: this.lastX402PaymentAmount, - willUseX402Pricing: !!this.lastX402PaymentAmount, - module: 'messages-passthrough', - }); - - import('../utils/usage-tracker.js') - .then(({ usageTracker }) => { - usageTracker.trackUsage( - model, - this.config.provider, - inputTokens, - outputTokens, - cacheCreationTokens, - cacheReadTokens, - this.lastX402PaymentAmount, // Pass x402 payment amount if available - ); - // Clear payment amount after tracking - this.lastX402PaymentAmount = undefined; - }) - .catch(error => { - logger.error('Usage tracking failed', error, { - provider: this.config.provider, - operation: 'passthrough', - module: 'messages-passthrough', - }); - }); - - this.initialUsage = null; - continue; - } - - if (!this.initialUsage) { - continue; - } - } - } - } catch (error) { - logger.error('Usage tracking failed', error, { - provider: this.config.provider, - operation: 'passthrough', - module: 'messages-passthrough', - }); - } - } - - async handleDirectRequest(request: any, res: ExpressResponse): Promise { - this.initialUsage = null; - this.streamBuffer = ''; - this.lastX402PaymentAmount = undefined; // Reset payment amount for new request - - this.applyModelOptions(request); - - if (request.stream) { - const response = await this.makeRequest(request, true); - - res.writeHead(200, { - 'Content-Type': CONTENT_TYPES.TEXT_PLAIN, - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - }); - - const reader = response.body!.getReader(); - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunkText = new TextDecoder().decode(value); - setImmediate(() => this.trackUsage(chunkText, request.model)); - - res.write(value); - } - res.end(); - return; - } - - const response = await this.makeRequest(request, false); - const json = await response.json(); - - if (json.usage && this.config.usage?.format === 'anthropic_messages') { - const inputTokens = json.usage.input_tokens || 0; - const cacheCreationTokens = json.usage.cache_creation_input_tokens || 0; - const cacheReadTokens = json.usage.cache_read_input_tokens || 0; - const outputTokens = json.usage.output_tokens || 0; - - logger.debug('Tracking non-streaming usage', { - provider: this.config.provider, - model: request.model, - inputTokens, - cacheCreationTokens, - cacheReadTokens, - outputTokens, - x402PaymentAmount: this.lastX402PaymentAmount, - willUseX402Pricing: !!this.lastX402PaymentAmount, - module: 'messages-passthrough', - }); - - import('../utils/usage-tracker.js') - .then(({ usageTracker }) => { - usageTracker.trackUsage( - request.model, - this.config.provider, - inputTokens, - outputTokens, - cacheCreationTokens, - cacheReadTokens, - this.lastX402PaymentAmount, // Pass x402 payment amount if available - ); - // Clear payment amount after tracking - this.lastX402PaymentAmount = undefined; - }) - .catch(error => { - logger.error('Usage tracking failed', error, { - provider: this.config.provider, - operation: 'passthrough', - module: 'messages-passthrough', - }); - }); - } - - res.json(json); - } - -} diff --git a/gateway/src/infrastructure/passthrough/messages-provider-config.ts b/gateway/src/infrastructure/passthrough/messages-provider-config.ts deleted file mode 100644 index ac4a6d1..0000000 --- a/gateway/src/infrastructure/passthrough/messages-provider-config.ts +++ /dev/null @@ -1,167 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import { logger } from '../utils/logger.js'; -import { getConfig } from '../config/app-config.js'; -import { - MessagesPassthroughConfig, - MessagesAuthConfig, - MessagesModelOptions, - MessagesUsageConfig, -} from './messages-passthrough.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const CATALOG_PATH = path.join(__dirname, '../../../../model_catalog/messages_providers_v1.json'); - -interface RawMessagesProviderCatalog { - providers: RawMessagesProviderEntry[]; -} - -interface RawMessagesProviderEntry { - provider: string; - models: string[]; - messages: RawMessagesConfig; -} - -interface RawMessagesConfig { - base_url: string; - auth: RawMessagesAuthConfig; - static_headers?: Record; - supported_client_formats: string[]; - model_options?: RawMessagesModelOptions; - usage?: RawMessagesUsageConfig; - force_stream_option?: boolean; -} - -interface RawMessagesAuthConfig { - env_var: string; - header: string; - scheme?: string; - template?: string; -} - -interface RawMessagesModelOptions { - ensure_anthropic_suffix?: boolean; -} - -interface RawMessagesUsageConfig { - format: 'anthropic_messages'; -} - -export interface MessagesProviderDefinition { - provider: string; - models: string[]; - config: MessagesPassthroughConfig; -} - -function toAuthConfig(raw: RawMessagesAuthConfig): MessagesAuthConfig { - return { - envVar: raw.env_var, - header: raw.header, - scheme: raw.scheme, - template: raw.template, - }; -} - -function toModelOptions(raw?: RawMessagesModelOptions): MessagesModelOptions | undefined { - if (!raw) return undefined; - return { - ensureAnthropicSuffix: raw.ensure_anthropic_suffix, - }; -} - -function toUsageConfig(raw?: RawMessagesUsageConfig): MessagesUsageConfig | undefined { - if (!raw) return undefined; - return { - format: raw.format, - }; -} - -function toPassthroughConfig(raw: RawMessagesConfig, provider: string): MessagesPassthroughConfig { - const config: MessagesPassthroughConfig = { - provider, - baseUrl: raw.base_url, - auth: toAuthConfig(raw.auth), - staticHeaders: raw.static_headers, - supportedClientFormats: raw.supported_client_formats, - modelOptions: toModelOptions(raw.model_options), - usage: toUsageConfig(raw.usage), - forceStreamOption: raw.force_stream_option, - }; - - return config; -} - -export function loadMessagesProviderDefinitions(): MessagesProviderDefinition[] { - if (!fs.existsSync(CATALOG_PATH)) { - logger.warn('Messages providers catalog not found', { - path: CATALOG_PATH, - module: 'messages-provider-config', - }); - return []; - } - - try { - const rawContent = fs.readFileSync(CATALOG_PATH, 'utf-8'); - const parsed = JSON.parse(rawContent) as RawMessagesProviderCatalog; - - if (!parsed.providers || !Array.isArray(parsed.providers)) { - logger.error('Invalid messages providers catalog structure', { - path: CATALOG_PATH, - module: 'messages-provider-config', - }); - return []; - } - - const definitions = parsed.providers.map(entry => ({ - provider: entry.provider, - models: entry.models || [], - config: toPassthroughConfig(entry.messages, entry.provider), - })); - - // Apply x402 as fallback: only for providers whose API key is NOT configured - const config = getConfig(); - if (config.x402.enabled) { - const x402Url = config.x402.messagesUrl; - - definitions.forEach(definition => { - const providerApiKey = definition.config.auth?.envVar; - const hasProviderKey = providerApiKey && process.env[providerApiKey]; - - if (!hasProviderKey) { - logger.info('Provider API key not found, using x402 payment gateway as fallback', { - provider: definition.provider, - envVar: providerApiKey, - originalUrl: definition.config.baseUrl, - x402Url: x402Url, - module: 'messages-provider-config', - }); - - definition.config = { - ...definition.config, - baseUrl: x402Url, - auth: undefined, // x402 uses payment instead of API keys - x402Enabled: true, - }; - } else { - logger.info('Provider API key found, using normal configuration', { - provider: definition.provider, - envVar: providerApiKey, - module: 'messages-provider-config', - }); - } - }); - } - - return definitions; - } catch (error) { - logger.error('Failed to load messages providers catalog', error, { - path: CATALOG_PATH, - module: 'messages-provider-config', - }); - return []; - } -} diff --git a/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts b/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts deleted file mode 100644 index 074733c..0000000 --- a/gateway/src/infrastructure/passthrough/ollama-responses-passthrough.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { Response as ExpressResponse } from 'express'; -import { logger } from '../utils/logger.js'; -import { ProviderError } from '../../shared/errors/index.js'; -import { CONTENT_TYPES } from '../../domain/types/provider.js'; -import { getConfig } from '../config/app-config.js'; -import { ResponsesPassthrough, ResponsesPassthroughConfig } from './responses-passthrough.js'; - -export class OllamaResponsesPassthrough implements ResponsesPassthrough { - constructor(private readonly config: ResponsesPassthroughConfig) {} - - private get baseUrl(): string { - if (this.config.baseUrl) { - return this.config.baseUrl; - } - const configBaseUrl = getConfig().providers.ollama.baseUrl; - return configBaseUrl.replace(/\/v1\/?$/, '/v1/responses'); - } - - private buildAuthHeader(): string { - const { auth } = this.config; - if (!auth) { - return ''; - } - - const envVar = auth.envVar; - if (envVar) { - const token = process.env[envVar]; - if (token) { - if (auth.scheme) { - return `${auth.scheme} ${token}`.trim(); - } - return token; - } - } - - return ''; - } - - private buildHeaders(): Record { - const headers: Record = { - 'Content-Type': 'application/json', - ...this.config.staticHeaders, - }; - - const headerName = this.config.auth?.header ?? 'Authorization'; - const authHeader = this.buildAuthHeader(); - if (authHeader) { - headers[headerName] = authHeader; - } - return headers; - } - - private usage: { - inputTokens?: number; - outputTokens?: number; - totalTokens?: number; - } | null = null; - - private eventBuffer: string = ''; - private assistantResponseBuffer: string = ''; - - private async makeRequest(body: any, stream: boolean): Promise { - const response = await fetch(this.baseUrl, { - method: 'POST', - headers: this.buildHeaders(), - body: JSON.stringify({ ...body, stream, store: false }) - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new ProviderError('ollama', errorText || `HTTP ${response.status}`, response.status, { endpoint: this.baseUrl }); - } - - return response; - } - - private trackUsage(text: string, model: string, clientIp?: string): void { - try { - this.eventBuffer += text; - - const textDeltaMatch = /"type":"response\.text\.delta"[^}]*"text":"([^"]+)"/g; - let match; - while ((match = textDeltaMatch.exec(text)) !== null) { - this.assistantResponseBuffer += match[1]; - } - - if (this.eventBuffer.includes('"type":"response.completed"')) { - const startIndex = this.eventBuffer.indexOf('{"type":"response.completed"'); - if (startIndex === -1) return; - - let braceCount = 0; - let endIndex = -1; - - for (let i = startIndex; i < this.eventBuffer.length; i++) { - if (this.eventBuffer[i] === '{') braceCount++; - if (this.eventBuffer[i] === '}') braceCount--; - - if (braceCount === 0) { - endIndex = i; - break; - } - } - - if (endIndex === -1) return; - - const jsonString = this.eventBuffer.substring(startIndex, endIndex + 1); - - logger.debug('JSON response found', { provider: 'ollama', operation: 'response_parsing', module: 'ollama-responses-passthrough' }); - - try { - const data = JSON.parse(jsonString); - logger.debug('Response parsed successfully', { provider: 'ollama', operation: 'usage_extraction', module: 'ollama-responses-passthrough' }); - - if (data.response?.usage) { - const usage = data.response.usage; - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const totalTokens = usage.total_tokens || (inputTokens + outputTokens); - - logger.debug('Usage tracking from response', { - provider: 'ollama', - model, - inputTokens, - outputTokens, - totalTokens, - module: 'ollama-responses-passthrough' - }); - - import('../utils/usage-tracker.js').then(({ usageTracker }) => { - usageTracker.trackUsage( - model, - 'ollama', - inputTokens, - outputTokens, - 0, - 0, - clientIp - ); - }).catch((error) => { - logger.error('Usage tracking failed', error, { provider: 'ollama', operation: 'passthrough', module: 'ollama-responses-passthrough' }); - }); - } else { - logger.warn('No usage data in response', { provider: 'ollama', operation: 'passthrough', module: 'ollama-responses-passthrough' }); - } - } catch (parseError) { - logger.error('JSON parse error', parseError, { provider: 'ollama', operation: 'response_parsing', module: 'ollama-responses-passthrough' }); - } - - this.eventBuffer = ''; - } - } catch (error) { - logger.error('Usage tracking failed', error, { provider: 'ollama', operation: 'passthrough', module: 'ollama-responses-passthrough' }); - } - } - - async handleDirectRequest(request: any, res: ExpressResponse, clientIp?: string): Promise { - this.usage = null; - this.eventBuffer = ''; - this.assistantResponseBuffer = ''; - - // TODO: Add memory context injection when memory service is implemented - - if (request.stream) { - const response = await this.makeRequest(request, true); - - res.writeHead(200, { - 'Content-Type': CONTENT_TYPES.EVENT_STREAM, - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - }); - - const reader = response.body!.getReader(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const text = new TextDecoder().decode(value); - setImmediate(() => this.trackUsage(text, request.model, clientIp)); - - res.write(value); - } - res.end(); - - // TODO: Persist memory when memory service is implemented - } else { - const response = await this.makeRequest(request, false); - const json = await response.json(); - - if (json.usage) { - const inputTokens = json.usage.input_tokens || 0; - const outputTokens = json.usage.output_tokens || 0; - const totalTokens = json.usage.total_tokens || (inputTokens + outputTokens); - - logger.debug('Tracking non-streaming usage', { - provider: 'ollama', - model: request.model, - inputTokens, - outputTokens, - totalTokens, - module: 'ollama-responses-passthrough' - }); - - import('../utils/usage-tracker.js').then(({ usageTracker }) => { - usageTracker.trackUsage(request.model, 'ollama', inputTokens, outputTokens, 0, 0, clientIp); - }).catch(() => {}); - } - - // TODO: Persist memory when memory service is implemented - - res.json(json); - } - } -} diff --git a/gateway/src/infrastructure/passthrough/openai-responses-passthrough.ts b/gateway/src/infrastructure/passthrough/openai-responses-passthrough.ts deleted file mode 100644 index 776f2e7..0000000 --- a/gateway/src/infrastructure/passthrough/openai-responses-passthrough.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Response as ExpressResponse } from 'express'; -import { logger } from '../utils/logger.js'; -import { AuthenticationError, ProviderError } from '../../shared/errors/index.js'; -import { CONTENT_TYPES } from '../../domain/types/provider.js'; -import { getConfig } from '../config/app-config.js'; -import { ResponsesPassthrough, ResponsesPassthroughConfig } from './responses-passthrough.js'; - -export class OpenAIResponsesPassthrough implements ResponsesPassthrough { - constructor(private readonly config: ResponsesPassthroughConfig) {} - - private get baseUrl(): string { - return this.config.baseUrl; - } - - private get apiKey(): string { - const envVar = this.config.auth?.envVar; - if (envVar) { - const token = process.env[envVar]; - if (token) return token; - } - - const fallback = getConfig().providers.openai.apiKey; - if (fallback) return fallback; - - throw new AuthenticationError('OpenAI API key not configured', { provider: this.config.provider }); - } - - private buildAuthHeader(): string { - const token = this.apiKey; - const { auth } = this.config; - if (!auth) { - return `Bearer ${token}`; - } - - if (auth.template) { - return auth.template.replace('{{token}}', token); - } - - if (auth.scheme) { - return `${auth.scheme} ${token}`.trim(); - } - - return token; - } - - private buildHeaders(): Record { - const headers: Record = { - 'Content-Type': 'application/json', - ...this.config.staticHeaders, - }; - - const headerName = this.config.auth?.header ?? 'Authorization'; - headers[headerName] = this.buildAuthHeader(); - return headers; - } - - // Store usage data for tracking - private usage: { - inputTokens?: number; - outputTokens?: number; - totalTokens?: number; - } | null = null; - - // Buffer to handle multi-chunk SSE events - private eventBuffer: string = ''; - - private async makeRequest(body: any, stream: boolean): Promise { - const response = await fetch(this.baseUrl, { - method: 'POST', - headers: this.buildHeaders(), - body: JSON.stringify({ ...body, stream, store: false }) // Not storing responses - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new ProviderError('openai', errorText || `HTTP ${response.status}`, response.status, { endpoint: this.baseUrl }); - } - - return response; - } - - private trackUsage(text: string, model: string): void { - try { - // Add to buffer to handle multi-chunk events - this.eventBuffer += text; - - // Look for the exact response.completed event - if (this.eventBuffer.includes('"type":"response.completed"')) { - - // Find the start of the JSON object - const startIndex = this.eventBuffer.indexOf('{"type":"response.completed"'); - if (startIndex === -1) return; - - // Find the end by counting braces - let braceCount = 0; - let endIndex = -1; - - for (let i = startIndex; i < this.eventBuffer.length; i++) { - if (this.eventBuffer[i] === '{') braceCount++; - if (this.eventBuffer[i] === '}') braceCount--; - - if (braceCount === 0) { - endIndex = i; - break; - } - } - - if (endIndex === -1) return; // Incomplete JSON, wait for more chunks - - // Extract the complete JSON - const jsonString = this.eventBuffer.substring(startIndex, endIndex + 1); - - logger.debug('JSON response found', { provider: 'openai', operation: 'response_parsing', module: 'openai-responses-passthrough' }); - - try { - const data = JSON.parse(jsonString); - logger.debug('Response parsed successfully', { provider: 'openai', operation: 'usage_extraction', module: 'openai-responses-passthrough' }); - - // Extract usage data from response.usage - if (data.response?.usage) { - const usage = data.response.usage; - const totalInputTokens = usage.input_tokens || 0; - const cachedTokens = usage.input_tokens_details?.cached_tokens || 0; - const nonCachedInputTokens = totalInputTokens - cachedTokens; // Split for pricing - const outputTokens = usage.output_tokens || 0; - const totalTokens = usage.total_tokens || (totalInputTokens + outputTokens); - const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0; - - logger.debug('Usage tracking from response', { - provider: 'openai', - model, - totalInputTokens, - nonCachedInputTokens, - cachedTokens, - outputTokens, - totalTokens, - reasoningTokens, - module: 'openai-responses-passthrough' - }); - - import('../utils/usage-tracker.js').then(({ usageTracker }) => { - usageTracker.trackUsage( - model, - 'openai', - nonCachedInputTokens, // Send non-cached input tokens for correct pricing - outputTokens, - cachedTokens, // Send cached tokens separately for correct pricing - 0, // cache read tokens - ); - }).catch((error) => { - logger.error('Usage tracking failed', error, { provider: 'openai', operation: 'passthrough', module: 'openai-responses-passthrough' }); - }); - } else { - logger.warn('No usage data in response', { provider: 'openai', operation: 'passthrough', module: 'openai-responses-passthrough' }); - } - } catch (parseError) { - logger.error('JSON parse error', parseError, { provider: 'openai', operation: 'response_parsing', module: 'openai-responses-passthrough' }); - logger.debug('Raw JSON data', { provider: 'openai', operation: 'response_parsing', module: 'openai-responses-passthrough' }); - } - - // Clear buffer after processing - this.eventBuffer = ''; - } - } catch (error) { - logger.error('Usage tracking failed', error, { provider: 'openai', operation: 'passthrough', module: 'openai-responses-passthrough' }); - } - } - - async handleDirectRequest(request: any, res: ExpressResponse): Promise { - // Reset usage tracking for new request - this.usage = null; - this.eventBuffer = ''; - - if (request.stream) { - const response = await this.makeRequest(request, true); - - res.writeHead(200, { - 'Content-Type': CONTENT_TYPES.EVENT_STREAM, - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - }); - - // Manual stream processing like Anthropic for usage tracking - const reader = response.body!.getReader(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const text = new TextDecoder().decode(value); - setImmediate(() => this.trackUsage(text, request.model)); - - res.write(value); - } - res.end(); - } else { - const response = await this.makeRequest(request, false); - const json = await response.json(); - - // Track usage for non-streaming requests - if (json.usage) { - const totalInputTokens = json.usage.input_tokens || 0; - const cachedTokens = json.usage.input_tokens_details?.cached_tokens || 0; - const nonCachedInputTokens = totalInputTokens - cachedTokens; // Split for pricing - const outputTokens = json.usage.output_tokens || 0; - const totalTokens = json.usage.total_tokens || (totalInputTokens + outputTokens); - const reasoningTokens = json.usage.output_tokens_details?.reasoning_tokens || 0; - - logger.debug('Tracking non-streaming usage', { - provider: 'openai', - model: request.model, - totalInputTokens, - nonCachedInputTokens, - cachedTokens, - outputTokens, - totalTokens, - reasoningTokens, - module: 'openai-responses-passthrough' - }); - - import('../utils/usage-tracker.js').then(({ usageTracker }) => { - usageTracker.trackUsage(request.model, 'openai', nonCachedInputTokens, outputTokens, cachedTokens, 0); - }).catch(() => {}); - } - - res.json(json); - } - } -} diff --git a/gateway/src/infrastructure/passthrough/responses-passthrough-registry.ts b/gateway/src/infrastructure/passthrough/responses-passthrough-registry.ts deleted file mode 100644 index d67bc09..0000000 --- a/gateway/src/infrastructure/passthrough/responses-passthrough-registry.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ResponsesPassthrough, ResponsesPassthroughConfig } from './responses-passthrough.js'; -import { OpenAIResponsesPassthrough } from './openai-responses-passthrough.js'; -import { OllamaResponsesPassthrough } from './ollama-responses-passthrough.js'; -import { loadResponsesProviderDefinitions, ResponsesProviderDefinition } from './responses-provider-config.js'; -import { logger } from '../utils/logger.js'; - -interface ProviderEntry { - definition: ResponsesProviderDefinition; - passthrough?: ResponsesPassthrough; -} - -const passthroughFactories: Record ResponsesPassthrough> = { - openai: (config) => new OpenAIResponsesPassthrough(config), - ollama: (config) => new OllamaResponsesPassthrough(config), -}; - -export class ResponsesPassthroughRegistry { - private readonly providers = new Map(); - - constructor(definitions: ResponsesProviderDefinition[]) { - definitions.forEach(definition => { - if (!passthroughFactories[definition.provider]) { - logger.warn('No responses passthrough factory registered for provider', { - provider: definition.provider, - module: 'responses-passthrough-registry', - }); - return; - } - this.providers.set(definition.provider, { definition }); - }); - } - - listProviders(): string[] { - return Array.from(this.providers.keys()); - } - - getSupportedClientFormats(provider: string): string[] { - const entry = this.providers.get(provider); - return entry?.definition.config.supportedClientFormats ?? []; - } - - getConfig(provider: string): ResponsesPassthroughConfig | undefined { - const entry = this.providers.get(provider); - return entry?.definition.config; - } - - getPassthrough(provider: string): ResponsesPassthrough | undefined { - const entry = this.providers.get(provider); - if (!entry) return undefined; - - if (!entry.passthrough) { - const factory = passthroughFactories[entry.definition.provider]; - if (!factory) return undefined; - entry.passthrough = factory(entry.definition.config); - this.providers.set(provider, entry); - } - - return entry.passthrough; - } -} - -export function createResponsesPassthroughRegistry(): ResponsesPassthroughRegistry { - const definitions = loadResponsesProviderDefinitions(); - return new ResponsesPassthroughRegistry(definitions); -} diff --git a/gateway/src/infrastructure/passthrough/responses-passthrough.ts b/gateway/src/infrastructure/passthrough/responses-passthrough.ts deleted file mode 100644 index 8d6c8ca..0000000 --- a/gateway/src/infrastructure/passthrough/responses-passthrough.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Response as ExpressResponse } from 'express'; - -export interface ResponsesAuthConfig { - envVar: string; - header: string; - scheme?: string; - template?: string; -} - -export interface ResponsesPassthroughConfig { - provider: string; - baseUrl: string; - auth?: ResponsesAuthConfig; - staticHeaders?: Record; - supportedClientFormats: string[]; -} - -export interface ResponsesPassthrough { - handleDirectRequest(request: any, res: ExpressResponse): Promise; -} diff --git a/gateway/src/infrastructure/passthrough/responses-provider-config.ts b/gateway/src/infrastructure/passthrough/responses-provider-config.ts deleted file mode 100644 index 66b94b4..0000000 --- a/gateway/src/infrastructure/passthrough/responses-provider-config.ts +++ /dev/null @@ -1,93 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import { logger } from '../utils/logger.js'; -import { ResponsesPassthroughConfig, ResponsesAuthConfig } from './responses-passthrough.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const CATALOG_PATH = path.join(__dirname, '../../../../model_catalog/responses_providers_v1.json'); - -interface RawResponsesProviderCatalog { - providers: RawResponsesProviderEntry[]; -} - -interface RawResponsesProviderEntry { - provider: string; - responses: RawResponsesConfig; -} - -interface RawResponsesConfig { - base_url: string; - auth?: RawResponsesAuthConfig; - static_headers?: Record; - supported_client_formats: string[]; -} - -interface RawResponsesAuthConfig { - env_var: string; - header: string; - scheme?: string; - template?: string; -} - -export interface ResponsesProviderDefinition { - provider: string; - config: ResponsesPassthroughConfig; -} - -function toAuthConfig(raw?: RawResponsesAuthConfig): ResponsesAuthConfig | undefined { - if (!raw) return undefined; - return { - envVar: raw.env_var, - header: raw.header, - scheme: raw.scheme, - template: raw.template, - }; -} - -function toDefinition(raw: RawResponsesProviderEntry): ResponsesProviderDefinition { - return { - provider: raw.provider, - config: { - provider: raw.provider, - baseUrl: raw.responses.base_url, - auth: toAuthConfig(raw.responses.auth), - staticHeaders: raw.responses.static_headers, - supportedClientFormats: raw.responses.supported_client_formats, - }, - }; -} - -export function loadResponsesProviderDefinitions(): ResponsesProviderDefinition[] { - if (!fs.existsSync(CATALOG_PATH)) { - logger.warn('Responses providers catalog not found', { - path: CATALOG_PATH, - module: 'responses-provider-config', - }); - return []; - } - - try { - const rawContent = fs.readFileSync(CATALOG_PATH, 'utf-8'); - const parsed = JSON.parse(rawContent) as RawResponsesProviderCatalog; - - if (!Array.isArray(parsed.providers)) { - logger.error('Invalid responses providers catalog structure', { - path: CATALOG_PATH, - module: 'responses-provider-config', - }); - return []; - } - - return parsed.providers.map(toDefinition); - } catch (error) { - logger.error('Failed to load responses providers catalog', error, { - path: CATALOG_PATH, - module: 'responses-provider-config', - }); - return []; - } -} diff --git a/gateway/src/infrastructure/payments/README.md b/gateway/src/infrastructure/payments/README.md deleted file mode 100644 index d9cca5b..0000000 --- a/gateway/src/infrastructure/payments/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Payments Infrastructure - -This directory contains payment protocol integrations for the Ekai Gateway. - -## x402 Payment Protocol - -The x402 module implements support for the [Coinbase x402 Payment Protocol](https://docs.cdp.coinbase.com/x402/quickstart-for-buyers), which enables cryptocurrency payments for API services. - -### How It Works - -1. **Initial Request**: Gateway makes a normal request to the x402-enabled endpoint -2. **402 Response**: If payment is required, server returns HTTP 402 (Payment Required) -3. **Automatic Payment**: x402-fetch wrapper intercepts the 402, extracts payment requirements, creates on-chain payment -4. **Request Retry**: Request is automatically retried with payment proof header -5. **Service Access**: Service validates payment and returns the requested resource - -### Current Implementation - -- **Provider**: OpenRouter chat completions -- **Network**: EVM-compatible chains (Base Sepolia, etc.) -- **Currency**: USDC -- **Wallet**: Viem-based private key wallet - -### Configuration - -Set environment variables: - -```bash -# Required for x402 payments -PRIVATE_KEY=0x... # EVM private key with USDC balance - -# Optional: override x402 endpoint -X402_URL=https://x402.ekailabs.xyz/v1/chat/completions -``` - -### Module Structure - -``` -payments/ -└── x402/ - ├── wallet.ts # Viem wallet creation and management - ├── client.ts # x402-fetch wrapper and payment utilities - ├── index.ts # Public API exports - └── README.md # Detailed documentation -``` - -### Features - -- ✅ Automatic 402 handling -- ✅ Payment verification -- ✅ Request retry with payment proof -- ✅ Payment logging and tracking -- ✅ Graceful fallback on errors -- ✅ Modular architecture for future protocols - -### Usage Example - -```typescript -import { getX402Account, createX402Fetch } from './payments/x402/index.js'; - -const account = getX402Account(); -if (account) { - const fetchWithPayment = createX402Fetch(account); - // Use fetchWithPayment instead of standard fetch - const response = await fetchWithPayment(url, options); -} -``` - diff --git a/gateway/src/infrastructure/payments/x402/client.ts b/gateway/src/infrastructure/payments/x402/client.ts deleted file mode 100644 index ab0f1ee..0000000 --- a/gateway/src/infrastructure/payments/x402/client.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { wrapFetchWithPayment, decodeXPaymentResponse } from 'x402-fetch'; -import type { PrivateKeyAccount } from 'viem/accounts'; -import { logger } from '../../utils/logger.js'; - -export interface X402PaymentInfo { - transactionHash?: string; - amount?: string; - token?: string; - network?: string; - [key: string]: any; -} - -/** - * Creates a payment-enabled fetch function for x402 protocol. - * - * @param account - Viem PrivateKeyAccount for signing payments - * @returns Wrapped fetch function that handles 402 Payment Required responses - */ -export function createX402Fetch(account: PrivateKeyAccount): typeof fetch { - return wrapFetchWithPayment(fetch, account); -} - -/** - * Extracts and decodes payment information from x-payment-response header. - * - * @param response - Fetch Response object - * @returns Decoded payment info or null if not present - */ -export function extractPaymentInfo(response: Response): X402PaymentInfo | null { - const paymentHeader = response.headers.get('x-payment-response'); - - if (!paymentHeader) { - return null; - } - - try { - return decodeXPaymentResponse(paymentHeader); - } catch (error) { - logger.debug('Could not decode x-payment-response header', { - error: error instanceof Error ? error.message : String(error), - module: 'x402-client', - }); - return null; - } -} - -/** - * Extracts and converts payment amount from x402 response for cost tracking. - * Converts from smallest unit (e.g., 20000) to USD (e.g., 0.02). - * - * @param response - Fetch Response object - * @returns Payment amount in USD or null if not present - */ -export function extractPaymentAmountUSD(response: Response): string | null { - const paymentInfo = extractPaymentInfo(response); - - if (!paymentInfo || !paymentInfo.amount) { - return null; - } - - try { - const amountInSmallestUnit = parseFloat(paymentInfo.amount); - const amountInUSD = amountInSmallestUnit / 1_000_000; - - logger.debug('x402 payment amount extracted', { - rawAmount: paymentInfo.amount, - convertedUSD: amountInUSD, - transactionHash: paymentInfo.transactionHash, - module: 'x402-client', - }); - - return amountInUSD.toString(); - } catch (error) { - logger.warn('Failed to convert x402 payment amount', { - error: error instanceof Error ? error.message : String(error), - rawAmount: paymentInfo.amount, - module: 'x402-client', - }); - return null; - } -} - -/** - * Logs payment information from a completed x402 transaction. - * - * @param paymentInfo - Payment details from x-payment-response header - * @param context - Additional context for logging - */ -export function logPaymentInfo( - paymentInfo: X402PaymentInfo, - context?: { provider?: string; model?: string } -): void { - logger.info('x402 payment completed', { - transactionHash: paymentInfo.transactionHash, - amount: paymentInfo.amount, - token: paymentInfo.token, - network: paymentInfo.network, - ...context, - module: 'x402-client', - }); -} - -/** - * Logs x402 payment readiness. - * - * @param account - Viem account address - * @param context - Additional context for logging - */ -export function logPaymentReady( - account: { address: string }, - context?: { provider?: string; baseUrl?: string } -): void { - logger.debug('x402 payment wallet ready', { - walletAddress: account.address, - ...context, - module: 'x402-client', - }); -} - diff --git a/gateway/src/infrastructure/payments/x402/index.ts b/gateway/src/infrastructure/payments/x402/index.ts deleted file mode 100644 index e47f70c..0000000 --- a/gateway/src/infrastructure/payments/x402/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * x402 Payment Protocol Integration - * - * This module provides x402 payment support for services that require - * cryptocurrency payments. Currently used for OpenRouter chat completions - * when accessing the x402 payment gateway. - * - * @see https://docs.cdp.coinbase.com/x402/quickstart-for-buyers - */ - -export { getX402Account, isX402Available } from './wallet.js'; -export { createX402Fetch, extractPaymentInfo, extractPaymentAmountUSD, logPaymentInfo, logPaymentReady } from './client.js'; -export type { X402PaymentInfo } from './client.js'; - diff --git a/gateway/src/infrastructure/payments/x402/wallet.ts b/gateway/src/infrastructure/payments/x402/wallet.ts deleted file mode 100644 index 47861b4..0000000 --- a/gateway/src/infrastructure/payments/x402/wallet.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { privateKeyToAccount } from 'viem/accounts'; -import type { PrivateKeyAccount } from 'viem/accounts'; -import { logger } from '../../utils/logger.js'; -import { getConfig } from '../../config/app-config.js'; - -let cachedAccount: PrivateKeyAccount | null = null; -let initialized = false; - -/** - * Gets or creates an x402 payment wallet account from PRIVATE_KEY env var. - * Returns null if PRIVATE_KEY is not set or invalid. - * - * @returns PrivateKeyAccount for x402 payments, or null if unavailable - */ -export function getX402Account(): PrivateKeyAccount | null { - if (initialized) { - return cachedAccount; - } - - initialized = true; - - const config = getConfig(); - const privateKey = config.x402.privateKey; - if (!privateKey) { - logger.warn('PRIVATE_KEY not set, x402 payments disabled', { - module: 'x402-wallet', - }); - return null; - } - - try { - // Ensure proper hex format - const formattedKey = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`; - cachedAccount = privateKeyToAccount(formattedKey as `0x${string}`); - - logger.info('x402 wallet initialized successfully', { - address: cachedAccount.address, - module: 'x402-wallet', - }); - - return cachedAccount; - } catch (error) { - logger.error('Failed to initialize x402 wallet from PRIVATE_KEY', error, { - module: 'x402-wallet', - }); - cachedAccount = null; - return null; - } -} - -/** - * Checks if x402 payments are available (wallet initialized successfully) - */ -export function isX402Available(): boolean { - return getX402Account() !== null; -} - diff --git a/gateway/src/infrastructure/utils/canonical-validator.ts b/gateway/src/infrastructure/utils/canonical-validator.ts deleted file mode 100644 index 141d12e..0000000 --- a/gateway/src/infrastructure/utils/canonical-validator.ts +++ /dev/null @@ -1,117 +0,0 @@ -import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv'; -import addFormats from 'ajv-formats'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import { Request, Response, StreamingResponse } from '../../canonical/types/index.js'; -import { logger } from './logger.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -class CanonicalValidator { - private ajv: Ajv; - private requestValidator: ValidateFunction | null = null; - private responseValidator: ValidateFunction | null = null; - private streamingResponseValidator: ValidateFunction | null = null; - - constructor() { - this.ajv = new Ajv({ - allErrors: true, - removeAdditional: false, - useDefaults: true, - coerceTypes: false, - strict: false // Allow draft-07 schemas - }); - - // Add format validators (uri, date-time, etc.) - addFormats(this.ajv); - - this.loadSchemas(); - } - - private loadSchemas() { - const schemasDir = path.join(__dirname, '../../schemas'); - - try { - // Load request schema - const requestSchema = JSON.parse( - fs.readFileSync(path.join(schemasDir, 'request.schema.json'), 'utf8') - ); - this.requestValidator = this.ajv.compile(requestSchema); - - // Load response schema - const responseSchema = JSON.parse( - fs.readFileSync(path.join(schemasDir, 'response.schema.json'), 'utf8') - ); - this.responseValidator = this.ajv.compile(responseSchema); - - // Load streaming response schema - const streamingResponseSchema = JSON.parse( - fs.readFileSync(path.join(schemasDir, 'streaming-response.schema.json'), 'utf8') - ); - this.streamingResponseValidator = this.ajv.compile(streamingResponseSchema); - - } catch (error) { - logger.error('Error loading canonical schemas', error, { module: 'canonical-validator' }); - throw new Error('Failed to initialize canonical validator'); - } - } - - validateRequest(data: unknown): { valid: boolean; data?: Request; errors?: string[] } { - if (!this.requestValidator) { - return { valid: false, errors: ['Request validator not initialized'] }; - } - - const valid = this.requestValidator(data); - - if (valid) { - return { valid: true, data: data as Request }; - } else { - const errors = this.requestValidator.errors?.map(error => - `${error.instancePath} ${error.message}` - ) || ['Unknown validation error']; - return { valid: false, errors }; - } - } - - validateResponse(data: unknown): { valid: boolean; data?: Response; errors?: string[] } { - if (!this.responseValidator) { - return { valid: false, errors: ['Response validator not initialized'] }; - } - - const valid = this.responseValidator(data); - - if (valid) { - return { valid: true, data: data as Response }; - } else { - const errors = this.responseValidator.errors?.map(error => - `${error.instancePath} ${error.message}` - ) || ['Unknown validation error']; - return { valid: false, errors }; - } - } - - validateStreamingResponse(data: unknown): { valid: boolean; data?: StreamingResponse; errors?: string[] } { - if (!this.streamingResponseValidator) { - return { valid: false, errors: ['Streaming response validator not initialized'] }; - } - - const valid = this.streamingResponseValidator(data); - - if (valid) { - return { valid: true, data: data as StreamingResponse }; - } else { - const errors = this.streamingResponseValidator.errors?.map(error => - `${error.instancePath} ${error.message}` - ) || ['Unknown validation error']; - return { valid: false, errors }; - } - } - -} - -// Singleton instance -const canonicalValidator = new CanonicalValidator(); -export default canonicalValidator; \ No newline at end of file diff --git a/gateway/src/infrastructure/utils/error-handler.ts b/gateway/src/infrastructure/utils/error-handler.ts deleted file mode 100644 index 114b286..0000000 --- a/gateway/src/infrastructure/utils/error-handler.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Response } from 'express'; -import { logger } from './logger.js'; -import { GatewayError, toGatewayError } from '../../shared/errors/index.js'; - -/** - * @deprecated Use GatewayError and its subclasses instead - * Kept for backward compatibility - */ -export class APIError extends Error { - constructor( - public statusCode: number, - message: string, - public code?: string - ) { - super(message); - this.name = 'APIError'; - } -} - -export function createErrorResponse(message: string, code?: string) { - return { - error: 'Request failed', - message, - ...(code && { code }) - }; -} - -export function createAnthropicErrorResponse(message: string, code?: string) { - return { - type: 'error', - error: { - message, - ...(code && { code }) - } - }; -} - -/** - * @deprecated Use the errorHandler middleware from middleware/error-handler.ts instead - * Kept for backward compatibility with existing code - */ -export function handleError(error: unknown, res: Response, clientFormat: 'openai' | 'anthropic' = 'openai') { - const req = res.req as any; - const requestId = req?.requestId; - - const isAnthropic = clientFormat === 'anthropic'; - - // Convert to GatewayError for consistent handling - const gatewayError = toGatewayError(error); - - // Log error - if (gatewayError.statusCode >= 500) { - logger.error('API Error', gatewayError, { requestId, ...gatewayError.context, module: 'error-handler' }); - } else { - logger.warn('API Error', { requestId, ...gatewayError.context, message: gatewayError.message, module: 'error-handler' }); - } - - // Handle legacy APIError for backward compatibility - if (error instanceof APIError) { - const errorResponse = isAnthropic - ? createAnthropicErrorResponse(error.message, error.code) - : createErrorResponse(error.message, error.code); - return res.status(error.statusCode).json(errorResponse); - } - - // Handle new GatewayError classes - if (error instanceof GatewayError) { - const errorResponse = isAnthropic - ? createAnthropicErrorResponse(gatewayError.message, gatewayError.code) - : createErrorResponse(gatewayError.message, gatewayError.code); - return res.status(gatewayError.statusCode).json(errorResponse); - } - - // Fallback for unknown errors - const message = error instanceof Error ? error.message : 'Unknown error'; - const errorResponse = isAnthropic - ? createAnthropicErrorResponse(message) - : createErrorResponse(message); - - res.status(500).json(errorResponse); -} - diff --git a/gateway/src/infrastructure/utils/logger.ts b/gateway/src/infrastructure/utils/logger.ts deleted file mode 100644 index d75cf00..0000000 --- a/gateway/src/infrastructure/utils/logger.ts +++ /dev/null @@ -1,74 +0,0 @@ -import pino from 'pino'; -import { existsSync, mkdirSync } from 'fs'; -import { join } from 'path'; -import { LOG_LEVEL } from '../config.js'; - -export interface LogContext { - requestId?: string; - operation?: string; - provider?: string; - model?: string; - duration?: number; - module?: string; - [key: string]: unknown; -} - -export interface Logger { - info(message: string, meta?: LogContext): void; - error(message: string, error?: unknown, meta?: LogContext): void; - warn(message: string, meta?: LogContext): void; - debug(message: string, meta?: LogContext): void; - timer(operation: string): () => void; -} - -class PinoLogger implements Logger { - private p: pino.Logger; - - constructor() { - // Ensure logs dir exists - const logsDir = join(process.cwd(), 'logs'); - if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true }); - const logFile = join(logsDir, 'gateway.log'); - - // Build base streams: stdout (raw JSON) + file - const streams: { stream: any; level?: string }[] = [ - { stream: process.stdout, level: LOG_LEVEL || 'info' }, - { stream: pino.destination(logFile), level: LOG_LEVEL || 'info' }, - ]; - - const baseOpts: pino.LoggerOptions = { - level: LOG_LEVEL || 'info', - timestamp: pino.stdTimeFunctions.isoTime, - formatters: { level: (label) => ({ level: label }) }, - redact: ['password','token','key','secret','authorization','headers.authorization'], - serializers: { err: pino.stdSerializers.err }, - base: { - service: 'ekai-gateway', - version: process.env.npm_package_version || 'dev' - } - }; - - this.p = pino(baseOpts, pino.multistream(streams)); - } - - info(message: string, meta?: LogContext): void { - this.p.info(meta || {}, message); - } - error(message: string, error?: unknown, meta?: LogContext): void { - const logData = { ...(meta || {}), ...(error && { err: error }) }; - this.p.error(logData, message); - } - warn(message: string, meta?: LogContext): void { - this.p.warn(meta || {}, message); - } - debug(message: string, meta?: LogContext): void { - this.p.debug(meta || {}, message); - } - timer(operation: string): () => void { - const start = Date.now(); - return () => this.debug('Operation completed', { operation, duration: Date.now() - start }); - } -} - -// Singleton -export const logger: Logger = new PinoLogger(); diff --git a/gateway/src/infrastructure/utils/model-utils.ts b/gateway/src/infrastructure/utils/model-utils.ts deleted file mode 100644 index 6cd95ae..0000000 --- a/gateway/src/infrastructure/utils/model-utils.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Model name utilities for handling different model naming conventions - */ -export class ModelUtils { - /** - * Remove provider prefix from model names - * Examples: - * - anthropic/claude-3-5-sonnet → claude-3-5-sonnet - * - openai/gpt-4o → gpt-4o - * - claude-sonnet-4 → claude-sonnet-4 (unchanged) - */ - static removeProviderPrefix(modelName: string): string { - return modelName.replace(/^[^/]+\//, ''); - } - - /** - * Normalize model names for global cost optimization - * Examples: - * - anthropic/claude-3-5-sonnet → claude-3-5-sonnet - * - claude-sonnet-4-20250514 → claude-sonnet-4 - * - gpt-4o-2024-08-06 → gpt-4o - * - openai/gpt-4o-latest → gpt-4o - * - gpt-4-32k → gpt-4 - */ - static normalizeModelName(modelName: string): string { - return this.removeProviderPrefix(modelName) - .replace(/-\d{8}$/, '') // Remove Anthropic dates: -20250514 - .replace(/-\d{4}-\d{2}-\d{2}$/, '') // Remove OpenAI dates: -2024-08-06 - .replace(/-(latest|preview|beta|alpha)$/, '') // Remove versions - .replace(/-\d+k$/, '') // Remove context: -32k - .replace(/claude-sonnet-4-5$/, 'claude-sonnet-4.5'); // Normalize claude-sonnet-4-5 to claude-sonnet-4.5 - } - - /** - * Ensure Anthropic models have required suffixes - * Anthropic API requires model names to have version suffixes - * Examples: - * - claude-3-5-sonnet → claude-3-5-sonnet-20241022 - * - claude-sonnet-4 → claude-sonnet-4-20250514 - * - claude-3-5-sonnet-latest → claude-3-5-sonnet-latest (unchanged) - */ - static ensureAnthropicSuffix(modelName: string): string { - // Already has a date suffix or version suffix - if (/-\d{8}$/.test(modelName) || /-(latest|preview|beta|alpha)$/.test(modelName)) { - return modelName; - } - - // Add default suffixes for known models (Claude 4.5 series) - if (modelName.includes('claude-opus-4-5')) { - return modelName + '-20251101'; - } - if (modelName.includes('claude-sonnet-4-5')) { - return modelName + '-20250929'; - } - if (modelName.includes('claude-haiku-4-5')) { - return modelName + '-20251001'; - } - // Legacy models - if (modelName.includes('claude-3-5-sonnet')) { - return modelName + '-20241022'; - } - if (modelName.includes('claude-sonnet-4')) { - return modelName + '-20250514'; - } - - // Fallback: add -latest for unknown models - return modelName + '-latest'; - } - - /** - * Check if a model requires max_completion_tokens instead of max_tokens - * OpenAI o1/o3/o4 series and GPT-5 series models require max_completion_tokens parameter - * Examples: - * - o1 → true - * - o1-mini → true - * - o1-pro → true - * - o3-mini → true - * - gpt-5 → true - * - gpt-5-mini → true - * - openai/gpt-5 → true - * - gpt-4o → false - */ - static requiresMaxCompletionTokens(modelName: string): boolean { - const normalizedName = this.removeProviderPrefix(modelName.toLowerCase()); - - // OpenAI o1, o3, o4 series models and GPT-5 series models - return /^o[1-4](-|$)/.test(normalizedName) || /^gpt-5(-|$)/.test(normalizedName); - } - -} \ No newline at end of file diff --git a/gateway/src/infrastructure/utils/pricing-loader.ts b/gateway/src/infrastructure/utils/pricing-loader.ts deleted file mode 100644 index f2cf501..0000000 --- a/gateway/src/infrastructure/utils/pricing-loader.ts +++ /dev/null @@ -1,499 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import yaml from 'js-yaml'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import fetch, { Response } from 'node-fetch'; -import { ModelUtils } from './model-utils.js'; -import { logger } from './logger.js'; -import { getConfig } from '../config/app-config.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Type definitions for pricing configuration -export interface PricingConfig { - provider: string; - currency: string; - unit: string; - models: Record; - metadata: PricingMetadata; -} - -export interface ModelPricing { - input: number; - output: number; - cache_write?: number; // Cost for writing to cache - cache_read?: number; // Cost for reading from cache - id?: string; - original_provider?: string; - region?: string; - tier?: string; -} - -export interface PricingMetadata { - last_updated: string; - source: string; - notes: string; - version: string; - contributor?: string; -} - -export interface CostCalculation { - inputCost: number; - cacheWriteCost: number; - cacheReadCost: number; - outputCost: number; - totalCost: number; - currency: string; - unit: string; -} - -export class PricingLoader { - private costsDir = path.join(__dirname, '../../costs'); - private generatedCostsDir = path.join(__dirname, '../../../costs/generated'); - private pricingCache = new Map(); - private lastLoadTime = 0; - private cacheExpiryMs = 5 * 60 * 1000; // 5 minutes - private openRouterRefreshPromise: Promise | null = null; - private static readonly OPENROUTER_PRICING_URL = 'https://openrouter.ai/api/v1/models'; - private static readonly OPENROUTER_COST_FILE = 'openrouter.yaml'; - - /** - * Load all pricing configurations from YAML files - * Includes caching for performance - */ - loadAllPricing(): Map { - const now = Date.now(); - - // Return cached data if still valid - if (this.pricingCache.size > 0 && (now - this.lastLoadTime) < this.cacheExpiryMs) { - return this.pricingCache; - } - - // Clear cache and reload - this.pricingCache.clear(); - - try { - // Attempt to refresh OpenRouter pricing before loading files. - // Fire-and-forget; failures fall back to existing YAML snapshot. - void this.refreshOpenRouterPricing().catch(() => undefined); - - const files = fs.readdirSync(this.costsDir); - - files.forEach(file => { - if (file.endsWith('.yaml') || file.endsWith('.yml')) { - const providerFromFile = path.basename(file, path.extname(file)); - - // Skip template files - if (providerFromFile === 'templates') return; - - // Normalize provider name to lowercase for consistent lookups - const provider = providerFromFile.toLowerCase(); - - try { - const pricing = this.loadProviderPricing(providerFromFile); - this.pricingCache.set(provider, pricing); - logger.debug('Pricing loaded', { provider, modelCount: Object.keys(pricing.models).length, operation: 'pricing_load', module: 'pricing-loader' }); - } catch (error) { - logger.error('Failed to load pricing', error, { provider, operation: 'pricing_load', module: 'pricing-loader' }); - } - } - }); - - this.lastLoadTime = now; - logger.info('Pricing cache loaded', { providerCount: this.pricingCache.size, operation: 'pricing_load', module: 'pricing-loader' }); - - } catch (error) { - logger.error('Failed to load pricing directory', error, { operation: 'pricing_load', module: 'pricing-loader' }); - } - - return this.pricingCache; - } - - /** - * Load pricing for a specific provider - */ - loadProviderPricing(provider: string): PricingConfig { - const filePath = this.getCostFilePath(provider); - - if (!fs.existsSync(filePath)) { - throw new Error(`Pricing file not found for provider: ${provider}`); - } - - const content = fs.readFileSync(filePath, 'utf8'); - const config = yaml.load(content) as PricingConfig; - - // Validate required fields - if (!config.provider || !config.models || !config.currency) { - throw new Error(`Invalid pricing configuration for provider: ${provider}`); - } - - // Provider-specific normalizations - const normalizer = this.modelNormalizers.get(provider.toLowerCase()); - if (normalizer) { - config.models = normalizer(config.models); - } - - return config; - } - private getCostFilePath(provider: string): string { - if (provider === 'openrouter') { - const generatedCandidates = [ - path.join(this.generatedCostsDir, `${provider}.yaml`), - path.join(this.costsDir, 'generated', `${provider}.yaml`) - ]; - - for (const candidate of generatedCandidates) { - if (fs.existsSync(candidate)) { - return candidate; - } - } - } - - return path.join(this.costsDir, `${provider}.yaml`); - } - - // Provider-specific pricing normalizers - private modelNormalizers: Map) => Record> = new Map([ - ['anthropic', (models) => { - const normalizedModels: Record = {}; - Object.entries(models).forEach(([modelName, modelPricing]: [string, any]) => { - const normalizedPricing: ModelPricing = { ...modelPricing }; - - if (modelPricing['5m_cache_write'] !== undefined) { - normalizedPricing.cache_write = modelPricing['5m_cache_write']; - } - if (modelPricing['1h_cache_write'] !== undefined) { - normalizedPricing.cache_write = normalizedPricing.cache_write || modelPricing['1h_cache_write']; - } - if (modelPricing['cache_read'] !== undefined) { - normalizedPricing.cache_read = modelPricing['cache_read']; - } - - normalizedModels[modelName] = normalizedPricing; - }); - return normalizedModels; - }], - ['xai', (models) => { - const normalizedModels: Record = {}; - Object.entries(models).forEach(([modelName, modelPricing]: [string, any]) => { - const normalizedPricing: ModelPricing = { ...modelPricing }; - if (modelPricing['cached_input'] !== undefined) { - normalizedPricing.cache_write = modelPricing['cached_input']; - normalizedPricing.cache_read = modelPricing['cached_input']; - } - normalizedModels[modelName] = normalizedPricing; - }); - return normalizedModels; - }] - ]); - - /** - * Get pricing for a specific model (with automatic model name normalization) - */ - getModelPricing(provider: string, model: string): ModelPricing | null { - let config = this.pricingCache.get(provider); - if (!config) { - this.loadAllPricing(); - config = this.pricingCache.get(provider); - if (!config) return null; - } - - const normalizedModel = ModelUtils.normalizeModelName(model); - return config.models[normalizedModel] || null; - } - - /** - * Get all available models for a provider - */ - getProviderModels(provider: string): string[] { - const config = this.pricingCache.get(provider); - if (!config) return []; - - return Object.keys(config.models); - } - - /** - * Get all available providers - */ - getAvailableProviders(): string[] { - return Array.from(this.pricingCache.keys()); - } - - /** - * Search for models across all providers - */ - searchModels(query: string): Array<{provider: string, model: string, pricing: ModelPricing}> { - const results: Array<{provider: string, model: string, pricing: ModelPricing}> = []; - - this.pricingCache.forEach((config, provider) => { - Object.entries(config.models).forEach(([model, pricing]) => { - if (model.toLowerCase().includes(query.toLowerCase())) { - results.push({ provider, model, pricing }); - } - }); - }); - - return results; - } - - /** - * Calculate cost for a specific model usage - */ - calculateCost(provider: string, model: string, inputTokens: number, outputTokens: number, cacheWriteTokens: number = 0, cacheReadTokens: number = 0): CostCalculation | null { - const pricing = this.getModelPricing(provider, model); - if (!pricing) return null; - - const inputCost = (inputTokens / 1_000_000) * pricing.input; - const cacheWriteCost = pricing.cache_write ? (cacheWriteTokens / 1_000_000) * pricing.cache_write : 0; - const cacheReadCost = pricing.cache_read ? (cacheReadTokens / 1_000_000) * pricing.cache_read : 0; - const outputCost = (outputTokens / 1_000_000) * pricing.output; - const totalCost = inputCost + cacheWriteCost + cacheReadCost + outputCost; - - const config = this.pricingCache.get(provider); - - return { - inputCost: Math.round(inputCost * 1000000) / 1000000, // Round to 6 decimal places - cacheWriteCost: Math.round(cacheWriteCost * 1000000) / 1000000, - cacheReadCost: Math.round(cacheReadCost * 1000000) / 1000000, - outputCost: Math.round(outputCost * 1000000) / 1000000, - totalCost: Math.round(totalCost * 1000000) / 1000000, - currency: config?.currency || 'USD', - unit: config?.unit || 'per_1m_tokens' - }; - } - - /** - * Get pricing summary for all providers - */ - getPricingSummary(): Array<{provider: string, modelCount: number, lastUpdated: string}> { - return Array.from(this.pricingCache.entries()).map(([provider, config]) => ({ - provider, - modelCount: Object.keys(config.models).length, - lastUpdated: config.metadata.last_updated - })); - } - - /** - * Force reload pricing (useful for testing or when files change) - */ - reloadPricing(): void { - this.pricingCache.clear(); - this.lastLoadTime = 0; - this.loadAllPricing(); - } - - async refreshOpenRouterPricing(): Promise { - const config = getConfig(); - if (config.openrouter.skipPricingRefresh) { - return; - } - - if (this.openRouterRefreshPromise) { - return this.openRouterRefreshPromise; - } - - const costsPath = path.join(this.costsDir, PricingLoader.OPENROUTER_COST_FILE); - - const execute = async (): Promise => { - try { - const payload = await this.fetchOpenRouterModels(); - const models = this.transformOpenRouterModels(payload); - - if (!models || Object.keys(models).length === 0) { - logger.warn('OpenRouter pricing refresh returned no supported models', { - operation: 'pricing_refresh', - provider: 'openrouter', - module: 'pricing-loader' - }); - return; - } - - const currency = payload?.meta?.currency?.toUpperCase?.() ?? 'USD'; - - const doc: PricingConfig = { - provider: 'openrouter', - currency, - unit: 'MTok', - models, - metadata: { - last_updated: new Date().toISOString().slice(0, 10), - source: PricingLoader.OPENROUTER_PRICING_URL, - notes: 'Auto-refreshed from OpenRouter models API', - version: 'auto' - } - }; - - const serialized = yaml.dump(doc, { lineWidth: 120, noRefs: true }); - await fs.promises.writeFile(costsPath, serialized, 'utf8'); - - // Refresh in-memory cache immediately to avoid transient misses. - const refreshedConfig = this.loadProviderPricing('openrouter'); - this.pricingCache.set('openrouter', refreshedConfig); - this.lastLoadTime = Date.now(); - - logger.info('OpenRouter pricing refreshed', { - operation: 'pricing_refresh', - provider: 'openrouter', - modelCount: Object.keys(models).length, - module: 'pricing-loader' - }); - } catch (error) { - logger.error('Failed to refresh OpenRouter pricing, using cached YAML', error, { - operation: 'pricing_refresh', - provider: 'openrouter', - module: 'pricing-loader' - }); - } - }; - - this.openRouterRefreshPromise = execute().finally(() => { - this.openRouterRefreshPromise = null; - }); - - return this.openRouterRefreshPromise; - } - - private async fetchOpenRouterModels(): Promise { - const config = getConfig(); - const timeoutMs = config.openrouter.pricingTimeoutMs; - const retries = config.openrouter.pricingRetries; - - let lastError: unknown; - for (let attempt = 0; attempt <= retries; attempt++) { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); - try { - const response: Response = await fetch(PricingLoader.OPENROUTER_PRICING_URL, { - method: 'GET', - headers: this.buildOpenRouterPricingHeaders(), - signal: controller.signal - }); - - if (!response.ok) { - throw new Error(`OpenRouter models API responded with ${response.status}`); - } - - return await response.json(); - } catch (error) { - lastError = error; - logger.warn('OpenRouter pricing fetch attempt failed', { - attempt: attempt + 1, - retries: retries + 1, - provider: 'openrouter', - error: error instanceof Error ? error.message : String(error), - module: 'pricing-loader' - }); - } finally { - clearTimeout(timeout); - } - } - - throw lastError instanceof Error ? lastError : new Error(String(lastError)); - } - - private buildOpenRouterPricingHeaders(): Record { - const headers: Record = { - 'User-Agent': 'Ekai-Gateway-Pricing-Refresh/1.0' - }; - - const config = getConfig(); - const apiKey = config.providers.openrouter.apiKey; - if (apiKey) { - headers['Authorization'] = `Bearer ${apiKey}`; - } - return headers; - } - - private transformOpenRouterModels(payload: any): Record { - if (!payload || !Array.isArray(payload.data)) { - return {}; - } - - const models: Record = {}; - - payload.data.forEach((model: any) => { - if (!model || typeof model.id !== 'string') return; - - const [provider] = model.id.split('/'); - - const pricing = model.pricing || {}; - const multiplier = this.resolvePricingUnitMultiplier(pricing.unit); - - const input = this.toMTok(pricing.prompt, multiplier); - const output = this.toMTok(pricing.completion, multiplier); - const cacheWrite = this.toMTok(pricing.cache_write ?? pricing.cached, multiplier); - const cacheRead = this.toMTok(pricing.cache_read ?? pricing.cached, multiplier); - - if (input === undefined || output === undefined) { - return; - } - - const normalizedKey = ModelUtils.removeProviderPrefix(model.id); - const entry: ModelPricing = { - id: model.id, - input, - output, - original_provider: provider - }; - - if (cacheWrite !== undefined) { - entry.cache_write = cacheWrite; - } - - if (cacheRead !== undefined) { - entry.cache_read = cacheRead; - } - - // Store both provider-qualified and normalized keys to support lookups - models[model.id] = entry; - if (!models[normalizedKey]) { - models[normalizedKey] = entry; - } - }); - - return models; - } - - private resolvePricingUnitMultiplier(unit?: string): number { - if (!unit) { - return 1_000_000; - } - - const normalized = unit.toLowerCase(); - if (normalized.includes('mtok') || normalized.includes('million')) { - return 1; - } - if (normalized.includes('ktok') || normalized.includes('thousand')) { - return 1_000; - } - if (normalized.includes('token')) { - return 1_000_000; - } - - return 1_000_000; - } - - private toMTok(value: unknown, multiplier: number): number | undefined { - let numericValue: number; - - if (typeof value === 'number') { - numericValue = value; - } else if (typeof value === 'string') { - numericValue = Number(value); - } else { - return undefined; - } - - if (!Number.isFinite(numericValue)) { - return undefined; - } - - const scaled = numericValue * multiplier; - return Math.round(scaled * 1_000_000) / 1_000_000; - } -} - -// Export singleton instance -export const pricingLoader = new PricingLoader(); diff --git a/gateway/src/infrastructure/utils/usage-tracker.ts b/gateway/src/infrastructure/utils/usage-tracker.ts deleted file mode 100644 index 37b9066..0000000 --- a/gateway/src/infrastructure/utils/usage-tracker.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { pricingLoader, CostCalculation } from './pricing-loader.js'; -import { dbQueries, type UsageRecord } from '../db/queries.js'; -import { ModelUtils } from './model-utils.js'; -import { logger } from './logger.js'; -/** - * Usage summary interface for consistent return types - */ -export interface UsageSummary { - totalRequests: number; - totalCost: number; - totalTokens: number; - costByProvider: Record; - costByModel: Record; - records: UsageRecord[]; -} - -/** - * UsageTracker class for tracking AI model usage, costs, and analytics - * All data is persisted to SQLite database for reliability and persistence - */ -export class UsageTracker { - private static readonly MAX_RECORDS_EXPORT = 100; - private static readonly HOURS_IN_DAY = 24; - - constructor() { - // Ensure pricing is loaded when UsageTracker is instantiated - pricingLoader.loadAllPricing(); - } - - /** - * Track a chat completion request and calculate costs - * @param model - The AI model name - * @param provider - The provider (openai, anthropic, openrouter) - * @param inputTokens - Number of input tokens - * @param outputTokens - Number of output tokens - * @param cacheWriteTokens - Number of cache write tokens - * @param cacheReadTokens - Number of cache read tokens - * @param x402PaymentAmount - Actual payment amount for x402 requests (overrides YAML pricing) - * @returns Cost calculation or null if pricing not found - */ - trackUsage( - model: string, - provider: string, - inputTokens: number, - outputTokens: number, - cacheWriteTokens: number = 0, - cacheReadTokens: number = 0, - x402PaymentAmount?: string - ): CostCalculation | null { - // Input validation - if (!model?.trim() || !provider?.trim()) { - throw new Error('Model and provider are required'); - } - - if (inputTokens < 0 || outputTokens < 0 || cacheWriteTokens < 0 || cacheReadTokens < 0 || - !Number.isInteger(inputTokens) || !Number.isInteger(outputTokens) || - !Number.isInteger(cacheWriteTokens) || !Number.isInteger(cacheReadTokens)) { - throw new Error('Token counts must be non-negative integers'); - } - - const now = new Date(); - - const pricing = pricingLoader.getModelPricing(provider, model); - - // For x402 payments, use actual payment amount instead of YAML pricing - let costCalculation: CostCalculation | null; - - const paymentMethod = x402PaymentAmount ? 'x402' : 'api_key'; - - if (x402PaymentAmount) { - // Parse the actual payment amount from x402 - const totalCost = parseFloat(x402PaymentAmount); - - if (isNaN(totalCost)) { - logger.warn('Invalid x402 payment amount, falling back to YAML pricing', { - x402PaymentAmount, - model, - provider, - module: 'usage-tracker' - }); - costCalculation = pricingLoader.calculateCost(provider, model, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens); - } else { - // Use actual payment amount - distribute proportionally across token types - costCalculation = { - inputCost: 0, - cacheWriteCost: 0, - cacheReadCost: 0, - outputCost: 0, - totalCost, - currency: 'USD', // x402 amounts are in USD - unit: 'request' // x402 charges per request - }; - - logger.debug('Using x402 actual payment amount for cost tracking', { - model, - provider, - x402Amount: totalCost, - module: 'usage-tracker' - }); - } - } else { - // Calculate cost using the pricing system from YAML - costCalculation = pricingLoader.calculateCost(provider, model, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens); - } - - if (costCalculation) { - // Generate unique request ID - const requestId = this.generateRequestId(provider, model, now); - - // Save to database - try { - dbQueries.insertUsageRecord({ - request_id: requestId, - provider: provider.toLowerCase(), - model, - timestamp: now.toISOString(), - input_tokens: inputTokens, - cache_write_input_tokens: cacheWriteTokens, - cache_read_input_tokens: cacheReadTokens, - output_tokens: outputTokens, - total_tokens: inputTokens + cacheWriteTokens + cacheReadTokens + outputTokens, - input_cost: costCalculation.inputCost, - cache_write_cost: costCalculation.cacheWriteCost, - cache_read_cost: costCalculation.cacheReadCost, - output_cost: costCalculation.outputCost, - total_cost: costCalculation.totalCost, - currency: costCalculation.currency, - payment_method: paymentMethod - }); - - logger.info('Usage tracked', { - requestId, - model, - provider, - cost: costCalculation.totalCost.toFixed(6), - inputTokens, - outputTokens, - module: 'usage-tracker' - }); - - } catch (error) { - logger.error('Failed to save usage record', error, { operation: 'usage_tracking', module: 'usage-tracker' }); - throw error instanceof Error ? error : new Error(String(error)); - } - } else { - logger.warn('No pricing data found', { model, provider, operation: 'usage_tracking', module: 'usage-tracker' }); - } - - return costCalculation; - } - - /** - * Get comprehensive usage summary from database - * @param startDate - Start date for filtering (ISO string) - * @param endDate - End date for filtering (ISO string) - * @param recordLimit - Maximum number of recent records to include (default: 100) - * @returns Usage summary with totals, breakdowns, and recent records - */ - getUsageFromDatabase(startDate: string, endDate: string, recordLimit: number = UsageTracker.MAX_RECORDS_EXPORT): UsageSummary { - try { - return { - totalRequests: dbQueries.getTotalRequests(startDate, endDate), - totalCost: Number(dbQueries.getTotalCost(startDate, endDate).toFixed(6)), - totalTokens: dbQueries.getTotalTokens(startDate, endDate), - costByProvider: dbQueries.getCostByProvider(startDate, endDate), - costByModel: dbQueries.getCostByModel(startDate, endDate), - records: dbQueries.getAllUsageRecords(recordLimit, startDate, endDate) - }; - } catch (error) { - logger.error('Failed to get usage data', error, { operation: 'usage_retrieval', module: 'usage-tracker' }); - // Return empty summary rather than circular reference - return { - totalRequests: 0, - totalCost: 0, - totalTokens: 0, - costByProvider: {}, - costByModel: {}, - records: [] - }; - } - } - - /** - * Export usage records for a date range - */ - getUsageRecords(startDate: string, endDate: string): UsageRecord[] { - return dbQueries.getUsageRecordsByDateRange(startDate, endDate); - } - - /** - * Convert usage records to CSV text - */ - toCsv(records: UsageRecord[]): string { - const headers = [ - 'request_id', - 'provider', - 'model', - 'timestamp', - 'input_tokens', - 'cache_write_input_tokens', - 'cache_read_input_tokens', - 'output_tokens', - 'total_tokens', - 'input_cost_usd', - 'cache_write_cost_usd', - 'cache_read_cost_usd', - 'output_cost_usd', - 'total_cost_usd' - ]; - - const escape = (value: string | number): string => { - const str = String(value ?? ''); - const needsQuotes = str.includes(',') || str.includes('"') || str.includes('\n'); - if (!needsQuotes) return str; - return `"${str.replace(/"/g, '""')}"`; - }; - - const rows = records.map(record => [ - record.request_id, - record.provider, - record.model, - record.timestamp, - record.input_tokens, - record.cache_write_input_tokens, - record.cache_read_input_tokens, - record.output_tokens, - record.total_tokens, - record.input_cost, - record.cache_write_cost, - record.cache_read_cost, - record.output_cost, - record.total_cost - ].map(escape).join(',')); - - return [headers.join(','), ...rows].join('\n'); - } - - /** - * Get cost breakdown by provider - * @param startDate - Start date for filtering (ISO string) - * @param endDate - End date for filtering (ISO string) - * @returns Record mapping provider names to total costs - */ - getCostByProvider(startDate: string, endDate: string): Record { - try { - return dbQueries.getCostByProvider(startDate, endDate); - } catch (error) { - logger.error('Failed to get cost by provider', error, { operation: 'usage_retrieval', module: 'usage-tracker' }); - return {}; - } - } - - /** - * Get cost breakdown by model type (e.g., "gpt-4" from "gpt-4o") - * @param startDate - Start date for filtering (ISO string) - * @param endDate - End date for filtering (ISO string) - * @returns Record mapping model types to total costs - */ - getCostByModelType(startDate: string, endDate: string): Record { - try { - const costByModel = dbQueries.getCostByModel(startDate, endDate); - const costByModelType: Record = {}; - - Object.entries(costByModel).forEach(([model, cost]) => { - const modelType = this.extractModelType(model); - costByModelType[modelType] = (costByModelType[modelType] || 0) + cost; - }); - - return costByModelType; - } catch (error) { - logger.error('Failed to get cost by model type', error, { operation: 'usage_retrieval', module: 'usage-tracker' }); - return {}; - } - } - - /** - * Get hourly cost breakdown for the last 24 hours - * @returns Record mapping ISO hour strings to costs - */ - getHourlyCostBreakdown(): Record { - try { - const hourlyCosts: Record = {}; - const now = new Date(); - const oneDayAgo = new Date(now.getTime() - UsageTracker.HOURS_IN_DAY * 60 * 60 * 1000); - - // Use date range query for better performance - const records = dbQueries.getUsageRecordsByDateRange( - oneDayAgo.toISOString(), - now.toISOString() - ); - - records.forEach(record => { - const hourKey = new Date(record.timestamp).toISOString().slice(0, 13) + ':00:00Z'; - hourlyCosts[hourKey] = (hourlyCosts[hourKey] || 0) + record.total_cost; - }); - - return hourlyCosts; - } catch (error) { - logger.error('Failed to get hourly cost breakdown', error, { operation: 'usage_retrieval', module: 'usage-tracker' }); - return {}; - } - } - - /** - * Reset all usage data (clears database) - * @returns Promise that resolves when reset is complete - */ - async reset(): Promise { - try { - // Note: This would require implementing clearAllUsageRecords in dbQueries - // For now, just log that it's not implemented - logger.warn('Usage reset not implemented', { operation: 'usage_reset', module: 'usage-tracker' }); - } catch (error) { - logger.error('Failed to reset usage tracker', error, { operation: 'usage_reset', module: 'usage-tracker' }); - throw error instanceof Error ? error : new Error(String(error)); - } - } - - - /** - * Get pricing information for all available models - * @returns Pricing summary from the pricing loader - */ - getPricingInfo() { - return pricingLoader.getPricingSummary(); - } - - /** - * Search for models by name or description - * @param query - Search query string - * @returns Array of matching models - */ - searchModels(query: string) { - if (!query?.trim()) { - throw new Error('Search query is required'); - } - return pricingLoader.searchModels(query.trim()); - } - - - /** - * Generate a unique request ID - * @private - */ - private generateRequestId(provider: string, model: string, timestamp: Date): string { - const randomSuffix = Math.random().toString(36).substring(2, 11); - return `${provider}-${model}-${timestamp.getTime()}-${randomSuffix}`; - } - - /** - * Extract model type from full model name - * @private - */ - private extractModelType(model: string): string { - const parts = model.split('-'); - if (parts.length >= 2) { - return `${parts[0]}-${parts[1]}`; - } - return model; // fallback to full name if parsing fails - } -} - -// Export singleton instance -export const usageTracker = new UsageTracker(); diff --git a/gateway/src/infrastructure/utils/validation.ts b/gateway/src/infrastructure/utils/validation.ts deleted file mode 100644 index 37c3b15..0000000 --- a/gateway/src/infrastructure/utils/validation.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ChatMessage, AnthropicMessagesRequest } from 'shared/types/index.js'; - -export const VALID_ROLES = ['system', 'user', 'assistant'] as const; -export const VALID_ANTHROPIC_ROLES = ['user', 'assistant'] as const; - -export function validateMessage(msg: any): msg is ChatMessage { - return msg && - typeof msg.role === 'string' && - VALID_ROLES.includes(msg.role as any) && - typeof msg.content === 'string'; -} - -export function validateAnthropicMessage(msg: any): boolean { - if (!msg || typeof msg.role !== 'string' || !VALID_ANTHROPIC_ROLES.includes(msg.role as any)) { - return false; - } - - // Handle both string content and array content (real Anthropic API format) - if (typeof msg.content === 'string') { - return true; - } - - if (Array.isArray(msg.content)) { - return msg.content.every((item: any) => - item && typeof item.type === 'string' && typeof item.text === 'string' - ); - } - - return false; -} - -export function validateMessagesArray(messages: any[]): string | null { - if (!messages || !Array.isArray(messages)) { - return 'Messages array is required'; - } - - if (messages.length === 0) { - return 'At least one message is required'; - } - - return null; -} - -export function validateAnthropicRequest(req: AnthropicMessagesRequest): string | null { - const messagesError = validateMessagesArray(req.messages); - if (messagesError) return messagesError; - - if (!req.messages.every(validateAnthropicMessage)) { - return 'Invalid message format. Each message must have role (user/assistant) and content (string or array)'; - } - - if (!req.model) { - return 'Model is required'; - } - - // Validate system field if present - can be string or array - if (req.system !== undefined && req.system !== null) { - if (typeof req.system !== 'string' && !Array.isArray(req.system)) { - return 'System field must be a string or array'; - } - - if (Array.isArray(req.system)) { - const isValidArray = req.system.every((item: any) => - item && typeof item.type === 'string' && typeof item.text === 'string' - ); - if (!isValidArray) { - return 'System array must contain objects with type and text fields'; - } - } - } - - // max_tokens is optional for some Claude models - make it optional - // if (!req.max_tokens) { - // return 'max_tokens is required'; - // } - - return null; -} - -export function validateChatCompletionRequest(req: any): string | null { - const messagesError = validateMessagesArray(req.messages); - if (messagesError) return messagesError; - - if (!req.messages.every(validateMessage)) { - return 'Invalid message format. Each message must have role (system/user/assistant) and content (string)'; - } - - if (!req.model) { - return 'Model is required'; - } - - return null; -} \ No newline at end of file diff --git a/gateway/src/schemas/request.schema.json b/gateway/src/schemas/request.schema.json deleted file mode 100644 index ea23054..0000000 --- a/gateway/src/schemas/request.schema.json +++ /dev/null @@ -1,634 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://ekailabs.xyz/schemas/canonical-request/v1.0.0", - "title": "Canonical AI Request Schema", - "description": "Universal schema for AI provider requests, superset of all input capabilities", - - "definitions": { - "inputContent": { - "oneOf": [ - { "$ref": "#/definitions/textInput" }, - { "$ref": "#/definitions/imageInput" }, - { "$ref": "#/definitions/audioInput" }, - { "$ref": "#/definitions/videoInput" }, - { "$ref": "#/definitions/documentInput" }, - { "$ref": "#/definitions/toolResultInput" } - ] - }, - - "textInput": { - "type": "object", - "properties": { - "type": { "const": "text" }, - "text": { - "type": "string", - "minLength": 1, - "maxLength": 1000000 - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - - "imageInput": { - "type": "object", - "properties": { - "type": { "const": "image" }, - "source": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "base64" }, - "media_type": { - "enum": ["image/jpeg", "image/png", "image/gif", "image/webp"] - }, - "data": { - "type": "string", - "pattern": "^[A-Za-z0-9+/]*={0,2}$" - } - }, - "required": ["type", "media_type", "data"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "url" }, - "url": { "type": "string", "format": "uri" } - }, - "required": ["type", "url"], - "additionalProperties": false - } - ] - } - }, - "required": ["type", "source"], - "additionalProperties": false - }, - - "audioInput": { - "type": "object", - "properties": { - "type": { "const": "audio" }, - "source": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "base64" }, - "media_type": { - "enum": ["audio/wav", "audio/mp3", "audio/aac", "audio/ogg", "audio/flac"] - }, - "data": { - "type": "string", - "pattern": "^[A-Za-z0-9+/]*={0,2}$" - } - }, - "required": ["type", "media_type", "data"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "url" }, - "url": { "type": "string", "format": "uri" } - }, - "required": ["type", "url"], - "additionalProperties": false - } - ] - } - }, - "required": ["type", "source"], - "additionalProperties": false - }, - - "videoInput": { - "type": "object", - "properties": { - "type": { "const": "video" }, - "source": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "base64" }, - "media_type": { - "enum": ["video/mp4", "video/mpeg", "video/quicktime", "video/webm"] - }, - "data": { - "type": "string", - "pattern": "^[A-Za-z0-9+/]*={0,2}$" - } - }, - "required": ["type", "media_type", "data"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "url" }, - "url": { "type": "string", "format": "uri" } - }, - "required": ["type", "url"], - "additionalProperties": false - } - ] - } - }, - "required": ["type", "source"], - "additionalProperties": false - }, - - "documentInput": { - "type": "object", - "properties": { - "type": { "const": "document" }, - "source": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "base64" }, - "media_type": { - "enum": ["application/pdf", "text/plain", "text/html", "text/markdown"] - }, - "data": { - "type": "string", - "pattern": "^[A-Za-z0-9+/]*={0,2}$" - } - }, - "required": ["type", "media_type", "data"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "url" }, - "url": { "type": "string", "format": "uri" } - }, - "required": ["type", "url"], - "additionalProperties": false - } - ] - } - }, - "required": ["type", "source"], - "additionalProperties": false - }, - - "toolResultInput": { - "type": "object", - "properties": { - "type": { "const": "tool_result" }, - "tool_use_id": { "type": "string", "minLength": 1 }, - "content": { - "oneOf": [ - { "type": "string" }, - { - "type": "array", - "items": { "$ref": "#/definitions/inputContent" } - } - ] - }, - "is_error": { "type": "boolean" } - }, - "required": ["type", "tool_use_id", "content"], - "additionalProperties": false - }, - - "message": { - "type": "object", - "properties": { - "role": { - "enum": ["user", "assistant", "tool"] - }, - "content": { - "oneOf": [ - { "type": "string" }, - { - "type": "array", - "items": { "$ref": "#/definitions/inputContent" }, - "minItems": 1 - } - ] - }, - "name": { "type": "string" }, - "tool_call_id": { "type": "string" }, - "tool_calls": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "type": { "const": "function" }, - "function": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "arguments": { "type": "string" } - }, - "required": ["name", "arguments"], - "additionalProperties": false - } - }, - "required": ["id", "type", "function"], - "additionalProperties": false - } - } - }, - "required": ["role", "content"], - "additionalProperties": false - }, - - "tool": { - "type": "object", - "properties": { - "type": { "const": "function" }, - "function": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "description": { "type": "string" }, - "parameters": { - "type": "object", - "additionalProperties": true - }, - "strict": { "type": "boolean" } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["type", "function"], - "additionalProperties": false - }, - - "toolChoice": { - "oneOf": [ - { "enum": ["auto", "none", "any", "required"] }, - { - "type": "object", - "properties": { - "type": { "const": "function" }, - "function": { - "type": "object", - "properties": { - "name": { "type": "string" } - }, - "required": ["name"], - "additionalProperties": false - }, - "allow_parallel": { "type": "boolean" } - }, - "required": ["type", "function"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "tool" }, - "name": { "type": "string" }, - "allow_parallel": { "type": "boolean" } - }, - "required": ["type", "name"], - "additionalProperties": false - } - ] - }, - - "safetySettings": { - "type": "object", - "properties": { - "category": { - "enum": [ - "HARM_CATEGORY_HARASSMENT", - "HARM_CATEGORY_HATE_SPEECH", - "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "HARM_CATEGORY_DANGEROUS_CONTENT", - "HARM_CATEGORY_UNSPECIFIED" - ] - }, - "threshold": { - "enum": [ - "BLOCK_NONE", - "BLOCK_ONLY_HIGH", - "BLOCK_MEDIUM_AND_ABOVE", - "BLOCK_LOW_AND_ABOVE" - ] - } - }, - "required": ["category", "threshold"], - "additionalProperties": false - }, - - "responseFormat": { - "oneOf": [ - { "enum": ["text", "json", "json_object"] }, - { - "type": "object", - "properties": { - "type": { "const": "json_schema" }, - "json_schema": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "description": { "type": "string" }, - "schema": { - "type": "object", - "additionalProperties": true - }, - "strict": { "type": "boolean" } - }, - "required": ["name", "schema"], - "additionalProperties": false - } - }, - "required": ["type", "json_schema"], - "additionalProperties": false - } - ] - } - }, - - "type": "object", - "properties": { - "schema_version": { "const": "1.0.1" }, - "model": { - "type": "string", - "minLength": 1 - }, - "messages": { - "type": "array", - "items": { "$ref": "#/definitions/message" }, - "minItems": 1 - }, - "system": { - "oneOf": [ - { "type": "string" }, - { - "type": "array", - "items": { "$ref": "#/definitions/inputContent" } - } - ] - }, - "generation": { - "type": "object", - "properties": { - "max_tokens": { - "type": "integer", - "minimum": 1, - "maximum": 1000000 - }, - "temperature": { - "type": "number", - "minimum": 0, - "maximum": 2 - }, - "top_p": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "top_k": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "stop": { - "oneOf": [ - { "type": "string" }, - { - "type": "array", - "items": { "type": "string" }, - "maxItems": 64 - } - ] - }, - "seed": { - "type": "integer", - "minimum": 0 - }, - "frequency_penalty": { - "type": "number", - "minimum": -2, - "maximum": 2 - }, - "presence_penalty": { - "type": "number", - "minimum": -2, - "maximum": 2 - }, - "n": { - "type": "integer", - "minimum": 1, - "maximum": 20 - }, - "logprobs": { "type": "boolean" }, - "top_logprobs": { - "type": "integer", - "minimum": 0, - "maximum": 5 - }, - "logit_bias": { - "type": "object", - "patternProperties": { - "^\\d+$": { - "type": "number", - "minimum": -100, - "maximum": 100 - } - }, - "additionalProperties": false - }, - "stop_sequences": { - "type": "array", - "items": { "type": "string" }, - "maxItems": 64 - } - }, - "additionalProperties": false - }, - "tools": { - "type": "array", - "items": { "$ref": "#/definitions/tool" } - }, - "tool_choice": { "$ref": "#/definitions/toolChoice" }, - "parallel_tool_calls": { "type": "boolean" }, - "functions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "description": { "type": "string" }, - "parameters": { - "type": "object", - "additionalProperties": true - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "function_call": { - "oneOf": [ - { "enum": ["auto", "none"] }, - { - "type": "object", - "properties": { - "name": { "type": "string" } - }, - "required": ["name"], - "additionalProperties": false - } - ] - }, - "response_format": { "$ref": "#/definitions/responseFormat" }, - "safety_settings": { - "type": "array", - "items": { "$ref": "#/definitions/safetySettings" } - }, - "candidate_count": { - "type": "integer", - "minimum": 1, - "maximum": 8 - }, - "stream": { "type": "boolean" }, - "stream_options": { - "type": "object", - "properties": { - "include_usage": { "type": "boolean" } - }, - "additionalProperties": false - }, - "service_tier": { - "enum": ["auto", "default", "scale", "flex", "priority", null] - }, - "reasoning_effort": { - "enum": ["low", "medium", "high"] - }, - "modalities": { - "type": "array", - "items": { - "enum": ["text", "audio"] - }, - "uniqueItems": true - }, - "audio": { - "type": "object", - "properties": { - "voice": { - "enum": ["alloy", "echo", "fable", "onyx", "nova", "shimmer"] - }, - "format": { - "enum": ["wav", "mp3", "flac", "aac", "opus", "pcm16"] - } - }, - "additionalProperties": false - }, - "prediction": { - "type": "object", - "properties": { - "type": { - "enum": ["content"] - }, - "content": { - "oneOf": [ - { "type": "string" }, - { - "type": "array", - "items": { "$ref": "#/definitions/inputContent" } - } - ] - } - }, - "required": ["type", "content"], - "additionalProperties": false - }, - "tier": { - "enum": ["priority", "standard"] - }, - "thinking": { - "type": "object", - "properties": { - "enabled": { "type": "boolean" }, - "budget": { - "type": "integer", - "minimum": 1024 - } - }, - "additionalProperties": false - }, - "betas": { - "type": "array", - "items": { "type": "string" } - }, - "extra_headers": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "timeout": { - "type": "number", - "minimum": 0 - }, - "user": { "type": "string" }, - "context": { - "type": "object", - "properties": { - "previous_response_id": { "type": "string" }, - "cache_ref": { "type": "string" }, - "provider_state": { - "type": "object", - "additionalProperties": true - } - }, - "additionalProperties": false - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "content_type": { "type": "string" }, - "size": { "type": "integer", "minimum": 0 }, - "url": { "type": "string", "format": "uri" } - }, - "required": ["id", "name"], - "additionalProperties": false - } - }, - "provider_params": { - "type": "object", - "properties": { - "openai": { "type": "object", "additionalProperties": true }, - "anthropic": { "type": "object", "additionalProperties": true }, - "gemini": { "type": "object", "additionalProperties": true } - }, - "additionalProperties": false - }, - "meta": { - "type": "object", - "properties": { - "user_id": { "type": "string" }, - "session_id": { "type": "string" }, - "tags": { - "type": "array", - "items": { "type": "string" } - } - }, - "additionalProperties": true - } - }, - "required": ["schema_version", "model", "messages"], - "additionalProperties": false -} \ No newline at end of file diff --git a/gateway/src/schemas/response.schema.json b/gateway/src/schemas/response.schema.json deleted file mode 100644 index 25a90d2..0000000 --- a/gateway/src/schemas/response.schema.json +++ /dev/null @@ -1,371 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://ekailabs.xyz/schemas/canonical-response/v1.0.0", - "title": "Canonical AI Response Schema", - "description": "Universal schema for AI provider responses - superset of all output capabilities", - - "definitions": { - "outputContent": { - "oneOf": [ - { "$ref": "#/definitions/textOutput" }, - { "$ref": "#/definitions/thinkingOutput" }, - { "$ref": "#/definitions/toolUseOutput" }, - { "$ref": "#/definitions/codeExecutionOutput" }, - { "$ref": "#/definitions/webSearchOutput" }, - { "$ref": "#/definitions/citationOutput" } - ] - }, - - "textOutput": { - "type": "object", - "properties": { - "type": { "const": "text" }, - "text": { "type": "string" }, - "annotations": { - "type": "object", - "properties": { - "citations": { - "type": "array", - "items": { "$ref": "#/definitions/citation" } - } - }, - "additionalProperties": false - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - - "thinkingOutput": { - "type": "object", - "properties": { - "type": { "const": "thinking" }, - "thinking": { "type": "string" } - }, - "required": ["type", "thinking"], - "additionalProperties": false - }, - - "toolUseOutput": { - "type": "object", - "properties": { - "type": { "const": "tool_use" }, - "id": { "type": "string", "minLength": 1 }, - "name": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" }, - "input": { - "oneOf": [ - { "type": "object", "additionalProperties": true }, - { "type": "string" }, - { "type": "array", "items": {} }, - { "type": "null" } - ] - } - }, - "required": ["type", "id", "name", "input"], - "additionalProperties": false - }, - - "codeExecutionOutput": { - "type": "object", - "properties": { - "type": { "const": "code_execution" }, - "language": { "type": "string" }, - "code": { "type": "string" }, - "output": { "type": "string" }, - "error": { "type": "string" }, - "execution_time": { "type": "number", "minimum": 0 } - }, - "required": ["type", "language", "code"], - "additionalProperties": false - }, - - "webSearchOutput": { - "type": "object", - "properties": { - "type": { "const": "web_search" }, - "query": { "type": "string" }, - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "url": { "type": "string", "format": "uri" }, - "title": { "type": "string" }, - "snippet": { "type": "string" }, - "relevance_score": { "type": "number", "minimum": 0, "maximum": 1 } - }, - "required": ["url", "title"], - "additionalProperties": false - } - } - }, - "required": ["type", "query", "results"], - "additionalProperties": false - }, - - "citationOutput": { - "type": "object", - "properties": { - "type": { "const": "citation" }, - "sources": { - "type": "array", - "items": { "$ref": "#/definitions/citation" } - } - }, - "required": ["type", "sources"], - "additionalProperties": false - }, - - "citation": { - "type": "object", - "properties": { - "url": { "type": "string", "format": "uri" }, - "title": { "type": "string" }, - "start_index": { "type": "integer", "minimum": 0 }, - "end_index": { "type": "integer", "minimum": 0 }, - "confidence": { "type": "number", "minimum": 0, "maximum": 1 } - }, - "required": ["url"], - "additionalProperties": false - }, - - "message": { - "type": "object", - "properties": { - "role": { "const": "assistant" }, - "content": { - "type": "array", - "items": { "$ref": "#/definitions/outputContent" }, - "minItems": 1 - } - }, - "required": ["role", "content"], - "additionalProperties": false - }, - - "choice": { - "type": "object", - "properties": { - "index": { "type": "integer", "minimum": 0 }, - "message": { "$ref": "#/definitions/message" }, - "finish_reason": { - "enum": [ - "stop", "length", "tool_calls", "content_filter", - "function_call", "end_turn", "max_tokens", - "stop_sequence", "tool_use", "error", "recitation", "safety" - ] - }, - "tool_calls": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "type": { "const": "function" }, - "function": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "arguments": { "type": "string" } - }, - "required": ["name", "arguments"], - "additionalProperties": false - } - }, - "required": ["id", "type", "function"], - "additionalProperties": false - } - }, - "logprobs": { - "type": "object", - "properties": { - "content": { - "type": "array", - "items": { - "type": "object", - "properties": { - "token": { "type": "string" }, - "logprob": { "type": "number" }, - "bytes": { - "type": "array", - "items": { "type": "integer" } - }, - "top_logprobs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "token": { "type": "string" }, - "logprob": { "type": "number" }, - "bytes": { - "type": "array", - "items": { "type": "integer" } - } - }, - "required": ["token", "logprob"], - "additionalProperties": false - } - } - }, - "required": ["token", "logprob"], - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "safety_ratings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "enum": [ - "HARM_CATEGORY_HARASSMENT", - "HARM_CATEGORY_HATE_SPEECH", - "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "HARM_CATEGORY_DANGEROUS_CONTENT" - ] - }, - "probability": { - "enum": ["NEGLIGIBLE", "LOW", "MEDIUM", "HIGH"] - }, - "blocked": { "type": "boolean" } - }, - "required": ["category", "probability"], - "additionalProperties": false - } - } - }, - "required": ["index", "message"], - "additionalProperties": false - }, - - "usage": { - "type": "object", - "properties": { - "prompt_tokens": { "type": "integer", "minimum": 0 }, - "completion_tokens": { "type": "integer", "minimum": 0 }, - "total_tokens": { "type": "integer", "minimum": 0 }, - "input_tokens": { "type": "integer", "minimum": 0 }, - "output_tokens": { "type": "integer", "minimum": 0 }, - "cached_tokens": { "type": "integer", "minimum": 0 }, - "reasoning_tokens": { "type": "integer", "minimum": 0 }, - "completion_tokens_details": { - "type": "object", - "properties": { - "reasoning_tokens": { "type": "integer", "minimum": 0 }, - "audio_tokens": { "type": "integer", "minimum": 0 }, - "accepted_prediction_tokens": { "type": "integer", "minimum": 0 }, - "rejected_prediction_tokens": { "type": "integer", "minimum": 0 } - }, - "additionalProperties": false - }, - "predictions": { - "type": "object", - "properties": { - "accepted_tokens": { "type": "integer", "minimum": 0 }, - "rejected_tokens": { "type": "integer", "minimum": 0 } - }, - "additionalProperties": false - }, - "provider_breakdown": { - "type": "object", - "additionalProperties": { - "type": ["number", "integer", "string", "object", "null"] - } - }, - "prompt_tokens_details": { - "type": "object", - "properties": { - "cached_tokens": { "type": "integer", "minimum": 0 }, - "audio_tokens": { "type": "integer", "minimum": 0 } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - - "type": "object", - "properties": { - "schema_version": { "const": "1.0.1" }, - "id": { - "type": "string", - "minLength": 1 - }, - "model": { - "type": "string", - "minLength": 1 - }, - "created": { - "type": "integer", - "minimum": 0 - }, - "choices": { - "type": "array", - "items": { "$ref": "#/definitions/choice" }, - "minItems": 1 - }, - "candidates": { - "type": "array", - "items": { "$ref": "#/definitions/choice" } - }, - "usage": { "$ref": "#/definitions/usage" }, - "system_fingerprint": { "type": "string" }, - "service_tier_utilized": { - "enum": ["default", "scale", "auto", "flex", "priority"] - }, - "object": { "type": "string" }, - "type": { "type": "string" }, - "role": { "type": "string" }, - "stop_reason": { - "enum": [ - "end_turn", "max_tokens", "stop_sequence", "tool_use", null - ] - }, - "stop_sequence": { "type": "string" }, - "provider": { - "type": "string", - "enum": ["openai", "anthropic", "gemini"] - }, - "provider_raw": { - "type": "object", - "additionalProperties": true - }, - "safety_feedback": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "enum": [ - "HARM_CATEGORY_HARASSMENT", - "HARM_CATEGORY_HATE_SPEECH", - "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "HARM_CATEGORY_DANGEROUS_CONTENT" - ] - }, - "probability": { - "enum": ["NEGLIGIBLE", "LOW", "MEDIUM", "HIGH"] - }, - "blocked": { "type": "boolean" } - }, - "required": ["category", "probability"], - "additionalProperties": false - } - }, - "metadata": { - "type": "object", - "properties": { - "provider": { "type": "string" }, - "original_model": { "type": "string" }, - "processing_time": { "type": "number", "minimum": 0 } - }, - "additionalProperties": true - } - }, - "required": ["schema_version", "id", "model", "created", "choices"], - "additionalProperties": false -} \ No newline at end of file diff --git a/gateway/src/schemas/streaming-response.schema.json b/gateway/src/schemas/streaming-response.schema.json deleted file mode 100644 index 4c8e49d..0000000 --- a/gateway/src/schemas/streaming-response.schema.json +++ /dev/null @@ -1,386 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://ekai.ai/schemas/canonical-streaming-response/v1.0.0", - "title": "Canonical AI Streaming Response Schema", - "description": "Universal schema for AI provider streaming responses - superset of all streaming capabilities", - - "definitions": { - "streamingChoice": { - "type": "object", - "properties": { - "index": { "type": "integer", "minimum": 0 }, - "delta": { - "type": "object", - "properties": { - "role": { "type": "string" }, - "content": { "type": "string" }, - "tool_calls": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "index": { "type": "integer", "minimum": 0 }, - "type": { "const": "function" }, - "function": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "arguments": { "type": "string" } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - "function_call": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "arguments": { "type": "string" } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "logprobs": { - "type": "object", - "properties": { - "content": { - "type": "array", - "items": { - "type": "object", - "properties": { - "token": { "type": "string" }, - "logprob": { "type": "number" }, - "bytes": { - "type": "array", - "items": { "type": "integer" } - }, - "top_logprobs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "token": { "type": "string" }, - "logprob": { "type": "number" }, - "bytes": { - "type": "array", - "items": { "type": "integer" } - } - }, - "required": ["token", "logprob"], - "additionalProperties": false - } - } - }, - "required": ["token", "logprob"], - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "finish_reason": { - "enum": [ - "stop", "length", "tool_calls", "content_filter", - "function_call", "end_turn", "max_tokens", - "stop_sequence", "tool_use", "error", null - ] - } - }, - "required": ["index"], - "additionalProperties": false - }, - - "anthropicEvent": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "message_start" }, - "message": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "type": { "const": "message" }, - "role": { "const": "assistant" }, - "content": { "type": "array" }, - "model": { "type": "string" }, - "stop_reason": { "type": "null" }, - "stop_sequence": { "type": "null" }, - "usage": { - "type": "object", - "properties": { - "input_tokens": { "type": "integer", "minimum": 0 }, - "output_tokens": { "type": "integer", "minimum": 0 } - }, - "required": ["input_tokens", "output_tokens"], - "additionalProperties": false - } - }, - "required": ["id", "type", "role", "content", "model", "stop_reason", "stop_sequence", "usage"], - "additionalProperties": false - } - }, - "required": ["type", "message"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "content_block_start" }, - "index": { "type": "integer", "minimum": 0 }, - "content_block": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "text" }, - "text": { "type": "string" } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "tool_use" }, - "id": { "type": "string" }, - "name": { "type": "string" }, - "input": { "type": "object" } - }, - "required": ["type", "id", "name", "input"], - "additionalProperties": false - } - ] - } - }, - "required": ["type", "index", "content_block"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "content_block_delta" }, - "index": { "type": "integer", "minimum": 0 }, - "delta": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "text_delta" }, - "text": { "type": "string" } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "input_json_delta" }, - "partial_json": { "type": "string" } - }, - "required": ["type", "partial_json"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "citations_delta" }, - "citations": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } - } - }, - "required": ["type", "citations"], - "additionalProperties": false - } - ] - } - }, - "required": ["type", "index", "delta"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "content_block_stop" }, - "index": { "type": "integer", "minimum": 0 } - }, - "required": ["type", "index"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "message_delta" }, - "delta": { - "type": "object", - "properties": { - "stop_reason": { - "enum": ["end_turn", "max_tokens", "stop_sequence", "tool_use"] - }, - "stop_sequence": { "type": "string" } - }, - "additionalProperties": false - }, - "usage": { - "type": "object", - "properties": { - "output_tokens": { "type": "integer", "minimum": 0 } - }, - "required": ["output_tokens"], - "additionalProperties": false - } - }, - "required": ["type", "delta", "usage"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "message_stop" } - }, - "required": ["type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "ping" } - }, - "required": ["type"], - "additionalProperties": false - } - ] - } - }, - - "oneOf": [ - { - "type": "object", - "properties": { - "schema_version": { "const": "1.0.1" }, - "stream_type": { "const": "canonical" }, - "event": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "message_start" }, - "id": { "type": "string" }, - "model": { "type": "string" } - }, - "required": ["type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "content_delta" }, - "part": { "enum": ["text", "tool_call", "thinking"] }, - "value": { "type": "string" } - }, - "required": ["type", "part", "value"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "tool_call" }, - "name": { "type": "string" }, - "arguments_json": { "type": "string" }, - "id": { "type": "string" } - }, - "required": ["type", "name", "arguments_json"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "usage" }, - "input_tokens": { "type": "integer" }, - "output_tokens": { "type": "integer" }, - "prompt_tokens": { "type": "integer" }, - "completion_tokens": { "type": "integer" } - }, - "required": ["type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "complete" }, - "finish_reason": { - "enum": ["stop", "length", "tool_call", "content_filter", "safety", "unknown"] - } - }, - "required": ["type", "finish_reason"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { "const": "error" }, - "code": { "type": "string" }, - "message": { "type": "string" } - }, - "required": ["type", "message"], - "additionalProperties": false - } - ] - }, - "provider_raw": { - "type": "object", - "additionalProperties": true - } - }, - "required": ["schema_version", "stream_type", "event"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "schema_version": { "const": "1.0.1" }, - "stream_type": { "const": "openai" }, - "id": { "type": "string", "minLength": 1 }, - "object": { "const": "chat.completion.chunk" }, - "created": { "type": "integer", "minimum": 0 }, - "model": { "type": "string", "minLength": 1 }, - "system_fingerprint": { "type": "string" }, - "choices": { - "type": "array", - "items": { "$ref": "#/definitions/streamingChoice" }, - "minItems": 1 - }, - "usage": { - "type": "object", - "properties": { - "prompt_tokens": { "type": "integer", "minimum": 0 }, - "completion_tokens": { "type": "integer", "minimum": 0 }, - "total_tokens": { "type": "integer", "minimum": 0 } - }, - "additionalProperties": false - } - }, - "required": ["schema_version", "stream_type", "id", "object", "created", "model", "choices"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "schema_version": { "const": "1.0.1" }, - "stream_type": { "const": "anthropic" }, - "event": { "$ref": "#/definitions/anthropicEvent" } - }, - "required": ["schema_version", "stream_type", "event"], - "additionalProperties": false - } - ] -} \ No newline at end of file diff --git a/gateway/src/shared/errors/gateway-errors.ts b/gateway/src/shared/errors/gateway-errors.ts deleted file mode 100644 index eec0a0b..0000000 --- a/gateway/src/shared/errors/gateway-errors.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Standardized error hierarchy for the gateway - * All errors extend from GatewayError with consistent structure - */ - -export interface ErrorContext { - [key: string]: any; -} - -/** - * Base error class for all gateway errors - */ -export class GatewayError extends Error { - constructor( - message: string, - public readonly code: string, - public readonly statusCode: number, - public readonly context?: ErrorContext - ) { - super(message); - this.name = this.constructor.name; - Error.captureStackTrace(this, this.constructor); - } - - /** - * Convert error to JSON for logging/API responses - */ - toJSON() { - return { - error: this.name, - code: this.code, - message: this.message, - statusCode: this.statusCode, - context: this.context, - }; - } -} - -/** - * Configuration-related errors (missing env vars, invalid config, etc.) - */ -export class ConfigurationError extends GatewayError { - constructor(message: string, context?: ErrorContext) { - super(message, 'CONFIGURATION_ERROR', 500, context); - } -} - -/** - * Provider-related errors (API failures, invalid responses, etc.) - */ -export class ProviderError extends GatewayError { - constructor( - provider: string, - message: string, - statusCode: number = 502, - context?: ErrorContext - ) { - super( - message, - 'PROVIDER_ERROR', - statusCode, - { ...context, provider } - ); - } -} - -/** - * Authentication/Authorization errors - */ -export class AuthenticationError extends GatewayError { - constructor(message: string, context?: ErrorContext) { - super(message, 'AUTHENTICATION_ERROR', 401, context); - } -} - -/** - * Payment-related errors (x402, insufficient balance, etc.) - */ -export class PaymentError extends GatewayError { - constructor(message: string, context?: ErrorContext) { - super(message, 'PAYMENT_FAILED', 402, context); - } -} - -/** - * Budget exceeded errors - */ -export class BudgetExceededError extends GatewayError { - constructor(message: string, context?: ErrorContext) { - super(message, 'BUDGET_EXCEEDED', 402, context); - } -} - -/** - * Validation errors (invalid request format, missing required fields, etc.) - */ -export class ValidationError extends GatewayError { - constructor(message: string, context?: ErrorContext) { - super(message, 'VALIDATION_ERROR', 400, context); - } -} - -/** - * Rate limit errors - */ -export class RateLimitError extends GatewayError { - constructor(message: string, context?: ErrorContext) { - super(message, 'RATE_LIMIT_EXCEEDED', 429, context); - } -} - -/** - * Resource not found errors - */ -export class NotFoundError extends GatewayError { - constructor(resource: string, context?: ErrorContext) { - super( - `${resource} not found`, - 'NOT_FOUND', - 404, - { ...context, resource } - ); - } -} - -/** - * Timeout errors - */ -export class TimeoutError extends GatewayError { - constructor(operation: string, timeoutMs: number, context?: ErrorContext) { - super( - `${operation} timed out after ${timeoutMs}ms`, - 'TIMEOUT', - 408, - { ...context, operation, timeoutMs } - ); - } -} - -/** - * Helper function to convert unknown errors to GatewayError - */ -export function toGatewayError(error: unknown): GatewayError { - if (error instanceof GatewayError) { - return error; - } - - if (error instanceof Error) { - return new GatewayError( - error.message, - 'INTERNAL_ERROR', - 500, - { originalError: error.name, stack: error.stack } - ); - } - - return new GatewayError( - String(error), - 'UNKNOWN_ERROR', - 500, - { originalError: error } - ); -} - -/** - * Helper to check if an error is a specific type - */ -export function isGatewayError(error: unknown): error is GatewayError { - return error instanceof GatewayError; -} - -export function isProviderError(error: unknown): error is ProviderError { - return error instanceof ProviderError; -} - -export function isPaymentError(error: unknown): error is PaymentError { - return error instanceof PaymentError; -} - -export function isAuthenticationError(error: unknown): error is AuthenticationError { - return error instanceof AuthenticationError; -} - diff --git a/gateway/src/shared/errors/index.ts b/gateway/src/shared/errors/index.ts deleted file mode 100644 index 2864a73..0000000 --- a/gateway/src/shared/errors/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './gateway-errors.js'; - diff --git a/gateway/tests/fixtures/usage-records.json b/gateway/tests/fixtures/usage-records.json deleted file mode 100644 index 922b9a8..0000000 --- a/gateway/tests/fixtures/usage-records.json +++ /dev/null @@ -1,53 +0,0 @@ -[ - { - "request_id": "openai-gpt-4-1704067200000-abc123", - "provider": "openai", - "model": "gpt-4", - "timestamp": "2024-01-01T00:00:00.000Z", - "input_tokens": 100, - "cache_write_input_tokens": 0, - "cache_read_input_tokens": 0, - "output_tokens": 50, - "total_tokens": 150, - "input_cost": 0.003, - "cache_write_cost": 0, - "cache_read_cost": 0, - "output_cost": 0.003, - "total_cost": 0.006, - "currency": "USD" - }, - { - "request_id": "anthropic-claude-3-sonnet-1704070800000-def456", - "provider": "anthropic", - "model": "claude-3-sonnet-20240229", - "timestamp": "2024-01-01T01:00:00.000Z", - "input_tokens": 200, - "cache_write_input_tokens": 50, - "cache_read_input_tokens": 25, - "output_tokens": 100, - "total_tokens": 375, - "input_cost": 0.0006, - "cache_write_cost": 0.0001875, - "cache_read_cost": 0.0000075, - "output_cost": 0.0015, - "total_cost": 0.0022950, - "currency": "USD" - }, - { - "request_id": "xai-grok-beta-1704074400000-ghi789", - "provider": "xai", - "model": "grok-beta", - "timestamp": "2024-01-01T02:00:00.000Z", - "input_tokens": 150, - "cache_write_input_tokens": 0, - "cache_read_input_tokens": 0, - "output_tokens": 75, - "total_tokens": 225, - "input_cost": 0.00075, - "cache_write_cost": 0, - "cache_read_cost": 0, - "output_cost": 0.001125, - "total_cost": 0.001875, - "currency": "USD" - } -] diff --git a/gateway/tests/integration/usage-endpoint.test.ts b/gateway/tests/integration/usage-endpoint.test.ts deleted file mode 100644 index cb7a90d..0000000 --- a/gateway/tests/integration/usage-endpoint.test.ts +++ /dev/null @@ -1,344 +0,0 @@ -// All vitest globals (beforeEach, afterEach, vi, describe, it, expect) are available globally due to vitest config globals: true -import request from 'supertest'; -import express from 'express'; -// We'll import the handler after we mock the DB -import { TestDatabase, setupTestDatabase } from '../utils/database-helpers.js'; -import { createBulkUsageRecords, createRealisticMockUsageRecord } from '../utils/test-helpers.js'; -import { RequestHelpers } from '../utils/request-helpers.js'; -import { MockFactories } from '../utils/mock-factories.js'; - -// We'll mock the database connection per-test to bind our in-memory DB - -describe.sequential('Usage Endpoint Integration', () => { - let app: express.Express; - let testDb: TestDatabase; - const { setup, cleanup } = setupTestDatabase(); - - beforeEach(async () => { - // Setup test database - vi.resetModules(); - testDb = setup(); - - // Mock the database connection module to return our in-memory DB - vi.doMock('../../src/infrastructure/db/connection.js', () => ({ - dbConnection: { - getDatabase: () => testDb.getDatabase() - } - })); - const { handleUsageRequest } = await import('../../src/app/handlers/usage-handler.js'); - - // Create Express app with usage endpoint - app = RequestHelpers.createTestApp(); - app.get('/usage', handleUsageRequest); - - // Clear any spies - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - vi.resetModules(); - }); - - describe('GET /usage', () => { - describe('Basic Functionality', () => { - it('should return usage data with default parameters', async () => { - // Insert test data within last few hours so default window includes them - const records = createBulkUsageRecords(5, new Date(Date.now() - 4 * 60 * 60 * 1000)); - records.forEach(record => testDb.insertUsageRecord(record)); - - const response = await request(app).get('/usage'); - - RequestHelpers.expectValidUsageResponse(response); - expect(response.body.totalRequests).toBeGreaterThan(0); - expect(response.body.records).toHaveLength(5); - }); - - it('should return empty data when no usage records exist', async () => { - const response = await request(app).get('/usage'); - - RequestHelpers.expectValidUsageResponse(response); - expect(response.body.totalRequests).toBe(0); - expect(response.body.totalCost).toBe(0); - expect(response.body.totalTokens).toBe(0); - expect(response.body.records).toHaveLength(0); - expect(response.body.costByProvider).toEqual({}); - expect(response.body.costByModel).toEqual({}); - }); - - it('should handle large datasets efficiently', async () => { - // Insert a large number of records strictly within [base, end) - const base = new Date(Date.now() - 1000 * 60 * 60); // 1000 hours ago - const largeDataset = createBulkUsageRecords(1000, base); // hourly increments - testDb.insertBulkUsageRecords(largeDataset); - - // Use an end bound that strictly exceeds the last record - const endBound = new Date(base.getTime() + (1000 + 1) * 60 * 60 * 1000); // base + 1001h - - const t0 = Date.now(); - const response = await request(app) - .get('/usage') - .query({ startTime: base.toISOString(), endTime: endBound.toISOString() }); - const t1 = Date.now(); - - RequestHelpers.expectValidUsageResponse(response); - expect(response.body.totalRequests).toBe(1000); - expect(response.body.records.length).toBeLessThanOrEqual(100); // Default limit - expect(t1 - t0).toBeLessThan(2000); - }); - }); - - describe('Query Parameters', () => { - beforeEach(() => { - // Insert test data with specific timestamps - const baseDate = new Date('2024-01-01T00:00:00Z'); - const records = [ - createRealisticMockUsageRecord({ - timestamp: new Date(baseDate.getTime()).toISOString(), - total_cost: 0.005 - }), - createRealisticMockUsageRecord({ - timestamp: new Date(baseDate.getTime() + 3600000).toISOString(), // +1 hour - total_cost: 0.010 - }), - createRealisticMockUsageRecord({ - timestamp: new Date(baseDate.getTime() + 86400000).toISOString(), // +1 day - total_cost: 0.008 - }) - ]; - - records.forEach(record => testDb.insertUsageRecord(record)); - }); - - it('should filter by date range', async () => { - const queryParams = RequestHelpers.createValidQueryParams({ - startTime: '2024-01-01T00:00:00Z', - endTime: '2024-01-01T02:00:00Z' - }); - - const response = await RequestHelpers.testUsageEndpoint(app, queryParams); - - RequestHelpers.expectValidUsageResponse(response); - expect(response.body.totalRequests).toBe(2); // Only first 2 records - expect(response.body.totalCost).toBeCloseTo(0.015, 6); - }); - - it('should handle timezone parameter', async () => { - const timezones = RequestHelpers.createTimezoneTestCases(); - - for (const timezone of timezones) { - const queryParams = RequestHelpers.createValidQueryParams({ timezone }); - - const response = await RequestHelpers.testUsageEndpoint(app, queryParams); - - RequestHelpers.expectValidUsageResponse(response); - } - }); - - it('should handle date range edge cases', async () => { - const dateRanges = RequestHelpers.createDateRangeTestCases(); - - for (const dateRange of dateRanges) { - const response = await RequestHelpers.testUsageEndpoint(app, { - startTime: dateRange.startTime, - endTime: dateRange.endTime - }); - - RequestHelpers.expectValidUsageResponse(response); - } - }); - }); - - describe('Error Handling', () => { - // Only test invalid cases that the handler actually rejects - const invalidCases = [ - { startTime: 'invalid-date', endTime: new Date().toISOString() }, - { startTime: new Date().toISOString(), endTime: 'invalid-date' }, - { startTime: new Date().toISOString(), endTime: new Date(Date.now() - 86400000).toISOString() }, - { startTime: '2024-01-01T00:00:00Z', endTime: '2024-01-01T00:00:00Z' }, - { startTime: new Date().toISOString(), endTime: new Date().toISOString(), timezone: 'Invalid/Timezone' } - ]; - - invalidCases.forEach((params, index) => { - it(`should reject invalid parameters (case ${index + 1})`, async () => { - const response = await RequestHelpers.testUsageEndpoint(app, params as any); - RequestHelpers.expectErrorResponse(response, 400); - }); - }); - - it('should handle database errors gracefully', async () => { - const { usageTracker } = await import('../../src/infrastructure/utils/usage-tracker.js'); - const spy = vi.spyOn(usageTracker as any, 'getUsageFromDatabase').mockImplementation(() => { - throw new Error('Database connection failed'); - }); - const response = await request(app).get('/usage'); - spy.mockRestore(); - expect(response.status).toBeGreaterThanOrEqual(400); - expect(response.body).toHaveProperty('error'); - }); - }); - - describe('Data Aggregation', () => { - beforeEach(() => { - // Insert diverse test data for aggregation testing - const records = [ - createRealisticMockUsageRecord({ - timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(), - provider: 'openai', - model: 'gpt-4o', - total_cost: 0.015, - total_tokens: 150 - }), - createRealisticMockUsageRecord({ - timestamp: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(), - provider: 'openai', - model: 'gpt-3.5-turbo', - total_cost: 0.005, - total_tokens: 200 - }), - createRealisticMockUsageRecord({ - timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), - provider: 'anthropic', - model: 'claude-3-5-sonnet', - total_cost: 0.012, - total_tokens: 180 - }), - createRealisticMockUsageRecord({ - timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(), - provider: 'xai', - model: 'grok-4', - total_cost: 0.008, - total_tokens: 120 - }) - ]; - - records.forEach(record => testDb.insertUsageRecord(record)); - }); - - it('should aggregate costs by provider correctly', async () => { - const response = await request(app).get('/usage'); - - RequestHelpers.expectValidUsageResponse(response); - - expect(response.body.costByProvider).toHaveProperty('openai'); - expect(response.body.costByProvider).toHaveProperty('anthropic'); - expect(response.body.costByProvider).toHaveProperty('xai'); - - expect(response.body.costByProvider.openai).toBeCloseTo(0.020, 6); // 0.015 + 0.005 - expect(response.body.costByProvider.anthropic).toBeCloseTo(0.012, 6); - expect(response.body.costByProvider.xai).toBeCloseTo(0.008, 6); - }); - - it('should aggregate costs by model correctly', async () => { - const response = await request(app).get('/usage'); - - RequestHelpers.expectValidUsageResponse(response); - - expect(response.body.costByModel).toHaveProperty('gpt-4o'); - expect(response.body.costByModel).toHaveProperty('gpt-3.5-turbo'); - expect(response.body.costByModel).toHaveProperty('claude-3-5-sonnet'); - expect(response.body.costByModel).toHaveProperty('grok-4'); - - expect(response.body.costByModel['gpt-4o']).toBeCloseTo(0.015, 6); - expect(response.body.costByModel['gpt-3.5-turbo']).toBeCloseTo(0.005, 6); - expect(response.body.costByModel['claude-3-5-sonnet']).toBeCloseTo(0.012, 6); - expect(response.body.costByModel['grok-4']).toBeCloseTo(0.008, 6); - }); - - it('should calculate total metrics correctly', async () => { - const response = await request(app).get('/usage'); - - RequestHelpers.expectValidUsageResponse(response); - - expect(response.body.totalRequests).toBe(4); - expect(response.body.totalCost).toBeCloseTo(0.040, 6); // Sum of all costs - expect(response.body.totalTokens).toBe(650); // Sum of all tokens - }); - }); - - describe('Real Provider/Model Integration', () => { - it('should handle real provider/model combinations', async () => { - // Get real provider/model combinations from actual pricing files - const realCombinations = MockFactories.getRealProviderModelCombinations(); - expect(realCombinations.length).toBeGreaterThan(0); - - // Insert records with real provider/model combinations - const records = realCombinations.slice(0, 10).map(combo => - createRealisticMockUsageRecord({ - provider: combo.provider, - model: combo.model, - timestamp: new Date().toISOString() - }) - ); - - records.forEach(record => testDb.insertUsageRecord(record)); - - const response = await request(app).get('/usage'); - - RequestHelpers.expectValidUsageResponse(response); - expect(response.body.totalRequests).toBe(10); - expect(response.body.records).toHaveLength(10); - - // Verify all records have valid provider/model combinations - response.body.records.forEach((record: any) => { - expect(realCombinations.some(combo => - combo.provider === record.provider && combo.model === record.model - )).toBe(true); - }); - }); - - it('should handle cache-enabled models appropriately', async () => { - const cacheEnabledCombos = MockFactories.getRealProviderModelCombinations() - .filter(combo => combo.hasCache); - - expect(cacheEnabledCombos.length).toBeGreaterThan(0); - - // Insert records with cache tokens for cache-enabled models - const records = cacheEnabledCombos.slice(0, 5).map(combo => - createRealisticMockUsageRecord({ - provider: combo.provider, - model: combo.model, - cache_write_input_tokens: 25, - cache_read_input_tokens: 10, - cache_write_cost: 0.001, - cache_read_cost: 0.0005, - timestamp: new Date().toISOString() - }) - ); - - records.forEach(record => testDb.insertUsageRecord(record)); - - const response = await request(app).get('/usage'); - - RequestHelpers.expectValidUsageResponse(response); - - // Verify cache costs are included in totals - response.body.records.forEach((record: any) => { - if (record.cache_write_input_tokens > 0) { - expect(record.cache_write_cost).toBeGreaterThan(0); - } - if (record.cache_read_input_tokens > 0) { - expect(record.cache_read_cost).toBeGreaterThan(0); - } - }); - }); - }); - - describe('Performance Tests', () => { - // Concurrent requests test removed for self-hosted environment - - it('should respond within acceptable time limits', async () => { - // Insert substantial test data - const records = createBulkUsageRecords(500); - testDb.insertBulkUsageRecords(records); - - const responseTime = await RequestHelpers.expectResponseTime( - () => request(app).get('/usage'), - 2000 // 2 seconds max - ); - - expect(responseTime).toBeLessThan(2000); - }); - }); - }); -}); diff --git a/gateway/tests/setup.ts b/gateway/tests/setup.ts deleted file mode 100644 index 9b486b9..0000000 --- a/gateway/tests/setup.ts +++ /dev/null @@ -1,52 +0,0 @@ -// All vitest globals (beforeAll, afterAll, beforeEach, afterEach, vi, describe, it, expect) are available globally due to vitest config globals: true -import fs from 'fs'; -import path from 'path'; - -// Set test environment variables -process.env.NODE_ENV = 'test'; -process.env.DB_PATH = ':memory:'; // Use in-memory database for tests - -// Clean up any test artifacts -const testDbPath = path.join(__dirname, '../src/infrastructure/db/test.db'); -if (fs.existsSync(testDbPath)) { - fs.unlinkSync(testDbPath); -} - -// Global test configuration -beforeAll(() => { - // Set consistent timezone for date testing - process.env.TZ = 'UTC'; - - // Mock console methods to reduce noise (but keep errors visible) - global.console.log = vi.fn(); - global.console.warn = vi.fn(); - // Keep console.error for debugging -}); - -afterAll(() => { - // Cleanup any remaining test files - const testFiles = [ - path.join(__dirname, '../src/infrastructure/db/test.db'), - path.join(__dirname, '../src/infrastructure/db/test.db-shm'), - path.join(__dirname, '../src/infrastructure/db/test.db-wal') - ]; - - testFiles.forEach(file => { - if (fs.existsSync(file)) { - try { - fs.unlinkSync(file); - } catch (error) { - // Ignore cleanup errors - } - } - }); -}); - -// Reset mocks between tests -beforeEach(() => { - vi.clearAllMocks(); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); diff --git a/gateway/tests/unit/app/handlers/usage-handler.test.ts b/gateway/tests/unit/app/handlers/usage-handler.test.ts deleted file mode 100644 index 8f18152..0000000 --- a/gateway/tests/unit/app/handlers/usage-handler.test.ts +++ /dev/null @@ -1,374 +0,0 @@ -// All vitest globals (beforeEach, afterEach, vi, describe, it, expect) are available globally due to vitest config globals: true -import { UsageHandler, handleUsageRequest } from '../../../../src/app/handlers/usage-handler.js'; -import { mockRequest, mockResponse, expectValidUsageSummary } from '../../../utils/test-helpers.js'; -import { RequestHelpers } from '../../../utils/request-helpers.js'; - -// Mock dependencies -vi.mock('../../../../src/infrastructure/utils/usage-tracker.js', () => ({ - usageTracker: { - getUsageFromDatabase: vi.fn() - } -})); - -vi.mock('../../../../src/infrastructure/utils/logger.js', () => ({ - logger: { - info: vi.fn(), - error: vi.fn() - } -})); - -vi.mock('../../../../src/infrastructure/utils/error-handler.js', () => ({ - handleError: vi.fn() -})); - -describe('UsageHandler', () => { - let usageHandler: UsageHandler; - let mockReq: any; - let mockRes: any; - - beforeEach(() => { - usageHandler = new UsageHandler(); - mockReq = mockRequest(); - mockRes = mockResponse(); - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('getUsage', () => { - describe('Parameter Validation', () => { - it('should use default date range when no parameters provided', async () => { - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - const mockUsageData = { - totalRequests: 5, - totalCost: 0.015, - totalTokens: 750, - costByProvider: { openai: 0.015 }, - costByModel: { 'gpt-4o': 0.015 }, - records: [] - }; - - (usageTracker.getUsageFromDatabase as any).mockReturnValue(mockUsageData); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(usageTracker.getUsageFromDatabase).toHaveBeenCalledWith( - expect.any(String), // startDate (7 days ago) - expect.any(String) // endDate (now) - ); - expect(mockRes.json).toHaveBeenCalledWith(mockUsageData); - }); - - it('should use provided startTime and endTime', async () => { - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - const startTime = '2024-01-01T00:00:00.000Z'; - const endTime = '2024-01-02T00:00:00.000Z'; - - mockReq.query = { startTime, endTime }; - - const mockUsageData = { - totalRequests: 0, - totalCost: 0, - totalTokens: 0, - costByProvider: {}, - costByModel: {}, - records: [] - }; - - (usageTracker.getUsageFromDatabase as any).mockReturnValue(mockUsageData); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(usageTracker.getUsageFromDatabase).toHaveBeenCalledWith(startTime, endTime); - expect(mockRes.json).toHaveBeenCalledWith(mockUsageData); - }); - - it('should default timezone to UTC when not provided', async () => { - const { logger } = await import('../../../../src/infrastructure/utils/logger.js'); - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - (usageTracker.getUsageFromDatabase as any).mockReturnValue({ - totalRequests: 0, totalCost: 0, totalTokens: 0, - costByProvider: {}, costByModel: {}, records: [] - }); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(logger.info).toHaveBeenCalledWith( - 'Fetching usage data', - expect.objectContaining({ - timezone: 'UTC', - operation: 'usage_fetch', - module: 'usage-handler' - }) - ); - }); - - it('should use provided timezone', async () => { - const { logger } = await import('../../../../src/infrastructure/utils/logger.js'); - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - mockReq.query = { timezone: 'America/New_York' }; - - (usageTracker.getUsageFromDatabase as any).mockReturnValue({ - totalRequests: 0, totalCost: 0, totalTokens: 0, - costByProvider: {}, costByModel: {}, records: [] - }); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(logger.info).toHaveBeenCalledWith( - 'Fetching usage data', - expect.objectContaining({ - timezone: 'America/New_York', - operation: 'usage_fetch', - module: 'usage-handler' - }) - ); - }); - }); - - describe('Date Validation', () => { - it('should reject invalid startTime format', async () => { - mockReq.query = { startTime: 'invalid-date' }; - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(400); - expect(mockRes.json).toHaveBeenCalledWith({ - error: 'Invalid startTime format. Use RFC3339 (e.g., 2024-01-01T00:00:00Z)' - }); - }); - - it('should reject invalid endTime format', async () => { - mockReq.query = { - startTime: '2024-01-01T00:00:00Z', - endTime: 'invalid-date' - }; - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(400); - expect(mockRes.json).toHaveBeenCalledWith({ - error: 'Invalid endTime format. Use RFC3339 (e.g., 2024-01-01T23:59:59Z)' - }); - }); - - it('should reject startTime >= endTime', async () => { - mockReq.query = { - startTime: '2024-01-02T00:00:00Z', - endTime: '2024-01-01T00:00:00Z' - }; - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(400); - expect(mockRes.json).toHaveBeenCalledWith({ - error: 'startTime must be before endTime' - }); - }); - - it('should reject equal startTime and endTime', async () => { - const sameTime = '2024-01-01T00:00:00Z'; - mockReq.query = { - startTime: sameTime, - endTime: sameTime - }; - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(400); - expect(mockRes.json).toHaveBeenCalledWith({ - error: 'startTime must be before endTime' - }); - }); - - it('should accept valid date range', async () => { - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - mockReq.query = { - startTime: '2024-01-01T00:00:00Z', - endTime: '2024-01-02T00:00:00Z' - }; - - (usageTracker.getUsageFromDatabase as any).mockReturnValue({ - totalRequests: 0, totalCost: 0, totalTokens: 0, - costByProvider: {}, costByModel: {}, records: [] - }); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.status).not.toHaveBeenCalledWith(400); - expect(mockRes.json).toHaveBeenCalled(); - }); - }); - - describe('Timezone Validation', () => { - const validTimezones = RequestHelpers.createTimezoneTestCases(); - - validTimezones.forEach(timezone => { - it(`should accept valid timezone: ${timezone}`, async () => { - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - mockReq.query = { timezone }; - - vi.mocked(usageTracker.getUsageFromDatabase).mockReturnValue({ - totalRequests: 0, totalCost: 0, totalTokens: 0, - costByProvider: {}, costByModel: {}, records: [] - }); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.status).not.toHaveBeenCalledWith(400); - expect(mockRes.json).toHaveBeenCalled(); - }); - }); - - it('should reject invalid timezone', async () => { - mockReq.query = { timezone: 'Invalid/Timezone' }; - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(400); - expect(mockRes.json).toHaveBeenCalledWith({ - error: 'Invalid timezone. Use IANA format (e.g., America/New_York, UTC)' - }); - }); - - it('should treat empty timezone as UTC', async () => { - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - (usageTracker.getUsageFromDatabase as any).mockReturnValue({ - totalRequests: 0, - totalCost: 0, - totalTokens: 0, - costByProvider: {}, - costByModel: {}, - records: [] - }); - - mockReq.query = { timezone: '' }; - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.status).not.toHaveBeenCalledWith(400); - expect(mockRes.json).toHaveBeenCalled(); - }); - }); - - describe('Response Format', () => { - it('should return valid usage summary structure', async () => { - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - const mockUsageData = { - totalRequests: 10, - totalCost: 0.025, - totalTokens: 1500, - costByProvider: { - openai: 0.015, - anthropic: 0.010 - }, - costByModel: { - 'gpt-4o': 0.015, - 'claude-3-5-sonnet': 0.010 - }, - records: [ - { - id: 1, - request_id: 'test-123', - provider: 'openai', - model: 'gpt-4o', - timestamp: '2024-01-01T00:00:00Z', - input_tokens: 100, - output_tokens: 50, - total_tokens: 150, - total_cost: 0.015, - currency: 'USD' - } - ] - }; - - (usageTracker.getUsageFromDatabase as any).mockReturnValue(mockUsageData); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.json).toHaveBeenCalledWith(mockUsageData); - expectValidUsageSummary(mockUsageData); - }); - - it('should handle empty usage data', async () => { - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - const emptyUsageData = { - totalRequests: 0, - totalCost: 0, - totalTokens: 0, - costByProvider: {}, - costByModel: {}, - records: [] - }; - - (usageTracker.getUsageFromDatabase as any).mockReturnValue(emptyUsageData); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(mockRes.json).toHaveBeenCalledWith(emptyUsageData); - expectValidUsageSummary(emptyUsageData); - }); - }); - - describe('Error Handling', () => { - it('should handle usageTracker errors', async () => { - const { handleError } = await import('../../../../src/infrastructure/utils/error-handler.js'); - const { logger } = await import('../../../../src/infrastructure/utils/logger.js'); - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - const testError = new Error('Database connection failed'); - (usageTracker.getUsageFromDatabase as any).mockImplementation(() => { - throw testError; - }); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(logger.error).toHaveBeenCalledWith('Failed to fetch usage data', testError, expect.objectContaining({ module: 'usage-handler' })); - expect(handleError).toHaveBeenCalledWith(testError, mockRes); - }); - - it('should handle non-Error exceptions', async () => { - const { handleError } = await import('../../../../src/infrastructure/utils/error-handler.js'); - const { logger } = await import('../../../../src/infrastructure/utils/logger.js'); - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - const testError = 'String error'; - (usageTracker.getUsageFromDatabase as any).mockImplementation(() => { - throw testError; - }); - - await usageHandler.getUsage(mockReq, mockRes); - - expect(logger.error).toHaveBeenCalledWith( - 'Failed to fetch usage data', - testError, - expect.objectContaining({ module: 'usage-handler' }) - ); - expect(handleError).toHaveBeenCalledWith(testError, mockRes); - }); - }); - }); - - describe('handleUsageRequest function', () => { - it('should delegate to UsageHandler.getUsage', async () => { - const { usageTracker } = await import('../../../../src/infrastructure/utils/usage-tracker.js'); - - (usageTracker.getUsageFromDatabase as any).mockReturnValue({ - totalRequests: 0, totalCost: 0, totalTokens: 0, - costByProvider: {}, costByModel: {}, records: [] - }); - - await handleUsageRequest(mockReq, mockRes); - - expect(mockRes.json).toHaveBeenCalled(); - }); - }); -}); diff --git a/gateway/tests/unit/infrastructure/db/queries.test.ts b/gateway/tests/unit/infrastructure/db/queries.test.ts deleted file mode 100644 index 9112ea5..0000000 --- a/gateway/tests/unit/infrastructure/db/queries.test.ts +++ /dev/null @@ -1,497 +0,0 @@ -// All vitest globals (beforeEach, afterEach, vi, describe, it, expect) are available globally due to vitest config globals: true -import type { UsageRecord } from '../../../../src/infrastructure/db/queries.js'; -import { TestDatabase, setupTestDatabase } from '../../../utils/database-helpers.js'; -import { createMockUsageRecord, createBulkUsageRecords, expectValidUsageRecord } from '../../../utils/test-helpers.js'; - -describe('DatabaseQueries', () => { - let testDb: TestDatabase; - let queries: any; // DatabaseQueries instance loaded after mocking - let DatabaseQueriesClass: any; - const { setup, cleanup } = setupTestDatabase(); - - beforeEach(async () => { - // Ensure fresh module graph each test - vi.resetModules(); - testDb = setup(); - - // Mock the connection module to return our in-memory DB - vi.doMock('../../../../src/infrastructure/db/connection.js', () => ({ - dbConnection: { - getDatabase: () => testDb.getDatabase() - } - })); - // Import the class only after mocking connection - ({ DatabaseQueries: DatabaseQueriesClass } = await import('../../../../src/infrastructure/db/queries.js')); - - // Create a fresh DatabaseQueries instance - queries = new DatabaseQueriesClass(); - }); - - afterEach(() => { - cleanup(); - }); - - describe('insertUsageRecord', () => { - it('should insert a valid usage record', () => { - const record = createMockUsageRecord(); - - const insertId = queries.insertUsageRecord(record); - - expect(insertId).toBeTypeOf('number'); - expect(insertId).toBeGreaterThan(0); - - // Verify the record was inserted - const allRecords = testDb.getAllRecords(); - expect(allRecords).toHaveLength(1); - expect(allRecords[0].request_id).toBe(record.request_id); - }); - - it('should insert record with all fields correctly', () => { - const record = createMockUsageRecord({ - provider: 'anthropic', - model: 'claude-3-5-sonnet', - input_tokens: 200, - cache_write_input_tokens: 50, - cache_read_input_tokens: 25, - output_tokens: 100, - total_tokens: 375, - input_cost: 0.0006, - cache_write_cost: 0.0001875, - cache_read_cost: 0.0000075, - output_cost: 0.0015, - total_cost: 0.0022950 - }); - - const insertId = queries.insertUsageRecord(record); - - const allRecords = testDb.getAllRecords(); - const insertedRecord = allRecords[0]; - - expect(insertedRecord.id).toBe(insertId); - expect(insertedRecord.provider).toBe('anthropic'); - expect(insertedRecord.model).toBe('claude-3-5-sonnet'); - expect(insertedRecord.input_tokens).toBe(200); - expect(insertedRecord.cache_write_input_tokens).toBe(50); - expect(insertedRecord.cache_read_input_tokens).toBe(25); - expect(insertedRecord.output_tokens).toBe(100); - expect(insertedRecord.total_tokens).toBe(375); - expect(insertedRecord.total_cost).toBeCloseTo(0.0022950, 6); - expect(insertedRecord.currency).toBe('USD'); - expect(insertedRecord.created_at).toBeTruthy(); - }); - - it('should handle multiple inserts', () => { - const records = createBulkUsageRecords(5); - const insertIds: number[] = []; - - records.forEach(record => { - const insertId = queries.insertUsageRecord(record); - insertIds.push(insertId); - }); - - expect(insertIds).toHaveLength(5); - expect(new Set(insertIds).size).toBe(5); // All IDs should be unique - expect(testDb.getRecordCount()).toBe(5); - }); - - it('should enforce unique request_id constraint', () => { - const record1 = createMockUsageRecord({ request_id: 'duplicate-id' }); - const record2 = createMockUsageRecord({ request_id: 'duplicate-id' }); - - queries.insertUsageRecord(record1); - - expect(() => queries.insertUsageRecord(record2)).toThrow(); - }); - }); - - describe('getAllUsageRecords', () => { - let testRecords: Array>; - let baseDate: Date; - - beforeEach(() => { - // Insert test data with specific, predictable timestamps - baseDate = new Date('2024-01-01T00:00:00.000Z'); - testRecords = [ - createMockUsageRecord({ - request_id: 'test-record-1', - timestamp: baseDate.toISOString(), // 00:00:00 - provider: 'openai', - model: 'gpt-4o', - total_cost: 0.005 - }), - createMockUsageRecord({ - request_id: 'test-record-2', - timestamp: new Date(baseDate.getTime() + 3600000).toISOString(), // 01:00:00 - provider: 'anthropic', - model: 'claude-3-5-sonnet', - total_cost: 0.010 - }), - createMockUsageRecord({ - request_id: 'test-record-3', - timestamp: new Date(baseDate.getTime() + 7200000).toISOString(), // 02:00:00 - provider: 'xai', - model: 'grok-4', - total_cost: 0.008 - }) - ]; - - testRecords.forEach(record => queries.insertUsageRecord(record)); - }); - - it('should return records within date range', () => { - const startDate = '2024-01-01T00:00:00.000Z'; - const endDate = '2024-01-01T03:00:00.000Z'; - - const records = queries.getAllUsageRecords(100, startDate, endDate); - - expect(records).toHaveLength(3); - records.forEach(record => { - expectValidUsageRecord(record); - const recordTime = new Date(record.timestamp).getTime(); - const startTime = new Date(startDate).getTime(); - const endTime = new Date(endDate).getTime(); - - expect(recordTime).toBeGreaterThanOrEqual(startTime); - expect(recordTime).toBeLessThan(endTime); - }); - }); - - it('should respect limit parameter', () => { - const startDate = '2024-01-01T00:00:00Z'; - const endDate = '2024-01-01T03:00:00Z'; - - const records = queries.getAllUsageRecords(2, startDate, endDate); - - expect(records).toHaveLength(2); - }); - - it('should return records in descending timestamp order', () => { - const startDate = '2024-01-01T00:00:00.000Z'; - const endDate = '2024-01-01T03:00:00.000Z'; - - const records = queries.getAllUsageRecords(100, startDate, endDate); - - expect(records).toHaveLength(3); - - // Records should be ordered: newest first (02:00:00, 01:00:00, 00:00:00) - expect(records[0].timestamp).toBe(new Date(baseDate.getTime() + 7200000).toISOString()); // 02:00:00 - expect(records[1].timestamp).toBe(new Date(baseDate.getTime() + 3600000).toISOString()); // 01:00:00 - expect(records[2].timestamp).toBe(baseDate.toISOString()); // 00:00:00 - - // Double-check with time comparison - for (let i = 0; i < records.length - 1; i++) { - const currentTime = new Date(records[i].timestamp).getTime(); - const nextTime = new Date(records[i + 1].timestamp).getTime(); - expect(currentTime).toBeGreaterThanOrEqual(nextTime); - } - }); - - it('should return empty array for date range with no data', () => { - const startDate = '2023-01-01T00:00:00Z'; - const endDate = '2023-01-02T00:00:00Z'; - - const records = queries.getAllUsageRecords(100, startDate, endDate); - - expect(records).toHaveLength(0); - }); - }); - - describe('getUsageRecordsByDateRange', () => { - it('should return records between dates (inclusive)', () => { - const baseDate = new Date('2024-01-01T12:00:00Z'); - const records = [ - createMockUsageRecord({ timestamp: new Date(baseDate.getTime() - 3600000).toISOString() }), // -1 hour - createMockUsageRecord({ timestamp: baseDate.toISOString() }), // exact start - createMockUsageRecord({ timestamp: new Date(baseDate.getTime() + 3600000).toISOString() }), // +1 hour - createMockUsageRecord({ timestamp: new Date(baseDate.getTime() + 7200000).toISOString() }) // +2 hours - ]; - - records.forEach(record => queries.insertUsageRecord(record)); - - const startDate = baseDate.toISOString(); - const endDate = new Date(baseDate.getTime() + 3600000).toISOString(); - - const result = queries.getUsageRecordsByDateRange(startDate, endDate); - - expect(result).toHaveLength(2); // Should include start and end times - }); - }); - - describe('getTotalCost', () => { - beforeEach(() => { - const records = [ - createMockUsageRecord({ - timestamp: '2024-01-01T00:00:00Z', - total_cost: 0.005 - }), - createMockUsageRecord({ - timestamp: '2024-01-01T01:00:00Z', - total_cost: 0.010 - }), - createMockUsageRecord({ - timestamp: '2024-01-02T00:00:00Z', - total_cost: 0.015 - }) - ]; - - records.forEach(record => queries.insertUsageRecord(record)); - }); - - it('should return total cost within date range', () => { - const startDate = '2024-01-01T00:00:00Z'; - const endDate = '2024-01-01T02:00:00Z'; - - const totalCost = queries.getTotalCost(startDate, endDate); - - expect(totalCost).toBeCloseTo(0.015, 6); - }); - - it('should return 0 for date range with no data', () => { - const startDate = '2023-01-01T00:00:00Z'; - const endDate = '2023-01-02T00:00:00Z'; - - const totalCost = queries.getTotalCost(startDate, endDate); - - expect(totalCost).toBe(0); - }); - }); - - describe('getTotalTokens', () => { - beforeEach(() => { - const records = [ - createMockUsageRecord({ - timestamp: '2024-01-01T00:00:00Z', - total_tokens: 150 - }), - createMockUsageRecord({ - timestamp: '2024-01-01T01:00:00Z', - total_tokens: 200 - }), - createMockUsageRecord({ - timestamp: '2024-01-02T00:00:00Z', - total_tokens: 100 - }) - ]; - - records.forEach(record => queries.insertUsageRecord(record)); - }); - - it('should return total tokens within date range', () => { - const startDate = '2024-01-01T00:00:00Z'; - const endDate = '2024-01-01T02:00:00Z'; - - const totalTokens = queries.getTotalTokens(startDate, endDate); - - expect(totalTokens).toBe(350); - }); - - it('should return 0 for date range with no data', () => { - const startDate = '2023-01-01T00:00:00Z'; - const endDate = '2023-01-02T00:00:00Z'; - - const totalTokens = queries.getTotalTokens(startDate, endDate); - - expect(totalTokens).toBe(0); - }); - }); - - describe('getTotalRequests', () => { - beforeEach(() => { - // Create 5 records within the target date range - const baseDate = new Date('2024-01-01T10:00:00.000Z'); - const records = []; - - for (let i = 0; i < 5; i++) { - const timestamp = new Date(baseDate.getTime() + (i * 3600000)).toISOString(); // Each hour apart - records.push(createMockUsageRecord({ - request_id: `total-requests-test-${i}`, - timestamp - })); - } - - records.forEach(record => queries.insertUsageRecord(record)); - - // Add one more record outside the date range (next day) - const outsideRecord = createMockUsageRecord({ - request_id: 'outside-range-record', - timestamp: '2024-01-02T10:00:00.000Z' - }); - queries.insertUsageRecord(outsideRecord); - }); - - it('should return request count within date range', () => { - const startDate = '2024-01-01T00:00:00.000Z'; - const endDate = '2024-01-02T00:00:00.000Z'; // End before the outside record - - const totalRequests = queries.getTotalRequests(startDate, endDate); - - expect(totalRequests).toBe(5); - }); - - it('should return 0 for date range with no data', () => { - const startDate = '2023-01-01T00:00:00Z'; - const endDate = '2023-01-02T00:00:00Z'; - - const totalRequests = queries.getTotalRequests(startDate, endDate); - - expect(totalRequests).toBe(0); - }); - }); - - describe('getCostByProvider', () => { - beforeEach(() => { - const records = [ - createMockUsageRecord({ - timestamp: '2024-01-01T00:00:00Z', - provider: 'openai', - total_cost: 0.005 - }), - createMockUsageRecord({ - timestamp: '2024-01-01T01:00:00Z', - provider: 'openai', - total_cost: 0.010 - }), - createMockUsageRecord({ - timestamp: '2024-01-01T02:00:00Z', - provider: 'anthropic', - total_cost: 0.008 - }), - createMockUsageRecord({ - timestamp: '2024-01-01T03:00:00Z', - provider: 'xai', - total_cost: 0.012 - }) - ]; - - records.forEach(record => queries.insertUsageRecord(record)); - }); - - it('should return cost grouped by provider', () => { - const startDate = '2024-01-01T00:00:00Z'; - const endDate = '2024-01-01T04:00:00Z'; - - const costByProvider = queries.getCostByProvider(startDate, endDate); - - expect(costByProvider).toEqual({ - openai: 0.015, - anthropic: 0.008, - xai: 0.012 - }); - }); - - it('should return empty object for date range with no data', () => { - const startDate = '2023-01-01T00:00:00Z'; - const endDate = '2023-01-02T00:00:00Z'; - - const costByProvider = queries.getCostByProvider(startDate, endDate); - - expect(costByProvider).toEqual({}); - }); - }); - - describe('getCostByModel', () => { - beforeEach(() => { - const records = [ - createMockUsageRecord({ - timestamp: '2024-01-01T00:00:00Z', - model: 'gpt-4o', - total_cost: 0.005 - }), - createMockUsageRecord({ - timestamp: '2024-01-01T01:00:00Z', - model: 'gpt-4o', - total_cost: 0.010 - }), - createMockUsageRecord({ - timestamp: '2024-01-01T02:00:00Z', - model: 'claude-3-5-sonnet', - total_cost: 0.008 - }), - createMockUsageRecord({ - timestamp: '2024-01-01T03:00:00Z', - model: 'grok-4', - total_cost: 0.012 - }) - ]; - - records.forEach(record => queries.insertUsageRecord(record)); - }); - - it('should return cost grouped by model', () => { - const startDate = '2024-01-01T00:00:00Z'; - const endDate = '2024-01-01T04:00:00Z'; - - const costByModel = queries.getCostByModel(startDate, endDate); - - expect(costByModel).toEqual({ - 'gpt-4o': 0.015, - 'claude-3-5-sonnet': 0.008, - 'grok-4': 0.012 - }); - }); - - it('should return empty object for date range with no data', () => { - const startDate = '2023-01-01T00:00:00Z'; - const endDate = '2023-01-02T00:00:00Z'; - - const costByModel = queries.getCostByModel(startDate, endDate); - - expect(costByModel).toEqual({}); - }); - }); - - describe('Edge Cases', () => { - it('should handle very large numbers', () => { - const record = createMockUsageRecord({ - input_tokens: 1000000, - output_tokens: 500000, - total_tokens: 1500000, - total_cost: 999.999999 - }); - - const insertId = queries.insertUsageRecord(record); - expect(insertId).toBeTypeOf('number'); - - const records = testDb.getAllRecords(); - expect(records[0].input_tokens).toBe(1000000); - expect(records[0].total_cost).toBeCloseTo(999.999999, 6); - }); - - it('should handle zero values', () => { - const record = createMockUsageRecord({ - input_tokens: 0, - cache_write_input_tokens: 0, - cache_read_input_tokens: 0, - output_tokens: 0, - total_tokens: 0, - input_cost: 0, - cache_write_cost: 0, - cache_read_cost: 0, - output_cost: 0, - total_cost: 0 - }); - - const insertId = queries.insertUsageRecord(record); - expect(insertId).toBeTypeOf('number'); - - const records = testDb.getAllRecords(); - expect(records[0].total_tokens).toBe(0); - expect(records[0].total_cost).toBe(0); - }); - - it('should handle date range edge cases', () => { - const record = createMockUsageRecord({ - timestamp: '2024-01-01T00:00:00.000Z' - }); - queries.insertUsageRecord(record); - - // Exact boundary test - const records1 = queries.getAllUsageRecords(100, '2024-01-01T00:00:00.000Z', '2024-01-01T00:00:00.001Z'); - expect(records1).toHaveLength(1); - - // Just before boundary - const records2 = queries.getAllUsageRecords(100, '2023-12-31T23:59:59.999Z', '2024-01-01T00:00:00.000Z'); - expect(records2).toHaveLength(0); - }); - }); -}); diff --git a/gateway/tests/utils/database-helpers.ts b/gateway/tests/utils/database-helpers.ts deleted file mode 100644 index 28ffc2a..0000000 --- a/gateway/tests/utils/database-helpers.ts +++ /dev/null @@ -1,143 +0,0 @@ -import Database from 'better-sqlite3'; -import fs from 'fs'; -import path from 'path'; -// vi is available globally due to vitest config globals: true -import type { UsageRecord } from '../../src/infrastructure/db/queries.js'; - -/** - * Database testing utilities - */ - -export const REAL_PRICING_DIR = path.join(__dirname, '../../src/costs'); -export const TEST_PRICING_DIR = path.join(__dirname, '../fixtures/pricing'); - -export class TestDatabase { - private db: Database.Database; - private schemaPath: string; - - constructor() { - // Create in-memory database for each test - this.db = new Database(':memory:'); - this.schemaPath = path.join(__dirname, '../../src/infrastructure/db/schema.sql'); - this.initializeSchema(); - } - - private initializeSchema(): void { - if (!fs.existsSync(this.schemaPath)) { - throw new Error(`Schema file not found: ${this.schemaPath}`); - } - - const schema = fs.readFileSync(this.schemaPath, 'utf8'); - this.db.exec(schema); - } - - getDatabase(): Database.Database { - return this.db; - } - - insertUsageRecord(record: Omit): number { - const stmt = this.db.prepare(` - INSERT INTO usage_records ( - request_id, provider, model, timestamp, - input_tokens, cache_write_input_tokens, cache_read_input_tokens, output_tokens, total_tokens, - input_cost, cache_write_cost, cache_read_cost, output_cost, total_cost, currency - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - const result = stmt.run( - record.request_id, - record.provider, - record.model, - record.timestamp, - record.input_tokens, - record.cache_write_input_tokens, - record.cache_read_input_tokens, - record.output_tokens, - record.total_tokens, - record.input_cost, - record.cache_write_cost, - record.cache_read_cost, - record.output_cost, - record.total_cost, - record.currency - ); - - return result.lastInsertRowid as number; - } - - insertBulkUsageRecords(records: Array>): number[] { - const ids: number[] = []; - const transaction = this.db.transaction((records: Array>) => { - for (const record of records) { - const id = this.insertUsageRecord(record); - ids.push(id); - } - }); - - transaction(records); - return ids; - } - - getAllRecords(): UsageRecord[] { - const stmt = this.db.prepare('SELECT * FROM usage_records ORDER BY timestamp DESC'); - return stmt.all() as UsageRecord[]; - } - - getRecordCount(): number { - const stmt = this.db.prepare('SELECT COUNT(*) as count FROM usage_records'); - const result = stmt.get() as { count: number }; - return result.count; - } - - clearAllRecords(): void { - this.db.exec('DELETE FROM usage_records'); - } - - close(): void { - this.db.close(); - } - - // Helper to create a mock database connection for dependency injection - static createMockConnection(testDb: TestDatabase): any { - return { - getDatabase: () => testDb.getDatabase() - }; - } -} - -/** - * Mock the database connection module for testing - */ -export const mockDatabaseConnection = (testDb: TestDatabase) => { - vi.doMock('../../src/infrastructure/db/connection.js', () => ({ - dbConnection: TestDatabase.createMockConnection(testDb) - })); -}; - -/** - * Create a fresh test database for each test - */ -export const createTestDatabase = (): TestDatabase => { - return new TestDatabase(); -}; - -/** - * Setup database for testing with automatic cleanup - */ -export const setupTestDatabase = () => { - let testDb: TestDatabase; - - const setup = () => { - testDb = createTestDatabase(); - mockDatabaseConnection(testDb); - return testDb; - }; - - const cleanup = () => { - if (testDb) { - testDb.close(); - } - }; - - return { setup, cleanup }; -}; diff --git a/gateway/tests/utils/mock-factories.ts b/gateway/tests/utils/mock-factories.ts deleted file mode 100644 index 489a824..0000000 --- a/gateway/tests/utils/mock-factories.ts +++ /dev/null @@ -1,243 +0,0 @@ -// vi is available globally due to vitest config globals: true -import fs from 'fs'; -import path from 'path'; -import type { CostCalculation, PricingConfig, ModelPricing } from '../../src/infrastructure/utils/pricing-loader.js'; - -/** - * Mock factories for creating test data - */ - -export class MockFactories { - - static createCostCalculation(overrides: Partial = {}): CostCalculation { - return { - inputCost: 0.001, - cacheWriteCost: 0.0005, - cacheReadCost: 0.0001, - outputCost: 0.002, - totalCost: 0.0036, - currency: 'USD', - unit: 'MTok', - ...overrides - }; - } - - static createModelPricing(overrides: Partial = {}): ModelPricing { - return { - input: 30.0, - output: 60.0, - cache_write: 3.75, - cache_read: 0.3, - ...overrides - }; - } - - /** - * Load real pricing config from actual src/costs files - */ - static loadRealPricingConfig(provider: string): PricingConfig | null { - const realPricingDir = path.join(__dirname, '../../src/costs'); - const filePath = path.join(realPricingDir, `${provider}.yaml`); - - if (!fs.existsSync(filePath)) { - return null; - } - - try { - const content = fs.readFileSync(filePath, 'utf8'); - const yaml = require('js-yaml'); - return yaml.load(content) as PricingConfig; - } catch { - return null; - } - } - - /** - * Create a minimal test pricing config for controlled unit tests - */ - static createMinimalPricingConfig(provider: string = 'test-provider', overrides: Partial = {}): PricingConfig { - return { - provider, - currency: 'USD', - unit: 'MTok', - models: { - // OpenAI pattern: cached_input - 'test-openai-model': { - input: 2.50, - output: 10.0, - cache_write: 1.25 // Will be mapped to cached_input for OpenAI - }, - // Anthropic pattern: 5m_cache_write, 1h_cache_write, cache_read - 'test-anthropic-model': { - input: 3.0, - output: 15.0, - cache_write: 3.75, // Will be mapped to 5m_cache_write - cache_read: 0.3 - }, - // Simple model without cache - 'test-simple-model': { - input: 1.0, - output: 2.0 - } - }, - metadata: { - last_updated: '2024-01-01', - source: 'test', - notes: 'Minimal test pricing configuration', - version: '1.0.0' - }, - ...overrides - }; - } - - static createYamlContent(config: PricingConfig): string { - return ` -provider: "${config.provider}" -currency: "${config.currency}" -unit: "${config.unit}" - -models: -${Object.entries(config.models).map(([model, pricing]) => ` - ${model}: - input: ${pricing.input} - output: ${pricing.output} - ${pricing.cache_write ? `cache_write: ${pricing.cache_write}` : ''} - ${pricing.cache_read ? `cache_read: ${pricing.cache_read}` : ''} -`).join('')} - -metadata: - last_updated: "${config.metadata.last_updated}" - source: "${config.metadata.source}" - notes: "${config.metadata.notes}" - version: "${config.metadata.version}" - `.trim(); - } - - static createMockFileSystem(files: Record) { - const mockFs = { - existsSync: vi.fn((path: string) => path in files), - readFileSync: vi.fn((path: string, encoding?: string) => { - if (path in files) { - return files[path]; - } - throw new Error(`ENOENT: no such file or directory, open '${path}'`); - }), - readdirSync: vi.fn((path: string) => { - // Return filenames that start with the directory path - return Object.keys(files) - .filter(file => file.startsWith(path)) - .map(file => file.replace(path + '/', '')) - .filter(file => !file.includes('/')); - }) - }; - - return mockFs; - } - - static createMockLogger() { - return { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn() - }; - } - - static createDateSequence(startDate: Date, count: number, intervalHours: number = 1): Date[] { - const dates = []; - for (let i = 0; i < count; i++) { - const date = new Date(startDate); - date.setHours(date.getHours() + (i * intervalHours)); - dates.push(date); - } - return dates; - } - - static createProviderModelCombinations(): Array<{ provider: string; model: string }> { - return [ - { provider: 'openai', model: 'gpt-4' }, - { provider: 'openai', model: 'gpt-3.5-turbo' }, - { provider: 'anthropic', model: 'claude-3-sonnet-20240229' }, - { provider: 'anthropic', model: 'claude-3-haiku-20240307' }, - { provider: 'xai', model: 'grok-beta' }, - { provider: 'openrouter', model: 'meta-llama/llama-2-70b-chat' } - ]; - } - - static createErrorScenarios() { - return { - databaseError: new Error('Database connection failed'), - fileNotFound: new Error('ENOENT: no such file or directory'), - invalidYaml: new Error('YAMLException: bad indentation'), - networkError: new Error('ECONNREFUSED'), - validationError: new Error('Invalid input parameters') - }; - } - - /** - * Create mock file system that points to real pricing files - */ - static createRealPricingMockFs() { - const realPricingDir = path.join(__dirname, '../../src/costs'); - const files: Record = {}; - - // Load actual pricing files - if (fs.existsSync(realPricingDir)) { - const pricingFiles = fs.readdirSync(realPricingDir) - .filter(file => file.endsWith('.yaml') || file.endsWith('.yml')); - - pricingFiles.forEach(file => { - const filePath = path.join(realPricingDir, file); - const content = fs.readFileSync(filePath, 'utf8'); - files[filePath] = content; - }); - } - - return MockFactories.createMockFileSystem(files); - } - - /** - * Get real provider/model combinations for testing - */ - static getRealProviderModelCombinations(): Array<{ provider: string; model: string; hasCache: boolean }> { - const combinations: Array<{ provider: string; model: string; hasCache: boolean }> = []; - const providers = ['openai', 'anthropic', 'xAI', 'openrouter']; - - providers.forEach(provider => { - const config = MockFactories.loadRealPricingConfig(provider); - if (config?.models) { - Object.entries(config.models).forEach(([model, pricing]) => { - const hasCache = !!(pricing.cache_write || pricing.cache_read || - (pricing as any).cached_input || - (pricing as any)['5m_cache_write'] || - (pricing as any)['1h_cache_write']); - - combinations.push({ provider, model, hasCache }); - }); - } - }); - - return combinations; - } - - /** - * Get real model names from actual pricing files for testing - */ - static getRealModelNames(provider: string): string[] { - const realPricingDir = path.join(__dirname, '../../src/costs'); - const filePath = path.join(realPricingDir, `${provider}.yaml`); - - if (!fs.existsSync(filePath)) { - return []; - } - - try { - const content = fs.readFileSync(filePath, 'utf8'); - const yaml = require('js-yaml'); - const config = yaml.load(content) as PricingConfig; - return Object.keys(config.models || {}); - } catch { - return []; - } - } -} diff --git a/gateway/tests/utils/request-helpers.ts b/gateway/tests/utils/request-helpers.ts deleted file mode 100644 index badbedb..0000000 --- a/gateway/tests/utils/request-helpers.ts +++ /dev/null @@ -1,143 +0,0 @@ -import request from 'supertest'; -import express from 'express'; -import type { Express } from 'express'; - -/** - * HTTP request testing utilities - */ - -export class RequestHelpers { - - static createTestApp(): Express { - const app = express(); - app.use(express.json({ limit: '50mb' })); - return app; - } - - static async testUsageEndpoint( - app: Express, - queryParams: Record = {} - ) { - const queryString = new URLSearchParams(queryParams).toString(); - const url = queryString ? `/usage?${queryString}` : '/usage'; - - return request(app).get(url); - } - - static createValidQueryParams(overrides: Record = {}) { - const now = new Date(); - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(now.getDate() - 7); - - return { - startTime: sevenDaysAgo.toISOString(), - endTime: now.toISOString(), - timezone: 'UTC', - ...overrides - }; - } - - static createInvalidQueryParams() { - return [ - // Invalid date formats - { startTime: 'invalid-date', endTime: new Date().toISOString() }, - { startTime: new Date().toISOString(), endTime: 'invalid-date' }, - - // Invalid date ranges - { startTime: new Date().toISOString(), endTime: new Date(Date.now() - 86400000).toISOString() }, - - // Invalid timezones - { startTime: new Date().toISOString(), endTime: new Date().toISOString(), timezone: 'Invalid/Timezone' }, - - // Edge cases - { startTime: '', endTime: '' }, - { startTime: null, endTime: null }, - ]; - } - - static createTimezoneTestCases() { - return [ - 'UTC', - 'America/New_York', - 'Europe/London', - 'Asia/Tokyo', - 'Australia/Sydney', - 'America/Los_Angeles' - ]; - } - - static createDateRangeTestCases() { - const now = new Date(); - - return [ - // Last hour - { - name: 'last hour', - startTime: new Date(now.getTime() - 60 * 60 * 1000).toISOString(), - endTime: now.toISOString() - }, - - // Last 24 hours - { - name: 'last 24 hours', - startTime: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(), - endTime: now.toISOString() - }, - - // Last 7 days - { - name: 'last 7 days', - startTime: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), - endTime: now.toISOString() - }, - - // Last 30 days - { - name: 'last 30 days', - startTime: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), - endTime: now.toISOString() - }, - - // Custom range - { - name: 'custom range', - startTime: '2024-01-01T00:00:00Z', - endTime: '2024-01-02T00:00:00Z' - } - ]; - } - - static expectValidUsageResponse(response: any) { - expect(response.status).toBe(200); - expect(response.body).toHaveProperty('totalRequests'); - expect(response.body).toHaveProperty('totalCost'); - expect(response.body).toHaveProperty('totalTokens'); - expect(response.body).toHaveProperty('costByProvider'); - expect(response.body).toHaveProperty('costByModel'); - expect(response.body).toHaveProperty('records'); - - expect(typeof response.body.totalRequests).toBe('number'); - expect(typeof response.body.totalCost).toBe('number'); - expect(typeof response.body.totalTokens).toBe('number'); - expect(Array.isArray(response.body.records)).toBe(true); - } - - static expectErrorResponse(response: any, statusCode: number = 400) { - expect(response.status).toBe(statusCode); - expect(response.body).toHaveProperty('error'); - expect(typeof response.body.error).toBe('string'); - } - - static async expectResponseTime( - testFn: () => Promise, - maxTimeMs: number = 1000 - ) { - const startTime = Date.now(); - await testFn(); - const endTime = Date.now(); - const duration = endTime - startTime; - - expect(duration).toBeLessThan(maxTimeMs); - return duration; - } -} diff --git a/gateway/tests/utils/test-helpers.ts b/gateway/tests/utils/test-helpers.ts deleted file mode 100644 index fb37a94..0000000 --- a/gateway/tests/utils/test-helpers.ts +++ /dev/null @@ -1,163 +0,0 @@ -// vi is available globally due to vitest config globals: true -import type { Request, Response } from 'express'; -import type { UsageRecord } from '../../src/infrastructure/db/queries.js'; -import type { PricingConfig } from '../../src/infrastructure/utils/pricing-loader.js'; - -/** - * Test helper utilities for usage-related tests - */ - -export const createMockUsageRecord = (overrides: Partial = {}): Omit => ({ - request_id: `test-request-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, - provider: 'openai', - model: 'gpt-4o', - timestamp: new Date().toISOString(), - input_tokens: 100, - cache_write_input_tokens: 0, - cache_read_input_tokens: 0, - output_tokens: 50, - total_tokens: 150, - input_cost: 0.001, - cache_write_cost: 0, - cache_read_cost: 0, - output_cost: 0.002, - total_cost: 0.003, - currency: 'USD', - ...overrides -}); - -/** - * Create mock usage record with realistic provider/model combinations from real pricing - */ -export const createRealisticMockUsageRecord = (overrides: Partial = {}): Omit => { - // Use real provider/model combinations - const combinations = [ - { provider: 'openai', model: 'gpt-4o' }, - { provider: 'openai', model: 'gpt-3.5-turbo' }, - { provider: 'anthropic', model: 'claude-3-5-sonnet' }, - { provider: 'anthropic', model: 'claude-3-haiku' }, - { provider: 'xai', model: 'grok-4' }, - { provider: 'openrouter', model: 'gpt-5' } - ]; - - const combo = combinations[Math.floor(Math.random() * combinations.length)]; - - return createMockUsageRecord({ - provider: combo.provider, - model: combo.model, - ...overrides - }); -}; - -export const sleep = (ms: number): Promise => { - return new Promise(resolve => setTimeout(resolve, ms)); -}; - -export const createSequentialTimestamps = (baseDate: Date, count: number, intervalMs: number = 3600000): string[] => { - const timestamps = []; - for (let i = 0; i < count; i++) { - const date = new Date(baseDate.getTime() + (i * intervalMs)); - timestamps.push(date.toISOString()); - } - return timestamps; -}; - -export const createDateRange = (daysAgo: number = 7): { start: string; end: string } => { - const end = new Date(); - const start = new Date(); - start.setDate(start.getDate() - daysAgo); - - return { - start: start.toISOString(), - end: end.toISOString() - }; -}; - -export const createFixedDate = (dateString: string): Date => { - return new Date(dateString); -}; - -export const mockResponse = (): Response => { - const res = {} as Response; - res.status = vi.fn().mockReturnValue(res); - res.json = vi.fn().mockReturnValue(res); - res.send = vi.fn().mockReturnValue(res); - return res; -}; - -export const mockRequest = (query: any = {}, body: any = {}, params: any = {}): Request => ({ - query, - body, - params, - headers: {}, - method: 'GET', - url: '/usage', - path: '/usage' -} as Request); - -export const expectValidUsageSummary = (summary: any) => { - expect(summary).toHaveProperty('totalRequests'); - expect(summary).toHaveProperty('totalCost'); - expect(summary).toHaveProperty('totalTokens'); - expect(summary).toHaveProperty('costByProvider'); - expect(summary).toHaveProperty('costByModel'); - expect(summary).toHaveProperty('records'); - - expect(typeof summary.totalRequests).toBe('number'); - expect(typeof summary.totalCost).toBe('number'); - expect(typeof summary.totalTokens).toBe('number'); - expect(Array.isArray(summary.records)).toBe(true); - expect(typeof summary.costByProvider).toBe('object'); - expect(typeof summary.costByModel).toBe('object'); -}; - -export const expectValidUsageRecord = (record: any) => { - expect(record).toHaveProperty('id'); - expect(record).toHaveProperty('request_id'); - expect(record).toHaveProperty('provider'); - expect(record).toHaveProperty('model'); - expect(record).toHaveProperty('timestamp'); - expect(record).toHaveProperty('input_tokens'); - expect(record).toHaveProperty('output_tokens'); - expect(record).toHaveProperty('total_tokens'); - expect(record).toHaveProperty('total_cost'); - expect(record).toHaveProperty('currency'); - - expect(typeof record.id).toBe('number'); - expect(typeof record.request_id).toBe('string'); - expect(typeof record.provider).toBe('string'); - expect(typeof record.model).toBe('string'); - expect(typeof record.timestamp).toBe('string'); - expect(typeof record.input_tokens).toBe('number'); - expect(typeof record.output_tokens).toBe('number'); - expect(typeof record.total_tokens).toBe('number'); - expect(typeof record.total_cost).toBe('number'); - expect(typeof record.currency).toBe('string'); -}; - -export const createBulkUsageRecords = (count: number, baseDate: Date = new Date()): Array> => { - const records = []; - const providers = ['openai', 'anthropic', 'xai']; - const models = ['gpt-4o', 'claude-3-5-sonnet', 'grok-4']; - - for (let i = 0; i < count; i++) { - const date = new Date(baseDate); - // Add time intervals instead of subtracting (go forward in time) - date.setHours(date.getHours() + i); - - records.push(createMockUsageRecord({ - request_id: `bulk-record-${i}-${Date.now()}`, // Ensure unique IDs - timestamp: date.toISOString(), - provider: providers[i % providers.length], - model: models[i % models.length], - input_tokens: 100 + (i * 10), // Predictable values for testing - output_tokens: 50 + (i * 5), - total_tokens: 150 + (i * 15), - input_cost: 0.001 + (i * 0.0001), - output_cost: 0.002 + (i * 0.0001), - total_cost: 0.003 + (i * 0.0002) - })); - } - - return records; -}; diff --git a/gateway/tsconfig.json b/gateway/tsconfig.json deleted file mode 100644 index 963c52b..0000000 --- a/gateway/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "node", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": false, - "skipLibCheck": true, - "noEmitOnError": false, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "outDir": "./dist", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "baseUrl": ".", - "paths": { - "shared/*": ["../shared/*"] - }, - "types": ["vitest/globals"] - }, - "include": ["src/**/*", "../shared/**/*", "tests/**/*", "vitest.d.ts"], - "exclude": ["node_modules", "dist"] -} \ No newline at end of file diff --git a/gateway/vitest.config.ts b/gateway/vitest.config.ts deleted file mode 100644 index 931d995..0000000 --- a/gateway/vitest.config.ts +++ /dev/null @@ -1,76 +0,0 @@ -// gateway/vitest.config.ts -import { defineConfig } from 'vitest/config' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -// __dirname isn't available in ESM; derive it: -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -export default defineConfig({ - test: { - environment: 'node', - globals: true, - - // Global setup (you'll add this file in the next step) - setupFiles: ['./tests/setup.ts'], - - // Test file patterns - include: [ - 'tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', - 'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', - ], - - // Coverage configuration - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - reportsDirectory: './coverage', - include: ['src/**/*.ts'], - exclude: [ - 'src/index.ts', - 'src/**/types/**', - 'src/**/*.d.ts', - 'tests/**', - 'coverage/**', - 'dist/**', - ], - thresholds: { - global: { - branches: 90, - functions: 90, - lines: 90, - statements: 90, - }, - }, - }, - - // Timeout & concurrency - testTimeout: 10_000, - pool: 'threads', - poolOptions: { - threads: { singleThread: false }, - }, - - // Valid test reporters - reporter: ['verbose', 'json'], - - // Watch exclusions - watchExclude: [ - '**/node_modules/**', - '**/dist/**', - '**/coverage/**', - '**/.git/**', - '**/logs/**', - '**/*.db', - '**/*.db-*', - ], - }, - - // Path resolution (match your tsconfig paths) - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - '@tests': path.resolve(__dirname, './tests'), - }, - }, -}) diff --git a/model_catalog/chat_completions_providers_v1.json b/model_catalog/chat_completions_providers_v1.json deleted file mode 100644 index b31f2fb..0000000 --- a/model_catalog/chat_completions_providers_v1.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "providers": [ - { - "provider": "openai", - "models": [ - "gpt-5.2", - "gpt-5.2-pro", - "gpt-5.1", - "gpt-5.1-codex", - "gpt-5", - "gpt-5-mini", - "gpt-5-nano", - "gpt-5-chat-latest", - "gpt-4.1", - "gpt-4.1-mini", - "gpt-4.1-nano", - "gpt-4o", - "gpt-4o-2024-05-13", - "gpt-4o-mini", - "gpt-4o-mini-4k", - "gpt-4o-mini-8k", - "gpt-realtime", - "gpt-4o-realtime-preview", - "gpt-4o-mini-realtime-preview", - "o1", - "o1-pro", - "o3-pro", - "o3", - "o3-deep-research", - "o4-mini", - "o4-mini-deep-research", - "o3-mini", - "o1-mini", - "gpt-4o-mini-search-preview", - "gpt-4o-search-preview", - "computer-use-preview", - "gpt-image-1", - "gpt-3.5-turbo", - "chatgpt-4o-latest", - "gpt-4-turbo-2024-04-09", - "gpt-4-0125-preview", - "gpt-4-1106-preview", - "gpt-4-1106-vision-preview", - "gpt-4-0613", - "gpt-4-0314", - "gpt-4-32k", - "gpt-3.5-turbo-0125", - "gpt-3.5-turbo-1106", - "gpt-3.5-turbo-0613", - "gpt-3.5-0301", - "gpt-3.5-turbo-instruct", - "gpt-3.5-turbo-16k-0613" - ], - "chat_completions": { - "base_url": "https://api.openai.com/v1/chat/completions", - "auth": { - "env_var": "OPENAI_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "supported_client_formats": ["openai"], - "payload_defaults": { - "stream_options": { - "include_usage": true - } - }, - "usage": { - "format": "openai_chat" - } - } - }, - { - "provider": "google", - "models": [ - "gemini-2.5-pro", - "gemini-2.5-flash", - "gemini-2.5-flash-lite", - "gemini-3.0-pro-preview" - ], - "chat_completions": { - "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", - "auth": { - "env_var": "GOOGLE_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "supported_client_formats": ["openai"], - "payload_defaults": { - "stream_options": { - "include_usage": true - } - }, - "usage": { - "format": "openai_chat" - } - } - }, - { - "provider": "xAI", - "models": [ - "grok-4", - "grok-3", - "grok-3-mini", - "grok-code-fast-1", - "grok-code-fast", - "grok-code-fast-1-0825" - ], - "chat_completions": { - "base_url": "https://api.x.ai/v1/chat/completions", - "auth": { - "env_var": "XAI_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "supported_client_formats": ["openai"], - "payload_defaults": { - "stream_options": { - "include_usage": true - } - }, - "usage": { - "format": "openai_chat" - } - } - }, - { - "provider": "openrouter", - "models": [ - "anthropic/claude-haiku-4.5", - "openai/gpt-4o", - "openai/gpt-5-pro", - "x-ai/grok-code-fast-1", - "google/gemini-2.0-flash-001", - "google/gemini-2.5-pro", - "deepseek/deepseek-chat-v3.1", - "z-ai/glm-4.5" - ], - "chat_completions": { - "base_url": "https://openrouter.ai/api/v1/chat/completions", - "auth": { - "env_var": "OPENROUTER_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "static_headers": { - "HTTP-Referer": "https://ekailabs.xyz", - "X-Title": "Ekai Gateway" - }, - "supported_client_formats": ["openai"], - "payload_defaults": { - "stream_options": { - "include_usage": true - } - }, - "usage": { - "format": "openai_chat" - } - } - }, - { - "provider": "ollama", - "models": [ - "llama3.3", - "llama3.2", - "llama3.1", - "llama3", - "gemma3", - "gemma2", - "qwen3", - "qwen2.5-coder", - "deepseek-r1", - "deepseek-coder-v2", - "phi4", - "phi3", - "mistral", - "mixtral", - "codellama", - "starcoder2" - ], - "chat_completions": { - "base_url": "http://localhost:11434/v1/chat/completions", - "auth": { - "env_var": "OLLAMA_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "supported_client_formats": ["openai"], - "payload_defaults": { - "stream_options": { - "include_usage": true - } - }, - "usage": { - "format": "openai_chat" - } - } - } - ] -} diff --git a/model_catalog/messages_providers_v1.json b/model_catalog/messages_providers_v1.json deleted file mode 100644 index 9235f59..0000000 --- a/model_catalog/messages_providers_v1.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "providers": [ - { - "provider": "anthropic", - "models": [ - "claude-opus-4-5", - "claude-sonnet-4-5", - "claude-haiku-4-5", - "claude-opus-4-1", - "claude-opus-4", - "claude-sonnet-4", - "claude-sonnet-3-7", - "claude-3-5-haiku", - "claude-3-5-sonnet", - "claude-3-opus", - "claude-3-haiku" - ], - "messages": { - "base_url": "https://api.anthropic.com/v1/messages", - "auth": { - "env_var": "ANTHROPIC_API_KEY", - "header": "x-api-key" - }, - "static_headers": { - "anthropic-version": "2023-06-01" - }, - "supported_client_formats": [ - "anthropic" - ], - "model_options": { - "ensure_anthropic_suffix": true - }, - "usage": { - "format": "anthropic_messages" - }, - "force_stream_option": true - } - }, - { - "provider": "xAI", - "models": [ - "grok-4", - "grok-3", - "grok-3-mini", - "grok-code-fast-1", - "grok-code-fast", - "grok-code-fast-1-0825" - ], - "messages": { - "base_url": "https://api.x.ai/v1/messages", - "auth": { - "env_var": "XAI_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "supported_client_formats": [ - "anthropic" - ], - "usage": { - "format": "anthropic_messages" - }, - "force_stream_option": false - } - }, - { - "provider": "zai", - "models": [ - "glm-4.6", - "glm-4.5", - "glm-4.5-air", - "glm-4.5-x", - "glm-4.5-airx", - "glm-4.5-flash", - "glm-4-32b-0414-128k" - ], - "messages": { - "base_url": "https://api.z.ai/api/anthropic/v1/messages", - "auth": { - "env_var": "ZAI_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "supported_client_formats": [ - "anthropic" - ], - "usage": { - "format": "anthropic_messages" - }, - "force_stream_option": false - } - } - ] -} diff --git a/model_catalog/responses_providers_v1.json b/model_catalog/responses_providers_v1.json deleted file mode 100644 index 472f6b8..0000000 --- a/model_catalog/responses_providers_v1.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "providers": [ - { - "provider": "openai", - "models": [ - "gpt-5-codex", - "codex-mini-latest" - ], - "responses": { - "base_url": "https://api.openai.com/v1/responses", - "auth": { - "env_var": "OPENAI_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "supported_client_formats": ["openai_responses"] - } - }, - { - "provider": "ollama", - "models": [ - "llama3.3", - "llama3.2", - "llama3.1", - "llama3", - "gemma3", - "gemma2", - "qwen3", - "qwen2.5-coder", - "deepseek-r1", - "deepseek-coder-v2", - "phi4", - "phi3", - "mistral", - "mixtral", - "codellama", - "starcoder2" - ], - "responses": { - "base_url": "http://localhost:11434/v1/responses", - "auth": { - "env_var": "OLLAMA_API_KEY", - "header": "Authorization", - "scheme": "Bearer" - }, - "supported_client_formats": ["openai_responses"] - } - } - ] -} diff --git a/package.json b/package.json index 16086ca..69c1ccb 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,17 @@ { "name": "ekai-gateway", "version": "0.1.0-beta.1", - "description": "Ekai Gateway - Multi-provider AI request routing with dashboard", + "description": "Ekai Gateway - AI proxy with memory and dashboard", "private": true, - "workspaces": ["gateway", "ui/dashboard", "memory", "integrations/openrouter"], + "workspaces": ["ui/dashboard", "memory", "integrations/openrouter"], "scripts": { "dev": "node scripts/launcher.js --mode dev", "start": "node scripts/launcher.js --mode start", "build": "npm install --workspaces && npm run build --workspaces --if-present", "clean": "npm run clean --workspaces --if-present && rm -rf node_modules", - "test": "npm run test --workspace=gateway", - "dev:gateway": "npm run dev --workspace=gateway", + "test": "npm run test --workspace=@ekai/openrouter", "dev:ui": "npm run dev --workspace=ui/dashboard", "dev:openrouter": "npm run dev --workspace=@ekai/openrouter", - "start:gateway": "npm run start --workspace=gateway", "start:ui": "npx --workspace=ui/dashboard next start -p 3000 -H 0.0.0.0" }, "devDependencies": { @@ -25,7 +23,6 @@ "gateway", "dashboard", "openai", - "anthropic", "openrouter" ], "author": "Ekai Labs" diff --git a/scripts/launcher.js b/scripts/launcher.js index 3488751..d4fd1e4 100644 --- a/scripts/launcher.js +++ b/scripts/launcher.js @@ -18,18 +18,10 @@ try { } catch {} // Resolve ports from env (each service owns its own port var) -const gatewayPort = process.env.PORT || "3001"; const dashboardPort = process.env.DASHBOARD_PORT || "3000"; const openrouterPort = process.env.OPENROUTER_PORT || "4010"; const SERVICES = { - gateway: { - dev: `PORT=${gatewayPort} npm run dev -w gateway`, - start: `PORT=${gatewayPort} npm run start -w gateway`, - label: "gateway", - color: "blue", - port: gatewayPort, - }, dashboard: { dev: `npx -w ui/dashboard next dev -p ${dashboardPort}`, start: `npx -w ui/dashboard next start -p ${dashboardPort} -H 0.0.0.0`, diff --git a/scripts/start-docker-fullstack.sh b/scripts/start-docker-fullstack.sh index 00ee4ec..81cd995 100755 --- a/scripts/start-docker-fullstack.sh +++ b/scripts/start-docker-fullstack.sh @@ -1,12 +1,10 @@ #!/bin/bash set -euo pipefail -PORT="${PORT:-3001}" UI_PORT="${UI_PORT:-3000}" OPENROUTER_PORT="${OPENROUTER_PORT:-4010}" # Service toggles (all enabled by default) -ENABLE_GATEWAY="${ENABLE_GATEWAY:-true}" ENABLE_DASHBOARD="${ENABLE_DASHBOARD:-true}" ENABLE_OPENROUTER="${ENABLE_OPENROUTER:-true}" @@ -21,7 +19,7 @@ trap cleanup INT TERM # Runtime API URL replacement for Next.js if [ "$ENABLE_DASHBOARD" != "false" ] && [ "$ENABLE_DASHBOARD" != "0" ]; then - API_URL="${NEXT_PUBLIC_API_BASE_URL:-http://localhost:${PORT}}" + API_URL="${NEXT_PUBLIC_API_BASE_URL:-http://localhost:${OPENROUTER_PORT}}" echo "Configuring API URL: $API_URL" cd /app/ui/dashboard if [ "$API_URL" != "__API_URL_PLACEHOLDER__" ]; then @@ -34,13 +32,6 @@ echo "" echo " ekai-gateway (docker)" echo "" -if [ "$ENABLE_GATEWAY" != "false" ] && [ "$ENABLE_GATEWAY" != "0" ]; then - echo " Starting gateway on :${PORT}" - cd /app/gateway - PORT="$PORT" node dist/gateway/src/index.js & - PIDS+=($!) -fi - if [ "$ENABLE_DASHBOARD" != "false" ] && [ "$ENABLE_DASHBOARD" != "0" ]; then echo " Starting dashboard on :${UI_PORT}" cd /app/ui/dashboard From 832d2a45da22149d24baf9e6cf0dd1bcec666d59 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:44:22 +0530 Subject: [PATCH 42/78] Update README to reflect gateway removal --- README.md | 236 +++++++++++++++--------------------------------------- 1 file changed, 66 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 0c33eb7..3a24c1b 100644 --- a/README.md +++ b/README.md @@ -4,241 +4,137 @@ [![GitHub stars](https://img.shields.io/github/stars/ekailabs/ekai-gateway.svg?style=social)](https://github.com/ekailabs/ekai-gateway) [![Discord](https://img.shields.io/badge/Discord-Join%20Server-7289da?logo=discord&logoColor=white)](https://discord.com/invite/5VsUUEfbJk) -Multi-provider AI proxy with usage dashboard supporting Anthropic, OpenAI, Google Gemini, xAI, and OpenRouter models through OpenAI-compatible and Anthropic-compatible APIs. +OpenRouter proxy with embedded agent memory and a management dashboard. Drop it in front of any OpenAI-compatible client and your AI tools gain persistent memory across conversations. -**Designed for self-hosted personal use** - run your own instance to securely proxy AI requests using your API keys. +**Designed for self-hosted personal use** — run your own instance using your OpenRouter API key. ## Features -- 🤖 **Multi-provider**: Anthropic + OpenAI + Google (Gemini) + xAI + OpenRouter models -- 🔄 **Dual APIs**: OpenAI-compatible + Anthropic-compatible endpoints -- 🔀 **Cost-optimized routing**: Automatic selection of cheapest provider for each model -- 💰 **Usage tracking**: Track token usage and costs with visual dashboard -- 🗄️ **Database storage**: SQLite database for persistent usage tracking -- 📊 **Analytics dashboard**: Real-time cost analysis and usage breakdowns +- 🔀 **OpenRouter proxy**: Full OpenAI-compatible `/v1/chat/completions` endpoint +- 🧠 **Embedded memory**: Automatically stores and injects relevant context from past conversations +- 📊 **Memory dashboard**: Browse, search, and manage stored memories +- 🔑 **BYOK**: Bring your own OpenRouter API key — or pass a key per-request -## 🎥 Demo Video +## Quick Start -{% embed url="https://www.youtube.com/watch?v=sLg9YmYtg64" %} - -## Quick Start (Beta) - -**Option 1: Using npm** +**Option 1: npm** ```bash -# 1. Install dependencies npm install - -# 2. Setup environment variables cp .env.example .env -# Edit .env and add at least one API key (see .env.example for details) - -# 3. Build and start the application +# Add OPENROUTER_API_KEY to .env npm run build npm start ``` -**Option 2: Using Docker (published image)** +**Option 2: Docker (published image)** ```bash -# 1. Setup environment variables cp .env.example .env -# Edit .env and add at least one API key (see .env.example for details) - -# 2. Pull + start the latest GHCR image +# Add OPENROUTER_API_KEY to .env docker compose up -d - -# Optional: run without Compose -docker pull ghcr.io/ekailabs/ekai-gateway:latest -docker run --env-file .env -p 3001:3001 -p 3000:3000 ghcr.io/ekailabs/ekai-gateway:latest ``` -Important: The dashboard is initially empty. After setup, send a query using your own client/tool (IDE, app, or API) through the gateway; usage appears once at least one request is processed. +**Access points (default ports):** +- OpenRouter proxy + memory APIs: port `4010` (`OPENROUTER_PORT`) +- Memory dashboard: port `3000` (`UI_PORT`) -**Access Points (default ports, configurable in `.env`):** -- Gateway API: port `3001` (`PORT`) - OpenAI/Anthropic compatible endpoints -- Dashboard UI: port `3000` (`UI_PORT`) - Usage analytics and cost tracking -- OpenRouter Integration: port `4010` (`OPENROUTER_PORT`) - OpenRouter proxy with embedded memory APIs +### Build the image yourself (optional) -The dashboard auto-detects the host and connects to the gateway on the same host using its configured port. No extra URL configuration needed. - -**Docker Service Configuration:** -All services run in a single Docker container. Control which services start via `.env`: ```bash -ENABLE_GATEWAY=true # API server (default: true) -ENABLE_DASHBOARD=true # Web dashboard (default: true) -ENABLE_OPENROUTER=true # OpenRouter integration (default: true) +docker build --target ekai-cloudrun -t ekai-gateway . +docker run --env-file .env -p 4010:4010 ekai-gateway ``` -Detailed setup steps live in `docs/getting-started.md`; check `docs/` for additional guides. - -### Build the Image Yourself (optional) +## Usage -If you’re contributing changes or need a custom build: +Point any OpenAI-compatible client at `http://localhost:4010`: ```bash -docker build --target ekai-gateway-runtime -t ekai-gateway . -docker run --env-file .env -p 3001:3001 -p 3000:3000 ekai-gateway -``` - -## Populate the Dashboard +# Chat completions — memory is injected automatically +curl -X POST http://localhost:4010/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "anthropic/claude-sonnet-4-5", "messages": [{"role": "user", "content": "Hello"}]}' -- Point your client/tool to the gateway (`http://localhost:3001` or `http://localhost:3001/v1`), see integration guides below. -- Send a query using your usual workflow; both OpenAI-compatible and Anthropic-compatible endpoints are tracked. -- Open `http://localhost:3000` to view usage and costs after your first request. +# Pass your own OpenRouter key per-request +curl -X POST http://localhost:4010/v1/chat/completions \ + -H "Authorization: Bearer sk-or-..." \ + -H "Content-Type: application/json" \ + -d '{"model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}' -**Required:** At least one API key from Anthropic, OpenAI, Google Gemini, xAI, or OpenRouter (see `.env.example` for configuration details). +# Health check +curl http://localhost:4010/health +``` ## Integration Guides -### 🤖 **Claude Code Integration** -Use the gateway with Claude Code for multi-provider AI assistance: +### 🤖 Claude Code ```bash -# Point Claude Code to the gateway -export ANTHROPIC_BASE_URL="http://localhost:3001" -export ANTHROPIC_MODEL="grok-code-fast-1" # or "gpt-4o","claude-sonnet-4-20250514" - -# Start Claude Code as usual +export ANTHROPIC_BASE_URL="http://localhost:4010" +export ANTHROPIC_MODEL="anthropic/claude-sonnet-4-5" claude ``` 📖 **[Complete Claude Code Guide →](./docs/USAGE_WITH_CLAUDE_CODE.md)** -### 💻 **Codex Integration** -Use the gateway with Codex for OpenAI-compatible development tools: +### 💻 Codex ```bash -# Point Codex to the gateway -export OPENAI_BASE_URL="http://localhost:3001/v1" - -# Start Codex as usual +export OPENAI_BASE_URL="http://localhost:4010/v1" codex ``` 📖 **[Complete Codex Guide →](./docs/USAGE_WITH_CODEX.md)** -## Beta Testing Notes -🚧 **This is a beta release** - please report any issues or feedback! - -**Known Limitations:** -- Some edge cases in model routing may exist - -**Getting Help:** -- Check the logs in `gateway/logs/gateway.log` for debugging -- Ensure your API keys have sufficient credits -- Test with simple requests first before complex workflows - -## Project Structure - -``` -ekai-gateway/ -├── gateway/ # Backend API and routing -├── ui/dashboard/ # Dashboard frontend (Next.js) -├── memory/ # Agent memory library (@ekai/memory) -├── integrations/ -│ └── openrouter/ # OpenRouter integration service -├── scripts/ -│ └── launcher.js # Unified service launcher -└── package.json # Root workspace configuration -``` +## Running Services -## API Endpoints +### npm (local development) ```bash -POST /v1/chat/completions # OpenAI-compatible chat endpoint -POST /v1/messages # Anthropic-compatible messages endpoint -POST /v1/responses # OpenAI Responses endpoint -GET /usage # View token usage and costs -GET /health # Health check endpoint +npm run dev # dashboard + openrouter with hot-reload +npm start # production mode ``` +Disable individual services via env: ```bash -# OpenAI-compatible endpoint (works with all providers) -curl -X POST http://localhost:3001/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}' - -# Use Claude models via OpenAI-compatible endpoint -curl -X POST http://localhost:3001/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{"model": "claude-3-5-sonnet-20241022", "messages": [{"role": "user", "content": "Hello"}]}' - -# Use xAI Grok models -curl -X POST http://localhost:3001/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{"model": "grok-code-fast", "messages": [{"role": "user", "content": "Hello"}]}' - -# Anthropic-compatible endpoint -curl -X POST http://localhost:3001/v1/messages \ - -H "Content-Type: application/json" \ - -d '{"model": "claude-3-5-sonnet-20241022", "max_tokens": 100, "messages": [{"role": "user", "content": "Hello"}]}' - -# OpenAI Responses endpoint -curl -X POST http://localhost:3001/v1/responses \ - -H "Content-Type: application/json" \ - -d '{"model": "gpt-4o-mini", "input": "Say hi in one short sentence.", "temperature": 0.7, "max_output_tokens": 128}' - -# Both endpoints support all models and share conversation context -# Client A uses OpenAI format, Client B uses Anthropic format - same conversation! - -# Check usage and costs -curl http://localhost:3001/usage +ENABLE_DASHBOARD=false npm run dev # openrouter only +ENABLE_OPENROUTER=false npm run dev # dashboard only ``` -## Model Routing (Cost-Optimized) - -The proxy uses **cost-based optimization** to automatically select the cheapest available provider: - -1. **Special routing**: Grok models (`grok-code-fast`, `grok-beta`) → xAI (if available) -2. **Cost optimization**: All other models are routed to the cheapest provider that supports them -3. **Provider fallback**: Graceful fallback if preferred provider is unavailable - -**Supported providers**: -- **Anthropic**: Claude models (direct API access) -- **OpenAI**: GPT models (direct API access) -- **xAI**: Grok models (direct API access) -- **OpenRouter**: Multi-provider access with `provider/model` format - -**Multi-client proxy**: Web apps, mobile apps, and scripts share conversations across providers with automatic cost tracking and optimization. - -## Running Services - -### With npm (local development) - -A unified launcher starts all 3 services by default (gateway, dashboard, openrouter): +### Docker ```bash -npm run dev # Development mode — all services with hot-reload -npm start # Production mode — all services from built output +docker compose up -d # start all services +docker compose logs -f # view logs +docker compose down # stop ``` -**Disable individual services** by setting `ENABLE_=false`: - +**Docker service toggles (`.env`):** ```bash -ENABLE_DASHBOARD=false npm run dev # Skip the dashboard -ENABLE_OPENROUTER=false npm start # Production without openrouter +ENABLE_DASHBOARD=true # memory dashboard (default: true) +ENABLE_OPENROUTER=true # proxy + memory APIs (default: true) ``` -**Individual service scripts** (escape hatches): +## Project Structure -```bash -npm run dev:gateway # Gateway only (port 3001) -npm run dev:ui # Dashboard only (port 3000) -npm run dev:openrouter # OpenRouter integration only (port 4010) -npm run start:gateway # Production gateway -npm run start:ui # Production dashboard +``` +ekai-gateway/ +├── integrations/ +│ └── openrouter/ # Proxy server with embedded memory (@ekai/openrouter) +├── memory/ # Agent memory library (@ekai/memory) +├── ui/dashboard/ # Memory management dashboard (Next.js) +├── scripts/ +│ └── launcher.js # Unified service launcher +└── package.json # Root workspace configuration ``` -### With Docker - -All services run together in a single container. The entrypoint script manages service lifecycle and ensures all enabled services stay running: +## Beta Testing Notes -```bash -docker compose up -d # Start all services -docker compose logs -f # View logs from all services -docker compose down # Stop all services -``` +🚧 **This is a beta release** — please report issues and feedback! -Services are controlled via `.env` file `ENABLE_*` variables. The Docker container will restart automatically on failure (see `docker-compose.yaml` for `restart: unless-stopped`). +**Getting help:** +- Join the [Discord](https://discord.com/invite/5VsUUEfbJk) +- Check logs with `docker compose logs -f` +- Ensure your OpenRouter API key has sufficient credits ## Contributing From b46502d11222bce38b6a0160362294a6c80f2461 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:12:33 +0530 Subject: [PATCH 43/78] Remove unused shared/types directory Dead code from the old canonical format architecture that was never wired up. No imports existed anywhere in the codebase. --- shared/types/canonical.ts | 61 --------------------------------- shared/types/index.ts | 3 -- shared/types/types.ts | 71 --------------------------------------- 3 files changed, 135 deletions(-) delete mode 100644 shared/types/canonical.ts delete mode 100644 shared/types/index.ts delete mode 100644 shared/types/types.ts diff --git a/shared/types/canonical.ts b/shared/types/canonical.ts deleted file mode 100644 index 6e2da7e..0000000 --- a/shared/types/canonical.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Canonical format - Universal internal representation for AI requests/responses -// This format serves as the common interface between different AI provider formats - -export interface CanonicalContent { - type: 'text'; - text: string; -} - -export interface CanonicalMessage { - role: 'system' | 'user' | 'assistant'; - content: CanonicalContent[]; -} - -export interface CanonicalUsage { - inputTokens: number; - cacheWriteInputTokens?: number; - cacheReadInputTokens?: number; - outputTokens: number; - totalTokens: number; -} - -export interface CanonicalRequest { - model: string; - messages: CanonicalMessage[]; - maxTokens?: number; - temperature?: number; - topP?: number; - stopSequences?: string[]; - stream?: boolean; - metadata?: Record; // Provider-specific fields -} - -export interface CanonicalResponse { - id: string; - model: string; - created: number; - message: { - role: 'assistant'; - content: CanonicalContent[]; - }; - finishReason: 'stop' | 'length' | 'tool_calls' | 'error'; - usage: CanonicalUsage; -} - -export interface CanonicalStreamChunk { - id: string; - model: string; - created: number; - delta: { - role?: 'assistant'; - content?: CanonicalContent[]; - }; - finishReason?: 'stop' | 'length' | 'tool_calls' | 'error'; - usage?: CanonicalUsage; -} - -export interface FormatAdapter { - toCanonical(input: ClientRequest): CanonicalRequest; - fromCanonical(response: CanonicalResponse): ClientResponse; - fromCanonicalStream(chunk: CanonicalStreamChunk): string; -} \ No newline at end of file diff --git a/shared/types/index.ts b/shared/types/index.ts deleted file mode 100644 index 8f68ca9..0000000 --- a/shared/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Re-export all types for easy importing -export * from './types.js'; -export * from './canonical.js'; diff --git a/shared/types/types.ts b/shared/types/types.ts deleted file mode 100644 index c98df8f..0000000 --- a/shared/types/types.ts +++ /dev/null @@ -1,71 +0,0 @@ -export interface ChatCompletionRequest { - messages: Array<{ - role: 'system' | 'user' | 'assistant'; - content: string; - }>; - model: string; - stream?: boolean; - temperature?: number; - max_tokens?: number; - [key: string]: any; -} - -export interface ChatCompletionResponse { - id: string; - object: string; - created: number; - model: string; - choices: Array<{ - index: number; - message: { - role: string; - content: string; - }; - finish_reason: string; - }>; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - - -export type ProviderName = 'openai' | 'openrouter' | 'anthropic' | 'ollama'; - -// Removed conversation types - no conversation storage - -export interface ChatMessage { - role: 'system' | 'user' | 'assistant'; - content: string; -} - -export interface AnthropicMessagesRequest { - model: string; - messages: Array<{ - role: 'user' | 'assistant'; - content: string | Array<{ type: string; text: string; }>; - }>; - max_tokens?: number; // Make optional since Claude Code might not send it - system?: string | Array<{ type: string; text: string; }>; // Can be string or array - temperature?: number; - stream?: boolean; - [key: string]: any; -} - -export interface AnthropicMessagesResponse { - id: string; - type: 'message'; - role: 'assistant'; - content: Array<{ - type: 'text'; - text: string; - }>; - model: string; - stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use'; - stop_sequence?: string; - usage: { - input_tokens: number; - output_tokens: number; - }; -} \ No newline at end of file From e7a6fd7903141b43a7abe69f2f19d874027faea6 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:36:17 +0530 Subject: [PATCH 44/78] Strip dead dashboard code, keep memory UI Remove all usage/analytics components, hooks, and API methods left over from the gateway service removal. The root page now redirects to /memory. Update README, .env.example, and metadata to reflect the memory-only dashboard. --- .env.example | 6 +- ui/dashboard/README.md | 126 +++---- ui/dashboard/src/app/layout.tsx | 4 +- ui/dashboard/src/app/page.tsx | 159 +-------- .../src/components/ActivityHeatmap.tsx | 311 ------------------ ui/dashboard/src/components/BudgetCard.tsx | 255 -------------- ui/dashboard/src/components/ConfigStatus.tsx | 167 ---------- .../src/components/DateRangePicker.tsx | 212 ------------ ui/dashboard/src/components/FirstRunModal.tsx | 147 --------- ui/dashboard/src/components/ModelChart.tsx | 112 ------- ui/dashboard/src/components/ProviderChart.tsx | 106 ------ ui/dashboard/src/components/TrendChart.tsx | 271 --------------- ui/dashboard/src/components/UsageTable.tsx | 234 ------------- ui/dashboard/src/components/ui/Card.tsx | 10 - .../src/components/ui/ChartTooltip.tsx | 102 ------ ui/dashboard/src/components/ui/EmptyState.tsx | 23 -- ui/dashboard/src/hooks/useBudget.ts | 46 --- ui/dashboard/src/hooks/useConfigStatus.ts | 27 -- ui/dashboard/src/hooks/useCopy.ts | 17 - ui/dashboard/src/hooks/useUsageData.ts | 51 --- ui/dashboard/src/lib/api.ts | 141 +------- ui/dashboard/src/lib/constants.ts | 15 +- ui/dashboard/src/lib/utils.ts | 147 --------- 23 files changed, 55 insertions(+), 2634 deletions(-) delete mode 100644 ui/dashboard/src/components/ActivityHeatmap.tsx delete mode 100644 ui/dashboard/src/components/BudgetCard.tsx delete mode 100644 ui/dashboard/src/components/ConfigStatus.tsx delete mode 100644 ui/dashboard/src/components/DateRangePicker.tsx delete mode 100644 ui/dashboard/src/components/FirstRunModal.tsx delete mode 100644 ui/dashboard/src/components/ModelChart.tsx delete mode 100644 ui/dashboard/src/components/ProviderChart.tsx delete mode 100644 ui/dashboard/src/components/TrendChart.tsx delete mode 100644 ui/dashboard/src/components/UsageTable.tsx delete mode 100644 ui/dashboard/src/components/ui/Card.tsx delete mode 100644 ui/dashboard/src/components/ui/ChartTooltip.tsx delete mode 100644 ui/dashboard/src/components/ui/EmptyState.tsx delete mode 100644 ui/dashboard/src/hooks/useBudget.ts delete mode 100644 ui/dashboard/src/hooks/useConfigStatus.ts delete mode 100644 ui/dashboard/src/hooks/useCopy.ts delete mode 100644 ui/dashboard/src/hooks/useUsageData.ts delete mode 100644 ui/dashboard/src/lib/utils.ts diff --git a/.env.example b/.env.example index ca59f61..863f578 100644 --- a/.env.example +++ b/.env.example @@ -11,9 +11,8 @@ GOOGLE_API_KEY=your_key_here # DATABASE_PATH=data/proxy.db # SQLite file path (default: data/proxy.db relative to gateway cwd) # Service ports -PORT=3001 # Gateway -# DASHBOARD_PORT=3000 # Dashboard -# OPENROUTER_PORT=4010 # OpenRouter integration (memory API served here too) +# OPENROUTER_PORT=4010 # OpenRouter integration (memory API served here) +# DASHBOARD_PORT=3000 # Dashboard UI # Memory is embedded in the OpenRouter process (no separate service). # MEMORY_DB_PATH=./memory.db # SQLite path for memory store (used by OpenRouter) @@ -27,6 +26,5 @@ PRIVATE_KEY= # OPENROUTER_PRICING_RETRIES=2 # Service toggles (all enabled by default, set to "false" to disable) -# ENABLE_GATEWAY=true # ENABLE_DASHBOARD=true # ENABLE_OPENROUTER=true diff --git a/ui/dashboard/README.md b/ui/dashboard/README.md index 02911d4..fd0bd0e 100644 --- a/ui/dashboard/README.md +++ b/ui/dashboard/README.md @@ -1,45 +1,19 @@ -# Ekai Gateway Dashboard +# Ekai Memory Vault Dashboard -A comprehensive spend dashboard for tracking AI model usage and pricing across multiple providers. +A dashboard for managing agent memory and exploring the knowledge graph. ## Features -- 📊 **Real-time Analytics**: Monitor spending and usage patterns with live data -- 📈 **Trend Visualization**: Track spend and token usage over time with interactive charts -- 🥧 **Provider Breakdown**: See cost distribution across different AI providers (OpenAI, Anthropic, etc.) -- 🎯 **Model Comparison**: Compare costs and usage across different AI models -- 📋 **Usage Table**: Detailed tabular view of all API requests with sorting and filtering -- 💰 **Cost Optimization**: Identify the most cost-effective models for your use cases -- 📅 **Date Range Filtering**: Filter analytics by custom date ranges or quick presets (Today, Yesterday, Last 7/30 Days, etc.) - -## Dashboard Components - -### TrendChart -- **Spend Over Time**: Bar/line charts showing daily or hourly cost trends -- **Tokens Over Time**: Bar/line charts showing token usage patterns -- **Burn Rate**: Daily average spending calculation - -### ProviderChart -- **Pie Chart**: Visual breakdown of costs by provider -- **Cost Distribution**: See which providers you're spending the most on -- **Percentage Analysis**: Understand your provider usage patterns - -### ModelChart -- **Pie Chart**: Visual breakdown of costs by AI model -- **Model Comparison**: Compare costs across GPT-4, Claude, and other models -- **Usage Insights**: Identify your most-used models - -### UsageTable -- **Detailed Records**: Sortable table showing all API requests -- **Request Details**: Timestamp, provider, model, tokens, and costs -- **Interactive Sorting**: Click column headers to sort data -- **Summary Statistics**: Total requests, tokens, and costs +- **Memory Management**: View, edit, and delete memories across episodic, semantic, procedural, and reflective sectors +- **Knowledge Graph**: Interactive visualization of entity relationships and triples +- **Profile Support**: Switch between memory profiles +- **Semantic Graph Explorer**: Traverse paths, neighbors, and connections between entities ## Getting Started ### Prerequisites -- Node.js 18+ -- Backend gateway running on port 3001 +- Node.js 18+ +- Memory service running (embedded in OpenRouter on port 4010) ### Installation @@ -58,73 +32,65 @@ A comprehensive spend dashboard for tracking AI model usage and pricing across m ### Optional: Environment Configuration -The dashboard automatically detects the host from the browser URL and connects to the gateway (port 3001) and memory service (port 4005) on the same host. No configuration is needed for standard deployments. +The dashboard automatically detects the host from the browser URL and connects to the memory API (port 4010) on the same host. No configuration is needed for standard deployments. -To override ports, set these in `.env`: +To override, set these in `.env`: ```bash -PORT=3001 # Gateway port -MEMORY_PORT=4005 # Memory service port +NEXT_PUBLIC_MEMORY_PORT=4010 # Memory API port +NEXT_PUBLIC_EMBEDDED_MODE=true # When UI is served from the same Express server ``` ## API Integration -The dashboard connects to the Ekai Gateway backend through these endpoints: - -- `GET /usage` - Fetch usage statistics and cost data - - Optional query parameters: - - `startTime` - ISO 8601 datetime string for filtering from this date/time - - `endTime` - ISO 8601 datetime string for filtering to this date/time - - `timezone` - Timezone identifier (defaults to UTC) - - Example: `/usage?startTime=2025-01-01T00:00:00.000Z&endTime=2025-01-31T23:59:59.999Z` -- `GET /health` - Check backend health status - -## Architecture - -### Component Structure -- **Shared Hook**: `useUsageData` - Centralized API data fetching -- **UI Components**: Reusable loading, error, and empty state components -- **Chart Components**: Interactive visualizations with shared tooltips -- **Table Component**: Sortable data table with detailed request information - -### Code Organization +The dashboard connects to the memory API through these endpoints: + +- `GET /v1/summary` — Fetch memory sector summaries and recent items +- `PUT /v1/memory/:id` — Update a memory +- `DELETE /v1/memory/:id` — Delete a memory +- `DELETE /v1/memory` — Delete all memories +- `GET /v1/graph/visualization` — Graph visualization data +- `GET /v1/graph/triples` — Query triples for an entity +- `GET /v1/graph/neighbors` — Get entity neighbors +- `GET /v1/graph/paths` — Find paths between entities +- `GET /v1/profiles` — List memory profiles +- `DELETE /v1/profiles/:name` — Delete a profile +- `DELETE /v1/graph/triple/:id` — Delete a graph triple + +## Code Organization ``` src/ +├── app/ +│ ├── layout.tsx +│ ├── page.tsx # Redirects to /memory +│ ├── globals.css +│ └── memory/ +│ └── page.tsx # Memory vault page ├── components/ -│ ├── TrendChart.tsx # Time-based analytics -│ ├── ProviderChart.tsx # Provider cost breakdown -│ ├── ModelChart.tsx # Model cost comparison -│ ├── UsageTable.tsx # Detailed request table -│ ├── DateRangePicker.tsx # Date range filtering component -│ └── ui/ # Shared UI components +│ ├── memory/ # Memory-specific components +│ └── ui/ │ ├── LoadingSkeleton.tsx -│ ├── ErrorState.tsx -│ ├── EmptyState.tsx -│ └── ChartTooltip.tsx -├── hooks/ -│ └── useUsageData.ts # Shared data fetching logic with date filtering +│ └── ErrorState.tsx └── lib/ - ├── api.ts # API service functions with date parameters - ├── constants.ts # Shared constants - └── utils.ts # Utility functions + ├── api.ts # API service functions + └── constants.ts # Port and URL config ``` ## Technology Stack -- **Next.js** - React framework with App Router -- **TypeScript** - Type-safe development -- **Tailwind CSS** - Utility-first CSS framework -- **Recharts** - Beautiful, composable charts for data visualization -- **Custom Hooks** - Shared logic and state management +- **Next.js** — React framework with App Router +- **TypeScript** — Type-safe development +- **Tailwind CSS** — Utility-first CSS framework ## Development ### Available Scripts -- `npm run dev` - Start development server -- `npm run build` - Build for production -- `npm run start` - Start production server -- `npm run lint` - Run ESLint +- `npm run dev` — Start development server +- `npm run build` — Build for production +- `npm run start` — Start production server +- `npm run lint` — Run ESLint +- `npm run type-check` — Run TypeScript type checking ## License diff --git a/ui/dashboard/src/app/layout.tsx b/ui/dashboard/src/app/layout.tsx index 8db14a4..3bc8414 100644 --- a/ui/dashboard/src/app/layout.tsx +++ b/ui/dashboard/src/app/layout.tsx @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Ekai Gateway Dashboard", - description: "AI Spend Analytics & Usage Tracking Dashboard", + title: "Ekai Memory Vault", + description: "Memory management and knowledge graph explorer", icons: { icon: '/favicon.ico', }, diff --git a/ui/dashboard/src/app/page.tsx b/ui/dashboard/src/app/page.tsx index 23f4318..5fb716d 100644 --- a/ui/dashboard/src/app/page.tsx +++ b/ui/dashboard/src/app/page.tsx @@ -1,158 +1,5 @@ -'use client'; +import { redirect } from 'next/navigation'; -import { useState, useEffect } from 'react'; -import TrendChart from '@/components/TrendChart'; -import ProviderChart from '@/components/ProviderChart'; -import ModelChart from '@/components/ModelChart'; -import UsageTable from '@/components/UsageTable'; -import ActivityHeatmap from '@/components/ActivityHeatmap'; -import DateRangePicker, { DateRange } from '@/components/DateRangePicker'; -import { useUsageData } from '@/hooks/useUsageData'; -import { useConfigStatus } from '@/hooks/useConfigStatus'; -import ConfigStatus from '@/components/ConfigStatus'; -import FirstRunModal from '@/components/FirstRunModal'; -import BudgetCard from '@/components/BudgetCard'; -import { useBudget } from '@/hooks/useBudget'; -import { apiService } from '@/lib/api'; -import { MEMORY_PORT } from '@/lib/constants'; - -export default function Dashboard() { - // Embedded mode: redirect root to Memory Vault - useEffect(() => { - if ( - process.env.NEXT_PUBLIC_EMBEDDED_MODE === 'true' || - window.location.port === MEMORY_PORT - ) { - window.location.replace('/memory'); - } - }, []); - - const [dateRange, setDateRange] = useState(null); - const [mounted, setMounted] = useState(false); - const usageData = useUsageData(dateRange?.from, dateRange?.to); - const configStatus = useConfigStatus(); - const budget = useBudget(); - const [exporting, setExporting] = useState(false); - const [exportError, setExportError] = useState(null); - const [showOnboarding, setShowOnboarding] = useState(true); - - useEffect(() => { - // Set default to last 7 days after hydration - const now = new Date(); - const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); - const start = new Date(now); - start.setDate(start.getDate() - 6); - start.setHours(0, 0, 0, 0); - setDateRange({ from: start, to: end }); - setMounted(true); - }, []); - - const handleExportCsv = async () => { - try { - setExportError(null); - setExporting(true); - await apiService.downloadUsageCsv(dateRange?.from, dateRange?.to); - } catch (err) { - setExportError(err instanceof Error ? err.message : 'Failed to export CSV'); - } finally { - setExporting(false); - } - }; - - const showFirstRunGuide = showOnboarding && !usageData.loading && !usageData.error && usageData.totalRequests === 0; - - return ( -
- {/* Header */} -
-
-
-
-

Ekai Gateway Dashboard

-

AI Spend Analytics & Usage Tracking

-
-
-
Last updated
-
- {mounted ? new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC' : 'Loading...'} -
-
-
- - {/* Date Range Filter */} -
-
-

Filter by Date Range

-

Select a time period to view usage analytics

-
-
- - -
-
- {exportError && ( -

CSV export failed: {exportError}

- )} -
-
- - {/* Main Content */} -
- - - {/* Budget Control */} -
- -
- -
- {/* Activity Overview */} - - - {/* Trend Chart */} - - - {/* Provider and Model Charts */} -
- - -
- - {/* Usage Table */} - -
-
- - setShowOnboarding(false)} - onRefresh={usageData.refetch} - /> -
- ); +export default function Home() { + redirect('/memory'); } diff --git a/ui/dashboard/src/components/ActivityHeatmap.tsx b/ui/dashboard/src/components/ActivityHeatmap.tsx deleted file mode 100644 index 7594605..0000000 --- a/ui/dashboard/src/components/ActivityHeatmap.tsx +++ /dev/null @@ -1,311 +0,0 @@ -'use client'; - -import { useMemo, useState } from 'react'; -import { UsageRecord } from '@/lib/api'; -import { groupByDate } from '@/lib/utils'; -import { formatCurrency, formatNumber } from '@/lib/utils'; - -interface ActivityHeatmapProps { - records: UsageRecord[]; - fromDate?: Date; - toDate?: Date; - className?: string; - showFullYear?: boolean; // If true, show last 52 weeks regardless of date range -} - -// Color levels for activity intensity (GitHub-style) -const getActivityLevel = (requests: number, maxRequests: number): number => { - if (requests === 0) return 0; - if (requests === 1) return 1; - if (requests <= maxRequests * 0.25) return 1; - if (requests <= maxRequests * 0.5) return 2; - if (requests <= maxRequests * 0.75) return 3; - return 4; -}; - -const getActivityColor = (level: number): string => { - // Using the same teal color scheme as the Edit budget button (#004f4f) - const colors = [ - 'bg-stone-100 hover:bg-stone-200', // Level 0: No activity - 'bg-[#e0f2f2] hover:bg-[#c0e5e5]', // Level 1: Light activity (very light teal) - 'bg-[#80cccc] hover:bg-[#66b8b8]', // Level 2: Medium activity (medium teal) - 'bg-[#006666] hover:bg-[#005555]', // Level 3: High activity (teal-light from globals) - 'bg-[#004f4f] hover:bg-[#003333]', // Level 4: Very high activity (main teal - matches Edit budget button) - ]; - return colors[level] || colors[0]; -}; - -interface DayData { - date: Date; - dateKey: string; - requests: number; - cost: number; - tokens: number; - level: number; -} - -export default function ActivityHeatmap({ records, fromDate, toDate, className = '', showFullYear = true }: ActivityHeatmapProps) { - const [hoveredDay, setHoveredDay] = useState(null); - const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); - - const { days } = useMemo(() => { - // If showFullYear is true, always show last 52 weeks (GitHub-style) - let startDate: Date; - let endDate: Date; - - if (showFullYear) { - endDate = new Date(); - endDate.setHours(23, 59, 59, 999); - startDate = new Date(endDate); - startDate.setDate(startDate.getDate() - (52 * 7 - 1)); // 52 weeks back - startDate.setHours(0, 0, 0, 0); - } else { - if (!fromDate || !toDate) return { days: [] }; - startDate = new Date(fromDate); - endDate = new Date(toDate); - } - - // Group records by day - const grouped = groupByDate(records, 'day'); - - // Create array of all days in the date range - const days: DayData[] = []; - const current = new Date(startDate); - const end = new Date(endDate); - - let maxRequests = 0; - - while (current <= end) { - const dateKey = current.toISOString().slice(0, 10); - const dayData = grouped[dateKey] || { - cost: 0, - tokens: 0, - requests: 0, - inputTokens: 0, - cacheWriteTokens: 0, - cacheReadTokens: 0, - outputTokens: 0 - }; - - const level = getActivityLevel(dayData.requests, Math.max(maxRequests, dayData.requests)); - - days.push({ - date: new Date(current), - dateKey, - requests: dayData.requests, - cost: dayData.cost, - tokens: dayData.tokens, - level - }); - - maxRequests = Math.max(maxRequests, dayData.requests); - current.setDate(current.getDate() + 1); - } - - // Recalculate levels now that we know maxRequests - days.forEach(day => { - day.level = getActivityLevel(day.requests, maxRequests); - }); - - return { days }; - }, [records, fromDate, toDate, showFullYear]); - - // Group days by week (starting Monday) - const weeks = useMemo(() => { - const weeks: DayData[][] = []; - let currentWeek: DayData[] = []; - - days.forEach(day => { - // Monday = 1, Sunday = 0, so we adjust - const dayOfWeek = day.date.getDay(); - const adjustedDay = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // 0=Monday, 6=Sunday - - if (adjustedDay === 0 && currentWeek.length > 0) { - // Start new week on Monday - weeks.push(currentWeek); - currentWeek = []; - } - - currentWeek.push(day); - }); - - if (currentWeek.length > 0) { - weeks.push(currentWeek); - } - - return weeks; - }, [days]); - - const handleMouseEnter = (day: DayData, event: React.MouseEvent) => { - setHoveredDay(day); - setTooltipPosition({ x: event.clientX, y: event.clientY }); - }; - - const handleMouseLeave = () => { - setHoveredDay(null); - }; - - if (days.length === 0) { - return ( -
-

Activity Overview

-

Daily API request activity visualization

-
-
- - - -
-

No activity data for the selected period

-
-
- ); - } - - const totalRequests = days.reduce((sum, d) => sum + d.requests, 0); - const activeDays = days.filter(d => d.requests > 0).length; - const currentYear = new Date().getFullYear(); - - return ( -
- {/* Header */} -
-
-

Activity Overview

-

- {totalRequests > 0 ? ( - <> - {totalRequests} request{totalRequests !== 1 ? 's' : ''} in {currentYear} - {' • '} - {activeDays} active day{activeDays !== 1 ? 's' : ''} - - ) : ( - 'Daily API request activity visualization' - )} -

-
-
- - {/* Calendar Container */} -
-
-
- {/* Day labels - GitHub style: only Mon, Wed, Fri */} -
-
{/* Spacer for month labels */} - {['Mon', '', 'Wed', '', 'Fri', '', 'Sun'].map((day, index) => ( -
- {day} -
- ))} -
- - {/* Calendar grid */} -
- {weeks.map((week, weekIndex) => { - const showMonth = week[0] && ( - weekIndex === 0 || - weeks[weekIndex - 1][0]?.date.getMonth() !== week[0].date.getMonth() - ); - - return ( -
- {/* Month label (show on first day of month) */} - {showMonth ? ( -
- {week[0].date.toLocaleDateString('en-US', { month: 'short' })} -
- ) : ( -
- )} - - {/* Days */} - {week.map(day => ( -
-
-
- - {/* Legend at the bottom - GitHub style */} -
- Less - {[0, 1, 2, 3, 4].map(level => ( -
- ))} - More -
-
- - {/* Enhanced Tooltip */} - {hoveredDay && ( -
-
- {hoveredDay.date.toLocaleDateString('en-US', { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric' - })} -
-
-
- Requests: - {hoveredDay.requests} -
-
- Tokens: - {formatNumber(hoveredDay.tokens)} -
-
- Cost: - {formatCurrency(hoveredDay.cost)} -
-
- {/* Tooltip arrow */} -
-
- )} -
- ); -} - diff --git a/ui/dashboard/src/components/BudgetCard.tsx b/ui/dashboard/src/components/BudgetCard.tsx deleted file mode 100644 index 25b0dae..0000000 --- a/ui/dashboard/src/components/BudgetCard.tsx +++ /dev/null @@ -1,255 +0,0 @@ -'use client'; - -import { useState, useRef, useEffect } from 'react'; -import LoadingSkeleton from '@/components/ui/LoadingSkeleton'; -import ErrorState from '@/components/ui/ErrorState'; -import EmptyState from '@/components/ui/EmptyState'; -import { BudgetResponse } from '@/lib/api'; - -interface BudgetCardProps { - data: BudgetResponse | null; - loading: boolean; - error: string | null; - onSave: (payload: { amountUsd: number | null; alertOnly?: boolean }) => Promise; - onRetry: () => Promise; -} - -export default function BudgetCard({ data, loading, error, onSave, onRetry }: BudgetCardProps) { - const [showModal, setShowModal] = useState(false); - const [amount, setAmount] = useState(''); - const [alertOnly, setAlertOnly] = useState(false); - const [saving, setSaving] = useState(false); - const [formError, setFormError] = useState(null); - const inputRef = useRef(null); - - const spent = data?.spentMonthToDate ?? 0; - const budget = data?.amountUsd ?? 0; - const usagePercent = budget > 0 ? Math.min((spent / budget) * 100, 100) : 0; - - // Calculate days until month end - const getDaysUntilReset = () => { - const now = new Date(); - const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); - const daysLeft = Math.ceil((lastDay.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); - return daysLeft; - }; - - // Get month name - const getMonthName = () => { - return new Date().toLocaleString('default', { month: 'long' }); - }; - - // Get progress bar color - using teal theme - const getProgressColor = () => { - if (budget === 0) return 'bg-gray-300'; - if (usagePercent >= 100) return 'bg-red-500'; - if (usagePercent >= 80) return 'bg-amber-500'; - return '#004f4f'; // Teal - }; - - useEffect(() => { - if (showModal && inputRef.current) { - inputRef.current.focus(); - inputRef.current.select(); - } - }, [showModal]); - - const handleOpenModal = () => { - setFormError(null); - setAmount(data?.amountUsd != null ? String(data.amountUsd) : ''); - setAlertOnly(data?.alertOnly ?? false); - setShowModal(true); - }; - - const handleCloseModal = () => { - setShowModal(false); - setFormError(null); - }; - - const handleSave = async () => { - const parsed = amount.trim() === '' ? null : Number(amount); - - if (parsed !== null && (!Number.isFinite(parsed) || parsed < 0)) { - setFormError('Enter a non-negative number or leave blank to disable'); - return; - } - - try { - setSaving(true); - setFormError(null); - await onSave({ amountUsd: parsed, alertOnly }); - setShowModal(false); - } catch (err) { - setFormError(err instanceof Error ? err.message : 'Failed to save budget'); - } finally { - setSaving(false); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - handleCloseModal(); - } - }; - - if (loading) { - return ; - } - - if (error) { - return ( - - ); - } - - if (!data) { - return ( - - ); - } - - return ( - <> -
-
-
-

{getMonthName()} budget

- -
-
- {loading ? ( - '…' - ) : ( - <> - ${spent.toFixed(2)} / ${budget > 0 ? budget.toFixed(2) : '—'} - - )} -
- - {/* Progress Bar */} - {budget > 0 && ( -
-
-
-
- {usagePercent > 0 && usagePercent < 100 && ( -
- )} -
- )} - -

Resets in {getDaysUntilReset()} {getDaysUntilReset() === 1 ? 'day' : 'days'}.

-
-
- - -
- - {error && ( -
- {error} - -
- )} -
- - {/* Modal */} - {showModal && ( -
-
e.stopPropagation()} - > -

Edit budget

-

- Your intended monthly budget. Your actual costs may exceed this budget based on usage. -

- -
- -
- $ - setAmount(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleSave(); - } - }} - placeholder="0.00" - className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-0 focus:border-[#004f4f]" - style={{ '--tw-ring-color': '#004f4f' } as React.CSSProperties} - /> -
-
- - - - {formError && ( -

{formError}

- )} - -
- - -
-
-
- )} - - ); -} diff --git a/ui/dashboard/src/components/ConfigStatus.tsx b/ui/dashboard/src/components/ConfigStatus.tsx deleted file mode 100644 index f5caa44..0000000 --- a/ui/dashboard/src/components/ConfigStatus.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { ConfigStatusResponse } from '@/lib/api'; -import { getProviderName } from '@/lib/utils'; -import LoadingSkeleton from '@/components/ui/LoadingSkeleton'; -import ErrorState from '@/components/ui/ErrorState'; -import EmptyState from '@/components/ui/EmptyState'; - -interface ConfigStatusProps { - status: ConfigStatusResponse | null; - loading: boolean; - error?: string | null; - onRetry: () => void; -} - -export default function ConfigStatus({ status, loading, error, onRetry }: ConfigStatusProps) { - if (loading) { - return ; - } - - if (error) { - return ( - - ); - } - - if (!status) { - return ( - - ); - } - - const activeProviders = Object.entries(status.providers) - .filter(([, enabled]) => enabled) - .map(([name]) => name); - - const inactiveProviders = Object.entries(status.providers) - .filter(([, enabled]) => !enabled) - .map(([name]) => name); - - const getModeDisplay = () => { - switch (status.mode) { - case 'byok': - return 'Bring Your Own Keys'; - case 'hybrid': - return 'Hybrid (BYOK + x402)'; - case 'x402-only': - return 'x402 Payments'; - default: - return status.mode; - } - }; - - return ( -
-
- {/* Providers Card */} -
-
Providers
-
- {activeProviders.length > 0 ? ( - <> -
- {activeProviders.slice(0, 3).map((provider) => ( - - {getProviderName(provider)} - - ))} - {activeProviders.length > 3 && ( - +{activeProviders.length - 3} - )} -
-
-
- {activeProviders.length} -
- - ) : ( - None active - )} -
-
- - {/* Mode Card */} -
-
Mode
-
{getModeDisplay()}
-
- - {/* x402 Status Card */} -
-
Payment Method
-
- {status.x402Enabled ? ( - <> - - x402 - - Enabled - - ) : ( - API Keys Only - )} -
-
-
- - {/* Warning for missing providers */} - {inactiveProviders.length > 0 && ( -
- - - -
-

- Missing providers: {inactiveProviders.map(p => getProviderName(p)).join(', ')} -

-

- {status.x402Enabled - ? ( - <> - These will automatically use x402 on-chain payments if supported by the x402 gateway URL.{' '} - - Learn more about x402 on Ekai - - . - - ) - : 'Add API keys to your .env file to enable these providers.'} -

-
-
- )} - - {/* Critical warning for no providers */} - {activeProviders.length === 0 && !status.x402Enabled && ( -
- - - -
-

No providers configured

-

- Add API keys to your .env file or enable x402 payments to start using the gateway. -

-
-
- )} -
- ); -} diff --git a/ui/dashboard/src/components/DateRangePicker.tsx b/ui/dashboard/src/components/DateRangePicker.tsx deleted file mode 100644 index 2064b5c..0000000 --- a/ui/dashboard/src/components/DateRangePicker.tsx +++ /dev/null @@ -1,212 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -export interface DateRange { - from: Date; - to: Date; -} - -export interface DateRangePickerProps { - value: DateRange | null; - onChange: (range: DateRange | null) => void; -} - -const presets = [ - { - label: 'Today', - getValue: () => { - const now = new Date(); - const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); - return { from: start, to: end }; - } - }, - { - label: 'Yesterday', - getValue: () => { - const now = new Date(); - const yesterday = new Date(now); - yesterday.setDate(yesterday.getDate() - 1); - const start = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate()); - const end = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 23, 59, 59); - return { from: start, to: end }; - } - }, - { - label: 'Last 7 Days', - getValue: () => { - const now = new Date(); - const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); - const start = new Date(now); - start.setDate(start.getDate() - 6); - start.setHours(0, 0, 0, 0); - return { from: start, to: end }; - } - }, - { - label: 'Last 30 Days', - getValue: () => { - const now = new Date(); - const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); - const start = new Date(now); - start.setDate(start.getDate() - 29); - start.setHours(0, 0, 0, 0); - return { from: start, to: end }; - } - }, - { - label: 'This Month', - getValue: () => { - const now = new Date(); - const start = new Date(now.getFullYear(), now.getMonth(), 1); - const end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); - return { from: start, to: end }; - } - }, - { - label: 'Last Month', - getValue: () => { - const now = new Date(); - const start = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const end = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59); - return { from: start, to: end }; - } - } -]; - -export default function DateRangePicker({ value, onChange }: DateRangePickerProps) { - const [isOpen, setIsOpen] = useState(false); - const [tempFrom, setTempFrom] = useState(value?.from ? formatDateForInput(value.from) : ''); - const [tempTo, setTempTo] = useState(value?.to ? formatDateForInput(value.to) : ''); - - function formatDateForInput(date: Date): string { - return date.toISOString().split('T')[0]; - } - - function formatDateForDisplay(date: Date): string { - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - } - - const handlePresetClick = (preset: typeof presets[0]) => { - const range = preset.getValue(); - onChange(range); - setTempFrom(formatDateForInput(range.from)); - setTempTo(formatDateForInput(range.to)); - setIsOpen(false); - }; - - const handleCustomRangeApply = () => { - if (tempFrom && tempTo) { - const fromDate = new Date(tempFrom); - const toDate = new Date(tempTo); - toDate.setHours(23, 59, 59, 999); - - if (fromDate <= toDate) { - onChange({ from: fromDate, to: toDate }); - setIsOpen(false); - } - } - }; - - const handleClear = () => { - onChange(null); - setTempFrom(''); - setTempTo(''); - setIsOpen(false); - }; - - return ( -
- - - {isOpen && ( -
-
- {/* Presets */} -
-

Quick Select

-
- {presets.map((preset) => ( - - ))} -
-
- - {/* Custom Range */} -
-

Custom Range

-
-
- - setTempFrom(e.target.value)} - className="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" - /> -
-
- - setTempTo(e.target.value)} - className="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" - /> -
-
-
- - {/* Actions */} -
- -
- - -
-
-
-
- )} -
- ); -} \ No newline at end of file diff --git a/ui/dashboard/src/components/FirstRunModal.tsx b/ui/dashboard/src/components/FirstRunModal.tsx deleted file mode 100644 index 09c0892..0000000 --- a/ui/dashboard/src/components/FirstRunModal.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { ReactNode } from 'react'; -import { useCopy } from '@/hooks/useCopy'; -import { API_CONFIG } from '@/lib/constants'; - -const API_BASE_URL = API_CONFIG.BASE_URL; -const DASHBOARD_URL = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'; - -interface FirstRunModalProps { - open: boolean; - onClose: () => void; - onRefresh: () => void; - children?: ReactNode; -} - -export default function FirstRunModal({ open, onClose, onRefresh, children }: FirstRunModalProps) { - if (!open) return null; - - return ( -
-
- -
-
- 🧭 - First run guide -
-

Get started with the gateway

-

- Configure your .env, start the gateway, then hit it with any OpenAI- or Anthropic-compatible client such as Claude Code, Codex, or any API client. Refresh once you send your first call. -

-
- -
- - Copy `.env.example` to `.env` and add at least one provider key (OpenAI, Anthropic, Gemini, xAI, OpenRouter, etc.). - - - Restart the services to ensure the gateway is running on port 3001. - - - Point your client to the gateway base URL shown below and make a test chat completion. Come back and refresh to see usage. - -
- -
- - -
- - {children} - -
- - -
-
-
- ); -} - -interface StepProps { - number: number; - title: string; - children: ReactNode; - commands?: string[]; -} - -function Step({ number, title, children, commands = [] }: StepProps) { - return ( -
-
- {number} -
-
-

{title}

-

{children}

- {commands.length > 0 && ( -
- {commands.map(cmd => ( - - ))} -
- )} -
-
- ); -} - -function CommandSnippet({ command }: { command: string }) { - const { copied, copy } = useCopy(); - - return ( -
- {command} - -
- ); -} - -function InfoCard({ label, value }: { label: string; value: string }) { - return ( -
-
{label}
-
{value}
-
- ); -} diff --git a/ui/dashboard/src/components/ModelChart.tsx b/ui/dashboard/src/components/ModelChart.tsx deleted file mode 100644 index 86743f4..0000000 --- a/ui/dashboard/src/components/ModelChart.tsx +++ /dev/null @@ -1,112 +0,0 @@ -'use client'; - -import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'; -import { formatCurrency } from '@/lib/utils'; -import { CHART_COLORS } from '@/lib/constants'; -import { UsageDataResult } from '@/hooks/useUsageData'; -import LoadingSkeleton from '@/components/ui/LoadingSkeleton'; -import ErrorState from '@/components/ui/ErrorState'; -import EmptyState from '@/components/ui/EmptyState'; -import ChartTooltip from '@/components/ui/ChartTooltip'; - -interface ModelChartProps { - className?: string; - usageData: UsageDataResult; -} - -export default function ModelChart({ className = '', usageData }: ModelChartProps) { - const { costByModel, totalCost, loading, error, refetch } = usageData; - - // Helper function to clean model names for display - const cleanModelName = (modelName: string) => { - // Just return the model name as-is, no formatting needed - return modelName; - }; - - // Convert to chart data format - const data = Object.entries(costByModel) - .map(([model, cost]) => ({ - name: cleanModelName(model), - value: Number(cost.toFixed(6)), - percentage: totalCost > 0 ? ((cost / totalCost) * 100).toFixed(1) : '0' - })) - .sort((a, b) => b.value - a.value); - - - if (loading) { - return ; - } - - if (error) { - return ; - } - - if (data.length === 0) { - return ( - - ); - } - - return ( -
-

Model Comparison

- -
- - - { - const percent = props.percent || 0; - return percent >= 5 ? props.name || '' : ''; - }} - outerRadius={80} - fill="#8884d8" - dataKey="value" - > - {data.map((entry, index) => ( - - ))} - - } /> - - -
- - {/* Model List */} -
- {data.map((model, index) => ( -
-
-
- {model.name} -
-
-

{formatCurrency(model.value)}

-

{model.percentage}%

-
-
- ))} -
- -
-
- Total Cost: - {formatCurrency(totalCost)} -
-
-
- ); -} diff --git a/ui/dashboard/src/components/ProviderChart.tsx b/ui/dashboard/src/components/ProviderChart.tsx deleted file mode 100644 index 7c995fa..0000000 --- a/ui/dashboard/src/components/ProviderChart.tsx +++ /dev/null @@ -1,106 +0,0 @@ -'use client'; - -import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'; -import { formatCurrency, getProviderName } from '@/lib/utils'; -import { CHART_COLORS } from '@/lib/constants'; -import { UsageDataResult } from '@/hooks/useUsageData'; -import LoadingSkeleton from '@/components/ui/LoadingSkeleton'; -import ErrorState from '@/components/ui/ErrorState'; -import EmptyState from '@/components/ui/EmptyState'; -import ChartTooltip from '@/components/ui/ChartTooltip'; - -interface ProviderChartProps { - className?: string; - usageData: UsageDataResult; -} - -export default function ProviderChart({ className = '', usageData }: ProviderChartProps) { - const { costByProvider, totalCost, loading, error, refetch } = usageData; - - // Convert to chart data format - const data = Object.entries(costByProvider) - .map(([provider, cost]) => ({ - name: getProviderName(provider), - value: Number(cost.toFixed(6)), - percentage: totalCost > 0 ? ((cost / totalCost) * 100).toFixed(1) : '0' - })) - .sort((a, b) => b.value - a.value); - - - if (loading) { - return ; - } - - if (error) { - return ; - } - - if (data.length === 0) { - return ( - - ); - } - - return ( -
-

Provider Breakdown

- -
- - - { - const percent = props.percent || 0; - return percent >= 5 ? `${props.name || ''} (${percent.toFixed(1)}%)` : ''; - }} - outerRadius={80} - fill="#8884d8" - dataKey="value" - > - {data.map((entry, index) => ( - - ))} - - } /> - - -
- - {/* Provider List */} -
- {data.map((provider, index) => ( -
-
-
- {provider.name} -
-
-

{formatCurrency(provider.value)}

-

{provider.percentage}%

-
-
- ))} -
- -
-
- Total Cost: - {formatCurrency(totalCost)} -
-
-
- ); -} diff --git a/ui/dashboard/src/components/TrendChart.tsx b/ui/dashboard/src/components/TrendChart.tsx deleted file mode 100644 index 0c7bfce..0000000 --- a/ui/dashboard/src/components/TrendChart.tsx +++ /dev/null @@ -1,271 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'; -import { groupByDate, formatForChart, calculateBurnRate, detectAnomalies, formatCurrency, formatNumber } from '@/lib/utils'; -import { UsageDataResult } from '@/hooks/useUsageData'; -import LoadingSkeleton from '@/components/ui/LoadingSkeleton'; -import ErrorState from '@/components/ui/ErrorState'; -import EmptyState from '@/components/ui/EmptyState'; -import ChartTooltip from '@/components/ui/ChartTooltip'; - -interface TrendChartProps { - className?: string; - usageData: UsageDataResult; -} - -export default function TrendChart({ className = '', usageData }: TrendChartProps) { - const { records, loading, error, refetch } = usageData; - const [chartType, setChartType] = useState<'line' | 'bar'>('bar'); - const [timeframe, setTimeframe] = useState<'hour' | 'day'>('day'); - - // Process data for chart - const grouped = groupByDate(records, timeframe); - const data = formatForChart(grouped); - const burnRate = calculateBurnRate(records); - const anomalies = detectAnomalies(records); - const totalStats = { - totalCost: records.reduce((sum, r) => sum + r.total_cost, 0), - totalTokens: records.reduce((sum, r) => sum + r.total_tokens, 0), - totalRequests: records.length - }; - - - if (loading) { - return ; - } - - if (error) { - return ; - } - - if (data.length === 0) { - return ( - - ); - } - - return ( -
- {/* Header */} -
-
-

Usage Analytics

-

- Daily burn rate: {formatCurrency(burnRate)}/day -

-
- - {/* Controls */} -
- - - -
-
- - {/* Anomalies Alert */} - {anomalies.length > 0 && ( -
-

- ⚠️ Spending Anomaly Detected: - {' '}{anomalies.length} day(s) with unusually high spending -

-
- )} - - {/* Two Charts Side by Side */} -
- {/* Spend Over Time Chart */} -
-

Spend Over Time

-
- - {chartType === 'line' ? ( - - - timeframe === 'hour' ? - new Date(value).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : - new Date(value).toLocaleDateString()} - tick={{ fontSize: 10 }} - /> - `$${value.toFixed(4)}`} - tick={{ fontSize: 10 }} - /> - } /> - - - ) : ( - - - timeframe === 'hour' ? - new Date(value).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : - new Date(value).toLocaleDateString()} - tick={{ fontSize: 10 }} - /> - `$${value.toFixed(4)}`} - tick={{ fontSize: 10 }} - /> - } /> - - - )} - -
-
- - {/* Tokens Over Time Chart */} -
-

Token Usage Breakdown

-
- - {chartType === 'line' ? ( - - - timeframe === 'hour' ? - new Date(value).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : - new Date(value).toLocaleDateString()} - tick={{ fontSize: 10 }} - /> - formatNumber(value)} - tick={{ fontSize: 10 }} - /> - } /> - - - - - - ) : ( - - - timeframe === 'hour' ? - new Date(value).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : - new Date(value).toLocaleDateString()} - tick={{ fontSize: 10 }} - /> - formatNumber(value)} - tick={{ fontSize: 10 }} - /> - } /> - - - - - - )} - -
- - {/* Token Type Legend */} -
-
-
- Input -
-
-
- Cache Write -
-
-
- Cache Read -
-
-
- Output -
-
-
-
- - {/* Summary Stats */} -
-
-

- {formatCurrency(totalStats.totalCost)} -

-

Total Spend

-
-
-

- {formatNumber(totalStats.totalTokens)} -

-

Total Tokens

-
-
-

- {totalStats.totalRequests} -

-

Total Requests

-
-
-
- ); -} diff --git a/ui/dashboard/src/components/UsageTable.tsx b/ui/dashboard/src/components/UsageTable.tsx deleted file mode 100644 index 511f059..0000000 --- a/ui/dashboard/src/components/UsageTable.tsx +++ /dev/null @@ -1,234 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { UsageRecord } from '@/lib/api'; -import { formatCurrency, formatNumber, getProviderName } from '@/lib/utils'; -import { UsageDataResult } from '@/hooks/useUsageData'; -import LoadingSkeleton from '@/components/ui/LoadingSkeleton'; -import ErrorState from '@/components/ui/ErrorState'; -import EmptyState from '@/components/ui/EmptyState'; - -interface UsageTableProps { - className?: string; - usageData: UsageDataResult; -} - -export default function UsageTable({ className = '', usageData }: UsageTableProps) { - const { records, loading, error, refetch } = usageData; - const [sortField, setSortField] = useState('timestamp'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); - - - const handleSort = (field: keyof UsageRecord) => { - if (sortField === field) { - setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); - } else { - setSortField(field); - setSortDirection('desc'); - } - }; - - const sortedData = [...records].sort((a, b) => { - const aVal = a[sortField]; - const bVal = b[sortField]; - - if (typeof aVal === 'string' && typeof bVal === 'string') { - return sortDirection === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); - } - - if (typeof aVal === 'number' && typeof bVal === 'number') { - return sortDirection === 'asc' ? aVal - bVal : bVal - aVal; - } - - return 0; - }); - - if (loading) { - return ; - } - - if (error) { - return ; - } - - if (records.length === 0) { - return ( - - ); - } - - const SortIcon = ({ field }: { field: keyof UsageRecord }) => { - if (sortField !== field) { - return ; - } - return {sortDirection === 'asc' ? '↑' : '↓'}; - }; - - return ( -
-
-

Usage History

-

{records.length} total requests

-
- -
- - - - - - - - - - - - - - - {sortedData.map((record) => ( - - - - - - - - - - - ))} - -
handleSort('timestamp')} - > -
- Timestamp -
-
handleSort('provider')} - > -
- Provider -
-
handleSort('model')} - > -
- Model -
-
handleSort('input_tokens')} - > -
- Input Tokens -
-
handleSort('cache_write_input_tokens')} - > -
- Cache Write -
-
handleSort('cache_read_input_tokens')} - > -
- Cache Read -
-
handleSort('output_tokens')} - > -
- Output Tokens -
-
handleSort('total_cost')} - > -
- Total Cost -
-
- {new Date(record.timestamp).toLocaleString()} - -
- - {getProviderName(record.provider)} - - {record.payment_method === 'x402' && ( - - x402 - - )} -
-
- {record.model} - - {formatNumber(record.input_tokens)} - - {formatNumber(record.cache_write_input_tokens)} - - {formatNumber(record.cache_read_input_tokens)} - - {formatNumber(record.output_tokens)} - - {formatCurrency(record.total_cost)} -
-
- - {/* Summary footer */} -
-
-
-

- {records.length} -

-

Requests

-
-
-

- {formatNumber(records.reduce((sum, r) => sum + r.input_tokens, 0))} -

-

Input Tokens

-
-
-

- {formatNumber(records.reduce((sum, r) => sum + r.cache_write_input_tokens, 0))} -

-

Cache Write

-
-
-

- {formatNumber(records.reduce((sum, r) => sum + r.cache_read_input_tokens, 0))} -

-

Cache Read

-
-
-

- {formatNumber(records.reduce((sum, r) => sum + r.output_tokens, 0))} -

-

Output Tokens

-
-
-

- {formatCurrency(records.reduce((sum, r) => sum + r.total_cost, 0))} -

-

Total Cost

-
-
-
-
- ); -} diff --git a/ui/dashboard/src/components/ui/Card.tsx b/ui/dashboard/src/components/ui/Card.tsx deleted file mode 100644 index 238c81b..0000000 --- a/ui/dashboard/src/components/ui/Card.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ReactNode } from 'react'; - -interface CardProps { - className?: string; - children: ReactNode; -} - -export function Card({ className = '', children }: CardProps) { - return
{children}
; -} diff --git a/ui/dashboard/src/components/ui/ChartTooltip.tsx b/ui/dashboard/src/components/ui/ChartTooltip.tsx deleted file mode 100644 index f971a0e..0000000 --- a/ui/dashboard/src/components/ui/ChartTooltip.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { formatCurrency, formatNumber } from '@/lib/utils'; - -interface TooltipPayload { - formattedDate?: string; - formattedTime?: string; - cost?: number; - value?: number; - tokens?: number; - requests?: number; - name?: string; - percentage?: number; - inputTokens?: number; - cacheWriteTokens?: number; - cacheReadTokens?: number; - outputTokens?: number; -} - -interface ChartTooltipProps { - active?: boolean; - payload?: { value: number; name: string; color: string; payload: TooltipPayload }[]; - label?: string; - type?: 'cost' | 'tokens' | 'provider' | 'model'; -} - -export default function ChartTooltip({ active, payload, label, type = 'cost' }: ChartTooltipProps) { - if (!active || !payload || !payload.length) { - return null; - } - - const data = payload[0].payload; - - return ( -
- {type === 'cost' && ( - <> -

{data.formattedDate || label}

- {data.formattedTime &&

{data.formattedTime}

} -

- Cost: {formatCurrency(data.cost || data.value || 0)} -

- {data.tokens && ( -

- Tokens: {formatNumber(data.tokens)} -

- )} - {data.requests && ( -

- Requests: {data.requests} -

- )} - - )} - - {type === 'tokens' && ( - <> -

{data.formattedDate || label}

- {data.inputTokens !== undefined && data.cacheWriteTokens !== undefined && - data.cacheReadTokens !== undefined && data.outputTokens !== undefined ? ( - <> -
-

- Input: {formatNumber(data.inputTokens)} -

-

- Cache Write: {formatNumber(data.cacheWriteTokens)} -

-

- Cache Read: {formatNumber(data.cacheReadTokens)} -

-

- Output: {formatNumber(data.outputTokens)} -

-
-

- Total: {formatNumber( - data.inputTokens + data.cacheWriteTokens + data.cacheReadTokens + data.outputTokens - )} -

-
- - ) : ( -

- Tokens: {formatNumber(data.tokens || data.value || 0)} -

- )} - - )} - - {(type === 'provider' || type === 'model') && ( - <> -

{data.name}

-

- Cost: {formatCurrency(data.value || 0)} -

-

- Percentage: {data.percentage}% -

- - )} -
- ); -} \ No newline at end of file diff --git a/ui/dashboard/src/components/ui/EmptyState.tsx b/ui/dashboard/src/components/ui/EmptyState.tsx deleted file mode 100644 index c41eb75..0000000 --- a/ui/dashboard/src/components/ui/EmptyState.tsx +++ /dev/null @@ -1,23 +0,0 @@ -interface EmptyStateProps { - className?: string; - title: string; - description: string; - suggestion?: string; -} - -export default function EmptyState({ - className = '', - title, - description, - suggestion = "Make some API requests to see data." -}: EmptyStateProps) { - return ( -
-

{title}

-
-

{description}

-

{suggestion}

-
-
- ); -} \ No newline at end of file diff --git a/ui/dashboard/src/hooks/useBudget.ts b/ui/dashboard/src/hooks/useBudget.ts deleted file mode 100644 index e47d905..0000000 --- a/ui/dashboard/src/hooks/useBudget.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { apiService, BudgetResponse } from '@/lib/api'; - -export interface BudgetResult { - data: BudgetResponse | null; - loading: boolean; - error: string | null; - refetch: () => Promise; - saveBudget: (payload: { amountUsd: number | null; alertOnly?: boolean }) => Promise; -} - -export const useBudget = (): BudgetResult => { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchBudget = useCallback(async () => { - try { - setLoading(true); - setError(null); - const response = await apiService.getBudget(); - setData(response); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch budget'); - } finally { - setLoading(false); - } - }, []); - - const saveBudget = useCallback(async (payload: { amountUsd: number | null; alertOnly?: boolean }) => { - try { - setError(null); - await apiService.updateBudget(payload); - await fetchBudget(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to update budget'); - throw err; - } - }, [fetchBudget]); - - useEffect(() => { - fetchBudget(); - }, [fetchBudget]); - - return { data, loading, error, refetch: fetchBudget, saveBudget }; -}; diff --git a/ui/dashboard/src/hooks/useConfigStatus.ts b/ui/dashboard/src/hooks/useConfigStatus.ts deleted file mode 100644 index afdfc12..0000000 --- a/ui/dashboard/src/hooks/useConfigStatus.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import { apiService, ConfigStatusResponse } from '@/lib/api'; - -export const useConfigStatus = () => { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchStatus = useCallback(async () => { - try { - setLoading(true); - setError(null); - const status = await apiService.getConfigStatus(); - setData(status); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch config status'); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchStatus(); - }, [fetchStatus]); - - return { data, loading, error, refetch: fetchStatus }; -}; diff --git a/ui/dashboard/src/hooks/useCopy.ts b/ui/dashboard/src/hooks/useCopy.ts deleted file mode 100644 index ea6f389..0000000 --- a/ui/dashboard/src/hooks/useCopy.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useState, useCallback } from 'react'; - -export function useCopy(timeoutMs: number = 1500) { - const [copied, setCopied] = useState(false); - - const copy = useCallback(async (text: string) => { - try { - await navigator.clipboard.writeText(text); - setCopied(true); - setTimeout(() => setCopied(false), timeoutMs); - } catch { - setCopied(false); - } - }, [timeoutMs]); - - return { copied, copy }; -} diff --git a/ui/dashboard/src/hooks/useUsageData.ts b/ui/dashboard/src/hooks/useUsageData.ts deleted file mode 100644 index 87155d9..0000000 --- a/ui/dashboard/src/hooks/useUsageData.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { apiService, UsageResponse, UsageRecord } from '@/lib/api'; - -export interface UsageDataResult { - data: UsageResponse | null; - loading: boolean; - error: string | null; - refetch: () => Promise; - records: UsageRecord[]; - totalCost: number; - totalTokens: number; - totalRequests: number; - costByProvider: Record; - costByModel: Record; -} - -export const useUsageData = (fromDate?: Date, toDate?: Date): UsageDataResult => { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - const response = await apiService.getUsage(fromDate, toDate); - setData(response); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch data'); - } finally { - setLoading(false); - } - }, [fromDate, toDate]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { - data, - loading, - error, - refetch: fetchData, - records: data?.records || [], - totalCost: data?.totalCost || 0, - totalTokens: data?.totalTokens || 0, - totalRequests: data?.totalRequests || 0, - costByProvider: data?.costByProvider || {}, - costByModel: data?.costByModel || {} - }; -}; diff --git a/ui/dashboard/src/lib/api.ts b/ui/dashboard/src/lib/api.ts index 40c8fb6..4c4f4f1 100644 --- a/ui/dashboard/src/lib/api.ts +++ b/ui/dashboard/src/lib/api.ts @@ -1,58 +1,7 @@ import { API_CONFIG } from './constants'; -const API_BASE_URL = API_CONFIG.BASE_URL; export const MEMORY_BASE_URL = API_CONFIG.MEMORY_URL; -// Types based on your backend response -export interface UsageRecord { - id: number; - request_id: string; - provider: string; - model: string; - timestamp: string; - input_tokens: number; - cache_write_input_tokens: number; - cache_read_input_tokens: number; - output_tokens: number; - total_tokens: number; - input_cost: number; - cache_write_cost: number; - cache_read_cost: number; - output_cost: number; - total_cost: number; - currency: string; - payment_method?: string; - created_at: string; -} - -export interface UsageResponse { - totalRequests: number; - totalCost: number; - totalTokens: number; - costByProvider: Record; - costByModel: Record; - records: UsageRecord[]; -} - -export interface ConfigStatusResponse { - providers: Record; - mode: 'byok' | 'hybrid' | 'x402-only'; - hasApiKeys: boolean; - x402Enabled: boolean; - server: { - environment: string; - port: number; - }; -} - -export interface BudgetResponse { - amountUsd: number | null; - alertOnly: boolean; - window: 'monthly'; - spentMonthToDate: number; - remaining: number | null; -} - export interface MemorySectorSummary { sector: 'episodic' | 'semantic' | 'procedural' | 'reflective'; count: number; @@ -82,99 +31,11 @@ export interface MemorySummaryResponse { // API service functions export const apiService = { - // Fetch usage data - async getUsage(fromDate?: Date, toDate?: Date): Promise { - let url = `${API_BASE_URL}/usage`; - - if (fromDate || toDate) { - const params = new URLSearchParams(); - if (fromDate) { - params.append('startTime', fromDate.toISOString()); - } - if (toDate) { - params.append('endTime', toDate.toISOString()); - } - url += `?${params.toString()}`; - } - - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch usage data: ${response.statusText}`); - } - return response.json(); - }, - - // Check health - async getHealth() { - const response = await fetch(`${API_BASE_URL}/health`); - if (!response.ok) { - throw new Error(`Failed to fetch health: ${response.statusText}`); - } - return response.json(); - }, - - async downloadUsageCsv(fromDate?: Date, toDate?: Date) { - const params = new URLSearchParams(); - if (fromDate) params.append('startTime', fromDate.toISOString()); - if (toDate) params.append('endTime', toDate.toISOString()); - params.append('format', 'csv'); - - const url = `${API_BASE_URL}/usage?${params.toString()}`; - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to export CSV: ${response.statusText}`); - } - - const blob = await response.blob(); - const link = document.createElement('a'); - const downloadUrl = window.URL.createObjectURL(blob); - link.href = downloadUrl; - - const startLabel = fromDate ? fromDate.toISOString().slice(0, 10) : 'start'; - const endLabel = toDate ? toDate.toISOString().slice(0, 10) : 'end'; - link.download = `usage-${startLabel}-${endLabel}.csv`; - - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(downloadUrl); - }, - - async getConfigStatus(): Promise { - const response = await fetch(`${API_BASE_URL}/config/status`); - if (!response.ok) { - throw new Error(`Failed to fetch config status: ${response.statusText}`); - } - return response.json(); - }, - - async getBudget(): Promise { - const response = await fetch(`${API_BASE_URL}/budget`); - if (!response.ok) { - throw new Error(`Failed to fetch budget: ${response.statusText}`); - } - return response.json(); - }, - - async updateBudget(payload: { amountUsd: number | null; alertOnly?: boolean }): Promise { - const response = await fetch(`${API_BASE_URL}/budget`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - throw new Error(`Failed to update budget: ${response.statusText}`); - } - - return response.json(); - }, - async getMemorySummary(limit = 50, profile?: string): Promise { const params = new URLSearchParams(); params.append('limit', String(limit)); if (profile) params.append('profile', profile); - + const response = await fetch(`${MEMORY_BASE_URL}/v1/summary?${params.toString()}`); if (!response.ok) { throw new Error(`Failed to fetch memory summary: ${response.statusText}`); diff --git a/ui/dashboard/src/lib/constants.ts b/ui/dashboard/src/lib/constants.ts index df52325..8424ec5 100644 --- a/ui/dashboard/src/lib/constants.ts +++ b/ui/dashboard/src/lib/constants.ts @@ -1,14 +1,3 @@ -export const CHART_COLORS = [ - '#3b82f6', // blue-500 - '#10b981', // emerald-500 - '#f59e0b', // amber-500 - '#ef4444', // red-500 - '#8b5cf6', // violet-500 - '#06b6d4', // cyan-500 - '#84cc16', // lime-500 - '#f97316', // orange-500 -]; - // Detect the base host (protocol + hostname) from the runtime environment const getBaseHost = (): string => { if (typeof window === 'undefined') { @@ -33,7 +22,6 @@ const buildUrl = (host: string, port: string): string => `${host}:${port}`; const baseHost = getBaseHost(); -export const GATEWAY_PORT = process.env.NEXT_PUBLIC_GATEWAY_PORT || '3001'; export const MEMORY_PORT = process.env.NEXT_PUBLIC_MEMORY_PORT || '4010'; // Embedded mode: UI is served from the same Express server as the API @@ -42,6 +30,5 @@ const isEmbedded = (typeof window !== 'undefined' && window.location.port === MEMORY_PORT); export const API_CONFIG = { - BASE_URL: buildUrl(baseHost, GATEWAY_PORT), MEMORY_URL: isEmbedded ? '' : buildUrl(baseHost, MEMORY_PORT), -} as const; \ No newline at end of file +} as const; diff --git a/ui/dashboard/src/lib/utils.ts b/ui/dashboard/src/lib/utils.ts deleted file mode 100644 index 1126c1f..0000000 --- a/ui/dashboard/src/lib/utils.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { UsageRecord } from './api'; - -// Date grouping utilities -export const groupByDate = (records: UsageRecord[], groupBy: 'hour' | 'day' = 'day') => { - const grouped: Record = {}; - - records.forEach(record => { - const date = new Date(record.timestamp); - let key: string; - - if (groupBy === 'hour') { - // Group by hour: YYYY-MM-DD HH:00 - key = date.toISOString().slice(0, 13) + ':00:00Z'; - } else { - // Group by day: YYYY-MM-DD - key = date.toISOString().slice(0, 10); - } - - if (!grouped[key]) { - grouped[key] = { - cost: 0, - tokens: 0, - requests: 0, - inputTokens: 0, - cacheWriteTokens: 0, - cacheReadTokens: 0, - outputTokens: 0 - }; - } - - grouped[key].cost += record.total_cost; - grouped[key].tokens += record.total_tokens; - grouped[key].requests += 1; - grouped[key].inputTokens += record.input_tokens; - grouped[key].cacheWriteTokens += record.cache_write_input_tokens; - grouped[key].cacheReadTokens += record.cache_read_input_tokens; - grouped[key].outputTokens += record.output_tokens; - }); - - return grouped; -}; - -// Convert grouped data to chart format -export const formatForChart = (grouped: Record) => { - return Object.entries(grouped) - .map(([date, data]) => ({ - date, - cost: Number(data.cost.toFixed(6)), - tokens: data.tokens, - requests: data.requests, - inputTokens: data.inputTokens, - cacheWriteTokens: data.cacheWriteTokens, - cacheReadTokens: data.cacheReadTokens, - outputTokens: data.outputTokens, - formattedDate: new Date(date).toLocaleDateString(), - formattedTime: new Date(date).toLocaleTimeString() - })) - .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); -}; - -// Calculate burn rate (daily average) -export const calculateBurnRate = (records: UsageRecord[]) => { - if (records.length === 0) return 0; - - const grouped = groupByDate(records, 'day'); - const dailyCosts = Object.values(grouped).map(d => d.cost); - - if (dailyCosts.length === 0) return 0; - - const totalCost = dailyCosts.reduce((sum, cost) => sum + cost, 0); - const averageDailyCost = totalCost / dailyCosts.length; - - return Number(averageDailyCost.toFixed(6)); -}; - -// Detect anomalies (spikes in spending) -export const detectAnomalies = (records: UsageRecord[]) => { - const grouped = groupByDate(records, 'day'); - const dailyCosts = Object.entries(grouped).map(([date, data]) => ({ - date, - cost: data.cost - })).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - - if (dailyCosts.length < 3) return []; - - const costs = dailyCosts.map(d => d.cost); - const mean = costs.reduce((sum, cost) => sum + cost, 0) / costs.length; - const variance = costs.reduce((sum, cost) => sum + Math.pow(cost - mean, 2), 0) / costs.length; - const stdDev = Math.sqrt(variance); - const threshold = mean + (2 * stdDev); // 2 standard deviations - - return dailyCosts.filter(d => d.cost > threshold); -}; - -// Format currency -export const formatCurrency = (amount: number, currency: string = 'USD') => { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: currency, - minimumFractionDigits: 6, - maximumFractionDigits: 6 - }).format(amount); -}; - -// Format large numbers with abbreviated units -export const formatNumber = (num: number) => { - if (num >= 1000000000) { // Billions - return (num / 1000000000).toFixed(1) + 'B'; - } - if (num >= 1000000) { // Millions - return (num / 1000000).toFixed(1) + 'M'; - } - if (num >= 1000) { // Thousands - return (num / 1000).toFixed(1) + 'K'; - } - return num.toString(); -}; - -// Get human-readable provider name -export const getProviderName = (provider: string): string => { - const providerMap: Record = { - openai: 'OpenAI', - anthropic: 'Anthropic', - google: 'Google', - xai: 'xAI', - openrouter: 'OpenRouter', - zai: 'Z.ai', - }; - - return providerMap[provider.toLowerCase()] || provider.charAt(0).toUpperCase() + provider.slice(1); -}; From 9578d4f37cd4a11ab4775deec81bdcb59d2d0b4a Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:39:12 +0530 Subject: [PATCH 45/78] Remove integration guides from README The Claude Code and Codex setup snippets reference stale docs that haven't been updated for the current architecture. --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index 3a24c1b..b278389 100644 --- a/README.md +++ b/README.md @@ -64,27 +64,6 @@ curl -X POST http://localhost:4010/v1/chat/completions \ curl http://localhost:4010/health ``` -## Integration Guides - -### 🤖 Claude Code - -```bash -export ANTHROPIC_BASE_URL="http://localhost:4010" -export ANTHROPIC_MODEL="anthropic/claude-sonnet-4-5" -claude -``` - -📖 **[Complete Claude Code Guide →](./docs/USAGE_WITH_CLAUDE_CODE.md)** - -### 💻 Codex - -```bash -export OPENAI_BASE_URL="http://localhost:4010/v1" -codex -``` - -📖 **[Complete Codex Guide →](./docs/USAGE_WITH_CODEX.md)** - ## Running Services ### npm (local development) From 8bdabf941a4fc31099961105c647a17aa21e9ff6 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:03:56 +0530 Subject: [PATCH 46/78] fix build issues --- integrations/openrouter/src/memory-client.ts | 3 +- package-lock.json | 2073 +----------------- 2 files changed, 56 insertions(+), 2020 deletions(-) diff --git a/integrations/openrouter/src/memory-client.ts b/integrations/openrouter/src/memory-client.ts index 5b9ab18..85df71e 100644 --- a/integrations/openrouter/src/memory-client.ts +++ b/integrations/openrouter/src/memory-client.ts @@ -32,14 +32,13 @@ export function initMemoryStore(s: SqliteMemoryStore): void { export async function fetchMemoryContext( query: string, profile: string, - userId?: string, ): Promise { if (!store) { console.warn('[memory] store not initialized'); return null; } try { - const data = await store.query(query, profile, userId); + const data = await store.query(query, profile); return data.workingMemory?.length ? data.workingMemory : null; } catch (err: any) { console.warn(`[memory] search failed: ${err.message}`); diff --git a/package-lock.json b/package-lock.json index e804753..d4b8704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "ekai-gateway", "version": "0.1.0-beta.1", "workspaces": [ - "gateway", "ui/dashboard", "memory", "integrations/openrouter" @@ -19,6 +18,7 @@ }, "gateway": { "version": "0.1.0-beta.1", + "extraneous": true, "dependencies": { "@types/js-yaml": "^4.0.9", "ajv": "^8.17.1", @@ -49,27 +49,6 @@ "vitest": "^1.0.4" } }, - "gateway/node_modules/x402": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/x402/-/x402-0.3.7.tgz", - "integrity": "sha512-8g2sXjWX7UbUNg9wJqgSBoYP7QV3/7qYYssdfPiQM5XDDThuVy7+MnH4cCuQ4UGGn2SVz1hpzWpwxMC3nwp+zA==", - "license": "Apache-2.0", - "dependencies": { - "viem": "^2.23.1", - "zod": "^3.24.2" - } - }, - "gateway/node_modules/x402-fetch": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/x402-fetch/-/x402-fetch-0.3.3.tgz", - "integrity": "sha512-gOVOm1gT+ViHY/oL8UHU6mC2DPDpC3pxBHaaAMcDGSb1myht26tYxYBPTKARuxXpyZEnZ4Us6+WDgNKZ5rze+A==", - "license": "Apache-2.0", - "dependencies": { - "viem": "^2.23.1", - "x402": "^0.3.3", - "zod": "^3.24.2" - } - }, "integrations/openrouter": { "name": "@ekai/openrouter", "version": "0.1.0", @@ -694,12 +673,6 @@ } } }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", - "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", - "license": "MIT" - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -713,73 +686,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", - "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -790,27 +696,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, "node_modules/@ekai/memory": { "resolved": "memory", "link": true @@ -1952,29 +1837,6 @@ "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2025,12 +1887,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2218,45 +2074,6 @@ "node": ">= 10" } }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2305,23 +2122,6 @@ "node": ">=12.4.0" } }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, "node_modules/@reactflow/background": { "version": "11.3.14", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", @@ -2772,49 +2572,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -3175,13 +2932,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -3504,16 +3254,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -3523,19 +3268,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", - "license": "MIT" - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -3610,29 +3342,6 @@ "@types/send": "*" } }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/supertest": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", - "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/superagent": "*" - } - }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -4166,49 +3875,6 @@ "win32" ] }, - "node_modules/@vitest/coverage-v8": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", - "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "1.6.1" - } - }, - "node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", @@ -4272,255 +3938,79 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">= 0.6" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.4.0" } }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=8" } }, - "node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^2.2.0" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@vitest/ui": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.6.1.tgz", - "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "fast-glob": "^3.3.2", - "fflate": "^0.8.1", - "flatted": "^3.2.9", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "sirv": "^2.0.4" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "1.6.1" - } + "license": "Python-2.0" }, - "node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/abitype": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz", - "integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4693,23 +4183,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4727,22 +4200,6 @@ "node": ">= 0.4" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5024,25 +4481,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5073,19 +4511,6 @@ "node": ">=8" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -5152,29 +4577,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5210,13 +4612,6 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -5253,81 +4648,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" - } - }, - "node_modules/copyfiles/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/copyfiles/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/copyfiles/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -5571,15 +4891,6 @@ "resolved": "ui/dashboard", "link": true }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5690,19 +5001,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -5755,16 +5053,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5793,27 +5081,6 @@ "node": ">=8" } }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -6642,30 +5909,6 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -6750,6 +5993,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -6796,38 +6040,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -6838,36 +6050,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6987,51 +6169,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/formidable": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", - "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7056,13 +6193,6 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -7118,10 +6248,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gateway": { - "resolved": "gateway", - "link": true - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7132,16 +6258,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -7179,19 +6295,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -7229,28 +6332,6 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7421,13 +6502,6 @@ "node": ">= 0.4" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -7444,16 +6518,6 @@ "node": ">= 0.8" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7533,18 +6597,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -7739,6 +6791,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7793,6 +6846,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7902,19 +6956,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -8026,88 +7067,6 @@ "dev": true, "license": "ISC" }, - "node_modules/isows": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", - "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -8147,6 +7106,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8162,35 +7122,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-to-typescript": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", - "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.5.5", - "@types/json-schema": "^7.0.15", - "@types/lodash": "^4.17.7", - "is-glob": "^4.0.3", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "prettier": "^3.2.5", - "tinyglobby": "^0.2.9" - }, - "bin": { - "json2ts": "dist/src/cli.js" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -8510,23 +7441,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8569,16 +7483,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -8589,34 +7493,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8644,13 +7520,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8717,19 +7586,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -8787,55 +7643,12 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8990,111 +7803,6 @@ "node": ">=10" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", - "dev": true, - "license": "ISC", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - } - }, - "node_modules/noms/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/noms/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/noms/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9216,15 +7924,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9246,22 +7945,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9298,36 +7981,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ox": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.6.tgz", - "integrity": "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.0.9", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9392,16 +8045,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -9425,23 +8068,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9461,62 +8087,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pino": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.11.0.tgz", - "integrity": "sha512-+YIodBB9sxcWeR8PrXC2K3gEDyfkUuVEITOcbqrfcj+z5QW4ioIcqZfYFbrLTYLsmAwunbS7nfU/dpBB6PZc1g==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "license": "MIT" - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9592,79 +8162,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9753,12 +8250,6 @@ ], "license": "MIT" }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9890,15 +8381,6 @@ "node": ">= 6" } }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, "node_modules/recharts": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", @@ -9995,15 +8477,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -10213,15 +8686,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10522,19 +8986,6 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -10580,30 +9031,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -10619,15 +9046,6 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -10832,19 +9250,6 @@ "node": ">=4" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10858,26 +9263,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -10901,57 +9286,6 @@ } } }, - "node_modules/superagent": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", - "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", - "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/supertest": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", - "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", - "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.1.2" - }, - "engines": { - "node": ">=6.4.0" - } - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -11053,81 +9387,6 @@ "node": ">=6" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -11152,6 +9411,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -11168,6 +9428,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -11185,6 +9446,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -11193,16 +9455,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tinyrainbow": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", @@ -11213,16 +9465,6 @@ "node": ">=14.0.0" } }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11245,16 +9487,6 @@ "node": ">=0.6" } }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -11342,16 +9574,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -11447,7 +9669,7 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11457,13 +9679,6 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -11534,16 +9749,6 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -11609,36 +9814,6 @@ "d3-timer": "^3.0.1" } }, - "node_modules/viem": { - "version": "2.38.4", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.38.4.tgz", - "integrity": "sha512-qnyPNg6Lz1EEC86si/1dq7GlOyZVFHSgAW+p8Q31R5idnAYCOdTM2q5KLE4/ykMeMXzY0bnp5MWTtR/wjCtWmQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.9.1", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.1.0", - "isows": "1.0.7", - "ox": "0.9.6", - "ws": "8.18.3" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/vite": { "version": "5.4.20", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", @@ -11699,29 +9874,6 @@ } } }, - "node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -12152,81 +10304,6 @@ "@esbuild/win32-x64": "0.21.5" } }, - "node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12383,37 +10460,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -12476,15 +10522,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", From 2db6ef71187a05c98b40244c2e9740ecdb95e75a Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 22 Feb 2026 00:38:20 +0530 Subject: [PATCH 47/78] Fix graph endpoints leaking user-scoped memories Graph queries (/v1/graph/*) ignored user_scope entirely, exposing user-specific facts to all users. Add userId filter to findTriplesBySubject and findTriplesByObject, and thread the optional userId query param through all 4 graph router endpoints. --- memory/src/router.ts | 18 +++-- memory/src/semantic-graph.ts | 150 +++++++++++++++++++---------------- memory/src/types.ts | 1 + 3 files changed, 93 insertions(+), 76 deletions(-) diff --git a/memory/src/router.ts b/memory/src/router.ts index e97ae22..7d8b052 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -310,7 +310,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { // Graph traversal endpoints router.get('/v1/graph/triples', (req: Request, res: Response) => { try { - const { entity, direction, maxResults, predicate, profile } = req.query; + const { entity, direction, maxResults, predicate, profile, userId } = req.query; if (!entity || typeof entity !== 'string') { return res.status(400).json({ error: 'entity query parameter is required' }); } @@ -319,6 +319,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { maxResults: maxResults ? Number(maxResults) : 100, predicateFilter: predicate as string | undefined, profile: profile as string, + userId: userId as string | undefined, }; let triples; @@ -341,12 +342,12 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/graph/neighbors', (req: Request, res: Response) => { try { - const { entity, profile } = req.query; + const { entity, profile, userId } = req.query; if (!entity || typeof entity !== 'string') { return res.status(400).json({ error: 'entity query parameter is required' }); } - const neighbors = Array.from(store.graph.findNeighbors(entity, { profile: profile as string })); + const neighbors = Array.from(store.graph.findNeighbors(entity, { profile: profile as string, userId: userId as string | undefined })); res.json({ entity, neighbors, count: neighbors.length }); } catch (err: any) { if (err?.message === 'invalid_profile') { @@ -358,7 +359,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/graph/paths', (req: Request, res: Response) => { try { - const { from, to, maxDepth, profile } = req.query; + const { from, to, maxDepth, profile, userId } = req.query; if (!from || typeof from !== 'string' || !to || typeof to !== 'string') { return res.status(400).json({ error: 'from and to query parameters are required' }); } @@ -366,6 +367,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { const paths = store.graph.findPaths(from, to, { maxDepth: maxDepth ? Number(maxDepth) : 3, profile: profile as string, + userId: userId as string | undefined, }); res.json({ from, to, paths, count: paths.length }); @@ -379,7 +381,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/graph/visualization', (req: Request, res: Response) => { try { - const { entity, maxDepth, maxNodes, profile, includeHistory } = req.query; + const { entity, maxDepth, maxNodes, profile, includeHistory, userId } = req.query; const profileValue = profile as string; const normalizedProfile = normalizeProfileSlug(profileValue); const centerEntity = (entity as string) || null; @@ -419,13 +421,13 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { } // Build graph from center entity - const graphOptions = { maxDepth: depth, profile: normalizedProfile, includeInvalidated: showHistory }; + const graphOptions = { maxDepth: depth, profile: normalizedProfile, includeInvalidated: showHistory, userId: userId as string | undefined }; const reachable = store.graph.findReachableEntities(centerEntity, graphOptions); const nodes = new Set([centerEntity]); const edges: Array<{ source: string; target: string; predicate: string; isHistorical?: boolean }> = []; // Add center entity's connections - const centerTriples = store.graph.findConnectedTriples(centerEntity, { maxResults: 100, profile: normalizedProfile, includeInvalidated: showHistory }); + const centerTriples = store.graph.findConnectedTriples(centerEntity, { maxResults: 100, profile: normalizedProfile, includeInvalidated: showHistory, userId: userId as string | undefined }); for (const triple of centerTriples) { nodes.add(triple.subject); nodes.add(triple.object); @@ -443,7 +445,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { .slice(0, nodeLimit - nodes.size); for (const [entity, _depth] of reachableArray) { - const entityTriples = store.graph.findTriplesBySubject(entity, { maxResults: 10, profile: normalizedProfile, includeInvalidated: showHistory }); + const entityTriples = store.graph.findTriplesBySubject(entity, { maxResults: 10, profile: normalizedProfile, includeInvalidated: showHistory, userId: userId as string | undefined }); for (const triple of entityTriples) { if (nodes.has(triple.subject) || nodes.has(triple.object)) { nodes.add(triple.subject); diff --git a/memory/src/semantic-graph.ts b/memory/src/semantic-graph.ts index 82dac41..717b082 100644 --- a/memory/src/semantic-graph.ts +++ b/memory/src/semantic-graph.ts @@ -1,6 +1,6 @@ -import Database from 'better-sqlite3'; -import { normalizeProfileSlug } from './utils.js'; -import type { SemanticMemoryRecord, GraphTraversalOptions, GraphPath } from './types.js'; +import Database from 'better-sqlite3'; +import { normalizeProfileSlug } from './utils.js'; +import type { SemanticMemoryRecord, GraphTraversalOptions, GraphPath } from './types.js'; /** * Graph traversal operations for semantic memory (RDF triples) @@ -18,33 +18,40 @@ export class SemanticGraphTraversal { /** * Find all triples where the given entity appears as subject (outgoing edges) */ - findTriplesBySubject( - subject: string, - options: GraphTraversalOptions & { profile?: string } = {}, - ): SemanticMemoryRecord[] { - const { maxResults = 100, includeInvalidated = false, predicateFilter } = options; - const profileId = normalizeProfileSlug(options.profile); - const now = this.now(); - - let query = `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, - created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId - from semantic_memory - where subject = @subject and profile_id = @profileId`; - + findTriplesBySubject( + subject: string, + options: GraphTraversalOptions & { profile?: string } = {}, + ): SemanticMemoryRecord[] { + const { maxResults = 100, includeInvalidated = false, predicateFilter, userId } = options; + const profileId = normalizeProfileSlug(options.profile); + const now = this.now(); + + let query = `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, + created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId + from semantic_memory + where subject = @subject and profile_id = @profileId`; + if (!includeInvalidated) { query += ` and (valid_to is null or valid_to > @now)`; } - + if (predicateFilter) { query += ` and predicate = @predicateFilter`; } - + + if (userId) { + query += ` and (user_scope is null or user_scope = @userId)`; + } + query += ` order by updated_at desc limit @maxResults`; - const params: Record = { subject, now, maxResults, profileId }; - if (predicateFilter) { - params.predicateFilter = predicateFilter; - } + const params: Record = { subject, now, maxResults, profileId }; + if (predicateFilter) { + params.predicateFilter = predicateFilter; + } + if (userId) { + params.userId = userId; + } const rows = this.db .prepare(query) @@ -60,33 +67,40 @@ export class SemanticGraphTraversal { /** * Find all triples where the given entity appears as object (incoming edges) */ - findTriplesByObject( - object: string, - options: GraphTraversalOptions & { profile?: string } = {}, - ): SemanticMemoryRecord[] { - const { maxResults = 100, includeInvalidated = false, predicateFilter } = options; - const profileId = normalizeProfileSlug(options.profile); - const now = this.now(); - - let query = `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, - created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId - from semantic_memory - where object = @object and profile_id = @profileId`; - + findTriplesByObject( + object: string, + options: GraphTraversalOptions & { profile?: string } = {}, + ): SemanticMemoryRecord[] { + const { maxResults = 100, includeInvalidated = false, predicateFilter, userId } = options; + const profileId = normalizeProfileSlug(options.profile); + const now = this.now(); + + let query = `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, + created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId + from semantic_memory + where object = @object and profile_id = @profileId`; + if (!includeInvalidated) { query += ` and (valid_to is null or valid_to > @now)`; } - + if (predicateFilter) { query += ` and predicate = @predicateFilter`; } - + + if (userId) { + query += ` and (user_scope is null or user_scope = @userId)`; + } + query += ` order by updated_at desc limit @maxResults`; - const params: Record = { object, now, maxResults, profileId }; - if (predicateFilter) { - params.predicateFilter = predicateFilter; - } + const params: Record = { object, now, maxResults, profileId }; + if (predicateFilter) { + params.predicateFilter = predicateFilter; + } + if (userId) { + params.userId = userId; + } const rows = this.db .prepare(query) @@ -102,12 +116,12 @@ export class SemanticGraphTraversal { /** * Find all triples connected to an entity (both as subject and object) */ - findConnectedTriples( - entity: string, - options: GraphTraversalOptions & { profile?: string } = {}, - ): SemanticMemoryRecord[] { - const outgoing = this.findTriplesBySubject(entity, options); - const incoming = this.findTriplesByObject(entity, options); + findConnectedTriples( + entity: string, + options: GraphTraversalOptions & { profile?: string } = {}, + ): SemanticMemoryRecord[] { + const outgoing = this.findTriplesBySubject(entity, options); + const incoming = this.findTriplesByObject(entity, options); // Deduplicate by id const seen = new Set(); @@ -126,11 +140,11 @@ export class SemanticGraphTraversal { /** * Find all entities connected to a given entity (neighbors) */ - findNeighbors( - entity: string, - options: GraphTraversalOptions & { profile?: string } = {}, - ): Set { - const triples = this.findConnectedTriples(entity, options); + findNeighbors( + entity: string, + options: GraphTraversalOptions & { profile?: string } = {}, + ): Set { + const triples = this.findConnectedTriples(entity, options); const neighbors = new Set(); for (const triple of triples) { @@ -148,12 +162,12 @@ export class SemanticGraphTraversal { /** * Find paths between two entities using breadth-first search */ - findPaths( - fromEntity: string, - toEntity: string, - options: GraphTraversalOptions & { profile?: string } = {}, - ): GraphPath[] { - const { maxDepth = 3 } = options; + findPaths( + fromEntity: string, + toEntity: string, + options: GraphTraversalOptions & { profile?: string } = {}, + ): GraphPath[] { + const { maxDepth = 3 } = options; if (fromEntity === toEntity) { return []; @@ -173,11 +187,11 @@ export class SemanticGraphTraversal { continue; } - // Find all outgoing edges from current entity - const outgoing = this.findTriplesBySubject(entity, { - ...options, - maxResults: 100, - }); + // Find all outgoing edges from current entity + const outgoing = this.findTriplesBySubject(entity, { + ...options, + maxResults: 100, + }); for (const triple of outgoing) { // Skip if already in path (avoid cycles) @@ -203,11 +217,11 @@ export class SemanticGraphTraversal { /** * Find all entities reachable from a given entity within maxDepth steps */ - findReachableEntities( - entity: string, - options: GraphTraversalOptions & { profile?: string } = {}, - ): Map { - const { maxDepth = 2 } = options; + findReachableEntities( + entity: string, + options: GraphTraversalOptions & { profile?: string } = {}, + ): Map { + const { maxDepth = 2 } = options; const reachable = new Map(); const queue: Array<{ entity: string; depth: number }> = [{ entity, depth: 0 }]; const visited = new Set([entity]); diff --git a/memory/src/types.ts b/memory/src/types.ts index fb514ec..1c5de74 100644 --- a/memory/src/types.ts +++ b/memory/src/types.ts @@ -129,6 +129,7 @@ export interface GraphTraversalOptions { includeInvalidated?: boolean; predicateFilter?: string; profile?: string; + userId?: string; } export interface GraphPath { From bc987239e6d26e2131ff56edc5c6164aff381ad0 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:07:16 +0530 Subject: [PATCH 48/78] Add user-scope filtering to dashboard - Add user_scope to getRecent() SQL and summary endpoint mapping - Add UserFilter dropdown component with user list from GET /v1/users - Wire up client-side filtering: logs show user-scoped + global memories, graph passes userId param, overview recomputes stats for filtered view - Add scope badges (@ userId / shared) to memory log rows - Fix stale test expecting removed reflective sector in perSector --- memory/src/router.ts | 1 + memory/src/sqlite-store.ts | 6 +- memory/tests/store.test.ts | 4 +- ui/dashboard/src/app/memory/page.tsx | 68 +++++-- .../src/components/memory/SemanticGraph.tsx | 7 +- .../src/components/memory/UserFilter.tsx | 175 ++++++++++++++++++ ui/dashboard/src/lib/api.ts | 13 +- 7 files changed, 248 insertions(+), 26 deletions(-) create mode 100644 ui/dashboard/src/components/memory/UserFilter.tsx diff --git a/memory/src/router.ts b/memory/src/router.ts index 7d8b052..39195c8 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -144,6 +144,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { preview: r.content, retrievalCount: (r as any).retrievalCount ?? 0, details: (r as any).details, + userScope: (r as any).userScope ?? null, })); res.json({ summary, recent, profile: normalizedProfile }); } catch (err: any) { diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index e2a71da..2fd2114 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -357,19 +357,19 @@ export class SqliteMemoryStore { this.ensureProfileExists(profileId); const rows = this.db .prepare( - `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount + `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, user_scope as userScope from memory where profile_id = @profileId union all select id, 'procedural' as sector, trigger as content, embedding, created_at as createdAt, last_accessed as lastAccessed, json_object('trigger', trigger, 'goal', goal, 'context', context, 'result', result, 'steps', json(steps)) as details, - null as eventStart, null as eventEnd, 0 as retrievalCount + null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope from procedural_memory where profile_id = @profileId union all select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'metadata', metadata, 'domain', domain) as details, - null as eventStart, null as eventEnd, 0 as retrievalCount + null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope from semantic_memory where profile_id = @profileId and (valid_to is null or valid_to > @now) diff --git a/memory/tests/store.test.ts b/memory/tests/store.test.ts index 30232d8..9869590 100644 --- a/memory/tests/store.test.ts +++ b/memory/tests/store.test.ts @@ -115,8 +115,8 @@ describe('SqliteMemoryStore (single-user mode)', () => { expect(result).toHaveProperty('profileId'); expect(Array.isArray(result.workingMemory)).toBe(true); - // perSector has all 4 sector keys - for (const sector of ['episodic', 'semantic', 'procedural', 'reflective'] as SectorName[]) { + // perSector has all 3 active sector keys (reflective is disabled) + for (const sector of ['episodic', 'semantic', 'procedural'] as SectorName[]) { expect(result.perSector).toHaveProperty(sector); expect(Array.isArray(result.perSector[sector])).toBe(true); } diff --git a/ui/dashboard/src/app/memory/page.tsx b/ui/dashboard/src/app/memory/page.tsx index d7f25b1..2d695f4 100644 --- a/ui/dashboard/src/app/memory/page.tsx +++ b/ui/dashboard/src/app/memory/page.tsx @@ -14,6 +14,7 @@ import { ActivityDistribution } from '@/components/memory/ActivityDistribution'; import { MemoryStrength } from '@/components/memory/MemoryStrength'; import { SemanticGraph } from '@/components/memory/SemanticGraph'; import ProfileSelector from '@/components/memory/ProfileSelector'; +import UserFilter from '@/components/memory/UserFilter'; import ProfileManagement from '@/components/memory/ProfileManagement'; import ProfileStats from '@/components/memory/ProfileStats'; import ProfileBadge from '@/components/memory/ProfileBadge'; @@ -35,6 +36,7 @@ export default function MemoryVaultPage() { const [editModal, setEditModal] = useState<{ id: string; content: string; sector: string } | null>(null); const [editBusy, setEditBusy] = useState(false); const [currentProfile, setCurrentProfile] = useState('default'); + const [selectedUserId, setSelectedUserId] = useState(null); const [profileResolved, setProfileResolved] = useState(false); const [showProfileManagement, setShowProfileManagement] = useState(false); const [profileSwitching, setProfileSwitching] = useState(false); @@ -80,6 +82,7 @@ export default function MemoryVaultPage() { const handleProfileChange = (profileSlug: string) => { setProfileSwitching(true); setCurrentProfile(profileSlug); + setSelectedUserId(null); setSearchTerm(''); setFilterSector('all'); setExpandedId(null); @@ -173,19 +176,34 @@ export default function MemoryVaultPage() { if (item.sector === 'semantic' || item.sector === 'reflective') return false; const matchesSearch = !searchTerm || item.preview.toLowerCase().includes(searchTerm.toLowerCase()); const matchesSector = filterSector === 'all' || item.sector === filterSector; - return matchesSearch && matchesSector; + const matchesUser = !selectedUserId || item.userScope == null || item.userScope === selectedUserId; + return matchesSearch && matchesSector && matchesUser; }); - }, [data, searchTerm, filterSector]); + }, [data, searchTerm, filterSector, selectedUserId]); - // Filter out reflective memories from display data + // Filter out reflective memories from display data, and apply user scope filter const displayData = useMemo(() => { if (!data) return null; + const filtered = data.recent?.filter(r => { + if (r.sector === 'reflective') return false; + if (selectedUserId && r.userScope != null && r.userScope !== selectedUserId) return false; + return true; + }); + // Recompute summary counts from filtered recent items when user filter is active + const summary = selectedUserId + ? data.summary + .filter(s => s.sector !== 'reflective') + .map(s => ({ + ...s, + count: filtered?.filter(r => r.sector === s.sector).length ?? 0, + })) + : data.summary.filter(s => s.sector !== 'reflective'); return { ...data, - summary: data.summary.filter(s => s.sector !== 'reflective'), - recent: data.recent?.filter(r => r.sector !== 'reflective'), + summary, + recent: filtered, }; - }, [data]); + }, [data, selectedUserId]); // Calculate quick stats const quickStats = useMemo(() => { @@ -253,6 +271,11 @@ export default function MemoryVaultPage() { onProfileChange={handleProfileChange} onManageProfiles={() => setShowProfileManagement(true)} /> + + + {isOpen && ( +
+ {/* Header */} +
+

Filter by User

+
+ +
+ {/* All Users option */} + + + {/* User list */} + {users.map((user) => ( + + ))} +
+ + {loading && ( +
+
Loading users...
+
+ )} +
+ )} +
+ ); +} diff --git a/ui/dashboard/src/lib/api.ts b/ui/dashboard/src/lib/api.ts index 4c4f4f1..527afe2 100644 --- a/ui/dashboard/src/lib/api.ts +++ b/ui/dashboard/src/lib/api.ts @@ -15,12 +15,14 @@ export interface MemoryRecentItem { lastAccessed: number; preview: string; retrievalCount?: number; + userScope?: string | null; details?: { trigger?: string; goal?: string; context?: string; result?: string; steps?: string[]; + domain?: string; }; } @@ -97,7 +99,7 @@ export const apiService = { }, // Graph traversal APIs - async getGraphVisualization(params?: { entity?: string; maxDepth?: number; maxNodes?: number; profile?: string; includeHistory?: boolean }): Promise<{ + async getGraphVisualization(params?: { entity?: string; maxDepth?: number; maxNodes?: number; profile?: string; includeHistory?: boolean; userId?: string }): Promise<{ center?: string; nodes: Array<{ id: string; label: string }>; edges: Array<{ source: string; target: string; predicate: string; isHistorical?: boolean }>; @@ -109,6 +111,7 @@ export const apiService = { if (params?.maxNodes) searchParams.append('maxNodes', String(params.maxNodes)); if (params?.profile) searchParams.append('profile', params.profile); if (params?.includeHistory) searchParams.append('includeHistory', 'true'); + if (params?.userId) searchParams.append('userId', params.userId); const url = `${MEMORY_BASE_URL}/v1/graph/visualization${searchParams.toString() ? `?${searchParams.toString()}` : ''}`; const response = await fetch(url); @@ -178,6 +181,14 @@ export const apiService = { return response.json(); }, + async getUsers(profile: string): Promise<{ users: Array<{ userId: string; firstSeen: number; lastSeen: number; interactionCount: number }>; profile: string }> { + const response = await fetch(`${MEMORY_BASE_URL}/v1/users?profile=${encodeURIComponent(profile)}`); + if (!response.ok) { + throw new Error(`Failed to fetch users: ${response.statusText}`); + } + return response.json(); + }, + async deleteProfile(profile: string): Promise<{ deleted: number; profile: string }> { const response = await fetch(`${MEMORY_BASE_URL}/v1/profiles/${encodeURIComponent(profile)}`, { method: 'DELETE', From 33bc0420cf31f87cf2380c406c4aef9112b10d21 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:46:22 +0530 Subject: [PATCH 49/78] updated package.json --- memory/package.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/memory/package.json b/memory/package.json index 4965289..30b9287 100644 --- a/memory/package.json +++ b/memory/package.json @@ -2,14 +2,23 @@ "name": "@ekai/memory", "version": "0.1.0", "description": "Neuroscience-inspired cognitive memory kernel", - "private": true, "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], "scripts": { "build": "tsc -p tsconfig.json", "type-check": "tsc --noEmit", "test": "vitest run", + "prepublishOnly": "npm run build", "prestart": "npm run build", "start": "node dist/server.js" }, From 371df4be87320b2f960daf6d1bd8c1e4801aa5a8 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:11:52 +0530 Subject: [PATCH 50/78] Agent-centric Memory SDK with provider config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename profile→agent across the entire memory system. Agents are now first-class entities with id, name, and soul_md. Provider config (apiKey, model) is passed via Memory constructor instead of env vars. - DB schema: profile_id→agent_id, profiles→agents table (clean, no migrations) - SDK: Memory({ provider, apiKey, agent }) with scoped data ops - Factories: createEmbedFn/createExtractFn for explicit provider config - Router: /v1/profiles→/v1/agents, all profile params→agent - OpenRouter integration: agent from env, body.user→userId - Tests: 15 passing (5 new for agent CRUD) --- integrations/openrouter/src/memory-client.ts | 48 +- integrations/openrouter/src/server.ts | 29 +- memory/README.md | 115 +++-- memory/src/documents.ts | 17 +- memory/src/index.ts | 8 +- memory/src/memory.ts | 143 ++++++ memory/src/providers/embed.ts | 24 +- memory/src/providers/extract.ts | 25 +- memory/src/providers/registry.ts | 8 +- memory/src/router.ts | 224 ++++---- memory/src/scoring.ts | 2 +- memory/src/semantic-graph.ts | 47 +- memory/src/server.ts | 20 +- memory/src/sqlite-store.ts | 509 +++++++++---------- memory/src/types.ts | 29 +- memory/src/utils.ts | 12 +- memory/tests/store.test.ts | 87 +++- 17 files changed, 804 insertions(+), 543 deletions(-) create mode 100644 memory/src/memory.ts diff --git a/integrations/openrouter/src/memory-client.ts b/integrations/openrouter/src/memory-client.ts index 85df71e..d4f935c 100644 --- a/integrations/openrouter/src/memory-client.ts +++ b/integrations/openrouter/src/memory-client.ts @@ -1,5 +1,4 @@ -import type { SqliteMemoryStore } from '@ekai/memory'; -import { extract } from '@ekai/memory'; +import type { Memory } from '@ekai/memory'; interface QueryResult { sector: 'episodic' | 'semantic' | 'procedural' | 'reflective'; @@ -16,13 +15,13 @@ interface QueryResult { }; } -let store: SqliteMemoryStore | null = null; +let memory: Memory | null = null; /** - * Initialize the memory store reference. Called once at startup. + * Initialize the Memory instance. Called once at startup. */ -export function initMemoryStore(s: SqliteMemoryStore): void { - store = s; +export function initMemory(m: Memory): void { + memory = m; } /** @@ -31,15 +30,15 @@ export function initMemoryStore(s: SqliteMemoryStore): void { */ export async function fetchMemoryContext( query: string, - profile: string, + userId?: string, ): Promise { - if (!store) { - console.warn('[memory] store not initialized'); + if (!memory) { + console.warn('[memory] not initialized'); return null; } try { - const data = await store.query(query, profile); - return data.workingMemory?.length ? data.workingMemory : null; + const results = await memory.search(query, { userId }); + return results.length ? results : null; } catch (err: any) { console.warn(`[memory] search failed: ${err.message}`); return null; @@ -52,29 +51,14 @@ export async function fetchMemoryContext( */ export function ingestMessages( messages: Array<{ role: string; content: string }>, - profile: string, + userId?: string, ): void { - if (!store) { - console.warn('[memory] store not initialized, skipping ingest'); + if (!memory) { + console.warn('[memory] not initialized, skipping ingest'); return; } - const allMessages = messages.filter((m) => m.content?.trim()); - const sourceText = allMessages - .map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content.trim()}`) - .join('\n\n'); - - if (!sourceText) return; - - // Fire-and-forget: extract then ingest - extract(sourceText) - .then((components) => { - if (!components || !store) return; - return store.ingest(components, profile, { - origin: { originType: 'conversation' }, - }); - }) - .catch((err) => { - console.warn(`[memory] ingest failed: ${err.message}`); - }); + memory.add(messages, { userId }).catch((err) => { + console.warn(`[memory] ingest failed: ${err.message}`); + }); } diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts index bc11cef..bda52c9 100644 --- a/integrations/openrouter/src/server.ts +++ b/integrations/openrouter/src/server.ts @@ -3,9 +3,9 @@ import cors from 'cors'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; -import { SqliteMemoryStore, embed, createMemoryRouter } from '@ekai/memory'; -import { PORT, MEMORY_DB_PATH } from './config.js'; -import { initMemoryStore, fetchMemoryContext, ingestMessages } from './memory-client.js'; +import { Memory, createMemoryRouter } from '@ekai/memory'; +import { PORT, MEMORY_DB_PATH, OPENROUTER_API_KEY } from './config.js'; +import { initMemory, fetchMemoryContext, ingestMessages } from './memory-client.js'; import { formatMemoryBlock, injectMemory } from './memory.js'; import { proxyToOpenRouter } from './proxy.js'; @@ -19,15 +19,18 @@ app.use(cors({ origin: corsOrigins })); app.options('*', cors({ origin: corsOrigins })); app.use(express.json({ limit: '10mb' })); -// Initialize embedded memory store -const store = new SqliteMemoryStore({ +// Initialize embedded memory with explicit provider config +const agentId = process.env.MEMORY_AGENT ?? 'default'; +const memory = new Memory({ + provider: 'openrouter', + apiKey: OPENROUTER_API_KEY, dbPath: MEMORY_DB_PATH, - embed, + agent: agentId, }); -initMemoryStore(store); +initMemory(memory); // Mount memory admin routes (dashboard, graph, etc.) -app.use(createMemoryRouter(store)); +app.use(createMemoryRouter(memory._store, memory._extractFn)); app.get('/health', (_req, res) => { res.json({ status: 'ok' }); @@ -36,8 +39,8 @@ app.get('/health', (_req, res) => { app.post('/v1/chat/completions', async (req, res) => { try { const body = req.body; - // Profile from body.user (PydanticAI openai_user), header, or default - const profile = body.user || (req.headers['x-memory-profile'] as string) || 'default'; + // userId from body.user (PydanticAI openai_user), header, or undefined + const userId = body.user || (req.headers['x-memory-user'] as string) || undefined; // Pass through client's API key if provided, otherwise proxy.ts falls back to env const authHeader = req.headers['authorization'] as string | undefined; const clientKey = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : undefined; @@ -61,7 +64,7 @@ app.post('/v1/chat/completions', async (req, res) => { // Fetch memory context (non-blocking on failure) if (query) { - const results = await fetchMemoryContext(query, profile); + const results = await fetchMemoryContext(query, userId); if (results) { const block = formatMemoryBlock(results); body.messages = injectMemory(body.messages, block); @@ -69,7 +72,7 @@ app.post('/v1/chat/completions', async (req, res) => { } // Fire-and-forget: ingest original messages for future recall - ingestMessages(originalMessages, profile); + ingestMessages(originalMessages, userId); await proxyToOpenRouter(body, res, clientKey); } catch (err: any) { @@ -109,5 +112,5 @@ if (fs.existsSync(DASHBOARD_DIR)) { } app.listen(PORT, () => { - console.log(`@ekai/openrouter listening on port ${PORT} (memory embedded, db at ${MEMORY_DB_PATH})`); + console.log(`@ekai/openrouter listening on port ${PORT} (memory embedded, agent=${agentId}, db at ${MEMORY_DB_PATH})`); }); diff --git a/memory/README.md b/memory/README.md index d27d5dc..28e747c 100644 --- a/memory/README.md +++ b/memory/README.md @@ -1,4 +1,4 @@ -# Memory Service (Ekai) +# Memory SDK (Ekai) Neuroscience-inspired, agent-centric memory kernel. Sectorized storage with PBWM gating — the agent reflects on conversations and decides what to learn. Memory is first-person, not a passive database about users. @@ -11,26 +11,63 @@ npm install -w @ekai/memory npm run build -w @ekai/memory ``` -See [Usage Modes](#usage-modes) below for direct import, mountable router, and standalone options. +See [Usage Modes](#usage-modes) below for SDK, mountable router, and standalone options. -Env (root `.env` or `memory/.env`): +### SDK Usage + +```ts +import { Memory } from '@ekai/memory'; + +// Setup — provider config is explicit, no env vars needed +const mem = new Memory({ provider: 'openai', apiKey: 'sk-...' }); +mem.addAgent('my-bot', { name: 'My Bot', soul: 'You are helpful' }); + +// Scoped instance — all data ops are agent-scoped +const bot = new Memory({ provider: 'openai', apiKey: 'sk-...', agent: 'my-bot' }); +await bot.add(messages, { userId: 'alice' }); +await bot.search('preferences', { userId: 'alice' }); +bot.users(); // agent's known users +bot.memories(); // all agent memories +bot.memories({ userId: 'alice' }); // memories about alice +bot.memories({ scope: 'global' }); // non-user-scoped memories +bot.delete(id); +``` + +### Environment Variables | Variable | Default | Required | |----------|---------|----------| -| `GOOGLE_API_KEY` | — | Yes | -| `GEMINI_EXTRACT_MODEL` | `gemini-2.5-flash` | No | -| `GEMINI_EMBED_MODEL` | `gemini-embedding-001` | No | +| `GOOGLE_API_KEY` | — | Yes (if using Gemini provider) | +| `OPENAI_API_KEY` | — | Yes (if using OpenAI provider) | +| `OPENROUTER_API_KEY` | — | Yes (if using OpenRouter provider) | +| `MEMORY_EMBED_PROVIDER` | `gemini` | No | +| `MEMORY_EXTRACT_PROVIDER` | `gemini` | No | | `MEMORY_DB_PATH` | `./memory.db` | No | | `MEMORY_CORS_ORIGIN` | `*` | No (standalone mode only) | ## Usage Modes -### 1. Direct import - -Use the memory store and embedding functions directly in your code: +### 1. SDK (recommended) ```ts -import { SqliteMemoryStore, embed } from '@ekai/memory'; +import { Memory } from '@ekai/memory'; + +const mem = new Memory({ + provider: 'openai', + apiKey: 'sk-...', + agent: 'my-bot', +}); + +// Management (no agent scope needed) +mem.addAgent('my-bot', { name: 'My Bot', soul: 'You are helpful' }); +mem.getAgents(); + +// Data ops (require agent scope) +await mem.add(messages, { userId: 'alice' }); +await mem.search('query', { userId: 'alice' }); +mem.users(); +mem.memories({ userId: 'alice' }); +mem.delete(id); ``` ### 2. Mountable router @@ -38,10 +75,10 @@ import { SqliteMemoryStore, embed } from '@ekai/memory'; Mount memory endpoints into an existing Express app: ```ts -import { createMemoryRouter } from '@ekai/memory'; +import { Memory, createMemoryRouter } from '@ekai/memory'; -const memoryRouter = createMemoryRouter(); -app.use(memoryRouter); +const memory = new Memory({ provider: 'openai', apiKey: 'sk-...' }); +app.use(createMemoryRouter(memory._store, memory._extractFn)); ``` This is how the OpenRouter integration embeds memory on port `4010`. @@ -145,12 +182,12 @@ Ingest a conversation. Full conversation (user + assistant) goes to the LLM for { "role": "user", "content": "I prefer dark mode and use TypeScript" }, { "role": "assistant", "content": "Noted!" } ], - "profile": "my-agent", + "agent": "my-bot", "userId": "sha" } ``` ```json -{ "stored": 3, "ids": ["...", "...", "..."], "profile": "my-agent" } +{ "stored": 3, "ids": ["...", "...", "..."], "agent": "my-bot" } ``` ### `POST /v1/search` @@ -158,15 +195,15 @@ Ingest a conversation. Full conversation (user + assistant) goes to the LLM for Search with PBWM gating. Pass `userId` for user-scoped retrieval. ```json -{ "query": "what does Sha prefer?", "profile": "my-agent", "userId": "sha" } +{ "query": "what does Sha prefer?", "agent": "my-bot", "userId": "sha" } ``` ```json { "workingMemory": [ { "sector": "semantic", "content": "Sha prefers dark mode", "score": 0.87, "details": { "subject": "Sha", "predicate": "prefers", "object": "dark mode", "domain": "user" } } ], - "perSector": { "episodic": [], "semantic": [...], "procedural": [], "reflective": [] }, - "profileId": "my-agent" + "perSector": { "episodic": [], "semantic": [...], "procedural": [] }, + "agentId": "my-bot" } ``` @@ -175,18 +212,17 @@ Search with PBWM gating. Pass `userId` for user-scoped retrieval. Per-sector counts + recent memories. ``` -GET /v1/summary?profile=my-agent&limit=20 +GET /v1/summary?agent=my-bot&limit=20 ``` ```json { "summary": [ { "sector": "episodic", "count": 3, "lastCreatedAt": 1700000000 }, { "sector": "semantic", "count": 12, "lastCreatedAt": 1700100000 }, - { "sector": "procedural", "count": 1, "lastCreatedAt": 1700050000 }, - { "sector": "reflective", "count": 2, "lastCreatedAt": 1700090000 } + { "sector": "procedural", "count": 1, "lastCreatedAt": 1700050000 } ], "recent": [{ "id": "...", "sector": "semantic", "preview": "dark mode", "details": {...} }], - "profile": "my-agent" + "agent": "my-bot" } ``` @@ -195,7 +231,7 @@ GET /v1/summary?profile=my-agent&limit=20 Ingest markdown files from a directory with deduplication. ```json -{ "path": "/path/to/docs", "profile": "project-x" } +{ "path": "/path/to/docs", "agent": "my-bot" } ``` ### `GET /v1/users` @@ -203,7 +239,7 @@ Ingest markdown files from a directory with deduplication. List all users the agent has interacted with. ``` -GET /v1/users?profile=my-agent +GET /v1/users?agent=my-bot ``` ```json { @@ -225,11 +261,11 @@ Get all memories scoped to a specific user. | GET | `/v1/summary` | Sector counts + recent | | GET | `/v1/users` | List agent's users | | GET | `/v1/users/:id/memories` | User-scoped memories | -| GET | `/v1/profiles` | List profiles | +| GET | `/v1/agents` | List agents | | PUT | `/v1/memory/:id` | Update a memory | | DELETE | `/v1/memory/:id` | Delete one memory | -| DELETE | `/v1/memory` | Delete all for profile | -| DELETE | `/v1/profiles/:slug` | Delete profile + memories | +| DELETE | `/v1/memory` | Delete all for agent | +| DELETE | `/v1/agents/:slug` | Delete agent + memories | | GET | `/v1/graph/triples` | Query semantic triples | | GET | `/v1/graph/neighbors` | Entity neighbors | | GET | `/v1/graph/paths` | Paths between entities | @@ -237,7 +273,7 @@ Get all memories scoped to a specific user. | DELETE | `/v1/graph/triple/:id` | Delete a triple | | GET | `/health` | Health check | -All endpoints support `profile` query/body param. In the default deployment, these are served on the OpenRouter port (`4010`). +All endpoints support `agent` query/body param. In the default deployment, these are served on the OpenRouter port (`4010`). ## Retrieval Pipeline @@ -249,7 +285,7 @@ graph LR classDef o fill:#e8f5e9,stroke:#388e3c,stroke-width:2px Q["Query + userId"]:::i - EMB["4 embeddings
(per sector)"]:::p + EMB["3 embeddings
(per sector)"]:::p F["cosine >= 0.2
+ user_scope"]:::e G["PBWM gate
sigmoid(1.0r + 0.4e + 0.05c - 0.02n)"]:::e W["Working Memory
top-4/sector, cap 8"]:::o @@ -257,27 +293,25 @@ graph LR Q --> EMB --> F --> G --> W ``` -Sector weights: episodic `1.0`, semantic `1.0`, procedural `1.0`, reflective `0.8`. - ## Data Model ```mermaid erDiagram - profiles ||--o{ memory : has - profiles ||--o{ semantic_memory : has - profiles ||--o{ procedural_memory : has - profiles ||--o{ reflective_memory : has - profiles ||--o{ agent_users : has + agents ||--o{ memory : has + agents ||--o{ semantic_memory : has + agents ||--o{ procedural_memory : has + agents ||--o{ reflective_memory : has + agents ||--o{ agent_users : has memory { text id PK; text sector; text content; text user_scope; text origin_type } semantic_memory { text id PK; text subject; text predicate; text object; text domain; text user_scope; real strength } procedural_memory { text id PK; text trigger; json steps; text user_scope; text origin_type } reflective_memory { text id PK; text observation; text origin_type; text origin_actor } agent_users { text agent_id PK; text user_id PK; int interaction_count } - profiles { text slug PK; int created_at } + agents { text id PK; text name; text soul_md; int created_at } ``` -All tables share: `embedding`, `created_at`, `last_accessed`, `profile_id`, `source`, `origin_type`, `origin_actor`, `origin_ref`. Schema auto-upgrades on startup. +All tables share: `embedding`, `created_at`, `last_accessed`, `agent_id`, `source`, `origin_type`, `origin_actor`, `origin_ref`. Clean schema — no migrations, old DBs re-create. ## Integration @@ -301,7 +335,8 @@ My observations: ## Notes -- Supports Gemini and OpenAI-compatible APIs for extraction/embedding +- Supports Gemini, OpenAI, and OpenRouter providers for extraction/embedding +- Provider config via constructor: `new Memory({ provider: 'openai', apiKey: '...' })` +- Agents are first-class: `addAgent()` required before data ops (auto-created for `default`) - `user_scope` is opt-in — no `userId` = all memories returned -- Schema migrations are additive — existing DBs auto-upgrade, no manual steps - Reflective weight `0.8` is a tuning knob diff --git a/memory/src/documents.ts b/memory/src/documents.ts index 805d0a4..8d64d1b 100644 --- a/memory/src/documents.ts +++ b/memory/src/documents.ts @@ -1,8 +1,9 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import type { SqliteMemoryStore } from './sqlite-store.js'; -import { extract } from './providers/extract.js'; -import { normalizeProfileSlug } from './utils.js'; +import type { ExtractFn } from './types.js'; +import { extract as defaultExtract } from './providers/extract.js'; +import { normalizeAgentId } from './utils.js'; const MAX_CHUNK_CHARS = 12_000; @@ -18,7 +19,7 @@ export interface IngestDocumentsResult { stored: number; skipped: number; errors: string[]; - profile: string; + agent: string; } /** @@ -132,11 +133,13 @@ async function collectMarkdownFiles(dirPath: string): Promise { export async function ingestDocuments( dirPath: string, store: SqliteMemoryStore, - profile?: string, + agent?: string, + extractFn?: ExtractFn, ): Promise { const resolvedPath = path.resolve(dirPath); const files = await collectMarkdownFiles(resolvedPath); const basePath = (await fs.stat(resolvedPath)).isDirectory() ? resolvedPath : path.dirname(resolvedPath); + const doExtract = extractFn ?? defaultExtract; let totalChunks = 0; let totalStored = 0; @@ -153,13 +156,13 @@ export async function ingestDocuments( for (const chunk of chunks) { try { - const components = await extract(chunk.text); + const components = await doExtract(chunk.text); if (!components) { totalSkipped++; continue; } - const rows = await store.ingest(components, profile, { + const rows = await store.ingest(components, agent, { source: chunk.source, deduplicate: true, }); @@ -181,6 +184,6 @@ export async function ingestDocuments( stored: totalStored, skipped: totalSkipped, errors, - profile: normalizeProfileSlug(profile), + agent: normalizeAgentId(agent), }; } diff --git a/memory/src/index.ts b/memory/src/index.ts index 5ffb933..f8ae5f6 100644 --- a/memory/src/index.ts +++ b/memory/src/index.ts @@ -1,10 +1,14 @@ export * from './types.js'; export * from './sqlite-store.js'; -export * from './providers/embed.js'; -export * from './providers/extract.js'; +export { embed, createEmbedFn } from './providers/embed.js'; +export { extract, createExtractFn } from './providers/extract.js'; export * from './providers/prompt.js'; +export { PROVIDERS } from './providers/registry.js'; +export type { ProviderConfig } from './providers/registry.js'; export * from './scoring.js'; export * from './wm.js'; export * from './utils.js'; export * from './documents.js'; export * from './router.js'; +export { Memory } from './memory.js'; +export type { MemoryConfig } from './memory.js'; diff --git a/memory/src/memory.ts b/memory/src/memory.ts new file mode 100644 index 0000000..f699951 --- /dev/null +++ b/memory/src/memory.ts @@ -0,0 +1,143 @@ +import { SqliteMemoryStore } from './sqlite-store.js'; +import { embed as defaultEmbed, createEmbedFn } from './providers/embed.js'; +import { extract as defaultExtract, createExtractFn } from './providers/extract.js'; +import type { + AgentInfo, + ExtractFn, + MemoryFilterOptions, + MemoryRecord, + ProviderName, + QueryResult, +} from './types.js'; + +export interface MemoryConfig { + provider?: ProviderName; + apiKey?: string; + dbPath?: string; + agent?: string; // scopes all data ops; omit for management-only + embedModel?: string; + extractModel?: string; +} + +export class Memory { + private store: SqliteMemoryStore; + private extractFn: ExtractFn; + private agentId: string | undefined; + + constructor(config?: MemoryConfig) { + this.agentId = config?.agent; + + // Build embed/extract functions: use explicit provider config if given, else fall back to env-based + const embedFn = (config?.provider && config?.apiKey) + ? createEmbedFn({ provider: config.provider, apiKey: config.apiKey, embedModel: config.embedModel }) + : defaultEmbed; + + this.extractFn = (config?.provider && config?.apiKey) + ? createExtractFn({ provider: config.provider, apiKey: config.apiKey, extractModel: config.extractModel }) + : defaultExtract; + + this.store = new SqliteMemoryStore({ + dbPath: config?.dbPath ?? './memory.db', + embed: embedFn, + }); + } + + // --- Management (always available) --- + + addAgent(id: string, opts?: { name?: string; soul?: string }): AgentInfo { + return this.store.addAgent(id, { name: opts?.name, soulMd: opts?.soul }); + } + + getAgents(): AgentInfo[] { + return this.store.getAgents(); + } + + // --- Data ops (require agent scope) --- + + private requireAgent(): string { + if (!this.agentId) { + throw new Error('agent_scope_required: create Memory with { agent: "..." } for data ops'); + } + return this.agentId; + } + + /** + * Add memories from raw conversation messages. + * Internally calls extract() to pull episodic/semantic/procedural components, + * then ingests them into the store. + */ + async add( + messages: Array<{ role: string; content: string }>, + opts?: { userId?: string }, + ): Promise<{ stored: number; ids: string[] }> { + const agent = this.requireAgent(); + + const allMessages = messages.filter((m) => m.content?.trim()); + const sourceText = allMessages + .map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content.trim()}`) + .join('\n\n'); + + if (!sourceText) return { stored: 0, ids: [] }; + + const components = await this.extractFn(sourceText); + if (!components) return { stored: 0, ids: [] }; + + const rows = await this.store.ingest(components, agent, { + origin: { originType: 'conversation' }, + userId: opts?.userId, + }); + + return { stored: rows.length, ids: rows.map((r) => r.id) }; + } + + /** Search memories semantically. */ + async search( + query: string, + opts?: { userId?: string }, + ): Promise { + const agent = this.requireAgent(); + const data = await this.store.query(query, agent, opts?.userId); + return data.workingMemory ?? []; + } + + /** List users who have memories for this agent. */ + users(): Array<{ + userId: string; + firstSeen: number; + lastSeen: number; + interactionCount: number; + }> { + const agent = this.requireAgent(); + return this.store.getAgentUsers(agent); + } + + /** Get memories, optionally filtered by userId or scope. */ + memories(opts?: MemoryFilterOptions): (MemoryRecord & { details?: any })[] { + const agent = this.requireAgent(); + const limit = opts?.limit ?? 50; + + if (opts?.scope === 'global') { + return this.store.getGlobalMemories(agent, limit); + } + if (opts?.userId) { + return this.store.getMemoriesForUser(agent, opts.userId, limit); + } + return this.store.getRecent(agent, limit); + } + + /** Delete a memory by ID. */ + delete(id: string): boolean { + const agent = this.requireAgent(); + return this.store.deleteById(id, agent) > 0; + } + + /** Get the underlying store (for advanced use / mounting HTTP routes). */ + get _store(): SqliteMemoryStore { + return this.store; + } + + /** Get the extract function (for passing to router/documents). */ + get _extractFn(): ExtractFn { + return this.extractFn; + } +} diff --git a/memory/src/providers/embed.ts b/memory/src/providers/embed.ts index b530c0c..8c30c88 100644 --- a/memory/src/providers/embed.ts +++ b/memory/src/providers/embed.ts @@ -1,10 +1,7 @@ -import type { SectorName } from '../types.js'; -import { buildUrl, getApiKey, getModel, resolveProvider } from './registry.js'; +import type { EmbedFn, ProviderName, SectorName } from '../types.js'; +import { PROVIDERS, buildUrl, getApiKey, getModel, resolveProvider, type ProviderConfig } from './registry.js'; -export async function embed(text: string, _sector: SectorName): Promise { - const cfg = resolveProvider('embed'); - const apiKey = getApiKey(cfg); - const model = getModel(cfg, 'embed'); +async function callEmbed(cfg: ProviderConfig, model: string, apiKey: string, text: string): Promise { const { url, headers } = buildUrl(cfg, 'embed', model, apiKey); const body = @@ -32,3 +29,18 @@ export async function embed(text: string, _sector: SectorName): Promise { + const cfg = resolveProvider('embed'); + const apiKey = getApiKey(cfg); + const model = getModel(cfg, 'embed'); + return callEmbed(cfg, model, apiKey, text); +} + +/** Factory: create an EmbedFn from explicit provider config. */ +export function createEmbedFn(opts: { provider: ProviderName; apiKey: string; embedModel?: string }): EmbedFn { + const cfg = PROVIDERS[opts.provider]; + const model = opts.embedModel ?? cfg.defaultEmbedModel; + const apiKey = opts.apiKey; + return (text: string, _sector: SectorName) => callEmbed(cfg, model, apiKey, text); +} diff --git a/memory/src/providers/extract.ts b/memory/src/providers/extract.ts index 5407bcd..6fa9b4b 100644 --- a/memory/src/providers/extract.ts +++ b/memory/src/providers/extract.ts @@ -1,6 +1,6 @@ -import type { IngestComponents, SemanticTripleInput } from '../types.js'; +import type { ExtractFn, IngestComponents, ProviderName, SemanticTripleInput } from '../types.js'; import { EXTRACT_PROMPT } from './prompt.js'; -import { buildUrl, getApiKey, getModel, resolveProvider } from './registry.js'; +import { PROVIDERS, buildUrl, getApiKey, getModel, resolveProvider, type ProviderConfig } from './registry.js'; /** * Normalize semantic output: single object → array, filter invalid triples. @@ -33,10 +33,7 @@ function parseResponse(parsed: any): IngestComponents { }; } -export async function extract(text: string): Promise { - const cfg = resolveProvider('extract'); - const apiKey = getApiKey(cfg); - const model = getModel(cfg, 'extract'); +async function callExtract(cfg: ProviderConfig, model: string, apiKey: string, text: string): Promise { const { url, headers } = buildUrl(cfg, 'extract', model, apiKey); if (cfg.name === 'gemini') { @@ -78,3 +75,19 @@ export async function extract(text: string): Promise { const content = json.choices[0]?.message?.content ?? '{}'; return parseResponse(JSON.parse(content)); } + +/** Env-based extract (legacy). Resolves provider from MEMORY_EXTRACT_PROVIDER env var. */ +export async function extract(text: string): Promise { + const cfg = resolveProvider('extract'); + const apiKey = getApiKey(cfg); + const model = getModel(cfg, 'extract'); + return callExtract(cfg, model, apiKey, text); +} + +/** Factory: create an ExtractFn from explicit provider config. */ +export function createExtractFn(opts: { provider: ProviderName; apiKey: string; extractModel?: string }): ExtractFn { + const cfg = PROVIDERS[opts.provider]; + const model = opts.extractModel ?? cfg.defaultExtractModel; + const apiKey = opts.apiKey; + return (text: string) => callExtract(cfg, model, apiKey, text); +} diff --git a/memory/src/providers/registry.ts b/memory/src/providers/registry.ts index 105797c..5bca6a0 100644 --- a/memory/src/providers/registry.ts +++ b/memory/src/providers/registry.ts @@ -1,8 +1,8 @@ -type ProviderName = 'gemini' | 'openai' | 'openrouter'; +import type { ProviderName } from '../types.js'; type AuthMode = 'queryKey' | 'bearer'; -interface ProviderConfig { +export interface ProviderConfig { name: ProviderName; apiKeyEnv: string; baseUrl: string; @@ -15,7 +15,7 @@ interface ProviderConfig { auth: AuthMode; } -const PROVIDERS: Record = { +export const PROVIDERS: Record = { gemini: { name: 'gemini', apiKeyEnv: 'GOOGLE_API_KEY', @@ -28,7 +28,6 @@ const PROVIDERS: Record = { extractModelEnv: 'GEMINI_EXTRACT_MODEL', auth: 'queryKey', }, - // TODO: add OpenAI provider wiring (bearer auth, /v1/embeddings and /v1/chat/completions) openai: { name: 'openai', apiKeyEnv: 'OPENAI_API_KEY', @@ -90,4 +89,3 @@ export function buildUrl(cfg: ProviderConfig, kind: 'embed' | 'extract', model: }, }; } - diff --git a/memory/src/router.ts b/memory/src/router.ts index 39195c8..fba4828 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -1,8 +1,9 @@ import { Router } from 'express'; import type { Request, Response } from 'express'; import type { SqliteMemoryStore } from './sqlite-store.js'; -import { extract } from './providers/extract.js'; -import { normalizeProfileSlug } from './utils.js'; +import type { ExtractFn } from './types.js'; +import { extract as defaultExtract } from './providers/extract.js'; +import { normalizeAgentId } from './utils.js'; import type { IngestComponents } from './types.js'; import { ingestDocuments } from './documents.js'; @@ -10,51 +11,52 @@ import { ingestDocuments } from './documents.js'; * Creates an Express Router with all memory API routes. * The store is received via closure — no global state needed. */ -export function createMemoryRouter(store: SqliteMemoryStore): Router { +export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: ExtractFn): Router { const router = Router(); + const doExtract = extractFn ?? defaultExtract; - router.get('/v1/profiles', (_req: Request, res: Response) => { + router.get('/v1/agents', (_req: Request, res: Response) => { try { - const profiles = store.getAvailableProfiles(); - res.json({ profiles }); + const agents = store.getAgents(); + res.json({ agents }); } catch (err: any) { - res.status(500).json({ error: err.message ?? 'failed to fetch profiles' }); + res.status(500).json({ error: err.message ?? 'failed to fetch agents' }); } }); - const handleDeleteProfile = (req: Request, res: Response) => { + const handleDeleteAgent = (req: Request, res: Response) => { try { const { slug } = req.params; - const normalizedProfile = normalizeProfileSlug(slug); - const deleted = store.deleteProfile(normalizedProfile); - res.json({ deleted, profile: normalizedProfile }); + const normalizedAgent = normalizeAgentId(slug); + const deleted = store.deleteAgent(normalizedAgent); + res.json({ deleted, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } - if (err?.message === 'cannot_delete_default_profile') { - return res.status(400).json({ error: 'default_profile_protected' }); + if (err?.message === 'cannot_delete_default_agent') { + return res.status(400).json({ error: 'default_agent_protected' }); } - res.status(500).json({ error: err.message ?? 'delete profile failed' }); + res.status(500).json({ error: err.message ?? 'delete agent failed' }); } }; - router.delete('/v1/profiles/:slug', handleDeleteProfile); + router.delete('/v1/agents/:slug', handleDeleteAgent); router.post('/v1/ingest', async (req: Request, res: Response) => { - const { messages, profile, userId } = req.body as { + const { messages, agent, userId } = req.body as { messages?: Array<{ role: 'user' | 'assistant' | string; content: string }>; - profile?: string; + agent?: string; userId?: string; }; - let normalizedProfile: string; + let normalizedAgent: string; try { - normalizedProfile = normalizeProfileSlug(profile); + normalizedAgent = normalizeAgentId(agent); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } - return res.status(500).json({ error: 'profile_normalization_failed' }); + return res.status(500).json({ error: 'agent_normalization_failed' }); } if (!messages || !messages.length) { @@ -74,7 +76,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { let finalComponents: IngestComponents | undefined; try { - finalComponents = await extract(sourceText); + finalComponents = await doExtract(sourceText); } catch (err: any) { return res.status(500).json({ error: err.message ?? 'extraction failed' }); } @@ -83,34 +85,34 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { return res.status(400).json({ error: 'unable to extract components from messages' }); } try { - const rows = await store.ingest(finalComponents, normalizedProfile, { + const rows = await store.ingest(finalComponents, normalizedAgent, { origin: { originType: 'conversation', originActor: userId }, userId, }); - res.json({ stored: rows.length, ids: rows.map((r) => r.id), profile: normalizedProfile }); + res.json({ stored: rows.length, ids: rows.map((r) => r.id), agent: normalizedAgent }); } catch (err: any) { res.status(500).json({ error: err.message ?? 'ingest failed' }); } }); router.post('/v1/ingest/documents', async (req: Request, res: Response) => { - const { path: docPath, profile } = req.body as { + const { path: docPath, agent } = req.body as { path?: string; - profile?: string; + agent?: string; }; if (!docPath || !docPath.trim()) { return res.status(400).json({ error: 'path_required' }); } - let normalizedProfile: string; + let normalizedAgent: string; try { - normalizedProfile = normalizeProfileSlug(profile); + normalizedAgent = normalizeAgentId(agent); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } - return res.status(500).json({ error: 'profile_normalization_failed' }); + return res.status(500).json({ error: 'agent_normalization_failed' }); } // Validate path exists @@ -122,7 +124,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { } try { - const result = await ingestDocuments(docPath.trim(), store, normalizedProfile); + const result = await ingestDocuments(docPath.trim(), store, normalizedAgent, doExtract); res.json(result); } catch (err: any) { res.status(500).json({ error: err.message ?? 'document ingestion failed' }); @@ -132,13 +134,13 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/summary', (req: Request, res: Response) => { try { const limit = Number(req.query.limit) || 50; - const profile = req.query.profile as string; - const normalizedProfile = normalizeProfileSlug(profile); - const summary = store.getSectorSummary(normalizedProfile); - const recent = store.getRecent(normalizedProfile, limit).map((r) => ({ + const agent = req.query.agent as string; + const normalizedAgent = normalizeAgentId(agent); + const summary = store.getSectorSummary(normalizedAgent); + const recent = store.getRecent(normalizedAgent, limit).map((r) => ({ id: r.id, sector: r.sector, - profile: r.profileId, + agent: r.agentId, createdAt: r.createdAt, lastAccessed: r.lastAccessed, preview: r.content, @@ -146,10 +148,10 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { details: (r as any).details, userScope: (r as any).userScope ?? null, })); - res.json({ summary, recent, profile: normalizedProfile }); + res.json({ summary, recent, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'summary failed' }); } @@ -158,28 +160,28 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.put('/v1/memory/:id', async (req: Request, res: Response) => { try { const { id } = req.params; - const { content, sector, profile } = req.body as { content?: string; sector?: string; profile?: string }; + const { content, sector, agent } = req.body as { content?: string; sector?: string; agent?: string }; if (!id) return res.status(400).json({ error: 'id_required' }); if (!content || !content.trim()) { return res.status(400).json({ error: 'content_required' }); } - let normalizedProfile: string; + let normalizedAgent: string; try { - normalizedProfile = normalizeProfileSlug(profile); + normalizedAgent = normalizeAgentId(agent); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } - return res.status(500).json({ error: 'profile_normalization_failed' }); + return res.status(500).json({ error: 'agent_normalization_failed' }); } - const updated = await store.updateById(id, content.trim(), sector as any, normalizedProfile); + const updated = await store.updateById(id, content.trim(), sector as any, normalizedAgent); if (!updated) { return res.status(404).json({ error: 'not_found', id }); } - res.json({ updated: true, id, profile: normalizedProfile }); + res.json({ updated: true, id, agent: normalizedAgent }); } catch (err: any) { res.status(500).json({ error: err.message ?? 'update failed' }); } @@ -188,25 +190,25 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.delete('/v1/memory/:id', (req: Request, res: Response) => { try { const { id } = req.params; - const profile = req.query.profile as string; + const agent = req.query.agent as string; if (!id) return res.status(400).json({ error: 'id_required' }); - let normalizedProfile: string; + let normalizedAgent: string; try { - normalizedProfile = normalizeProfileSlug(profile); + normalizedAgent = normalizeAgentId(agent); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } - return res.status(500).json({ error: 'profile_normalization_failed' }); + return res.status(500).json({ error: 'agent_normalization_failed' }); } - const deleted = store.deleteById(id, normalizedProfile); + const deleted = store.deleteById(id, normalizedAgent); if (!deleted) { return res.status(404).json({ error: 'not_found', id }); } - res.json({ deleted, profile: normalizedProfile }); + res.json({ deleted, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'delete failed' }); } @@ -214,13 +216,13 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.delete('/v1/memory', (req: Request, res: Response) => { try { - const profile = req.query.profile as string; - const normalizedProfile = normalizeProfileSlug(profile); - const deleted = store.deleteAll(normalizedProfile); - res.json({ deleted, profile: normalizedProfile }); + const agent = req.query.agent as string; + const normalizedAgent = normalizeAgentId(agent); + const deleted = store.deleteAll(normalizedAgent); + res.json({ deleted, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'delete all failed' }); } @@ -230,34 +232,34 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.delete('/v1/graph/triple/:id', (req: Request, res: Response) => { try { const { id } = req.params; - const profile = req.query.profile as string; + const agent = req.query.agent as string; if (!id) return res.status(400).json({ error: 'id_required' }); - const deleted = store.deleteSemanticById(id, profile); + const deleted = store.deleteSemanticById(id, agent); if (!deleted) { return res.status(404).json({ error: 'not_found', id }); } res.json({ deleted }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'triple delete failed' }); } }); router.post('/v1/search', async (req: Request, res: Response) => { - const { query, profile, userId } = req.body as { query?: string; profile?: string; userId?: string }; + const { query, agent, userId } = req.body as { query?: string; agent?: string; userId?: string }; if (!query || !query.trim()) { return res.status(400).json({ error: 'query is required' }); } try { - const result = await store.query(query, profile, userId); + const result = await store.query(query, agent, userId); res.json(result); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'query failed' }); } @@ -267,13 +269,13 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/users', (req: Request, res: Response) => { try { - const profile = req.query.profile as string; - const normalizedProfile = normalizeProfileSlug(profile); - const users = store.getAgentUsers(normalizedProfile); - res.json({ users, profile: normalizedProfile }); + const agent = req.query.agent as string; + const normalizedAgent = normalizeAgentId(agent); + const users = store.getAgentUsers(normalizedAgent); + res.json({ users, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'failed to fetch users' }); } @@ -282,27 +284,27 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/users/:id/memories', (req: Request, res: Response) => { try { const { id: userId } = req.params; - const profile = req.query.profile as string; + const agent = req.query.agent as string; const limit = Number(req.query.limit) || 50; - const normalizedProfile = normalizeProfileSlug(profile); + const normalizedAgent = normalizeAgentId(agent); if (!userId) { return res.status(400).json({ error: 'user_id_required' }); } - const memories = store.getMemoriesForUser(normalizedProfile, userId, limit).map((r) => ({ + const memories = store.getMemoriesForUser(normalizedAgent, userId, limit).map((r) => ({ id: r.id, sector: r.sector, - profile: r.profileId, + agent: r.agentId, createdAt: r.createdAt, lastAccessed: r.lastAccessed, preview: r.content, details: (r as any).details, })); - res.json({ memories, userId, profile: normalizedProfile }); + res.json({ memories, userId, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'failed to fetch user memories' }); } @@ -311,7 +313,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { // Graph traversal endpoints router.get('/v1/graph/triples', (req: Request, res: Response) => { try { - const { entity, direction, maxResults, predicate, profile, userId } = req.query; + const { entity, direction, maxResults, predicate, agent, userId } = req.query; if (!entity || typeof entity !== 'string') { return res.status(400).json({ error: 'entity query parameter is required' }); } @@ -319,7 +321,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { const options: any = { maxResults: maxResults ? Number(maxResults) : 100, predicateFilter: predicate as string | undefined, - profile: profile as string, + agent: agent as string, userId: userId as string | undefined, }; @@ -334,8 +336,8 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { res.json({ entity, triples, count: triples.length }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'graph query failed' }); } @@ -343,16 +345,16 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/graph/neighbors', (req: Request, res: Response) => { try { - const { entity, profile, userId } = req.query; + const { entity, agent, userId } = req.query; if (!entity || typeof entity !== 'string') { return res.status(400).json({ error: 'entity query parameter is required' }); } - const neighbors = Array.from(store.graph.findNeighbors(entity, { profile: profile as string, userId: userId as string | undefined })); + const neighbors = Array.from(store.graph.findNeighbors(entity, { agent: agent as string, userId: userId as string | undefined })); res.json({ entity, neighbors, count: neighbors.length }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'neighbors query failed' }); } @@ -360,21 +362,21 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/graph/paths', (req: Request, res: Response) => { try { - const { from, to, maxDepth, profile, userId } = req.query; + const { from, to, maxDepth, agent, userId } = req.query; if (!from || typeof from !== 'string' || !to || typeof to !== 'string') { return res.status(400).json({ error: 'from and to query parameters are required' }); } const paths = store.graph.findPaths(from, to, { maxDepth: maxDepth ? Number(maxDepth) : 3, - profile: profile as string, + agent: agent as string, userId: userId as string | undefined, }); res.json({ from, to, paths, count: paths.length }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'paths query failed' }); } @@ -382,9 +384,9 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { router.get('/v1/graph/visualization', (req: Request, res: Response) => { try { - const { entity, maxDepth, maxNodes, profile, includeHistory, userId } = req.query; - const profileValue = profile as string; - const normalizedProfile = normalizeProfileSlug(profileValue); + const { entity, maxDepth, maxNodes, agent, includeHistory, userId } = req.query; + const agentValue = agent as string; + const normalizedAgent = normalizeAgentId(agentValue); const centerEntity = (entity as string) || null; const depth = maxDepth ? Number(maxDepth) : 2; const nodeLimit = maxNodes ? Number(maxNodes) : 50; @@ -392,7 +394,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { // If no center entity, get a sample of semantic triples if (!centerEntity) { - const allSemantic = store.getRecent(normalizedProfile, 100).filter((r) => r.sector === 'semantic'); + const allSemantic = store.getRecent(normalizedAgent, 100).filter((r) => r.sector === 'semantic'); const triples = allSemantic .slice(0, nodeLimit) .map((r) => (r as any).details) @@ -417,18 +419,18 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { nodes: Array.from(nodes).map((id) => ({ id, label: id })), edges, includeHistory: showHistory, - profile: normalizedProfile, + agent: normalizedAgent, }); } // Build graph from center entity - const graphOptions = { maxDepth: depth, profile: normalizedProfile, includeInvalidated: showHistory, userId: userId as string | undefined }; + const graphOptions = { maxDepth: depth, agent: normalizedAgent, includeInvalidated: showHistory, userId: userId as string | undefined }; const reachable = store.graph.findReachableEntities(centerEntity, graphOptions); const nodes = new Set([centerEntity]); const edges: Array<{ source: string; target: string; predicate: string; isHistorical?: boolean }> = []; // Add center entity's connections - const centerTriples = store.graph.findConnectedTriples(centerEntity, { maxResults: 100, profile: normalizedProfile, includeInvalidated: showHistory, userId: userId as string | undefined }); + const centerTriples = store.graph.findConnectedTriples(centerEntity, { maxResults: 100, agent: normalizedAgent, includeInvalidated: showHistory, userId: userId as string | undefined }); for (const triple of centerTriples) { nodes.add(triple.subject); nodes.add(triple.object); @@ -446,7 +448,7 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { .slice(0, nodeLimit - nodes.size); for (const [entity, _depth] of reachableArray) { - const entityTriples = store.graph.findTriplesBySubject(entity, { maxResults: 10, profile: normalizedProfile, includeInvalidated: showHistory, userId: userId as string | undefined }); + const entityTriples = store.graph.findTriplesBySubject(entity, { maxResults: 10, agent: normalizedAgent, includeInvalidated: showHistory, userId: userId as string | undefined }); for (const triple of entityTriples) { if (nodes.has(triple.subject) || nodes.has(triple.object)) { nodes.add(triple.subject); @@ -466,11 +468,11 @@ export function createMemoryRouter(store: SqliteMemoryStore): Router { nodes: Array.from(nodes).map((id) => ({ id, label: id })), edges, includeHistory: showHistory, - profile: normalizedProfile, + agent: normalizedAgent, }); } catch (err: any) { - if (err?.message === 'invalid_profile') { - return res.status(400).json({ error: 'invalid_profile' }); + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); } res.status(500).json({ error: err.message ?? 'visualization query failed' }); } diff --git a/memory/src/scoring.ts b/memory/src/scoring.ts index b949beb..fa1729d 100644 --- a/memory/src/scoring.ts +++ b/memory/src/scoring.ts @@ -41,7 +41,7 @@ export function scoreRowPBWM( return { sector: row.sector, id: row.id, - profileId: row.profileId, + agentId: row.agentId, content: row.content, score, similarity: relevance, diff --git a/memory/src/semantic-graph.ts b/memory/src/semantic-graph.ts index 717b082..315b97c 100644 --- a/memory/src/semantic-graph.ts +++ b/memory/src/semantic-graph.ts @@ -1,5 +1,5 @@ import Database from 'better-sqlite3'; -import { normalizeProfileSlug } from './utils.js'; +import { normalizeAgentId } from './utils.js'; import type { SemanticMemoryRecord, GraphTraversalOptions, GraphPath } from './types.js'; /** @@ -20,16 +20,16 @@ export class SemanticGraphTraversal { */ findTriplesBySubject( subject: string, - options: GraphTraversalOptions & { profile?: string } = {}, + options: GraphTraversalOptions = {}, ): SemanticMemoryRecord[] { const { maxResults = 100, includeInvalidated = false, predicateFilter, userId } = options; - const profileId = normalizeProfileSlug(options.profile); + const agentId = normalizeAgentId(options.agent); const now = this.now(); let query = `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, - created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId + created_at as createdAt, updated_at as updatedAt, embedding, metadata, agent_id as agentId from semantic_memory - where subject = @subject and profile_id = @profileId`; + where subject = @subject and agent_id = @agentId`; if (!includeInvalidated) { query += ` and (valid_to is null or valid_to > @now)`; @@ -45,14 +45,14 @@ export class SemanticGraphTraversal { query += ` order by updated_at desc limit @maxResults`; - const params: Record = { subject, now, maxResults, profileId }; + const params: Record = { subject, now, maxResults, agentId }; if (predicateFilter) { params.predicateFilter = predicateFilter; } if (userId) { params.userId = userId; } - + const rows = this.db .prepare(query) .all(params) as Array>; @@ -69,16 +69,16 @@ export class SemanticGraphTraversal { */ findTriplesByObject( object: string, - options: GraphTraversalOptions & { profile?: string } = {}, + options: GraphTraversalOptions = {}, ): SemanticMemoryRecord[] { const { maxResults = 100, includeInvalidated = false, predicateFilter, userId } = options; - const profileId = normalizeProfileSlug(options.profile); + const agentId = normalizeAgentId(options.agent); const now = this.now(); let query = `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, - created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId + created_at as createdAt, updated_at as updatedAt, embedding, metadata, agent_id as agentId from semantic_memory - where object = @object and profile_id = @profileId`; + where object = @object and agent_id = @agentId`; if (!includeInvalidated) { query += ` and (valid_to is null or valid_to > @now)`; @@ -94,14 +94,14 @@ export class SemanticGraphTraversal { query += ` order by updated_at desc limit @maxResults`; - const params: Record = { object, now, maxResults, profileId }; + const params: Record = { object, now, maxResults, agentId }; if (predicateFilter) { params.predicateFilter = predicateFilter; } if (userId) { params.userId = userId; } - + const rows = this.db .prepare(query) .all(params) as Array>; @@ -118,22 +118,22 @@ export class SemanticGraphTraversal { */ findConnectedTriples( entity: string, - options: GraphTraversalOptions & { profile?: string } = {}, + options: GraphTraversalOptions = {}, ): SemanticMemoryRecord[] { const outgoing = this.findTriplesBySubject(entity, options); const incoming = this.findTriplesByObject(entity, options); - + // Deduplicate by id const seen = new Set(); const result: SemanticMemoryRecord[] = []; - + for (const triple of [...outgoing, ...incoming]) { if (!seen.has(triple.id)) { seen.add(triple.id); result.push(triple); } } - + return result; } @@ -142,11 +142,11 @@ export class SemanticGraphTraversal { */ findNeighbors( entity: string, - options: GraphTraversalOptions & { profile?: string } = {}, + options: GraphTraversalOptions = {}, ): Set { const triples = this.findConnectedTriples(entity, options); const neighbors = new Set(); - + for (const triple of triples) { if (triple.subject !== entity) { neighbors.add(triple.subject); @@ -155,7 +155,7 @@ export class SemanticGraphTraversal { neighbors.add(triple.object); } } - + return neighbors; } @@ -165,10 +165,10 @@ export class SemanticGraphTraversal { findPaths( fromEntity: string, toEntity: string, - options: GraphTraversalOptions & { profile?: string } = {}, + options: GraphTraversalOptions = {}, ): GraphPath[] { const { maxDepth = 3 } = options; - + if (fromEntity === toEntity) { return []; } @@ -219,7 +219,7 @@ export class SemanticGraphTraversal { */ findReachableEntities( entity: string, - options: GraphTraversalOptions & { profile?: string } = {}, + options: GraphTraversalOptions = {}, ): Map { const { maxDepth = 2 } = options; const reachable = new Map(); @@ -250,4 +250,3 @@ export class SemanticGraphTraversal { return reachable; } } - diff --git a/memory/src/server.ts b/memory/src/server.ts index 8024bb7..25537d8 100644 --- a/memory/src/server.ts +++ b/memory/src/server.ts @@ -8,13 +8,22 @@ dotenv.config({ path: path.resolve(__dirname, '../.env') }); dotenv.config({ path: path.resolve(__dirname, '../../.env') }); import express from 'express'; import cors from 'cors'; -import { SqliteMemoryStore } from './sqlite-store.js'; -import { embed } from './providers/embed.js'; +import type { ProviderName } from './types.js'; +import { Memory } from './memory.js'; import { createMemoryRouter } from './router.js'; const PORT = Number(process.env.MEMORY_PORT ?? 4005); const DB_PATH = process.env.MEMORY_DB_PATH ?? './memory.db'; +// Resolve provider config from env +const provider = (process.env.MEMORY_EMBED_PROVIDER ?? process.env.MEMORY_EXTRACT_PROVIDER ?? 'gemini') as ProviderName; +const apiKeyEnvMap: Record = { + gemini: 'GOOGLE_API_KEY', + openai: 'OPENAI_API_KEY', + openrouter: 'OPENROUTER_API_KEY', +}; +const apiKey = process.env[apiKeyEnvMap[provider]] ?? ''; + async function main() { const app = express(); const corsOrigins = @@ -27,16 +36,17 @@ async function main() { app.options('*', cors({ origin: corsOrigins })); app.use(express.json({ limit: '2mb' })); - const store = new SqliteMemoryStore({ + const memory = new Memory({ + provider, + apiKey, dbPath: DB_PATH, - embed, }); app.get('/health', (_req, res) => { res.json({ status: 'ok' }); }); - app.use(createMemoryRouter(store)); + app.use(createMemoryRouter(memory._store, memory._extractFn)); // Fallback 404 with CORS headers app.use((req, res) => { diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 2fd2114..6f6b823 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -1,6 +1,7 @@ import Database from 'better-sqlite3'; import { randomUUID } from 'crypto'; import type { + AgentInfo, EmbedFn, IngestComponents, IngestOptions, @@ -15,7 +16,7 @@ import type { } from './types.js'; import { determineConsolidationAction } from './consolidation.js'; import { PBWM_SECTOR_WEIGHTS, scoreRowPBWM } from './scoring.js'; -import { cosineSimilarity, DEFAULT_PROFILE, normalizeProfileSlug } from './utils.js'; +import { cosineSimilarity, DEFAULT_AGENT, normalizeAgentId } from './utils.js'; import { filterAndCapWorkingMemory } from './wm.js'; import { SemanticGraphTraversal } from './semantic-graph.js'; @@ -39,9 +40,9 @@ export class SqliteMemoryStore { this.graph = new SemanticGraphTraversal(this.db, this.now); } - async ingest(components: IngestComponents, profile?: string, options?: IngestOptions): Promise { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); + async ingest(components: IngestComponents, agent?: string, options?: IngestOptions): Promise { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); const createdAt = this.now(); const rows: MemoryRecord[] = []; const source = options?.source; @@ -51,7 +52,7 @@ export class SqliteMemoryStore { // Upsert into agent_users when userId is provided if (userId) { - this.upsertAgentUser(profileId, userId); + this.upsertAgentUser(agentId, userId); } // --- Episodic --- @@ -60,7 +61,7 @@ export class SqliteMemoryStore { const embedding = await this.embed(episodic, 'episodic'); if (dedup) { - const existingDup = this.findDuplicateMemory(embedding, 'episodic', profileId, 0.9); + const existingDup = this.findDuplicateMemory(embedding, 'episodic', agentId, 0.9); if (existingDup) { if (source && !existingDup.source) { this.setMemorySource(existingDup.id, source); @@ -70,18 +71,18 @@ export class SqliteMemoryStore { sector: 'episodic', content: existingDup.content, embedding, - profileId, + agentId, createdAt: existingDup.createdAt, lastAccessed: existingDup.lastAccessed, source: existingDup.source ?? source, }); } else { - const row = this.buildEpisodicRow(episodic, embedding, profileId, createdAt, source, origin, userId); + const row = this.buildEpisodicRow(episodic, embedding, agentId, createdAt, source, origin, userId); this.insertRow(row); rows.push(row); } } else { - const row = this.buildEpisodicRow(episodic, embedding, profileId, createdAt, source, origin, userId); + const row = this.buildEpisodicRow(episodic, embedding, agentId, createdAt, source, origin, userId); this.insertRow(row); rows.push(row); } @@ -102,7 +103,7 @@ export class SqliteMemoryStore { subject: triple.subject, predicate: triple.predicate, object: triple.object, - profileId, + agentId, embedding, validFrom: createdAt, validTo: null, @@ -118,7 +119,7 @@ export class SqliteMemoryStore { }; // Consolidation - const allFactsForSubject = this.findActiveFactsForSubject(triple.subject, profileId); + const allFactsForSubject = this.findActiveFactsForSubject(triple.subject, agentId); const matchingFacts = await this.findSemanticallyMatchingFacts( triple.predicate, allFactsForSubject, @@ -141,7 +142,7 @@ export class SqliteMemoryStore { sector: 'semantic', content: triple.object, embedding, - profileId, + agentId, createdAt, lastAccessed: createdAt, eventStart: null, @@ -161,7 +162,7 @@ export class SqliteMemoryStore { sector: 'semantic', content: triple.object, embedding, - profileId, + agentId, createdAt, lastAccessed: createdAt, eventStart: null, @@ -183,7 +184,7 @@ export class SqliteMemoryStore { procRow = { id: randomUUID(), trigger: procInput, - profileId, + agentId, goal: '', context: '', result: '', @@ -202,7 +203,7 @@ export class SqliteMemoryStore { procRow = { id: randomUUID(), trigger: procInput.trigger, - profileId, + agentId, goal: procInput.goal ?? '', context: procInput.context ?? '', result: procInput.result ?? '', @@ -223,7 +224,7 @@ export class SqliteMemoryStore { procRow.embedding = embedding; if (dedup) { - const existingDup = this.findDuplicateProcedural(embedding, profileId, 0.9); + const existingDup = this.findDuplicateProcedural(embedding, agentId, 0.9); if (existingDup) { if (source && !existingDup.source) { this.setProceduralSource(existingDup.id, source); @@ -233,7 +234,7 @@ export class SqliteMemoryStore { sector: 'procedural', content: existingDup.trigger, embedding, - profileId, + agentId, createdAt: existingDup.createdAt, lastAccessed: existingDup.lastAccessed, source: existingDup.source ?? source, @@ -245,7 +246,7 @@ export class SqliteMemoryStore { sector: 'procedural', content: procRow.trigger, embedding, - profileId, + agentId, createdAt, lastAccessed: createdAt, source, @@ -258,7 +259,7 @@ export class SqliteMemoryStore { sector: 'procedural', content: procRow.trigger, embedding, - profileId, + agentId, createdAt, lastAccessed: createdAt, source, @@ -272,11 +273,11 @@ export class SqliteMemoryStore { async query( queryText: string, - profile?: string, + agent?: string, userId?: string, - ): Promise<{ workingMemory: QueryResult[]; perSector: Record; profileId: string }> { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); + ): Promise<{ workingMemory: QueryResult[]; perSector: Record; agentId: string }> { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); const queryEmbeddings: Record = {} as Record; for (const sector of SECTORS) { queryEmbeddings[sector] = await this.embed(queryText, sector); @@ -289,7 +290,7 @@ export class SqliteMemoryStore { }; for (const sector of SECTORS) { - const candidates = this.getCandidatesForSector(sector, profileId, userId); + const candidates = this.getCandidatesForSector(sector, agentId, userId); const scored = candidates .filter((row) => cosineSimilarity(queryEmbeddings[sector], row.embedding) >= 0.2) .map((row) => scoreRowPBWM(row, queryEmbeddings[sector], PBWM_SECTOR_WEIGHTS[sector])) @@ -297,7 +298,7 @@ export class SqliteMemoryStore { .slice(0, PER_SECTOR_K) .map((row) => ({ ...row, - profileId, + agentId, // propagate temporal fields for episodic; procedural has none eventStart: (row as any).eventStart ?? null, eventEnd: (row as any).eventEnd ?? null, @@ -309,36 +310,36 @@ export class SqliteMemoryStore { const workingMemory = filterAndCapWorkingMemory(perSectorResults, WORKING_MEMORY_CAP); this.bumpRetrievalCounts(workingMemory.map((r) => r.id)); - return { workingMemory, perSector: perSectorResults, profileId }; + return { workingMemory, perSector: perSectorResults, agentId }; } - getSectorSummary(profile?: string): Array<{ sector: SectorName; count: number; lastCreatedAt: number | null }> { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); + getSectorSummary(agent?: string): Array<{ sector: SectorName; count: number; lastCreatedAt: number | null }> { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); const rows = this.db .prepare( `select sector, count(*) as count, max(created_at) as lastCreatedAt from memory - where profile_id = @profileId + where agent_id = @agentId group by sector`, ) - .all({ profileId }) as Array<{ sector: SectorName; count: number; lastCreatedAt: number | null }>; + .all({ agentId }) as Array<{ sector: SectorName; count: number; lastCreatedAt: number | null }>; const proceduralRow = this.db .prepare( `select 'procedural' as sector, count(*) as count, max(created_at) as lastCreatedAt from procedural_memory - where profile_id = @profileId`, + where agent_id = @agentId`, ) - .get({ profileId }) as { sector: SectorName; count: number; lastCreatedAt: number | null }; + .get({ agentId }) as { sector: SectorName; count: number; lastCreatedAt: number | null }; const semanticRow = this.db .prepare( `select 'semantic' as sector, count(*) as count, max(created_at) as lastCreatedAt from semantic_memory - where profile_id = @profileId`, + where agent_id = @agentId`, ) - .get({ profileId }) as { sector: SectorName; count: number; lastCreatedAt: number | null }; + .get({ agentId }) as { sector: SectorName; count: number; lastCreatedAt: number | null }; const defaults = SECTORS.map((s) => ({ sector: s, @@ -352,36 +353,36 @@ export class SqliteMemoryStore { return defaults.map((d) => map.get(d.sector) ?? d); } - getRecent(profile: string | undefined, limit: number): (MemoryRecord & { details?: any })[] { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); + getRecent(agent: string | undefined, limit: number): (MemoryRecord & { details?: any })[] { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); const rows = this.db .prepare( `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, user_scope as userScope from memory - where profile_id = @profileId + where agent_id = @agentId union all select id, 'procedural' as sector, trigger as content, embedding, created_at as createdAt, last_accessed as lastAccessed, json_object('trigger', trigger, 'goal', goal, 'context', context, 'result', result, 'steps', json(steps)) as details, null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope from procedural_memory - where profile_id = @profileId + where agent_id = @agentId union all select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'metadata', metadata, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope from semantic_memory - where profile_id = @profileId + where agent_id = @agentId and (valid_to is null or valid_to > @now) order by createdAt desc limit @limit`, ) - .all({ profileId, limit, now: this.now() }) as Array & { details: string }>; + .all({ agentId, limit, now: this.now() }) as Array & { details: string }>; return rows.map((row) => { const parsed = { ...row, - profileId, + agentId, embedding: JSON.parse((row as any).embedding) as number[], details: row.details ? JSON.parse(row.details) : undefined, eventStart: row.eventStart ?? null, @@ -406,6 +407,40 @@ export class SqliteMemoryStore { }); } + // --- Agent Management --- + + addAgent(id: string, opts?: { name?: string; soulMd?: string }): AgentInfo { + const agentId = normalizeAgentId(id); + const now = this.now(); + const name = opts?.name ?? agentId; + const soulMd = opts?.soulMd ?? null; + this.db + .prepare( + `INSERT INTO agents (id, name, soul_md, created_at) + VALUES (@id, @name, @soulMd, @createdAt) + ON CONFLICT(id) DO UPDATE SET + name = @name, + soul_md = @soulMd`, + ) + .run({ id: agentId, name, soulMd, createdAt: now }); + return { id: agentId, name, soulMd: soulMd ?? undefined, createdAt: now }; + } + + getAgent(agentId: string): AgentInfo | undefined { + const row = this.db + .prepare('SELECT id, name, soul_md as soulMd, created_at as createdAt FROM agents WHERE id = @id') + .get({ id: agentId }) as { id: string; name: string; soulMd: string | null; createdAt: number } | undefined; + if (!row) return undefined; + return { id: row.id, name: row.name, soulMd: row.soulMd ?? undefined, createdAt: row.createdAt }; + } + + getAgents(): AgentInfo[] { + const rows = this.db + .prepare('SELECT id, name, soul_md as soulMd, created_at as createdAt FROM agents ORDER BY id') + .all() as Array<{ id: string; name: string; soulMd: string | null; createdAt: number }>; + return rows.map((r) => ({ id: r.id, name: r.name, soulMd: r.soulMd ?? undefined, createdAt: r.createdAt })); + } + // --- Agent Users --- upsertAgentUser(agentId: string, userId: string): void { @@ -432,35 +467,71 @@ export class SqliteMemoryStore { .all({ agentId }) as Array<{ userId: string; firstSeen: number; lastSeen: number; interactionCount: number }>; } - getMemoriesForUser(profile: string, userId: string, limit: number = 50): (MemoryRecord & { details?: any })[] { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); + getMemoriesForUser(agent: string, userId: string, limit: number = 50): (MemoryRecord & { details?: any })[] { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); + const rows = this.db + .prepare( + `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount + from memory + where agent_id = @agentId and user_scope = @userId + union all + select id, 'procedural' as sector, trigger as content, embedding, created_at as createdAt, last_accessed as lastAccessed, + json_object('trigger', trigger, 'goal', goal, 'context', context, 'result', result, 'steps', json(steps)) as details, + null as eventStart, null as eventEnd, 0 as retrievalCount + from procedural_memory + where agent_id = @agentId and user_scope = @userId + union all + select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, + json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'domain', domain) as details, + null as eventStart, null as eventEnd, 0 as retrievalCount + from semantic_memory + where agent_id = @agentId and user_scope = @userId + and (valid_to is null or valid_to > @now) + order by createdAt desc + limit @limit`, + ) + .all({ agentId, userId, limit, now: this.now() }) as Array & { details: string }>; + + return rows.map((row) => ({ + ...row, + agentId, + embedding: JSON.parse((row as any).embedding) as number[], + details: row.details ? JSON.parse(row.details) : undefined, + eventStart: row.eventStart ?? null, + eventEnd: row.eventEnd ?? null, + })); + } + + getGlobalMemories(agent: string, limit: number = 50): (MemoryRecord & { details?: any })[] { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); const rows = this.db .prepare( `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount from memory - where profile_id = @profileId and user_scope = @userId + where agent_id = @agentId and user_scope is null union all select id, 'procedural' as sector, trigger as content, embedding, created_at as createdAt, last_accessed as lastAccessed, json_object('trigger', trigger, 'goal', goal, 'context', context, 'result', result, 'steps', json(steps)) as details, null as eventStart, null as eventEnd, 0 as retrievalCount from procedural_memory - where profile_id = @profileId and user_scope = @userId + where agent_id = @agentId and user_scope is null union all select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount from semantic_memory - where profile_id = @profileId and user_scope = @userId + where agent_id = @agentId and user_scope is null and (valid_to is null or valid_to > @now) order by createdAt desc limit @limit`, ) - .all({ profileId, userId, limit, now: this.now() }) as Array & { details: string }>; + .all({ agentId, limit, now: this.now() }) as Array & { details: string }>; return rows.map((row) => ({ ...row, - profileId, + agentId, embedding: JSON.parse((row as any).embedding) as number[], details: row.details ? JSON.parse(row.details) : undefined, eventStart: row.eventStart ?? null, @@ -474,10 +545,10 @@ export class SqliteMemoryStore { this.db .prepare( `INSERT INTO reflective_memory ( - id, observation, embedding, created_at, last_accessed, profile_id, source, + id, observation, embedding, created_at, last_accessed, agent_id, source, origin_type, origin_actor, origin_ref ) VALUES ( - @id, @observation, json(@embedding), @createdAt, @lastAccessed, @profileId, @source, + @id, @observation, json(@embedding), @createdAt, @lastAccessed, @agentId, @source, @originType, @originActor, @originRef )`, ) @@ -487,7 +558,7 @@ export class SqliteMemoryStore { embedding: JSON.stringify(row.embedding), createdAt: row.createdAt, lastAccessed: row.lastAccessed, - profileId: row.profileId ?? DEFAULT_PROFILE, + agentId: row.agentId ?? DEFAULT_AGENT, source: row.source ?? null, originType: row.originType ?? null, originActor: row.originActor ?? null, @@ -495,17 +566,17 @@ export class SqliteMemoryStore { }); } - getReflectiveRows(profileId: string, limit: number): ReflectiveMemoryRecord[] { + getReflectiveRows(agentId: string, limit: number): ReflectiveMemoryRecord[] { const rows = this.db .prepare( `SELECT id, observation, embedding, created_at as createdAt, last_accessed as lastAccessed, - profile_id as profileId, source, origin_type as originType, origin_actor as originActor, origin_ref as originRef + agent_id as agentId, source, origin_type as originType, origin_actor as originActor, origin_ref as originRef FROM reflective_memory - WHERE profile_id = @profileId + WHERE agent_id = @agentId ORDER BY last_accessed DESC LIMIT @limit`, ) - .all({ profileId, limit }) as any[]; + .all({ agentId, limit }) as any[]; return rows.map((row: any) => ({ ...row, @@ -516,7 +587,7 @@ export class SqliteMemoryStore { private buildEpisodicRow( content: string, embedding: number[], - profileId: string, + agentId: string, createdAt: number, source?: string, origin?: { originType?: string; originActor?: string; originRef?: string }, @@ -527,7 +598,7 @@ export class SqliteMemoryStore { sector: 'episodic' as SectorName, content, embedding, - profileId, + agentId, createdAt, lastAccessed: createdAt, eventStart: createdAt, @@ -568,10 +639,10 @@ export class SqliteMemoryStore { this.db .prepare( `insert into memory ( - id, sector, content, embedding, created_at, last_accessed, event_start, event_end, retrieval_count, profile_id, source, + id, sector, content, embedding, created_at, last_accessed, event_start, event_end, retrieval_count, agent_id, source, origin_type, origin_actor, origin_ref, user_scope ) values ( - @id, @sector, @content, json(@embedding), @createdAt, @lastAccessed, @eventStart, @eventEnd, @retrievalCount, @profileId, @source, + @id, @sector, @content, json(@embedding), @createdAt, @lastAccessed, @eventStart, @eventEnd, @retrievalCount, @agentId, @source, @originType, @originActor, @originRef, @userScope )`, ) @@ -585,7 +656,7 @@ export class SqliteMemoryStore { eventStart: row.eventStart ?? null, eventEnd: row.eventEnd ?? null, retrievalCount: (row as any).retrievalCount ?? DEFAULT_RETRIEVAL_COUNT, - profileId: row.profileId ?? DEFAULT_PROFILE, + agentId: row.agentId ?? DEFAULT_AGENT, source: row.source ?? null, originType: row.originType ?? null, originActor: row.originActor ?? null, @@ -598,10 +669,10 @@ export class SqliteMemoryStore { this.db .prepare( `insert into procedural_memory ( - id, trigger, goal, context, result, steps, embedding, created_at, last_accessed, profile_id, source, + id, trigger, goal, context, result, steps, embedding, created_at, last_accessed, agent_id, source, origin_type, origin_actor, origin_ref, user_scope ) values ( - @id, @trigger, @goal, @context, @result, json(@steps), json(@embedding), @createdAt, @lastAccessed, @profileId, @source, + @id, @trigger, @goal, @context, @result, json(@steps), json(@embedding), @createdAt, @lastAccessed, @agentId, @source, @originType, @originActor, @originRef, @userScope )`, ) @@ -615,7 +686,7 @@ export class SqliteMemoryStore { embedding: JSON.stringify(row.embedding), createdAt: row.createdAt, lastAccessed: row.lastAccessed, - profileId: row.profileId ?? DEFAULT_PROFILE, + agentId: row.agentId ?? DEFAULT_AGENT, source: row.source ?? null, originType: row.originType ?? null, originActor: row.originActor ?? null, @@ -628,10 +699,10 @@ export class SqliteMemoryStore { this.db .prepare( `insert into semantic_memory ( - id, subject, predicate, object, valid_from, valid_to, created_at, updated_at, embedding, metadata, profile_id, strength, source, + id, subject, predicate, object, valid_from, valid_to, created_at, updated_at, embedding, metadata, agent_id, strength, source, domain, origin_type, origin_actor, origin_ref, user_scope ) values ( - @id, @subject, @predicate, @object, @validFrom, @validTo, @createdAt, @updatedAt, json(@embedding), json(@metadata), @profileId, @strength, @source, + @id, @subject, @predicate, @object, @validFrom, @validTo, @createdAt, @updatedAt, json(@embedding), json(@metadata), @agentId, @strength, @source, @domain, @originType, @originActor, @originRef, @userScope )`, ) @@ -646,7 +717,7 @@ export class SqliteMemoryStore { updatedAt: row.updatedAt, embedding: JSON.stringify(row.embedding), metadata: row.metadata ? JSON.stringify(row.metadata) : null, - profileId: row.profileId ?? DEFAULT_PROFILE, + agentId: row.agentId ?? DEFAULT_AGENT, strength: row.strength ?? 1.0, source: row.source ?? null, domain: row.domain ?? null, @@ -663,10 +734,10 @@ export class SqliteMemoryStore { private findDuplicateMemory( embedding: number[], sector: SectorName, - profileId: string, + agentId: string, threshold: number, ): (MemoryRecord & { source?: string }) | null { - const candidates = this.getRowsForSector(sector, profileId, SECTOR_SCAN_LIMIT); + const candidates = this.getRowsForSector(sector, agentId, SECTOR_SCAN_LIMIT); for (const row of candidates) { if (cosineSimilarity(embedding, row.embedding) >= threshold) { return row as MemoryRecord & { source?: string }; @@ -680,10 +751,10 @@ export class SqliteMemoryStore { */ private findDuplicateProcedural( embedding: number[], - profileId: string, + agentId: string, threshold: number, ): (ProceduralMemoryRecord & { source?: string }) | null { - const candidates = this.getProceduralRows(profileId, SECTOR_SCAN_LIMIT); + const candidates = this.getProceduralRows(agentId, SECTOR_SCAN_LIMIT); for (const row of candidates) { if (cosineSimilarity(embedding, row.embedding) >= threshold) { return row as ProceduralMemoryRecord & { source?: string }; @@ -704,45 +775,45 @@ export class SqliteMemoryStore { this.db.prepare('UPDATE semantic_memory SET source = @source WHERE id = @id').run({ id, source }); } - private getCandidatesForSector(sector: SectorName, profileId: string, userId?: string): MemoryRecord[] { + private getCandidatesForSector(sector: SectorName, agentId: string, userId?: string): MemoryRecord[] { switch (sector) { case 'procedural': - return this.getProceduralRows(profileId, SECTOR_SCAN_LIMIT, userId).map((r) => ({ + return this.getProceduralRows(agentId, SECTOR_SCAN_LIMIT, userId).map((r) => ({ id: r.id, sector: 'procedural' as SectorName, content: r.trigger, embedding: r.embedding, - profileId: r.profileId, + agentId: r.agentId, createdAt: r.createdAt, lastAccessed: r.lastAccessed, details: { trigger: r.trigger, goal: r.goal, context: r.context, result: r.result, steps: r.steps }, } as any)); case 'semantic': - return this.getSemanticRows(profileId, SECTOR_SCAN_LIMIT, userId).map((r) => ({ + return this.getSemanticRows(agentId, SECTOR_SCAN_LIMIT, userId).map((r) => ({ id: r.id, sector: 'semantic' as SectorName, content: `${r.subject} ${r.predicate} ${r.object}`, embedding: r.embedding ?? [], - profileId: r.profileId, + agentId: r.agentId, createdAt: r.createdAt, lastAccessed: r.updatedAt, details: { subject: r.subject, predicate: r.predicate, object: r.object, validFrom: r.validFrom, validTo: r.validTo, domain: r.domain }, } as any)); default: - return this.getRowsForSector(sector, profileId, SECTOR_SCAN_LIMIT, userId); + return this.getRowsForSector(sector, agentId, SECTOR_SCAN_LIMIT, userId); } } - private getRowsForSector(sector: SectorName, profileId: string, limit: number, userId?: string): MemoryRecord[] { + private getRowsForSector(sector: SectorName, agentId: string, limit: number, userId?: string): MemoryRecord[] { const userFilter = userId ? 'and (user_scope is null or user_scope = @userId)' : ''; const rows = this.db.prepare( `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, - event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, profile_id as profileId, source + event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, agent_id as agentId, source from memory - where sector = @sector and profile_id = @profileId ${userFilter} + where sector = @sector and agent_id = @agentId ${userFilter} order by last_accessed desc limit @limit`, - ).all({ sector, limit, profileId, userId }) as Array>; + ).all({ sector, limit, agentId, userId }) as Array>; return rows.map((row) => ({ ...row, @@ -750,20 +821,20 @@ export class SqliteMemoryStore { })); } - private getSemanticRows(profileId: string, limit: number, userId?: string): SemanticMemoryRecord[] { + private getSemanticRows(agentId: string, limit: number, userId?: string): SemanticMemoryRecord[] { const now = this.now(); const userFilter = userId ? 'and (user_scope is null or user_scope = @userId)' : ''; const rows = this.db.prepare( `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, - created_at as createdAt, updated_at as updatedAt, embedding, metadata, profile_id as profileId, + created_at as createdAt, updated_at as updatedAt, embedding, metadata, agent_id as agentId, strength, domain from semantic_memory - where profile_id = @profileId + where agent_id = @agentId and (valid_to is null or valid_to > @now) ${userFilter} order by strength desc, updated_at desc limit @limit`, - ).all({ limit, profileId, now, userId }) as any[]; + ).all({ limit, agentId, now, userId }) as any[]; return rows.map((row: any) => ({ ...row, @@ -773,15 +844,15 @@ export class SqliteMemoryStore { })); } - private getProceduralRows(profileId: string, limit: number, userId?: string): ProceduralMemoryRecord[] { + private getProceduralRows(agentId: string, limit: number, userId?: string): ProceduralMemoryRecord[] { const userFilter = userId ? 'and (user_scope is null or user_scope = @userId)' : ''; const rows = this.db.prepare( - `select id, trigger, goal, context, result, steps, embedding, created_at as createdAt, last_accessed as lastAccessed, profile_id as profileId, source + `select id, trigger, goal, context, result, steps, embedding, created_at as createdAt, last_accessed as lastAccessed, agent_id as agentId, source from procedural_memory - where profile_id = @profileId ${userFilter} + where agent_id = @agentId ${userFilter} order by last_accessed desc limit @limit`, - ).all({ limit, profileId, userId }) as Array>; + ).all({ limit, agentId, userId }) as Array>; return rows.map((row) => ({ ...row, @@ -820,12 +891,19 @@ export class SqliteMemoryStore { event_start integer, event_end integer, retrieval_count integer not null default 0, - profile_id text not null default '${DEFAULT_PROFILE}' + agent_id text not null default '${DEFAULT_AGENT}', + source text, + origin_type text, + origin_actor text, + origin_ref text, + user_scope text )`, ) .run(); this.db.prepare('create index if not exists idx_memory_sector on memory(sector)').run(); this.db.prepare('create index if not exists idx_memory_last_accessed on memory(last_accessed)').run(); + this.db.prepare('create index if not exists idx_memory_agent_sector on memory(agent_id, sector, last_accessed)').run(); + this.db.prepare('create index if not exists idx_memory_user_scope on memory(user_scope)').run(); this.db .prepare( @@ -839,11 +917,18 @@ export class SqliteMemoryStore { embedding json not null, created_at integer not null, last_accessed integer not null, - profile_id text not null default '${DEFAULT_PROFILE}' + agent_id text not null default '${DEFAULT_AGENT}', + source text, + origin_type text, + origin_actor text, + origin_ref text, + user_scope text )`, ) .run(); this.db.prepare('create index if not exists idx_proc_last_accessed on procedural_memory(last_accessed)').run(); + this.db.prepare('create index if not exists idx_proc_agent on procedural_memory(agent_id, last_accessed)').run(); + this.db.prepare('create index if not exists idx_proc_user_scope on procedural_memory(user_scope)').run(); this.db .prepare( @@ -858,14 +943,24 @@ export class SqliteMemoryStore { updated_at integer not null, embedding json not null, metadata json, - profile_id text not null default '${DEFAULT_PROFILE}' + agent_id text not null default '${DEFAULT_AGENT}', + strength real not null default 1.0, + source text, + domain text, + origin_type text, + origin_actor text, + origin_ref text, + user_scope text )`, ) .run(); this.db.prepare('create index if not exists idx_semantic_subject_pred on semantic_memory(subject, predicate)').run(); this.db.prepare('create index if not exists idx_semantic_object on semantic_memory(object)').run(); + this.db.prepare('create index if not exists idx_semantic_agent on semantic_memory(agent_id, updated_at)').run(); + this.db.prepare('create index if not exists idx_semantic_slot on semantic_memory(subject, predicate, agent_id)').run(); + this.db.prepare('create index if not exists idx_semantic_user_scope on semantic_memory(user_scope)').run(); + this.db.prepare('create index if not exists idx_semantic_domain on semantic_memory(domain)').run(); - // New tables this.db .prepare( `create table if not exists reflective_memory ( @@ -874,7 +969,7 @@ export class SqliteMemoryStore { embedding json not null, created_at integer not null, last_accessed integer not null, - profile_id text not null default '${DEFAULT_PROFILE}', + agent_id text not null default '${DEFAULT_AGENT}', source text, origin_type text, origin_actor text, @@ -882,7 +977,7 @@ export class SqliteMemoryStore { )`, ) .run(); - this.db.prepare('create index if not exists idx_reflective_profile on reflective_memory(profile_id, last_accessed)').run(); + this.db.prepare('create index if not exists idx_reflective_agent on reflective_memory(agent_id, last_accessed)').run(); this.db .prepare( @@ -898,78 +993,25 @@ export class SqliteMemoryStore { .run(); this.db.prepare('create index if not exists idx_agent_users_agent on agent_users(agent_id)').run(); - this.ensureProfileColumns(); - this.ensureProfileIndexes(); - this.ensureProfilesTable(); - this.ensureStrengthColumn(); - this.ensureSourceColumn(); - this.ensureAttributionColumns(); - } - - /** - * Add strength column to semantic_memory if it doesn't exist. - * Strength tracks evidence count from consolidation (starts at 1.0). - */ - private ensureStrengthColumn() { - const columns = this.db.prepare('PRAGMA table_info(semantic_memory)').all() as Array<{ name: string }>; - if (!columns.some((c) => c.name === 'strength')) { - this.db.prepare('ALTER TABLE semantic_memory ADD COLUMN strength REAL NOT NULL DEFAULT 1.0').run(); - } - // Index for slot-based lookups (subject + predicate) this.db - .prepare('CREATE INDEX IF NOT EXISTS idx_semantic_slot ON semantic_memory(subject, predicate, profile_id)') + .prepare( + `create table if not exists agents ( + id text primary key, + name text not null, + soul_md text, + created_at integer not null + )`, + ) .run(); - } - /** - * Add source column to all 3 tables if it doesn't exist. - * Stores the relative file path for document-ingested memories. - */ - private ensureSourceColumn() { - const tables = ['memory', 'procedural_memory', 'semantic_memory']; - for (const table of tables) { - const columns = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>; - if (!columns.some((c) => c.name === 'source')) { - this.db.prepare(`ALTER TABLE ${table} ADD COLUMN source TEXT DEFAULT NULL`).run(); - } - } + // Ensure the default agent always exists + this.ensureAgentExists(DEFAULT_AGENT); } - /** - * Add attribution and user_scope columns to all memory tables. - * origin_type, origin_actor, origin_ref track where a memory came from. - * user_scope limits visibility to a specific user. - * domain on semantic_memory classifies facts. - */ - private ensureAttributionColumns() { - const tablesWithUserScope = ['memory', 'procedural_memory', 'semantic_memory']; - for (const table of tablesWithUserScope) { - const columns = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>; - if (!columns.some((c) => c.name === 'origin_type')) { - this.db.prepare(`ALTER TABLE ${table} ADD COLUMN origin_type TEXT DEFAULT NULL`).run(); - } - if (!columns.some((c) => c.name === 'origin_actor')) { - this.db.prepare(`ALTER TABLE ${table} ADD COLUMN origin_actor TEXT DEFAULT NULL`).run(); - } - if (!columns.some((c) => c.name === 'origin_ref')) { - this.db.prepare(`ALTER TABLE ${table} ADD COLUMN origin_ref TEXT DEFAULT NULL`).run(); - } - if (!columns.some((c) => c.name === 'user_scope')) { - this.db.prepare(`ALTER TABLE ${table} ADD COLUMN user_scope TEXT DEFAULT NULL`).run(); - } - } - - // Domain column on semantic_memory only - const semCols = this.db.prepare('PRAGMA table_info(semantic_memory)').all() as Array<{ name: string }>; - if (!semCols.some((c) => c.name === 'domain')) { - this.db.prepare("ALTER TABLE semantic_memory ADD COLUMN domain TEXT DEFAULT NULL").run(); - } - - // Indexes for user_scope filtering - this.db.prepare('CREATE INDEX IF NOT EXISTS idx_memory_user_scope ON memory(user_scope)').run(); - this.db.prepare('CREATE INDEX IF NOT EXISTS idx_proc_user_scope ON procedural_memory(user_scope)').run(); - this.db.prepare('CREATE INDEX IF NOT EXISTS idx_semantic_user_scope ON semantic_memory(user_scope)').run(); - this.db.prepare('CREATE INDEX IF NOT EXISTS idx_semantic_domain ON semantic_memory(domain)').run(); + ensureAgentExists(agentId: string) { + this.db + .prepare('insert or ignore into agents (id, name, created_at) values (@id, @name, @createdAt)') + .run({ id: agentId, name: agentId, createdAt: this.now() }); } /** @@ -978,7 +1020,7 @@ export class SqliteMemoryStore { */ findActiveFactsForSubject( subject: string, - profileId: string + agentId: string ): Array<{ id: string; predicate: string; object: string; updatedAt: number }> { const now = this.now(); const rows = this.db @@ -986,11 +1028,11 @@ export class SqliteMemoryStore { `SELECT id, predicate, object, updated_at as updatedAt FROM semantic_memory WHERE subject = @subject - AND profile_id = @profileId + AND agent_id = @agentId AND (valid_to IS NULL OR valid_to > @now) ORDER BY updated_at DESC` ) - .all({ subject, profileId, now }) as Array<{ id: string; predicate: string; object: string; updatedAt: number }>; + .all({ subject, agentId, now }) as Array<{ id: string; predicate: string; object: string; updatedAt: number }>; return rows; } @@ -1072,71 +1114,13 @@ export class SqliteMemoryStore { .run({ id, now: this.now() }); } - /** - * Backfills profile_id column for existing databases and creates profile-aware indexes. - */ - private ensureProfileColumns() { - const tables = ['memory', 'procedural_memory', 'semantic_memory']; - for (const table of tables) { - const columns = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>; - const hasProfile = columns.some((c) => c.name === 'profile_id'); - if (!hasProfile) { - this.db - .prepare(`ALTER TABLE ${table} ADD COLUMN profile_id text not null default '${DEFAULT_PROFILE}'`) - .run(); - } - } - } - - private ensureProfileIndexes() { - this.db - .prepare('create index if not exists idx_memory_profile_sector on memory(profile_id, sector, last_accessed)') - .run(); - this.db - .prepare('create index if not exists idx_proc_profile on procedural_memory(profile_id, last_accessed)') - .run(); - this.db - .prepare('create index if not exists idx_semantic_profile on semantic_memory(profile_id, updated_at)') - .run(); - } - - private ensureProfilesTable() { - this.db - .prepare( - `create table if not exists profiles ( - slug text primary key, - created_at integer not null - )`, - ) - .run(); - - this.ensureProfileExists(DEFAULT_PROFILE); - - // One-time backfill of existing profile ids into profiles table - const now = this.now(); - const backfill = [ - 'insert or ignore into profiles (slug, created_at) select distinct profile_id, @now from memory', - 'insert or ignore into profiles (slug, created_at) select distinct profile_id, @now from procedural_memory', - 'insert or ignore into profiles (slug, created_at) select distinct profile_id, @now from semantic_memory', - ]; - for (const sql of backfill) { - this.db.prepare(sql).run({ now }); - } - } - - private ensureProfileExists(profileId: string) { - this.db - .prepare('insert or ignore into profiles (slug, created_at) values (@slug, @createdAt)') - .run({ slug: profileId, createdAt: this.now() }); - } - - async updateById(id: string, content: string, sector?: SectorName, profile?: string): Promise { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); + async updateById(id: string, content: string, sector?: SectorName, agent?: string): Promise { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); // First, get the existing record to preserve sector if not changing const existing = this.db - .prepare('select sector from memory where id = ? and profile_id = ?') - .get(id, profileId) as { sector: SectorName } | undefined; + .prepare('select sector from memory where id = ? and agent_id = ?') + .get(id, agentId) as { sector: SectorName } | undefined; if (!existing) { return false; @@ -1149,7 +1133,7 @@ export class SqliteMemoryStore { // Update the record const stmt = this.db.prepare( - 'update memory set content = ?, sector = ?, embedding = json(?), last_accessed = ? where id = ? and profile_id = ?' + 'update memory set content = ?, sector = ?, embedding = json(?), last_accessed = ? where id = ? and agent_id = ?' ); const res = stmt.run( content, @@ -1157,58 +1141,53 @@ export class SqliteMemoryStore { JSON.stringify(embedding), this.now(), id, - profileId, + agentId, ); return res.changes > 0; } - deleteById(id: string, profile?: string): number { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); - const res1 = this.db.prepare('delete from memory where id = ? and profile_id = ?').run(id, profileId); - const res2 = this.db.prepare('delete from procedural_memory where id = ? and profile_id = ?').run(id, profileId); - const res3 = this.db.prepare('delete from semantic_memory where id = ? and profile_id = ?').run(id, profileId); - const res4 = this.db.prepare('delete from reflective_memory where id = ? and profile_id = ?').run(id, profileId); + deleteById(id: string, agent?: string): number { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); + const res1 = this.db.prepare('delete from memory where id = ? and agent_id = ?').run(id, agentId); + const res2 = this.db.prepare('delete from procedural_memory where id = ? and agent_id = ?').run(id, agentId); + const res3 = this.db.prepare('delete from semantic_memory where id = ? and agent_id = ?').run(id, agentId); + const res4 = this.db.prepare('delete from reflective_memory where id = ? and agent_id = ?').run(id, agentId); return (res1.changes ?? 0) + (res2.changes ?? 0) + (res3.changes ?? 0) + (res4.changes ?? 0); } - deleteAll(profile?: string): number { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); - const res1 = this.db.prepare('delete from memory where profile_id = ?').run(profileId); - const res2 = this.db.prepare('delete from procedural_memory where profile_id = ?').run(profileId); - const res3 = this.db.prepare('delete from semantic_memory where profile_id = ?').run(profileId); - const res4 = this.db.prepare('delete from reflective_memory where profile_id = ?').run(profileId); + deleteAll(agent?: string): number { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); + const res1 = this.db.prepare('delete from memory where agent_id = ?').run(agentId); + const res2 = this.db.prepare('delete from procedural_memory where agent_id = ?').run(agentId); + const res3 = this.db.prepare('delete from semantic_memory where agent_id = ?').run(agentId); + const res4 = this.db.prepare('delete from reflective_memory where agent_id = ?').run(agentId); return (res1.changes ?? 0) + (res2.changes ?? 0) + (res3.changes ?? 0) + (res4.changes ?? 0); } - deleteProfile(profile?: string): number { - const profileId = normalizeProfileSlug(profile); - if (profileId === DEFAULT_PROFILE) { - throw new Error('cannot_delete_default_profile'); + deleteAgent(agent?: string): number { + const agentId = normalizeAgentId(agent); + if (agentId === DEFAULT_AGENT) { + throw new Error('cannot_delete_default_agent'); } - // Delete all memories for this profile - const deletedCount = this.deleteAll(profileId); - // Delete the profile itself from the profiles table - const res = this.db.prepare('delete from profiles where slug = ?').run(profileId); - // Delete agent_users entries for this profile - this.db.prepare('delete from agent_users where agent_id = ?').run(profileId); + // Delete all memories for this agent + const deletedCount = this.deleteAll(agentId); + // Delete the agent itself from the agents table + this.db.prepare('delete from agents where id = ?').run(agentId); + // Delete agent_users entries for this agent + this.db.prepare('delete from agent_users where agent_id = ?').run(agentId); return deletedCount; } - deleteSemanticById(id: string, profile?: string): number { - const profileId = normalizeProfileSlug(profile); - this.ensureProfileExists(profileId); - const res = this.db.prepare('delete from semantic_memory where id = ? and profile_id = ?').run(id, profileId); + deleteSemanticById(id: string, agent?: string): number { + const agentId = normalizeAgentId(agent); + this.ensureAgentExists(agentId); + const res = this.db.prepare('delete from semantic_memory where id = ? and agent_id = ?').run(id, agentId); return res.changes ?? 0; } - getAvailableProfiles(): string[] { - const rows = this.db.prepare('select slug from profiles order by slug').all() as Array<{ slug: string }>; - return rows.map((r) => r.slug); - } - private bumpRetrievalCounts(ids: string[]) { if (!ids.length) return; const placeholders = ids.map(() => '?').join(','); diff --git a/memory/src/types.ts b/memory/src/types.ts index 1c5de74..1cb5601 100644 --- a/memory/src/types.ts +++ b/memory/src/types.ts @@ -2,6 +2,21 @@ export type SectorName = 'episodic' | 'semantic' | 'procedural'; export type SemanticDomain = 'user' | 'world' | 'self'; +export type ProviderName = 'gemini' | 'openai' | 'openrouter'; + +export interface AgentInfo { + id: string; + name: string; + soulMd?: string; + createdAt: number; +} + +export interface MemoryFilterOptions { + userId?: string; + scope?: 'global'; + limit?: number; +} + export interface SemanticTripleInput { subject: string; predicate: string; @@ -37,7 +52,7 @@ export interface MemoryRecord { sector: SectorName; content: string; embedding: number[]; - profileId: string; + agentId: string; createdAt: number; lastAccessed: number; eventStart?: number | null; @@ -52,7 +67,7 @@ export interface MemoryRecord { export interface ProceduralMemoryRecord { id: string; trigger: string; - profileId: string; + agentId: string; goal?: string; context?: string; result?: string; @@ -72,7 +87,7 @@ export interface SemanticMemoryRecord { subject: string; predicate: string; object: string; - profileId: string; + agentId: string; embedding: number[]; validFrom: number; validTo: number | null; @@ -91,7 +106,7 @@ export interface SemanticMemoryRecord { export interface ReflectiveMemoryRecord { id: string; observation: string; - profileId: string; + agentId: string; embedding: number[]; createdAt: number; lastAccessed: number; @@ -112,7 +127,7 @@ export type ConsolidationAction = export interface QueryResult { sector: SectorName; id: string; - profileId: string; + agentId: string; content: string; score: number; similarity: number; @@ -123,12 +138,14 @@ export interface QueryResult { export type EmbedFn = (input: string, sector: SectorName) => Promise; +export type ExtractFn = (text: string) => Promise; + export interface GraphTraversalOptions { maxDepth?: number; maxResults?: number; includeInvalidated?: boolean; predicateFilter?: string; - profile?: string; + agent?: string; userId?: string; } diff --git a/memory/src/utils.ts b/memory/src/utils.ts index 32dbb60..646154e 100644 --- a/memory/src/utils.ts +++ b/memory/src/utils.ts @@ -23,19 +23,19 @@ export function gaussianNoise(mean: number, std: number): number { return mean + std * z0; } -export const DEFAULT_PROFILE = 'default'; +export const DEFAULT_AGENT = 'default'; /** - * Normalize a human-friendly profile string to a slug we can safely index on. + * Normalize a human-friendly agent string to a slug we can safely index on. * Falls back to "default" when empty. Throws on invalid input. */ -export function normalizeProfileSlug(profile?: string | null): string { - const trimmed = profile?.trim(); - if (!trimmed) return DEFAULT_PROFILE; +export function normalizeAgentId(agent?: string | null): string { + const trimmed = agent?.trim(); + if (!trimmed) return DEFAULT_AGENT; const slug = trimmed.toLowerCase(); if (!/^[a-z0-9_-]{1,40}$/.test(slug)) { - throw new Error('invalid_profile'); + throw new Error('invalid_agent'); } return slug; } diff --git a/memory/tests/store.test.ts b/memory/tests/store.test.ts index 9869590..69aa5ab 100644 --- a/memory/tests/store.test.ts +++ b/memory/tests/store.test.ts @@ -112,7 +112,7 @@ describe('SqliteMemoryStore (single-user mode)', () => { // Structure expect(result).toHaveProperty('workingMemory'); expect(result).toHaveProperty('perSector'); - expect(result).toHaveProperty('profileId'); + expect(result).toHaveProperty('agentId'); expect(Array.isArray(result.workingMemory)).toBe(true); // perSector has all 3 active sector keys (reflective is disabled) @@ -350,9 +350,13 @@ describe('SqliteMemoryStore (single-user mode)', () => { } }); - // ─── 10. Profile isolation (dashboard profile selector) ─────────── - it('isolates memories by profile and getAvailableProfiles returns both', async () => { - // Ingest for profile "agent-a" + // ─── 10. Agent isolation (agent selector) ─────────── + it('isolates memories by agent and getAgents returns both', async () => { + // Create agents explicitly + store.addAgent('agent-a', { name: 'Agent A' }); + store.addAgent('agent-b', { name: 'Agent B' }); + + // Ingest for agent "agent-a" await store.ingest( { episodic: 'deployed v2 to staging', @@ -361,7 +365,7 @@ describe('SqliteMemoryStore (single-user mode)', () => { 'agent-a', ); - // Ingest for profile "agent-b" + // Ingest for agent "agent-b" await store.ingest( { episodic: 'TypeScript', @@ -372,27 +376,82 @@ describe('SqliteMemoryStore (single-user mode)', () => { // Query agent-a should return only its memories const resultA = await store.query('dark mode', 'agent-a'); - expect(resultA.profileId).toBe('agent-a'); + expect(resultA.agentId).toBe('agent-a'); // Episodic and semantic should only come from agent-a for (const sector of ['episodic', 'semantic'] as SectorName[]) { for (const row of resultA.perSector[sector]) { - expect(row.profileId).toBe('agent-a'); + expect(row.agentId).toBe('agent-a'); } } // Query agent-b should return only its memories const resultB = await store.query('TypeScript', 'agent-b'); - expect(resultB.profileId).toBe('agent-b'); + expect(resultB.agentId).toBe('agent-b'); for (const sector of ['episodic', 'semantic'] as SectorName[]) { for (const row of resultB.perSector[sector]) { - expect(row.profileId).toBe('agent-b'); + expect(row.agentId).toBe('agent-b'); } } - // getAvailableProfiles returns both (plus 'default') - const profiles = store.getAvailableProfiles(); - expect(profiles).toContain('agent-a'); - expect(profiles).toContain('agent-b'); - expect(profiles).toContain('default'); + // getAgents returns both (plus 'default') + const agents = store.getAgents(); + const agentIds = agents.map((a) => a.id); + expect(agentIds).toContain('agent-a'); + expect(agentIds).toContain('agent-b'); + expect(agentIds).toContain('default'); + }); + + // ─── 11. addAgent and getAgent ─────────────────────────────────── + it('addAgent creates agent and getAgent retrieves it', () => { + const agent = store.addAgent('test-bot', { name: 'Test Bot', soulMd: 'You are helpful' }); + expect(agent.id).toBe('test-bot'); + expect(agent.name).toBe('Test Bot'); + expect(agent.soulMd).toBe('You are helpful'); + expect(agent.createdAt).toBe(NOW); + + const retrieved = store.getAgent('test-bot'); + expect(retrieved).toBeDefined(); + expect(retrieved!.id).toBe('test-bot'); + expect(retrieved!.name).toBe('Test Bot'); + expect(retrieved!.soulMd).toBe('You are helpful'); + }); + + // ─── 12. addAgent upserts on conflict ───────────────────────────── + it('addAgent upserts name and soulMd on conflict', () => { + store.addAgent('my-bot', { name: 'V1' }); + store.addAgent('my-bot', { name: 'V2', soulMd: 'Updated soul' }); + + const agent = store.getAgent('my-bot'); + expect(agent!.name).toBe('V2'); + expect(agent!.soulMd).toBe('Updated soul'); + }); + + // ─── 13. getAgent returns undefined for non-existent ───────────── + it('getAgent returns undefined for non-existent agent', () => { + const agent = store.getAgent('does-not-exist'); + expect(agent).toBeUndefined(); + }); + + // ─── 14. deleteAgent removes agent and all its data ────────────── + it('deleteAgent removes agent and its memories', async () => { + store.addAgent('temp-agent'); + await store.ingest( + { episodic: 'deployed v2 to staging' }, + 'temp-agent', + ); + + const summary = store.getSectorSummary('temp-agent'); + expect(summary.find((s) => s.sector === 'episodic')!.count).toBe(1); + + store.deleteAgent('temp-agent'); + + // Agent should be gone + const agent = store.getAgent('temp-agent'); + expect(agent).toBeUndefined(); + }); + + // ─── 15. Cannot delete default agent ────────────────────────────── + it('throws when trying to delete default agent', () => { + expect(() => store.deleteAgent('default')).toThrow('cannot_delete_default_agent'); }); }); From db8facdba895f33bbb59645e0d55d063cad8865b Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:41:03 +0530 Subject: [PATCH 51/78] Separate provider config from agent scoping Remove `agent` from MemoryConfig constructor. Add `mem.agent(id)` method that returns a scoped instance sharing the same store. Provider config is global, agent scoping is per-instance. --- integrations/openrouter/src/server.ts | 6 +++--- memory/README.md | 31 ++++++++++++++------------- memory/src/memory.ts | 20 +++++++++++++---- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts index bda52c9..89cdaab 100644 --- a/integrations/openrouter/src/server.ts +++ b/integrations/openrouter/src/server.ts @@ -21,16 +21,16 @@ app.use(express.json({ limit: '10mb' })); // Initialize embedded memory with explicit provider config const agentId = process.env.MEMORY_AGENT ?? 'default'; -const memory = new Memory({ +const mem = new Memory({ provider: 'openrouter', apiKey: OPENROUTER_API_KEY, dbPath: MEMORY_DB_PATH, - agent: agentId, }); +const memory = mem.agent(agentId); initMemory(memory); // Mount memory admin routes (dashboard, graph, etc.) -app.use(createMemoryRouter(memory._store, memory._extractFn)); +app.use(createMemoryRouter(mem._store, mem._extractFn)); app.get('/health', (_req, res) => { res.json({ status: 'ok' }); diff --git a/memory/README.md b/memory/README.md index 28e747c..cb3342d 100644 --- a/memory/README.md +++ b/memory/README.md @@ -18,12 +18,14 @@ See [Usage Modes](#usage-modes) below for SDK, mountable router, and standalone ```ts import { Memory } from '@ekai/memory'; -// Setup — provider config is explicit, no env vars needed +// Provider config is global — shared across all agents const mem = new Memory({ provider: 'openai', apiKey: 'sk-...' }); + +// Register an agent (soul is optional) mem.addAgent('my-bot', { name: 'My Bot', soul: 'You are helpful' }); -// Scoped instance — all data ops are agent-scoped -const bot = new Memory({ provider: 'openai', apiKey: 'sk-...', agent: 'my-bot' }); +// Get a scoped instance — all data ops go through this +const bot = mem.agent('my-bot'); await bot.add(messages, { userId: 'alice' }); await bot.search('preferences', { userId: 'alice' }); bot.users(); // agent's known users @@ -52,22 +54,21 @@ bot.delete(id); ```ts import { Memory } from '@ekai/memory'; -const mem = new Memory({ - provider: 'openai', - apiKey: 'sk-...', - agent: 'my-bot', -}); +// Provider config is global +const mem = new Memory({ provider: 'openai', apiKey: 'sk-...' }); -// Management (no agent scope needed) +// Register agents (soul is optional) mem.addAgent('my-bot', { name: 'My Bot', soul: 'You are helpful' }); +mem.addAgent('support-bot', { name: 'Support Bot' }); mem.getAgents(); -// Data ops (require agent scope) -await mem.add(messages, { userId: 'alice' }); -await mem.search('query', { userId: 'alice' }); -mem.users(); -mem.memories({ userId: 'alice' }); -mem.delete(id); +// Scope to an agent for data ops +const bot = mem.agent('my-bot'); +await bot.add(messages, { userId: 'alice' }); +await bot.search('query', { userId: 'alice' }); +bot.users(); +bot.memories({ userId: 'alice' }); +bot.delete(id); ``` ### 2. Mountable router diff --git a/memory/src/memory.ts b/memory/src/memory.ts index f699951..aadcc06 100644 --- a/memory/src/memory.ts +++ b/memory/src/memory.ts @@ -14,7 +14,6 @@ export interface MemoryConfig { provider?: ProviderName; apiKey?: string; dbPath?: string; - agent?: string; // scopes all data ops; omit for management-only embedModel?: string; extractModel?: string; } @@ -25,8 +24,6 @@ export class Memory { private agentId: string | undefined; constructor(config?: MemoryConfig) { - this.agentId = config?.agent; - // Build embed/extract functions: use explicit provider config if given, else fall back to env-based const embedFn = (config?.provider && config?.apiKey) ? createEmbedFn({ provider: config.provider, apiKey: config.apiKey, embedModel: config.embedModel }) @@ -42,8 +39,18 @@ export class Memory { }); } + /** Create an internal Memory sharing this store, scoped to an agent. */ + private static _scoped(store: SqliteMemoryStore, extractFn: ExtractFn, agentId: string): Memory { + const m = Object.create(Memory.prototype) as Memory; + m.store = store; + m.extractFn = extractFn; + m.agentId = agentId; + return m; + } + // --- Management (always available) --- + /** Register an agent. `soul` is optional agent-level context/personality. */ addAgent(id: string, opts?: { name?: string; soul?: string }): AgentInfo { return this.store.addAgent(id, { name: opts?.name, soulMd: opts?.soul }); } @@ -52,11 +59,16 @@ export class Memory { return this.store.getAgents(); } + /** Return an agent-scoped instance sharing the same store. */ + agent(id: string): Memory { + return Memory._scoped(this.store, this.extractFn, id); + } + // --- Data ops (require agent scope) --- private requireAgent(): string { if (!this.agentId) { - throw new Error('agent_scope_required: create Memory with { agent: "..." } for data ops'); + throw new Error('agent_scope_required: use mem.agent("id") for data ops'); } return this.agentId; } From 5fd99109272f14589f3440d4a7d4d6547368ca8b Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:43:47 +0530 Subject: [PATCH 52/78] Remove /v1/ingest/documents endpoint and documents.ts --- memory/README.md | 9 -- memory/src/documents.ts | 189 ---------------------------------------- memory/src/index.ts | 2 +- memory/src/router.ts | 38 +------- 4 files changed, 2 insertions(+), 236 deletions(-) delete mode 100644 memory/src/documents.ts diff --git a/memory/README.md b/memory/README.md index cb3342d..3a507a3 100644 --- a/memory/README.md +++ b/memory/README.md @@ -227,14 +227,6 @@ GET /v1/summary?agent=my-bot&limit=20 } ``` -### `POST /v1/ingest/documents` - -Ingest markdown files from a directory with deduplication. - -```json -{ "path": "/path/to/docs", "agent": "my-bot" } -``` - ### `GET /v1/users` List all users the agent has interacted with. @@ -257,7 +249,6 @@ Get all memories scoped to a specific user. | Method | Endpoint | Description | |--------|----------|-------------| | POST | `/v1/ingest` | Ingest conversation | -| POST | `/v1/ingest/documents` | Ingest markdown directory | | POST | `/v1/search` | Search with PBWM gating | | GET | `/v1/summary` | Sector counts + recent | | GET | `/v1/users` | List agent's users | diff --git a/memory/src/documents.ts b/memory/src/documents.ts deleted file mode 100644 index 8d64d1b..0000000 --- a/memory/src/documents.ts +++ /dev/null @@ -1,189 +0,0 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; -import type { SqliteMemoryStore } from './sqlite-store.js'; -import type { ExtractFn } from './types.js'; -import { extract as defaultExtract } from './providers/extract.js'; -import { normalizeAgentId } from './utils.js'; - -const MAX_CHUNK_CHARS = 12_000; - -interface Chunk { - text: string; - source: string; - index: number; -} - -export interface IngestDocumentsResult { - ingested: number; - chunks: number; - stored: number; - skipped: number; - errors: string[]; - agent: string; -} - -/** - * Strip YAML frontmatter (---...---) from the beginning of markdown content. - */ -function stripFrontmatter(content: string): string { - const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/); - return match ? content.slice(match[0].length) : content; -} - -/** - * Split markdown content into chunks by headings, with paragraph-level sub-splitting - * for sections that exceed MAX_CHUNK_CHARS. - */ -export function chunkMarkdown(content: string, filePath: string): Chunk[] { - const stripped = stripFrontmatter(content).trim(); - if (!stripped) return []; - - // Split on headings (# through ###) - const sections: Array<{ heading: string; body: string }> = []; - const headingRegex = /^(#{1,3})\s+(.+)$/gm; - let lastIndex = 0; - let lastHeading = ''; - let match: RegExpExecArray | null; - - while ((match = headingRegex.exec(stripped)) !== null) { - // Capture text before this heading - if (match.index > lastIndex) { - const body = stripped.slice(lastIndex, match.index).trim(); - if (body) { - sections.push({ heading: lastHeading, body }); - } - } - lastHeading = match[0]; - lastIndex = match.index + match[0].length; - } - - // Capture remaining text after last heading - const remaining = stripped.slice(lastIndex).trim(); - if (remaining) { - sections.push({ heading: lastHeading, body: remaining }); - } - - // If no headings were found, treat entire content as one section - if (sections.length === 0) { - sections.push({ heading: '', body: stripped }); - } - - // Build chunks, splitting large sections at paragraph boundaries - const chunks: Chunk[] = []; - let chunkIndex = 0; - - for (const section of sections) { - const fullText = section.heading ? `${section.heading}\n\n${section.body}` : section.body; - - if (fullText.length <= MAX_CHUNK_CHARS) { - chunks.push({ text: fullText, source: filePath, index: chunkIndex++ }); - } else { - // Split at paragraph boundaries - const paragraphs = section.body.split(/\n\n+/); - let current = section.heading ? `${section.heading}\n\n` : ''; - - for (const para of paragraphs) { - if (current.length + para.length + 2 > MAX_CHUNK_CHARS && current.trim()) { - chunks.push({ text: current.trim(), source: filePath, index: chunkIndex++ }); - current = section.heading ? `${section.heading} (cont.)\n\n` : ''; - } - current += para + '\n\n'; - } - - if (current.trim()) { - chunks.push({ text: current.trim(), source: filePath, index: chunkIndex++ }); - } - } - } - - return chunks; -} - -/** - * Recursively collect all .md files from a directory path, sorted alphabetically. - * If path is a single file, return it. - */ -async function collectMarkdownFiles(dirPath: string): Promise { - const stat = await fs.stat(dirPath); - - if (stat.isFile()) { - if (dirPath.endsWith('.md')) return [dirPath]; - return []; - } - - const files: string[] = []; - const entries = await fs.readdir(dirPath, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name); - if (entry.isDirectory()) { - const nested = await collectMarkdownFiles(fullPath); - files.push(...nested); - } else if (entry.isFile() && entry.name.endsWith('.md')) { - files.push(fullPath); - } - } - - return files.sort(); -} - -/** - * Ingest markdown documents from a directory (or single file) into the memory store. - */ -export async function ingestDocuments( - dirPath: string, - store: SqliteMemoryStore, - agent?: string, - extractFn?: ExtractFn, -): Promise { - const resolvedPath = path.resolve(dirPath); - const files = await collectMarkdownFiles(resolvedPath); - const basePath = (await fs.stat(resolvedPath)).isDirectory() ? resolvedPath : path.dirname(resolvedPath); - const doExtract = extractFn ?? defaultExtract; - - let totalChunks = 0; - let totalStored = 0; - let totalSkipped = 0; - const errors: string[] = []; - - for (const filePath of files) { - const content = await fs.readFile(filePath, 'utf-8'); - if (!content.trim()) continue; - - const relativePath = path.relative(basePath, filePath); - const chunks = chunkMarkdown(content, relativePath); - totalChunks += chunks.length; - - for (const chunk of chunks) { - try { - const components = await doExtract(chunk.text); - if (!components) { - totalSkipped++; - continue; - } - - const rows = await store.ingest(components, agent, { - source: chunk.source, - deduplicate: true, - }); - - if (rows.length > 0) { - totalStored += rows.length; - } else { - totalSkipped++; - } - } catch (err: any) { - errors.push(`${chunk.source}[${chunk.index}]: ${err.message ?? 'extraction failed'}`); - } - } - } - - return { - ingested: files.length, - chunks: totalChunks, - stored: totalStored, - skipped: totalSkipped, - errors, - agent: normalizeAgentId(agent), - }; -} diff --git a/memory/src/index.ts b/memory/src/index.ts index f8ae5f6..7328139 100644 --- a/memory/src/index.ts +++ b/memory/src/index.ts @@ -8,7 +8,7 @@ export type { ProviderConfig } from './providers/registry.js'; export * from './scoring.js'; export * from './wm.js'; export * from './utils.js'; -export * from './documents.js'; + export * from './router.js'; export { Memory } from './memory.js'; export type { MemoryConfig } from './memory.js'; diff --git a/memory/src/router.ts b/memory/src/router.ts index fba4828..e4469d4 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -5,7 +5,7 @@ import type { ExtractFn } from './types.js'; import { extract as defaultExtract } from './providers/extract.js'; import { normalizeAgentId } from './utils.js'; import type { IngestComponents } from './types.js'; -import { ingestDocuments } from './documents.js'; + /** * Creates an Express Router with all memory API routes. @@ -95,42 +95,6 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract } }); - router.post('/v1/ingest/documents', async (req: Request, res: Response) => { - const { path: docPath, agent } = req.body as { - path?: string; - agent?: string; - }; - - if (!docPath || !docPath.trim()) { - return res.status(400).json({ error: 'path_required' }); - } - - let normalizedAgent: string; - try { - normalizedAgent = normalizeAgentId(agent); - } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - return res.status(500).json({ error: 'agent_normalization_failed' }); - } - - // Validate path exists - try { - const fs = await import('node:fs/promises'); - await fs.stat(docPath.trim()); - } catch { - return res.status(400).json({ error: 'path_not_found' }); - } - - try { - const result = await ingestDocuments(docPath.trim(), store, normalizedAgent, doExtract); - res.json(result); - } catch (err: any) { - res.status(500).json({ error: err.message ?? 'document ingestion failed' }); - } - }); - router.get('/v1/summary', (req: Request, res: Response) => { try { const limit = Number(req.query.limit) || 50; From 41a46b3f6663b56564492962e8056842e03beb6c Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:56:05 +0530 Subject: [PATCH 53/78] Remove unused graph endpoints and SDK graph methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop /v1/graph/neighbors, /v1/graph/paths routes and their backing methods (findNeighbors, findPaths, GraphPath). Remove triples() and deleteTriple() from Memory SDK class — the existing search/memories/delete API already covers semantic records transparently. --- memory/AGENT.md | 4 +- memory/README.md | 6 +-- memory/src/router.ts | 39 ------------------ memory/src/semantic-graph.ts | 79 +----------------------------------- memory/src/types.ts | 5 --- ui/dashboard/src/lib/api.ts | 26 ------------ 6 files changed, 4 insertions(+), 155 deletions(-) diff --git a/memory/AGENT.md b/memory/AGENT.md index e608982..59c7d22 100644 --- a/memory/AGENT.md +++ b/memory/AGENT.md @@ -358,8 +358,6 @@ The provider layer abstracts LLM and embedding API calls behind a two-provider r **`semantic-graph.ts`** — Class `SemanticGraphTraversal`: - `findTriplesBySubject(subject, options)` / `findTriplesByObject(object, options)` - `findConnectedTriples(entity, options)` — Union of subject + object queries -- `findNeighbors(entity, options)` — Set of connected entities -- `findPaths(from, to, options)` — BFS path-finding (default maxDepth=3) - `findReachableEntities(entity, options)` — BFS reachability (default maxDepth=2) **`utils.ts`** — Math primitives and profile normalization: @@ -861,5 +859,5 @@ The ekai-gateway has **two independent memory systems**: The dashboard (`ui/dashboard/src/lib/api.ts`) connects to the memory service at `NEXT_PUBLIC_MEMORY_BASE_URL` (default `http://localhost:4005`). It exposes methods for: - `getMemorySummary`, `deleteMemory`, `deleteAllMemories`, `updateMemory` -- `getGraphVisualization`, `getGraphTriples`, `getGraphNeighbors`, `getGraphPaths` +- `getGraphVisualization`, `getGraphTriples` - `getProfiles`, `deleteProfile`, `deleteGraphTriple` diff --git a/memory/README.md b/memory/README.md index 3a507a3..b10ae87 100644 --- a/memory/README.md +++ b/memory/README.md @@ -258,10 +258,8 @@ Get all memories scoped to a specific user. | DELETE | `/v1/memory/:id` | Delete one memory | | DELETE | `/v1/memory` | Delete all for agent | | DELETE | `/v1/agents/:slug` | Delete agent + memories | -| GET | `/v1/graph/triples` | Query semantic triples | -| GET | `/v1/graph/neighbors` | Entity neighbors | -| GET | `/v1/graph/paths` | Paths between entities | -| GET | `/v1/graph/visualization` | Graph visualization data | +| GET | `/v1/graph/triples` | Query semantic triples by entity | +| GET | `/v1/graph/visualization` | Graph visualization data (dashboard) | | DELETE | `/v1/graph/triple/:id` | Delete a triple | | GET | `/health` | Health check | diff --git a/memory/src/router.ts b/memory/src/router.ts index e4469d4..57682d7 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -307,45 +307,6 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract } }); - router.get('/v1/graph/neighbors', (req: Request, res: Response) => { - try { - const { entity, agent, userId } = req.query; - if (!entity || typeof entity !== 'string') { - return res.status(400).json({ error: 'entity query parameter is required' }); - } - - const neighbors = Array.from(store.graph.findNeighbors(entity, { agent: agent as string, userId: userId as string | undefined })); - res.json({ entity, neighbors, count: neighbors.length }); - } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'neighbors query failed' }); - } - }); - - router.get('/v1/graph/paths', (req: Request, res: Response) => { - try { - const { from, to, maxDepth, agent, userId } = req.query; - if (!from || typeof from !== 'string' || !to || typeof to !== 'string') { - return res.status(400).json({ error: 'from and to query parameters are required' }); - } - - const paths = store.graph.findPaths(from, to, { - maxDepth: maxDepth ? Number(maxDepth) : 3, - agent: agent as string, - userId: userId as string | undefined, - }); - - res.json({ from, to, paths, count: paths.length }); - } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'paths query failed' }); - } - }); - router.get('/v1/graph/visualization', (req: Request, res: Response) => { try { const { entity, maxDepth, maxNodes, agent, includeHistory, userId } = req.query; diff --git a/memory/src/semantic-graph.ts b/memory/src/semantic-graph.ts index 315b97c..f15fe1e 100644 --- a/memory/src/semantic-graph.ts +++ b/memory/src/semantic-graph.ts @@ -1,6 +1,6 @@ import Database from 'better-sqlite3'; import { normalizeAgentId } from './utils.js'; -import type { SemanticMemoryRecord, GraphTraversalOptions, GraphPath } from './types.js'; +import type { SemanticMemoryRecord, GraphTraversalOptions } from './types.js'; /** * Graph traversal operations for semantic memory (RDF triples) @@ -137,83 +137,6 @@ export class SemanticGraphTraversal { return result; } - /** - * Find all entities connected to a given entity (neighbors) - */ - findNeighbors( - entity: string, - options: GraphTraversalOptions = {}, - ): Set { - const triples = this.findConnectedTriples(entity, options); - const neighbors = new Set(); - - for (const triple of triples) { - if (triple.subject !== entity) { - neighbors.add(triple.subject); - } - if (triple.object !== entity) { - neighbors.add(triple.object); - } - } - - return neighbors; - } - - /** - * Find paths between two entities using breadth-first search - */ - findPaths( - fromEntity: string, - toEntity: string, - options: GraphTraversalOptions = {}, - ): GraphPath[] { - const { maxDepth = 3 } = options; - - if (fromEntity === toEntity) { - return []; - } - - // BFS to find all paths - const paths: GraphPath[] = []; - const queue: Array<{ entity: string; path: SemanticMemoryRecord[]; depth: number }> = [ - { entity: fromEntity, path: [], depth: 0 }, - ]; - const visited = new Set([fromEntity]); - - while (queue.length > 0) { - const { entity, path, depth } = queue.shift()!; - - if (depth >= maxDepth) { - continue; - } - - // Find all outgoing edges from current entity - const outgoing = this.findTriplesBySubject(entity, { - ...options, - maxResults: 100, - }); - - for (const triple of outgoing) { - // Skip if already in path (avoid cycles) - if (path.some((p) => p.id === triple.id)) { - continue; - } - - const newPath = [...path, triple]; - - if (triple.object === toEntity) { - // Found a path! - paths.push({ path: newPath, depth: depth + 1 }); - } else if (!visited.has(triple.object)) { - visited.add(triple.object); - queue.push({ entity: triple.object, path: newPath, depth: depth + 1 }); - } - } - } - - return paths; - } - /** * Find all entities reachable from a given entity within maxDepth steps */ diff --git a/memory/src/types.ts b/memory/src/types.ts index 1cb5601..ceb373d 100644 --- a/memory/src/types.ts +++ b/memory/src/types.ts @@ -149,11 +149,6 @@ export interface GraphTraversalOptions { userId?: string; } -export interface GraphPath { - path: SemanticMemoryRecord[]; - depth: number; -} - export interface IngestOptions { source?: string; deduplicate?: boolean; diff --git a/ui/dashboard/src/lib/api.ts b/ui/dashboard/src/lib/api.ts index 527afe2..93f44ae 100644 --- a/ui/dashboard/src/lib/api.ts +++ b/ui/dashboard/src/lib/api.ts @@ -147,32 +147,6 @@ export const apiService = { return response.json(); }, - async getGraphNeighbors(entity: string): Promise<{ entity: string; neighbors: string[]; count: number }> { - const response = await fetch(`${MEMORY_BASE_URL}/v1/graph/neighbors?entity=${encodeURIComponent(entity)}`); - if (!response.ok) { - throw new Error(`Failed to fetch neighbors: ${response.statusText}`); - } - return response.json(); - }, - - async getGraphPaths(from: string, to: string, maxDepth?: number): Promise<{ - from: string; - to: string; - paths: Array<{ path: Array<{ subject: string; predicate: string; object: string }>; depth: number }>; - count: number; - }> { - const searchParams = new URLSearchParams(); - searchParams.append('from', from); - searchParams.append('to', to); - if (maxDepth) searchParams.append('maxDepth', String(maxDepth)); - - const response = await fetch(`${MEMORY_BASE_URL}/v1/graph/paths?${searchParams.toString()}`); - if (!response.ok) { - throw new Error(`Failed to fetch paths: ${response.statusText}`); - } - return response.json(); - }, - async getProfiles(): Promise<{ profiles: string[] }> { const response = await fetch(`${MEMORY_BASE_URL}/v1/profiles`); if (!response.ok) { From 2f866e26f08f2755f3d283e0bf8ff3a4ce7e7322 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:52:22 +0530 Subject: [PATCH 54/78] Dashboard overhaul: agent params, source display, userScope editing - Add source field to getRecent() SQL across all memory sectors - Add userScope support to PUT /v1/memory/:id endpoint - Switch all frontend profile params to agent, use /v1/agents endpoint - Default memory tab to logs, show source in expanded rows - Add userScope editing in EditModal - Fix invalid div-in-tr HTML causing row misalignment - Fix SectorTooltip overflow: reposition below badge, narrow width - Update README with userScope docs --- integrations/openrouter/src/memory.ts | 4 + memory/README.md | 67 +++- memory/src/router.ts | 21 +- memory/src/sqlite-store.ts | 30 +- ui/dashboard/src/app/memory/page.tsx | 53 ++-- .../src/components/memory/MemoryModals.tsx | 19 +- .../components/memory/ProfileManagement.tsx | 300 +++++++++--------- .../src/components/memory/ProfileSelector.tsx | 68 ++-- .../src/components/memory/SectorTooltip.tsx | 10 +- .../src/components/memory/SemanticGraph.tsx | 8 +- ui/dashboard/src/lib/api.ts | 47 +-- 11 files changed, 366 insertions(+), 261 deletions(-) diff --git a/integrations/openrouter/src/memory.ts b/integrations/openrouter/src/memory.ts index 973bb86..5c6e73d 100644 --- a/integrations/openrouter/src/memory.ts +++ b/integrations/openrouter/src/memory.ts @@ -23,6 +23,7 @@ export function formatMemoryBlock(results: QueryResult[]): string { const facts: string[] = []; const events: string[] = []; const procedures: string[] = []; + const observations: string[] = []; for (const r of results) { if (r.sector === 'semantic' && r.details?.subject) { @@ -30,6 +31,8 @@ export function formatMemoryBlock(results: QueryResult[]): string { } else if (r.sector === 'procedural' && r.details?.trigger) { const steps = r.details.steps?.join(' → ') || r.content; procedures.push(`- When ${r.details.trigger}: ${steps}`); + } else if (r.sector === 'reflective') { + observations.push(`- ${r.content}`); } else { events.push(`- ${r.content}`); } @@ -39,6 +42,7 @@ export function formatMemoryBlock(results: QueryResult[]): string { if (facts.length) sections.push(`What I know:\n${facts.join('\n')}`); if (events.length) sections.push(`What I remember:\n${events.join('\n')}`); if (procedures.length) sections.push(`How I do things:\n${procedures.join('\n')}`); + if (observations.length) sections.push(`My observations:\n${observations.join('\n')}`); return `\n[Recalled context for this conversation. Use naturally if relevant, ignore if not.]\n\n${sections.join('\n\n')}\n`; } diff --git a/memory/README.md b/memory/README.md index b10ae87..89596ae 100644 --- a/memory/README.md +++ b/memory/README.md @@ -227,6 +227,47 @@ GET /v1/summary?agent=my-bot&limit=20 } ``` +### `GET /v1/agents` + +List all registered agents. + +```json +{ + "agents": [{ "id": "my-bot", "name": "My Bot", "soulMd": "You are helpful", "createdAt": 1700000000 }] +} +``` + +### `POST /v1/agents` + +Register a new agent. `name` and `soul` are optional. + +```json +{ "id": "my-bot", "name": "My Bot", "soul": "You are helpful and concise" } +``` +```json +{ "agent": { "id": "my-bot", "name": "My Bot", "soulMd": "You are helpful and concise", "createdAt": 1700000000 } } +``` + +### `DELETE /v1/agents/:slug` + +Delete an agent and all its memories. Cannot delete the `default` agent. + +```json +{ "deleted": 5, "agent": "my-bot" } +``` + +### `PUT /v1/memory/:id` + +Update a memory's content and/or user scope. Body: `{ "content": "...", "sector?": "episodic", "agent?": "my-bot", "userScope?": "sha" }`. Set `userScope` to `null` to make a memory shared/global. + +### `DELETE /v1/memory/:id` + +Delete a single memory. Query: `?agent=my-bot`. + +### `DELETE /v1/memory` + +Delete all memories for an agent. Query: `?agent=my-bot`. + ### `GET /v1/users` List all users the agent has interacted with. @@ -236,28 +277,42 @@ GET /v1/users?agent=my-bot ``` ```json { - "users": [{ "userId": "sha", "firstSeen": 1700000000, "lastSeen": 1700100000, "interactionCount": 5 }] + "users": [{ "userId": "sha", "firstSeen": 1700000000, "lastSeen": 1700100000, "interactionCount": 5 }], + "agent": "my-bot" } ``` ### `GET /v1/users/:id/memories` -Get all memories scoped to a specific user. +Get memories scoped to a specific user. Query: `?agent=my-bot&limit=20`. + +### `GET /v1/graph/triples` + +Query semantic triples by entity. Query: `?entity=Sha&direction=outgoing&agent=my-bot&predicate=...&maxResults=100&userId=...`. + +### `DELETE /v1/graph/triple/:id` + +Delete a single semantic triple. Query: `?agent=my-bot`. + +### `GET /v1/graph/visualization` + +Graph visualization data (nodes + edges). Query: `?entity=Sha&maxDepth=2&maxNodes=50&agent=my-bot&includeHistory=true&userId=...`. ### All Endpoints | Method | Endpoint | Description | |--------|----------|-------------| +| GET | `/v1/agents` | List agents | +| POST | `/v1/agents` | Create agent | +| DELETE | `/v1/agents/:slug` | Delete agent + memories | | POST | `/v1/ingest` | Ingest conversation | | POST | `/v1/search` | Search with PBWM gating | | GET | `/v1/summary` | Sector counts + recent | -| GET | `/v1/users` | List agent's users | -| GET | `/v1/users/:id/memories` | User-scoped memories | -| GET | `/v1/agents` | List agents | | PUT | `/v1/memory/:id` | Update a memory | | DELETE | `/v1/memory/:id` | Delete one memory | | DELETE | `/v1/memory` | Delete all for agent | -| DELETE | `/v1/agents/:slug` | Delete agent + memories | +| GET | `/v1/users` | List agent's users | +| GET | `/v1/users/:id/memories` | User-scoped memories | | GET | `/v1/graph/triples` | Query semantic triples by entity | | GET | `/v1/graph/visualization` | Graph visualization data (dashboard) | | DELETE | `/v1/graph/triple/:id` | Delete a triple | diff --git a/memory/src/router.ts b/memory/src/router.ts index 57682d7..d73cd6d 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -24,6 +24,22 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract } }); + router.post('/v1/agents', (req: Request, res: Response) => { + try { + const { id, name, soul } = req.body as { id?: string; name?: string; soul?: string }; + if (!id || !id.trim()) { + return res.status(400).json({ error: 'id is required' }); + } + const agent = store.addAgent(id.trim(), { name: name?.trim(), soulMd: soul }); + res.json({ agent }); + } catch (err: any) { + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); + } + res.status(500).json({ error: err.message ?? 'failed to add agent' }); + } + }); + const handleDeleteAgent = (req: Request, res: Response) => { try { const { slug } = req.params; @@ -111,6 +127,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract retrievalCount: (r as any).retrievalCount ?? 0, details: (r as any).details, userScope: (r as any).userScope ?? null, + source: (r as any).source ?? null, })); res.json({ summary, recent, agent: normalizedAgent }); } catch (err: any) { @@ -124,7 +141,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract router.put('/v1/memory/:id', async (req: Request, res: Response) => { try { const { id } = req.params; - const { content, sector, agent } = req.body as { content?: string; sector?: string; agent?: string }; + const { content, sector, agent, userScope } = req.body as { content?: string; sector?: string; agent?: string; userScope?: string | null }; if (!id) return res.status(400).json({ error: 'id_required' }); if (!content || !content.trim()) { @@ -141,7 +158,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract return res.status(500).json({ error: 'agent_normalization_failed' }); } - const updated = await store.updateById(id, content.trim(), sector as any, normalizedAgent); + const updated = await store.updateById(id, content.trim(), sector as any, normalizedAgent, userScope); if (!updated) { return res.status(404).json({ error: 'not_found', id }); } diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 6f6b823..3973ac6 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -358,19 +358,19 @@ export class SqliteMemoryStore { this.ensureAgentExists(agentId); const rows = this.db .prepare( - `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, user_scope as userScope + `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, user_scope as userScope, source from memory where agent_id = @agentId union all select id, 'procedural' as sector, trigger as content, embedding, created_at as createdAt, last_accessed as lastAccessed, json_object('trigger', trigger, 'goal', goal, 'context', context, 'result', result, 'steps', json(steps)) as details, - null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope + null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope, source from procedural_memory where agent_id = @agentId union all select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'metadata', metadata, 'domain', domain) as details, - null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope + null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope, source from semantic_memory where agent_id = @agentId and (valid_to is null or valid_to > @now) @@ -1114,7 +1114,7 @@ export class SqliteMemoryStore { .run({ id, now: this.now() }); } - async updateById(id: string, content: string, sector?: SectorName, agent?: string): Promise { + async updateById(id: string, content: string, sector?: SectorName, agent?: string, userScope?: string | null): Promise { const agentId = normalizeAgentId(agent); this.ensureAgentExists(agentId); // First, get the existing record to preserve sector if not changing @@ -1131,18 +1131,20 @@ export class SqliteMemoryStore { // Regenerate embedding with new content const embedding = await this.embed(content, targetSector); - // Update the record + // Build update dynamically to support optional userScope + const setClauses = ['content = ?', 'sector = ?', 'embedding = json(?)', 'last_accessed = ?']; + const params: any[] = [content, targetSector, JSON.stringify(embedding), this.now()]; + + if (userScope !== undefined) { + setClauses.push('user_scope = ?'); + params.push(userScope); + } + + params.push(id, agentId); const stmt = this.db.prepare( - 'update memory set content = ?, sector = ?, embedding = json(?), last_accessed = ? where id = ? and agent_id = ?' - ); - const res = stmt.run( - content, - targetSector, - JSON.stringify(embedding), - this.now(), - id, - agentId, + `update memory set ${setClauses.join(', ')} where id = ? and agent_id = ?` ); + const res = stmt.run(...params); return res.changes > 0; } diff --git a/ui/dashboard/src/app/memory/page.tsx b/ui/dashboard/src/app/memory/page.tsx index 2d695f4..5c34fe6 100644 --- a/ui/dashboard/src/app/memory/page.tsx +++ b/ui/dashboard/src/app/memory/page.tsx @@ -27,13 +27,13 @@ export default function MemoryVaultPage() { const [error, setError] = useState(null); const [busyId, setBusyId] = useState(null); const [bulkBusy, setBulkBusy] = useState(false); - const [activeTab, setActiveTab] = useState<'overview' | 'logs' | 'graph'>('overview'); + const [activeTab, setActiveTab] = useState<'overview' | 'logs' | 'graph'>('logs'); const [searchTerm, setSearchTerm] = useState(''); const [filterSector, setFilterSector] = useState('all'); const [expandedId, setExpandedId] = useState(null); const [deleteModal, setDeleteModal] = useState<{ type: 'single'; id: string; preview?: string } | { type: 'bulk' } | null>(null); const [bulkScope, setBulkScope] = useState<'current' | 'all'>('current'); - const [editModal, setEditModal] = useState<{ id: string; content: string; sector: string } | null>(null); + const [editModal, setEditModal] = useState<{ id: string; content: string; sector: string; userScope: string | null } | null>(null); const [editBusy, setEditBusy] = useState(false); const [currentProfile, setCurrentProfile] = useState('default'); const [selectedUserId, setSelectedUserId] = useState(null); @@ -49,11 +49,11 @@ export default function MemoryVaultPage() { ); }, []); - // On mount, pick the first non-default agent profile (or fall back to 'default') + // On mount, pick the first non-default agent (or fall back to 'default') useEffect(() => { - apiService.getProfiles().then(({ profiles }) => { - const agent = profiles.find(p => p !== 'default'); - if (agent) setCurrentProfile(agent); + apiService.getAgents().then(({ agents }) => { + const agent = agents.find(a => a.id !== 'default'); + if (agent) setCurrentProfile(agent.id); setProfileResolved(true); }).catch(() => { setProfileResolved(true); @@ -99,8 +99,8 @@ export default function MemoryVaultPage() { } }; - const handleEditClick = (id: string, content: string, sector: string) => { - setEditModal({ id, content, sector }); + const handleEditClick = (id: string, content: string, sector: string, userScope?: string | null) => { + setEditModal({ id, content, sector, userScope: userScope ?? null }); }; const handleDeleteClick = (id: string, preview?: string) => { @@ -118,7 +118,7 @@ export default function MemoryVaultPage() { try { setEditBusy(true); setError(null); - await apiService.updateMemory(editModal.id, editModal.content, editModal.sector, currentProfile); + await apiService.updateMemory(editModal.id, editModal.content, editModal.sector, currentProfile, editModal.userScope); setEditModal(null); await fetchData(); } catch (err) { @@ -142,11 +142,11 @@ export default function MemoryVaultPage() { if (bulkScope === 'current') { await apiService.deleteAllMemories(currentProfile); } else { - const { profiles } = await apiService.getProfiles(); - // Delete sequentially to surface the first failing profile clearly - const targets = profiles.length ? profiles : ['default']; - for (const profile of targets) { - await apiService.deleteAllMemories(profile); + const { agents } = await apiService.getAgents(); + // Delete sequentially to surface the first failing agent clearly + const targets = agents.length ? agents.map(a => a.id) : ['default']; + for (const agentId of targets) { + await apiService.deleteAllMemories(agentId); } } } @@ -384,7 +384,7 @@ export default function MemoryVaultPage() {
) : activeTab === 'graph' ? (
- +
) : (
@@ -487,12 +487,11 @@ export default function MemoryVaultPage() { }`} onClick={() => setExpandedId(expandedId === item.id ? null : item.id)} > - {/* Left border indicator for active memories */} - {isActive && ( -
- )} - - + + {/* Left border indicator for active memories */} + {isActive && ( +
+ )}
Last accessed: {new Date(item.lastAccessed).toLocaleString()} + {item.source && ( + + + + + Source: {item.source} + + )} {isRecent && ( New @@ -567,7 +574,7 @@ export default function MemoryVaultPage() {
+
+ + onUpdate({ userScope: e.target.value || null })} + placeholder="Leave empty for shared/global" + /> +

+ Assign this memory to a specific user ID, or leave empty for shared access. +

+
diff --git a/ui/dashboard/src/components/memory/ProfileManagement.tsx b/ui/dashboard/src/components/memory/ProfileManagement.tsx index a552c56..a6fa812 100644 --- a/ui/dashboard/src/components/memory/ProfileManagement.tsx +++ b/ui/dashboard/src/components/memory/ProfileManagement.tsx @@ -9,12 +9,12 @@ interface Profile { color: string; } -interface ProfileManagementProps { - isOpen: boolean; - onClose: () => void; - currentProfile: string; - onProfileDeleted?: (profile: string) => void; -} +interface ProfileManagementProps { + isOpen: boolean; + onClose: () => void; + currentProfile: string; + onProfileDeleted?: (profile: string) => void; +} const formatProfileName = (slug: string): string => { return slug @@ -39,53 +39,53 @@ const getProfileColor = (slug: string, index: number): string => { return colors[index % colors.length]; }; -export default function ProfileManagement({ isOpen, onClose, currentProfile, onProfileDeleted }: ProfileManagementProps) { - const [profiles, setProfiles] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - const [deleting, setDeleting] = useState(null); - const [deleteError, setDeleteError] = useState(''); - const [deleteTarget, setDeleteTarget] = useState(null); - - const loadProfiles = async () => { - try { - setLoading(true); - setError(''); - const response = await apiService.getProfiles(); - const profileList: Profile[] = response.profiles.map((slug, index) => ({ - slug, - displayName: formatProfileName(slug), - color: getProfileColor(slug, index), - })); - setProfiles(profileList); - } catch (err) { - setError('Failed to load profiles'); - console.error('Failed to fetch profiles', err); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - if (!isOpen) return; - loadProfiles(); - }, [isOpen]); - - const handleDeleteProfile = async (slug: string) => { - try { - setDeleting(slug); - setDeleteError(''); - await apiService.deleteProfile(slug); - if (onProfileDeleted) onProfileDeleted(slug); - await loadProfiles(); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to delete profile'; - setDeleteError(message); - } finally { - setDeleting(null); - setDeleteTarget(null); - } - }; +export default function ProfileManagement({ isOpen, onClose, currentProfile, onProfileDeleted }: ProfileManagementProps) { + const [profiles, setProfiles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [deleting, setDeleting] = useState(null); + const [deleteError, setDeleteError] = useState(''); + const [deleteTarget, setDeleteTarget] = useState(null); + + const loadProfiles = async () => { + try { + setLoading(true); + setError(''); + const response = await apiService.getAgents(); + const profileList: Profile[] = response.agents.map((agent, index) => ({ + slug: agent.id, + displayName: agent.name ? formatProfileName(agent.name) : formatProfileName(agent.id), + color: getProfileColor(agent.id, index), + })); + setProfiles(profileList); + } catch (err) { + setError('Failed to load profiles'); + console.error('Failed to fetch agents', err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!isOpen) return; + loadProfiles(); + }, [isOpen]); + + const handleDeleteProfile = async (slug: string) => { + try { + setDeleting(slug); + setDeleteError(''); + await apiService.deleteAgent(slug); + if (onProfileDeleted) onProfileDeleted(slug); + await loadProfiles(); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'Failed to delete profile'; + setDeleteError(message); + } finally { + setDeleting(null); + setDeleteTarget(null); + } + }; if (!isOpen) return null; @@ -117,22 +117,22 @@ export default function ProfileManagement({ isOpen, onClose, currentProfile, onP

Loading profiles...

- ) : error ? ( -
-

{error}

-
- ) : ( -
- {deleteError && ( -
- {deleteError} -
- )} -

- - - - Available Profiles + ) : error ? ( +
+

{error}

+
+ ) : ( +
+ {deleteError && ( +
+ {deleteError} +
+ )} +

+ + + + Available Profiles

{profiles.length === 0 ? ( @@ -143,11 +143,11 @@ export default function ProfileManagement({ isOpen, onClose, currentProfile, onP ) : (
{profiles.map((profile) => ( -
-
+
+
{profile.displayName[0].toUpperCase()}
@@ -156,86 +156,86 @@ export default function ProfileManagement({ isOpen, onClose, currentProfile, onP
{profile.slug}
- {profile.slug === currentProfile && ( - - Active - - )} - - {profile.slug !== 'default' && ( - - )} -
- ))} -
- )} -
+ {profile.slug === currentProfile && ( + + Active + + )} + + {profile.slug !== 'default' && ( + + )} +
+ ))} +

+ )} +
)} -
- - {/* Footer */} -
- -
-
- - {deleteTarget && ( -
-
setDeleteTarget(null)} /> -
-
-
- - - -
-
-

Delete profile?

-

- This will remove the profile "{deleteTarget.displayName}" and all of its memories. This action cannot be undone. -

-
-
-
- - -
-
-
- )} -
- ); -} +
+
+ + {deleteTarget && ( +
+
setDeleteTarget(null)} /> +
+
+
+ + + +
+
+

Delete profile?

+

+ This will remove the profile "{deleteTarget.displayName}" and all of its memories. This action cannot be undone. +

+
+
+
+ + +
+
+
+ )} +
+ ); +} diff --git a/ui/dashboard/src/components/memory/ProfileSelector.tsx b/ui/dashboard/src/components/memory/ProfileSelector.tsx index 025fbf8..e704133 100644 --- a/ui/dashboard/src/components/memory/ProfileSelector.tsx +++ b/ui/dashboard/src/components/memory/ProfileSelector.tsx @@ -3,18 +3,18 @@ import { useState, useEffect, useRef } from 'react'; import { apiService } from '@/lib/api'; -export interface Profile { - slug: string; - displayName: string; - color: string; - memoryCount?: number; -} - -interface ProfileSelectorProps { - currentProfile: string; - onProfileChange: (profileSlug: string) => void; - onManageProfiles: () => void; -} +export interface Profile { + slug: string; + displayName: string; + color: string; + memoryCount?: number; +} + +interface ProfileSelectorProps { + currentProfile: string; + onProfileChange: (profileSlug: string) => void; + onManageProfiles: () => void; +} // Color mapping for profiles const getProfileColor = (slug: string, index: number): string => { @@ -40,7 +40,7 @@ const formatProfileName = (slug: string): string => { .join(' '); }; -export default function ProfileSelector({ currentProfile, onProfileChange, onManageProfiles }: ProfileSelectorProps) { +export default function ProfileSelector({ currentProfile, onProfileChange, onManageProfiles }: ProfileSelectorProps) { const [isOpen, setIsOpen] = useState(false); const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); @@ -57,20 +57,20 @@ export default function ProfileSelector({ currentProfile, onProfileChange, onMan return () => document.removeEventListener('mousedown', handleClickOutside); }, []); - // Load profiles from backend + // Load agents from backend useEffect(() => { - const fetchProfiles = async () => { + const fetchAgents = async () => { try { setLoading(true); - const response = await apiService.getProfiles(); - const profileList: Profile[] = response.profiles.map((slug, index) => ({ - slug, - displayName: formatProfileName(slug), - color: getProfileColor(slug, index), + const response = await apiService.getAgents(); + const profileList: Profile[] = response.agents.map((agent, index) => ({ + slug: agent.id, + displayName: agent.name ? formatProfileName(agent.name) : formatProfileName(agent.id), + color: getProfileColor(agent.id, index), })); setProfiles(profileList); } catch (err) { - console.error('Failed to fetch profiles', err); + console.error('Failed to fetch agents', err); // Fallback to default profile if fetch fails setProfiles([{ slug: 'default', displayName: 'Default', color: 'bg-emerald-500' }]); } finally { @@ -78,7 +78,7 @@ export default function ProfileSelector({ currentProfile, onProfileChange, onMan } }; - fetchProfiles(); + fetchAgents(); }, []); const currentProfileData = profiles.find(p => p.slug === currentProfile) || (profiles[0] || { slug: 'default', displayName: 'Default', color: 'bg-emerald-500' }); @@ -115,18 +115,18 @@ export default function ProfileSelector({ currentProfile, onProfileChange, onMan {isOpen && (
{/* Header */} -
-

Switch Profile

- -
+
+

Switch Profile

+ +
{/* Profile List */}
diff --git a/ui/dashboard/src/components/memory/SectorTooltip.tsx b/ui/dashboard/src/components/memory/SectorTooltip.tsx index dfcce87..14fff63 100644 --- a/ui/dashboard/src/components/memory/SectorTooltip.tsx +++ b/ui/dashboard/src/components/memory/SectorTooltip.tsx @@ -37,15 +37,15 @@ export function SectorTooltip({ sector, children }: SectorTooltipProps) { > {children} {showTooltip && ( -
-
+
+
{capitalizeSector(sector)}
-
{sectorDescriptions[sector]}
+
{sectorDescriptions[sector]}
{/* Arrow */} -
-
+
+
)} diff --git a/ui/dashboard/src/components/memory/SemanticGraph.tsx b/ui/dashboard/src/components/memory/SemanticGraph.tsx index 8bee75d..b747847 100644 --- a/ui/dashboard/src/components/memory/SemanticGraph.tsx +++ b/ui/dashboard/src/components/memory/SemanticGraph.tsx @@ -23,7 +23,7 @@ interface SemanticGraphProps { maxDepth?: number; maxNodes?: number; height?: number; - profile?: string; + agent?: string; userId?: string | null; } @@ -62,7 +62,7 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { return { nodes: layoutedNodes, edges }; }; -export function SemanticGraph({ entity, maxDepth = 2, maxNodes = 50, height = 500, profile, userId }: SemanticGraphProps) { +export function SemanticGraph({ entity, maxDepth = 2, maxNodes = 50, height = 500, agent, userId }: SemanticGraphProps) { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [loading, setLoading] = useState(true); @@ -74,7 +74,7 @@ export function SemanticGraph({ entity, maxDepth = 2, maxNodes = 50, height = 50 try { setLoading(true); setError(null); - const data = await apiService.getGraphVisualization({ entity, maxDepth, maxNodes, profile, userId: userId ?? undefined }); + const data = await apiService.getGraphVisualization({ entity, maxDepth, maxNodes, agent, userId: userId ?? undefined }); const initialNodes: Node[] = data.nodes.map((n) => ({ id: n.id, @@ -135,7 +135,7 @@ export function SemanticGraph({ entity, maxDepth = 2, maxNodes = 50, height = 50 } finally { setLoading(false); } - }, [entity, maxDepth, maxNodes, profile, userId, setNodes, setEdges]); + }, [entity, maxDepth, maxNodes, agent, userId, setNodes, setEdges]); useEffect(() => { fetchGraphData(); diff --git a/ui/dashboard/src/lib/api.ts b/ui/dashboard/src/lib/api.ts index 93f44ae..833e9a1 100644 --- a/ui/dashboard/src/lib/api.ts +++ b/ui/dashboard/src/lib/api.ts @@ -16,6 +16,7 @@ export interface MemoryRecentItem { preview: string; retrievalCount?: number; userScope?: string | null; + source?: string | null; details?: { trigger?: string; goal?: string; @@ -33,10 +34,10 @@ export interface MemorySummaryResponse { // API service functions export const apiService = { - async getMemorySummary(limit = 50, profile?: string): Promise { + async getMemorySummary(limit = 50, agent?: string): Promise { const params = new URLSearchParams(); params.append('limit', String(limit)); - if (profile) params.append('profile', profile); + if (agent) params.append('agent', agent); const response = await fetch(`${MEMORY_BASE_URL}/v1/summary?${params.toString()}`); if (!response.ok) { @@ -45,11 +46,15 @@ export const apiService = { return response.json(); }, - async updateMemory(id: string, content: string, sector?: string, profile?: string): Promise<{ updated: boolean; id: string; profile?: string }> { + async updateMemory(id: string, content: string, sector?: string, agent?: string, userScope?: string | null): Promise<{ updated: boolean; id: string; agent?: string }> { + const body: Record = { content, sector, agent }; + if (userScope !== undefined) { + body.userScope = userScope; + } const response = await fetch(`${MEMORY_BASE_URL}/v1/memory/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content, sector, profile }), + body: JSON.stringify(body), }); if (!response.ok) { let errorMessage = `Failed to update memory: ${response.statusText}`; @@ -66,9 +71,9 @@ export const apiService = { return response.json(); }, - async deleteMemory(id: string, profile?: string): Promise { + async deleteMemory(id: string, agent?: string): Promise { const params = new URLSearchParams(); - if (profile) params.append('profile', profile); + if (agent) params.append('agent', agent); const url = `${MEMORY_BASE_URL}/v1/memory/${encodeURIComponent(id)}${params.toString() ? `?${params.toString()}` : ''}`; const response = await fetch(url, { method: 'DELETE' }); @@ -86,9 +91,9 @@ export const apiService = { } }, - async deleteAllMemories(profile?: string): Promise<{ deleted: number; profile?: string }> { + async deleteAllMemories(agent?: string): Promise<{ deleted: number; agent?: string }> { const params = new URLSearchParams(); - if (profile) params.append('profile', profile); + if (agent) params.append('agent', agent); const url = `${MEMORY_BASE_URL}/v1/memory${params.toString() ? `?${params.toString()}` : ''}`; const response = await fetch(url, { method: 'DELETE' }); @@ -99,7 +104,7 @@ export const apiService = { }, // Graph traversal APIs - async getGraphVisualization(params?: { entity?: string; maxDepth?: number; maxNodes?: number; profile?: string; includeHistory?: boolean; userId?: string }): Promise<{ + async getGraphVisualization(params?: { entity?: string; maxDepth?: number; maxNodes?: number; agent?: string; includeHistory?: boolean; userId?: string }): Promise<{ center?: string; nodes: Array<{ id: string; label: string }>; edges: Array<{ source: string; target: string; predicate: string; isHistorical?: boolean }>; @@ -109,7 +114,7 @@ export const apiService = { if (params?.entity) searchParams.append('entity', params.entity); if (params?.maxDepth) searchParams.append('maxDepth', String(params.maxDepth)); if (params?.maxNodes) searchParams.append('maxNodes', String(params.maxNodes)); - if (params?.profile) searchParams.append('profile', params.profile); + if (params?.agent) searchParams.append('agent', params.agent); if (params?.includeHistory) searchParams.append('includeHistory', 'true'); if (params?.userId) searchParams.append('userId', params.userId); @@ -147,36 +152,36 @@ export const apiService = { return response.json(); }, - async getProfiles(): Promise<{ profiles: string[] }> { - const response = await fetch(`${MEMORY_BASE_URL}/v1/profiles`); + async getAgents(): Promise<{ agents: Array<{ id: string; name: string; createdAt: number }> }> { + const response = await fetch(`${MEMORY_BASE_URL}/v1/agents`); if (!response.ok) { - throw new Error(`Failed to fetch profiles: ${response.statusText}`); + throw new Error(`Failed to fetch agents: ${response.statusText}`); } return response.json(); }, - async getUsers(profile: string): Promise<{ users: Array<{ userId: string; firstSeen: number; lastSeen: number; interactionCount: number }>; profile: string }> { - const response = await fetch(`${MEMORY_BASE_URL}/v1/users?profile=${encodeURIComponent(profile)}`); + async getUsers(agent: string): Promise<{ users: Array<{ userId: string; firstSeen: number; lastSeen: number; interactionCount: number }>; agent: string }> { + const response = await fetch(`${MEMORY_BASE_URL}/v1/users?agent=${encodeURIComponent(agent)}`); if (!response.ok) { throw new Error(`Failed to fetch users: ${response.statusText}`); } return response.json(); }, - async deleteProfile(profile: string): Promise<{ deleted: number; profile: string }> { - const response = await fetch(`${MEMORY_BASE_URL}/v1/profiles/${encodeURIComponent(profile)}`, { + async deleteAgent(agent: string): Promise<{ deleted: number; agent: string }> { + const response = await fetch(`${MEMORY_BASE_URL}/v1/agents/${encodeURIComponent(agent)}`, { method: 'DELETE', }); if (!response.ok) { const body = await response.json().catch(() => ({})); // Treat "not_found" as a no-op to keep the flow idempotent if (response.status === 404 && body?.error === 'not_found') { - return { deleted: 0, profile }; + return { deleted: 0, agent }; } - const reason = body?.error === 'default_profile_protected' - ? 'Default profile cannot be deleted' + const reason = body?.error === 'default_agent_protected' + ? 'Default agent cannot be deleted' : body?.error ?? response.statusText; - throw new Error(`Failed to delete profile: ${reason}`); + throw new Error(`Failed to delete agent: ${reason}`); } return response.json(); }, From b2d00988011888f9322d723496b30c9581e55ede Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:58:23 +0530 Subject: [PATCH 55/78] Add .nvmrc (Node 22) and gitignore .next/ build cache --- .gitignore | 1 + .nvmrc | 1 + 2 files changed, 2 insertions(+) create mode 100644 .nvmrc diff --git a/.gitignore b/.gitignore index 7ce81ac..569c4a6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules/ # Build output dist/ +.next/ # TypeScript compiled files *.js diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2e1d452 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 From b75dc638472cd7f0de2bd995af53198608345ed1 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:35:59 +0530 Subject: [PATCH 56/78] Disable live ingestion to stop runaway memory growth Every /v1/chat/completions call was re-ingesting the full conversation history without deduplication, causing memory counts to grow unboundedly (e.g. Rio: 3542 memories from 9 conversations). Commenting out the ingestMessages call until proper dedup is implemented. --- integrations/openrouter/src/server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/integrations/openrouter/src/server.ts b/integrations/openrouter/src/server.ts index bc11cef..3c26ecc 100644 --- a/integrations/openrouter/src/server.ts +++ b/integrations/openrouter/src/server.ts @@ -68,8 +68,9 @@ app.post('/v1/chat/completions', async (req, res) => { } } - // Fire-and-forget: ingest original messages for future recall - ingestMessages(originalMessages, profile); + // Ingestion disabled — re-ingesting full conversation on every call causes + // runaway memory growth (no dedup). Will re-enable with proper deduplication. + // ingestMessages(originalMessages, profile); await proxyToOpenRouter(body, res, clientKey); } catch (err: any) { From 82fc8ad172969060f9736a126d08123ae18da01e Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:52:49 +0530 Subject: [PATCH 57/78] Add user scope filtering for semantic memories on dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass userId through /v1/summary → getRecent() so all memory sectors (episodic, procedural, semantic) filter server-side with the "global + mine" pattern. Show semantic rows in the Logs tab alongside other sectors. --- memory/src/router.ts | 3 ++- memory/src/sqlite-store.ts | 10 ++++++---- ui/dashboard/src/app/memory/page.tsx | 8 ++++---- ui/dashboard/src/lib/api.ts | 3 ++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/memory/src/router.ts b/memory/src/router.ts index d73cd6d..0f5d2cf 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -115,9 +115,10 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract try { const limit = Number(req.query.limit) || 50; const agent = req.query.agent as string; + const userId = req.query.userId as string | undefined; const normalizedAgent = normalizeAgentId(agent); const summary = store.getSectorSummary(normalizedAgent); - const recent = store.getRecent(normalizedAgent, limit).map((r) => ({ + const recent = store.getRecent(normalizedAgent, limit, userId || undefined).map((r) => ({ id: r.id, sector: r.sector, agent: r.agentId, diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 3973ac6..2297c32 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -353,20 +353,21 @@ export class SqliteMemoryStore { return defaults.map((d) => map.get(d.sector) ?? d); } - getRecent(agent: string | undefined, limit: number): (MemoryRecord & { details?: any })[] { + getRecent(agent: string | undefined, limit: number, userId?: string): (MemoryRecord & { details?: any })[] { const agentId = normalizeAgentId(agent); this.ensureAgentExists(agentId); + const userFilter = userId ? 'and (user_scope is null or user_scope = @userId)' : ''; const rows = this.db .prepare( `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, user_scope as userScope, source from memory - where agent_id = @agentId + where agent_id = @agentId ${userFilter} union all select id, 'procedural' as sector, trigger as content, embedding, created_at as createdAt, last_accessed as lastAccessed, json_object('trigger', trigger, 'goal', goal, 'context', context, 'result', result, 'steps', json(steps)) as details, null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope, source from procedural_memory - where agent_id = @agentId + where agent_id = @agentId ${userFilter} union all select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'metadata', metadata, 'domain', domain) as details, @@ -374,10 +375,11 @@ export class SqliteMemoryStore { from semantic_memory where agent_id = @agentId and (valid_to is null or valid_to > @now) + ${userFilter} order by createdAt desc limit @limit`, ) - .all({ agentId, limit, now: this.now() }) as Array & { details: string }>; + .all({ agentId, limit, now: this.now(), userId }) as Array & { details: string }>; return rows.map((row) => { const parsed = { diff --git a/ui/dashboard/src/app/memory/page.tsx b/ui/dashboard/src/app/memory/page.tsx index 5c34fe6..9555144 100644 --- a/ui/dashboard/src/app/memory/page.tsx +++ b/ui/dashboard/src/app/memory/page.tsx @@ -66,14 +66,14 @@ export default function MemoryVaultPage() { setLoading(true); setError(null); // Fetch more items for better visualization (limit=100) - const res = await apiService.getMemorySummary(100, currentProfile); + const res = await apiService.getMemorySummary(100, currentProfile, selectedUserId || undefined); setData(res); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load memory summary'); } finally { setLoading(false); } - }, [currentProfile, profileResolved]); + }, [currentProfile, profileResolved, selectedUserId]); useEffect(() => { fetchData(); @@ -172,8 +172,8 @@ export default function MemoryVaultPage() { const filteredMemories = useMemo(() => { if (!data?.recent) return []; return data.recent.filter((item) => { - // Exclude semantic memories (visualized in Knowledge Graph tab) and reflective memories (disabled) - if (item.sector === 'semantic' || item.sector === 'reflective') return false; + // Exclude reflective memories (disabled) + if (item.sector === 'reflective') return false; const matchesSearch = !searchTerm || item.preview.toLowerCase().includes(searchTerm.toLowerCase()); const matchesSector = filterSector === 'all' || item.sector === filterSector; const matchesUser = !selectedUserId || item.userScope == null || item.userScope === selectedUserId; diff --git a/ui/dashboard/src/lib/api.ts b/ui/dashboard/src/lib/api.ts index 833e9a1..fddad37 100644 --- a/ui/dashboard/src/lib/api.ts +++ b/ui/dashboard/src/lib/api.ts @@ -34,10 +34,11 @@ export interface MemorySummaryResponse { // API service functions export const apiService = { - async getMemorySummary(limit = 50, agent?: string): Promise { + async getMemorySummary(limit = 50, agent?: string, userId?: string): Promise { const params = new URLSearchParams(); params.append('limit', String(limit)); if (agent) params.append('agent', agent); + if (userId) params.append('userId', userId); const response = await fetch(`${MEMORY_BASE_URL}/v1/summary?${params.toString()}`); if (!response.ok) { From 220512cd3ed8ac8befeb7d3cefc516be93e696c8 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:48:32 +0530 Subject: [PATCH 58/78] Fix graph userId filter, strict user scoping, and remove back arrow Pass userId to getRecent() in graph visualization overview branch so the knowledge graph respects user selection. Tighten user_scope filters to match exactly (no null fallthrough). Remove unnecessary back arrow from memory page header since memory is the main page. --- memory/src/router.ts | 2 +- memory/src/sqlite-store.ts | 8 ++++---- ui/dashboard/src/app/memory/page.tsx | 26 +++----------------------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/memory/src/router.ts b/memory/src/router.ts index 0f5d2cf..540c14c 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -337,7 +337,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract // If no center entity, get a sample of semantic triples if (!centerEntity) { - const allSemantic = store.getRecent(normalizedAgent, 100).filter((r) => r.sector === 'semantic'); + const allSemantic = store.getRecent(normalizedAgent, 100, userId as string | undefined).filter((r) => r.sector === 'semantic'); const triples = allSemantic .slice(0, nodeLimit) .map((r) => (r as any).details) diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 2297c32..62bd2b4 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -356,7 +356,7 @@ export class SqliteMemoryStore { getRecent(agent: string | undefined, limit: number, userId?: string): (MemoryRecord & { details?: any })[] { const agentId = normalizeAgentId(agent); this.ensureAgentExists(agentId); - const userFilter = userId ? 'and (user_scope is null or user_scope = @userId)' : ''; + const userFilter = userId ? 'and user_scope = @userId' : ''; const rows = this.db .prepare( `select id, sector, content, embedding, created_at as createdAt, last_accessed as lastAccessed, '{}' as details, event_start as eventStart, event_end as eventEnd, retrieval_count as retrievalCount, user_scope as userScope, source @@ -369,7 +369,7 @@ export class SqliteMemoryStore { from procedural_memory where agent_id = @agentId ${userFilter} union all - select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, + select id, 'semantic' as sector, subject || ' → ' || predicate || ' → ' || object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'metadata', metadata, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope, source from semantic_memory @@ -484,7 +484,7 @@ export class SqliteMemoryStore { from procedural_memory where agent_id = @agentId and user_scope = @userId union all - select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, + select id, 'semantic' as sector, subject || ' → ' || predicate || ' → ' || object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount from semantic_memory @@ -520,7 +520,7 @@ export class SqliteMemoryStore { from procedural_memory where agent_id = @agentId and user_scope is null union all - select id, 'semantic' as sector, object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, + select id, 'semantic' as sector, subject || ' → ' || predicate || ' → ' || object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount from semantic_memory diff --git a/ui/dashboard/src/app/memory/page.tsx b/ui/dashboard/src/app/memory/page.tsx index 9555144..fff9fe7 100644 --- a/ui/dashboard/src/app/memory/page.tsx +++ b/ui/dashboard/src/app/memory/page.tsx @@ -5,7 +5,6 @@ import { apiService, type MemorySummaryResponse, } from '@/lib/api'; -import Link from 'next/link'; import { SectorTooltip, sectorColors, capitalizeSector } from '@/components/memory/SectorTooltip'; import { ProceduralCard } from '@/components/memory/ProceduralCard'; import { DeleteModal, EditModal } from '@/components/memory/MemoryModals'; @@ -18,7 +17,6 @@ import UserFilter from '@/components/memory/UserFilter'; import ProfileManagement from '@/components/memory/ProfileManagement'; import ProfileStats from '@/components/memory/ProfileStats'; import ProfileBadge from '@/components/memory/ProfileBadge'; -import { MEMORY_PORT } from '@/lib/constants'; export default function MemoryVaultPage() { @@ -40,15 +38,6 @@ export default function MemoryVaultPage() { const [profileResolved, setProfileResolved] = useState(false); const [showProfileManagement, setShowProfileManagement] = useState(false); const [profileSwitching, setProfileSwitching] = useState(false); - const [embedded, setEmbedded] = useState(false); - - useEffect(() => { - setEmbedded( - process.env.NEXT_PUBLIC_EMBEDDED_MODE === 'true' || - window.location.port === MEMORY_PORT - ); - }, []); - // On mount, pick the first non-default agent (or fall back to 'default') useEffect(() => { apiService.getAgents().then(({ agents }) => { @@ -176,7 +165,7 @@ export default function MemoryVaultPage() { if (item.sector === 'reflective') return false; const matchesSearch = !searchTerm || item.preview.toLowerCase().includes(searchTerm.toLowerCase()); const matchesSector = filterSector === 'all' || item.sector === filterSector; - const matchesUser = !selectedUserId || item.userScope == null || item.userScope === selectedUserId; + const matchesUser = !selectedUserId || item.userScope === selectedUserId; return matchesSearch && matchesSector && matchesUser; }); }, [data, searchTerm, filterSector, selectedUserId]); @@ -186,7 +175,7 @@ export default function MemoryVaultPage() { if (!data) return null; const filtered = data.recent?.filter(r => { if (r.sector === 'reflective') return false; - if (selectedUserId && r.userScope != null && r.userScope !== selectedUserId) return false; + if (selectedUserId && r.userScope !== selectedUserId) return false; return true; }); // Recompute summary counts from filtered recent items when user filter is active @@ -248,16 +237,6 @@ export default function MemoryVaultPage() {
- {!embedded && ( - - - - - - )}

Memory Vault

@@ -426,6 +405,7 @@ export default function MemoryVaultPage() { +

From e29d477572bfd5d582b84a8400d073bbcbc2460c Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:32:15 +0530 Subject: [PATCH 59/78] Make deduplication always-on, remove unused deduplicate flag No caller ever passed deduplicate=true, making the opt-in flag dead code. Ingestion was disabled in the OpenRouter proxy due to runaway memory growth from lack of dedup. Now dedup runs unconditionally for all sectors. --- memory/src/sqlite-store.ts | 83 +++++++++++++------------------------- memory/src/types.ts | 1 - memory/tests/store.test.ts | 9 ++--- 3 files changed, 33 insertions(+), 60 deletions(-) diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 62bd2b4..9c40a06 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -46,7 +46,6 @@ export class SqliteMemoryStore { const createdAt = this.now(); const rows: MemoryRecord[] = []; const source = options?.source; - const dedup = options?.deduplicate === true; const origin = options?.origin; const userId = options?.userId; @@ -60,27 +59,21 @@ export class SqliteMemoryStore { if (episodic && typeof episodic === 'string' && episodic.trim()) { const embedding = await this.embed(episodic, 'episodic'); - if (dedup) { - const existingDup = this.findDuplicateMemory(embedding, 'episodic', agentId, 0.9); - if (existingDup) { - if (source && !existingDup.source) { - this.setMemorySource(existingDup.id, source); - } - rows.push({ - id: existingDup.id, - sector: 'episodic', - content: existingDup.content, - embedding, - agentId, - createdAt: existingDup.createdAt, - lastAccessed: existingDup.lastAccessed, - source: existingDup.source ?? source, - }); - } else { - const row = this.buildEpisodicRow(episodic, embedding, agentId, createdAt, source, origin, userId); - this.insertRow(row); - rows.push(row); + const existingDup = this.findDuplicateMemory(embedding, 'episodic', agentId, 0.9); + if (existingDup) { + if (source && !existingDup.source) { + this.setMemorySource(existingDup.id, source); } + rows.push({ + id: existingDup.id, + sector: 'episodic', + content: existingDup.content, + embedding, + agentId, + createdAt: existingDup.createdAt, + lastAccessed: existingDup.lastAccessed, + source: existingDup.source ?? source, + }); } else { const row = this.buildEpisodicRow(episodic, embedding, agentId, createdAt, source, origin, userId); this.insertRow(row); @@ -132,11 +125,7 @@ export class SqliteMemoryStore { switch (action.type) { case 'merge': - if (dedup) { - if (source) this.setSemanticSource(action.targetId, source); - } else { - this.strengthenFact(action.targetId); - } + if (source) this.setSemanticSource(action.targetId, source); rows.push({ id: action.targetId, sector: 'semantic', @@ -223,35 +212,21 @@ export class SqliteMemoryStore { const embedding = await this.embed(textToEmbed, 'procedural'); procRow.embedding = embedding; - if (dedup) { - const existingDup = this.findDuplicateProcedural(embedding, agentId, 0.9); - if (existingDup) { - if (source && !existingDup.source) { - this.setProceduralSource(existingDup.id, source); - } - rows.push({ - id: existingDup.id, - sector: 'procedural', - content: existingDup.trigger, - embedding, - agentId, - createdAt: existingDup.createdAt, - lastAccessed: existingDup.lastAccessed, - source: existingDup.source ?? source, - }); - } else { - this.insertProceduralRow(procRow); - rows.push({ - id: procRow.id, - sector: 'procedural', - content: procRow.trigger, - embedding, - agentId, - createdAt, - lastAccessed: createdAt, - source, - }); + const existingDup = this.findDuplicateProcedural(embedding, agentId, 0.9); + if (existingDup) { + if (source && !existingDup.source) { + this.setProceduralSource(existingDup.id, source); } + rows.push({ + id: existingDup.id, + sector: 'procedural', + content: existingDup.trigger, + embedding, + agentId, + createdAt: existingDup.createdAt, + lastAccessed: existingDup.lastAccessed, + source: existingDup.source ?? source, + }); } else { this.insertProceduralRow(procRow); rows.push({ diff --git a/memory/src/types.ts b/memory/src/types.ts index ceb373d..91da92a 100644 --- a/memory/src/types.ts +++ b/memory/src/types.ts @@ -151,7 +151,6 @@ export interface GraphTraversalOptions { export interface IngestOptions { source?: string; - deduplicate?: boolean; origin?: MemoryOrigin; userId?: string; } diff --git a/memory/tests/store.test.ts b/memory/tests/store.test.ts index 69aa5ab..d8578c7 100644 --- a/memory/tests/store.test.ts +++ b/memory/tests/store.test.ts @@ -142,8 +142,8 @@ describe('SqliteMemoryStore (single-user mode)', () => { } }); - // ─── 3. Semantic consolidation — merge ──────────────────────────── - it('merges duplicate semantic triples and increases strength', async () => { + // ─── 3. Semantic consolidation — merge (dedup always on) ───────── + it('deduplicates semantic triples and keeps strength at 1.0', async () => { // Ingest same triple twice await store.ingest({ semantic: { subject: 'Sha', predicate: 'prefers', object: 'dark mode' }, @@ -154,12 +154,11 @@ describe('SqliteMemoryStore (single-user mode)', () => { const summary = store.getSectorSummary(); const semanticCount = summary.find((s) => s.sector === 'semantic')!.count; - // Should only have 1 row because the second ingest merged + // Should only have 1 row because the second ingest was deduplicated expect(semanticCount).toBe(1); - // Query to verify strength increased + // Query to verify the fact exists and strength stayed at 1.0 const result = await store.query('Sha prefers dark mode'); - // The fact should be findable expect(result.perSector.semantic.length).toBeGreaterThanOrEqual(1); }); From 0a4b5df0b83939990c495eb8953c4d480eb4c68d Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:43:14 +0530 Subject: [PATCH 60/78] Remove dead strength field and strengthenFact() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With dedup always-on, strengthenFact() is never called. strength is always 1.0, so strengthScore = log(1.0) = 0 — contributing nothing to scoring. Remove from types, scoring, store, and DDL. --- memory/AGENT.md | 26 +++++++++++--------------- memory/src/scoring.ts | 8 +------- memory/src/sqlite-store.ts | 33 +++++++-------------------------- memory/src/types.ts | 3 +-- 4 files changed, 20 insertions(+), 50 deletions(-) diff --git a/memory/AGENT.md b/memory/AGENT.md index 59c7d22..1f85915 100644 --- a/memory/AGENT.md +++ b/memory/AGENT.md @@ -153,7 +153,7 @@ CONTROL_SIGNAL = 0.3 (fixed) **Formula** (`scoreRowPBWM`, `scoring.ts:22-53`): ``` relevance = cosineSimilarity(queryEmbedding, row.embedding) -expectedValue = normalizeRetrieval(row) // log-normalized retrieval_count + strength +expectedValue = normalizeRetrieval(row) // log-normalized retrieval_count noise = gaussianNoise(mean=0, std=0.05) x = 1.0 * relevance + 0.4 * expectedValue + 0.05 * 0.3 - 0.02 * noise @@ -161,11 +161,10 @@ gateScore = sigmoid(x) score = gateScore * sectorWeight ``` -**`normalizeRetrieval`** (`scoring.ts:57-67`): +**`normalizeRetrieval`** (`scoring.ts:57-61`): ``` retrievalScore = count > 0 ? log(1 + count) / log(1 + 10) : 0 -strengthScore = log(strength) / log(1 + 10) -return min(1, retrievalScore + strengthScore) +return min(1, retrievalScore) ``` ### 3c. Working Memory Gating @@ -192,8 +191,8 @@ When a new semantic fact `{subject, predicate, object}` is ingested: 1. **Find active facts**: Query all non-expired facts for the same subject (`sqlite-store.ts:632-648`) 2. **Semantic predicate matching**: Embed the new predicate and each existing predicate; match if cosine similarity >= 0.9 (`sqlite-store.ts:654-696`). This lets `"is co-founder of"` match `"cofounded"`. 3. **Determine action** (`consolidation.ts:23-43`): - - **No existing facts** → `insert` (new fact with `strength = 1.0`) - - **Exact object match** (case-insensitive) → `merge` (increment `strength` by 1.0) + - **No existing facts** → `insert` (new fact) + - **Exact object match** (case-insensitive) → `merge` (skip, add source attribution) - **Different object** → `supersede` (close old fact by setting `valid_to = now`, then insert new) ### 3e. Multi-Profile System @@ -267,7 +266,6 @@ Knowledge graph triples with temporal validity. | `embedding` | json | NOT NULL — embedded on `"subject predicate object"` | | `metadata` | json | nullable | | `profile_id` | text | NOT NULL DEFAULT 'default' | -| `strength` | REAL | NOT NULL DEFAULT 1.0 — consolidation evidence count | | `source` | text | DEFAULT NULL — relative file path for document-ingested memories | Indexes: `idx_semantic_subject_pred`, `idx_semantic_object`, `idx_semantic_profile`, `idx_semantic_slot` @@ -342,7 +340,7 @@ The provider layer abstracts LLM and embedding API calls behind a two-provider r - `deleteMemory(id, profile)` — Deletes across all 3 tables - `getSummary(limit, profile)` — UNION ALL across tables for dashboard - `getAvailableProfiles()` / `deleteProfile(slug)` — Profile CRUD -- Graph helpers: `findActiveFactsForSubject()`, `findSemanticallyMatchingFacts()`, `strengthenFact()`, `supersedeFact()` +- Graph helpers: `findActiveFactsForSubject()`, `findSemanticallyMatchingFacts()`, `supersedeFact()` - Dedup helpers: `findDuplicateMemory()`, `findDuplicateProcedural()`, `setMemorySource()`, `setProceduralSource()`, `setSemanticSource()` **`documents.ts`** — Document ingestion module: @@ -386,9 +384,9 @@ POST /v1/ingest { messages, profile } ├─ findActiveFactsForSubject(subject, profile) ├─ findSemanticallyMatchingFacts(predicate, facts, threshold=0.9) ├─ determineConsolidationAction(newFact, matchingFacts) - │ ├─ merge → strengthenFact(targetId, delta=1.0) + │ ├─ merge → skip (add source attribution) │ ├─ supersede → supersedeFact(targetId) + INSERT new - │ └─ insert → INSERT new with strength=1.0 + │ └─ insert → INSERT new └─ INSERT into semantic_memory (if not merge) ``` @@ -415,9 +413,7 @@ POST /v1/ingest/documents { path, profile } │ ├─ Duplicate found → skip (add source attribution to existing) │ └─ No duplicate → INSERT with source │ - ├─ Semantic: consolidation as normal, but on merge: - │ ├─ deduplicate=true → skip strengthenFact, just add source - │ └─ deduplicate=false → strengthenFact as before + ├─ Semantic: consolidation as normal, on merge → add source attribution │ └─ Procedural: embed trigger → findDuplicateProcedural (cosine > 0.9) ├─ Duplicate found → skip (add source attribution to existing) @@ -512,7 +508,7 @@ Ingest markdown files from a directory (or single file). Chunks by headings, ext **Deduplication strategy:** - **Episodic**: Embed content, compare against existing rows for same sector+profile. Skip if cosine similarity > 0.9. -- **Semantic**: Uses existing consolidation. On merge (same subject+predicate+object), skips the strength bump to avoid inflating ranking. Adds source attribution to existing fact. +- **Semantic**: Uses existing consolidation. On merge (same subject+predicate+object), adds source attribution to existing fact. - **Procedural**: Embed trigger, compare against existing procedural rows. Skip if cosine similarity > 0.9. ### `POST /v1/search` @@ -811,7 +807,7 @@ sqlite3 memory/memory.db SELECT sector, COUNT(*) FROM memory GROUP BY sector; # View semantic triples -SELECT subject, predicate, object, strength, valid_to IS NULL as active FROM semantic_memory; +SELECT subject, predicate, object, valid_to IS NULL as active FROM semantic_memory; # Check procedural memories SELECT trigger, goal, steps FROM procedural_memory; diff --git a/memory/src/scoring.ts b/memory/src/scoring.ts index fa1729d..51bcdc7 100644 --- a/memory/src/scoring.ts +++ b/memory/src/scoring.ts @@ -56,12 +56,6 @@ export const PBWM_SECTOR_WEIGHTS = DEFAULT_SECTOR_WEIGHTS; function normalizeRetrieval(row: MemoryRecord): number { const count = (row as any).retrievalCount ?? 0; - const strength = (row as any).strength ?? 1.0; - - // Combine retrieval count and strength into expected value - // Both are log-normalized to [0,1] with a soft cap const retrievalScore = count > 0 ? Math.log(1 + count) / Math.log(1 + RETRIEVAL_SOFTCAP) : 0; - const strengthScore = Math.log(strength) / Math.log(1 + RETRIEVAL_SOFTCAP); // strength starts at 1.0 - - return Math.min(1, retrievalScore + strengthScore); + return Math.min(1, retrievalScore); } diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 9c40a06..7f49bf7 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -102,7 +102,6 @@ export class SqliteMemoryStore { validTo: null, createdAt, updatedAt: createdAt, - strength: 1.0, source, domain, originType: origin?.originType, @@ -345,7 +344,7 @@ export class SqliteMemoryStore { where agent_id = @agentId ${userFilter} union all select id, 'semantic' as sector, subject || ' → ' || predicate || ' → ' || object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, - json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'metadata', metadata, 'domain', domain) as details, + json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'metadata', metadata, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount, user_scope as userScope, source from semantic_memory where agent_id = @agentId @@ -460,7 +459,7 @@ export class SqliteMemoryStore { where agent_id = @agentId and user_scope = @userId union all select id, 'semantic' as sector, subject || ' → ' || predicate || ' → ' || object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, - json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'domain', domain) as details, + json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount from semantic_memory where agent_id = @agentId and user_scope = @userId @@ -496,7 +495,7 @@ export class SqliteMemoryStore { where agent_id = @agentId and user_scope is null union all select id, 'semantic' as sector, subject || ' → ' || predicate || ' → ' || object as content, json('[]') as embedding, created_at as createdAt, updated_at as lastAccessed, - json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'strength', strength, 'domain', domain) as details, + json_object('subject', subject, 'predicate', predicate, 'object', object, 'validFrom', valid_from, 'validTo', valid_to, 'domain', domain) as details, null as eventStart, null as eventEnd, 0 as retrievalCount from semantic_memory where agent_id = @agentId and user_scope is null @@ -676,10 +675,10 @@ export class SqliteMemoryStore { this.db .prepare( `insert into semantic_memory ( - id, subject, predicate, object, valid_from, valid_to, created_at, updated_at, embedding, metadata, agent_id, strength, source, + id, subject, predicate, object, valid_from, valid_to, created_at, updated_at, embedding, metadata, agent_id, source, domain, origin_type, origin_actor, origin_ref, user_scope ) values ( - @id, @subject, @predicate, @object, @validFrom, @validTo, @createdAt, @updatedAt, json(@embedding), json(@metadata), @agentId, @strength, @source, + @id, @subject, @predicate, @object, @validFrom, @validTo, @createdAt, @updatedAt, json(@embedding), json(@metadata), @agentId, @source, @domain, @originType, @originActor, @originRef, @userScope )`, ) @@ -695,7 +694,6 @@ export class SqliteMemoryStore { embedding: JSON.stringify(row.embedding), metadata: row.metadata ? JSON.stringify(row.metadata) : null, agentId: row.agentId ?? DEFAULT_AGENT, - strength: row.strength ?? 1.0, source: row.source ?? null, domain: row.domain ?? null, originType: row.originType ?? null, @@ -804,12 +802,12 @@ export class SqliteMemoryStore { const rows = this.db.prepare( `select id, subject, predicate, object, valid_from as validFrom, valid_to as validTo, created_at as createdAt, updated_at as updatedAt, embedding, metadata, agent_id as agentId, - strength, domain + domain from semantic_memory where agent_id = @agentId and (valid_to is null or valid_to > @now) ${userFilter} - order by strength desc, updated_at desc + order by updated_at desc limit @limit`, ).all({ limit, agentId, now, userId }) as any[]; @@ -817,7 +815,6 @@ export class SqliteMemoryStore { ...row, embedding: JSON.parse(row.embedding) as number[], metadata: row.metadata ? JSON.parse(row.metadata) : undefined, - strength: row.strength ?? 1.0, })); } @@ -921,7 +918,6 @@ export class SqliteMemoryStore { embedding json not null, metadata json, agent_id text not null default '${DEFAULT_AGENT}', - strength real not null default 1.0, source text, domain text, origin_type text, @@ -1061,21 +1057,6 @@ export class SqliteMemoryStore { return matchingFacts; } - /** - * Strengthen an existing semantic fact (merge operation). - * Increases strength by delta and updates timestamp. - */ - strengthenFact(id: string, delta: number = 1.0): void { - this.db - .prepare( - `UPDATE semantic_memory - SET strength = strength + @delta, - updated_at = @now - WHERE id = @id` - ) - .run({ id, delta, now: this.now() }); - } - /** * Supersede a semantic fact by closing its validity window. * The fact remains in the database for historical queries. diff --git a/memory/src/types.ts b/memory/src/types.ts index 91da92a..963e8f5 100644 --- a/memory/src/types.ts +++ b/memory/src/types.ts @@ -93,7 +93,6 @@ export interface SemanticMemoryRecord { validTo: number | null; createdAt: number; updatedAt: number; - strength: number; // Evidence count from consolidation (starts at 1.0) source?: string; metadata?: Record; domain?: SemanticDomain; @@ -120,7 +119,7 @@ export interface ReflectiveMemoryRecord { * Consolidation action for semantic facts */ export type ConsolidationAction = - | { type: 'merge'; targetId: string } // Same object exists: strengthen it + | { type: 'merge'; targetId: string } // Same object exists: skip (dedup) | { type: 'supersede'; targetId: string } // Different object: close old, insert new | { type: 'insert' }; // No existing fact for this slot From 3652f9f5bd7a05bf47ff4b894fe25494fe0d0738 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:42:23 +0530 Subject: [PATCH 61/78] Add relevance gate for per-agent content filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents can now set a relevancePrompt that gates ingest — an LLM checks if incoming content matches the agent's scope before extraction/embedding. Irrelevant content is rejected early with a reason. Adds GET/PUT single agent endpoints and updates README with new API docs and flow diagram. --- memory/README.md | 39 ++++++++++++++++--- memory/src/index.ts | 1 + memory/src/memory.ts | 15 ++++++-- memory/src/providers/relevance.ts | 62 +++++++++++++++++++++++++++++++ memory/src/router.ts | 58 ++++++++++++++++++++++++++++- memory/src/sqlite-store.ts | 33 ++++++++++------ memory/src/types.ts | 1 + 7 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 memory/src/providers/relevance.ts diff --git a/memory/README.md b/memory/README.md index 89596ae..867854f 100644 --- a/memory/README.md +++ b/memory/README.md @@ -21,8 +21,9 @@ import { Memory } from '@ekai/memory'; // Provider config is global — shared across all agents const mem = new Memory({ provider: 'openai', apiKey: 'sk-...' }); -// Register an agent (soul is optional) +// Register an agent (soul and relevancePrompt are optional) mem.addAgent('my-bot', { name: 'My Bot', soul: 'You are helpful' }); +mem.addAgent('chef-bot', { name: 'Chef', relevancePrompt: 'Only store memories about cooking and recipes' }); // Get a scoped instance — all data ops go through this const bot = mem.agent('my-bot'); @@ -104,6 +105,7 @@ graph TB classDef output fill:#e8f5e9,stroke:#388e3c,stroke-width:2px IN["POST /v1/ingest
messages + userId"]:::input + RG{"Relevance Gate
(if relevancePrompt)"}:::engine EXT["Agent Reflection (LLM)
first-person, multi-fact"]:::process EP["Episodic"]:::sector @@ -123,7 +125,8 @@ graph TB OUT["Response"]:::output SUM["GET /v1/summary"]:::input - IN --> EXT + IN --> RG -->|relevant| EXT + RG -->|irrelevant| OUT EXT --> EP & SE & PR & RE SE --> CON EP & CON & PR & RE --> EMB --> DB @@ -191,6 +194,12 @@ Ingest a conversation. Full conversation (user + assistant) goes to the LLM for { "stored": 3, "ids": ["...", "...", "..."], "agent": "my-bot" } ``` +If the agent has a `relevancePrompt` and the content is irrelevant, the response short-circuits: + +```json +{ "stored": 0, "ids": [], "filtered": true, "reason": "Content is about geography, not cooking" } +``` + ### `POST /v1/search` Search with PBWM gating. Pass `userId` for user-scoped retrieval. @@ -239,13 +248,29 @@ List all registered agents. ### `POST /v1/agents` -Register a new agent. `name` and `soul` are optional. +Register a new agent. `name`, `soul`, and `relevancePrompt` are optional. + +```json +{ "id": "my-bot", "name": "My Bot", "soul": "You are helpful", "relevancePrompt": "Only store cooking-related memories" } +``` +```json +{ "agent": { "id": "my-bot", "name": "My Bot", "soulMd": "You are helpful", "relevancePrompt": "Only store cooking-related memories", "createdAt": 1700000000 } } +``` + +### `GET /v1/agents/:slug` + +Get a single agent by ID. ```json -{ "id": "my-bot", "name": "My Bot", "soul": "You are helpful and concise" } +{ "agent": { "id": "my-bot", "name": "My Bot", "relevancePrompt": "...", "createdAt": 1700000000 } } ``` + +### `PUT /v1/agents/:slug` + +Update agent properties. Set `relevancePrompt` to `null` to remove it. + ```json -{ "agent": { "id": "my-bot", "name": "My Bot", "soulMd": "You are helpful and concise", "createdAt": 1700000000 } } +{ "name": "Updated Name", "relevancePrompt": null } ``` ### `DELETE /v1/agents/:slug` @@ -304,6 +329,8 @@ Graph visualization data (nodes + edges). Query: `?entity=Sha&maxDepth=2&maxNode |--------|----------|-------------| | GET | `/v1/agents` | List agents | | POST | `/v1/agents` | Create agent | +| GET | `/v1/agents/:slug` | Get single agent | +| PUT | `/v1/agents/:slug` | Update agent | | DELETE | `/v1/agents/:slug` | Delete agent + memories | | POST | `/v1/ingest` | Ingest conversation | | POST | `/v1/search` | Search with PBWM gating | @@ -353,7 +380,7 @@ erDiagram procedural_memory { text id PK; text trigger; json steps; text user_scope; text origin_type } reflective_memory { text id PK; text observation; text origin_type; text origin_actor } agent_users { text agent_id PK; text user_id PK; int interaction_count } - agents { text id PK; text name; text soul_md; int created_at } + agents { text id PK; text name; text soul_md; text relevance_prompt; int created_at } ``` All tables share: `embedding`, `created_at`, `last_accessed`, `agent_id`, `source`, `origin_type`, `origin_actor`, `origin_ref`. Clean schema — no migrations, old DBs re-create. diff --git a/memory/src/index.ts b/memory/src/index.ts index 7328139..79afb17 100644 --- a/memory/src/index.ts +++ b/memory/src/index.ts @@ -2,6 +2,7 @@ export * from './types.js'; export * from './sqlite-store.js'; export { embed, createEmbedFn } from './providers/embed.js'; export { extract, createExtractFn } from './providers/extract.js'; +export { checkRelevance } from './providers/relevance.js'; export * from './providers/prompt.js'; export { PROVIDERS } from './providers/registry.js'; export type { ProviderConfig } from './providers/registry.js'; diff --git a/memory/src/memory.ts b/memory/src/memory.ts index aadcc06..bef25da 100644 --- a/memory/src/memory.ts +++ b/memory/src/memory.ts @@ -9,6 +9,7 @@ import type { ProviderName, QueryResult, } from './types.js'; +import { checkRelevance } from './providers/relevance.js'; export interface MemoryConfig { provider?: ProviderName; @@ -51,8 +52,8 @@ export class Memory { // --- Management (always available) --- /** Register an agent. `soul` is optional agent-level context/personality. */ - addAgent(id: string, opts?: { name?: string; soul?: string }): AgentInfo { - return this.store.addAgent(id, { name: opts?.name, soulMd: opts?.soul }); + addAgent(id: string, opts?: { name?: string; soul?: string; relevancePrompt?: string }): AgentInfo { + return this.store.addAgent(id, { name: opts?.name, soulMd: opts?.soul, relevancePrompt: opts?.relevancePrompt }); } getAgents(): AgentInfo[] { @@ -81,7 +82,7 @@ export class Memory { async add( messages: Array<{ role: string; content: string }>, opts?: { userId?: string }, - ): Promise<{ stored: number; ids: string[] }> { + ): Promise<{ stored: number; ids: string[]; filtered?: boolean; reason?: string }> { const agent = this.requireAgent(); const allMessages = messages.filter((m) => m.content?.trim()); @@ -91,6 +92,14 @@ export class Memory { if (!sourceText) return { stored: 0, ids: [] }; + const agentInfo = this.store.getAgent(agent); + if (agentInfo?.relevancePrompt) { + const check = await checkRelevance(sourceText, agentInfo.relevancePrompt); + if (!check.relevant) { + return { stored: 0, ids: [], filtered: true, reason: check.reason }; + } + } + const components = await this.extractFn(sourceText); if (!components) return { stored: 0, ids: [] }; diff --git a/memory/src/providers/relevance.ts b/memory/src/providers/relevance.ts new file mode 100644 index 0000000..d156952 --- /dev/null +++ b/memory/src/providers/relevance.ts @@ -0,0 +1,62 @@ +import { resolveProvider, getApiKey, getModel, buildUrl } from './registry.js'; + +const RELEVANCE_SYSTEM = `You are a relevance filter. The user will provide text from a conversation. +You must decide if this text is relevant to the agent's memory scope described below. + +AGENT RELEVANCE CRITERIA: +{RELEVANCE_PROMPT} + +Respond with JSON only: { "relevant": true/false, "reason": "brief explanation" } +If unsure, lean toward relevant (true).`; + +export async function checkRelevance( + text: string, + relevancePrompt: string, +): Promise<{ relevant: boolean; reason: string }> { + try { + const cfg = resolveProvider('extract'); + const apiKey = getApiKey(cfg); + const model = getModel(cfg, 'extract'); + const { url, headers } = buildUrl(cfg, 'extract', model, apiKey); + + const systemPrompt = RELEVANCE_SYSTEM.replace('{RELEVANCE_PROMPT}', relevancePrompt); + + if (cfg.name === 'gemini') { + const resp = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ + contents: [{ role: 'user', parts: [{ text: `${systemPrompt}\n\n${text}` }] }], + generationConfig: { temperature: 0, responseMimeType: 'application/json' }, + }), + }); + if (!resp.ok) throw new Error(`gemini relevance failed: ${resp.status}`); + const json = (await resp.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> }; + const content = json.candidates?.[0]?.content?.parts?.[0]?.text ?? '{}'; + const parsed = JSON.parse(content); + return { relevant: !!parsed.relevant, reason: parsed.reason ?? '' }; + } + + const resp = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ + model, + temperature: 0, + response_format: { type: 'json_object' }, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: text }, + ], + }), + }); + if (!resp.ok) throw new Error(`openai relevance failed: ${resp.status}`); + const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> }; + const content = json.choices[0]?.message?.content ?? '{}'; + const parsed = JSON.parse(content); + return { relevant: !!parsed.relevant, reason: parsed.reason ?? '' }; + } catch (_) { + // Fail-open: if relevance check errors, proceed with ingestion + return { relevant: true, reason: 'filter_error' }; + } +} diff --git a/memory/src/router.ts b/memory/src/router.ts index 540c14c..ea056b8 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -3,6 +3,7 @@ import type { Request, Response } from 'express'; import type { SqliteMemoryStore } from './sqlite-store.js'; import type { ExtractFn } from './types.js'; import { extract as defaultExtract } from './providers/extract.js'; +import { checkRelevance } from './providers/relevance.js'; import { normalizeAgentId } from './utils.js'; import type { IngestComponents } from './types.js'; @@ -26,11 +27,11 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract router.post('/v1/agents', (req: Request, res: Response) => { try { - const { id, name, soul } = req.body as { id?: string; name?: string; soul?: string }; + const { id, name, soul, relevancePrompt } = req.body as { id?: string; name?: string; soul?: string; relevancePrompt?: string }; if (!id || !id.trim()) { return res.status(400).json({ error: 'id is required' }); } - const agent = store.addAgent(id.trim(), { name: name?.trim(), soulMd: soul }); + const agent = store.addAgent(id.trim(), { name: name?.trim(), soulMd: soul, relevancePrompt }); res.json({ agent }); } catch (err: any) { if (err?.message === 'invalid_agent') { @@ -58,6 +59,46 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract }; router.delete('/v1/agents/:slug', handleDeleteAgent); + router.get('/v1/agents/:slug', (req: Request, res: Response) => { + try { + const { slug } = req.params; + const normalizedAgent = normalizeAgentId(slug); + const agent = store.getAgent(normalizedAgent); + if (!agent) { + return res.status(404).json({ error: 'agent_not_found' }); + } + res.json({ agent }); + } catch (err: any) { + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); + } + res.status(500).json({ error: err.message ?? 'failed to get agent' }); + } + }); + + router.put('/v1/agents/:slug', (req: Request, res: Response) => { + try { + const { slug } = req.params; + const normalizedAgent = normalizeAgentId(slug); + const existing = store.getAgent(normalizedAgent); + if (!existing) { + return res.status(404).json({ error: 'agent_not_found' }); + } + const { name, soul, relevancePrompt } = req.body as { name?: string; soul?: string; relevancePrompt?: string | null }; + const agent = store.addAgent(normalizedAgent, { + name: name?.trim() ?? existing.name, + soulMd: soul !== undefined ? soul : existing.soulMd, + relevancePrompt: relevancePrompt !== undefined ? (relevancePrompt ?? undefined) : existing.relevancePrompt, + }); + res.json({ agent }); + } catch (err: any) { + if (err?.message === 'invalid_agent') { + return res.status(400).json({ error: 'invalid_agent' }); + } + res.status(500).json({ error: err.message ?? 'failed to update agent' }); + } + }); + router.post('/v1/ingest', async (req: Request, res: Response) => { const { messages, agent, userId } = req.body as { messages?: Array<{ role: 'user' | 'assistant' | string; content: string }>; @@ -89,6 +130,19 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract .map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content.trim()}`) .join('\n\n'); + // Relevance gate: skip extraction + embedding if agent has a relevance prompt and content is irrelevant + const agentInfo = store.getAgent(normalizedAgent); + if (agentInfo?.relevancePrompt) { + try { + const check = await checkRelevance(sourceText, agentInfo.relevancePrompt); + if (!check.relevant) { + return res.json({ stored: 0, ids: [], filtered: true, reason: check.reason }); + } + } catch (_) { + // Fail-open: proceed with ingestion on error + } + } + let finalComponents: IngestComponents | undefined; try { diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index 7f49bf7..c0e0305 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -385,36 +385,38 @@ export class SqliteMemoryStore { // --- Agent Management --- - addAgent(id: string, opts?: { name?: string; soulMd?: string }): AgentInfo { + addAgent(id: string, opts?: { name?: string; soulMd?: string; relevancePrompt?: string }): AgentInfo { const agentId = normalizeAgentId(id); const now = this.now(); const name = opts?.name ?? agentId; const soulMd = opts?.soulMd ?? null; + const relevancePrompt = opts?.relevancePrompt ?? null; this.db .prepare( - `INSERT INTO agents (id, name, soul_md, created_at) - VALUES (@id, @name, @soulMd, @createdAt) + `INSERT INTO agents (id, name, soul_md, relevance_prompt, created_at) + VALUES (@id, @name, @soulMd, @relevancePrompt, @createdAt) ON CONFLICT(id) DO UPDATE SET name = @name, - soul_md = @soulMd`, + soul_md = @soulMd, + relevance_prompt = @relevancePrompt`, ) - .run({ id: agentId, name, soulMd, createdAt: now }); - return { id: agentId, name, soulMd: soulMd ?? undefined, createdAt: now }; + .run({ id: agentId, name, soulMd, relevancePrompt, createdAt: now }); + return { id: agentId, name, soulMd: soulMd ?? undefined, relevancePrompt: relevancePrompt ?? undefined, createdAt: now }; } getAgent(agentId: string): AgentInfo | undefined { const row = this.db - .prepare('SELECT id, name, soul_md as soulMd, created_at as createdAt FROM agents WHERE id = @id') - .get({ id: agentId }) as { id: string; name: string; soulMd: string | null; createdAt: number } | undefined; + .prepare('SELECT id, name, soul_md as soulMd, relevance_prompt as relevancePrompt, created_at as createdAt FROM agents WHERE id = @id') + .get({ id: agentId }) as { id: string; name: string; soulMd: string | null; relevancePrompt: string | null; createdAt: number } | undefined; if (!row) return undefined; - return { id: row.id, name: row.name, soulMd: row.soulMd ?? undefined, createdAt: row.createdAt }; + return { id: row.id, name: row.name, soulMd: row.soulMd ?? undefined, relevancePrompt: row.relevancePrompt ?? undefined, createdAt: row.createdAt }; } getAgents(): AgentInfo[] { const rows = this.db - .prepare('SELECT id, name, soul_md as soulMd, created_at as createdAt FROM agents ORDER BY id') - .all() as Array<{ id: string; name: string; soulMd: string | null; createdAt: number }>; - return rows.map((r) => ({ id: r.id, name: r.name, soulMd: r.soulMd ?? undefined, createdAt: r.createdAt })); + .prepare('SELECT id, name, soul_md as soulMd, relevance_prompt as relevancePrompt, created_at as createdAt FROM agents ORDER BY id') + .all() as Array<{ id: string; name: string; soulMd: string | null; relevancePrompt: string | null; createdAt: number }>; + return rows.map((r) => ({ id: r.id, name: r.name, soulMd: r.soulMd ?? undefined, relevancePrompt: r.relevancePrompt ?? undefined, createdAt: r.createdAt })); } // --- Agent Users --- @@ -977,6 +979,13 @@ export class SqliteMemoryStore { ) .run(); + // Migration: add relevance_prompt column to agents table (idempotent) + try { + this.db.prepare('ALTER TABLE agents ADD COLUMN relevance_prompt text default null').run(); + } catch (_) { + // Column already exists — ignore + } + // Ensure the default agent always exists this.ensureAgentExists(DEFAULT_AGENT); } diff --git a/memory/src/types.ts b/memory/src/types.ts index 963e8f5..d1d2506 100644 --- a/memory/src/types.ts +++ b/memory/src/types.ts @@ -8,6 +8,7 @@ export interface AgentInfo { id: string; name: string; soulMd?: string; + relevancePrompt?: string; createdAt: number; } From 2eccb20b192f683e0f99c1be6aabeb1103f480c6 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:53:02 +0530 Subject: [PATCH 62/78] Fix ensureAgentExists auto-recreate bug Replace silent INSERT OR IGNORE with a strict existence check that throws agent_not_found. Only the default agent is auto-created at init via a private upsertDefaultAgent(). Add routeError helper to router so all catch blocks return 404 on agent_not_found instead of 500. --- memory/src/router.ts | 70 +++++++++++--------------------------- memory/src/sqlite-store.ts | 15 ++++++-- 2 files changed, 32 insertions(+), 53 deletions(-) diff --git a/memory/src/router.ts b/memory/src/router.ts index ea056b8..8136952 100644 --- a/memory/src/router.ts +++ b/memory/src/router.ts @@ -8,6 +8,12 @@ import { normalizeAgentId } from './utils.js'; import type { IngestComponents } from './types.js'; +function routeError(err: any, res: Response, fallback = 'operation failed') { + if (err?.message === 'invalid_agent') return res.status(400).json({ error: 'invalid_agent' }); + if (err?.message === 'agent_not_found') return res.status(404).json({ error: 'agent_not_found' }); + res.status(500).json({ error: err?.message ?? fallback }); +} + /** * Creates an Express Router with all memory API routes. * The store is received via closure — no global state needed. @@ -34,10 +40,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract const agent = store.addAgent(id.trim(), { name: name?.trim(), soulMd: soul, relevancePrompt }); res.json({ agent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'failed to add agent' }); + routeError(err, res, 'failed to add agent'); } }); @@ -48,13 +51,10 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract const deleted = store.deleteAgent(normalizedAgent); res.json({ deleted, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } if (err?.message === 'cannot_delete_default_agent') { return res.status(400).json({ error: 'default_agent_protected' }); } - res.status(500).json({ error: err.message ?? 'delete agent failed' }); + routeError(err, res, 'delete agent failed'); } }; router.delete('/v1/agents/:slug', handleDeleteAgent); @@ -69,10 +69,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract } res.json({ agent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'failed to get agent' }); + routeError(err, res, 'failed to get agent'); } }); @@ -92,10 +89,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract }); res.json({ agent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'failed to update agent' }); + routeError(err, res, 'failed to update agent'); } }); @@ -161,7 +155,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract }); res.json({ stored: rows.length, ids: rows.map((r) => r.id), agent: normalizedAgent }); } catch (err: any) { - res.status(500).json({ error: err.message ?? 'ingest failed' }); + routeError(err, res, 'ingest failed'); } }); @@ -186,10 +180,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract })); res.json({ summary, recent, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'summary failed' }); + routeError(err, res, 'summary failed'); } }); @@ -219,7 +210,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract } res.json({ updated: true, id, agent: normalizedAgent }); } catch (err: any) { - res.status(500).json({ error: err.message ?? 'update failed' }); + routeError(err, res, 'update failed'); } }); @@ -243,10 +234,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract } res.json({ deleted, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'delete failed' }); + routeError(err, res, 'delete failed'); } }); @@ -257,10 +245,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract const deleted = store.deleteAll(normalizedAgent); res.json({ deleted, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'delete all failed' }); + routeError(err, res, 'delete all failed'); } }); @@ -278,10 +263,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract res.json({ deleted }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'triple delete failed' }); + routeError(err, res, 'triple delete failed'); } }); @@ -294,10 +276,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract const result = await store.query(query, agent, userId); res.json(result); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'query failed' }); + routeError(err, res, 'query failed'); } }); @@ -310,10 +289,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract const users = store.getAgentUsers(normalizedAgent); res.json({ users, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'failed to fetch users' }); + routeError(err, res, 'failed to fetch users'); } }); @@ -339,10 +315,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract })); res.json({ memories, userId, agent: normalizedAgent }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'failed to fetch user memories' }); + routeError(err, res, 'failed to fetch user memories'); } }); @@ -372,10 +345,7 @@ export function createMemoryRouter(store: SqliteMemoryStore, extractFn?: Extract res.json({ entity, triples, count: triples.length }); } catch (err: any) { - if (err?.message === 'invalid_agent') { - return res.status(400).json({ error: 'invalid_agent' }); - } - res.status(500).json({ error: err.message ?? 'graph query failed' }); + routeError(err, res, 'graph query failed'); } }); diff --git a/memory/src/sqlite-store.ts b/memory/src/sqlite-store.ts index c0e0305..792b57e 100644 --- a/memory/src/sqlite-store.ts +++ b/memory/src/sqlite-store.ts @@ -987,13 +987,22 @@ export class SqliteMemoryStore { } // Ensure the default agent always exists - this.ensureAgentExists(DEFAULT_AGENT); + this.upsertDefaultAgent(); } - ensureAgentExists(agentId: string) { + private upsertDefaultAgent() { this.db .prepare('insert or ignore into agents (id, name, created_at) values (@id, @name, @createdAt)') - .run({ id: agentId, name: agentId, createdAt: this.now() }); + .run({ id: DEFAULT_AGENT, name: DEFAULT_AGENT, createdAt: this.now() }); + } + + ensureAgentExists(agentId: string) { + const exists = this.db + .prepare('select 1 from agents where id = @id') + .get({ id: agentId }); + if (!exists) { + throw new Error('agent_not_found'); + } } /** From 3f9f55ed1562d3be556a59023dd2831e4a33b659 Mon Sep 17 00:00:00 2001 From: sh1hsh1nk <13179671+sm86@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:05:45 +0530 Subject: [PATCH 63/78] Add /agents dashboard page with full agent management - New /agents page: agent cards with soul/relevance prompts, per-agent stats (users, episodic, semantic, procedural), edit/create/delete modals - api.ts: add soulMd + relevancePrompt to getAgents() type; add createAgent() and updateAgent() calls - layout.tsx: add global top nav with Memory Vault and Agents links - memory/page.tsx: offset sticky header to top-11 to clear global nav --- ui/dashboard/src/app/agents/page.tsx | 487 +++++++++++++++++++++++++++ ui/dashboard/src/app/layout.tsx | 12 + ui/dashboard/src/app/memory/page.tsx | 2 +- ui/dashboard/src/lib/api.ts | 28 +- 4 files changed, 527 insertions(+), 2 deletions(-) create mode 100644 ui/dashboard/src/app/agents/page.tsx diff --git a/ui/dashboard/src/app/agents/page.tsx b/ui/dashboard/src/app/agents/page.tsx new file mode 100644 index 0000000..d38eaa4 --- /dev/null +++ b/ui/dashboard/src/app/agents/page.tsx @@ -0,0 +1,487 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { apiService } from '@/lib/api'; + +// ── helpers (mirrored from ProfileManagement) ────────────────────────────────── +const COLORS = [ + 'bg-emerald-500', 'bg-purple-500', 'bg-blue-500', 'bg-amber-500', + 'bg-rose-500', 'bg-cyan-500', 'bg-indigo-500','bg-pink-500', + 'bg-teal-500', 'bg-orange-500', +]; + +function getColor(index: number) { return COLORS[index % COLORS.length]; } + +function formatName(slug: string) { + return slug.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); +} + +const SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,38}[a-z0-9]$|^[a-z0-9]$/; + +// ── types ────────────────────────────────────────────────────────────────────── +interface Agent { + id: string; + name: string; + createdAt: number; + soulMd?: string; + relevancePrompt?: string; +} + +interface AgentStats { + userCount: number; + episodic: number; + semantic: number; + procedural: number; + loaded: boolean; +} + +// ── sub-components (inline, one-off) ────────────────────────────────────────── +function PromptBlock({ label, text }: { label: string; text?: string }) { + return ( +
+
{label}
+ {text ? ( +
+          {text}
+        
+ ) : ( +

+ Not set +

+ )} +
+ ); +} + +// ── main page ────────────────────────────────────────────────────────────────── +export default function AgentsPage() { + const [agents, setAgents] = useState([]); + const [stats, setStats] = useState>({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + // edit modal + const [editTarget, setEditTarget] = useState(null); + const [editName, setEditName] = useState(''); + const [editSoul, setEditSoul] = useState(''); + const [editRelevance, setEditRelevance] = useState(''); + const [editBusy, setEditBusy] = useState(false); + const [editError, setEditError] = useState(''); + + // create modal + const [showCreate, setShowCreate] = useState(false); + const [createId, setCreateId] = useState(''); + const [createName, setCreateName] = useState(''); + const [createSoul, setCreateSoul] = useState(''); + const [createRelevance, setCreateRelevance] = useState(''); + const [createBusy, setCreateBusy] = useState(false); + const [createError, setCreateError] = useState(''); + + // delete modal + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleteBusy, setDeleteBusy] = useState(false); + const [deleteError, setDeleteError] = useState(''); + + const loadAgents = async () => { + try { + setLoading(true); + setError(''); + const { agents: list } = await apiService.getAgents(); + setAgents(list); + // load stats lazily after paint + setTimeout(() => loadStats(list), 0); + } catch (e) { + setError(e instanceof Error ? e.message : 'Failed to load agents'); + } finally { + setLoading(false); + } + }; + + const loadStats = async (list: Agent[]) => { + await Promise.all(list.map(async (agent, _i) => { + try { + const [usersRes, summaryRes] = await Promise.all([ + apiService.getUsers(agent.id), + apiService.getMemorySummary(1, agent.id), + ]); + setStats(prev => ({ + ...prev, + [agent.id]: { + userCount: usersRes.users.length, + episodic: summaryRes.summary.find(s => s.sector === 'episodic')?.count ?? 0, + semantic: summaryRes.summary.find(s => s.sector === 'semantic')?.count ?? 0, + procedural:summaryRes.summary.find(s => s.sector === 'procedural')?.count?? 0, + loaded: true, + }, + })); + } catch { + setStats(prev => ({ + ...prev, + [agent.id]: { userCount: 0, episodic: 0, semantic: 0, procedural: 0, loaded: true }, + })); + } + })); + }; + + useEffect(() => { loadAgents(); }, []); + + // ── edit handlers ──────────────────────────────────────────────────────────── + const openEdit = (agent: Agent) => { + setEditTarget(agent); + setEditName(agent.name ?? ''); + setEditSoul(agent.soulMd ?? ''); + setEditRelevance(agent.relevancePrompt ?? ''); + setEditError(''); + }; + + const handleEditSave = async () => { + if (!editTarget) return; + try { + setEditBusy(true); + setEditError(''); + await apiService.updateAgent(editTarget.id, { + name: editName || undefined, + soulMd: editSoul || undefined, + relevancePrompt: editRelevance || undefined, + }); + setAgents(prev => prev.map(a => a.id === editTarget.id + ? { ...a, name: editName || a.name, soulMd: editSoul, relevancePrompt: editRelevance } + : a + )); + setEditTarget(null); + } catch (e) { + setEditError(e instanceof Error ? e.message : 'Failed to save'); + } finally { + setEditBusy(false); + } + }; + + // ── create handlers ────────────────────────────────────────────────────────── + const openCreate = () => { + setCreateId(''); setCreateName(''); setCreateSoul(''); setCreateRelevance(''); + setCreateError(''); + setShowCreate(true); + }; + + const handleCreate = async () => { + if (!SLUG_RE.test(createId)) { + setCreateError('ID must be 1-40 chars, lowercase alphanumeric, dash, or underscore'); + return; + } + try { + setCreateBusy(true); + setCreateError(''); + await apiService.createAgent(createId, { + name: createName || undefined, + soulMd: createSoul || undefined, + relevancePrompt: createRelevance || undefined, + }); + setShowCreate(false); + await loadAgents(); + } catch (e) { + setCreateError(e instanceof Error ? e.message : 'Failed to create agent'); + } finally { + setCreateBusy(false); + } + }; + + // ── delete handlers ────────────────────────────────────────────────────────── + const handleDelete = async () => { + if (!deleteTarget) return; + try { + setDeleteBusy(true); + setDeleteError(''); + await apiService.deleteAgent(deleteTarget.id); + setDeleteTarget(null); + setAgents(prev => prev.filter(a => a.id !== deleteTarget.id)); + setStats(prev => { const n = { ...prev }; delete n[deleteTarget.id]; return n; }); + } catch (e) { + setDeleteError(e instanceof Error ? e.message : 'Failed to delete agent'); + } finally { + setDeleteBusy(false); + } + }; + + // ── render ──────────────────────────────────────────────────────────────────── + return ( +
+ {/* Page header */} +
+
+
+

Agents

+

+ {agents.length} agent{agents.length !== 1 ? 's' : ''} configured +

+
+ +
+ + {error && ( +
{error}
+ )} + + {loading ? ( +
+
+
+ ) : agents.length === 0 ? ( +
+

No agents found. Create one to get started.

+
+ ) : ( +
+ {agents.map((agent, idx) => { + const agentStats = stats[agent.id]; + const displayName = agent.name ? formatName(agent.name) : formatName(agent.id); + const color = getColor(idx); + return ( +
+ {/* Avatar + name */} +
+
+ {displayName[0].toUpperCase()} +
+
+
{displayName}
+
{agent.id}
+ {agent.createdAt > 0 && ( +
+ Created {new Date(agent.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} +
+ )} +
+
+ + {/* Stats row */} +
+ {!agentStats?.loaded ? ( + Loading stats… + ) : ( + <> + + + + + + )} +
+ + {/* Soul / Relevance prompts */} + + + + {/* Actions */} + {agent.id !== 'default' && ( +
+ + +
+ )} +
+ ); + })} +
+ )} +
+ + {/* ── Edit Modal ─────────────────────────────────────────────────────────── */} + {editTarget && ( + setEditTarget(null)}> +

Edit Agent — {editTarget.id}

+ {editError &&

{editError}

} + +