mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #12488 from Kilo-Org/fix/show-vercel-response-id
fix: show Vercel ID for incomplete responses
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user