import { OpenAIResponsesLanguageModel } from "@opencode-ai/core/github-copilot/responses/openai-responses-language-model" import { convertToOpenAIResponsesInput } from "@opencode-ai/core/github-copilot/responses/convert-to-openai-responses-input" import { describe, test, expect, mock } from "bun:test" import type { LanguageModelV3Prompt } from "@ai-sdk/provider" const TEST_PROMPT: LanguageModelV3Prompt = [{ role: "user", content: [{ type: "text", text: "Hello" }] }] function createMockFetch(body: unknown) { return mock( async () => new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } }), ) } function createModel(fetchFn: ReturnType) { return new OpenAIResponsesLanguageModel("test-model", { provider: "copilot", url: () => "https://api.test.com/responses", headers: () => ({ Authorization: "Bearer test-token" }), fetch: fetchFn as any, }) } // GitHub Copilot's Responses model echoes item metadata (itemId, reasoningEncryptedContent, // responseId, ...) under the "copilot" providerOptions/providerMetadata namespace, matching the // namespace request options already use. It used to echo this metadata under "openai" (a leftover // from forking the OpenAI Responses model), which left it unreachable by anything reading the // "copilot" namespace and let stale itemIds slip past stripping meant for that namespace. describe("doGenerate", () => { test("attaches item metadata under the copilot namespace, not openai", async () => { const mockFetch = createMockFetch({ id: "resp_1", created_at: 0, model: "gpt-5.5", output: [ { type: "reasoning", id: "rs_1", encrypted_content: "enc_1", summary: [{ type: "summary_text", text: "thinking..." }], }, { type: "message", role: "assistant", id: "msg_1", content: [{ type: "output_text", text: "Hello there", annotations: [] }], }, { type: "function_call", call_id: "call_1", name: "bash", arguments: "{}", id: "fc_1", }, ], usage: { input_tokens: 10, output_tokens: 5 }, }) const model = createModel(mockFetch) const { content, providerMetadata } = await model.doGenerate({ prompt: TEST_PROMPT, includeRawChunks: false, } as any) const reasoning = content.find((part: any) => part.type === "reasoning") as any expect(reasoning.providerMetadata?.copilot?.itemId).toBe("rs_1") expect(reasoning.providerMetadata?.copilot?.reasoningEncryptedContent).toBe("enc_1") expect(reasoning.providerMetadata?.openai).toBeUndefined() const text = content.find((part: any) => part.type === "text") as any expect(text.providerMetadata?.copilot?.itemId).toBe("msg_1") expect(text.providerMetadata?.openai).toBeUndefined() const toolCall = content.find((part: any) => part.type === "tool-call") as any expect(toolCall.providerMetadata?.copilot?.itemId).toBe("fc_1") expect(toolCall.providerMetadata?.openai).toBeUndefined() expect(providerMetadata?.copilot?.responseId).toBe("resp_1") expect(providerMetadata?.openai).toBeUndefined() }) }) describe("convertToOpenAIResponsesInput", () => { test("echoes a stale tool-call itemId from the copilot namespace as the function_call id", async () => { const { input } = await convertToOpenAIResponsesInput({ prompt: [ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_1", toolName: "bash", input: { command: "ls" }, providerOptions: { copilot: { itemId: "fc_999" } }, }, ], }, ], systemMessageMode: "system", store: false, }) expect(input).toEqual([ { type: "function_call", call_id: "call_1", name: "bash", arguments: JSON.stringify({ command: "ls" }), id: "fc_999", }, ]) }) test("omits the function_call id once the stale copilot itemId has been stripped", async () => { const { input } = await convertToOpenAIResponsesInput({ prompt: [ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_1", toolName: "bash", input: { command: "ls" }, providerOptions: {}, }, ], }, ], systemMessageMode: "system", store: false, }) expect((input[0] as any).id).toBeUndefined() }) test("preserves reasoning items keyed by the copilot namespace instead of dropping them", async () => { const { input, warnings } = await convertToOpenAIResponsesInput({ prompt: [ { role: "assistant", content: [ { type: "reasoning", text: "thinking...", providerOptions: { copilot: { itemId: "rs_1", reasoningEncryptedContent: "enc_1" } }, }, ], }, ], systemMessageMode: "system", store: false, }) expect(warnings).toEqual([]) expect(input).toEqual([ { type: "reasoning", id: "rs_1", encrypted_content: "enc_1", summary: [{ type: "summary_text", text: "thinking..." }], }, ]) }) test("drops reasoning items with no copilot itemId and warns, as before", async () => { const { input, warnings } = await convertToOpenAIResponsesInput({ prompt: [ { role: "assistant", content: [{ type: "reasoning", text: "thinking...", providerOptions: {} }], }, ], systemMessageMode: "system", store: false, }) expect(input).toEqual([]) expect(warnings).toHaveLength(1) expect(warnings[0]).toMatchObject({ message: expect.stringContaining("Non-OpenAI reasoning parts are not supported"), }) }) test("reads imageDetail from the copilot namespace on user file parts", async () => { const { input } = await convertToOpenAIResponsesInput({ prompt: [ { role: "user", content: [ { type: "file", mediaType: "image/png", data: "aGVsbG8=", providerOptions: { copilot: { imageDetail: "high" } }, }, ], }, ], systemMessageMode: "system", store: false, }) expect((input[0] as any).content[0].detail).toBe("high") }) })