feat(core): report Claude Agent plan usage on step metadata (#60879)

This commit is contained in:
Shoubhit Dash
2026-11-01 21:34:13 +05:30
committed by GitHub
Unverified
parent 4f3156ab3c
commit c8a473bead
2 changed files with 23 additions and 1 deletions
@@ -1,7 +1,7 @@
export * as ConvertClaudeStream from "./convert-claude-stream.js"
import type { LanguageModelV3FunctionTool, LanguageModelV3StreamPart } from "@ai-sdk/provider"
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"
import type { SDKMessage, SDKRateLimitInfo } from "@anthropic-ai/claude-agent-sdk"
import { Provider } from "@opencode/schema/provider"
import { ClaudeUsage } from "./usage.js"
import { ToolNames } from "../tools/names.js"
@@ -15,11 +15,15 @@ export function make(tools: readonly LanguageModelV3FunctionTool[]) {
let messageID = ""
let calledTools = false
let metadata: Record<string, { sessionID: string; messageID: string }> | undefined
// Plugins read plan usage from step metadata instead of polling Anthropic for it.
let rateLimit: SDKRateLimitInfo | undefined
const finishMetadata = () => (rateLimit ? { [Provider.ID.claudeAgent]: { rateLimit } } : undefined)
return (message: SDKMessage): LanguageModelV3StreamPart[] => {
const parts: LanguageModelV3StreamPart[] = []
const error = failure(message)
if (error) return [{ type: "error", error }]
if (message.type === "rate_limit_event") rateLimit = message.rate_limit_info
if (message.type === "stream_event" && message.parent_tool_use_id === null) {
const event = message.event
switch (event.type) {
@@ -74,6 +78,7 @@ export function make(tools: readonly LanguageModelV3FunctionTool[]) {
type: "finish",
finishReason: { unified: "tool-calls", raw: "tool_use" },
usage: usage.snapshot(),
providerMetadata: finishMetadata(),
})
}
break
@@ -126,6 +131,7 @@ export function make(tools: readonly LanguageModelV3FunctionTool[]) {
raw: message.stop_reason ?? undefined,
},
usage: usage.snapshot(),
providerMetadata: finishMetadata(),
})
}
return parts
@@ -28,6 +28,22 @@ describe("Claude conversions", () => {
expect(unknown.some((part) => part.type === "tool-call" || part.type === "finish")).toBe(false)
})
test("a step's finish carries the plan usage Claude Code reported during it", () => {
const recorded = Recorder.load("tools").find(
(line) => line.from === "claude" && line.message.type === "rate_limit_event",
)
if (recorded?.from !== "claude" || recorded.message.type !== "rate_limit_event")
throw new Error("Expected a recorded rate limit event")
expect(firstToolReply(TestTools.tools).at(-1)).toMatchObject({
type: "finish",
providerMetadata: { "claude-agent": { rateLimit: recorded.message.rate_limit_info } },
})
expect(ConvertClaudeStream.make([])(Recorder.result("text")).at(-1)).toMatchObject({
type: "finish",
providerMetadata: undefined,
})
})
test("utility calls keep system instructions separate from conversation content and omit thinking", () => {
const prompt: LanguageModelV3Prompt = [
{ role: "system", content: "Summarize this conversation." },