mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-30 00:05:33 +08:00
The old token UI mixed context pressure, cache counters, and dollar estimates. That made the percentage look precise even when provider data was incomplete or different. The composer and /cost view now show concrete counts instead of a pie percentage. Token payloads now share a smaller shape: used, inputTokens, outputTokens, and breakdown. Claude uses per-step usage where available and Codex reads total_token_usage events. Gemini reads its tokens object without inventing a context window. OpenCode reads opencode.db session totals and includes all token columns in used. The /cost backend no longer returns cache display fields or input/output dollar estimates. This avoids derived values that look reliable but are not comparable across providers. Verification: npm run typecheck; targeted eslint; OpenCode session provider test.
115 lines
2.9 KiB
JavaScript
115 lines
2.9 KiB
JavaScript
// Gemini Response Handler - JSON Stream processing
|
|
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
import { createNormalizedMessage } from './shared/utils.js';
|
|
|
|
function buildGeminiTokenBudget(tokens) {
|
|
if (!tokens || typeof tokens !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
const inputTokens = Number(tokens.input || 0);
|
|
const outputTokens = Number(tokens.output || 0);
|
|
const used = Number(tokens.total || inputTokens + outputTokens || 0);
|
|
if (used <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
used,
|
|
inputTokens,
|
|
outputTokens,
|
|
breakdown: {
|
|
input: inputTokens,
|
|
output: outputTokens,
|
|
},
|
|
};
|
|
}
|
|
|
|
class GeminiResponseHandler {
|
|
constructor(ws, options = {}) {
|
|
this.ws = ws;
|
|
this.buffer = '';
|
|
this.onContentFragment = options.onContentFragment || null;
|
|
this.onInit = options.onInit || null;
|
|
this.onToolUse = options.onToolUse || null;
|
|
this.onToolResult = options.onToolResult || null;
|
|
}
|
|
|
|
// Process incoming raw data from Gemini stream-json
|
|
processData(data) {
|
|
this.buffer += data;
|
|
|
|
// Split by newline
|
|
const lines = this.buffer.split('\n');
|
|
|
|
// Keep the last incomplete line in the buffer
|
|
this.buffer = lines.pop() || '';
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
|
|
try {
|
|
const event = JSON.parse(line);
|
|
this.handleEvent(event);
|
|
} catch (err) {
|
|
// Not a JSON line, probably debug output or CLI warnings
|
|
}
|
|
}
|
|
}
|
|
|
|
handleEvent(event) {
|
|
const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
|
|
|
|
if (event.type === 'init') {
|
|
if (this.onInit) {
|
|
this.onInit(event);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Invoke per-type callbacks for session tracking
|
|
if (event.type === 'message' && event.role === 'assistant') {
|
|
const content = event.content || '';
|
|
if (this.onContentFragment && content) {
|
|
this.onContentFragment(content);
|
|
}
|
|
} else if (event.type === 'tool_use' && this.onToolUse) {
|
|
this.onToolUse(event);
|
|
} else if (event.type === 'tool_result' && this.onToolResult) {
|
|
this.onToolResult(event);
|
|
}
|
|
|
|
// Normalize via adapter and send all resulting messages
|
|
const normalized = sessionsService.normalizeMessage('gemini', event, sid);
|
|
for (const msg of normalized) {
|
|
this.ws.send(msg);
|
|
}
|
|
|
|
const tokenBudget = buildGeminiTokenBudget(event.tokens);
|
|
if (tokenBudget) {
|
|
this.ws.send(createNormalizedMessage({
|
|
kind: 'status',
|
|
text: 'token_budget',
|
|
tokenBudget,
|
|
sessionId: sid,
|
|
provider: 'gemini',
|
|
}));
|
|
}
|
|
}
|
|
|
|
forceFlush() {
|
|
if (this.buffer.trim()) {
|
|
try {
|
|
const event = JSON.parse(this.buffer);
|
|
this.handleEvent(event);
|
|
} catch (err) { }
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.buffer = '';
|
|
}
|
|
}
|
|
|
|
export default GeminiResponseHandler;
|