Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 157 additions & 33 deletions src/claude-code-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,121 @@ import {
} from "./session-manager.js"
import { log } from "./logger.js"

type ClaudeCodeEffort = "low" | "medium" | "high" | "max"

function buildUsage(
usage?: ClaudeStreamMessage["usage"],
): LanguageModelV2Usage {
const hasInputTokens =
typeof usage?.input_tokens === "number" ||
typeof usage?.cache_read_input_tokens === "number" ||
typeof usage?.cache_creation_input_tokens === "number"
const inputTokens = hasInputTokens
? (usage?.input_tokens ?? 0) +
(usage?.cache_read_input_tokens ?? 0) +
(usage?.cache_creation_input_tokens ?? 0)
: undefined
const outputTokens = usage?.output_tokens

return {
inputTokens,
outputTokens,
totalTokens:
typeof inputTokens === "number" && typeof outputTokens === "number"
? inputTokens + outputTokens
: undefined,
cachedInputTokens: usage?.cache_read_input_tokens,
}
}

function buildProviderMetadata(meta: {
sessionId?: string
costUsd?: number
durationMs?: number
cacheCreationInputTokens?: number
}): Record<string, Record<string, string | number | null>> | undefined {
const data: Record<string, string | number | null> = {}
const anthropic: Record<string, string | number | null> = {}

if (typeof meta.sessionId === "string") data.sessionId = meta.sessionId
if (typeof meta.costUsd === "number") data.costUsd = meta.costUsd
if (typeof meta.durationMs === "number") data.durationMs = meta.durationMs
if (typeof meta.cacheCreationInputTokens === "number") {
anthropic.cacheCreationInputTokens = meta.cacheCreationInputTokens
}

if (Object.keys(data).length === 0 && Object.keys(anthropic).length === 0) {
return undefined
}

return {
...(Object.keys(data).length > 0 ? { "claude-code": data } : {}),
...(Object.keys(anthropic).length > 0 ? { anthropic } : {}),
}
}

function buildStreamUsage(
usage: LanguageModelV2Usage,
cacheWriteInputTokens?: number,
) {
const cacheReadInputTokens = usage.cachedInputTokens
const noCacheInputTokens =
typeof usage.inputTokens === "number"
? usage.inputTokens -
(cacheReadInputTokens ?? 0) -
(cacheWriteInputTokens ?? 0)
: undefined

return {
inputTokens: {
total: usage.inputTokens,
noCache: noCacheInputTokens,
cacheRead: cacheReadInputTokens,
cacheWrite: cacheWriteInputTokens,
},
outputTokens: {
total: usage.outputTokens,
text: undefined,
reasoning: usage.reasoningTokens,
},
totalTokens: usage.totalTokens,
}
}

function getClaudeCodeEffort(
provider: string,
providerOptions: Record<string, any> | undefined,
): ClaudeCodeEffort | undefined {
const scoped = providerOptions?.[provider]
if (!scoped || typeof scoped !== "object" || Array.isArray(scoped)) {
return undefined
}

const raw =
scoped.effort ??
scoped.reasoningEffort ??
scoped.thinkingLevel

if (typeof raw !== "string") {
return undefined
}

const normalized = raw.toLowerCase()
if (normalized === "low" || normalized === "medium" || normalized === "high" || normalized === "max") {
return normalized
}

if (normalized === "xhigh") {
return "max"
}

log.warn("ignoring unsupported claude effort", {
provider,
effort: raw,
})
return undefined
}

export class ClaudeCodeLanguageModel implements LanguageModelV2 {
readonly specificationVersion = "v2"
readonly modelId: string
Expand Down Expand Up @@ -136,7 +251,8 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
const warnings: LanguageModelV2CallWarning[] = []
const cwd = this.config.cwd ?? process.cwd()
const scope = this.requestScope(options as any)
const sk = sessionKey(cwd, `${this.modelId}::${scope}`)
const effort = getClaudeCodeEffort(this.provider, options.providerOptions as any)
const sk = sessionKey(cwd, `${this.modelId}::${scope}`, effort)

if (scope === "no-tools") {
const text = this.synthesizeTitle(options.prompt)
Expand Down Expand Up @@ -179,17 +295,18 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {

const userMsg = getClaudeUserMessage(options.prompt, includeHistoryContext)

// doGenerate always spawns a fresh process, never reuse session ID
const cliArgs = buildCliArgs({
sessionKey: sk,
skipPermissions: this.config.skipPermissions !== false,
includeSessionId: false,
model: this.modelId,
effort,
})

log.info("doGenerate starting", {
cwd,
model: this.modelId,
effort,
textLength: userMsg.length,
includeHistoryContext,
})
Expand Down Expand Up @@ -436,7 +553,8 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
const cliPath = this.config.cliPath
const skipPermissions = this.config.skipPermissions !== false
const scope = this.requestScope(options as any)
const sk = sessionKey(cwd, `${this.modelId}::${scope}`)
const effort = getClaudeCodeEffort(this.provider, options.providerOptions as any)
const sk = sessionKey(cwd, `${this.modelId}::${scope}`, effort)

if (scope === "no-tools") {
const text = this.synthesizeTitle(options.prompt)
Expand All @@ -453,19 +571,23 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
controller.enqueue({ type: "text-end", id: textId })
controller.enqueue({
type: "finish",
finishReason: "stop",
usage: {
finishReason: {
unified: "stop",
raw: undefined,
} as any,
rawFinishReason: undefined as any,
usage: buildStreamUsage({
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
},
}) as any,
providerMetadata: {
"claude-code": {
synthetic: true,
path: "no-tools",
},
},
})
} as any)
controller.close()
},
})
Expand Down Expand Up @@ -496,6 +618,7 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
log.info("doStream starting", {
cwd,
model: this.modelId,
effort,
textLength: userMsg.length,
includeHistoryContext,
hasActiveProcess,
Expand All @@ -505,6 +628,7 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
sessionKey: sk,
skipPermissions,
model: this.modelId,
effort,
})

const stream = new ReadableStream<LanguageModelV2StreamPart>({
Expand Down Expand Up @@ -548,6 +672,7 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
costUsd?: number
durationMs?: number
usage?: ClaudeStreamMessage["usage"]
cacheCreationInputTokens?: number
} = {}

const lineHandler = (line: string) => {
Expand Down Expand Up @@ -969,6 +1094,7 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
costUsd: msg.total_cost_usd,
durationMs: msg.duration_ms,
usage: msg.usage,
cacheCreationInputTokens: msg.usage?.cache_creation_input_tokens,
}

log.info("conversation result", {
Expand All @@ -993,24 +1119,21 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
}
}

const usage = buildUsage(msg.usage)

controller.enqueue({
type: "finish",
finishReason:
toolCallMap.size > 0 ? "tool-calls" : "stop",
usage: {
inputTokens: msg.usage?.input_tokens,
outputTokens: msg.usage?.output_tokens,
totalTokens:
msg.usage?.input_tokens &&
msg.usage?.output_tokens
? msg.usage.input_tokens +
msg.usage.output_tokens
: undefined,
},
providerMetadata: {
"claude-code": resultMeta,
},
})
finishReason: {
unified: toolCallMap.size > 0 ? "tool-calls" : "stop",
raw: undefined,
} as any,
rawFinishReason: undefined as any,
usage: buildStreamUsage(
usage,
msg.usage?.cache_creation_input_tokens,
) as any,
providerMetadata: buildProviderMetadata(resultMeta),
} as any)

controllerClosed = true
lineEmitter.off("line", lineHandler)
Expand Down Expand Up @@ -1039,16 +1162,17 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
}
controller.enqueue({
type: "finish",
finishReason: "stop",
usage: {
inputTokens: undefined,
outputTokens: undefined,
totalTokens: undefined,
},
providerMetadata: {
"claude-code": resultMeta,
},
})
finishReason: {
unified: "stop",
raw: undefined,
} as any,
rawFinishReason: undefined as any,
usage: buildStreamUsage(
buildUsage(resultMeta.usage),
resultMeta.cacheCreationInputTokens,
) as any,
providerMetadata: buildProviderMetadata(resultMeta),
} as any)
try {
controller.close()
} catch {}
Expand Down
15 changes: 10 additions & 5 deletions src/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ export function buildCliArgs(opts: {
skipPermissions: boolean
includeSessionId?: boolean
model?: string
effort?: string
}): string[] {
const { sessionKey, skipPermissions, includeSessionId = true, model } = opts
const { sessionKey, skipPermissions, includeSessionId = true, model, effort } = opts
const args = [
"--output-format",
"stream-json",
Expand All @@ -121,6 +122,10 @@ export function buildCliArgs(opts: {
args.push("--model", model)
}

if (effort) {
args.push("--effort", effort)
}

if (includeSessionId) {
const sessionId = claudeSessions.get(sessionKey)
if (sessionId && !activeProcesses.has(sessionKey)) {
Expand All @@ -136,9 +141,9 @@ export function buildCliArgs(opts: {
}

/**
* Build a session key that includes both cwd and model,
* so different models get separate processes.
* Build a session key that includes cwd, model, and runtime options
* that affect Claude session behavior.
*/
export function sessionKey(cwd: string, modelId: string): string {
return `${cwd}::${modelId}`
export function sessionKey(cwd: string, modelId: string, effort?: string): string {
return `${cwd}::${modelId}::${effort ?? "default"}`
}