From 2fcb137ebcbf9101ca655804d0a61af2f222bbc5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 23 Jul 2026 19:05:40 +0200 Subject: [PATCH] fix: preserve unexpected provider finish reasons --- .changeset/quiet-ravens-finish.md | 7 ++++ packages/core/src/v1/session.ts | 1 + .../tests/unit/session-outcome.test.ts | 34 ++++++++++++++++++- .../src/components/shared/TurnOutcome.tsx | 10 +++++- .../webview-ui/src/context/session-outcome.ts | 15 ++++++-- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/it.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 1 + .../webview-ui/src/types/messages/parts.ts | 1 + packages/llm/src/schema/ids.ts | 10 +++++- .../src/kilocode/session/processor.ts | 10 +++++- packages/opencode/src/session/processor.ts | 2 ++ .../kilocode/session-generation-id.test.ts | 23 +++++++++++++ ...session-processor-empty-tool-calls.test.ts | 12 ++++++- packages/opencode/test/session/llm.test.ts | 4 +-- packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/sdk/openapi.json | 3 ++ 34 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 .changeset/quiet-ravens-finish.md create mode 100644 packages/opencode/test/kilocode/session-generation-id.test.ts diff --git a/.changeset/quiet-ravens-finish.md b/.changeset/quiet-ravens-finish.md new file mode 100644 index 0000000000..ad636e17d5 --- /dev/null +++ b/.changeset/quiet-ravens-finish.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": patch +"@kilocode/sdk": patch +"kilo-code": patch +--- + +Preserve unexpected provider finish reasons and show the Gateway generation ID when a response ends unexpectedly. diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index bb193febc0..df96b523a6 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -253,6 +253,7 @@ export const StepFinishPart = Schema.Struct({ modelID: ModelV2.ID, }), ), + generationID: Schema.optional(Schema.String), // kilocode_change metrics: Schema.optional( Schema.Struct({ prompt: Schema.optional(Schema.Finite), diff --git a/packages/kilo-vscode/tests/unit/session-outcome.test.ts b/packages/kilo-vscode/tests/unit/session-outcome.test.ts index 0a5dba04a0..27b99efb2d 100644 --- a/packages/kilo-vscode/tests/unit/session-outcome.test.ts +++ b/packages/kilo-vscode/tests/unit/session-outcome.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test" import { terminal } from "../../webview-ui/src/context/session-outcome" -import type { Message, TodoItem } from "../../webview-ui/src/types/messages" +import type { Message, Part, TodoItem } from "../../webview-ui/src/types/messages" function message(finish?: string, error?: Message["error"]): Message { return { @@ -70,6 +70,38 @@ describe("terminal", () => { expect(terminal({ reason: "completed", messages: [message("other")], todos: [] })?.kind).toBe("unexpected") }) + it("includes a gateway generation id for unexpected provider finishes", () => { + const parts: Part[] = [ + { + id: "p1", + sessionID: "s1", + messageID: "m1", + type: "step-finish", + reason: "other", + generationID: "gen_test", + }, + ] + expect( + terminal({ reason: "completed", messages: [message("other")], todos: [], parts: () => parts }), + ).toMatchObject({ kind: "unexpected", finish: "other", generationID: "gen_test" }) + }) + + it("does not expose generation ids for other terminal outcomes", () => { + const parts: Part[] = [ + { + id: "p1", + sessionID: "s1", + messageID: "m1", + type: "step-finish", + reason: "unknown", + generationID: "gen_test", + }, + ] + expect( + terminal({ reason: "completed", messages: [message("unknown")], todos: [], parts: () => parts }), + ).not.toHaveProperty("generationID") + }) + it("surfaces interruption and failures without a rendered error", () => { expect(terminal({ reason: "interrupted", messages: [message("stop")], todos: [todo("pending")] })).toEqual({ kind: "interrupted", 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 7d5e83aacd..06859ed37b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/TurnOutcome.tsx @@ -1,4 +1,4 @@ -import { Card } from "@kilocode/kilo-ui/card" +import { Card, CardDescription } from "@kilocode/kilo-ui/card" import { type Component, Show, createMemo } from "solid-js" import { useSession } from "../../context/session" import { terminal, type TerminalState } from "../../context/session-outcome" @@ -12,6 +12,7 @@ export const TurnOutcome: Component = () => { reason: session.closeReason(), messages: session.visibleMessages(), todos: session.todos(), + parts: session.getParts, hidden: session.isErrorHidden, }), ) @@ -37,6 +38,13 @@ export const TurnOutcome: Component = () => {
{label(value())}
{(id) => Request ID: {id()}} + + {(id) => ( + + {language.t("session.outcome.generationId", { 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 e261fbff33..661774a1fe 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts @@ -1,4 +1,4 @@ -import type { Message, SessionCloseReason, TodoItem } from "../types/messages" +import type { Message, Part, SessionCloseReason, TodoItem } from "../types/messages" type TerminalKind = "incomplete" | "limit" | "unknown" | "filtered" | "unexpected" | "interrupted" | "error" type TerminalTone = "warning" | "critical" @@ -8,6 +8,7 @@ export interface TerminalState { tone: TerminalTone finish?: string vercelID?: string + generationID?: string remaining: number } @@ -15,6 +16,7 @@ interface Input { reason?: SessionCloseReason messages: Message[] todos: TodoItem[] + parts?: (messageID: string) => Part[] hidden?: (id: string) => boolean } @@ -26,10 +28,19 @@ function vercelID(message: Message | undefined) { )?.[1] } +function generation(parts: Part[]) { + const part = parts.findLast( + (item): item is Extract => item.type === "step-finish" && item.reason === "other", + ) + return part?.generationID +} + export function terminal(input: Input): TerminalState | undefined { if (!input.reason) return undefined const last = input.messages[input.messages.length - 1] const finish = last?.role === "assistant" ? last.finish : undefined + const generationID = + finish === "other" && last?.role === "assistant" ? generation(input.parts?.(last.id) ?? []) : undefined const remaining = input.todos.filter((item) => item.status !== "completed" && item.status !== "cancelled").length if (input.reason === "interrupted") return { kind: "interrupted", tone: "warning", finish, remaining } @@ -40,6 +51,6 @@ export function terminal(input: Input): TerminalState | undefined { if (finish === "length") return { kind: "limit", 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 } + if (finish === "other") return { kind: "unexpected", tone: "warning", finish, generationID, remaining } return undefined } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index e2aa743f7e..0464f159fd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1282,6 +1282,7 @@ export const dict = { "session.outcome.unknown": "انتهى الدور بدون سبب إنهاء من النموذج", "session.outcome.filtered": "أوقف المزود هذه الاستجابة بسبب عامل تصفية المحتوى.", "session.outcome.unexpected": "انتهت الاستجابة بشكل غير متوقع وقد تكون غير مكتملة.", + "session.outcome.generationId": "معرّف التوليد: {{id}}", "session.outcome.interrupted": "تمت مقاطعة الدور", "session.outcome.error": "فشل الدور", "session.outcome.finish": "سبب الإنهاء: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index da2c4927b4..efda6ac53a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1308,6 +1308,7 @@ export const dict = { "session.outcome.unknown": "Turno encerrado sem um motivo de conclusão do modelo", "session.outcome.filtered": "O provedor interrompeu esta resposta devido a um filtro de conteúdo.", "session.outcome.unexpected": "A resposta terminou inesperadamente e pode estar incompleta.", + "session.outcome.generationId": "ID da geração: {{id}}", "session.outcome.interrupted": "Turno interrompido", "session.outcome.error": "Turno falhou", "session.outcome.finish": "Motivo da conclusão: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index d169455a52..c35edd0e04 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1311,6 +1311,7 @@ export const dict = { "session.outcome.unknown": "Potez završen bez razloga završetka modela", "session.outcome.filtered": "Provajder je zaustavio ovaj odgovor zbog filtera sadržaja.", "session.outcome.unexpected": "Odgovor je neočekivano završen i može biti nepotpun.", + "session.outcome.generationId": "ID generiranja: {{id}}", "session.outcome.interrupted": "Potez prekinut", "session.outcome.error": "Potez nije uspio", "session.outcome.finish": "Razlog završetka: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 90b32db89b..b6b55dd740 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1304,6 +1304,7 @@ export const dict = { "session.outcome.unknown": "Tur afsluttet uden en model-afslutningsårsag", "session.outcome.filtered": "Udbyderen stoppede dette svar på grund af et indholdsfilter.", "session.outcome.unexpected": "Svaret sluttede uventet og kan være ufuldstændigt.", + "session.outcome.generationId": "Genererings-id: {{id}}", "session.outcome.interrupted": "Tur afbrudt", "session.outcome.error": "Tur mislykkedes", "session.outcome.finish": "Afslutningsårsag: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 5ed17f2a8f..ac85e8f2f2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1325,6 +1325,7 @@ export const dict = { "session.outcome.unknown": "Zug ohne Modell-Abschlussgrund beendet", "session.outcome.filtered": "Der Anbieter hat diese Antwort aufgrund eines Inhaltsfilters gestoppt.", "session.outcome.unexpected": "Die Antwort wurde unerwartet beendet und ist möglicherweise unvollständig.", + "session.outcome.generationId": "Generierungs-ID: {{id}}", "session.outcome.interrupted": "Zug unterbrochen", "session.outcome.error": "Zug fehlgeschlagen", "session.outcome.finish": "Abschlussgrund: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 5a1970bdf4..28230db101 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1254,6 +1254,7 @@ export const dict = { "session.outcome.unknown": "Response ended without a finish reason and may be incomplete.", "session.outcome.filtered": "The provider stopped this response because of a content filter.", "session.outcome.unexpected": "Response ended unexpectedly and may be incomplete.", + "session.outcome.generationId": "Generation ID: {{id}}", "session.outcome.interrupted": "Turn interrupted.", "session.outcome.error": "Turn failed.", "session.outcome.finish": "Technical finish reason: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 7b2e1de34f..96355a1d99 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1318,6 +1318,7 @@ export const dict = { "session.outcome.unknown": "Turno finalizado sin motivo de finalización del modelo", "session.outcome.filtered": "El proveedor detuvo esta respuesta debido a un filtro de contenido.", "session.outcome.unexpected": "La respuesta terminó inesperadamente y puede estar incompleta.", + "session.outcome.generationId": "ID de generación: {{id}}", "session.outcome.interrupted": "Turno interrumpido", "session.outcome.error": "Turno fallido", "session.outcome.finish": "Motivo de finalización: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 2ad8dda23a..df4295a248 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1329,6 +1329,7 @@ export const dict = { "session.outcome.unknown": "Tour terminé sans motif de fin du modèle", "session.outcome.filtered": "Le fournisseur a arrêté cette réponse en raison d'un filtre de contenu.", "session.outcome.unexpected": "La réponse s'est terminée de manière inattendue et peut être incomplète.", + "session.outcome.generationId": "ID de génération : {{id}}", "session.outcome.interrupted": "Tour interrompu", "session.outcome.error": "Échec du tour", "session.outcome.finish": "Motif de fin : {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index db9b285121..4f35191d61 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1692,6 +1692,7 @@ export const dict = { "session.outcome.unknown": "La risposta è terminata senza un motivo di fine e potrebbe essere incompleta.", "session.outcome.filtered": "Il provider ha interrotto questa risposta a causa di un filtro contenuti.", "session.outcome.unexpected": "La risposta è terminata inaspettatamente e potrebbe essere incompleta.", + "session.outcome.generationId": "ID della generazione: {{id}}", "session.outcome.interrupted": "Turno interrotto.", "session.outcome.error": "Turno fallito.", "session.outcome.finish": "Motivo tecnico di fine: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index a1323f1904..ec67ba12e9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1299,6 +1299,7 @@ export const dict = { "session.outcome.unknown": "モデルの終了理由なしでターンが終了しました", "session.outcome.filtered": "コンテンツフィルターにより、プロバイダーがこの応答を停止しました。", "session.outcome.unexpected": "応答が予期せず終了したため、不完全である可能性があります。", + "session.outcome.generationId": "生成 ID: {{id}}", "session.outcome.interrupted": "ターンが中断されました", "session.outcome.error": "ターンが失敗しました", "session.outcome.finish": "終了理由: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 05974cf8de..d77213dfbe 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1295,6 +1295,7 @@ export const dict = { "session.outcome.unknown": "모델의 종료 이유 없이 턴이 종료되었습니다", "session.outcome.filtered": "콘텐츠 필터로 인해 제공자가 이 응답을 중단했습니다.", "session.outcome.unexpected": "응답이 예기치 않게 종료되었으며 불완전할 수 있습니다.", + "session.outcome.generationId": "생성 ID: {{id}}", "session.outcome.interrupted": "턴이 중단되었습니다", "session.outcome.error": "턴이 실패했습니다", "session.outcome.finish": "종료 이유: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index dbb142a269..2f62185fae 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1299,6 +1299,7 @@ export const dict = { "session.outcome.unknown": "Beurt beëindigd zonder voltooiingsreden van het model", "session.outcome.filtered": "De provider heeft deze reactie gestopt vanwege een contentfilter.", "session.outcome.unexpected": "Reactie is onverwacht beëindigd en is mogelijk onvolledig.", + "session.outcome.generationId": "Generatie-ID: {{id}}", "session.outcome.interrupted": "Beurt onderbroken", "session.outcome.error": "Beurt mislukt", "session.outcome.finish": "Voltooiingsreden: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index a018d431c7..f7dce12412 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1264,6 +1264,7 @@ export const dict = { "session.outcome.unknown": "Runden ble avsluttet uten avslutningsårsak fra modellen", "session.outcome.filtered": "Leverandøren stoppet denne responsen på grunn av et innholdsfilter.", "session.outcome.unexpected": "Responsen ble avsluttet uventet og kan være ufullstendig.", + "session.outcome.generationId": "Genererings-ID: {{id}}", "session.outcome.interrupted": "Runde avbrutt", "session.outcome.error": "Runden feilet", "session.outcome.finish": "Avslutningsårsak: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 9613d6fb4f..2191be0ae2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1263,6 +1263,7 @@ export const dict = { "session.outcome.unknown": "Tura zakończona bez powodu zakończenia od modelu", "session.outcome.filtered": "Dostawca zatrzymał tę odpowiedź z powodu filtru treści.", "session.outcome.unexpected": "Odpowiedź zakończyła się nieoczekiwanie i może być niekompletna.", + "session.outcome.generationId": "ID generowania: {{id}}", "session.outcome.interrupted": "Tura przerwana", "session.outcome.error": "Tura nie powiodła się", "session.outcome.finish": "Powód zakończenia: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 2f76ab2cd1..d4c8296549 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1310,6 +1310,7 @@ export const dict = { "session.outcome.unknown": "Раунд завершен без указания причины моделью", "session.outcome.filtered": "Провайдер остановил этот ответ из-за фильтра контента.", "session.outcome.unexpected": "Ответ неожиданно завершился и может быть неполным.", + "session.outcome.generationId": "Идентификатор генерации: {{id}}", "session.outcome.interrupted": "Раунд прерван", "session.outcome.error": "Раунд завершился с ошибкой", "session.outcome.finish": "Причина завершения: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index a865d89b21..a04602cd50 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1291,6 +1291,7 @@ export const dict = { "session.outcome.unknown": "เทิร์นสิ้นสุดโดยไม่มีเหตุผลการเสร็จสิ้นของโมเดล", "session.outcome.filtered": "ผู้ให้บริการหยุดการตอบกลับนี้เนื่องจากตัวกรองเนื้อหา", "session.outcome.unexpected": "การตอบกลับสิ้นสุดลงโดยไม่คาดคิดและอาจไม่สมบูรณ์", + "session.outcome.generationId": "รหัสการสร้าง: {{id}}", "session.outcome.interrupted": "เทิร์นถูกขัดจังหวะ", "session.outcome.error": "เทิร์นล้มเหลว", "session.outcome.finish": "เหตุผลการเสร็จสิ้น: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index ee59bf88a2..f8fe461bfa 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1295,6 +1295,7 @@ export const dict = { "session.outcome.unknown": "Tur, model bitiş nedeni olmadan sona erdi", "session.outcome.filtered": "Sağlayıcı, bir içerik filtresi nedeniyle bu yanıtı durdurdu.", "session.outcome.unexpected": "Yanıt beklenmedik bir şekilde sona erdi ve eksik olabilir.", + "session.outcome.generationId": "Oluşturma kimliği: {{id}}", "session.outcome.interrupted": "Tur kesintiye uğradı", "session.outcome.error": "Tur başarısız oldu", "session.outcome.finish": "Bitiş nedeni: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 87919d1b26..4cafd81b05 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1292,6 +1292,7 @@ export const dict = { "session.outcome.unknown": "Хід завершився без причини завершення моделі", "session.outcome.filtered": "Провайдер зупинив цю відповідь через фільтр вмісту.", "session.outcome.unexpected": "Відповідь завершилася несподівано і може бути неповною.", + "session.outcome.generationId": "Ідентифікатор генерації: {{id}}", "session.outcome.interrupted": "Хід перервано", "session.outcome.error": "Хід не вдався", "session.outcome.finish": "Причина завершення: {{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index ab7370f660..e79ce14c71 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1267,6 +1267,7 @@ export const dict = { "session.outcome.unknown": "回合已结束,模型未提供结束原因", "session.outcome.filtered": "提供商因为内容过滤器停止了此响应。", "session.outcome.unexpected": "响应意外结束,可能不完整。", + "session.outcome.generationId": "生成 ID:{{id}}", "session.outcome.interrupted": "回合已中断", "session.outcome.error": "回合失败", "session.outcome.finish": "结束原因:{{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 9db47684c6..1dfa7e9713 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1228,6 +1228,7 @@ export const dict = { "session.outcome.unknown": "回合已結束,模型未提供結束原因", "session.outcome.filtered": "提供商因為內容過濾器停止了此回應。", "session.outcome.unexpected": "回應意外結束,可能不完整。", + "session.outcome.generationId": "生成 ID:{{id}}", "session.outcome.interrupted": "回合已中斷", "session.outcome.error": "回合失敗", "session.outcome.finish": "結束原因:{{reason}}", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts b/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts index 00624cba3a..ef8581d2f1 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts @@ -98,6 +98,7 @@ export interface StepFinishPart extends BasePart { providerID: string modelID: string } + generationID?: string cost?: number tokens?: { input: number diff --git a/packages/llm/src/schema/ids.ts b/packages/llm/src/schema/ids.ts index 61289aa9d0..9d98a676e6 100644 --- a/packages/llm/src/schema/ids.ts +++ b/packages/llm/src/schema/ids.ts @@ -33,7 +33,15 @@ export type TextVerbosity = Schema.Schema.Type export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"]) export type MessageRole = Schema.Schema.Type -export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"]) +export const FinishReason = Schema.Literals([ + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "other", // kilocode_change - preserve the AI SDK's unexpected provider finish reason + "unknown", +]) export type FinishReason = Schema.Schema.Type export const JsonSchema = Schema.Record(Schema.String, Schema.Unknown) diff --git a/packages/opencode/src/kilocode/session/processor.ts b/packages/opencode/src/kilocode/session/processor.ts index 2906c0dfeb..69882cfd50 100644 --- a/packages/opencode/src/kilocode/session/processor.ts +++ b/packages/opencode/src/kilocode/session/processor.ts @@ -10,7 +10,7 @@ import * as Log from "@opencode-ai/core/util/log" import { Cause, Effect, Exit } from "effect" import { Flag } from "@opencode-ai/core/flag/flag" import { EffectBridge } from "@/effect/bridge" -import type { LLMEvent, Usage } from "@opencode-ai/llm" +import type { LLMEvent, ProviderMetadata, Usage } from "@opencode-ai/llm" import type { ProviderV2 } from "@opencode-ai/core/provider" import { SessionRetry } from "@/session/retry" import { computeMetrics as computeMetricsHelper, type TokenRates } from "@/kilocode/session/metrics" @@ -137,6 +137,14 @@ export namespace KiloSessionProcessor { /** Returned shape for downstream consumers that prefer the namespace. */ export type Metrics = TokenRates + export function generationID(meta: ProviderMetadata | undefined) { + const value = meta?.gateway?.generationId + if (typeof value !== "string") return + const id = value.trim() + if (!/^gen_[A-Za-z0-9_-]{1,200}$/.test(id)) return + return id + } + /** * Effect-based offline handler for the retry schedule. * Shows offline status, waits for network reconnection or user rejection. diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index c20599959a..703a4de1b4 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -869,6 +869,7 @@ export const layer = Layer.effect( modelID: ctx.model.id, selected: ctx.assistantMessage.modelID, }) + const generationID = KiloSessionProcessor.generationID(value.providerMetadata) // kilocode_change end // kilocode_change start - guard against finish-step without start-step: // ctx.stepStart is 0 until `start-step` fires, which would feed a @@ -922,6 +923,7 @@ export const layer = Layer.effect( type: "step-finish", time: { start: startDate, end: endDate, elapsed: elapsedMs }, // kilocode_change ...(model ? { model } : {}), // kilocode_change + ...(generationID ? { generationID } : {}), // kilocode_change ...(metrics ? { metrics } : {}), // kilocode_change tokens: usage.tokens, cost: usage.cost, diff --git a/packages/opencode/test/kilocode/session-generation-id.test.ts b/packages/opencode/test/kilocode/session-generation-id.test.ts new file mode 100644 index 0000000000..dd86751648 --- /dev/null +++ b/packages/opencode/test/kilocode/session-generation-id.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "bun:test" +import { KiloSessionProcessor } from "../../src/kilocode/session/processor" + +describe("session generation id", () => { + test("extracts a bounded Gateway generation id", () => { + expect( + KiloSessionProcessor.generationID({ + gateway: { + generationId: " gen_test-123 ", + routing: { finalProvider: "novita" }, + marketCost: "0.1", + }, + }), + ).toBe("gen_test-123") + }) + + test("rejects arbitrary or oversized metadata values", () => { + expect(KiloSessionProcessor.generationID({ gateway: { generationId: "request-secret" } })).toBeUndefined() + expect(KiloSessionProcessor.generationID({ gateway: { generationId: `gen_${"a".repeat(201)}` } })).toBeUndefined() + expect(KiloSessionProcessor.generationID({ gateway: { generationId: 42 } })).toBeUndefined() + expect(KiloSessionProcessor.generationID({ openai: { responseId: "gen_response" } })).toBeUndefined() + }) +}) diff --git a/packages/opencode/test/kilocode/session-processor-empty-tool-calls.test.ts b/packages/opencode/test/kilocode/session-processor-empty-tool-calls.test.ts index 3c53f10bdd..040d8e2f4d 100644 --- a/packages/opencode/test/kilocode/session-processor-empty-tool-calls.test.ts +++ b/packages/opencode/test/kilocode/session-processor-empty-tool-calls.test.ts @@ -577,7 +577,14 @@ describe("session processor empty tool-calls", () => { index: 0, reason: "stop", usage: usage(), - providerMetadata: { kilocode: { routedModelID: "openai/gpt-5.5-20260423" } }, + providerMetadata: { + kilocode: { routedModelID: "openai/gpt-5.5-20260423" }, + gateway: { + generationId: "gen_test", + routing: { finalProvider: "openai" }, + marketCost: "0.1", + }, + }, }), LLMEvent.finish({ reason: "stop", usage: usage() }), ) @@ -632,6 +639,9 @@ describe("session processor empty tool-calls", () => { providerID: selection.providerID, modelID: ModelV2.ID.make("openai/gpt-5.5-20260423"), }) + expect(part?.generationID).toBe("gen_test") + expect(part).not.toHaveProperty("providerMetadata") + expect(part).not.toHaveProperty("gateway") }), { git: true }, ), diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index b985628f91..7c2df36e61 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -281,7 +281,7 @@ describe("session.llm.ai-sdk adapter", () => { { type: "step-finish", index: 0, - reason: "unknown", + reason: "other", // kilocode_change usage: { inputTokens: 10, outputTokens: 5, @@ -294,7 +294,7 @@ describe("session.llm.ai-sdk adapter", () => { }, { type: "finish", - reason: "unknown", + reason: "other", // kilocode_change usage: { inputTokens: 11, outputTokens: 6, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 4de7a0bc8c..476eeb7cc0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -782,6 +782,7 @@ export type StepFinishPart = { providerID: string modelID: string } + generationID?: string metrics?: { prompt?: number generation?: number diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 950e77e38e..593481f655 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -26075,6 +26075,9 @@ "required": ["providerID", "modelID"], "additionalProperties": false }, + "generationID": { + "type": "string" + }, "metrics": { "type": "object", "properties": {