orquesta-cli 0.1.24 → 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 +24 -3
- package/dist/core/pricing.d.ts +8 -0
- package/dist/core/pricing.js +41 -0
- package/dist/core/routing-state.d.ts +9 -0
- package/dist/core/routing-state.js +15 -0
- package/dist/core/slash-command-handler.js +16 -1
- 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,12 +5,31 @@ 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 } from '../routing-state.js';
|
|
8
|
+
import { getForcedTier, getBatutaSessionId, setLastBatutaRoute } from '../routing-state.js';
|
|
9
9
|
function buildPerRequestHeaders() {
|
|
10
|
+
const headers = {
|
|
11
|
+
'X-Batuta-Session-ID': getBatutaSessionId(),
|
|
12
|
+
};
|
|
10
13
|
const tier = getForcedTier();
|
|
11
|
-
if (
|
|
14
|
+
if (tier)
|
|
15
|
+
headers['X-Batuta-Force-Tier'] = tier;
|
|
16
|
+
return headers;
|
|
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
|
+
}
|
|
12
25
|
return undefined;
|
|
13
|
-
|
|
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
|
+
}
|
|
14
33
|
}
|
|
15
34
|
export class LLMClient {
|
|
16
35
|
axiosInstance;
|
|
@@ -122,6 +141,7 @@ export class LLMClient {
|
|
|
122
141
|
});
|
|
123
142
|
this.currentAbortController = null;
|
|
124
143
|
const elapsed = logger.endTimer('llm-api-call');
|
|
144
|
+
captureBatutaHeaders(response.headers);
|
|
125
145
|
logger.flow('API response received');
|
|
126
146
|
if (!response.data.choices || !Array.isArray(response.data.choices)) {
|
|
127
147
|
logger.error('Invalid response structure - missing choices array', response.data);
|
|
@@ -280,6 +300,7 @@ export class LLMClient {
|
|
|
280
300
|
signal: this.currentAbortController.signal,
|
|
281
301
|
headers: buildPerRequestHeaders(),
|
|
282
302
|
});
|
|
303
|
+
captureBatutaHeaders(response.headers);
|
|
283
304
|
logger.debug('Streaming response started', { status: response.status });
|
|
284
305
|
const stream = response.data;
|
|
285
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
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export type ForcedTier = 'fast' | 'balanced' | 'premium' | null;
|
|
2
2
|
export declare function getForcedTier(): ForcedTier;
|
|
3
3
|
export declare function setForcedTier(tier: ForcedTier): void;
|
|
4
|
+
export declare function getBatutaSessionId(): string;
|
|
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;
|
|
4
13
|
//# sourceMappingURL=routing-state.d.ts.map
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
1
2
|
let forcedTier = null;
|
|
3
|
+
let sessionId = randomUUID();
|
|
2
4
|
export function getForcedTier() {
|
|
3
5
|
return forcedTier;
|
|
4
6
|
}
|
|
5
7
|
export function setForcedTier(tier) {
|
|
6
8
|
forcedTier = tier;
|
|
7
9
|
}
|
|
10
|
+
export function getBatutaSessionId() {
|
|
11
|
+
return sessionId;
|
|
12
|
+
}
|
|
13
|
+
export function resetBatutaSession() {
|
|
14
|
+
sessionId = randomUUID();
|
|
15
|
+
}
|
|
16
|
+
let lastBatutaRoute = null;
|
|
17
|
+
export function getLastBatutaRoute() {
|
|
18
|
+
return lastBatutaRoute;
|
|
19
|
+
}
|
|
20
|
+
export function setLastBatutaRoute(route) {
|
|
21
|
+
lastBatutaRoute = route;
|
|
22
|
+
}
|
|
8
23
|
//# sourceMappingURL=routing-state.js.map
|
|
@@ -3,7 +3,7 @@ import { usageTracker } from './usage-tracker.js';
|
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
4
|
import { fullSync } from '../orquesta/config-sync.js';
|
|
5
5
|
import { configManager } from './config/config-manager.js';
|
|
6
|
-
import { getForcedTier, setForcedTier } from './routing-state.js';
|
|
6
|
+
import { getForcedTier, setForcedTier, resetBatutaSession } from './routing-state.js';
|
|
7
7
|
export async function executeSlashCommand(command, context) {
|
|
8
8
|
const trimmedCommand = command.trim();
|
|
9
9
|
logger.enter('executeSlashCommand', { command: trimmedCommand });
|
|
@@ -17,6 +17,7 @@ export async function executeSlashCommand(command, context) {
|
|
|
17
17
|
logger.flow('Clear command - resetting messages and todos');
|
|
18
18
|
context.setMessages([]);
|
|
19
19
|
context.setTodos([]);
|
|
20
|
+
resetBatutaSession();
|
|
20
21
|
logger.exit('executeSlashCommand', { handled: true, command: 'clear' });
|
|
21
22
|
return {
|
|
22
23
|
handled: true,
|
|
@@ -168,6 +169,19 @@ export async function executeSlashCommand(command, context) {
|
|
|
168
169
|
updatedContext: { messages: updatedMessages },
|
|
169
170
|
};
|
|
170
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
|
+
}
|
|
171
185
|
if (trimmedCommand === '/usage') {
|
|
172
186
|
const usageMessage = usageTracker.formatUsageDisplay();
|
|
173
187
|
const updatedMessages = [
|
|
@@ -313,6 +327,7 @@ Available commands:
|
|
|
313
327
|
/tool - Enable/disable optional tools (Browser, Background)
|
|
314
328
|
/load - Load a saved session
|
|
315
329
|
/usage - Show token usage statistics
|
|
330
|
+
/cost - Estimated USD spend this process (by model)
|
|
316
331
|
/route - Pin Batuta Auto tier (fast/balanced/premium/auto)
|
|
317
332
|
/sync - Bidirectional sync with Orquesta dashboard (pull & push LLM configs)
|
|
318
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
|