ktx/packages/context/src/llm/debug-request-recorder.ts

132 lines
3.7 KiB
TypeScript
Raw Normal View History

2026-05-10 23:12:26 +02:00
import { appendFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import type { ModelMessage } from 'ai';
2026-05-10 23:51:24 +02:00
import type { KtxModelRole } from '@ktx/llm';
2026-05-10 23:12:26 +02:00
type ProviderOptionsCarrier = { providerOptions?: unknown; [key: string]: unknown };
type ToolMap = Record<string, ProviderOptionsCarrier>;
2026-05-10 23:51:24 +02:00
export interface KtxLlmDebugProviderOptionsEntry {
2026-05-10 23:12:26 +02:00
target: 'message' | 'message-part' | 'tool';
index?: number;
role?: string;
partIndex?: number;
name?: string;
providerOptions: unknown;
}
2026-05-10 23:51:24 +02:00
export interface KtxLlmDebugRequest {
2026-05-10 23:12:26 +02:00
timestamp: string;
operationName: string;
source?: string;
jobId?: string;
unitKey?: string;
2026-05-10 23:51:24 +02:00
modelRole: KtxModelRole;
2026-05-10 23:12:26 +02:00
modelId: string;
messageCount: number;
toolNames: string[];
2026-05-10 23:51:24 +02:00
providerOptions: KtxLlmDebugProviderOptionsEntry[];
2026-05-10 23:12:26 +02:00
}
2026-05-10 23:51:24 +02:00
export interface KtxLlmDebugRequestRecorder {
record(request: KtxLlmDebugRequest): Promise<void> | void;
2026-05-10 23:12:26 +02:00
}
2026-05-10 23:51:24 +02:00
export interface SummarizeKtxLlmDebugRequestInput {
2026-05-10 23:12:26 +02:00
operationName: string;
source?: string;
jobId?: string;
unitKey?: string;
2026-05-10 23:51:24 +02:00
modelRole: KtxModelRole;
2026-05-10 23:12:26 +02:00
modelId: string;
messages: ModelMessage[];
tools: ToolMap;
timestamp?: string;
}
function messageRole(message: ModelMessage): string {
return typeof message.role === 'string' ? message.role : 'unknown';
}
function isProviderOptionsCarrier(value: unknown): value is ProviderOptionsCarrier {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
2026-05-10 23:51:24 +02:00
function contentPartProviderOptions(message: ModelMessage, index: number): KtxLlmDebugProviderOptionsEntry[] {
2026-05-10 23:12:26 +02:00
if (!Array.isArray(message.content)) {
return [];
}
return message.content.flatMap((part, partIndex) => {
if (!isProviderOptionsCarrier(part) || !part.providerOptions) {
return [];
}
return [
{
target: 'message-part' as const,
index,
role: messageRole(message),
partIndex,
providerOptions: part.providerOptions,
},
];
});
}
2026-05-10 23:51:24 +02:00
function messageProviderOptions(messages: ModelMessage[]): KtxLlmDebugProviderOptionsEntry[] {
2026-05-10 23:12:26 +02:00
return messages.flatMap((message, index) => {
2026-05-10 23:51:24 +02:00
const entries: KtxLlmDebugProviderOptionsEntry[] = [];
2026-05-10 23:12:26 +02:00
const providerOptions = (message as ProviderOptionsCarrier).providerOptions;
if (providerOptions) {
entries.push({
target: 'message',
index,
role: messageRole(message),
providerOptions,
});
}
entries.push(...contentPartProviderOptions(message, index));
return entries;
});
}
2026-05-10 23:51:24 +02:00
function toolProviderOptions(tools: ToolMap): KtxLlmDebugProviderOptionsEntry[] {
2026-05-10 23:12:26 +02:00
return Object.entries(tools).flatMap(([name, tool]) => {
return tool.providerOptions
? [
{
target: 'tool' as const,
name,
providerOptions: tool.providerOptions,
},
]
: [];
});
}
2026-05-10 23:51:24 +02:00
export function summarizeKtxLlmDebugRequest(input: SummarizeKtxLlmDebugRequestInput): KtxLlmDebugRequest {
2026-05-10 23:12:26 +02:00
const toolNames = Object.keys(input.tools).sort();
return {
timestamp: input.timestamp ?? new Date().toISOString(),
operationName: input.operationName,
...(input.source ? { source: input.source } : {}),
...(input.jobId ? { jobId: input.jobId } : {}),
...(input.unitKey ? { unitKey: input.unitKey } : {}),
modelRole: input.modelRole,
modelId: input.modelId,
messageCount: input.messages.length,
toolNames,
providerOptions: [...messageProviderOptions(input.messages), ...toolProviderOptions(input.tools)],
};
}
2026-05-10 23:51:24 +02:00
export function createJsonlKtxLlmDebugRequestRecorder(filePath: string): KtxLlmDebugRequestRecorder {
2026-05-10 23:12:26 +02:00
return {
async record(request) {
await mkdir(dirname(filePath), { recursive: true });
await appendFile(filePath, `${JSON.stringify(request)}\n`, 'utf8');
},
};
}