Skip to content
Merged
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
23 changes: 8 additions & 15 deletions src/backends/claude-code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {
SDKStatusMessage,
SDKSystemMessage,
} from '@anthropic-ai/claude-agent-sdk';
import { storeLlmCall } from '../../db/repositories/runsRepository.js';
import { logger } from '../../utils/logging.js';
import { extractPRUrl } from '../../utils/prUrl.js';
import { getWorkspaceDir } from '../../utils/repo.js';
Expand All @@ -23,6 +22,7 @@ import {
} from '../completion.js';
import { cleanupContextFiles } from '../contextFiles.js';
import { buildSystemPrompt, buildTaskPrompt } from '../nativeTools.js';
import { logLlmCall } from '../shared/llmCallLogger.js';
import type { AgentEngine, AgentEngineResult, AgentExecutionPlan } from '../types.js';
import { buildClaudeEnv } from './env.js';
import { buildHooks } from './hooks.js';
Expand Down Expand Up @@ -306,13 +306,13 @@ function resolveNativeTools(nativeToolCapabilities?: string[]): string[] {
return tools.size > 0 ? [...tools] : ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'];
}

function logLlmCall(
function logClaudeCodeLlmCall(
input: AgentExecutionPlan,
assistantMsg: SDKAssistantMessage,
turnCount: number,
model: string,
): void {
if (!input.runId || !assistantMsg.message?.usage) return;
if (!assistantMsg.message?.usage) return;

const usage = assistantMsg.message.usage;
let response: string | undefined;
Expand All @@ -322,23 +322,16 @@ function logLlmCall(
// Ignore serialization errors
}

storeLlmCall({
logLlmCall({
runId: input.runId,
callNumber: turnCount,
request: undefined,
response,
model,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cachedTokens: undefined,
costUsd: undefined,
durationMs: undefined,
model,
}).catch((err) => {
logger.warn('Failed to store Claude Code LLM call in real-time', {
runId: input.runId,
turn: turnCount,
error: String(err),
});
response,
engineLabel: 'Claude Code',
});
}

Expand Down Expand Up @@ -374,7 +367,7 @@ async function consumeStream(
await input.progressReporter.onIteration(turnCount, input.maxIterations);
processAssistantMessage(assistantMsg, turnCount, input);
toolCallCount += countToolCalls(assistantMsg);
logLlmCall(input, assistantMsg, turnCount, model);
logClaudeCodeLlmCall(input, assistantMsg, turnCount, model);
} else if (message.type === 'system') {
const sysMsg = message as { subtype: string; [key: string]: unknown };
if (sysMsg.subtype === 'task_notification') {
Expand Down
20 changes: 5 additions & 15 deletions src/backends/codex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import {
findCredentialIdByEnvVarKey,
updateCredential,
} from '../../db/repositories/credentialsRepository.js';
import { storeLlmCall } from '../../db/repositories/runsRepository.js';
import { logger } from '../../utils/logging.js';
import { extractPRUrl } from '../../utils/prUrl.js';
import { CODEX_ENGINE_DEFINITION } from '../catalog.js';
import { cleanupContextFiles } from '../contextFiles.js';
import { buildSystemPrompt, buildTaskPrompt } from '../nativeTools.js';
import { logLlmCall } from '../shared/llmCallLogger.js';
import type { AgentEngine, AgentEngineResult, AgentExecutionPlan, LogWriter } from '../types.js';
import { buildEnv } from './env.js';
import { CODEX_MODEL_IDS, DEFAULT_CODEX_MODEL } from './models.js';
Expand Down Expand Up @@ -249,26 +248,17 @@ function logText(context: CodexLineContext, text: string): void {

function trackUsage(context: CodexLineContext, responseLine: string, usage: UsageSummary): void {
context.cost = usage.costUsd ?? context.cost;
if (!context.input.runId) return;

context.llmCallCount += 1;
void storeLlmCall({
logLlmCall({
runId: context.input.runId,
callNumber: context.llmCallCount,
request: undefined,
response: responseLine,
model: context.model,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
cachedTokens: usage.cachedTokens,
costUsd: usage.costUsd,
durationMs: undefined,
model: context.model,
}).catch((error) => {
logger.warn('Failed to store Codex LLM call in real-time', {
runId: context.input.runId,
call: context.llmCallCount,
error: String(error),
});
response: responseLine,
engineLabel: 'Codex',
});
}

Expand Down
24 changes: 8 additions & 16 deletions src/backends/opencode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
ToolPart,
} from '@opencode-ai/sdk/client';

import { storeLlmCall } from '../../db/repositories/runsRepository.js';
import { logger } from '../../utils/logging.js';
import { extractPRUrl } from '../../utils/prUrl.js';
import { OPENCODE_ENGINE_DEFINITION } from '../catalog.js';
Expand All @@ -28,6 +27,7 @@ import {
retryNativeToolOperation,
} from '../nativeToolRetry.js';
import { buildSystemPrompt, buildTaskPrompt } from '../nativeTools.js';
import { logLlmCall } from '../shared/llmCallLogger.js';
import type { AgentEngine, AgentEngineResult, AgentExecutionPlan } from '../types.js';
import { buildEnv } from './env.js';
import { DEFAULT_OPENCODE_MODEL } from './models.js';
Expand Down Expand Up @@ -276,24 +276,22 @@ function reportToolPart(
input.progressReporter.onToolCall(part.tool, part.state.input);
}

async function storeUsage(
function storeUsage(
input: AgentExecutionPlan,
model: string,
llmCallCount: number,
part: Extract<Part, { type: 'step-finish' }>,
): Promise<void> {
if (!input.runId) return;
await storeLlmCall({
): void {
logLlmCall({
runId: input.runId,
callNumber: llmCallCount,
request: undefined,
response: JSON.stringify(part),
model,
inputTokens: part.tokens.input,
outputTokens: part.tokens.output,
cachedTokens: part.tokens.cache.read,
costUsd: part.cost,
durationMs: undefined,
model,
response: JSON.stringify(part),
engineLabel: 'OpenCode',
});
}

Expand Down Expand Up @@ -402,13 +400,7 @@ async function handleMessagePartUpdated(
if (part.type === 'step-finish') {
state.llmCallCount += 1;
state.totalCost += part.cost;
await storeUsage(state.input, state.model, state.llmCallCount, part).catch((error) => {
logger.warn('Failed to store OpenCode LLM call in real-time', {
runId: state.input.runId,
call: state.llmCallCount,
error: String(error),
});
});
storeUsage(state.input, state.model, state.llmCallCount, part);
return;
}

Expand Down
53 changes: 53 additions & 0 deletions src/backends/shared/llmCallLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { storeLlmCall } from '../../db/repositories/runsRepository.js';
import { logger } from '../../utils/logging.js';

export interface LlmCallLogPayload {
/** The run ID. If undefined or empty, the call is a no-op. */
runId: string | undefined;
/** Sequential call number within the run. */
callNumber: number;
/** Model identifier string. */
model: string;
/** Number of input tokens consumed. */
inputTokens?: number;
/** Number of output tokens generated. */
outputTokens?: number;
/** Number of cached tokens (optional; some engines don't report this). */
cachedTokens?: number;
/** Cost in USD (optional; some engines don't report this). */
costUsd?: number;
/** Raw response payload to store (optional). */
response?: string;
/** Human-readable engine label used in warning logs (e.g. "Claude Code"). */
engineLabel: string;
}

/**
* Shared fire-and-forget helper for storing LLM call records.
*
* Guards on runId (no-op when absent), calls storeLlmCall asynchronously,
* and catches/logs any storage errors using the engine label for context.
* Returns void — callers do not need to await.
*/
export function logLlmCall(payload: LlmCallLogPayload): void {
if (!payload.runId) return;

storeLlmCall({
runId: payload.runId,
callNumber: payload.callNumber,
request: undefined,
response: payload.response,
inputTokens: payload.inputTokens,
outputTokens: payload.outputTokens,
cachedTokens: payload.cachedTokens,
costUsd: payload.costUsd,
durationMs: undefined,
model: payload.model,
}).catch((err) => {
logger.warn(`Failed to store ${payload.engineLabel} LLM call in real-time`, {
runId: payload.runId,
call: payload.callNumber,
error: String(err),
});
});
}
Loading
Loading