mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: prevent session inspector context stalls
Session inspection was coupling quick status and usage rendering to live context control requests, so a slow get_context_usage path could leave the desktop inspector stuck on a loading state. The desktop panel now loads basic inspection data first, renders a transcript-based context estimate immediately, and only asks for live context details as a background refinement. The translation hook now returns a stable function per locale so effects that depend on translation do not reset and re-fetch after every render. Constraint: Third-party provider sessions may not expose reliable live context capabilities through the control request path. Rejected: Keep waiting on live get_context_usage for the context tab | it recreates the user-visible loading stall. Rejected: Hardcode provider-specific context windows | provider capabilities are not always known from the desktop session record. Confidence: high Scope-risk: moderate Directive: Do not make the inspector first render depend on live CLI control requests without a transcript or cached fallback. Tested: bun test src/server/__tests__/conversations.test.ts -t "structured session inspection|Sonnet 4.6 transcript usage" Tested: cd desktop && bun run test -- --run src/i18n/index.test.tsx Tested: cd desktop && bun run build Tested: git diff --check Tested: agent-browser verified /context renders transcript estimate without Loading context data Not-tested: Provider-specific true context-window discovery for every third-party vendor
This commit is contained in:
parent
15c0982159
commit
f57f163604
@ -135,6 +135,7 @@ export type SessionInspectionResponse = {
|
||||
}
|
||||
usage?: SessionUsageSnapshot
|
||||
context?: SessionContextSnapshot
|
||||
contextEstimate?: SessionContextSnapshot
|
||||
errors?: Record<string, string>
|
||||
}
|
||||
|
||||
@ -177,9 +178,12 @@ export const sessionsApi = {
|
||||
return api.get<{ commands: Array<{ name: string; description: string }> }>(`/api/sessions/${sessionId}/slash-commands`)
|
||||
},
|
||||
|
||||
getInspection(sessionId: string) {
|
||||
return api.get<SessionInspectionResponse>(`/api/sessions/${sessionId}/inspection`, {
|
||||
timeout: 25_000,
|
||||
getInspection(sessionId: string, options?: { includeContext?: boolean; timeout?: number }) {
|
||||
const query = options?.includeContext === undefined
|
||||
? ''
|
||||
: `?includeContext=${options.includeContext ? '1' : '0'}`
|
||||
return api.get<SessionInspectionResponse>(`/api/sessions/${sessionId}/inspection${query}`, {
|
||||
timeout: options?.timeout ?? (options?.includeContext ? 45_000 : 25_000),
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { skillsApi } from '../../api/skills'
|
||||
import { mcpApi } from '../../api/mcp'
|
||||
import {
|
||||
@ -494,8 +494,19 @@ function ContextOverview({ context, categories, t }: { context: SessionContextSn
|
||||
)
|
||||
}
|
||||
|
||||
function ContextTab({ context, error, t }: { context?: SessionContextSnapshot; error?: string; t: Translate }) {
|
||||
function ContextTab({
|
||||
context,
|
||||
error,
|
||||
loading,
|
||||
t,
|
||||
}: {
|
||||
context?: SessionContextSnapshot
|
||||
error?: string
|
||||
loading?: boolean
|
||||
t: Translate
|
||||
}) {
|
||||
if (error && !context) return <ErrorState message={error} />
|
||||
if (loading && !context) return <LoadingState label={t('slash.inspector.context.loading')} />
|
||||
if (!context) {
|
||||
return <EmptyState title={t('slash.inspector.context.emptyTitle')} body={t('slash.inspector.context.emptyBody')} />
|
||||
}
|
||||
@ -668,6 +679,9 @@ function SessionInspectorPanel({
|
||||
const [selectedTab, setSelectedTab] = useState<SessionInspectorTab>(() => sessionInspectorInitialTab(command))
|
||||
const [data, setData] = useState<SessionInspectionResponse | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [contextLoading, setContextLoading] = useState(false)
|
||||
const [contextError, setContextError] = useState<string | null>(null)
|
||||
const contextRequestSessionRef = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (command !== 'status' && command !== 'cost' && command !== 'context') return
|
||||
@ -682,7 +696,10 @@ function SessionInspectorPanel({
|
||||
let cancelled = false
|
||||
setData(null)
|
||||
setError(null)
|
||||
sessionsApi.getInspection(sessionId)
|
||||
setContextLoading(false)
|
||||
setContextError(null)
|
||||
contextRequestSessionRef.current = null
|
||||
sessionsApi.getInspection(sessionId, { includeContext: false })
|
||||
.then((response) => {
|
||||
if (!cancelled) setData(assertSessionInspectionResponse(response, t))
|
||||
})
|
||||
@ -692,7 +709,41 @@ function SessionInspectorPanel({
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [sessionId])
|
||||
}, [sessionId, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId || selectedTab !== 'context' || data === null || data.context) return
|
||||
if (contextRequestSessionRef.current === sessionId) return
|
||||
contextRequestSessionRef.current = sessionId
|
||||
let cancelled = false
|
||||
setContextLoading(true)
|
||||
setContextError(null)
|
||||
sessionsApi.getInspection(sessionId, { includeContext: true, timeout: 45_000 })
|
||||
.then((response) => {
|
||||
if (cancelled) return
|
||||
const inspected = assertSessionInspectionResponse(response, t)
|
||||
setData((current) => current
|
||||
? {
|
||||
...current,
|
||||
context: inspected.context,
|
||||
errors: {
|
||||
...(current.errors ?? {}),
|
||||
...(inspected.errors ?? {}),
|
||||
},
|
||||
}
|
||||
: inspected)
|
||||
setContextError(inspected.errors?.context ?? null)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) setContextError(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setContextLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [data, selectedTab, sessionId, t])
|
||||
|
||||
const tabs: Array<{ id: SessionInspectorTab; label: string }> = [
|
||||
{ id: 'status', label: t('slash.inspector.tab.status') },
|
||||
@ -709,7 +760,12 @@ function SessionInspectorPanel({
|
||||
) : selectedTab === 'usage' ? (
|
||||
<UsageTab usage={data.usage} context={data.context} error={data.errors?.usage} t={t} />
|
||||
) : selectedTab === 'context' ? (
|
||||
<ContextTab context={data.context} error={data.errors?.context} t={t} />
|
||||
<ContextTab
|
||||
context={data.context ?? data.contextEstimate}
|
||||
error={contextError ?? data.errors?.context}
|
||||
loading={contextLoading && !data.contextEstimate}
|
||||
t={t}
|
||||
/>
|
||||
) : (
|
||||
<StatusTab data={data} commands={commands} t={t} />
|
||||
)}
|
||||
|
||||
29
desktop/src/i18n/index.test.tsx
Normal file
29
desktop/src/i18n/index.test.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import { useTranslation } from '.'
|
||||
|
||||
describe('useTranslation', () => {
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
useSettingsStore.getState().setLocale('zh')
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the translation function stable until the locale changes', () => {
|
||||
act(() => {
|
||||
useSettingsStore.getState().setLocale('zh')
|
||||
})
|
||||
|
||||
const { result, rerender } = renderHook(() => useTranslation())
|
||||
const initial = result.current
|
||||
|
||||
rerender()
|
||||
expect(result.current).toBe(initial)
|
||||
|
||||
act(() => {
|
||||
useSettingsStore.getState().setLocale('en')
|
||||
})
|
||||
expect(result.current).not.toBe(initial)
|
||||
})
|
||||
})
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import { en, type TranslationKey } from './locales/en'
|
||||
import { zh } from './locales/zh'
|
||||
@ -38,8 +39,11 @@ export function translate(
|
||||
*/
|
||||
export function useTranslation() {
|
||||
const locale = useSettingsStore((s) => s.locale)
|
||||
return (key: TranslationKey, params?: Record<string, string | number>) =>
|
||||
translate(locale, key, params)
|
||||
return useCallback(
|
||||
(key: TranslationKey, params?: Record<string, string | number>) =>
|
||||
translate(locale, key, params),
|
||||
[locale],
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -572,6 +572,7 @@ export const en = {
|
||||
'slash.inspector.usage.tokens': 'Tokens',
|
||||
'slash.inspector.usage.noModelTitle': 'No model usage',
|
||||
'slash.inspector.usage.noModelBody': 'Token usage will appear here after a model response is recorded.',
|
||||
'slash.inspector.context.loading': 'Loading context data',
|
||||
'slash.inspector.context.emptyTitle': 'No context data yet',
|
||||
'slash.inspector.context.emptyBody': 'Context usage requires an active CLI session.',
|
||||
'slash.inspector.context.windowUsage': 'Context window usage',
|
||||
|
||||
@ -574,6 +574,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'slash.inspector.usage.tokens': 'Tokens',
|
||||
'slash.inspector.usage.noModelTitle': '暂无模型用量',
|
||||
'slash.inspector.usage.noModelBody': '记录到模型回复后,这里会显示 token 用量。',
|
||||
'slash.inspector.context.loading': '正在加载上下文数据',
|
||||
'slash.inspector.context.emptyTitle': '暂无上下文数据',
|
||||
'slash.inspector.context.emptyBody': '上下文用量需要活跃的 CLI 会话。',
|
||||
'slash.inspector.context.windowUsage': '上下文窗口用量',
|
||||
|
||||
@ -278,6 +278,61 @@ describe('ConversationService', () => {
|
||||
await fs.rm(workDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('should reconstruct Sonnet 4.6 transcript usage before CLI config is initialized', async () => {
|
||||
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const previousNodeEnv = process.env.NODE_ENV
|
||||
const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-transcript-sonnet-'))
|
||||
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-workdir-sonnet-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpConfigDir
|
||||
process.env.NODE_ENV = 'development'
|
||||
|
||||
try {
|
||||
const svc = new SessionService()
|
||||
const { sessionId } = await svc.createSession(workDir)
|
||||
const found = await svc.findSessionFile(sessionId)
|
||||
expect(found).not.toBeNull()
|
||||
|
||||
await fs.appendFile(found!.filePath, JSON.stringify({
|
||||
type: 'assistant',
|
||||
uuid: crypto.randomUUID(),
|
||||
timestamp: '2026-04-27T12:00:00.000Z',
|
||||
cwd: workDir,
|
||||
version: '999.0.0-test',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
model: 'claude-sonnet-4-6',
|
||||
content: [{ type: 'text', text: 'hello' }],
|
||||
usage: {
|
||||
input_tokens: 100,
|
||||
output_tokens: 20,
|
||||
},
|
||||
},
|
||||
}) + '\n')
|
||||
|
||||
const usage = await svc.getTranscriptUsage(sessionId)
|
||||
const contextEstimate = await svc.getTranscriptContextEstimate(sessionId)
|
||||
|
||||
expect(usage?.models[0]?.model).toBe('claude-sonnet-4-6')
|
||||
expect(usage?.models[0]?.contextWindow).toBe(200_000)
|
||||
expect(contextEstimate?.model).toBe('claude-sonnet-4-6')
|
||||
expect(contextEstimate?.totalTokens).toBe(100)
|
||||
expect(contextEstimate?.rawMaxTokens).toBe(200_000)
|
||||
} finally {
|
||||
if (previousConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
|
||||
}
|
||||
if (previousNodeEnv === undefined) {
|
||||
delete process.env.NODE_ENV
|
||||
} else {
|
||||
process.env.NODE_ENV = previousNodeEnv
|
||||
}
|
||||
await fs.rm(tmpConfigDir, { recursive: true, force: true })
|
||||
await fs.rm(workDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
@ -636,6 +691,12 @@ describe('WebSocket Chat Integration', () => {
|
||||
expect(body.usage.source).toBe('current_process')
|
||||
expect(body.context.model).toBe('mock-opus')
|
||||
expect(body.status.mcpServers).toEqual([{ name: 'mock', status: 'connected' }])
|
||||
|
||||
const basicRes = await fetch(`${baseUrl}/api/sessions/${sessionId}/inspection?includeContext=0`)
|
||||
expect(basicRes.status).toBe(200)
|
||||
const basicBody = await basicRes.json() as any
|
||||
expect(basicBody.usage.source).toBe('current_process')
|
||||
expect(basicBody.context).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should complete the client turn when the CLI exits after startup', async () => {
|
||||
|
||||
@ -106,7 +106,7 @@ export async function handleSessionsApi(
|
||||
{ status: 405 }
|
||||
)
|
||||
}
|
||||
return await getSessionInspection(sessionId)
|
||||
return await getSessionInspection(sessionId, url)
|
||||
}
|
||||
|
||||
// Route to conversations handler if sub-resource is 'chat'
|
||||
@ -217,7 +217,8 @@ async function getSessionSlashCommands(sessionId: string): Promise<Response> {
|
||||
return Response.json({ commands: slashCommands })
|
||||
}
|
||||
|
||||
async function getSessionInspection(sessionId: string): Promise<Response> {
|
||||
async function getSessionInspection(sessionId: string, url: URL): Promise<Response> {
|
||||
const includeContext = url.searchParams.get('includeContext') !== '0'
|
||||
const workDir =
|
||||
conversationService.getSessionWorkDir(sessionId) ||
|
||||
await sessionService.getSessionWorkDir(sessionId)
|
||||
@ -264,6 +265,10 @@ async function getSessionInspection(sessionId: string): Promise<Response> {
|
||||
errors: {},
|
||||
}
|
||||
const transcriptUsage = await sessionService.getTranscriptUsage(sessionId)
|
||||
const transcriptContextEstimate = await sessionService.getTranscriptContextEstimate(sessionId)
|
||||
if (transcriptContextEstimate) {
|
||||
response.contextEstimate = transcriptContextEstimate
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
if (transcriptUsage) {
|
||||
@ -271,15 +276,18 @@ async function getSessionInspection(sessionId: string): Promise<Response> {
|
||||
}
|
||||
response.errors = {
|
||||
...(transcriptUsage ? {} : { usage: 'CLI session is not running' }),
|
||||
context: 'CLI session is not running',
|
||||
...(includeContext ? { context: 'CLI session is not running' } : {}),
|
||||
}
|
||||
return Response.json(response)
|
||||
}
|
||||
|
||||
const basicControlTimeoutMs = includeContext ? 10_000 : 4_000
|
||||
const [usageResult, contextResult, mcpResult] = await Promise.allSettled([
|
||||
conversationService.requestControl(sessionId, { subtype: 'get_session_usage' }),
|
||||
conversationService.requestControl(sessionId, { subtype: 'get_context_usage' }, 20_000),
|
||||
conversationService.requestControl(sessionId, { subtype: 'mcp_status' }),
|
||||
conversationService.requestControl(sessionId, { subtype: 'get_session_usage' }, basicControlTimeoutMs),
|
||||
includeContext
|
||||
? conversationService.requestControl(sessionId, { subtype: 'get_context_usage' }, 20_000)
|
||||
: Promise.resolve(null),
|
||||
conversationService.requestControl(sessionId, { subtype: 'mcp_status' }, basicControlTimeoutMs),
|
||||
])
|
||||
|
||||
const errors: Record<string, string> = {}
|
||||
@ -296,7 +304,10 @@ async function getSessionInspection(sessionId: string): Promise<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
if (contextResult.status === 'fulfilled') {
|
||||
if (!includeContext) {
|
||||
// Context can be expensive on large live sessions. The desktop UI loads it
|
||||
// separately when the context tab is actually selected.
|
||||
} else if (contextResult.status === 'fulfilled' && contextResult.value) {
|
||||
response.context = contextResult.value
|
||||
} else {
|
||||
errors.context = contextResult.reason instanceof Error ? contextResult.reason.message : String(contextResult.reason)
|
||||
|
||||
@ -66,6 +66,7 @@ export function startServer(port = PORT, host = HOST) {
|
||||
const server = Bun.serve<WebSocketData>({
|
||||
port,
|
||||
hostname: host,
|
||||
idleTimeout: 60,
|
||||
|
||||
async fetch(req, server) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
@ -12,7 +12,11 @@ import { ApiError } from '../middleware/errorHandler.js'
|
||||
import { sanitizePath as sanitizePortablePath } from '../../utils/sessionStoragePortable.js'
|
||||
import type { FileHistorySnapshot } from '../../utils/fileHistory.js'
|
||||
import { calculateUSDCost, MODEL_COSTS } from '../../utils/modelCost.js'
|
||||
import { getContextWindowForModel, getModelMaxOutputTokens } from '../../utils/context.js'
|
||||
import {
|
||||
MODEL_CONTEXT_WINDOW_DEFAULT,
|
||||
getContextWindowForModel,
|
||||
getModelMaxOutputTokens,
|
||||
} from '../../utils/context.js'
|
||||
import { getCanonicalName } from '../../utils/model/model.js'
|
||||
|
||||
// ============================================================================
|
||||
@ -93,6 +97,37 @@ export type TranscriptMetadataSnapshot = {
|
||||
version?: string
|
||||
}
|
||||
|
||||
export type TranscriptContextEstimate = {
|
||||
categories: Array<{
|
||||
name: string
|
||||
tokens: number
|
||||
color: string
|
||||
isDeferred?: boolean
|
||||
}>
|
||||
totalTokens: number
|
||||
maxTokens: number
|
||||
rawMaxTokens: number
|
||||
percentage: number
|
||||
gridRows: Array<Array<{
|
||||
color: string
|
||||
isFilled: boolean
|
||||
categoryName: string
|
||||
tokens: number
|
||||
percentage: number
|
||||
squareFullness: number
|
||||
}>>
|
||||
model: string
|
||||
memoryFiles: Array<{ path: string; type: string; tokens: number }>
|
||||
mcpTools: Array<{ name: string; serverName: string; tokens: number; isLoaded?: boolean }>
|
||||
agents: Array<{ agentType: string; source: string; tokens: number }>
|
||||
apiUsage: {
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
cache_creation_input_tokens: number
|
||||
cache_read_input_tokens: number
|
||||
}
|
||||
}
|
||||
|
||||
/** Raw entry parsed from a single JSONL line */
|
||||
type RawEntry = {
|
||||
type?: string
|
||||
@ -704,6 +739,20 @@ export class SessionService {
|
||||
return `$${cost > 0.5 ? (Math.round(cost * 100) / 100).toFixed(2) : cost.toFixed(4)}`
|
||||
}
|
||||
|
||||
private getTranscriptContextWindow(model: string): number {
|
||||
try {
|
||||
return getContextWindowForModel(model)
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.message.includes('Config accessed before allowed')
|
||||
) {
|
||||
return MODEL_CONTEXT_WINDOW_DEFAULT
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getTranscriptMetadata(sessionId: string): Promise<TranscriptMetadataSnapshot | null> {
|
||||
const found = await this.findSessionFile(sessionId)
|
||||
if (!found) return null
|
||||
@ -728,6 +777,88 @@ export class SessionService {
|
||||
return metadata
|
||||
}
|
||||
|
||||
async getTranscriptContextEstimate(sessionId: string): Promise<TranscriptContextEstimate | null> {
|
||||
const found = await this.findSessionFile(sessionId)
|
||||
if (!found) return null
|
||||
|
||||
const entries = await this.readJsonlFile(found.filePath)
|
||||
let latest: {
|
||||
model: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheReadInputTokens: number
|
||||
cacheCreationInputTokens: number
|
||||
} | null = null
|
||||
|
||||
for (const entry of entries) {
|
||||
const usage = entry.message?.usage
|
||||
const model = entry.message?.model
|
||||
if (!usage || typeof model !== 'string') continue
|
||||
|
||||
const inputTokens = typeof usage.input_tokens === 'number' ? usage.input_tokens : 0
|
||||
const outputTokens = typeof usage.output_tokens === 'number' ? usage.output_tokens : 0
|
||||
const cacheReadInputTokens = typeof usage.cache_read_input_tokens === 'number' ? usage.cache_read_input_tokens : 0
|
||||
const cacheCreationInputTokens = typeof usage.cache_creation_input_tokens === 'number' ? usage.cache_creation_input_tokens : 0
|
||||
const promptTokens = inputTokens + cacheReadInputTokens + cacheCreationInputTokens
|
||||
if (promptTokens === 0 && outputTokens === 0) continue
|
||||
|
||||
latest = {
|
||||
model,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadInputTokens,
|
||||
cacheCreationInputTokens,
|
||||
}
|
||||
}
|
||||
|
||||
if (!latest) return null
|
||||
|
||||
const rawMaxTokens = this.getTranscriptContextWindow(latest.model)
|
||||
const totalTokens = latest.inputTokens + latest.cacheReadInputTokens + latest.cacheCreationInputTokens
|
||||
const percentage = rawMaxTokens > 0 ? Math.round((totalTokens / rawMaxTokens) * 100) : 0
|
||||
const categories: TranscriptContextEstimate['categories'] = [
|
||||
{ name: 'Input tokens', tokens: latest.inputTokens, color: '#8f3217' },
|
||||
{ name: 'Cache read', tokens: latest.cacheReadInputTokens, color: '#0f5c8f' },
|
||||
{ name: 'Cache write', tokens: latest.cacheCreationInputTokens, color: '#7c3aed' },
|
||||
{ name: 'Free space', tokens: Math.max(0, rawMaxTokens - totalTokens), color: '#a1a1aa', isDeferred: true },
|
||||
].filter((category) => category.tokens > 0)
|
||||
|
||||
const filledSquares = Math.max(0, Math.min(100, Math.round((totalTokens / Math.max(1, rawMaxTokens)) * 100)))
|
||||
const gridRows = Array.from({ length: 10 }, (_, row) =>
|
||||
Array.from({ length: 10 }, (_, col) => {
|
||||
const index = row * 10 + col
|
||||
const isFilled = index < filledSquares
|
||||
return {
|
||||
color: isFilled ? '#8f3217' : '#a1a1aa',
|
||||
isFilled,
|
||||
categoryName: isFilled ? 'Input context' : 'Free space',
|
||||
tokens: Math.round(rawMaxTokens / 100),
|
||||
percentage: 1,
|
||||
squareFullness: isFilled ? 1 : 0,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
categories,
|
||||
totalTokens,
|
||||
maxTokens: rawMaxTokens,
|
||||
rawMaxTokens,
|
||||
percentage,
|
||||
gridRows,
|
||||
model: latest.model,
|
||||
memoryFiles: [],
|
||||
mcpTools: [],
|
||||
agents: [],
|
||||
apiUsage: {
|
||||
input_tokens: latest.inputTokens,
|
||||
output_tokens: latest.outputTokens,
|
||||
cache_creation_input_tokens: latest.cacheCreationInputTokens,
|
||||
cache_read_input_tokens: latest.cacheReadInputTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async getTranscriptUsage(sessionId: string): Promise<TranscriptUsageSnapshot | null> {
|
||||
const found = await this.findSessionFile(sessionId)
|
||||
if (!found) return null
|
||||
@ -794,7 +925,7 @@ export class SessionService {
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
costDisplay: '$0.0000',
|
||||
contextWindow: getContextWindowForModel(model),
|
||||
contextWindow: this.getTranscriptContextWindow(model),
|
||||
maxOutputTokens: getModelMaxOutputTokens(model).default,
|
||||
}
|
||||
models.set(model, modelUsage)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user