diff --git a/CHANGELOG.md b/CHANGELOG.md index f81b88517..71dcb752b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai - Auto-reply/Sessions: remove auth-key labels from `/new` and `/reset` confirmation messages so session reset notices never expose API key prefixes or env-key labels in chat output. (#24384, #24409) Thanks @Clawborn. - Slack/Group policy: move Slack account `groupPolicy` defaulting to provider-level schema defaults so multi-account configs inherit top-level `channels.slack.groupPolicy` instead of silently overriding inheritance with per-account `allowlist`. (#17579) Thanks @ZetiMente. - Providers/Anthropic: skip `context-1m-*` beta injection for OAuth/subscription tokens (`sk-ant-oat-*`) while preserving OAuth-required betas, avoiding Anthropic 401 auth failures when `params.context1m` is enabled. (#10647, #20354) Thanks @ClumsyWizardHands and @dcruver. +- Providers/DashScope: mark DashScope-compatible `openai-completions` endpoints as `supportsDeveloperRole=false` so OpenClaw sends `system` instead of unsupported `developer` role on Qwen/DashScope APIs. (#19130) Thanks @Putzhuawa and @vincentkoc. - Providers/Bedrock: disable prompt-cache retention for non-Anthropic Bedrock models so Nova/Mistral requests do not send unsupported cache metadata. (#20866) Thanks @pierreeurope. - Providers/Bedrock: apply Anthropic-Claude cacheRetention defaults and runtime pass-through for `amazon-bedrock/*anthropic.claude*` model refs, while keeping non-Anthropic Bedrock models excluded. (#22303) Thanks @snese. - Providers/OpenRouter: remove conflicting top-level `reasoning_effort` when injecting nested `reasoning.effort`, preventing OpenRouter 400 payload-validation failures for reasoning models. (#24120) thanks @tenequm. diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.test.ts index 962724c66..a7404d304 100644 --- a/src/agents/model-compat.test.ts +++ b/src/agents/model-compat.test.ts @@ -77,6 +77,32 @@ describe("normalizeModelCompat", () => { ).toBe(false); }); + it("forces supportsDeveloperRole off for DashScope provider ids", () => { + const model = { + ...baseModel(), + provider: "dashscope", + baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", + }; + delete (model as { compat?: unknown }).compat; + const normalized = normalizeModelCompat(model); + expect( + (normalized.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole, + ).toBe(false); + }); + + it("forces supportsDeveloperRole off for DashScope-compatible endpoints", () => { + const model = { + ...baseModel(), + provider: "custom-qwen", + baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + }; + delete (model as { compat?: unknown }).compat; + const normalized = normalizeModelCompat(model); + expect( + (normalized.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole, + ).toBe(false); + }); + it("leaves non-zai models untouched", () => { const model = { ...baseModel(), diff --git a/src/agents/model-compat.ts b/src/agents/model-compat.ts index d97d39651..2b5eba130 100644 --- a/src/agents/model-compat.ts +++ b/src/agents/model-compat.ts @@ -4,6 +4,14 @@ function isOpenAiCompletionsModel(model: Model): model is Model<"openai-com return model.api === "openai-completions"; } +function isDashScopeCompatibleEndpoint(baseUrl: string): boolean { + return ( + baseUrl.includes("dashscope.aliyuncs.com") || + baseUrl.includes("dashscope-intl.aliyuncs.com") || + baseUrl.includes("dashscope-us.aliyuncs.com") + ); +} + export function normalizeModelCompat(model: Model): Model { const baseUrl = model.baseUrl ?? ""; const isZai = model.provider === "zai" || baseUrl.includes("api.z.ai"); @@ -11,7 +19,8 @@ export function normalizeModelCompat(model: Model): Model { model.provider === "moonshot" || baseUrl.includes("moonshot.ai") || baseUrl.includes("moonshot.cn"); - if ((!isZai && !isMoonshot) || !isOpenAiCompletionsModel(model)) { + const isDashScope = model.provider === "dashscope" || isDashScopeCompatibleEndpoint(baseUrl); + if ((!isZai && !isMoonshot && !isDashScope) || !isOpenAiCompletionsModel(model)) { return model; } diff --git a/src/gateway/sessions-patch.test.ts b/src/gateway/sessions-patch.test.ts index 5d07ea1a1..3d8c575cf 100644 --- a/src/gateway/sessions-patch.test.ts +++ b/src/gateway/sessions-patch.test.ts @@ -179,6 +179,38 @@ describe("gateway sessions patch", () => { expect(res.entry.authProfileOverrideCompactionCount).toBeUndefined(); }); + test("accepts explicit allowlisted provider/model refs from sessions.patch", async () => { + const store: Record = {}; + const cfg = { + agents: { + defaults: { + model: { primary: "openai/gpt-5.2" }, + models: { + "anthropic/claude-sonnet-4-6": { alias: "sonnet" }, + }, + }, + }, + } as OpenClawConfig; + + const res = await applySessionsPatchToStore({ + cfg, + store, + storeKey: "agent:main:main", + patch: { key: "agent:main:main", model: "anthropic/claude-sonnet-4-6" }, + loadGatewayModelCatalog: async () => [ + { provider: "anthropic", id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, + { provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }, + ], + }); + + expect(res.ok).toBe(true); + if (!res.ok) { + return; + } + expect(res.entry.providerOverride).toBe("anthropic"); + expect(res.entry.modelOverride).toBe("claude-sonnet-4-6"); + }); + test("sets spawnDepth for subagent sessions", async () => { const store: Record = {}; const res = await applySessionsPatchToStore({