mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 10:45:33 -04:00
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
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<typeof mock>) {
|
|
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")
|
|
})
|
|
})
|