Providers: disable developer role for DashScope-compatible endpoints (#24675)

* Agents: disable developer role for DashScope-compatible endpoints

* Agents: test DashScope developer-role compatibility

* Gateway: test allowlisted sessions.patch model selection

* Changelog: add DashScope role-compat fix note
This commit is contained in:
Vincent Koc
2026-02-23 19:51:16 -05:00
committed by GitHub
parent 83eae14ed6
commit 30c622554f
4 changed files with 69 additions and 1 deletions

View File

@@ -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.

View File

@@ -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(),

View File

@@ -4,6 +4,14 @@ function isOpenAiCompletionsModel(model: Model<Api>): 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<Api>): Model<Api> {
const baseUrl = model.baseUrl ?? "";
const isZai = model.provider === "zai" || baseUrl.includes("api.z.ai");
@@ -11,7 +19,8 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
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;
}

View File

@@ -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<string, SessionEntry> = {};
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<string, SessionEntry> = {};
const res = await applySessionsPatchToStore({