From c25f041eb3922defc4dadb9ad7b2f8c8edb74fbd Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Thu, 23 Jul 2026 12:14:46 +0200 Subject: [PATCH 1/3] fix: show Vercel ID for incomplete responses --- .changeset/show-vercel-response-id.md | 6 +++ .../tests/unit/session-outcome.test.ts | 22 +++++++++++ .../src/components/shared/TurnOutcome.tsx | 7 +++- .../webview-ui/src/context/session-outcome.ts | 11 +++++- .../src/kilocode/session/processor.ts | 13 ++++--- .../src/kilocode/session/response-metadata.ts | 17 ++++++++ packages/opencode/src/session/llm/ai-sdk.ts | 8 +++- packages/opencode/src/session/processor.ts | 5 ++- ...rocessor-incomplete-response-retry.test.ts | 12 +++--- .../session-response-metadata.test.ts | 39 +++++++++++++++++++ 10 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 .changeset/show-vercel-response-id.md create mode 100644 packages/opencode/src/kilocode/session/response-metadata.ts create mode 100644 packages/opencode/test/kilocode/session-response-metadata.test.ts diff --git a/.changeset/show-vercel-response-id.md b/.changeset/show-vercel-response-id.md new file mode 100644 index 00000000000..315d6fe6eae --- /dev/null +++ b/.changeset/show-vercel-response-id.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Show the `x-vercel-id` response header when a model response ends without a finish reason. diff --git a/packages/kilo-vscode/tests/unit/session-outcome.test.ts b/packages/kilo-vscode/tests/unit/session-outcome.test.ts index ec30837e93b..0a5dba04a0a 100644 --- a/packages/kilo-vscode/tests/unit/session-outcome.test.ts +++ b/packages/kilo-vscode/tests/unit/session-outcome.test.ts @@ -43,6 +43,28 @@ describe("terminal", () => { expect(terminal({ reason: "completed", messages: [message("unknown")], todos: [] })?.kind).toBe("unknown") }) + it("includes the Vercel response ID for an unknown finish", () => { + expect( + terminal({ + reason: "completed", + messages: [ + message("unknown", { + name: "APIError", + data: { responseHeaders: { "X-Vercel-Id": "fra1::abc" } }, + }), + ], + todos: [], + hidden: () => true, + }), + ).toEqual({ + kind: "unknown", + tone: "warning", + finish: "unknown", + remaining: 0, + vercelID: "fra1::abc", + }) + }) + it("surfaces filtered and unexpected provider finishes", () => { expect(terminal({ reason: "completed", messages: [message("content-filter")], todos: [] })?.kind).toBe("filtered") expect(terminal({ reason: "completed", messages: [message("other")], todos: [] })?.kind).toBe("unexpected") diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx index 9bacd25fb00..f25d218fcd4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx @@ -34,7 +34,12 @@ export const TurnOutcome: Component = () => { role="status" title={value().finish ? language.t("session.outcome.finish", { reason: value().finish! }) : undefined} > - {label(value())} + +
{label(value())}
+ + {(id) => x-vercel-id: {id()}} + +
)} diff --git a/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts b/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts index dcf8f26faf4..e261fbff33f 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts @@ -7,6 +7,7 @@ export interface TerminalState { kind: TerminalKind tone: TerminalTone finish?: string + vercelID?: string remaining: number } @@ -17,6 +18,14 @@ interface Input { hidden?: (id: string) => boolean } +function vercelID(message: Message | undefined) { + const headers = message?.error?.data?.responseHeaders + if (!headers || typeof headers !== "object" || Array.isArray(headers)) return + return Object.entries(headers).find( + ([name, value]) => name.toLowerCase() === "x-vercel-id" && typeof value === "string", + )?.[1] +} + export function terminal(input: Input): TerminalState | undefined { if (!input.reason) return undefined const last = input.messages[input.messages.length - 1] @@ -29,7 +38,7 @@ export function terminal(input: Input): TerminalState | undefined { return { kind: "error", tone: "critical", finish, remaining } } if (finish === "length") return { kind: "limit", tone: "warning", finish, remaining } - if (finish === "unknown") return { kind: "unknown", tone: "warning", finish, remaining } + if (finish === "unknown") return { kind: "unknown", tone: "warning", finish, remaining, vercelID: vercelID(last) } if (finish === "content-filter") return { kind: "filtered", tone: "warning", finish, remaining } if (finish === "other") return { kind: "unexpected", tone: "warning", finish, remaining } return undefined diff --git a/packages/opencode/src/kilocode/session/processor.ts b/packages/opencode/src/kilocode/session/processor.ts index 92f8e95e9cc..2906c0dfebf 100644 --- a/packages/opencode/src/kilocode/session/processor.ts +++ b/packages/opencode/src/kilocode/session/processor.ts @@ -28,7 +28,7 @@ export namespace KiloSessionProcessor { export const INCOMPLETE_RESPONSE_MESSAGE = "The provider repeatedly ended the response before returning usable output." export class IncompleteResponseError extends Error { - constructor() { + constructor(readonly vercelID?: string) { super(INCOMPLETE_RESPONSE_MESSAGE) this.name = "IncompleteResponseError" } @@ -270,13 +270,13 @@ export namespace KiloSessionProcessor { return Effect.gen(function* () { for (const index of Array.from({ length: INCOMPLETE_RESPONSE_RETRIES + 1 }, (_, index) => index)) { const result = yield* input.run().pipe(Effect.exit) - if (Exit.isFailure(result)) { - const error = Cause.squash(result.cause) - if (!(error instanceof IncompleteResponseError)) return yield* Effect.fail(error) - } else if (!input.replayable()) return + const error = Exit.isFailure(result) ? Cause.squash(result.cause) : undefined + if (error && !(error instanceof IncompleteResponseError)) return yield* Effect.fail(error) + if (!error && !input.replayable()) return yield* input.discard() - if (index === INCOMPLETE_RESPONSE_RETRIES) return yield* Effect.fail(new IncompleteResponseError()) + if (index === INCOMPLETE_RESPONSE_RETRIES) + return yield* Effect.fail(error ?? new IncompleteResponseError()) const wait = SessionRetry.delay(index + 1) yield* input.set({ attempt: index + 1, message: INCOMPLETE_RESPONSE_MESSAGE, next: Date.now() + wait }) yield* Effect.sleep(`${wait} millis`) @@ -289,6 +289,7 @@ export namespace KiloSessionProcessor { return new MessageV2.APIError({ message: error.message, isRetryable: true, + responseHeaders: error.vercelID ? { "x-vercel-id": error.vercelID } : undefined, }).toObject() } diff --git a/packages/opencode/src/kilocode/session/response-metadata.ts b/packages/opencode/src/kilocode/session/response-metadata.ts new file mode 100644 index 00000000000..cf7b174af14 --- /dev/null +++ b/packages/opencode/src/kilocode/session/response-metadata.ts @@ -0,0 +1,17 @@ +import type { ProviderMetadata } from "@opencode-ai/llm" +import { isRecord } from "@/util/record" + +export namespace KiloResponseMetadata { + export function write(metadata: ProviderMetadata | undefined, headers: Record | undefined) { + const id = Object.entries(headers ?? {}).find(([name]) => name.toLowerCase() === "x-vercel-id")?.[1] + if (!id) return metadata + const kilo = isRecord(metadata?.kilo) ? metadata.kilo : {} + return { ...metadata, kilo: { ...kilo, vercelID: id } } + } + + export function read(metadata: ProviderMetadata | undefined) { + const kilo = metadata?.kilo + if (!isRecord(kilo)) return + return typeof kilo.vercelID === "string" ? kilo.vercelID : undefined + } +} diff --git a/packages/opencode/src/session/llm/ai-sdk.ts b/packages/opencode/src/session/llm/ai-sdk.ts index b806bdbbe20..ddfd3447429 100644 --- a/packages/opencode/src/session/llm/ai-sdk.ts +++ b/packages/opencode/src/session/llm/ai-sdk.ts @@ -3,6 +3,7 @@ import { Effect, Schema } from "effect" import { type streamText } from "ai" import { errorMessage } from "@/util/error" import { KiloRoutedModel } from "@/kilocode/session/routed-model" // kilocode_change +import { KiloResponseMetadata } from "@/kilocode/session/response-metadata" // kilocode_change type Result = Awaited> type AISDKEvent = Result["fullStream"] extends AsyncIterable ? T : never @@ -106,7 +107,12 @@ export function toLLMEvents( index: state.step++, reason: finishReason(event.finishReason), usage: usage(event.usage), - providerMetadata: KiloRoutedModel.write(metadata, event.response?.modelId), // kilocode_change + // kilocode_change start + providerMetadata: KiloResponseMetadata.write( + KiloRoutedModel.write(metadata, event.response?.modelId), + event.response?.headers, + ), + // kilocode_change end }), ] }) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 2e8e4bb72f6..c20599959af 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -24,6 +24,7 @@ import { Question } from "@/question" import { KiloSessionProcessor, type ReviewTelemetry } from "@/kilocode/session/processor" import { KiloSessionOverflow } from "@/kilocode/session/overflow" import { KiloRoutedModel } from "@/kilocode/session/routed-model" +import { KiloResponseMetadata } from "@/kilocode/session/response-metadata" import { Suggestion } from "@/kilocode/suggestion" // kilocode_change end import { errorMessage } from "@/util/error" @@ -845,7 +846,9 @@ export const layer = Layer.effect( usage: attempt.usage, }) ) - return yield* Effect.fail(new KiloSessionProcessor.IncompleteResponseError()) + return yield* Effect.fail( + new KiloSessionProcessor.IncompleteResponseError(KiloResponseMetadata.read(value.providerMetadata)), + ) // kilocode_change end // kilocode_change start - pass turn context for slow-snapshot UI/policy handling const completedSnapshot = yield* snapshot.track({ diff --git a/packages/opencode/test/kilocode/session-processor-incomplete-response-retry.test.ts b/packages/opencode/test/kilocode/session-processor-incomplete-response-retry.test.ts index 8344c03f0f0..cfb8f3de051 100644 --- a/packages/opencode/test/kilocode/session-processor-incomplete-response-retry.test.ts +++ b/packages/opencode/test/kilocode/session-processor-incomplete-response-retry.test.ts @@ -72,13 +72,14 @@ function model(): Provider.Model { } as Provider.Model } -function empty() { +function empty(vercelID?: string) { const usage = new Usage({}) + const providerMetadata = vercelID ? { kilo: { vercelID } } : undefined return [ LLMEvent.stepStart({ index: 0 }), LLMEvent.reasoningStart({ id: "reasoning" }), LLMEvent.reasoningEnd({ id: "reasoning" }), - LLMEvent.stepFinish({ index: 0, reason: "unknown", usage }), + LLMEvent.stepFinish({ index: 0, reason: "unknown", usage, providerMetadata }), LLMEvent.finish({ reason: "unknown", usage }), ] } @@ -242,9 +243,9 @@ describe("session processor incomplete response retry", () => { (dir) => Effect.gen(function* () { const ctx = yield* setup(dir) - yield* ctx.test.reply(...empty()) - yield* ctx.test.reply(...empty()) - yield* ctx.test.reply(...empty()) + yield* ctx.test.reply(...empty("attempt-1")) + yield* ctx.test.reply(...empty("attempt-2")) + yield* ctx.test.reply(...empty("final-id")) yield* ctx.test.push(Stream.fail(new Error("unexpected extra llm call"))) const delay = spyOn(SessionRetry, "delay").mockReturnValue(0) @@ -260,6 +261,7 @@ describe("session processor incomplete response retry", () => { expect(MessageV2.APIError.isInstance(error)).toBe(true) if (!MessageV2.APIError.isInstance(error)) throw new Error("expected API error") expect(error.data.message).toBe(KiloSessionProcessor.INCOMPLETE_RESPONSE_MESSAGE) + expect(error.data.responseHeaders?.["x-vercel-id"]).toBe("final-id") expect(yield* MessageV2.parts(ctx.msg.id)).toEqual([]) }), { git: true }, diff --git a/packages/opencode/test/kilocode/session-response-metadata.test.ts b/packages/opencode/test/kilocode/session-response-metadata.test.ts new file mode 100644 index 00000000000..1c76274b824 --- /dev/null +++ b/packages/opencode/test/kilocode/session-response-metadata.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { LLMAISDK } from "@/session/llm/ai-sdk" +import { KiloResponseMetadata } from "@/kilocode/session/response-metadata" + +describe("session response metadata", () => { + test("carries x-vercel-id from an AI SDK response", async () => { + const events = await Effect.runPromise( + LLMAISDK.toLLMEvents(LLMAISDK.adapterState(), { + type: "finish-step", + response: { + id: "response-1", + timestamp: new Date(0), + modelId: "gpt-test", + headers: { "X-Vercel-Id": "fra1::abc" }, + }, + finishReason: "other", + rawFinishReason: undefined, + providerMetadata: undefined, + usage: { + inputTokens: 1, + outputTokens: 0, + totalTokens: 1, + inputTokenDetails: { noCacheTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 }, + outputTokenDetails: { textTokens: 0, reasoningTokens: 0 }, + }, + }), + ) + + expect(events).toHaveLength(1) + const event = events[0] + if (event?.type !== "step-finish") throw new Error("expected step-finish") + expect(KiloResponseMetadata.read(event.providerMetadata)).toBe("fra1::abc") + }) + + test("does not add metadata when the header is absent", () => { + expect(KiloResponseMetadata.write(undefined, { server: "vercel" })).toBeUndefined() + }) +}) From 804242b04becb813df4de143d40286276c21f1ca Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Thu, 23 Jul 2026 12:23:03 +0200 Subject: [PATCH 2/3] fix(vscode): label Vercel ID as request ID --- .changeset/show-vercel-response-id.md | 2 +- .../webview-ui/src/components/shared/TurnOutcome.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/show-vercel-response-id.md b/.changeset/show-vercel-response-id.md index 315d6fe6eae..80947e8b866 100644 --- a/.changeset/show-vercel-response-id.md +++ b/.changeset/show-vercel-response-id.md @@ -3,4 +3,4 @@ "kilo-code": patch --- -Show the `x-vercel-id` response header when a model response ends without a finish reason. +Show the request ID when a model response ends without a finish reason. diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx index f25d218fcd4..2bbd4059818 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx @@ -37,7 +37,7 @@ export const TurnOutcome: Component = () => {
{label(value())}
- {(id) => x-vercel-id: {id()}} + {(id) => Request ID: {id()}}
From d4250199fbadb4c9e9ccc189496cbc6d8d7c04e3 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Thu, 23 Jul 2026 12:35:26 +0200 Subject: [PATCH 3/3] chore(vscode): format request ID label --- .../webview-ui/src/components/shared/TurnOutcome.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx index 2bbd4059818..7d5e83aacd4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx @@ -36,9 +36,7 @@ export const TurnOutcome: Component = () => { >
{label(value())}
- - {(id) => Request ID: {id()}} - + {(id) => Request ID: {id()}}
)}