Skip to content

Commit 35c610c

Browse files
authored
fix(core): align Copilot response continuation (#41452)
1 parent 072ad47 commit 35c610c

6 files changed

Lines changed: 191 additions & 144 deletions

File tree

packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export async function convertToOpenAIResponsesInput({
127127
input.push({
128128
role: "assistant",
129129
content: [{ type: "output_text", text: part.text }],
130-
id: (part.providerOptions?.copilot?.itemId as string) ?? undefined,
130+
id: store ? ((part.providerOptions?.copilot?.itemId as string) ?? undefined) : undefined,
131131
})
132132
break
133133
}
@@ -143,7 +143,7 @@ export async function convertToOpenAIResponsesInput({
143143
input.push({
144144
type: "local_shell_call",
145145
call_id: part.toolCallId,
146-
id: (part.providerOptions?.copilot?.itemId as string) ?? undefined,
146+
id: store ? ((part.providerOptions?.copilot?.itemId as string) ?? undefined) : undefined,
147147
action: {
148148
type: "exec",
149149
command: parsedInput.action.command,
@@ -162,7 +162,7 @@ export async function convertToOpenAIResponsesInput({
162162
call_id: part.toolCallId,
163163
name: part.toolName,
164164
arguments: JSON.stringify(part.input),
165-
id: (part.providerOptions?.copilot?.itemId as string) ?? undefined,
165+
id: store ? ((part.providerOptions?.copilot?.itemId as string) ?? undefined) : undefined,
166166
})
167167
break
168168
}
@@ -206,50 +206,20 @@ export async function convertToOpenAIResponsesInput({
206206
summary: [],
207207
}
208208
}
209-
} else {
210-
const summaryParts: Array<{
211-
type: "summary_text"
212-
text: string
213-
}> = []
214-
215-
if (part.text.length > 0) {
216-
summaryParts.push({
217-
type: "summary_text",
218-
text: part.text,
219-
})
220-
} else if (reasoningMessage !== undefined) {
221-
warnings.push({
222-
type: "other",
223-
message: `Cannot append empty reasoning part to existing reasoning sequence. Skipping reasoning part: ${JSON.stringify(part)}.`,
224-
})
225-
}
226-
227-
if (reasoningMessage === undefined) {
228-
reasoningMessages[reasoningId] = {
229-
type: "reasoning",
230-
id: reasoningId,
231-
encrypted_content: providerOptions?.reasoningEncryptedContent,
232-
summary: summaryParts,
233-
}
234-
input.push(reasoningMessages[reasoningId])
235-
} else {
236-
reasoningMessage.summary.push(...summaryParts)
209+
} else if (providerOptions?.reasoningEncryptedContent != null && reasoningMessage === undefined) {
210+
reasoningMessages[reasoningId] = {
211+
type: "reasoning",
212+
id: reasoningId,
213+
encrypted_content: providerOptions.reasoningEncryptedContent,
214+
summary: [],
237215
}
216+
input.push(reasoningMessages[reasoningId])
238217
}
239218
} else {
240-
const encryptedContent = providerOptions?.reasoningEncryptedContent
241-
if (encryptedContent != null) {
242-
input.push({
243-
type: "reasoning",
244-
encrypted_content: encryptedContent,
245-
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
246-
})
247-
} else {
248-
warnings.push({
249-
type: "other",
250-
message: `Non-OpenAI reasoning parts are not supported. Skipping reasoning part: ${JSON.stringify(part)}.`,
251-
})
252-
}
219+
warnings.push({
220+
type: "other",
221+
message: `Non-OpenAI reasoning parts are not supported. Skipping reasoning part: ${JSON.stringify(part)}.`,
222+
})
253223
}
254224
break
255225
}

packages/core/src/github-copilot/responses/openai-responses-api-types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export type OpenAIResponsesComputerCall = {
7171

7272
export type OpenAIResponsesLocalShellCall = {
7373
type: "local_shell_call"
74-
id: string
74+
id?: string
7575
call_id: string
7676
action: {
7777
type: "exec"
@@ -205,7 +205,7 @@ export type OpenAIResponsesTool =
205205

206206
export type OpenAIResponsesReasoning = {
207207
type: "reasoning"
208-
id?: string
208+
id: string
209209
encrypted_content?: string | null
210210
summary: Array<{
211211
type: "summary_text"

packages/core/src/github-copilot/responses/openai-responses-language-model.ts

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,13 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
198198
providerOptions,
199199
schema: openaiResponsesProviderOptionsSchema,
200200
})
201+
const store = openaiOptions?.store ?? false
201202

202203
const { input, warnings: inputWarnings } = await convertToOpenAIResponsesInput({
203204
prompt,
204205
systemMessageMode: modelConfig.systemMessageMode,
205206
fileIdPrefixes: this.config.fileIdPrefixes,
206-
store: openaiOptions?.store ?? true,
207+
store,
207208
hasLocalShellTool: hasOpenAITool("openai.local_shell"),
208209
})
209210

@@ -214,9 +215,12 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
214215
let include: OpenAIResponsesIncludeOptions = openaiOptions?.include
215216

216217
function addInclude(key: OpenAIResponsesIncludeValue) {
218+
if (include?.includes(key)) return
217219
include = include != null ? [...include, key] : [key]
218220
}
219221

222+
addInclude("reasoning.encrypted_content")
223+
220224
function hasOpenAITool(id: string) {
221225
return tools?.find((tool) => tool.type === "provider" && tool.id === id) != null
222226
}
@@ -282,7 +286,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
282286
metadata: openaiOptions?.metadata,
283287
parallel_tool_calls: openaiOptions?.parallelToolCalls,
284288
previous_response_id: openaiOptions?.previousResponseId,
285-
store: openaiOptions?.store,
289+
store,
286290
user: openaiOptions?.user,
287291
instructions: openaiOptions?.instructions,
288292
service_tier: openaiOptions?.serviceTier,
@@ -840,7 +844,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
840844
{
841845
canonicalId: string // the item.id from output_item.added
842846
encryptedContent?: string | null
843-
summaryParts: number[]
847+
summaryParts: Record<number, "active" | "can-conclude" | "concluded">
844848
}
845849
> = {}
846850

@@ -960,11 +964,14 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
960964
},
961965
})
962966
} else if (isResponseOutputItemAddedReasoningChunk(value)) {
963-
if (activeReasoning[value.output_index]) return
967+
if (activeReasoning[value.output_index]) {
968+
currentReasoningOutputIndex = value.output_index
969+
return
970+
}
964971
activeReasoning[value.output_index] = {
965972
canonicalId: value.item.id,
966973
encryptedContent: value.item.encrypted_content,
967-
summaryParts: [0],
974+
summaryParts: { 0: "active" },
968975
}
969976
currentReasoningOutputIndex = value.output_index
970977

@@ -1118,14 +1125,14 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
11181125
} else if (isResponseOutputItemDoneReasoningChunk(value)) {
11191126
const activeReasoningPart = activeReasoning[value.output_index]
11201127
if (activeReasoningPart) {
1121-
const summaryIndex = activeReasoningPart.summaryParts.at(-1)
1122-
if (summaryIndex !== undefined) {
1128+
for (const [summaryIndex, status] of Object.entries(activeReasoningPart.summaryParts)) {
1129+
if (status === "concluded") continue
11231130
controller.enqueue({
11241131
type: "reasoning-end",
11251132
id: `${activeReasoningPart.canonicalId}:${summaryIndex}`,
11261133
providerMetadata: {
11271134
copilot: {
1128-
itemId: activeReasoningPart.canonicalId,
1135+
itemId: value.item.id,
11291136
reasoningEncryptedContent: value.item.encrypted_content ?? null,
11301137
},
11311138
},
@@ -1230,18 +1237,19 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
12301237
currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null
12311238

12321239
// the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk.
1233-
if (activeItem && value.summary_index > 0 && !activeItem.summaryParts.includes(value.summary_index)) {
1234-
const previousSummaryIndex = activeItem.summaryParts.at(-1)
1235-
if (previousSummaryIndex !== undefined) {
1240+
if (activeItem && value.summary_index > 0 && activeItem.summaryParts[value.summary_index] === undefined) {
1241+
for (const [summaryIndex, status] of Object.entries(activeItem.summaryParts)) {
1242+
if (status !== "can-conclude") continue
12361243
controller.enqueue({
12371244
type: "reasoning-end",
1238-
id: `${activeItem.canonicalId}:${previousSummaryIndex}`,
1245+
id: `${activeItem.canonicalId}:${summaryIndex}`,
12391246
providerMetadata: {
12401247
copilot: { itemId: activeItem.canonicalId },
12411248
},
12421249
})
1250+
activeItem.summaryParts[Number(summaryIndex)] = "concluded"
12431251
}
1244-
activeItem.summaryParts.push(value.summary_index)
1252+
activeItem.summaryParts[value.summary_index] = "active"
12451253

12461254
controller.enqueue({
12471255
type: "reasoning-start",
@@ -1254,6 +1262,22 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
12541262
},
12551263
})
12561264
}
1265+
} else if (isResponseReasoningSummaryPartDoneChunk(value)) {
1266+
const activeItem =
1267+
currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null
1268+
if (!activeItem || activeItem.summaryParts[value.summary_index] !== "active") return
1269+
if (body.store === false) {
1270+
activeItem.summaryParts[value.summary_index] = "can-conclude"
1271+
return
1272+
}
1273+
controller.enqueue({
1274+
type: "reasoning-end",
1275+
id: `${activeItem.canonicalId}:${value.summary_index}`,
1276+
providerMetadata: {
1277+
copilot: { itemId: activeItem.canonicalId },
1278+
},
1279+
})
1280+
activeItem.summaryParts[value.summary_index] = "concluded"
12571281
} else if (isResponseReasoningSummaryTextDeltaChunk(value)) {
12581282
const activeItem =
12591283
currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null
@@ -1316,6 +1340,16 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
13161340
controller.enqueue({ type: "text-end", id: currentTextId })
13171341
currentTextId = null
13181342
}
1343+
for (const activeItem of Object.values(activeReasoning)) {
1344+
for (const [summaryIndex, status] of Object.entries(activeItem.summaryParts)) {
1345+
if (status === "concluded") continue
1346+
controller.enqueue({
1347+
type: "reasoning-end",
1348+
id: `${activeItem.canonicalId}:${summaryIndex}`,
1349+
providerMetadata: { copilot: { itemId: activeItem.canonicalId } },
1350+
})
1351+
}
1352+
}
13191353

13201354
const providerMetadata: SharedV3ProviderMetadata = {
13211355
copilot: {
@@ -1564,6 +1598,12 @@ const responseReasoningSummaryTextDeltaSchema = z.object({
15641598
delta: z.string(),
15651599
})
15661600

1601+
const responseReasoningSummaryPartDoneSchema = z.object({
1602+
type: z.literal("response.reasoning_summary_part.done"),
1603+
item_id: z.string(),
1604+
summary_index: z.number(),
1605+
})
1606+
15671607
const openaiResponsesChunkSchema = z.union([
15681608
textDeltaChunkSchema,
15691609
responseFinishedChunkSchema,
@@ -1576,6 +1616,7 @@ const openaiResponsesChunkSchema = z.union([
15761616
responseCodeInterpreterCallCodeDoneSchema,
15771617
responseAnnotationAddedSchema,
15781618
responseReasoningSummaryPartAddedSchema,
1619+
responseReasoningSummaryPartDoneSchema,
15791620
responseReasoningSummaryTextDeltaSchema,
15801621
errorChunkSchema,
15811622
z.object({ type: z.string() }).loose(), // fallback for unknown chunks
@@ -1664,6 +1705,12 @@ function isResponseReasoningSummaryPartAddedChunk(
16641705
return chunk.type === "response.reasoning_summary_part.added"
16651706
}
16661707

1708+
function isResponseReasoningSummaryPartDoneChunk(
1709+
chunk: z.infer<typeof openaiResponsesChunkSchema>,
1710+
): chunk is z.infer<typeof responseReasoningSummaryPartDoneSchema> {
1711+
return chunk.type === "response.reasoning_summary_part.done"
1712+
}
1713+
16671714
function isResponseReasoningSummaryTextDeltaChunk(
16681715
chunk: z.infer<typeof openaiResponsesChunkSchema>,
16691716
): chunk is z.infer<typeof responseReasoningSummaryTextDeltaSchema> {

packages/core/src/plugin/provider/github-copilot.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
2-
import { Message } from "@opencode-ai/ai"
32
import { Effect, Option, Schema, Semaphore, Stream } from "effect"
43
import { Catalog } from "../../catalog"
54
import { Credential } from "../../credential"
@@ -202,12 +201,7 @@ export const GithubCopilotPlugin = define({
202201
if (!loaded.models.has(Model.ID.make(id))) evt.model.remove(item.provider.id, id)
203202
}
204203
for (const [id, model] of loaded.models) {
205-
evt.model.update(item.provider.id, id, (draft) => {
206-
Object.assign(draft, structuredClone(model))
207-
if (Provider.packageName(draft.package) === "@ai-sdk/github-copilot") {
208-
draft.settings = Provider.mergeOverlay(draft.settings, { store: false })
209-
}
210-
})
204+
evt.model.update(item.provider.id, id, (draft) => Object.assign(draft, structuredClone(model)))
211205
}
212206
} else if (loaded.baseURL) {
213207
for (const id of item.models.keys()) {
@@ -255,28 +249,6 @@ export const GithubCopilotPlugin = define({
255249
applyHeaders(evt.request.headers, token, ctx.app, requestMetadata(evt.request.url, body), true)
256250
}),
257251
)
258-
yield* ctx.session.hook("context", (evt) =>
259-
Effect.sync(() => {
260-
if (evt.model.providerID !== Provider.ID.githubCopilot) return
261-
evt.messages = evt.messages.map(
262-
(message) =>
263-
new Message({
264-
...message,
265-
content: message.content.map((part) => {
266-
if (!("providerMetadata" in part)) return part
267-
const metadata = part.providerMetadata?.copilot
268-
if (metadata === undefined || !("itemId" in metadata)) return part
269-
const next = { ...metadata }
270-
delete next.itemId
271-
return {
272-
...part,
273-
providerMetadata: { ...part.providerMetadata, copilot: next },
274-
}
275-
}),
276-
}),
277-
)
278-
}),
279-
)
280252
yield* ctx.aisdk.hook(
281253
"language",
282254
Effect.fn(function* (evt) {

0 commit comments

Comments
 (0)