@decido/kernel-bridge 1.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,237 +0,0 @@
1
- /**
2
- * TokenWallet — Usage tracking & cost estimation for LLM providers
3
- *
4
- * Records every inference call and calculates estimated costs
5
- * using a per-model pricing table. Persists via localStorage
6
- * (with Tauri fs fallback).
7
- */
8
-
9
- // ─── Pricing Table (USD per 1M tokens) ─────────────────────
10
-
11
- interface ModelPricing {
12
- input: number; // per 1M input tokens
13
- output: number; // per 1M output tokens
14
- }
15
-
16
- const PRICING: Record<string, ModelPricing> = {
17
- // Ollama / local — free
18
- 'qwen2:latest': { input: 0, output: 0 },
19
- 'llama3:latest': { input: 0, output: 0 },
20
- 'mistral:latest': { input: 0, output: 0 },
21
- 'codellama:latest': { input: 0, output: 0 },
22
-
23
- // Gemini
24
- 'gemini-flash-lite-latest': { input: 0.10, output: 0.40 },
25
- 'gemini-flash-lite-latest-lite': { input: 0.075, output: 0.30 },
26
- 'gemini-1.5-pro': { input: 1.25, output: 5.00 },
27
- 'gemini-flash-latest': { input: 0.075, output: 0.30 },
28
-
29
- // Anthropic
30
- 'claude-sonnet-4-20250514': { input: 3.00, output: 15.00 },
31
- 'claude-3-5-sonnet-20241022': { input: 3.00, output: 15.00 },
32
- 'claude-3-5-haiku-20241022': { input: 0.80, output: 4.00 },
33
- 'claude-3-opus-20240229': { input: 15.00, output: 75.00 },
34
-
35
- // OpenAI
36
- 'gpt-4o': { input: 2.50, output: 10.00 },
37
- 'gpt-4o-mini': { input: 0.15, output: 0.60 },
38
- 'gpt-4-turbo': { input: 10.00, output: 30.00 },
39
- 'gpt-4': { input: 30.00, output: 60.00 },
40
- 'gpt-3.5-turbo': { input: 0.50, output: 1.50 },
41
- 'o3-mini': { input: 1.10, output: 4.40 },
42
- };
43
-
44
- // Default pricing for unknown models (assume cloud provider)
45
- const DEFAULT_PRICING: ModelPricing = { input: 1.00, output: 3.00 };
46
-
47
- // ─── Types ──────────────────────────────────────────────────
48
-
49
- export interface TokenUsageEntry {
50
- provider: string;
51
- model: string;
52
- inputTokens: number;
53
- outputTokens: number;
54
- totalTokens: number;
55
- estimatedCostUsd: number;
56
- timestamp: number;
57
- }
58
-
59
- export interface ProviderSummary {
60
- tokens: number;
61
- cost: number;
62
- calls: number;
63
- }
64
-
65
- export interface WalletSummary {
66
- totalTokens: number;
67
- totalCostUsd: number;
68
- totalCalls: number;
69
- byProvider: Record<string, ProviderSummary>;
70
- sessionStart: number;
71
- }
72
-
73
- type WalletListener = (summary: WalletSummary) => void;
74
-
75
- // ─── Storage Key ────────────────────────────────────────────
76
-
77
- const STORAGE_KEY = 'decido-token-wallet';
78
- const MAX_HISTORY = 500; // keep last 500 entries
79
-
80
- // ─── TokenWallet Class ──────────────────────────────────────
81
-
82
- class TokenWalletImpl {
83
- private history: TokenUsageEntry[] = [];
84
- private sessionStart = Date.now();
85
- private listeners = new Set<WalletListener>();
86
-
87
- constructor() {
88
- this._loadFromStorage();
89
- }
90
-
91
- // ── Record Usage ────────────────────────────────────────
92
-
93
- record(entry: {
94
- provider: string;
95
- model: string;
96
- tokensUsed?: number;
97
- inputTokens?: number;
98
- outputTokens?: number;
99
- latencyMs?: number;
100
- }): void {
101
- // Estimate input/output split if not provided
102
- const totalTokens = entry.tokensUsed ?? (entry.inputTokens ?? 0) + (entry.outputTokens ?? 0);
103
- const inputTokens = entry.inputTokens ?? Math.round(totalTokens * 0.4);
104
- const outputTokens = entry.outputTokens ?? (totalTokens - inputTokens);
105
-
106
- const pricing = this._getPricing(entry.model, entry.provider);
107
- const estimatedCostUsd =
108
- (inputTokens / 1_000_000) * pricing.input +
109
- (outputTokens / 1_000_000) * pricing.output;
110
-
111
- const usageEntry: TokenUsageEntry = {
112
- provider: entry.provider,
113
- model: entry.model,
114
- inputTokens,
115
- outputTokens,
116
- totalTokens,
117
- estimatedCostUsd,
118
- timestamp: Date.now(),
119
- };
120
-
121
- this.history.push(usageEntry);
122
-
123
- // Trim history if too large
124
- if (this.history.length > MAX_HISTORY) {
125
- this.history = this.history.slice(-MAX_HISTORY);
126
- }
127
-
128
- this._saveToStorage();
129
- this._emit();
130
-
131
- console.log(
132
- `💰 [Wallet] ${entry.provider}/${entry.model}: ${totalTokens} tokens ≈ $${estimatedCostUsd.toFixed(6)}`
133
- );
134
- }
135
-
136
- // ── Summary ─────────────────────────────────────────────
137
-
138
- getSummary(): WalletSummary {
139
- const byProvider: Record<string, ProviderSummary> = {};
140
- let totalTokens = 0;
141
- let totalCostUsd = 0;
142
-
143
- for (const entry of this.history) {
144
- totalTokens += entry.totalTokens;
145
- totalCostUsd += entry.estimatedCostUsd;
146
-
147
- if (!byProvider[entry.provider]) {
148
- byProvider[entry.provider] = { tokens: 0, cost: 0, calls: 0 };
149
- }
150
- byProvider[entry.provider].tokens += entry.totalTokens;
151
- byProvider[entry.provider].cost += entry.estimatedCostUsd;
152
- byProvider[entry.provider].calls += 1;
153
- }
154
-
155
- return {
156
- totalTokens,
157
- totalCostUsd,
158
- totalCalls: this.history.length,
159
- byProvider,
160
- sessionStart: this.sessionStart,
161
- };
162
- }
163
-
164
- getHistory(): TokenUsageEntry[] {
165
- return [...this.history];
166
- }
167
-
168
- getRecentHistory(count: number): TokenUsageEntry[] {
169
- return this.history.slice(-count);
170
- }
171
-
172
- // ── Lifecycle ───────────────────────────────────────────
173
-
174
- clearHistory(): void {
175
- this.history = [];
176
- this.sessionStart = Date.now();
177
- this._saveToStorage();
178
- this._emit();
179
- }
180
-
181
- // ── Subscriptions ───────────────────────────────────────
182
-
183
- subscribe(listener: WalletListener): () => void {
184
- this.listeners.add(listener);
185
- return () => { this.listeners.delete(listener); };
186
- }
187
-
188
- // ── Pricing Lookup ──────────────────────────────────────
189
-
190
- private _getPricing(model: string, provider: string): ModelPricing {
191
- // Exact match
192
- if (PRICING[model]) return PRICING[model];
193
-
194
- // Local providers are always free
195
- if (provider === 'ollama' || provider === 'mlx') {
196
- return { input: 0, output: 0 };
197
- }
198
-
199
- return DEFAULT_PRICING;
200
- }
201
-
202
- // ── Persistence ─────────────────────────────────────────
203
-
204
- private _saveToStorage(): void {
205
- try {
206
- const data = JSON.stringify({
207
- history: this.history,
208
- sessionStart: this.sessionStart,
209
- });
210
- localStorage.setItem(STORAGE_KEY, data);
211
- } catch { /* storage full or unavailable */ }
212
- }
213
-
214
- private _loadFromStorage(): void {
215
- try {
216
- const raw = localStorage.getItem(STORAGE_KEY);
217
- if (raw) {
218
- const data = JSON.parse(raw);
219
- this.history = data.history || [];
220
- this.sessionStart = data.sessionStart || Date.now();
221
- }
222
- } catch { /* corrupted data, start fresh */ }
223
- }
224
-
225
- // ── Emit ────────────────────────────────────────────────
226
-
227
- private _emit(): void {
228
- const summary = this.getSummary();
229
- for (const listener of this.listeners) {
230
- try { listener(summary); } catch { /* ignore listener errors */ }
231
- }
232
- }
233
- }
234
-
235
- // ─── Singleton Export ───────────────────────────────────────
236
-
237
- export const tokenWallet = new TokenWalletImpl();
@@ -1,229 +0,0 @@
1
- /**
2
- * AnthropicProvider — Claude LLM provider
3
- *
4
- * Supports Claude 3.5 Sonnet, Claude 3 Opus/Haiku via Anthropic Messages API.
5
- * Implements both streaming and non-streaming chat.
6
- */
7
-
8
- import type { LLMProvider, ChatMessage, ChatOptions, ChatResult, ProviderStatus } from './LLMProvider';
9
-
10
- // ─── Config ─────────────────────────────────────────────────
11
-
12
- const ANTHROPIC_API_BASE = 'https://api.anthropic.com/v1';
13
- const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
14
- const API_VERSION = '2023-06-01';
15
-
16
- const AVAILABLE_MODELS = [
17
- 'claude-sonnet-4-20250514',
18
- 'claude-3-5-sonnet-20241022',
19
- 'claude-3-5-haiku-20241022',
20
- 'claude-3-opus-20240229',
21
- ];
22
-
23
- // ─── Provider Implementation ────────────────────────────────
24
-
25
- export class AnthropicProvider implements LLMProvider {
26
- readonly id = 'anthropic';
27
- readonly name = 'Anthropic Claude';
28
- readonly requiresApiKey = true;
29
- readonly defaultModel = DEFAULT_MODEL;
30
-
31
- private apiKey: string | null = null;
32
-
33
- setApiKey(key: string): void {
34
- this.apiKey = key;
35
- }
36
-
37
- async listModels(): Promise<string[]> {
38
- return AVAILABLE_MODELS;
39
- }
40
-
41
- async checkStatus(): Promise<ProviderStatus> {
42
- if (!this.apiKey) return 'unconfigured';
43
-
44
- try {
45
- // Quick health check — list models endpoint
46
- const res = await fetch(`${ANTHROPIC_API_BASE}/messages`, {
47
- method: 'POST',
48
- headers: {
49
- 'x-api-key': this.apiKey,
50
- 'anthropic-version': API_VERSION,
51
- 'content-type': 'application/json',
52
- },
53
- body: JSON.stringify({
54
- model: this.defaultModel,
55
- max_tokens: 1,
56
- messages: [{ role: 'user', content: 'ping' }],
57
- }),
58
- signal: AbortSignal.timeout(5000),
59
- });
60
- return res.ok || res.status === 429 ? 'available' : 'error';
61
- } catch {
62
- return 'unavailable';
63
- }
64
- }
65
-
66
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResult> {
67
- if (!this.apiKey) {
68
- throw new Error('Anthropic API key not configured. Set it in Settings → Providers.');
69
- }
70
-
71
- const model = options?.model || this.defaultModel;
72
- const start = Date.now();
73
-
74
- // Separate system from conversation
75
- const systemMsg = messages.find(m => m.role === 'system');
76
- const conversationMsgs = messages
77
- .filter(m => m.role !== 'system')
78
- .map(m => ({
79
- role: m.role as 'user' | 'assistant',
80
- content: m.content,
81
- }));
82
-
83
- // Ensure alternating user/assistant messages (Anthropic requirement)
84
- const sanitized = this._sanitizeMessages(conversationMsgs);
85
-
86
- const body: Record<string, unknown> = {
87
- model,
88
- max_tokens: options?.maxTokens ?? 2048,
89
- messages: sanitized,
90
- };
91
-
92
- if (options?.temperature !== undefined) {
93
- body.temperature = options.temperature;
94
- }
95
-
96
- if (systemMsg) {
97
- body.system = systemMsg.content;
98
- }
99
-
100
- const res = await fetch(`${ANTHROPIC_API_BASE}/messages`, {
101
- method: 'POST',
102
- headers: {
103
- 'x-api-key': this.apiKey,
104
- 'anthropic-version': API_VERSION,
105
- 'content-type': 'application/json',
106
- },
107
- body: JSON.stringify(body),
108
- signal: options?.signal ?? AbortSignal.timeout(60000),
109
- });
110
-
111
- if (!res.ok) {
112
- const errorBody = await res.text();
113
- throw new Error(`Anthropic API error ${res.status}: ${errorBody}`);
114
- }
115
-
116
- const data = await res.json();
117
- const latencyMs = Date.now() - start;
118
-
119
- // Extract text from content blocks
120
- const text = (data.content || [])
121
- .filter((block: { type: string }) => block.type === 'text')
122
- .map((block: { text: string }) => block.text)
123
- .join('') || '(sin respuesta)';
124
-
125
- const tokensUsed =
126
- (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0);
127
-
128
- return { text, model, latencyMs, tokensUsed };
129
- }
130
-
131
- /** Stream chat completion — yields text chunks */
132
- async *chatStream(
133
- messages: ChatMessage[],
134
- options?: ChatOptions,
135
- ): AsyncGenerator<string, void, unknown> {
136
- if (!this.apiKey) {
137
- throw new Error('Anthropic API key not configured.');
138
- }
139
-
140
- const model = options?.model || this.defaultModel;
141
- const systemMsg = messages.find(m => m.role === 'system');
142
- const conversationMsgs = messages
143
- .filter(m => m.role !== 'system')
144
- .map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));
145
-
146
- const sanitized = this._sanitizeMessages(conversationMsgs);
147
-
148
- const body: Record<string, unknown> = {
149
- model,
150
- max_tokens: options?.maxTokens ?? 2048,
151
- messages: sanitized,
152
- stream: true,
153
- };
154
-
155
- if (options?.temperature !== undefined) body.temperature = options.temperature;
156
- if (systemMsg) body.system = systemMsg.content;
157
-
158
- const res = await fetch(`${ANTHROPIC_API_BASE}/messages`, {
159
- method: 'POST',
160
- headers: {
161
- 'x-api-key': this.apiKey,
162
- 'anthropic-version': API_VERSION,
163
- 'content-type': 'application/json',
164
- },
165
- body: JSON.stringify(body),
166
- signal: options?.signal ?? AbortSignal.timeout(120000),
167
- });
168
-
169
- if (!res.ok) {
170
- const errorBody = await res.text();
171
- throw new Error(`Anthropic stream error ${res.status}: ${errorBody}`);
172
- }
173
-
174
- const reader = res.body?.getReader();
175
- if (!reader) return;
176
-
177
- const decoder = new TextDecoder();
178
- let buffer = '';
179
-
180
- while (true) {
181
- const { done, value } = await reader.read();
182
- if (done) break;
183
-
184
- buffer += decoder.decode(value, { stream: true });
185
- const lines = buffer.split('\n');
186
- buffer = lines.pop() || '';
187
-
188
- for (const line of lines) {
189
- if (!line.startsWith('data: ')) continue;
190
- const data = line.slice(6).trim();
191
- if (data === '[DONE]') return;
192
-
193
- try {
194
- const event = JSON.parse(data);
195
- if (event.type === 'content_block_delta' && event.delta?.text) {
196
- yield event.delta.text;
197
- }
198
- } catch { /* skip malformed lines */ }
199
- }
200
- }
201
- }
202
-
203
- /** Ensure alternating user/assistant (Anthropic requires this) */
204
- private _sanitizeMessages(
205
- msgs: Array<{ role: string; content: string }>,
206
- ): Array<{ role: string; content: string }> {
207
- if (msgs.length === 0) return [{ role: 'user', content: '...' }];
208
-
209
- const result: Array<{ role: string; content: string }> = [];
210
- let lastRole = '';
211
-
212
- for (const msg of msgs) {
213
- if (msg.role === lastRole) {
214
- // Merge consecutive same-role messages
215
- result[result.length - 1].content += '\n' + msg.content;
216
- } else {
217
- result.push({ ...msg });
218
- lastRole = msg.role;
219
- }
220
- }
221
-
222
- // Must start with 'user'
223
- if (result[0]?.role !== 'user') {
224
- result.unshift({ role: 'user', content: '...' });
225
- }
226
-
227
- return result;
228
- }
229
- }
@@ -1,121 +0,0 @@
1
- /**
2
- * GeminiProvider — Google Gemini LLM provider
3
- *
4
- * Uses the Gemini REST API directly (no SDK dependency).
5
- * Supports gemini-flash-lite-latest, gemini-1.5-pro, etc.
6
- */
7
-
8
- import type { LLMProvider, ChatMessage, ChatOptions, ChatResult, ProviderStatus } from './LLMProvider';
9
-
10
- // ─── Config ─────────────────────────────────────────────────
11
-
12
- const GEMINI_API_BASE = 'https://generativelanguage.googleapis.com/v1beta';
13
- const DEFAULT_MODEL = 'gemini-flash-lite-latest';
14
-
15
- const AVAILABLE_MODELS = [
16
- 'gemini-flash-lite-latest',
17
- 'gemini-flash-lite-latest-lite',
18
- 'gemini-1.5-pro',
19
- 'gemini-flash-latest',
20
- ];
21
-
22
- // ─── Provider Implementation ────────────────────────────────
23
-
24
- export class GeminiProvider implements LLMProvider {
25
- readonly id = 'gemini';
26
- readonly name = 'Google Gemini';
27
- readonly requiresApiKey = true;
28
- readonly defaultModel = DEFAULT_MODEL;
29
-
30
- private apiKey: string | null = null;
31
-
32
- setApiKey(key: string): void {
33
- this.apiKey = key;
34
- }
35
-
36
- async listModels(): Promise<string[]> {
37
- return AVAILABLE_MODELS;
38
- }
39
-
40
- async checkStatus(): Promise<ProviderStatus> {
41
- if (!this.apiKey) return 'unconfigured';
42
-
43
- try {
44
- // Quick validation — list models endpoint
45
- const res = await fetch(
46
- `${GEMINI_API_BASE}/models?key=${this.apiKey}`,
47
- { signal: AbortSignal.timeout(5000) }
48
- );
49
- return res.ok ? 'available' : 'error';
50
- } catch {
51
- return 'unavailable';
52
- }
53
- }
54
-
55
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResult> {
56
- if (!this.apiKey) {
57
- throw new Error('Gemini API key not configured. Set it in Settings → Providers.');
58
- }
59
-
60
- const model = options?.model || this.defaultModel;
61
- const start = Date.now();
62
-
63
- // Separate system instruction from conversation
64
- const systemMsg = messages.find(m => m.role === 'system');
65
- const conversationMsgs = messages.filter(m => m.role !== 'system');
66
-
67
- // Convert to Gemini format
68
- const contents = conversationMsgs.map(m => ({
69
- role: m.role === 'assistant' ? 'model' : 'user',
70
- parts: [{ text: m.content }],
71
- }));
72
-
73
- const body: Record<string, unknown> = {
74
- contents,
75
- generationConfig: {
76
- temperature: options?.temperature ?? 0.7,
77
- maxOutputTokens: options?.maxTokens ?? 2048,
78
- },
79
- };
80
-
81
- if (systemMsg) {
82
- body.systemInstruction = {
83
- parts: [{ text: systemMsg.content }],
84
- };
85
- }
86
-
87
- const url = `${GEMINI_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`;
88
-
89
- const res = await fetch(url, {
90
- method: 'POST',
91
- headers: { 'Content-Type': 'application/json' },
92
- body: JSON.stringify(body),
93
- signal: options?.signal ?? AbortSignal.timeout(60000),
94
- });
95
-
96
- if (!res.ok) {
97
- const errorBody = await res.text();
98
- throw new Error(`Gemini API error ${res.status}: ${errorBody}`);
99
- }
100
-
101
- const data = await res.json();
102
- const latencyMs = Date.now() - start;
103
-
104
- // Extract text from response
105
- const candidate = data.candidates?.[0];
106
- const text = candidate?.content?.parts
107
- ?.map((p: { text?: string }) => p.text || '')
108
- .join('') || '(sin respuesta)';
109
-
110
- const tokensUsed =
111
- (data.usageMetadata?.promptTokenCount || 0) +
112
- (data.usageMetadata?.candidatesTokenCount || 0);
113
-
114
- return {
115
- text,
116
- model,
117
- latencyMs,
118
- tokensUsed,
119
- };
120
- }
121
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * LLMProvider — Universal interface for LLM backends
3
- *
4
- * All providers (Ollama, Gemini, OpenAI, etc.) implement this
5
- * interface so the InferenceRouter can swap them transparently.
6
- */
7
-
8
- // ─── Types ──────────────────────────────────────────────────
9
-
10
- export interface ChatMessage {
11
- role: 'system' | 'user' | 'assistant';
12
- content: string;
13
- }
14
-
15
- export interface ChatOptions {
16
- model?: string;
17
- temperature?: number;
18
- maxTokens?: number;
19
- /** AbortSignal for cancellation */
20
- signal?: AbortSignal;
21
- }
22
-
23
- export interface ChatResult {
24
- text: string;
25
- model: string;
26
- latencyMs: number;
27
- tokensUsed?: number;
28
- }
29
-
30
- export type ProviderStatus = 'available' | 'unavailable' | 'error' | 'unconfigured';
31
-
32
- // ─── Provider Interface ─────────────────────────────────────
33
-
34
- export interface LLMProvider {
35
- /** Unique identifier */
36
- readonly id: string;
37
- /** Display name */
38
- readonly name: string;
39
- /** Whether this provider needs an API key */
40
- readonly requiresApiKey: boolean;
41
- /** Default model for this provider */
42
- readonly defaultModel: string;
43
- /** Available models */
44
- listModels(): Promise<string[]>;
45
- /** Health check */
46
- checkStatus(): Promise<ProviderStatus>;
47
- /** Send a chat completion request */
48
- chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResult>;
49
- /** Set API key if required */
50
- setApiKey?(key: string): void;
51
- }
52
-
53
- // ─── Provider Registry ──────────────────────────────────────
54
-
55
- const providers = new Map<string, LLMProvider>();
56
-
57
- export function registerProvider(provider: LLMProvider): void {
58
- providers.set(provider.id, provider);
59
- console.log(`🔌 [LLM] Provider registered: ${provider.name} (${provider.id})`);
60
- }
61
-
62
- export function getProvider(id: string): LLMProvider | undefined {
63
- return providers.get(id);
64
- }
65
-
66
- export function getAllProviders(): LLMProvider[] {
67
- return Array.from(providers.values());
68
- }
69
-
70
- export function getAvailableProviderIds(): string[] {
71
- return Array.from(providers.keys());
72
- }