Skip to content

Commit 50ad5c0

Browse files
committed
test: add failing tests proving 3 reasoning round-trip bugs
Bug 1: options() missing include: [reasoning.encrypted_content] for openai/copilot/azure reasoning models (only set in variants, not defaults) Bug 2: convertToOpenAIResponsesInput drops reasoning with encrypted_content but no itemId when store=false Bug 3: differentModel=true strips providerMetadata (itemId + reasoningEncryptedContent) from reasoning parts, breaking multi-turn
1 parent 4f9e8e1 commit 50ad5c0

3 files changed

Lines changed: 282 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { convertToOpenAIResponsesInput } from "@/provider/sdk/copilot/responses/convert-to-openai-responses-input"
2+
import { describe, test, expect } from "bun:test"
3+
4+
describe("convertToOpenAIResponsesInput - reasoning round-trip", () => {
5+
test("BUG: reasoning parts with encrypted_content and itemId are preserved when store=false", async () => {
6+
const result = await convertToOpenAIResponsesInput({
7+
prompt: [
8+
{
9+
role: "user",
10+
content: [{ type: "text", text: "hello" }],
11+
},
12+
{
13+
role: "assistant",
14+
content: [
15+
{
16+
type: "reasoning",
17+
text: "thinking about this...",
18+
providerOptions: {
19+
copilot: {
20+
itemId: "rs_abc123",
21+
reasoningEncryptedContent: "encrypted_blob_data",
22+
},
23+
},
24+
},
25+
{ type: "text", text: "hi there" },
26+
],
27+
},
28+
],
29+
systemMessageMode: "system",
30+
store: false,
31+
})
32+
33+
const reasoning = result.input.find((item: any) => item.type === "reasoning")
34+
expect(reasoning).toBeDefined()
35+
expect(reasoning.encrypted_content).toBe("encrypted_blob_data")
36+
expect(reasoning.summary).toEqual([{ type: "summary_text", text: "thinking about this..." }])
37+
})
38+
39+
test("reasoning parts with encrypted_content and itemId are preserved when store=true", async () => {
40+
const result = await convertToOpenAIResponsesInput({
41+
prompt: [
42+
{
43+
role: "user",
44+
content: [{ type: "text", text: "hello" }],
45+
},
46+
{
47+
role: "assistant",
48+
content: [
49+
{
50+
type: "reasoning",
51+
text: "thinking about this...",
52+
providerOptions: {
53+
copilot: {
54+
itemId: "rs_abc123",
55+
reasoningEncryptedContent: "encrypted_blob_data",
56+
},
57+
},
58+
},
59+
{ type: "text", text: "hi there" },
60+
],
61+
},
62+
],
63+
systemMessageMode: "system",
64+
store: true,
65+
})
66+
67+
const ref = result.input.find((item: any) => item.type === "item_reference")
68+
expect(ref).toBeDefined()
69+
expect((ref as any).id).toBe("rs_abc123")
70+
})
71+
72+
test("BUG: reasoning parts without itemId are silently dropped when store=false", async () => {
73+
const result = await convertToOpenAIResponsesInput({
74+
prompt: [
75+
{
76+
role: "user",
77+
content: [{ type: "text", text: "hello" }],
78+
},
79+
{
80+
role: "assistant",
81+
content: [
82+
{
83+
type: "reasoning",
84+
text: "thinking about this...",
85+
},
86+
{ type: "text", text: "hi there" },
87+
],
88+
},
89+
],
90+
systemMessageMode: "system",
91+
store: false,
92+
})
93+
94+
const reasoning = result.input.find((item: any) => item.type === "reasoning")
95+
expect(reasoning).toBeUndefined()
96+
97+
expect(result.warnings.some((w) => w.message.includes("Non-OpenAI reasoning parts are not supported"))).toBe(true)
98+
})
99+
100+
test("BUG: reasoning with encrypted_content but no itemId still drops data", async () => {
101+
const result = await convertToOpenAIResponsesInput({
102+
prompt: [
103+
{
104+
role: "user",
105+
content: [{ type: "text", text: "hello" }],
106+
},
107+
{
108+
role: "assistant",
109+
content: [
110+
{
111+
type: "reasoning",
112+
text: "thinking...",
113+
providerOptions: {
114+
copilot: {
115+
reasoningEncryptedContent: "encrypted_but_no_item_id",
116+
},
117+
},
118+
},
119+
{ type: "text", text: "response" },
120+
],
121+
},
122+
],
123+
systemMessageMode: "system",
124+
store: false,
125+
})
126+
127+
const reasoning = result.input.find((item: any) => item.type === "reasoning")
128+
expect(reasoning).toBeDefined()
129+
expect((reasoning as any).encrypted_content).toBe("encrypted_but_no_item_id")
130+
})
131+
})

‎packages/opencode/test/provider/transform.test.ts‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2913,3 +2913,112 @@ describe("ProviderTransform.options - reasoning include for OpenAI (issue #24190
29132913
expect(defaultOptions.include).toBeUndefined()
29142914
})
29152915
})
2916+
2917+
describe("BUG PROOF: ProviderTransform.options - encrypted_content for reasoning round-trip", () => {
2918+
const sessionID = "test-session-123"
2919+
2920+
test("BUG: options() should include reasoning.encrypted_content for openai reasoning models (not just variants)", () => {
2921+
const openaiReasoningModel = {
2922+
id: "openai/o3",
2923+
providerID: "openai",
2924+
api: {
2925+
id: "o3",
2926+
url: "https://api.openai.com",
2927+
npm: "@ai-sdk/openai",
2928+
},
2929+
name: "O3",
2930+
capabilities: {
2931+
temperature: false,
2932+
reasoning: true,
2933+
attachment: false,
2934+
toolcall: true,
2935+
input: { text: true, audio: false, image: true, video: false, pdf: false },
2936+
output: { text: true, audio: false, image: false, video: false, pdf: false },
2937+
interleaved: false,
2938+
},
2939+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
2940+
limit: { context: 200000, output: 100000 },
2941+
status: "active",
2942+
options: {},
2943+
headers: {},
2944+
release_date: "2025-12-04",
2945+
} as any
2946+
2947+
const result = ProviderTransform.options({
2948+
model: openaiReasoningModel,
2949+
sessionID,
2950+
})
2951+
2952+
expect(result.include).toEqual(["reasoning.encrypted_content"])
2953+
})
2954+
2955+
test("BUG: options() should include reasoning.encrypted_content for copilot reasoning models", () => {
2956+
const copilotReasoningModel = {
2957+
id: "github-copilot/gpt-4.1",
2958+
providerID: "github-copilot",
2959+
api: {
2960+
id: "gpt-4.1",
2961+
url: "https://api.githubcopilot.com",
2962+
npm: "@ai-sdk/github-copilot",
2963+
},
2964+
name: "GPT-4.1",
2965+
capabilities: {
2966+
temperature: true,
2967+
reasoning: true,
2968+
attachment: true,
2969+
toolcall: true,
2970+
input: { text: true, audio: false, image: true, video: false, pdf: false },
2971+
output: { text: true, audio: false, image: false, video: false, pdf: false },
2972+
interleaved: false,
2973+
},
2974+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
2975+
limit: { context: 128000, output: 16384 },
2976+
status: "active",
2977+
options: {},
2978+
headers: {},
2979+
release_date: "2025-12-04",
2980+
} as any
2981+
2982+
const result = ProviderTransform.options({
2983+
model: copilotReasoningModel,
2984+
sessionID,
2985+
})
2986+
2987+
expect(result.include).toEqual(["reasoning.encrypted_content"])
2988+
})
2989+
2990+
test("BUG: options() should include reasoning.encrypted_content for azure reasoning models", () => {
2991+
const azureReasoningModel = {
2992+
id: "azure/o3",
2993+
providerID: "azure",
2994+
api: {
2995+
id: "o3",
2996+
url: "https://test.openai.azure.com",
2997+
npm: "@ai-sdk/azure",
2998+
},
2999+
name: "O3",
3000+
capabilities: {
3001+
temperature: false,
3002+
reasoning: true,
3003+
attachment: false,
3004+
toolcall: true,
3005+
input: { text: true, audio: false, image: true, video: false, pdf: false },
3006+
output: { text: true, audio: false, image: false, video: false, pdf: false },
3007+
interleaved: false,
3008+
},
3009+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
3010+
limit: { context: 200000, output: 100000 },
3011+
status: "active",
3012+
options: {},
3013+
headers: {},
3014+
release_date: "2025-12-04",
3015+
} as any
3016+
3017+
const result = ProviderTransform.options({
3018+
model: azureReasoningModel,
3019+
sessionID,
3020+
})
3021+
3022+
expect(result.include).toEqual(["reasoning.encrypted_content"])
3023+
})
3024+
})

‎packages/opencode/test/session/message-v2.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,4 +1036,46 @@ describe("MessageV2.toModelMessages - reasoning providerMetadata round-trip (iss
10361036

10371037
expect(reasoningPart.providerOptions).toBeUndefined()
10381038
})
1039+
1040+
test("BUG FIX: should preserve reasoningEncryptedContent even when model changes for multi-turn round-trip", async () => {
1041+
const otherModel: Provider.Model = {
1042+
...reasoningModel,
1043+
id: ModelID.make("openai/o4-mini"),
1044+
providerID: ProviderID.make("openai"),
1045+
api: { ...reasoningModel.api, id: "o4-mini" },
1046+
}
1047+
1048+
const input: MessageV2.WithParts[] = [
1049+
{
1050+
info: assistantInfo("m-1", "m-parent"),
1051+
parts: [
1052+
{
1053+
...basePart("m-1", "p-1"),
1054+
type: "reasoning",
1055+
text: "thinking...",
1056+
time: { start: 0 },
1057+
metadata: {
1058+
openai: {
1059+
itemId: "rs_abc123",
1060+
reasoningEncryptedContent: "encrypted_reasoning_data",
1061+
},
1062+
},
1063+
},
1064+
{
1065+
...basePart("m-1", "p-2"),
1066+
type: "text",
1067+
text: "Hello!",
1068+
},
1069+
] as MessageV2.Part[],
1070+
},
1071+
]
1072+
1073+
const result = await MessageV2.toModelMessages(input, otherModel)
1074+
const assistantMsg = result.find((m) => m.role === "assistant") as any
1075+
const reasoningPart = assistantMsg.content.find((p: any) => p.type === "reasoning")
1076+
1077+
expect(reasoningPart.providerOptions).toBeDefined()
1078+
expect(reasoningPart.providerOptions.openai.reasoningEncryptedContent).toBe("encrypted_reasoning_data")
1079+
expect(reasoningPart.providerOptions.openai.itemId).toBe("rs_abc123")
1080+
})
10391081
})

0 commit comments

Comments
 (0)