mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-28 08:49:38 +02:00
feat: add codex llm backend
This commit is contained in:
parent
21744fc520
commit
64b8a416fe
28 changed files with 1462 additions and 14 deletions
144
packages/cli/src/context/llm/codex-exec-events.ts
Normal file
144
packages/cli/src/context/llm/codex-exec-events.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import type { LlmTokenUsage, RunLoopStopReason } from './runtime-port.js';
|
||||
|
||||
export interface CodexExecEventSummary {
|
||||
finalText: string;
|
||||
stopReason: RunLoopStopReason;
|
||||
usage: LlmTokenUsage;
|
||||
stepCount: number;
|
||||
stepBoundariesMs: number[];
|
||||
toolCallCount: number;
|
||||
toolFailures: string[];
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
interface CodexEventParseOptions {
|
||||
startedAt?: number;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === 'object' ? (value as Record<string, unknown>) : undefined;
|
||||
}
|
||||
|
||||
function text(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number | undefined {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function usageFrom(value: unknown): LlmTokenUsage {
|
||||
const usage = record(value);
|
||||
if (!usage) {
|
||||
return {};
|
||||
}
|
||||
const inputTokens = numberValue(usage.input_tokens ?? usage.inputTokens);
|
||||
const outputTokens = numberValue(usage.output_tokens ?? usage.outputTokens);
|
||||
const totalTokens = numberValue(usage.total_tokens ?? usage.totalTokens);
|
||||
return {
|
||||
...(inputTokens !== undefined ? { inputTokens } : {}),
|
||||
...(outputTokens !== undefined ? { outputTokens } : {}),
|
||||
...(totalTokens !== undefined ? { totalTokens } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function stopReasonFrom(value: unknown): RunLoopStopReason {
|
||||
const reason = text(value)?.toLowerCase();
|
||||
if (reason && /(budget|max_turn|max-turn|limit)/.test(reason)) {
|
||||
return 'budget';
|
||||
}
|
||||
return 'natural';
|
||||
}
|
||||
|
||||
function errorMessageFrom(value: unknown): string {
|
||||
if (value instanceof Error) {
|
||||
return value.message;
|
||||
}
|
||||
const asRecord = record(value);
|
||||
const message = text(asRecord?.message);
|
||||
return message ?? text(value) ?? 'Codex turn failed';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function parseCodexExecEventLine(line: string): unknown {
|
||||
try {
|
||||
return JSON.parse(line) as unknown;
|
||||
} catch (error) {
|
||||
throw new Error(`Codex JSONL event stream was malformed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function summarizeCodexExecEvents(
|
||||
events: Iterable<unknown>,
|
||||
options: CodexEventParseOptions = {},
|
||||
): CodexExecEventSummary {
|
||||
const startedAt = options.startedAt ?? Date.now();
|
||||
const now = options.now ?? Date.now;
|
||||
let finalText = '';
|
||||
let stopReason: RunLoopStopReason = 'natural';
|
||||
let usage: LlmTokenUsage = {};
|
||||
let stepCount = 0;
|
||||
const stepBoundariesMs: number[] = [];
|
||||
let toolCallCount = 0;
|
||||
const toolFailures: string[] = [];
|
||||
let error: Error | undefined;
|
||||
|
||||
for (const event of events) {
|
||||
const eventRecord = record(event);
|
||||
const eventType = text(eventRecord?.type);
|
||||
if (!eventRecord || !eventType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === 'turn.started') {
|
||||
stepCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === 'turn.completed') {
|
||||
usage = usageFrom(eventRecord.usage);
|
||||
stepBoundariesMs.push(now() - startedAt);
|
||||
stopReason = stopReasonFrom(eventRecord.reason ?? eventRecord.stop_reason ?? eventRecord.terminal_reason);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === 'turn.failed' || eventType === 'error') {
|
||||
stopReason = 'error';
|
||||
error = new Error(errorMessageFrom(eventRecord.error ?? eventRecord.message));
|
||||
continue;
|
||||
}
|
||||
|
||||
const item = record(eventRecord.item);
|
||||
const itemType = text(item?.type);
|
||||
if (!item || !itemType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === 'item.completed' && itemType === 'agent_message') {
|
||||
finalText = text(item.text) ?? finalText;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === 'item.started' && itemType === 'mcp_tool_call') {
|
||||
toolCallCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === 'item.completed' && itemType === 'mcp_tool_call' && item.error !== undefined) {
|
||||
const name = text(item.name) ?? text(item.tool_name) ?? 'unknown';
|
||||
toolFailures.push(`${name}: ${errorMessageFrom(item.error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
finalText,
|
||||
stopReason,
|
||||
usage,
|
||||
stepCount,
|
||||
stepBoundariesMs,
|
||||
toolCallCount,
|
||||
toolFailures,
|
||||
...(error ? { error } : {}),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue