mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-17 03:56:45 +00:00
Fix OpenCode MCP usage reporting (#318)
* Fix OpenCode MCP usage reporting * Move OpenCode MCP changelog entry to Unreleased section --------- Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com> Co-authored-by: iamtoruk <hello@agentseal.org>
This commit is contained in:
parent
c85beeaeae
commit
a1b5e4bd00
4 changed files with 151 additions and 6 deletions
|
|
@ -11,6 +11,10 @@
|
|||
- **Claude 1-hour cache write pricing.** 1-hour cache writes are now priced
|
||||
at 2x base input (previously used the 5-minute 1.25x rate for all writes).
|
||||
Daily cache bumped to v6 so stale totals are recomputed. Closes #276.
|
||||
- **OpenCode MCP usage now counted.** OpenCode stores MCP tool calls as
|
||||
`<server>_<tool>` names, which the shared MCP pipeline did not recognize.
|
||||
The provider now normalizes these to the canonical `mcp__<server>__<tool>`
|
||||
form so MCP breakdowns and `optimize` work correctly. Closes #308.
|
||||
|
||||
## 0.9.8 - 2026-05-10
|
||||
|
||||
|
|
|
|||
|
|
@ -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