mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-17 21:11:37 +00:00
Fix OpenCode MCP usage reporting
This commit is contained in:
parent
d9acd8c4cd
commit
b6088c2a42
4 changed files with 154 additions and 6 deletions
|
|
@ -41,6 +41,13 @@
|
|||
reconcile. Closes #279.
|
||||
|
||||
### Fixed (CLI)
|
||||
- **OpenCode MCP usage is now counted.** OpenCode records MCP tool calls as
|
||||
`<server>_<tool>` part names such as `clickup_clickup_get_task`, while the
|
||||
shared CodeBurn MCP reporting pipeline expects `mcp__<server>__<tool>`.
|
||||
The OpenCode provider now normalizes external tool calls into that canonical
|
||||
shape before aggregation, so dashboard/JSON MCP breakdowns show servers like
|
||||
ClickUp and Figma as used and `optimize` no longer recommends removing
|
||||
configured servers that were invoked from OpenCode. Closes #308.
|
||||
- **Cursor sessions break down by project, not one row called "cursor".**
|
||||
Cursor's chat history sat under a single dashboard row labeled `cursor`
|
||||
because the provider had no way to attribute bubbles to a workspace.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ OpenCode (sst/opencode).
|
|||
|
||||
- **Source:** `src/providers/opencode.ts`
|
||||
- **Loading:** lazy (`src/providers/index.ts:59-75`)
|
||||
- **Test:** `tests/providers/opencode.test.ts` (558 lines, the largest provider test)
|
||||
- **Test:** `tests/providers/opencode.test.ts` (676 lines, the largest provider test)
|
||||
|
||||
## Where it reads from
|
||||
|
||||
|
|
@ -20,14 +20,18 @@ None.
|
|||
|
||||
## Deduplication
|
||||
|
||||
Per `<sessionId>:<messageId>` (`opencode.ts:242`).
|
||||
Per `<sessionId>:<messageId>`.
|
||||
|
||||
## Quirks
|
||||
|
||||
- **Schema validation is loud.** When a required table is missing, the parser logs an actionable warning telling the user which table is gone and what version of OpenCode it expects (`opencode.ts:104-131`). This is the right behavior; do not silently swallow these.
|
||||
- Source paths are encoded as `<dbPath>:<sessionId>` (`opencode.ts:147-150`).
|
||||
- Each message's `parts` are indexed (`opencode.ts:177-191`); preserving the order matters for reasoning-token correctness.
|
||||
- **Schema validation is loud.** When a required table is missing, the parser logs an actionable warning telling the user which table is gone and what version of OpenCode it expects. This is the right behavior; do not silently swallow these.
|
||||
- Source paths are encoded as `<dbPath>:<sessionId>`.
|
||||
- Each message's `parts` are indexed; preserving the order matters for reasoning-token correctness.
|
||||
- Tokens are reported across `input`, `output`, `reasoning`, `cache.read`, and `cache.write`. Anthropic semantics.
|
||||
- External MCP tools are stored as `<server>_<tool>` names (for example
|
||||
`clickup_clickup_get_task`). The provider normalizes those to CodeBurn's
|
||||
canonical `mcp__<server>__<tool>` names before aggregation so shared MCP
|
||||
panels and `optimize` findings count OpenCode usage.
|
||||
|
||||
## When fixing a bug here
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,25 @@ const toolNameMap: Record<string, string> = {
|
|||
patch: 'Patch',
|
||||
}
|
||||
|
||||
function normalizeToolName(rawTool?: string): string {
|
||||
if (!rawTool) return ''
|
||||
if (rawTool.startsWith('mcp__')) return rawTool
|
||||
|
||||
const builtIn = toolNameMap[rawTool]
|
||||
if (builtIn) return builtIn
|
||||
|
||||
// OpenCode stores MCP calls as `<server>_<tool>` with no separate server field.
|
||||
// Built-ins are handled above, and server ids are assumed not to contain `_`.
|
||||
const serverSeparator = rawTool.indexOf('_')
|
||||
if (serverSeparator > 0 && serverSeparator < rawTool.length - 1) {
|
||||
const server = rawTool.slice(0, serverSeparator)
|
||||
const tool = rawTool.slice(serverSeparator + 1)
|
||||
return `mcp__${server}__${tool}`
|
||||
}
|
||||
|
||||
return rawTool
|
||||
}
|
||||
|
||||
function sanitize(dir: string): string {
|
||||
return dir.replace(/^\//, '').replace(/\//g, '-')
|
||||
}
|
||||
|
|
@ -232,7 +251,7 @@ function createParser(
|
|||
const msgParts = partsByMsg.get(msg.id) ?? []
|
||||
const toolParts = msgParts.filter((p) => p.type === 'tool')
|
||||
const tools = toolParts
|
||||
.map((p) => toolNameMap[p.tool ?? ''] ?? p.tool ?? '')
|
||||
.map((p) => normalizeToolName(p.tool))
|
||||
.filter(Boolean)
|
||||
|
||||
const bashCommands = toolParts
|
||||
|
|
|
|||
|
|
@ -337,6 +337,124 @@ skipUnlessSqlite('opencode provider - session parsing', () => {
|
|||
expect(call.deduplicationKey).toBe('opencode:sess-1:msg-2')
|
||||
})
|
||||
|
||||
it('normalizes opencode MCP tool names for shared MCP reporting', async () => {
|
||||
const dbPath = createTestDb(tmpDir)
|
||||
withTestDb(dbPath, (db) => {
|
||||
insertSession(db, 'sess-1')
|
||||
|
||||
insertMessage(db, 'msg-1', 'sess-1', 1700000000000, { role: 'user' })
|
||||
insertPart(db, 'part-1', 'msg-1', 'sess-1', { type: 'text', text: 'look up the ClickUp task' })
|
||||
|
||||
insertMessage(db, 'msg-2', 'sess-1', 1700000001000, {
|
||||
role: 'assistant',
|
||||
modelID: 'claude-opus-4-6',
|
||||
cost: 0.05,
|
||||
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
})
|
||||
insertPart(db, 'part-2', 'msg-2', 'sess-1', {
|
||||
type: 'tool',
|
||||
tool: 'clickup_clickup_get_task',
|
||||
state: { status: 'completed', input: {} },
|
||||
})
|
||||
insertPart(db, 'part-3', 'msg-2', 'sess-1', {
|
||||
type: 'tool',
|
||||
tool: 'figma_get_file',
|
||||
state: { status: 'completed', input: {} },
|
||||
})
|
||||
})
|
||||
|
||||
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.tools).toEqual([
|
||||
'mcp__clickup__clickup_get_task',
|
||||
'mcp__figma__get_file',
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves already-normalized MCP tool names', async () => {
|
||||
const dbPath = createTestDb(tmpDir)
|
||||
withTestDb(dbPath, (db) => {
|
||||
insertSession(db, 'sess-1')
|
||||
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
||||
role: 'assistant',
|
||||
modelID: 'claude-opus-4-6',
|
||||
cost: 0.05,
|
||||
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
})
|
||||
insertPart(db, 'part-1', 'msg-1', 'sess-1', {
|
||||
type: 'tool',
|
||||
tool: 'mcp__github__search_code',
|
||||
state: { status: 'completed', input: {} },
|
||||
})
|
||||
})
|
||||
|
||||
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.tools).toEqual(['mcp__github__search_code'])
|
||||
})
|
||||
|
||||
it('keeps extension tool names without a server prefix as regular tools', async () => {
|
||||
const dbPath = createTestDb(tmpDir)
|
||||
withTestDb(dbPath, (db) => {
|
||||
insertSession(db, 'sess-1')
|
||||
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
||||
role: 'assistant',
|
||||
modelID: 'claude-opus-4-6',
|
||||
cost: 0.05,
|
||||
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
})
|
||||
insertPart(db, 'part-1', 'msg-1', 'sess-1', {
|
||||
type: 'tool',
|
||||
tool: 'customtool',
|
||||
state: { status: 'completed', input: {} },
|
||||
})
|
||||
})
|
||||
|
||||
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.tools).toEqual(['customtool'])
|
||||
})
|
||||
|
||||
it('keeps malformed server-prefixed tool names as regular tools', async () => {
|
||||
const dbPath = createTestDb(tmpDir)
|
||||
withTestDb(dbPath, (db) => {
|
||||
insertSession(db, 'sess-1')
|
||||
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
||||
role: 'assistant',
|
||||
modelID: 'claude-opus-4-6',
|
||||
cost: 0.05,
|
||||
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
})
|
||||
insertPart(db, 'part-1', 'msg-1', 'sess-1', {
|
||||
type: 'tool',
|
||||
tool: '_missing_server',
|
||||
state: { status: 'completed', input: {} },
|
||||
})
|
||||
insertPart(db, 'part-2', 'msg-1', 'sess-1', {
|
||||
type: 'tool',
|
||||
tool: 'missing_',
|
||||
state: { status: 'completed', input: {} },
|
||||
})
|
||||
insertPart(db, 'part-3', 'msg-1', 'sess-1', {
|
||||
type: 'tool',
|
||||
tool: '_',
|
||||
state: { status: 'completed', input: {} },
|
||||
})
|
||||
})
|
||||
|
||||
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.tools).toEqual([
|
||||
'_missing_server',
|
||||
'missing_',
|
||||
'_',
|
||||
])
|
||||
})
|
||||
|
||||
it('skips zero-token messages with zero cost', async () => {
|
||||
const dbPath = createTestDb(tmpDir)
|
||||
withTestDb(dbPath, (db) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue