orquesta-cli 0.1.25 → 0.1.26
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.
- package/dist/agents/planner/index.js +1 -1
- package/dist/core/llm/llm-client.js +19 -1
- package/dist/core/pricing.d.ts +8 -0
- package/dist/core/pricing.js +41 -0
- package/dist/core/routing-state.d.ts +7 -0
- package/dist/core/routing-state.js +7 -0
- package/dist/core/slash-command-handler.js +14 -0
- package/dist/core/usage-tracker.d.ts +1 -0
- package/dist/core/usage-tracker.js +42 -0
- package/dist/ui/components/StatusBar.js +20 -2
- package/dist/ui/hooks/useBatutaRoute.d.ts +3 -0
- package/dist/ui/hooks/useBatutaRoute.js +22 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { NetworkError, APIError, TimeoutError, ConnectionError, } from '../../er
|
|
|
5
5
|
import { LLMError, TokenLimitError, RateLimitError, ContextLengthError, } from '../../errors/llm.js';
|
|
6
6
|
import { logger, isLLMLogEnabled } from '../../utils/logger.js';
|
|
7
7
|
import { usageTracker } from '../usage-tracker.js';
|
|
8
|
-
import { getForcedTier, getBatutaSessionId } from '../routing-state.js';
|
|
8
|
+
import { getForcedTier, getBatutaSessionId, setLastBatutaRoute } from '../routing-state.js';
|
|
9
9
|
function buildPerRequestHeaders() {
|
|
10
10
|
const headers = {
|
|
11
11
|
'X-Batuta-Session-ID': getBatutaSessionId(),
|
|
@@ -15,6 +15,22 @@ function buildPerRequestHeaders() {
|
|
|
15
15
|
headers['X-Batuta-Force-Tier'] = tier;
|
|
16
16
|
return headers;
|
|
17
17
|
}
|
|
18
|
+
function captureBatutaHeaders(headers) {
|
|
19
|
+
const get = (k) => {
|
|
20
|
+
const lower = k.toLowerCase();
|
|
21
|
+
for (const key of Object.keys(headers)) {
|
|
22
|
+
if (key.toLowerCase() === lower)
|
|
23
|
+
return headers[key];
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
};
|
|
27
|
+
const routedFrom = get('X-Batuta-Routed-From');
|
|
28
|
+
const routedTo = get('X-Batuta-Routed-To');
|
|
29
|
+
const tier = get('X-Batuta-Tier');
|
|
30
|
+
if (routedFrom && routedTo && (tier === 'fast' || tier === 'balanced' || tier === 'premium')) {
|
|
31
|
+
setLastBatutaRoute({ tier, routedTo, routedFrom });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
18
34
|
export class LLMClient {
|
|
19
35
|
axiosInstance;
|
|
20
36
|
baseUrl;
|
|
@@ -125,6 +141,7 @@ export class LLMClient {
|
|
|
125
141
|
});
|
|
126
142
|
this.currentAbortController = null;
|
|
127
143
|
const elapsed = logger.endTimer('llm-api-call');
|
|
144
|
+
captureBatutaHeaders(response.headers);
|
|
128
145
|
logger.flow('API response received');
|
|
129
146
|
if (!response.data.choices || !Array.isArray(response.data.choices)) {
|
|
130
147
|
logger.error('Invalid response structure - missing choices array', response.data);
|
|
@@ -283,6 +300,7 @@ export class LLMClient {
|
|
|
283
300
|
signal: this.currentAbortController.signal,
|
|
284
301
|
headers: buildPerRequestHeaders(),
|
|
285
302
|
});
|
|
303
|
+
captureBatutaHeaders(response.headers);
|
|
286
304
|
logger.debug('Streaming response started', { status: response.status });
|
|
287
305
|
const stream = response.data;
|
|
288
306
|
let buffer = '';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ModelPricing {
|
|
2
|
+
input: number;
|
|
3
|
+
output: number;
|
|
4
|
+
}
|
|
5
|
+
export declare const MODEL_PRICING: Record<string, ModelPricing>;
|
|
6
|
+
export declare function estimateCost(modelId: string, inputTokens: number, outputTokens: number): number | null;
|
|
7
|
+
export declare function formatCostUsd(usd: number): string;
|
|
8
|
+
//# sourceMappingURL=pricing.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const MODEL_PRICING = {
|
|
2
|
+
'claude-haiku-4-5-20251001': { input: 1, output: 5 },
|
|
3
|
+
'claude-sonnet-4-6': { input: 3, output: 15 },
|
|
4
|
+
'claude-sonnet-4-5-20250514': { input: 3, output: 15 },
|
|
5
|
+
'claude-opus-4-6': { input: 15, output: 75 },
|
|
6
|
+
'claude-opus-4-7': { input: 15, output: 75 },
|
|
7
|
+
'gpt-4o': { input: 2.50, output: 10 },
|
|
8
|
+
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
9
|
+
'gpt-4-turbo': { input: 10, output: 30 },
|
|
10
|
+
'o1': { input: 15, output: 60 },
|
|
11
|
+
'o1-mini': { input: 3, output: 12 },
|
|
12
|
+
'o3-mini': { input: 1.10, output: 4.40 },
|
|
13
|
+
'deepseek-chat': { input: 0.27, output: 1.10 },
|
|
14
|
+
'deepseek-reasoner': { input: 0.55, output: 2.19 },
|
|
15
|
+
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
16
|
+
'gemini-2.0-pro': { input: 1.25, output: 5 },
|
|
17
|
+
'gemini-1.5-pro': { input: 1.25, output: 5 },
|
|
18
|
+
'gemini-1.5-flash': { input: 0.075, output: 0.30 },
|
|
19
|
+
'llama-3.3-70b-versatile': { input: 0.59, output: 0.79 },
|
|
20
|
+
'mistral-large-latest': { input: 2, output: 6 },
|
|
21
|
+
'codestral-latest': { input: 0.30, output: 0.90 },
|
|
22
|
+
'grok-4': { input: 5, output: 15 },
|
|
23
|
+
'grok-3': { input: 5, output: 15 },
|
|
24
|
+
'claude-cli': { input: 0, output: 0 },
|
|
25
|
+
};
|
|
26
|
+
export function estimateCost(modelId, inputTokens, outputTokens) {
|
|
27
|
+
const price = MODEL_PRICING[modelId];
|
|
28
|
+
if (!price)
|
|
29
|
+
return null;
|
|
30
|
+
return (inputTokens / 1_000_000) * price.input + (outputTokens / 1_000_000) * price.output;
|
|
31
|
+
}
|
|
32
|
+
export function formatCostUsd(usd) {
|
|
33
|
+
if (usd === 0)
|
|
34
|
+
return '$0.00';
|
|
35
|
+
if (usd < 0.01)
|
|
36
|
+
return `$${usd.toFixed(4)}`;
|
|
37
|
+
if (usd < 1)
|
|
38
|
+
return `$${usd.toFixed(3)}`;
|
|
39
|
+
return `$${usd.toFixed(2)}`;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=pricing.js.map
|
|
@@ -3,4 +3,11 @@ export declare function getForcedTier(): ForcedTier;
|
|
|
3
3
|
export declare function setForcedTier(tier: ForcedTier): void;
|
|
4
4
|
export declare function getBatutaSessionId(): string;
|
|
5
5
|
export declare function resetBatutaSession(): void;
|
|
6
|
+
export interface LastBatutaRoute {
|
|
7
|
+
tier: 'fast' | 'balanced' | 'premium';
|
|
8
|
+
routedTo: string;
|
|
9
|
+
routedFrom: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function getLastBatutaRoute(): LastBatutaRoute | null;
|
|
12
|
+
export declare function setLastBatutaRoute(route: LastBatutaRoute | null): void;
|
|
6
13
|
//# sourceMappingURL=routing-state.d.ts.map
|
|
@@ -13,4 +13,11 @@ export function getBatutaSessionId() {
|
|
|
13
13
|
export function resetBatutaSession() {
|
|
14
14
|
sessionId = randomUUID();
|
|
15
15
|
}
|
|
16
|
+
let lastBatutaRoute = null;
|
|
17
|
+
export function getLastBatutaRoute() {
|
|
18
|
+
return lastBatutaRoute;
|
|
19
|
+
}
|
|
20
|
+
export function setLastBatutaRoute(route) {
|
|
21
|
+
lastBatutaRoute = route;
|
|
22
|
+
}
|
|
16
23
|
//# sourceMappingURL=routing-state.js.map
|
|
@@ -169,6 +169,19 @@ export async function executeSlashCommand(command, context) {
|
|
|
169
169
|
updatedContext: { messages: updatedMessages },
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
|
+
if (trimmedCommand === '/cost') {
|
|
173
|
+
const costMessage = usageTracker.formatCostDisplay();
|
|
174
|
+
const updatedMessages = [
|
|
175
|
+
...context.messages,
|
|
176
|
+
{ role: 'assistant', content: costMessage },
|
|
177
|
+
];
|
|
178
|
+
context.setMessages(updatedMessages);
|
|
179
|
+
return {
|
|
180
|
+
handled: true,
|
|
181
|
+
shouldContinue: false,
|
|
182
|
+
updatedContext: { messages: updatedMessages },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
172
185
|
if (trimmedCommand === '/usage') {
|
|
173
186
|
const usageMessage = usageTracker.formatUsageDisplay();
|
|
174
187
|
const updatedMessages = [
|
|
@@ -314,6 +327,7 @@ Available commands:
|
|
|
314
327
|
/tool - Enable/disable optional tools (Browser, Background)
|
|
315
328
|
/load - Load a saved session
|
|
316
329
|
/usage - Show token usage statistics
|
|
330
|
+
/cost - Estimated USD spend this process (by model)
|
|
317
331
|
/route - Pin Batuta Auto tier (fast/balanced/premium/auto)
|
|
318
332
|
/sync - Bidirectional sync with Orquesta dashboard (pull & push LLM configs)
|
|
319
333
|
|
|
@@ -63,6 +63,7 @@ declare class UsageTrackerClass {
|
|
|
63
63
|
resetSession(): void;
|
|
64
64
|
formatSessionStatus(currentActivity?: string): string;
|
|
65
65
|
formatUsageDisplay(): string;
|
|
66
|
+
formatCostDisplay(): string;
|
|
66
67
|
clearData(): void;
|
|
67
68
|
}
|
|
68
69
|
export declare const usageTracker: UsageTrackerClass;
|
|
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
4
|
import { contextTracker } from './compact/context-tracker.js';
|
|
5
|
+
import { estimateCost, formatCostUsd } from './pricing.js';
|
|
5
6
|
const DATA_DIR = path.join(process.env['HOME'] || '.', '.local-cli');
|
|
6
7
|
const USAGE_FILE = path.join(DATA_DIR, 'usage.json');
|
|
7
8
|
class UsageTrackerClass {
|
|
@@ -232,6 +233,47 @@ class UsageTrackerClass {
|
|
|
232
233
|
logger.exit('UsageTracker.formatUsageDisplay');
|
|
233
234
|
return lines.join('\n');
|
|
234
235
|
}
|
|
236
|
+
formatCostDisplay() {
|
|
237
|
+
const records = this.data.records;
|
|
238
|
+
if (records.length === 0) {
|
|
239
|
+
return '💸 Cost: no usage recorded yet this session.';
|
|
240
|
+
}
|
|
241
|
+
const byModel = new Map();
|
|
242
|
+
let totalCost = 0;
|
|
243
|
+
let hadUnknown = false;
|
|
244
|
+
for (const r of records) {
|
|
245
|
+
const cost = estimateCost(r.model, r.inputTokens, r.outputTokens);
|
|
246
|
+
const b = byModel.get(r.model) ?? { model: r.model, requests: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
|
247
|
+
b.requests++;
|
|
248
|
+
b.inputTokens += r.inputTokens;
|
|
249
|
+
b.outputTokens += r.outputTokens;
|
|
250
|
+
if (cost === null) {
|
|
251
|
+
b.cost = null;
|
|
252
|
+
hadUnknown = true;
|
|
253
|
+
}
|
|
254
|
+
else if (b.cost !== null) {
|
|
255
|
+
b.cost += cost;
|
|
256
|
+
totalCost += cost;
|
|
257
|
+
}
|
|
258
|
+
byModel.set(r.model, b);
|
|
259
|
+
}
|
|
260
|
+
const lines = [];
|
|
261
|
+
lines.push('💸 Estimated Cost (this process)');
|
|
262
|
+
lines.push('');
|
|
263
|
+
lines.push(` Total: ${formatCostUsd(totalCost)}${hadUnknown ? ' (some models unknown — excluded)' : ''}`);
|
|
264
|
+
lines.push(` Requests: ${records.length}`);
|
|
265
|
+
lines.push('');
|
|
266
|
+
lines.push(' By model:');
|
|
267
|
+
const sorted = Array.from(byModel.values()).sort((a, b) => (b.cost ?? 0) - (a.cost ?? 0));
|
|
268
|
+
for (const b of sorted) {
|
|
269
|
+
const cost = b.cost === null ? '?' : formatCostUsd(b.cost);
|
|
270
|
+
const subsidy = b.model === 'claude-cli' ? ' (subscription)' : '';
|
|
271
|
+
lines.push(` ${b.model.padEnd(32)} ${b.requests}× in=${b.inputTokens.toLocaleString()} out=${b.outputTokens.toLocaleString()} ${cost}${subsidy}`);
|
|
272
|
+
}
|
|
273
|
+
lines.push('');
|
|
274
|
+
lines.push('Note: estimates use public list pricing. Batuta tier subscriptions absorb claude-cli routed costs (shown as $0.00).');
|
|
275
|
+
return lines.join('\n');
|
|
276
|
+
}
|
|
235
277
|
clearData() {
|
|
236
278
|
logger.flow('Clearing all usage data');
|
|
237
279
|
this.data = {
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
|
+
import { useBatutaRoute } from '../hooks/useBatutaRoute.js';
|
|
5
|
+
const MODEL_SHORT_LABEL = {
|
|
6
|
+
'claude-cli-haiku': 'Haiku 4.5',
|
|
7
|
+
'claude-cli-sonnet': 'Sonnet 4.6',
|
|
8
|
+
'claude-cli-opus': 'Opus 4.7',
|
|
9
|
+
'claude-haiku-4-5-20251001': 'Haiku 4.5',
|
|
10
|
+
'claude-sonnet-4-6': 'Sonnet 4.6',
|
|
11
|
+
'claude-opus-4-7': 'Opus 4.7',
|
|
12
|
+
'claude-opus-4-6': 'Opus 4.6',
|
|
13
|
+
};
|
|
4
14
|
function formatTokens(count) {
|
|
5
15
|
if (count < 1000)
|
|
6
16
|
return count.toString();
|
|
@@ -63,6 +73,11 @@ function formatElapsedTime(seconds) {
|
|
|
63
73
|
return `${mins}m ${secs}s`;
|
|
64
74
|
}
|
|
65
75
|
export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizationName: _organizationName, workingDirectory, status = 'idle', message: _message, messageCount = 0, sessionTokens = 0, contextUsage, contextRemainingPercent, todoCount, todoCompleted, healthStatus, currentActivity, sessionElapsedSeconds, batutaUsage, }) => {
|
|
76
|
+
const batutaRoute = useBatutaRoute();
|
|
77
|
+
const showBatutaSuffix = model === 'batuta-auto' && !!batutaRoute;
|
|
78
|
+
const batutaSuffix = showBatutaSuffix
|
|
79
|
+
? ` → ${MODEL_SHORT_LABEL[batutaRoute.routedTo] || batutaRoute.routedTo}`
|
|
80
|
+
: '';
|
|
66
81
|
void _endpoint;
|
|
67
82
|
void _message;
|
|
68
83
|
useEffect(() => {
|
|
@@ -109,7 +124,9 @@ export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizatio
|
|
|
109
124
|
formatTokens(sessionTokens),
|
|
110
125
|
" tokens")),
|
|
111
126
|
React.createElement(Text, { color: "gray" }, ")")),
|
|
112
|
-
React.createElement(Box, null, model && React.createElement(
|
|
127
|
+
React.createElement(Box, null, model && (React.createElement(React.Fragment, null,
|
|
128
|
+
React.createElement(Text, { color: "cyan" }, model.slice(0, 15)),
|
|
129
|
+
batutaSuffix && React.createElement(Text, { color: "magenta" }, batutaSuffix))))));
|
|
113
130
|
}
|
|
114
131
|
const getContextColor = (percent) => {
|
|
115
132
|
if (percent > 50)
|
|
@@ -131,7 +148,8 @@ export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizatio
|
|
|
131
148
|
getStatusIndicator(),
|
|
132
149
|
model && (React.createElement(React.Fragment, null,
|
|
133
150
|
React.createElement(Text, { color: "gray" }, " | "),
|
|
134
|
-
React.createElement(Text, { color: "cyan" }, model.slice(0, 15))
|
|
151
|
+
React.createElement(Text, { color: "cyan" }, model.slice(0, 15)),
|
|
152
|
+
batutaSuffix && React.createElement(Text, { color: "magenta" }, batutaSuffix))),
|
|
135
153
|
projectName && (React.createElement(React.Fragment, null,
|
|
136
154
|
React.createElement(Text, { color: "gray" }, " | "),
|
|
137
155
|
React.createElement(Text, { color: "magenta" },
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { getLastBatutaRoute } from '../../core/routing-state.js';
|
|
3
|
+
export function useBatutaRoute() {
|
|
4
|
+
const [route, setRoute] = useState(getLastBatutaRoute());
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const interval = setInterval(() => {
|
|
7
|
+
const next = getLastBatutaRoute();
|
|
8
|
+
setRoute(prev => {
|
|
9
|
+
if (prev === next)
|
|
10
|
+
return prev;
|
|
11
|
+
if (!prev || !next)
|
|
12
|
+
return next;
|
|
13
|
+
if (prev.tier === next.tier && prev.routedTo === next.routedTo)
|
|
14
|
+
return prev;
|
|
15
|
+
return next;
|
|
16
|
+
});
|
|
17
|
+
}, 500);
|
|
18
|
+
return () => clearInterval(interval);
|
|
19
|
+
}, []);
|
|
20
|
+
return route;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=useBatutaRoute.js.map
|