From 7ae9fff663b5227fe214c5225a767ed4c4380a7b Mon Sep 17 00:00:00 2001 From: fjia Date: Wed, 17 Jun 2026 10:34:53 +0700 Subject: [PATCH] fix(executors): strip params unsupported by provider/model Add config-driven stripUnsupportedParams helper and use it in the default and github executors. Removes the deprecated temperature param for claude-opus-4 models (Anthropic 400) and consolidates github's scattered capability checks into one rule table. Fixes #1748 Co-Authored-By: fjia Co-authored-by: Cursor --- open-sse/executors/default.js | 2 + open-sse/executors/github.js | 41 ++------------------ open-sse/translator/concerns/paramSupport.js | 31 +++++++++++++++ 3 files changed, 36 insertions(+), 38 deletions(-) create mode 100644 open-sse/translator/concerns/paramSupport.js diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index 2f3752b4..e80fe228 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -6,6 +6,7 @@ import { buildClineHeaders } from "../shared/clineAuth.js"; import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; +import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js"; // Auth header descriptors — derived from registry transport.auth, fallback to hardcoded defaults. const BEARER = { combined: true, header: "Authorization", scheme: "bearer" }; @@ -88,6 +89,7 @@ export class DefaultExecutor extends BaseExecutor { if (this.config.quirks?.dropClientMetadata) { delete transformed.client_metadata; } + stripUnsupportedParams(this.provider, model, transformed); } return injectReasoningContent({ provider: this.provider, model, body: transformed }); diff --git a/open-sse/executors/github.js b/open-sse/executors/github.js index 67baa969..2f4d68ba 100644 --- a/open-sse/executors/github.js +++ b/open-sse/executors/github.js @@ -7,6 +7,7 @@ import { openaiResponsesToOpenAIResponse } from "../translator/response/openai-r import { initState } from "../translator/index.js"; import { parseSSELine, formatSSE } from "../utils/streamHelpers.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; +import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js"; import { SSE_DONE } from "../utils/sseConstants.js"; import crypto from "crypto"; @@ -109,54 +110,18 @@ export class GithubExecutor extends BaseExecutor { return /gpt-5|o[134]-/i.test(model); } - // Some models (like gpt-5.4) don't support the temperature parameter - supportsTemperature(model) { - // gpt-5.4 and similar newer models don't support temperature - return !/gpt-5\.4/i.test(model); - } - - // GitHub Copilot /chat/completions rejects Claude-style thinking payloads - // (OpenClaw sends thinking: { type: "enabled" } → upstream 400). - // GPT-5 family on Copilot DOES honor reasoning_effort, so only strip for Claude. (#713) - supportsThinking(model) { - return !/claude/i.test(model); - } - - // reasoning_effort works for GPT-5 family AND Claude Opus 4.6 / Sonnet 4.6 - // on GitHub Copilot. Only strip for models that don't support it: - // Claude Haiku 4.5, Claude Opus 4.7 (rejected upstream). - supportsReasoningEffort(model) { - const m = model.toLowerCase(); - // Claude models that DO support reasoning_effort - if (/claude.*opus.*4\.6/i.test(m) || /claude.*sonnet.*4\.6/i.test(m)) return true; - // All other Claude models: strip - if (/claude/i.test(model)) return false; - // GPT-5 family, Gemini, etc.: keep - return true; - } - transformRequest(model, body, stream, credentials) { const transformed = { ...body }; if (this.requiresMaxCompletionTokens(model) && transformed.max_tokens !== undefined) { transformed.max_completion_tokens = transformed.max_tokens; delete transformed.max_tokens; } - // Strip temperature for models that don't support it - if (!this.supportsTemperature(model) && transformed.temperature !== undefined) { - delete transformed.temperature; - } - // Always strip Claude-style thinking payload (Copilot doesn't understand it) - if (!this.supportsThinking(model)) { - delete transformed.thinking; - } // "none" means no thinking — strip it so models that don't support "none" don't 400 if (transformed.reasoning_effort === "none") { delete transformed.reasoning_effort; } - // Strip reasoning_effort only for models that reject it - if (!this.supportsReasoningEffort(model) && transformed.reasoning_effort !== undefined) { - delete transformed.reasoning_effort; - } + // Config-driven strip of params unsupported by this provider/model + stripUnsupportedParams("github", model, transformed); return transformed; } diff --git a/open-sse/translator/concerns/paramSupport.js b/open-sse/translator/concerns/paramSupport.js new file mode 100644 index 00000000..3b8fc9a9 --- /dev/null +++ b/open-sse/translator/concerns/paramSupport.js @@ -0,0 +1,31 @@ +// Strip request params a given provider/model rejects upstream (e.g. HTTP 400). +// Config-driven: add a rule instead of scattering `delete body.x` across executors. + +// Each rule: optional provider, regex match on model, list of params to drop. +// A param is removed only when it is present (!== undefined). +const STRIP_RULES = [ + // claude-opus-4 series: temperature is deprecated (Anthropic 400). #1748 + { match: /claude-opus-4/i, drop: ["temperature"] }, + // GitHub Copilot gpt-5.4: temperature unsupported. + { provider: "github", match: /gpt-5\.4/i, drop: ["temperature"] }, + // GitHub Copilot Claude (except opus/sonnet 4.6): thinking + reasoning_effort rejected. #713 + { provider: "github", match: (m) => /claude/i.test(m) && !/claude.*(opus|sonnet).*4\.6/i.test(m), drop: ["thinking", "reasoning_effort"] }, +]; + +// Test a rule's match (regex or predicate) against the model id. +function matches(rule, model) { + return typeof rule.match === "function" ? rule.match(model) : rule.match.test(model); +} + +// Remove unsupported params from body in place; returns body. +export function stripUnsupportedParams(provider, model, body) { + if (!model || !body || typeof body !== "object") return body; + for (const rule of STRIP_RULES) { + if (rule.provider && rule.provider !== provider) continue; + if (!matches(rule, model)) continue; + for (const key of rule.drop) { + if (body[key] !== undefined) delete body[key]; + } + } + return body; +}