#22741: fix(models): add DashScope/Qwen to normalizeModelCompat developer role guard (#22710)
agents
size: S
experienced-contributor
Cluster:
DashScope/Qwen Role Management
## Summary
- **Bug**: DashScope Qwen3 reasoning models fail with `developer is not one of ['system', 'assistant', 'user', 'tool', 'function']` because `normalizeModelCompat()` only guards Z.ai but not DashScope/Qwen providers
- **Root cause**: `normalizeModelCompat()` in `src/agents/model-compat.ts` only checks for Z.ai provider/baseUrl, so DashScope and Qwen Portal models with `reasoning: true` get `supportsDeveloperRole` defaulting to `true` from pi-ai's `detectCompat()`, causing `developer` role to be sent
- **Fix**: Extend `normalizeModelCompat()` to also detect DashScope (`dashscope` provider / `dashscope.aliyuncs.com` baseUrl) and Qwen Portal (`qwen-portal` provider / `portal.qwen.ai` baseUrl), setting `supportsDeveloperRole: false` for these providers
Fixes #22710
## Problem
When a DashScope/Qwen model has `reasoning: true`, the openai-completions provider sends `developer` role for system prompts. DashScope's compatible-mode API does not support the `developer` role and returns HTTP 400:
```
developer is not one of ['system', 'assistant', 'user', 'tool', 'function']
```
**Before fix:**
```bash
curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-d '{"model":"qwen3.5-plus","messages":[{"role":"developer","content":"You are helpful."},{"role":"user","content":"Hello"}]}'
# → 400: "developer is not one of ['system', 'assistant', 'user', 'tool', 'function']"
```
## Approach
This fix follows the same pattern established for Z.ai in `normalizeModelCompat()`. Z.ai previously encountered the identical issue — its compatible API doesn't support the `developer` role either — and the project introduced `normalizeModelCompat()` as an application-level guard to set `supportsDeveloperRole: false` before models reach pi-ai's provider pipeline.
DashScope and Qwen Portal have the same constraint, so we extend the existing `needsDeveloperRoleOff` guard to cover them:
```typescript
const needsDeveloperRoleOff =
provider === "zai" ||
baseUrl.includes("api.z.ai") ||
provider === "dashscope" || // new
baseUrl.includes("dashscope.aliyuncs.com") || // new
provider === "qwen-portal" || // new
baseUrl.includes("portal.qwen.ai"); // new
```
**Why this approach works well:**
- Minimal change — extends an existing, proven pattern rather than introducing new abstractions
- Centralized guard — catches all DashScope/Qwen models regardless of where they're configured (user config, presets, etc.)
- Defensive — both `provider` string and `baseUrl` substring are checked, so it works even if only one is set
**Suggestion for a more robust long-term fix:**
The current approach (both this PR and the original Z.ai guard) uses a blacklist: providers not in the list default to `supportsDeveloperRole: true`. This means every new provider that doesn't support `developer` role requires a code change.
A more robust approach would be to invert the default in pi-ai's `detectCompat()` — use a whitelist of providers known to support `developer` role (OpenAI, Azure OpenAI, etc.), and default all others to `supportsDeveloperRole: false`. The `system` role is universally supported by all OpenAI-compatible APIs, while `developer` role is only supported by a few. Defaulting to `system` would be safer and eliminate the need for per-provider patches.
This would be a change in pi-ai's `detectCompat()` rather than in `normalizeModelCompat()`, so it's out of scope for this PR.
## Changes
- `src/agents/model-compat.ts` — Extend provider detection to include `dashscope`/`dashscope.aliyuncs.com` and `qwen-portal`/`portal.qwen.ai`, refactor the guard into a single `needsDeveloperRoleOff` boolean
- `src/agents/model-compat.e2e.test.ts` — Add 3 regression tests: DashScope provider, Qwen Portal provider, and explicit compat preservation
- `src/agents/dashscope-developer-role.live.test.ts` — Add live e2e test verifying real DashScope API behavior (requires `DASHSCOPE_LIVE_TEST=1` + API key)
- `CHANGELOG.md` — Add fix entry
**After fix:**
DashScope/Qwen models get `supportsDeveloperRole: false` via `normalizeModelCompat()`, so the provider sends `system` role instead of `developer`, and requests succeed.
## Test plan
- [x] Live e2e: `completeSimple()` with `normalizeModelCompat` → DashScope returns response ✅
- [x] Live e2e: `completeSimple()` without `normalizeModelCompat` (developer role) → DashScope returns 400 error ✅
- [x] Real API curl: `developer` role → 400; `system` role → 200
- [x] New test: DashScope provider gets `supportsDeveloperRole: false`
- [x] New test: Qwen Portal provider gets `supportsDeveloperRole: false`
- [x] New test: Explicit DashScope compat `false` is preserved
- [x] All 3 existing Z.ai tests still pass (6 total e2e tests pass)
- [x] Format check passes
## Effect on User Experience
**Before:** DashScope Qwen3 reasoning models fail on every request with a cryptic 400 error about unsupported `developer` role. Users must manually add `compat: { supportsDeveloperRole: false }` to their model config as a workaround.
**After:** DashScope and Qwen Portal models automatically get the correct compat setting. Reasoning models work out of the box without manual configuration.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Extends `normalizeModelCompat()` to disable the unsupported `developer` role for DashScope and Qwen Portal providers. Previously only Z.ai was guarded, causing DashScope/Qwen reasoning models to fail with HTTP 400 errors. The fix adds provider/baseUrl detection for both DashScope (`dashscope`/`dashscope.aliyuncs.com`) and Qwen Portal (`qwen-portal`/`portal.qwen.ai`), consolidating the logic into a clear `needsDeveloperRoleOff` boolean.
- Refactored provider detection to use a single boolean guard
- Added comprehensive test coverage for both new providers
- Preserves explicit compat settings to avoid unnecessary mutations
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk
- The fix is a straightforward extension of existing, well-tested logic. It adds defensive checks for two additional providers using the same pattern as Z.ai, with comprehensive test coverage including edge cases. The early-return structure prevents any impact on non-openai-completions models, and explicit compat preservation ensures no unintended mutations.
- No files require special attention
<sub>Last reviewed commit: 499c2ea</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#23711: fix: disable developer role for DashScope/Qwen models (#23575)
by echoVic · 2026-02-22
91.3%
#23655: fix(model): map developer role to system for Aliyun/Dashscope/Qianf...
by SleuthCo · 2026-02-22
84.2%
#11297: fix: add thinkingFormat 'qwen' compat for bailian/dashscope providers
by AdJIa · 2026-02-07
81.0%
#14187: fix: add Moonshot AI to non-standard provider detection
by shad0wca7 · 2026-02-11
77.4%
#23256: fix(providers): disable developer role for zhipu provider and bigmo...
by SidQin-cyber · 2026-02-22
76.5%
#16766: fix(model): apply provider baseUrl/headers override to registry-fou...
by dzianisv · 2026-02-15
75.8%
#9451: feat(qwen): enable DashScope/Qwen enable_thinking for /think
by sm-yjr · 2026-02-05
75.7%
#6673: fix: preserve allowAny flag in createModelSelectionState for custom...
by tenor0 · 2026-02-01
75.2%
#22194: fix(agent) Moonshot/Kimi kimi-k2.5 returns ROLE_UNSPECIFIED
by ShengFuC · 2026-02-20
74.8%
#16290: fix: add field-level validation for custom LLM provider config
by superlowburn · 2026-02-14
74.5%