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 <fjia@suntekcorps.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { buildClineHeaders } from "../shared/clineAuth.js";
|
|||||||
import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js";
|
import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js";
|
||||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
import { injectReasoningContent } from "../utils/reasoningContentInjector.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.
|
// Auth header descriptors — derived from registry transport.auth, fallback to hardcoded defaults.
|
||||||
const BEARER = { combined: true, header: "Authorization", scheme: "bearer" };
|
const BEARER = { combined: true, header: "Authorization", scheme: "bearer" };
|
||||||
@@ -88,6 +89,7 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
if (this.config.quirks?.dropClientMetadata) {
|
if (this.config.quirks?.dropClientMetadata) {
|
||||||
delete transformed.client_metadata;
|
delete transformed.client_metadata;
|
||||||
}
|
}
|
||||||
|
stripUnsupportedParams(this.provider, model, transformed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return injectReasoningContent({ provider: this.provider, model, body: transformed });
|
return injectReasoningContent({ provider: this.provider, model, body: transformed });
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { openaiResponsesToOpenAIResponse } from "../translator/response/openai-r
|
|||||||
import { initState } from "../translator/index.js";
|
import { initState } from "../translator/index.js";
|
||||||
import { parseSSELine, formatSSE } from "../utils/streamHelpers.js";
|
import { parseSSELine, formatSSE } from "../utils/streamHelpers.js";
|
||||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
|
import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js";
|
||||||
import { SSE_DONE } from "../utils/sseConstants.js";
|
import { SSE_DONE } from "../utils/sseConstants.js";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
@@ -109,54 +110,18 @@ export class GithubExecutor extends BaseExecutor {
|
|||||||
return /gpt-5|o[134]-/i.test(model);
|
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) {
|
transformRequest(model, body, stream, credentials) {
|
||||||
const transformed = { ...body };
|
const transformed = { ...body };
|
||||||
if (this.requiresMaxCompletionTokens(model) && transformed.max_tokens !== undefined) {
|
if (this.requiresMaxCompletionTokens(model) && transformed.max_tokens !== undefined) {
|
||||||
transformed.max_completion_tokens = transformed.max_tokens;
|
transformed.max_completion_tokens = transformed.max_tokens;
|
||||||
delete 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
|
// "none" means no thinking — strip it so models that don't support "none" don't 400
|
||||||
if (transformed.reasoning_effort === "none") {
|
if (transformed.reasoning_effort === "none") {
|
||||||
delete transformed.reasoning_effort;
|
delete transformed.reasoning_effort;
|
||||||
}
|
}
|
||||||
// Strip reasoning_effort only for models that reject it
|
// Config-driven strip of params unsupported by this provider/model
|
||||||
if (!this.supportsReasoningEffort(model) && transformed.reasoning_effort !== undefined) {
|
stripUnsupportedParams("github", model, transformed);
|
||||||
delete transformed.reasoning_effort;
|
|
||||||
}
|
|
||||||
return transformed;
|
return transformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
open-sse/translator/concerns/paramSupport.js
Normal file
31
open-sse/translator/concerns/paramSupport.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user