Keep OpenCode router calls without usage (#342)
Some checks are pending
CI / semgrep (push) Waiting to run

This commit is contained in:
ozymandiashh 2026-05-19 02:48:03 +03:00 committed by GitHub
parent 06f69484f3
commit c9487e7b0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 57 additions and 3 deletions

View file

@ -43,6 +43,11 @@
child and grandchild agent sessions contribute token and tool usage under
the discovered root session while still excluding child sessions from
top-level discovery to avoid double counting.
- **OpenCode router sessions with missing usage are still reported.**
Some OpenCode router/provider combinations can persist assistant messages
with text or tool activity but zero token and cost fields. The OpenCode
parser now keeps those turns as zero-cost calls instead of dropping the
session entirely. Closes #341.
## 0.9.9 - 2026-05-15

View file

@ -32,6 +32,9 @@ Per `<sessionId>:<messageId>`.
token, and tool usage back to the root session.
- 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.
- Assistant messages with missing router usage are kept as zero-cost calls
when their parts contain non-empty text or tool activity. Empty zero-usage
assistant placeholders are still skipped.
- 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

View file

@ -264,16 +264,19 @@ function createParser(
cacheWrite: data.tokens?.cache?.write ?? 0,
}
const msgParts = partsByMsg.get(msg.id) ?? []
const toolParts = msgParts.filter((p) => p.type === 'tool' && normalizeToolName(p.tool))
const hasTextOutput = msgParts.some((p) => p.type === 'text' && typeof p.text === 'string' && p.text.trim().length > 0)
const hasActivity = hasTextOutput || toolParts.length > 0
const allZero =
tokens.input === 0 &&
tokens.output === 0 &&
tokens.reasoning === 0 &&
tokens.cacheRead === 0 &&
tokens.cacheWrite === 0
if (allZero && (data.cost ?? 0) === 0) continue
if (allZero && (data.cost ?? 0) === 0 && !hasActivity) continue
const msgParts = partsByMsg.get(msg.id) ?? []
const toolParts = msgParts.filter((p) => p.type === 'tool')
const tools = toolParts
.map((p) => normalizeToolName(p.tool))
.filter(Boolean)

View file

@ -469,6 +469,49 @@ skipUnlessSqlite('opencode provider - session parsing', () => {
expect(calls).toHaveLength(0)
})
it('keeps zero-usage assistant messages when router responses contain text', async () => {
const dbPath = createTestDb(tmpDir)
withTestDb(dbPath, (db) => {
insertSession(db, 'sess-1')
insertMessage(db, 'msg-u1', 'sess-1', 1700000000000, { role: 'user' })
insertPart(db, 'part-u1', 'msg-u1', 'sess-1', { type: 'text', text: 'use the configured router' })
insertMessage(db, 'msg-a1', 'sess-1', 1700000001000, {
role: 'assistant', modelID: 'edenai/router-model', cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
})
insertPart(db, 'part-a1', 'msg-a1', 'sess-1', { type: 'text', text: 'router response text' })
})
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('edenai/router-model')
expect(calls[0]!.inputTokens).toBe(0)
expect(calls[0]!.outputTokens).toBe(0)
expect(calls[0]!.costUSD).toBe(0)
expect(calls[0]!.userMessage).toBe('use the configured router')
})
it('keeps zero-usage assistant messages when router responses contain tool calls', async () => {
const dbPath = createTestDb(tmpDir)
withTestDb(dbPath, (db) => {
insertSession(db, 'sess-1')
insertMessage(db, 'msg-a1', 'sess-1', 1700000001000, {
role: 'assistant', modelID: 'edenai/router-model', cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
})
insertPart(db, 'part-a1', 'msg-a1', 'sess-1', {
type: 'tool', tool: 'bash',
state: { status: 'completed', input: { command: 'npm test' } },
})
})
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
expect(calls).toHaveLength(1)
expect(calls[0]!.tools).toEqual(['Bash'])
expect(calls[0]!.bashCommands).toEqual(['npm'])
expect(calls[0]!.costUSD).toBe(0)
})
it('deduplicates messages across parses', async () => {
const dbPath = createTestDb(tmpDir)
withTestDb(dbPath, (db) => {