feat: add central llm runtime logging
This commit is contained in:
@@ -7,12 +7,12 @@ describe("generateValidatedJson", () => {
|
||||
const originalProvider = process.env.LLM_PROVIDER;
|
||||
const originalDeepSeekKey = process.env.DEEPSEEK_API_KEY;
|
||||
|
||||
afterEach(() => {
|
||||
process.env.LLM_PROVIDER = originalProvider;
|
||||
process.env.DEEPSEEK_API_KEY = originalDeepSeekKey;
|
||||
client.setGenerateJsonForValidation(client.generateJson);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
process.env.LLM_PROVIDER = originalProvider;
|
||||
process.env.DEEPSEEK_API_KEY = originalDeepSeekKey;
|
||||
client.setGenerateJsonForValidation(client.generateJson);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns null when no provider key is configured", async () => {
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
@@ -26,10 +26,10 @@ describe("generateValidatedJson", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns parsed data when the model response matches the schema", async () => {
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
client.setGenerateJsonForValidation(async () => ({ value: "from-llm" }));
|
||||
it("returns parsed data when the model response matches the schema", async () => {
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
client.setGenerateJsonForValidation(async () => ({ value: "from-llm" }));
|
||||
|
||||
const result = await client.generateValidatedJson({
|
||||
schema: z.object({ value: z.string() }),
|
||||
@@ -39,10 +39,10 @@ describe("generateValidatedJson", () => {
|
||||
expect(result).toEqual({ value: "from-llm" });
|
||||
});
|
||||
|
||||
it("returns null when the model response fails schema validation", async () => {
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
client.setGenerateJsonForValidation(async () => ({ value: 42 }));
|
||||
it("returns null when the model response fails schema validation", async () => {
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
client.setGenerateJsonForValidation(async () => ({ value: 42 }));
|
||||
|
||||
const result = await client.generateValidatedJson({
|
||||
schema: z.object({ value: z.string() }),
|
||||
@@ -52,12 +52,12 @@ describe("generateValidatedJson", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when the provider call rejects", async () => {
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
client.setGenerateJsonForValidation(async () => {
|
||||
throw new Error("provider down");
|
||||
});
|
||||
it("returns null when the provider call rejects", async () => {
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
client.setGenerateJsonForValidation(async () => {
|
||||
throw new Error("provider down");
|
||||
});
|
||||
|
||||
const result = await client.generateValidatedJson({
|
||||
schema: z.object({ value: z.string() }),
|
||||
@@ -66,4 +66,104 @@ describe("generateValidatedJson", () => {
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("logs validation success with the supplied task label", async () => {
|
||||
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
client.setGenerateJsonForValidation(async () => ({ value: "from-llm" }));
|
||||
|
||||
const result = await client.generateValidatedJson({
|
||||
schema: z.object({ value: z.string() }),
|
||||
prompt: "Return JSON.",
|
||||
task: "article_optimizer",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ value: "from-llm" });
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
"[llm:validated] task=article_optimizer ok=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("logs validation failure with a compact Zod summary", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
client.setGenerateJsonForValidation(async () => ({ value: 42 }));
|
||||
|
||||
const result = await client.generateValidatedJson({
|
||||
schema: z.object({ value: z.string() }),
|
||||
prompt: "Return JSON.",
|
||||
task: "fact_extractor",
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[llm:validated] task=fact_extractor ok=false"),
|
||||
);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("value"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateJson logging", () => {
|
||||
const originalProvider = process.env.LLM_PROVIDER;
|
||||
const originalDeepSeekKey = process.env.DEEPSEEK_API_KEY;
|
||||
|
||||
afterEach(() => {
|
||||
process.env.LLM_PROVIDER = originalProvider;
|
||||
process.env.DEEPSEEK_API_KEY = originalDeepSeekKey;
|
||||
client.setChatCompletionForTesting(null);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("logs provider, model, task, duration, and truncated raw response", async () => {
|
||||
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "test-key";
|
||||
const longContent = `{"value":"${"x".repeat(4100)}"}`;
|
||||
client.setChatCompletionForTesting(async () => ({
|
||||
choices: [{ message: { content: longContent } }],
|
||||
}));
|
||||
|
||||
const result = await client.generateJson<{ value: string }>({
|
||||
prompt: "Return JSON.",
|
||||
task: "article_optimizer",
|
||||
});
|
||||
|
||||
expect(result.value).toHaveLength(4100);
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
"[llm:start] provider=deepseek model=deepseek-v4-pro task=article_optimizer",
|
||||
);
|
||||
const responseLog = infoSpy.mock.calls
|
||||
.map((call) => call[0])
|
||||
.find((line) => line.startsWith("[llm:response]"));
|
||||
expect(responseLog).toContain("task=article_optimizer");
|
||||
expect(responseLog).toContain("duration_ms=");
|
||||
expect(responseLog).toContain("raw=");
|
||||
expect(responseLog?.length).toBeLessThan(4200);
|
||||
});
|
||||
|
||||
it("logs provider errors without leaking API keys", async () => {
|
||||
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
process.env.LLM_PROVIDER = "deepseek";
|
||||
process.env.DEEPSEEK_API_KEY = "super-secret-key";
|
||||
client.setChatCompletionForTesting(async () => {
|
||||
throw new Error("upstream unavailable");
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.generateJson({
|
||||
prompt: "Return JSON.",
|
||||
task: "quality_inspector",
|
||||
}),
|
||||
).rejects.toThrow("LLM provider error: upstream unavailable");
|
||||
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
"[llm:start] provider=deepseek model=deepseek-v4-pro task=quality_inspector",
|
||||
);
|
||||
const errorLog = errorSpy.mock.calls.map((call) => call[0]).join("\n");
|
||||
expect(errorLog).toContain("[llm:error] task=quality_inspector");
|
||||
expect(errorLog).not.toContain("super-secret-key");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user