Merge pull request #12488 from Kilo-Org/fix/show-vercel-response-id

fix: show Vercel ID for incomplete responses
This commit is contained in:
Christiaan Arnoldus
2026-07-23 14:29:12 +02:00
committed by GitHub
10 changed files with 123 additions and 15 deletions
@@ -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()
}
@@ -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<string, string> | 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
}
}
+7 -1
View File
@@ -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<ReturnType<typeof streamText>>
type AISDKEvent = Result["fullStream"] extends AsyncIterable<infer T> ? 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
}),
]
})
+4 -1
View File
@@ -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({
@@ -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 },
@@ -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()
})
})