Skip to content

Commit abd17c4

Browse files
authored
feat(litellm): forward taskId as X-Zoo-Session-ID request header (#591)
* feat(litellm): forward taskId as X-Zoo-Session-ID request header Closes: #590 LiteLLM recognizes X-<vendor>-Session-ID headers for per-conversation request correlation in logs and spend tracking. This follows the same convention used by Claude Code (x-claude-code-session-id) and GitHub Copilot (x-copilot-session-id). The header is injected only when metadata.taskId is present, passed as the second argument to client.chat.completions.create() alongside the existing .withResponse() pattern so no existing tests need to change. * docs(litellm): clarify X-Zoo-Session-ID header distinction from Zoo gateways * test(litellm): add empty-string taskId case for X-Zoo-Session-ID header guard
1 parent cac7d0d commit abd17c4

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"zoo-code": patch
3+
---
4+
5+
Forward the active task ID to the LiteLLM proxy as an `X-Zoo-Session-ID` request header so individual conversations can be correlated in LiteLLM logs and spend tracking. The header is only sent when a task ID is present, and follows the `x-<vendor>-session-id` convention used by Claude Code (`x-claude-code-session-id`) and GitHub Copilot (`x-copilot-session-id`).

src/api/providers/__tests__/lite-llm.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,4 +1117,61 @@ describe("LiteLLMHandler", () => {
11171117
expect(id1).not.toBe(id2)
11181118
})
11191119
})
1120+
1121+
describe("session ID header", () => {
1122+
const mockStream = {
1123+
async *[Symbol.asyncIterator]() {
1124+
yield {
1125+
choices: [{ delta: { content: "ok" } }],
1126+
usage: { prompt_tokens: 1, completion_tokens: 1 },
1127+
}
1128+
},
1129+
}
1130+
1131+
it("should send the X-Zoo-Session-ID header when a taskId is provided", async () => {
1132+
mockCreate.mockReturnValue({
1133+
withResponse: vi.fn().mockResolvedValue({ data: mockStream }),
1134+
})
1135+
1136+
const generator = handler.createMessage("system", [{ role: "user", content: "hi" }], {
1137+
taskId: "task-123",
1138+
})
1139+
for await (const _chunk of generator) {
1140+
// drain the stream
1141+
}
1142+
1143+
const requestHeaders = mockCreate.mock.calls[0][1]?.headers
1144+
expect(requestHeaders).toMatchObject({ "X-Zoo-Session-ID": "task-123" })
1145+
})
1146+
1147+
it("should not send the X-Zoo-Session-ID header when no taskId is provided", async () => {
1148+
mockCreate.mockReturnValue({
1149+
withResponse: vi.fn().mockResolvedValue({ data: mockStream }),
1150+
})
1151+
1152+
const generator = handler.createMessage("system", [{ role: "user", content: "hi" }])
1153+
for await (const _chunk of generator) {
1154+
// drain the stream
1155+
}
1156+
1157+
const requestHeaders = mockCreate.mock.calls[0][1]?.headers
1158+
expect(requestHeaders).not.toHaveProperty("X-Zoo-Session-ID")
1159+
})
1160+
1161+
it("should not send the X-Zoo-Session-ID header when taskId is an empty string", async () => {
1162+
mockCreate.mockReturnValue({
1163+
withResponse: vi.fn().mockResolvedValue({ data: mockStream }),
1164+
})
1165+
1166+
const generator = handler.createMessage("system", [{ role: "user", content: "hi" }], {
1167+
taskId: "",
1168+
})
1169+
for await (const _chunk of generator) {
1170+
// drain the stream
1171+
}
1172+
1173+
const requestHeaders = mockCreate.mock.calls[0][1]?.headers
1174+
expect(requestHeaders).not.toHaveProperty("X-Zoo-Session-ID")
1175+
})
1176+
})
11201177
})

src/api/providers/lite-llm.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,22 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa
223223
requestOptions.temperature = this.options.modelTemperature ?? 0
224224
}
225225

226+
// LiteLLM recognizes X-<vendor>-Session-ID for per-conversation request correlation.
227+
// This header enables LiteLLM to group related API calls by task for logging and tracing.
228+
// Unlike Zoo gateways (which use X-Zoo-Task-ID to correlate requests across multiple
229+
// models within a single conversation), this header is specific to the LiteLLM provider
230+
// and facilitates provider-level logging and debugging on LiteLLM's admin panel.
231+
// Matches the convention used by Claude Code (x-claude-code-session-id) and
232+
// GitHub Copilot (x-copilot-session-id).
233+
const requestHeaders: Record<string, string> = {}
234+
if (metadata?.taskId) {
235+
requestHeaders["X-Zoo-Session-ID"] = metadata.taskId
236+
}
237+
226238
try {
227-
const { data: completion } = await this.client.chat.completions.create(requestOptions).withResponse()
239+
const { data: completion } = await this.client.chat.completions
240+
.create(requestOptions, { headers: requestHeaders })
241+
.withResponse()
228242

229243
let lastUsage
230244

0 commit comments

Comments
 (0)