orquesta-cli 0.1.21 → 0.1.22
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/core/llm/llm-client.js +9 -0
- package/dist/core/routing-state.d.ts +4 -0
- package/dist/core/routing-state.js +8 -0
- package/dist/core/slash-command-handler.js +38 -0
- package/dist/ui/TodoPanel.d.ts +4 -0
- package/dist/ui/TodoPanel.js +25 -3
- package/dist/ui/components/PlanExecuteApp.js +4 -2
- package/dist/ui/components/StatusBar.d.ts +5 -0
- package/dist/ui/components/StatusBar.js +11 -1
- package/dist/ui/hooks/useBatutaUsage.d.ts +8 -0
- package/dist/ui/hooks/useBatutaUsage.js +46 -0
- package/package.json +1 -1
|
@@ -5,6 +5,13 @@ 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';
|
|
9
|
+
function buildPerRequestHeaders() {
|
|
10
|
+
const tier = getForcedTier();
|
|
11
|
+
if (!tier)
|
|
12
|
+
return undefined;
|
|
13
|
+
return { 'X-Batuta-Force-Tier': tier };
|
|
14
|
+
}
|
|
8
15
|
export class LLMClient {
|
|
9
16
|
axiosInstance;
|
|
10
17
|
baseUrl;
|
|
@@ -111,6 +118,7 @@ export class LLMClient {
|
|
|
111
118
|
this.currentAbortController = new AbortController();
|
|
112
119
|
const response = await this.axiosInstance.post(url, requestBody, {
|
|
113
120
|
signal: this.currentAbortController.signal,
|
|
121
|
+
headers: buildPerRequestHeaders(),
|
|
114
122
|
});
|
|
115
123
|
this.currentAbortController = null;
|
|
116
124
|
const elapsed = logger.endTimer('llm-api-call');
|
|
@@ -270,6 +278,7 @@ export class LLMClient {
|
|
|
270
278
|
const response = await this.axiosInstance.post(url, requestBody, {
|
|
271
279
|
responseType: 'stream',
|
|
272
280
|
signal: this.currentAbortController.signal,
|
|
281
|
+
headers: buildPerRequestHeaders(),
|
|
273
282
|
});
|
|
274
283
|
logger.debug('Streaming response started', { status: response.status });
|
|
275
284
|
const stream = response.data;
|
|
@@ -3,6 +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
7
|
export async function executeSlashCommand(command, context) {
|
|
7
8
|
const trimmedCommand = command.trim();
|
|
8
9
|
logger.enter('executeSlashCommand', { command: trimmedCommand });
|
|
@@ -131,6 +132,42 @@ export async function executeSlashCommand(command, context) {
|
|
|
131
132
|
},
|
|
132
133
|
};
|
|
133
134
|
}
|
|
135
|
+
if (trimmedCommand === '/route' || trimmedCommand.startsWith('/route ')) {
|
|
136
|
+
const arg = trimmedCommand.slice('/route'.length).trim().toLowerCase();
|
|
137
|
+
const currentModel = configManager.getCurrentModel();
|
|
138
|
+
const currentModelId = currentModel?.id ?? null;
|
|
139
|
+
const onBatutaAuto = currentModelId === 'batuta-auto';
|
|
140
|
+
let body;
|
|
141
|
+
if (!arg) {
|
|
142
|
+
const pinned = getForcedTier();
|
|
143
|
+
body = pinned
|
|
144
|
+
? `Batuta Auto tier pinned to: ${pinned}.\n\nUse /route auto to let the classifier decide again.`
|
|
145
|
+
: `Batuta Auto: classifier-driven (default).\n\nUsage: /route [auto|fast|balanced|premium]\n fast → Haiku 4.5 (cheapest, simple Q&A)\n balanced → Sonnet 4.6 (code, day-to-day)\n premium → Opus 4.7 (reasoning, long context)\n auto → let the classifier decide per prompt`;
|
|
146
|
+
}
|
|
147
|
+
else if (arg === 'auto' || arg === 'reset' || arg === 'clear') {
|
|
148
|
+
setForcedTier(null);
|
|
149
|
+
body = `Batuta Auto: pin cleared. Classifier will choose tier per prompt.`;
|
|
150
|
+
}
|
|
151
|
+
else if (arg === 'fast' || arg === 'balanced' || arg === 'premium') {
|
|
152
|
+
setForcedTier(arg);
|
|
153
|
+
body = onBatutaAuto
|
|
154
|
+
? `Batuta Auto: pinned to ${arg}. Every prompt this session routes to ${arg === 'fast' ? 'Haiku 4.5' : arg === 'balanced' ? 'Sonnet 4.6' : 'Opus 4.7'}.`
|
|
155
|
+
: `Pin set to ${arg}, but the active model is "${currentModelId ?? 'none'}", not "batuta-auto" — the pin only takes effect when you switch to batuta-auto (use /model).`;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
body = `Unknown tier: "${arg}". Use one of: auto, fast, balanced, premium.`;
|
|
159
|
+
}
|
|
160
|
+
const updatedMessages = [
|
|
161
|
+
...context.messages,
|
|
162
|
+
{ role: 'assistant', content: body },
|
|
163
|
+
];
|
|
164
|
+
context.setMessages(updatedMessages);
|
|
165
|
+
return {
|
|
166
|
+
handled: true,
|
|
167
|
+
shouldContinue: false,
|
|
168
|
+
updatedContext: { messages: updatedMessages },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
134
171
|
if (trimmedCommand === '/usage') {
|
|
135
172
|
const usageMessage = usageTracker.formatUsageDisplay();
|
|
136
173
|
const updatedMessages = [
|
|
@@ -276,6 +313,7 @@ Available commands:
|
|
|
276
313
|
/tool - Enable/disable optional tools (Browser, Background)
|
|
277
314
|
/load - Load a saved session
|
|
278
315
|
/usage - Show token usage statistics
|
|
316
|
+
/route - Pin Batuta Auto tier (fast/balanced/premium/auto)
|
|
279
317
|
/sync - Bidirectional sync with Orquesta dashboard (pull & push LLM configs)
|
|
280
318
|
|
|
281
319
|
CLI Options (restart required):
|
package/dist/ui/TodoPanel.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare const TodoPanel: React.FC<TodoPanelProps>;
|
|
|
9
9
|
export declare const TodoStatusBar: React.FC<{
|
|
10
10
|
todos: TodoItem[];
|
|
11
11
|
projectName?: string;
|
|
12
|
+
batutaUsage?: {
|
|
13
|
+
tokensUsed: number;
|
|
14
|
+
tokenLimit: number;
|
|
15
|
+
} | null;
|
|
12
16
|
}>;
|
|
13
17
|
export default TodoPanel;
|
|
14
18
|
//# sourceMappingURL=TodoPanel.d.ts.map
|
package/dist/ui/TodoPanel.js
CHANGED
|
@@ -100,12 +100,31 @@ export const TodoPanel = React.memo(({ todos, currentTodoId, isProcessing = fals
|
|
|
100
100
|
}
|
|
101
101
|
return true;
|
|
102
102
|
});
|
|
103
|
-
export const TodoStatusBar = React.memo(({ todos, projectName }) => {
|
|
103
|
+
export const TodoStatusBar = React.memo(({ todos, projectName, batutaUsage }) => {
|
|
104
104
|
useEffect(() => {
|
|
105
105
|
logger.debug('TodoStatusBar rendered', { todoCount: todos.length });
|
|
106
106
|
}, [todos.length]);
|
|
107
|
+
const batutaIndicator = batutaUsage && batutaUsage.tokenLimit > 0 ? (() => {
|
|
108
|
+
const pct = (batutaUsage.tokensUsed / batutaUsage.tokenLimit) * 100;
|
|
109
|
+
const color = pct > 90 ? 'red' : pct > 70 ? 'yellow' : 'greenBright';
|
|
110
|
+
const fmt = (n) => n < 1000 ? `${n}` : n < 1_000_000 ? `${(n / 1000).toFixed(1)}k` : `${(n / 1_000_000).toFixed(2)}M`;
|
|
111
|
+
return (React.createElement(React.Fragment, null,
|
|
112
|
+
React.createElement(Text, { color: "gray" }, "Batuta "),
|
|
113
|
+
React.createElement(Text, { color: color },
|
|
114
|
+
fmt(batutaUsage.tokensUsed),
|
|
115
|
+
"/",
|
|
116
|
+
fmt(batutaUsage.tokenLimit))));
|
|
117
|
+
})() : null;
|
|
107
118
|
if (todos.length === 0) {
|
|
108
|
-
|
|
119
|
+
if (!batutaIndicator)
|
|
120
|
+
return null;
|
|
121
|
+
return (React.createElement(Box, null,
|
|
122
|
+
projectName && (React.createElement(React.Fragment, null,
|
|
123
|
+
React.createElement(Text, { color: "magenta" },
|
|
124
|
+
"\uD83D\uDCC1 ",
|
|
125
|
+
projectName),
|
|
126
|
+
React.createElement(Text, { color: "white" }, " \u2502 "))),
|
|
127
|
+
batutaIndicator));
|
|
109
128
|
}
|
|
110
129
|
const completedCount = todos.filter(t => t.status === 'completed').length;
|
|
111
130
|
const inProgressCount = todos.filter(t => t.status === 'in_progress').length;
|
|
@@ -134,7 +153,10 @@ export const TodoStatusBar = React.memo(({ todos, projectName }) => {
|
|
|
134
153
|
currentTodo.title.length > 30 ? '...' : ''))),
|
|
135
154
|
inProgressCount === 0 && completedCount === todos.length && (React.createElement(React.Fragment, null,
|
|
136
155
|
React.createElement(Text, { color: "white" }, " \u2502 "),
|
|
137
|
-
React.createElement(Text, { color: "greenBright" }, "Done")))
|
|
156
|
+
React.createElement(Text, { color: "greenBright" }, "Done"))),
|
|
157
|
+
batutaIndicator && (React.createElement(React.Fragment, null,
|
|
158
|
+
React.createElement(Text, { color: "white" }, " \u2502 "),
|
|
159
|
+
batutaIndicator))));
|
|
138
160
|
});
|
|
139
161
|
export default TodoPanel;
|
|
140
162
|
//# sourceMappingURL=TodoPanel.js.map
|
|
@@ -8,6 +8,7 @@ import { getShellConfig, isNativeWindows } from '../../utils/platform-utils.js';
|
|
|
8
8
|
import { CustomTextInput } from './CustomTextInput.js';
|
|
9
9
|
import { LLMClient, createLLMClient } from '../../core/llm/llm-client.js';
|
|
10
10
|
import { TodoPanel, TodoStatusBar } from '../TodoPanel.js';
|
|
11
|
+
import { useBatutaUsage } from '../hooks/useBatutaUsage.js';
|
|
11
12
|
import { sessionManager } from '../../core/session/session-manager.js';
|
|
12
13
|
import { initializeDocsDirectory, setDocsSearchProgressCallback } from '../../agents/docs-search/index.js';
|
|
13
14
|
import { DocsSearchProgress } from './DocsSearchProgress.js';
|
|
@@ -96,6 +97,7 @@ export const PlanExecuteApp = ({ llmClient: initialLlmClient, modelInfo }) => {
|
|
|
96
97
|
const [subActivities, setSubActivities] = useState([]);
|
|
97
98
|
const [sessionTokens, setSessionTokens] = useState(0);
|
|
98
99
|
const [sessionElapsed, setSessionElapsed] = useState(0);
|
|
100
|
+
const batutaUsage = useBatutaUsage();
|
|
99
101
|
const [currentToolName, setCurrentToolName] = useState(null);
|
|
100
102
|
const [showSessionBrowser, setShowSessionBrowser] = useState(false);
|
|
101
103
|
const [showSettings, setShowSettings] = useState(false);
|
|
@@ -1339,9 +1341,9 @@ export const PlanExecuteApp = ({ llmClient: initialLlmClient, modelInfo }) => {
|
|
|
1339
1341
|
React.createElement(Text, { color: "cyan" }, currentModelInfo.model),
|
|
1340
1342
|
React.createElement(Text, { color: "gray" }, " \u2502 "),
|
|
1341
1343
|
React.createElement(Text, { color: "gray" }, shortenPath(process.cwd())),
|
|
1342
|
-
planExecutionState.todos.length > 0 && (React.createElement(React.Fragment, null,
|
|
1344
|
+
(planExecutionState.todos.length > 0 || batutaUsage) && (React.createElement(React.Fragment, null,
|
|
1343
1345
|
React.createElement(Text, { color: "gray" }, " \u2502 "),
|
|
1344
|
-
React.createElement(TodoStatusBar, { todos: planExecutionState.todos, projectName: configManager.getOrquestaConfig()?.projectName })))),
|
|
1346
|
+
React.createElement(TodoStatusBar, { todos: planExecutionState.todos, projectName: configManager.getOrquestaConfig()?.projectName, batutaUsage: batutaUsage })))),
|
|
1345
1347
|
React.createElement(Text, { color: "gray", dimColor: true }, "Tab: mode \u2502 /help")))),
|
|
1346
1348
|
showLogFiles && (React.createElement(Box, { marginTop: 0 },
|
|
1347
1349
|
React.createElement(LogBrowser, { onClose: () => setShowLogFiles(false) })))));
|
|
@@ -19,6 +19,11 @@ export interface StatusBarProps {
|
|
|
19
19
|
healthStatus?: 'healthy' | 'unhealthy' | 'checking' | 'unknown';
|
|
20
20
|
currentActivity?: string;
|
|
21
21
|
sessionElapsedSeconds?: number;
|
|
22
|
+
batutaUsage?: {
|
|
23
|
+
tokensUsed: number;
|
|
24
|
+
tokenLimit: number;
|
|
25
|
+
tier: 'llm' | 'llm_team' | null;
|
|
26
|
+
} | null;
|
|
22
27
|
}
|
|
23
28
|
export declare const StatusBar: React.FC<StatusBarProps>;
|
|
24
29
|
export default StatusBar;
|
|
@@ -62,7 +62,7 @@ function formatElapsedTime(seconds) {
|
|
|
62
62
|
const secs = seconds % 60;
|
|
63
63
|
return `${mins}m ${secs}s`;
|
|
64
64
|
}
|
|
65
|
-
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, }) => {
|
|
65
|
+
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, }) => {
|
|
66
66
|
void _endpoint;
|
|
67
67
|
void _message;
|
|
68
68
|
useEffect(() => {
|
|
@@ -149,6 +149,16 @@ export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizatio
|
|
|
149
149
|
React.createElement(Text, { color: "cyan" },
|
|
150
150
|
"\u26A1 ",
|
|
151
151
|
formatTokens(sessionTokens)))),
|
|
152
|
+
batutaUsage && batutaUsage.tokenLimit > 0 && (() => {
|
|
153
|
+
const pct = (batutaUsage.tokensUsed / batutaUsage.tokenLimit) * 100;
|
|
154
|
+
const color = pct > 90 ? 'red' : pct > 70 ? 'yellow' : 'green';
|
|
155
|
+
return (React.createElement(Box, { marginRight: 2 },
|
|
156
|
+
React.createElement(Text, { color: "gray" }, "Batuta "),
|
|
157
|
+
React.createElement(Text, { color: color },
|
|
158
|
+
formatTokens(batutaUsage.tokensUsed),
|
|
159
|
+
"/",
|
|
160
|
+
formatTokens(batutaUsage.tokenLimit))));
|
|
161
|
+
})(),
|
|
152
162
|
contextUsage && contextUsage.current > 0 && (React.createElement(Box, { marginRight: 2 },
|
|
153
163
|
React.createElement(Text, { color: "gray" }, "CTX "),
|
|
154
164
|
React.createElement(ContextMiniBar, { current: contextUsage.current, max: contextUsage.max }))),
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { configManager } from '../../core/config/config-manager.js';
|
|
3
|
+
const ORQUESTA_API = process.env['ORQUESTA_API_URL'] || 'https://getorquesta.com';
|
|
4
|
+
const POLL_INTERVAL_MS = 60_000;
|
|
5
|
+
export function useBatutaUsage() {
|
|
6
|
+
const [usage, setUsage] = useState(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
let cancelled = false;
|
|
9
|
+
async function tick() {
|
|
10
|
+
const endpoint = configManager.getCurrentEndpoint();
|
|
11
|
+
const orquestaConfig = configManager.getOrquestaConfig();
|
|
12
|
+
const onBatuta = endpoint?.provider === 'batuta' || endpoint?.id === 'batuta-proxy';
|
|
13
|
+
if (!onBatuta || !orquestaConfig?.token) {
|
|
14
|
+
if (!cancelled)
|
|
15
|
+
setUsage(null);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${ORQUESTA_API}/api/billing`, {
|
|
20
|
+
headers: { Authorization: `Bearer ${orquestaConfig.token}` },
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok || cancelled)
|
|
23
|
+
return;
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
if (data.batutaLlm && !cancelled) {
|
|
26
|
+
setUsage({
|
|
27
|
+
tokensUsed: data.batutaLlm.tokensUsed ?? 0,
|
|
28
|
+
tokenLimit: data.batutaLlm.tokenLimit ?? 0,
|
|
29
|
+
tier: data.batutaLlm.tier ?? null,
|
|
30
|
+
enabled: data.batutaLlm.enabled ?? false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
tick();
|
|
38
|
+
const interval = setInterval(tick, POLL_INTERVAL_MS);
|
|
39
|
+
return () => {
|
|
40
|
+
cancelled = true;
|
|
41
|
+
clearInterval(interval);
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
return usage;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=useBatutaUsage.js.map
|