Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5738a54
refactor: queue chat logs in middleware
steebchen Mar 29, 2026
9d8f7ea
fix: avoid chat log stream deadlock
steebchen Mar 29, 2026
4059a40
fix: flush non-stream chat logs
steebchen Mar 29, 2026
13ba95e
refactor: drop insertLog alias
steebchen Mar 30, 2026
4ff9139
Merge branch 'main' into chat-log-post-hook
steebchen Mar 30, 2026
35cb4f5
Merge remote-tracking branch 'origin/main' into chat-log-post-hook
steebchen Mar 30, 2026
e28767c
fix: handle streaming terminal events
steebchen Mar 30, 2026
cb19ace
Revert "fix: handle streaming terminal events"
steebchen Mar 30, 2026
c8bb0b6
chore: sync branch with origin/main
steebchen Mar 30, 2026
fa1c4b5
chore: sync branch with latest origin/main
steebchen Mar 30, 2026
0d9f9d0
chore: sync main into chat log hook
steebchen Apr 5, 2026
b9c865f
chore(autofix): apply diff
steebchen Apr 5, 2026
4ecd857
fix: avoid duplicate client error logs
steebchen Apr 5, 2026
b852c51
fix: warn on client errors in activity
steebchen Apr 5, 2026
313d1dd
chore: sync main into branch
steebchen Apr 5, 2026
d6cac67
fix: log validation client errors
steebchen Apr 5, 2026
a664ea4
fix: lazily parse fallback body
steebchen Apr 5, 2026
30e90ab
chore(autofix): apply diff
steebchen Apr 5, 2026
9939752
chore: sync main into branch
steebchen Apr 7, 2026
8a3349c
chore: sync main into branch
steebchen Apr 8, 2026
f4a7425
Merge remote-tracking branch 'origin/main' into chat-log-post-hook
steebchen Apr 9, 2026
cb0a263
chore: sync main into branch
steebchen Apr 11, 2026
c8aba78
chore: sync main into branch
steebchen Apr 20, 2026
7ea7dd8
chore: sync main into branch
steebchen Apr 22, 2026
e6c4e69
fix: add dataStorageCost to cancelled streaming cost stub
steebchen Apr 22, 2026
54e3806
fix: port retry/routing features into log middleware
steebchen Apr 26, 2026
d9b5c26
Merge remote-tracking branch 'origin/main' into chat-log-post-hook
steebchen Apr 26, 2026
1e4a1ff
Merge remote-tracking branch 'origin/main' into chat-log-post-hook
steebchen Apr 28, 2026
48060a1
Merge remote-tracking branch 'origin/main' into chat-log-post-hook
steebchen Apr 30, 2026
46ff515
Merge remote-tracking branch 'origin/main' into chat-log-post-hook
steebchen May 4, 2026
ac0b2d1
Merge remote-tracking branch 'origin/main' into chat-log-post-hook
steebchen May 6, 2026
2106177
merge: resolve conflicts with origin/main
steebchen-bot May 7, 2026
001081b
Merge remote-tracking branch 'origin/main' into chat-log-post-hook
steebchen May 7, 2026
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
6 changes: 6 additions & 0 deletions apps/gateway/src/api-individual.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ describe("e2e individual tests", () => {
expect((log.errorDetails as { message?: string })?.message).toContain(
"the word 'json'",
);

const matchingLogs = await db
.select()
.from(tables.log)
.where(eq(tables.log.requestId, requestId));
expect(matchingLogs).toHaveLength(1);
},
);

Expand Down
96 changes: 94 additions & 2 deletions apps/gateway/src/api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";

import { db, tables } from "@llmgateway/db";
import { db, eq, tables } from "@llmgateway/db";
import { logger } from "@llmgateway/logger";

import { app } from "./app.js";
Expand All @@ -10,7 +10,12 @@ import {
resetKeyHealth,
} from "./lib/api-key-health.js";
import { createGatewayApiTestHarness } from "./test-utils/gateway-api-test-harness.js";
import { readAll, waitForLogs } from "./test-utils/test-helpers.js";
import {
readAll,
processPendingLogs,
waitForLogByRequestId,
waitForLogs,
} from "./test-utils/test-helpers.js";

describe("api", () => {
const harness = createGatewayApiTestHarness({
Expand Down Expand Up @@ -1616,6 +1621,7 @@ describe("api", () => {
});

test("Reasoning effort error for unsupported model", async () => {
const requestId = "reasoning-effort-unsupported-request-id";
await db.insert(tables.apiKey).values({
id: "token-id",
token: "real-token",
Expand All @@ -1628,6 +1634,7 @@ describe("api", () => {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-request-id": requestId,
Authorization: `Bearer real-token`,
},
body: JSON.stringify({
Expand All @@ -1646,6 +1653,70 @@ describe("api", () => {

const json = await res.json();
expect(json.message).toContain("does not support reasoning");

const log = await waitForLogByRequestId(requestId);
expect(log.finishReason).toBe("client_error");
expect(log.unifiedFinishReason).toBe("client_error");

const matchingLogs = await db
.select()
.from(tables.log)
.where(eq(tables.log.requestId, requestId));
expect(matchingLogs).toHaveLength(1);
});

test("Schema validation errors are logged as client_error", async () => {
const requestId = "schema-validation-client-error-request-id";
await db.insert(tables.apiKey).values({
id: "token-id-schema-validation",
token: "real-token-schema-validation",
projectId: "project-id",
description: "Test API Key",
createdBy: "user-id",
});

const res = await app.request("/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-request-id": requestId,
Authorization: "Bearer real-token-schema-validation",
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [
{
role: "user",
content: 5555,
},
],
}),
});

expect(res.status).toBe(400);

const json = await res.json();
expect(json.success).toBe(false);
expect(JSON.stringify(json)).toContain("invalid_union");

const log = await waitForLogByRequestId(requestId);
expect(log.finishReason).toBe("client_error");
expect(log.unifiedFinishReason).toBe("client_error");
expect(log.errorDetails?.statusCode).toBe(400);
expect(log.errorDetails?.responseText).toContain("invalid_union");
expect(log.errorDetails?.responseText).toContain("messages");
expect(log.messages).toEqual([
{
role: "user",
content: 5555,
},
]);

const matchingLogs = await db
.select()
.from(tables.log)
.where(eq(tables.log.requestId, requestId));
expect(matchingLogs).toHaveLength(1);
});

test("Max tokens validation error when exceeding model limit", async () => {
Expand Down Expand Up @@ -1802,10 +1873,12 @@ describe("api", () => {

// test for missing Authorization header
test("/v1/chat/completions missing Authorization header", async () => {
const requestId = "missing-auth-request-id";
const res = await app.request("/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-request-id": requestId,
// Intentionally not setting Authorization header
},
body: JSON.stringify({
Expand All @@ -1819,6 +1892,13 @@ describe("api", () => {
}),
});
expect(res.status).toBe(401);

await processPendingLogs();
const logs = await db
.select()
.from(tables.log)
.where(eq(tables.log.requestId, requestId));
expect(logs).toHaveLength(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

// test for explicitly specifying a provider in the format "provider/model"
Expand Down Expand Up @@ -1954,6 +2034,7 @@ describe("api", () => {

// test for missing provider API key
test("/v1/chat/completions with missing provider API key", async () => {
const requestId = "missing-provider-key-request-id";
await db.insert(tables.apiKey).values({
id: "token-id",
token: "real-token",
Expand All @@ -1966,6 +2047,7 @@ describe("api", () => {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-request-id": requestId,
Authorization: `Bearer real-token`,
},
body: JSON.stringify({
Expand All @@ -1983,6 +2065,16 @@ describe("api", () => {
expect(errorMessage).toMatchInlineSnapshot(
`"{"error":true,"status":400,"message":"No API key set for provider: openai. Please add a provider key in your settings or add credits and switch to credits or hybrid mode."}"`,
);

const log = await waitForLogByRequestId(requestId);
expect(log.finishReason).toBe("client_error");
expect(log.unifiedFinishReason).toBe("client_error");

const matchingLogs = await db
.select()
.from(tables.log)
.where(eq(tables.log.requestId, requestId));
expect(matchingLogs).toHaveLength(1);
});

// test for provider error response and error logging
Expand Down
Loading
Loading