From cb7c941cffb14addf303179780e40dfbb1cfe64c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 29 Sep 2026 17:49:33 -0500 Subject: [PATCH 1/2] fix(core): pass through Copilot Responses settings --- .../openai-responses-language-model.ts | 150 ++---------------- .../openai-responses-options.test.ts | 103 ++++++++++++ 2 files changed, 113 insertions(+), 140 deletions(-) create mode 100644 packages/core/test/github-copilot/openai-responses-options.test.ts diff --git a/packages/core/src/github-copilot/responses/openai-responses-language-model.ts b/packages/core/src/github-copilot/responses/openai-responses-language-model.ts index 53667db090bd..9ff8a5f6627b 100644 --- a/packages/core/src/github-copilot/responses/openai-responses-language-model.ts +++ b/packages/core/src/github-copilot/responses/openai-responses-language-model.ts @@ -149,7 +149,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { responseFormat, }: LanguageModelV3CallOptions) { const warnings: SharedV3Warning[] = [] - const modelConfig = getResponsesModelConfig(this.modelId) if (topK != null) { warnings.push({ type: "unsupported", feature: "topK" }) @@ -186,7 +185,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { const { input, warnings: inputWarnings } = await convertToOpenAIResponsesInput({ prompt, - systemMessageMode: modelConfig.systemMessageMode, + systemMessageMode: "system", fileIdPrefixes: this.config.fileIdPrefixes, store, }) @@ -286,84 +285,18 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { safety_identifier: openaiOptions?.safetyIdentifier, top_logprobs: topLogprobs, - // model-specific settings: - ...(modelConfig.isReasoningModel && - (openaiOptions?.reasoningEffort != null || openaiOptions?.reasoningSummary != null) && { - reasoning: { - ...(openaiOptions?.reasoningEffort != null && { - effort: openaiOptions.reasoningEffort, - }), - ...(openaiOptions?.reasoningSummary != null && { - summary: openaiOptions.reasoningSummary, - }), - }, - }), - ...(modelConfig.requiredAutoTruncation && { - truncation: "auto", + ...((openaiOptions?.reasoningEffort != null || openaiOptions?.reasoningSummary != null) && { + reasoning: { + ...(openaiOptions?.reasoningEffort != null && { + effort: openaiOptions.reasoningEffort, + }), + ...(openaiOptions?.reasoningSummary != null && { + summary: openaiOptions.reasoningSummary, + }), + }, }), } - if (modelConfig.isReasoningModel) { - // remove unsupported settings for reasoning models - // see https://platform.openai.com/docs/guides/reasoning#limitations - if (baseArgs.temperature != null) { - baseArgs.temperature = undefined - warnings.push({ - type: "unsupported", - feature: "temperature", - details: "temperature is not supported for reasoning models", - }) - } - - if (baseArgs.top_p != null) { - baseArgs.top_p = undefined - warnings.push({ - type: "unsupported", - feature: "topP", - details: "topP is not supported for reasoning models", - }) - } - } else { - if (openaiOptions?.reasoningEffort != null) { - warnings.push({ - type: "unsupported", - feature: "reasoningEffort", - details: "reasoningEffort is not supported for non-reasoning models", - }) - } - - if (openaiOptions?.reasoningSummary != null) { - warnings.push({ - type: "unsupported", - feature: "reasoningSummary", - details: "reasoningSummary is not supported for non-reasoning models", - }) - } - } - - // Validate flex processing support - if (openaiOptions?.serviceTier === "flex" && !modelConfig.supportsFlexProcessing) { - warnings.push({ - type: "unsupported", - feature: "serviceTier", - details: "flex processing is only available for o3, o4-mini, and gpt-5 models", - }) - // Remove from args if not supported - baseArgs.service_tier = undefined - } - - // Validate priority processing support - if (openaiOptions?.serviceTier === "priority" && !modelConfig.supportsPriorityProcessing) { - warnings.push({ - type: "unsupported", - feature: "serviceTier", - details: - "priority processing is only available for supported models (gpt-4, gpt-5, gpt-5-mini, o3, o4-mini) and requires Enterprise access. gpt-5-nano is not supported", - }) - // Remove from args if not supported - baseArgs.service_tier = undefined - } - return { getHostedToolName, args: { @@ -1658,69 +1591,6 @@ function isErrorChunk(chunk: z.infer): chunk return chunk.type === "error" } -type ResponsesModelConfig = { - isReasoningModel: boolean - systemMessageMode: "remove" | "system" | "developer" - requiredAutoTruncation: boolean - supportsFlexProcessing: boolean - supportsPriorityProcessing: boolean -} - -function getResponsesModelConfig(modelId: string): ResponsesModelConfig { - const supportsFlexProcessing = - modelId.startsWith("o3") || - modelId.startsWith("o4-mini") || - (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-chat")) - const supportsPriorityProcessing = - modelId.startsWith("gpt-4") || - modelId.startsWith("gpt-5-mini") || - (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-nano") && !modelId.startsWith("gpt-5-chat")) || - modelId.startsWith("o3") || - modelId.startsWith("o4-mini") - const defaults = { - requiredAutoTruncation: false, - systemMessageMode: "system" as const, - supportsFlexProcessing, - supportsPriorityProcessing, - } - - // gpt-5-chat models are non-reasoning - if (modelId.startsWith("gpt-5-chat")) { - return { - ...defaults, - isReasoningModel: false, - } - } - - // o series reasoning models: - if ( - modelId.startsWith("o") || - modelId.startsWith("gpt-5") || - modelId.startsWith("codex-") || - modelId.startsWith("computer-use") - ) { - if (modelId.startsWith("o1-mini") || modelId.startsWith("o1-preview")) { - return { - ...defaults, - isReasoningModel: true, - systemMessageMode: "remove", - } - } - - return { - ...defaults, - isReasoningModel: true, - systemMessageMode: "developer", - } - } - - // gpt models: - return { - ...defaults, - isReasoningModel: false, - } -} - // TODO AI SDK 6: use optional here instead of nullish const openaiResponsesProviderOptionsSchema = z.object({ include: z diff --git a/packages/core/test/github-copilot/openai-responses-options.test.ts b/packages/core/test/github-copilot/openai-responses-options.test.ts new file mode 100644 index 000000000000..171897e6e33c --- /dev/null +++ b/packages/core/test/github-copilot/openai-responses-options.test.ts @@ -0,0 +1,103 @@ +import type { LanguageModelV3CallOptions } from "@ai-sdk/provider" +import { createOpenaiCompatible } from "@opencode/core/github-copilot/copilot-provider" +import { expect } from "bun:test" +import { Effect, Schema } from "effect" +import { it } from "../lib/effect" + +const models = [ + "gpt-6-luna", + "gpt-6-sol", + "gpt-6-astra", + "gpt-5.6-luna", + "gpt-5-chat-latest", + "o1-mini", + "o1-preview", + "future-model", +] + +const options = [ + { reasoningEffort: "max", reasoningSummary: "auto", serviceTier: "flex" }, + { reasoningEffort: "future-effort", serviceTier: "priority" }, + { reasoningSummary: "detailed" }, + {}, +] + +// #51850: supplied settings must reach Copilot without model-name capability guesses. +for (const modelID of models) { + for (const mode of ["generate", "stream"] as const) { + for (const settings of options) { + it.live(`${modelID} ${mode} preserves supplied settings ${JSON.stringify(settings)}`, () => + Effect.gen(function* () { + const requests: Array> = [] + const server = yield* Effect.acquireRelease( + Effect.sync(() => + Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + const body = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown))( + await request.json(), + ) + requests.push(body) + const response = { + id: "resp_fixture", + created_at: 0, + model: modelID, + output: [], + usage: { input_tokens: 1, output_tokens: 1 }, + } + if (body.stream !== true) return Response.json(response) + return new Response(`data: ${JSON.stringify({ type: "response.completed", response })}\n\n`, { + headers: { "Content-Type": "text/event-stream" }, + }) + }, + }), + ), + (server) => Effect.sync(() => server.stop(true)), + ) + const model = createOpenaiCompatible({ baseURL: server.url.href.replace(/\/$/, "") }).responses(modelID) + const input: LanguageModelV3CallOptions = { + prompt: [ + { role: "system", content: "System instruction" }, + { role: "user", content: [{ type: "text", text: "Hello" }] }, + ], + temperature: 0.25, + topP: 0.9, + providerOptions: { copilot: settings }, + } + const warnings = yield* Effect.promise(async () => { + if (mode === "generate") return (await model.doGenerate(input)).warnings + const response = await model.doStream(input) + const events = [] + for await (const event of response.stream) events.push(event) + return events.find((event) => event.type === "stream-start")?.warnings + }) + + expect(requests).toHaveLength(1) + expect(requests[0]).toMatchObject({ + model: modelID, + temperature: 0.25, + top_p: 0.9, + input: [ + { role: "system", content: "System instruction" }, + { role: "user", content: [{ type: "input_text", text: "Hello" }] }, + ], + include: ["reasoning.encrypted_content"], + store: false, + }) + expect(requests[0]?.reasoning).toEqual( + settings.reasoningEffort !== undefined || settings.reasoningSummary !== undefined + ? { + ...(settings.reasoningEffort !== undefined ? { effort: settings.reasoningEffort } : {}), + ...(settings.reasoningSummary !== undefined ? { summary: settings.reasoningSummary } : {}), + } + : undefined, + ) + expect(requests[0]?.service_tier).toBe(settings.serviceTier) + expect(requests[0]?.stream).toBe(mode === "stream" ? true : undefined) + expect(warnings).toEqual([]) + }), + ) + } + } +} From d847fdb487f906ee2da4e2087647134ae89110ca Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 29 Sep 2026 17:51:03 -0500 Subject: [PATCH 2/2] test(core): remove Copilot settings test matrix --- .../openai-responses-options.test.ts | 103 ------------------ 1 file changed, 103 deletions(-) delete mode 100644 packages/core/test/github-copilot/openai-responses-options.test.ts diff --git a/packages/core/test/github-copilot/openai-responses-options.test.ts b/packages/core/test/github-copilot/openai-responses-options.test.ts deleted file mode 100644 index 171897e6e33c..000000000000 --- a/packages/core/test/github-copilot/openai-responses-options.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { LanguageModelV3CallOptions } from "@ai-sdk/provider" -import { createOpenaiCompatible } from "@opencode/core/github-copilot/copilot-provider" -import { expect } from "bun:test" -import { Effect, Schema } from "effect" -import { it } from "../lib/effect" - -const models = [ - "gpt-6-luna", - "gpt-6-sol", - "gpt-6-astra", - "gpt-5.6-luna", - "gpt-5-chat-latest", - "o1-mini", - "o1-preview", - "future-model", -] - -const options = [ - { reasoningEffort: "max", reasoningSummary: "auto", serviceTier: "flex" }, - { reasoningEffort: "future-effort", serviceTier: "priority" }, - { reasoningSummary: "detailed" }, - {}, -] - -// #51850: supplied settings must reach Copilot without model-name capability guesses. -for (const modelID of models) { - for (const mode of ["generate", "stream"] as const) { - for (const settings of options) { - it.live(`${modelID} ${mode} preserves supplied settings ${JSON.stringify(settings)}`, () => - Effect.gen(function* () { - const requests: Array> = [] - const server = yield* Effect.acquireRelease( - Effect.sync(() => - Bun.serve({ - hostname: "127.0.0.1", - port: 0, - async fetch(request) { - const body = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown))( - await request.json(), - ) - requests.push(body) - const response = { - id: "resp_fixture", - created_at: 0, - model: modelID, - output: [], - usage: { input_tokens: 1, output_tokens: 1 }, - } - if (body.stream !== true) return Response.json(response) - return new Response(`data: ${JSON.stringify({ type: "response.completed", response })}\n\n`, { - headers: { "Content-Type": "text/event-stream" }, - }) - }, - }), - ), - (server) => Effect.sync(() => server.stop(true)), - ) - const model = createOpenaiCompatible({ baseURL: server.url.href.replace(/\/$/, "") }).responses(modelID) - const input: LanguageModelV3CallOptions = { - prompt: [ - { role: "system", content: "System instruction" }, - { role: "user", content: [{ type: "text", text: "Hello" }] }, - ], - temperature: 0.25, - topP: 0.9, - providerOptions: { copilot: settings }, - } - const warnings = yield* Effect.promise(async () => { - if (mode === "generate") return (await model.doGenerate(input)).warnings - const response = await model.doStream(input) - const events = [] - for await (const event of response.stream) events.push(event) - return events.find((event) => event.type === "stream-start")?.warnings - }) - - expect(requests).toHaveLength(1) - expect(requests[0]).toMatchObject({ - model: modelID, - temperature: 0.25, - top_p: 0.9, - input: [ - { role: "system", content: "System instruction" }, - { role: "user", content: [{ type: "input_text", text: "Hello" }] }, - ], - include: ["reasoning.encrypted_content"], - store: false, - }) - expect(requests[0]?.reasoning).toEqual( - settings.reasoningEffort !== undefined || settings.reasoningSummary !== undefined - ? { - ...(settings.reasoningEffort !== undefined ? { effort: settings.reasoningEffort } : {}), - ...(settings.reasoningSummary !== undefined ? { summary: settings.reasoningSummary } : {}), - } - : undefined, - ) - expect(requests[0]?.service_tier).toBe(settings.serviceTier) - expect(requests[0]?.stream).toBe(mode === "stream" ? true : undefined) - expect(warnings).toEqual([]) - }), - ) - } - } -}