feat: add central llm runtime logging
This commit is contained in:
+82
-6
@@ -6,6 +6,7 @@ export interface GenerateInput {
|
||||
prompt: string;
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
task?: string;
|
||||
}
|
||||
|
||||
export interface GenerateValidatedJsonInput<T> extends GenerateInput {
|
||||
@@ -51,6 +52,51 @@ export function isLlmConfigured() {
|
||||
return getLlmProviderStatus().configured;
|
||||
}
|
||||
|
||||
interface ChatCompletionResult {
|
||||
choices: Array<{ message: { content?: string | null } }>;
|
||||
}
|
||||
|
||||
type ChatCompletionRequest = {
|
||||
model: string;
|
||||
temperature: number;
|
||||
response_format?: { type: "json_object" };
|
||||
messages: Array<{ role: "system" | "user"; content: string }>;
|
||||
};
|
||||
|
||||
let chatCompletionForTesting:
|
||||
| ((request: ChatCompletionRequest) => Promise<ChatCompletionResult>)
|
||||
| null = null;
|
||||
|
||||
export function setChatCompletionForTesting(
|
||||
handler: ((request: ChatCompletionRequest) => Promise<ChatCompletionResult>) | null,
|
||||
) {
|
||||
chatCompletionForTesting = handler;
|
||||
}
|
||||
|
||||
function getTask(input: GenerateInput) {
|
||||
return input.task?.trim() || "unknown";
|
||||
}
|
||||
|
||||
function truncateRaw(value: string, maxLength = 4000) {
|
||||
return value.length > maxLength
|
||||
? `${value.slice(0, maxLength)}...[truncated ${value.length - maxLength} chars]`
|
||||
: value;
|
||||
}
|
||||
|
||||
function summarizeZodError(error: z.ZodError) {
|
||||
return error.issues
|
||||
.slice(0, 5)
|
||||
.map((issue) => {
|
||||
const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
||||
return `${path}: ${issue.message}`;
|
||||
})
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
function quoteLogValue(value: string) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function createClient() {
|
||||
const status = getLlmProviderStatus();
|
||||
|
||||
@@ -93,21 +139,40 @@ export async function generateText(input: GenerateInput) {
|
||||
}
|
||||
|
||||
export async function generateJson<T>(input: GenerateInput): Promise<T> {
|
||||
const task = getTask(input);
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const { client, model } = createClient();
|
||||
const response = await client.chat.completions.create({
|
||||
model: input.model ?? model,
|
||||
const status = getLlmProviderStatus();
|
||||
if (!status.configured) {
|
||||
throw new Error(status.reason ?? "LLM provider is not configured");
|
||||
}
|
||||
const effectiveModel = input.model ?? status.model;
|
||||
console.info(
|
||||
`[llm:start] provider=${status.provider} model=${effectiveModel} task=${task}`,
|
||||
);
|
||||
const request = {
|
||||
model: effectiveModel,
|
||||
temperature: input.temperature ?? 0.1,
|
||||
response_format: { type: "json_object" },
|
||||
messages: [
|
||||
...(input.system ? [{ role: "system" as const, content: input.system }] : []),
|
||||
{ role: "user" as const, content: input.prompt },
|
||||
],
|
||||
});
|
||||
} satisfies ChatCompletionRequest;
|
||||
const response = chatCompletionForTesting
|
||||
? await chatCompletionForTesting(request)
|
||||
: await createClient().client.chat.completions.create(request);
|
||||
const content = response.choices[0]?.message.content ?? "{}";
|
||||
console.info(
|
||||
`[llm:response] task=${task} duration_ms=${Date.now() - startedAt} raw=${truncateRaw(content)}`,
|
||||
);
|
||||
return JSON.parse(content) as T;
|
||||
} catch (error) {
|
||||
throw normalizeLlmError(error);
|
||||
const normalized = normalizeLlmError(error);
|
||||
console.error(
|
||||
`[llm:error] task=${task} duration_ms=${Date.now() - startedAt} message=${quoteLogValue(normalized.message)}`,
|
||||
);
|
||||
throw normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,14 +189,25 @@ export async function generateValidatedJson<T>({
|
||||
schema,
|
||||
...input
|
||||
}: GenerateValidatedJsonInput<T>): Promise<T | null> {
|
||||
const task = getTask(input);
|
||||
if (!isLlmConfigured()) {
|
||||
console.info(`[llm:validated] task=${task} ok=false reason=not_configured`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const generated = await generateJsonForValidation<unknown>(input);
|
||||
return schema.parse(generated);
|
||||
const parsed = schema.safeParse(generated);
|
||||
if (parsed.success) {
|
||||
console.info(`[llm:validated] task=${task} ok=true`);
|
||||
return parsed.data;
|
||||
}
|
||||
console.warn(
|
||||
`[llm:validated] task=${task} ok=false zod_error=${quoteLogValue(summarizeZodError(parsed.error))}`,
|
||||
);
|
||||
return null;
|
||||
} catch {
|
||||
console.info(`[llm:validated] task=${task} ok=false reason=provider_error`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user