fix: conditionally include encrypted reasoning only for models that support it

- gpt-5-chat-latest does not support encrypted reasoning
- Only include reasoning.encrypted_content for models with supportsReasoningEffort !== false
- Add tests to verify gpt-5-chat-latest works without encrypted reasoning
- Fixes #9225
This commit is contained in:
Roo Code 2025-11-13 11:18:34 +00:00
parent 4e6cdad052
commit 2cd9f1311b
2 changed files with 165 additions and 3 deletions

View file

@ -1316,5 +1316,159 @@ describe("GPT-5 streaming event coverage (additional)", () => {
expect(bodyStr).not.toContain('"verbosity"')
})
})
describe("gpt-5-chat-latest encrypted reasoning fix", () => {
it("should NOT include encrypted reasoning for gpt-5-chat-latest", async () => {
const mockFetch = vitest.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Response without encrypted reasoning"}}\n\n',
),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
global.fetch = mockFetch as any
// Mock SDK to fail so it uses fetch
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-chat-latest",
openAiNativeApiKey: "test-api-key",
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
// Verify the request was made
expect(mockFetch).toHaveBeenCalledWith(
"https://api.openai.com/v1/responses",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"Content-Type": "application/json",
Authorization: "Bearer test-api-key",
Accept: "text/event-stream",
}),
body: expect.any(String),
}),
)
const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body)
// CRITICAL: gpt-5-chat-latest should NOT have encrypted reasoning
expect(requestBody.include).toBeUndefined()
expect(requestBody).not.toHaveProperty("include")
// Should still have other expected properties
expect(requestBody.model).toBe("gpt-5-chat-latest")
expect(requestBody.instructions).toBe("You are a helpful assistant.")
expect(requestBody.stream).toBe(true)
expect(requestBody.store).toBe(false)
// gpt-5-chat-latest supports verbosity
expect(requestBody.text?.verbosity).toBe("medium")
// gpt-5-chat-latest does NOT support reasoning effort
expect(requestBody.reasoning).toBeUndefined()
})
it("should still include encrypted reasoning for models that support it (gpt-5-2025-08-07)", async () => {
const mockFetch = vitest.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Response with encrypted reasoning"}}\n\n',
),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
global.fetch = mockFetch as any
// Mock SDK to fail so it uses fetch
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
openAiNativeApiKey: "test-api-key",
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body)
// gpt-5-2025-08-07 SHOULD have encrypted reasoning
expect(requestBody.include).toEqual(["reasoning.encrypted_content"])
// Should also have reasoning effort
expect(requestBody.reasoning?.effort).toBe("medium")
})
it("should NOT include encrypted reasoning for gpt-5-chat-latest in completePrompt", async () => {
// Clear the mock before this test
mockResponsesCreate.mockClear()
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-chat-latest",
openAiNativeApiKey: "test-api-key",
})
// Mock the responses.create method
mockResponsesCreate.mockResolvedValue({
output: [
{
type: "message",
content: [
{
type: "output_text",
text: "Completion without encrypted reasoning",
},
],
},
],
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("Completion without encrypted reasoning")
expect(mockResponsesCreate).toHaveBeenCalledTimes(1)
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "gpt-5-chat-latest",
stream: false,
store: false,
}),
)
// Check that include is NOT in the request
const callArg = mockResponsesCreate.mock.calls[0][0]
expect(callArg.include).toBeUndefined()
expect(callArg).not.toHaveProperty("include")
})
})
})
})

View file

@ -206,17 +206,21 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
const requestedTier = (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined
const allowedTierNames = new Set(model.info.tiers?.map((t) => t.name).filter(Boolean) || [])
// Check if the model supports reasoning (and thus encrypted reasoning)
const supportsReasoning = model.info.supportsReasoningEffort !== false || reasoningEffort !== undefined
const body: Gpt5RequestBody = {
model: model.id,
input: formattedInput,
stream: true,
// Always use stateless operation with encrypted reasoning
// Always use stateless operation
store: false,
// Always include instructions (system prompt) for Responses API.
// Unlike Chat Completions, system/developer roles in input have no special semantics here.
// The official way to set system behavior is the top-level `instructions` field.
instructions: systemPrompt,
include: ["reasoning.encrypted_content"],
// Only include encrypted reasoning for models that support reasoning
...(supportsReasoning ? { include: ["reasoning.encrypted_content"] } : {}),
...(reasoningEffort
? {
reasoning: {
@ -1087,6 +1091,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
// Resolve reasoning effort for models that support it
const reasoningEffort = this.getReasoningEffort(model)
// Check if the model supports reasoning (and thus encrypted reasoning)
const supportsReasoning = model.info.supportsReasoningEffort !== false || reasoningEffort !== undefined
// Build request body for Responses API
const requestBody: any = {
model: model.id,
@ -1098,7 +1105,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
],
stream: false, // Non-streaming for completePrompt
store: false, // Don't store prompt completions
include: ["reasoning.encrypted_content"],
// Only include encrypted reasoning for models that support reasoning
...(supportsReasoning ? { include: ["reasoning.encrypted_content"] } : {}),
}
// Include service tier if selected and supported