diff --git a/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts b/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts index 7186b62af90..3db274340e0 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts @@ -14,6 +14,6 @@ export function mapOpenAICompatibleFinishReason( case "tool_calls": return "tool-calls" default: - return "other" + return "unknown" as any // kilocode_change } } diff --git a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts index 280970c41b4..a11554d402f 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts @@ -345,6 +345,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { unified: "other", raw: undefined, } + let receivedFinishReason = false // kilocode_change const usage: { completionTokens: number | undefined completionTokensDetails: { @@ -453,6 +454,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { const choice = value.choices[0] if (choice?.finish_reason != null) { + receivedFinishReason = true // kilocode_change finishReason = { unified: mapOpenAICompatibleFinishReason(choice.finish_reason), raw: choice.finish_reason ?? undefined, @@ -645,6 +647,12 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { }, flush(controller) { + // kilocode_change start + if (!receivedFinishReason && !isFirstChunk) { + finishReason = { unified: "unknown" as any, raw: undefined } + } + // kilocode_change end + if (isActiveReasoning) { controller.enqueue({ type: "reasoning-end", diff --git a/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts b/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts index 4f443b511ba..03ee7c504bd 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts @@ -17,6 +17,6 @@ export function mapOpenAIResponseFinishReason({ case "content_filter": return "content-filter" default: - return hasFunctionCall ? "tool-calls" : "other" + return (hasFunctionCall ? "tool-calls" : "unknown") as any // kilocode_change } } diff --git a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts index 250d1f6f340..49486d6af2a 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts @@ -803,6 +803,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { unified: "other", raw: undefined, } + let receivedFinishChunk = false // kilocode_change const usage: { inputTokens: number | undefined outputTokens: number | undefined @@ -1259,6 +1260,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { }) } } else if (isResponseFinishedChunk(value)) { + receivedFinishChunk = true // kilocode_change finishReason = { unified: mapOpenAIResponseFinishReason({ finishReason: value.response.incomplete_details?.reason, @@ -1305,6 +1307,12 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { currentTextId = null } + // kilocode_change start + if (!receivedFinishChunk && responseId !== null) { + finishReason = { unified: "unknown" as any, raw: undefined } + } + // kilocode_change end + const providerMetadata: SharedV3ProviderMetadata = { openai: { responseId, diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 0687f166cc8..f79a1bae47e 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1164,6 +1164,10 @@ export function fromError( ).toObject() case OutputLengthError.isInstance(e): return e + // kilocode_change start + case APIError.isInstance(e): + return e.toObject() + // kilocode_change end case LoadAPIKeyError.isInstance(e): return new AuthError( { diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 67f3418e9b0..6c51df70556 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -625,6 +625,20 @@ export const layer: Layer.Layer< Stream.takeUntil(() => ctx.needsCompaction), Stream.runDrain, ) + + // kilocode_change start + if (ctx.assistantMessage.finish === "unknown") { + const parts = MessageV2.parts(ctx.assistantMessage.id) + if (!parts.some((p) => p.type === "tool" && p.state.status === "completed")) { + yield* Effect.fail( + new MessageV2.APIError({ + message: "Stream terminated prematurely without a finish reason", + isRetryable: true, + }) + ) + } + } + // kilocode_change end }).pipe( Effect.onInterrupt(() => Effect.gen(function* () { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index e43cac7a26b..58587df59c7 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1592,7 +1592,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the return "break" as const } - const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish) + const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish) // kilocode_change if (finished && !handle.message.error) { if (format.type === "json_schema") { handle.message.error = new MessageV2.StructuredOutputError({