diff --git a/.changeset/show-vercel-response-id.md b/.changeset/show-vercel-response-id.md
new file mode 100644
index 00000000000..80947e8b866
--- /dev/null
+++ b/.changeset/show-vercel-response-id.md
@@ -0,0 +1,6 @@
+---
+"@kilocode/cli": patch
+"kilo-code": patch
+---
+
+Show the request ID 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..7d5e83aacd4 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,10 @@ export const TurnOutcome: Component = () => {
role="status"
title={value().finish ? language.t("session.outcome.finish", { reason: value().finish! }) : undefined}
>
- {label(value())}
+
+ {label(value())}
+ {(id) => Request 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()
+ })
+})