fix(core): stop replaying stale GitHub Copilot Responses item IDs (#34686)
This commit is contained in:
@@ -86,7 +86,7 @@ export async function convertToOpenAIResponsesInput({
|
|||||||
: {
|
: {
|
||||||
image_url: `data:${mediaType};base64,${convertToBase64(part.data)}`,
|
image_url: `data:${mediaType};base64,${convertToBase64(part.data)}`,
|
||||||
}),
|
}),
|
||||||
detail: part.providerOptions?.openai?.imageDetail,
|
detail: part.providerOptions?.copilot?.imageDetail,
|
||||||
}
|
}
|
||||||
} else if (part.mediaType === "application/pdf") {
|
} else if (part.mediaType === "application/pdf") {
|
||||||
if (part.data instanceof URL) {
|
if (part.data instanceof URL) {
|
||||||
@@ -127,7 +127,7 @@ export async function convertToOpenAIResponsesInput({
|
|||||||
input.push({
|
input.push({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "output_text", text: part.text }],
|
content: [{ type: "output_text", text: part.text }],
|
||||||
id: (part.providerOptions?.openai?.itemId as string) ?? undefined,
|
id: (part.providerOptions?.copilot?.itemId as string) ?? undefined,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ export async function convertToOpenAIResponsesInput({
|
|||||||
input.push({
|
input.push({
|
||||||
type: "local_shell_call",
|
type: "local_shell_call",
|
||||||
call_id: part.toolCallId,
|
call_id: part.toolCallId,
|
||||||
id: (part.providerOptions?.openai?.itemId as string) ?? undefined,
|
id: (part.providerOptions?.copilot?.itemId as string) ?? undefined,
|
||||||
action: {
|
action: {
|
||||||
type: "exec",
|
type: "exec",
|
||||||
command: parsedInput.action.command,
|
command: parsedInput.action.command,
|
||||||
@@ -162,7 +162,7 @@ export async function convertToOpenAIResponsesInput({
|
|||||||
call_id: part.toolCallId,
|
call_id: part.toolCallId,
|
||||||
name: part.toolName,
|
name: part.toolName,
|
||||||
arguments: JSON.stringify(part.input),
|
arguments: JSON.stringify(part.input),
|
||||||
id: (part.providerOptions?.openai?.itemId as string) ?? undefined,
|
id: (part.providerOptions?.copilot?.itemId as string) ?? undefined,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -275,7 +275,7 @@ export async function convertToOpenAIResponsesInput({
|
|||||||
const output = part.output
|
const output = part.output
|
||||||
|
|
||||||
if (output.type === "execution-denied") {
|
if (output.type === "execution-denied") {
|
||||||
const approvalId = (output.providerOptions?.openai as { approvalId?: string } | undefined)?.approvalId
|
const approvalId = (output.providerOptions?.copilot as { approvalId?: string } | undefined)?.approvalId
|
||||||
|
|
||||||
if (approvalId) {
|
if (approvalId) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -525,7 +525,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
type: "reasoning" as const,
|
type: "reasoning" as const,
|
||||||
text: summary.text,
|
text: summary.text,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: part.id,
|
itemId: part.id,
|
||||||
reasoningEncryptedContent: part.encrypted_content ?? null,
|
reasoningEncryptedContent: part.encrypted_content ?? null,
|
||||||
},
|
},
|
||||||
@@ -563,7 +563,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
toolName: "local_shell",
|
toolName: "local_shell",
|
||||||
input: JSON.stringify({ action: part.action } satisfies z.infer<typeof localShellInputSchema>),
|
input: JSON.stringify({ action: part.action } satisfies z.infer<typeof localShellInputSchema>),
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: part.id,
|
itemId: part.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -574,7 +574,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
|
|
||||||
case "message": {
|
case "message": {
|
||||||
for (const contentPart of part.content) {
|
for (const contentPart of part.content) {
|
||||||
if (options.providerOptions?.openai?.logprobs && contentPart.logprobs) {
|
if (options.providerOptions?.copilot?.logprobs && contentPart.logprobs) {
|
||||||
logprobs.push(contentPart.logprobs)
|
logprobs.push(contentPart.logprobs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +582,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
type: "text",
|
type: "text",
|
||||||
text: contentPart.text,
|
text: contentPart.text,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: part.id,
|
itemId: part.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -622,7 +622,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
toolName: part.name,
|
toolName: part.name,
|
||||||
input: part.arguments,
|
input: part.arguments,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: part.id,
|
itemId: part.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -724,15 +724,15 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const providerMetadata: SharedV3ProviderMetadata = {
|
const providerMetadata: SharedV3ProviderMetadata = {
|
||||||
openai: { responseId: response.id },
|
copilot: { responseId: response.id },
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logprobs.length > 0) {
|
if (logprobs.length > 0) {
|
||||||
providerMetadata.openai.logprobs = logprobs
|
providerMetadata.copilot.logprobs = logprobs
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof response.service_tier === "string") {
|
if (typeof response.service_tier === "string") {
|
||||||
providerMetadata.openai.serviceTier = response.service_tier
|
providerMetadata.copilot.serviceTier = response.service_tier
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -954,7 +954,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
type: "text-start",
|
type: "text-start",
|
||||||
id: value.item.id,
|
id: value.item.id,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: value.item.id,
|
itemId: value.item.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -971,7 +971,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
type: "reasoning-start",
|
type: "reasoning-start",
|
||||||
id: `${value.item.id}:0`,
|
id: `${value.item.id}:0`,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: value.item.id,
|
itemId: value.item.id,
|
||||||
reasoningEncryptedContent: value.item.encrypted_content ?? null,
|
reasoningEncryptedContent: value.item.encrypted_content ?? null,
|
||||||
},
|
},
|
||||||
@@ -994,7 +994,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
toolName: value.item.name,
|
toolName: value.item.name,
|
||||||
input: value.item.arguments,
|
input: value.item.arguments,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: value.item.id,
|
itemId: value.item.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1103,7 +1103,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
},
|
},
|
||||||
} satisfies z.infer<typeof localShellInputSchema>),
|
} satisfies z.infer<typeof localShellInputSchema>),
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: { itemId: value.item.id },
|
copilot: { itemId: value.item.id },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (value.item.type === "message") {
|
} else if (value.item.type === "message") {
|
||||||
@@ -1122,7 +1122,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
type: "reasoning-end",
|
type: "reasoning-end",
|
||||||
id: `${activeReasoningPart.canonicalId}:${summaryIndex}`,
|
id: `${activeReasoningPart.canonicalId}:${summaryIndex}`,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: activeReasoningPart.canonicalId,
|
itemId: activeReasoningPart.canonicalId,
|
||||||
reasoningEncryptedContent: value.item.encrypted_content ?? null,
|
reasoningEncryptedContent: value.item.encrypted_content ?? null,
|
||||||
},
|
},
|
||||||
@@ -1209,7 +1209,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
type: "text-start",
|
type: "text-start",
|
||||||
id: currentTextId,
|
id: currentTextId,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: { itemId: value.item_id },
|
copilot: { itemId: value.item_id },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1220,7 +1220,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
delta: value.delta,
|
delta: value.delta,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (options.providerOptions?.openai?.logprobs && value.logprobs) {
|
if (options.providerOptions?.copilot?.logprobs && value.logprobs) {
|
||||||
logprobs.push(value.logprobs)
|
logprobs.push(value.logprobs)
|
||||||
}
|
}
|
||||||
} else if (isResponseReasoningSummaryPartAddedChunk(value)) {
|
} else if (isResponseReasoningSummaryPartAddedChunk(value)) {
|
||||||
@@ -1235,7 +1235,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
type: "reasoning-start",
|
type: "reasoning-start",
|
||||||
id: `${activeItem.canonicalId}:${value.summary_index}`,
|
id: `${activeItem.canonicalId}:${value.summary_index}`,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: activeItem.canonicalId,
|
itemId: activeItem.canonicalId,
|
||||||
reasoningEncryptedContent: activeItem.encryptedContent ?? null,
|
reasoningEncryptedContent: activeItem.encryptedContent ?? null,
|
||||||
},
|
},
|
||||||
@@ -1252,7 +1252,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
id: `${activeItem.canonicalId}:${value.summary_index}`,
|
id: `${activeItem.canonicalId}:${value.summary_index}`,
|
||||||
delta: value.delta,
|
delta: value.delta,
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
openai: {
|
copilot: {
|
||||||
itemId: activeItem.canonicalId,
|
itemId: activeItem.canonicalId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1306,17 +1306,17 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const providerMetadata: SharedV3ProviderMetadata = {
|
const providerMetadata: SharedV3ProviderMetadata = {
|
||||||
openai: {
|
copilot: {
|
||||||
responseId,
|
responseId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logprobs.length > 0) {
|
if (logprobs.length > 0) {
|
||||||
providerMetadata.openai.logprobs = logprobs
|
providerMetadata.copilot.logprobs = logprobs
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceTier !== undefined) {
|
if (serviceTier !== undefined) {
|
||||||
providerMetadata.openai.serviceTier = serviceTier
|
providerMetadata.copilot.serviceTier = serviceTier
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.enqueue({
|
controller.enqueue({
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -463,7 +463,9 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re
|
|||||||
if (
|
if (
|
||||||
options.store !== true &&
|
options.store !== true &&
|
||||||
key &&
|
key &&
|
||||||
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(model.api.npm)
|
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle", "@ai-sdk/github-copilot"].includes(
|
||||||
|
model.api.npm,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
msgs = mapProviderOptions(msgs, (options) => {
|
msgs = mapProviderOptions(msgs, (options) => {
|
||||||
if (!options?.[key] || !("itemId" in options[key])) return options
|
if (!options?.[key] || !("itemId" in options[key])) return options
|
||||||
|
|||||||
@@ -2268,6 +2268,82 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
|
|||||||
expect(result[0].content[0].providerOptions?.openai?.reasoningEncryptedContent).toBe("encrypted")
|
expect(result[0].content[0].providerOptions?.openai?.reasoningEncryptedContent).toBe("encrypted")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("strips GitHub Copilot itemId from the copilot namespace, preserving other copilot options", () => {
|
||||||
|
const copilotModel = {
|
||||||
|
...openaiModel,
|
||||||
|
id: "github-copilot/gpt-5.5",
|
||||||
|
providerID: "github-copilot",
|
||||||
|
api: {
|
||||||
|
id: "gpt-5.5",
|
||||||
|
url: "https://api.githubcopilot.com",
|
||||||
|
npm: "@ai-sdk/github-copilot",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const msgs = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "reasoning",
|
||||||
|
text: "thinking...",
|
||||||
|
providerOptions: {
|
||||||
|
copilot: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The stale itemId on tool-call parts is what Copilot echoes back as the
|
||||||
|
// `function_call` item `id`, which is what the upstream connection rejects.
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "call_1",
|
||||||
|
toolName: "bash",
|
||||||
|
input: { command: "ls" },
|
||||||
|
providerOptions: {
|
||||||
|
copilot: { itemId: "fc_456", reasoningEffort: "medium" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as any[]
|
||||||
|
|
||||||
|
const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[]
|
||||||
|
|
||||||
|
expect(result[0].content[0].providerOptions?.copilot?.itemId).toBeUndefined()
|
||||||
|
expect(result[0].content[0].providerOptions?.copilot?.reasoningEncryptedContent).toBe("encrypted")
|
||||||
|
expect(result[0].content[1].providerOptions?.copilot?.itemId).toBeUndefined()
|
||||||
|
expect(result[0].content[1].providerOptions?.copilot?.reasoningEffort).toBe("medium")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("leaves a stray openai namespace on a Copilot model untouched, since Copilot's Responses model only reads the copilot namespace", () => {
|
||||||
|
const copilotModel = {
|
||||||
|
...openaiModel,
|
||||||
|
id: "github-copilot/gpt-5.5",
|
||||||
|
providerID: "github-copilot",
|
||||||
|
api: {
|
||||||
|
id: "gpt-5.5",
|
||||||
|
url: "https://api.githubcopilot.com",
|
||||||
|
npm: "@ai-sdk/github-copilot",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const msgs = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Hello",
|
||||||
|
providerOptions: {
|
||||||
|
openai: { itemId: "msg_456" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as any[]
|
||||||
|
|
||||||
|
const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[]
|
||||||
|
|
||||||
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_456")
|
||||||
|
})
|
||||||
|
|
||||||
test("preserves metadata for openai package when store is true", () => {
|
test("preserves metadata for openai package when store is true", () => {
|
||||||
const msgs = [
|
const msgs = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user