orquesta-cli 0.1.21 → 0.1.23
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/cli.js +9 -0
- 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/setup/browser-login.d.ts +5 -0
- package/dist/setup/browser-login.js +90 -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
package/dist/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ program
|
|
|
30
30
|
.option('--debug', 'Enable debug logging')
|
|
31
31
|
.option('--llm-log', 'Enable LLM logging')
|
|
32
32
|
.option('--eval', 'Evaluation mode: read JSON from stdin, output NDJSON events')
|
|
33
|
+
.option('--login', 'Sign in to Orquesta via browser (no token paste required)')
|
|
33
34
|
.option('--token <token>', 'Connect to Orquesta dashboard with CLI token')
|
|
34
35
|
.option('--project <projectId>', 'Select project ID when connecting with token')
|
|
35
36
|
.option('--switch-project [projectId]', 'Switch to a different project')
|
|
@@ -67,6 +68,14 @@ program
|
|
|
67
68
|
const success = await switchProject(projectId);
|
|
68
69
|
process.exit(success ? 0 : 1);
|
|
69
70
|
}
|
|
71
|
+
if (options.login) {
|
|
72
|
+
const { browserLogin } = await import('./setup/browser-login.js');
|
|
73
|
+
const result = await browserLogin();
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
console.error(chalk.red(result.error || 'Login failed'));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
70
79
|
if (options.token) {
|
|
71
80
|
const result = await connectWithToken(options.token, options.project);
|
|
72
81
|
if (!result.connected) {
|
|
@@ -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):
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { configManager } from '../core/config/config-manager.js';
|
|
5
|
+
import { syncOrquestaConfigs } from '../orquesta/config-sync.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
const ORQUESTA_API = process.env['ORQUESTA_API_URL'] || 'https://getorquesta.com';
|
|
8
|
+
const WS_URL = process.env['ORQUESTA_WS_URL']?.replace(/^wss?:\/\//, 'https://') || 'https://ws.orquesta.live';
|
|
9
|
+
const POLL_INTERVAL_MS = 2000;
|
|
10
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
|
+
function openBrowser(url) {
|
|
12
|
+
const opener = process.platform === 'darwin' ? 'open' :
|
|
13
|
+
process.platform === 'win32' ? 'start' :
|
|
14
|
+
'xdg-open';
|
|
15
|
+
try {
|
|
16
|
+
const child = spawn(opener, [url], { detached: true, stdio: 'ignore', shell: process.platform === 'win32' });
|
|
17
|
+
child.unref();
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
logger.debug('Failed to open browser', { err });
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function pollOnce(sessionId) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${WS_URL}/auth/result/${sessionId}`);
|
|
28
|
+
if (res.status !== 200)
|
|
29
|
+
return null;
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
if (!data.token || !data.organizationId)
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
token: data.token,
|
|
35
|
+
organizationId: data.organizationId,
|
|
36
|
+
organizationName: data.organizationName || 'Unknown',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function browserLogin() {
|
|
44
|
+
const sessionId = randomUUID();
|
|
45
|
+
const url = `${ORQUESTA_API}/cli/auth?session=${sessionId}`;
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(chalk.cyan('Logging in to Orquesta…'));
|
|
48
|
+
console.log();
|
|
49
|
+
const opened = openBrowser(url);
|
|
50
|
+
if (opened) {
|
|
51
|
+
console.log(chalk.dim('Opened your browser. If nothing happened, copy this URL:'));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(chalk.yellow('Could not open a browser automatically. Open this URL on any device:'));
|
|
55
|
+
}
|
|
56
|
+
console.log(' ' + chalk.cyan(url));
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(chalk.dim('Waiting for authorization…'));
|
|
59
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
60
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
61
|
+
while (Date.now() < deadline) {
|
|
62
|
+
const hit = await pollOnce(sessionId);
|
|
63
|
+
if (hit) {
|
|
64
|
+
await configManager.setOrquestaConfig({
|
|
65
|
+
token: hit.token,
|
|
66
|
+
organizationId: hit.organizationId,
|
|
67
|
+
organizationName: hit.organizationName,
|
|
68
|
+
projectId: undefined,
|
|
69
|
+
projectName: undefined,
|
|
70
|
+
autoSync: true,
|
|
71
|
+
connectedAt: new Date().toISOString(),
|
|
72
|
+
});
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(chalk.green(`✓ Logged in as ${hit.organizationName}`));
|
|
75
|
+
try {
|
|
76
|
+
const sync = await syncOrquestaConfigs();
|
|
77
|
+
if (sync.success) {
|
|
78
|
+
console.log(chalk.dim(` Synced ${sync.added + sync.updated} endpoint(s) from the dashboard.`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
logger.debug('Initial sync after login failed', { err });
|
|
83
|
+
}
|
|
84
|
+
return { success: true };
|
|
85
|
+
}
|
|
86
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
87
|
+
}
|
|
88
|
+
return { success: false, error: 'Timed out waiting for authorization (5 min).' };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=browser-login.js.map
|
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
|