diff --git a/.gitignore b/.gitignore
index 7ab3576d..1c5efc1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ wheels/
.vscode
.env
.env.local
+AGENTS.local
/tests_local
uv.toml
.idea/*
diff --git a/AGENTS.md b/AGENTS.md
index af2245e7..a2f47632 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,6 +4,13 @@ This file is the root guidance for AI agents working in this repository. It is i
Pythinker sessions via `PYTHINKER_AGENTS_MD`; keep it durable, portable, and focused on rules
that should apply across many tasks.
+## Local-only instructions
+
+If `AGENTS.local` exists at the repository root, read it after this file for machine-specific or
+private local instructions. `AGENTS.local` is intentionally gitignored; do not commit it or copy its
+contents into tracked files. Local instructions may add workflow details, but they must not weaken
+or override this repository's non-negotiable rules.
+
## Mission
Pythinker CLI is a Python CLI agent for software engineering workflows. It supports an
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99c7a3e7..f2a23ff8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,14 @@ GitHub Releases page; `0.8.0` is the new starting line.
## Unreleased
+## 0.36.0 (2026-06-05)
+
+- **Alibaba DashScope multi-region fallback.** Logging in with a China-region key (`dashscope.aliyuncs.com`) against the default US Virginia endpoint now auto-detects the mismatch and reconfigures for the correct endpoint rather than failing with a misleading "API key is wrong" error.
+- **Alibaba Token Plan compatibility (`sk-ws-`).** `/login alibaba` now requires the dedicated workspace Base URL shown in the Token Plan console instead of accepting a public `/models` response as credential validation. Dedicated workspace endpoints hide Kimi K2.6 when Alibaba advertises it without a working route, and use non-streaming Chat Completions for DeepSeek V3.2 because those endpoints return an empty SSE stream. Kimi requests on other Alibaba routes use DashScope's `enable_thinking` parameter.
+- **Alibaba model catalog refresh.** Added Qwen3.7 Plus (1M context), Qwen3 Coder Plus, and Qwen3 Coder Flash. Removed `kimi-k2.5`, `glm-5`, and `MiniMax-M2.5` (absent from the live endpoint). Corrected Qwen3.7 Max context window to 1M tokens.
+
+Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.36.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
+
## 0.35.0 (2026-06-04)
- **Alibaba DashScope provider and MiniMax M3 catalog.** `/login alibaba` now configures Alibaba Cloud Model Studio / DashScope Token Plan models, including workspace-compatible endpoints and native GLM thinking behavior. MiniMax API-key login now defaults to MiniMax M3 with its larger context and multimodal capabilities.
diff --git a/README.md b/README.md
index 3a8b8351..472e6ecd 100644
--- a/README.md
+++ b/README.md
@@ -50,13 +50,13 @@ It speaks the [**Agent Client Protocol (ACP)**](https://github.com/agentclientpr
---
-## 🆕 What's New in 0.35.0
+## 🆕 What's New in 0.36.0
-- **`/stats` usage dashboard.** New slash command opens an interactive TUI showing token and cost breakdown by provider/model across Today / This Week / Last Week / All Time. Powered by a static pricing table and a session collector that walks `~/.pythinker/sessions/` wire files.
-- **Z AI provider auth.** Login/logout via API key, model discovery, and OAuth selector wired into the TUI and `refresh_managed_models`.
-- **Moonshot provider auth.** Login/logout via API key, model discovery (Kimi K2.x catalog), OAuth selector wired into the TUI and `refresh_managed_models`.
+- **Alibaba DashScope multi-region fallback.** China-region keys now auto-detect the endpoint mismatch and reconfigure correctly instead of showing a misleading "API key is wrong" error.
+- **Alibaba Token Plan compatibility (`sk-ws-`).** `/login alibaba` now asks for the dedicated workspace endpoint, avoids unroutable Kimi entries on those endpoints, and uses DeepSeek V3.2's working non-streaming mode.
+- **Alibaba model catalog refresh.** Qwen3.7 Plus, Qwen3 Coder Plus, and Qwen3 Coder Flash added; deprecated `kimi-k2.5`, `glm-5`, and `MiniMax-M2.5` removed.
-Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.35.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
+Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.36.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
---
@@ -146,7 +146,7 @@ matches your OS — no Python, Node, or `uv` prerequisite.
| Platform | Recommended install | Artifact source |
|---|---|---|
-| **🪟 Windows** | `irm https://pythinker.com/install.ps1 \| iex` | `PythinkerSetup-0.35.0.exe` from [Releases](https://github.com/Pythoughts-labs/pythinker-code/releases/latest) |
+| **🪟 Windows** | `irm https://pythinker.com/install.ps1 \| iex` | `PythinkerSetup-0.36.0.exe` from [Releases](https://github.com/Pythoughts-labs/pythinker-code/releases/latest) |
| **
/
** | `curl -fsSL https://pythinker.com/install.sh \| bash` | native tarball from [Releases](https://github.com/Pythoughts-labs/pythinker-code/releases/latest) |
| **
— Homebrew** | `brew install Pythoughts-labs/pythinker/pythinker-code` | auto-published Homebrew tap |
| **🐳 Docker** | `docker run --rm -it ghcr.io/pythoughts-labs/pythinker-code` | GHCR multi-arch image |
@@ -174,7 +174,7 @@ pythinker # start the interactive TUI
### 🪟 Windows — native installer
-`PythinkerSetup-0.35.0.exe` is a signed* Inno Setup wizard. Installs per-user
+`PythinkerSetup-0.36.0.exe` is a signed* Inno Setup wizard. Installs per-user
into `%LOCALAPPDATA%\Programs\Pythinker`, registers `pythinker` on your user
PATH (`HKCU\Environment`), broadcasts `WM_SETTINGCHANGE` so new shells see
the change. **No UAC prompt.**
@@ -185,13 +185,13 @@ irm https://pythinker.com/install.ps1 | iex
# Or manually download the installer + checksum from the Releases page,
# verify with Get-FileHash, then run:
-.\PythinkerSetup-0.35.0.exe
+.\PythinkerSetup-0.36.0.exe
# Open a fresh PowerShell
pythinker --version
```
-**Per-machine install** (IT-managed boxes): `.\PythinkerSetup-0.35.0.exe /ALLUSERS`
+**Per-machine install** (IT-managed boxes): `.\PythinkerSetup-0.36.0.exe /ALLUSERS`
installs to `%ProgramFiles%\Pythinker` and writes PATH to HKLM (requires admin).
**Upgrade:** `pythinker update` from inside the running app — it downloads
@@ -242,26 +242,26 @@ attached to every GitHub Release.
```sh
# Debian / Ubuntu (x86_64)
-sudo dpkg -i pythinker-code_0.35.0_amd64.deb
+sudo dpkg -i pythinker-code_0.36.0_amd64.deb
sudo apt-get install -f # only if dpkg reports missing deps
# Debian / Ubuntu (ARM64)
-sudo dpkg -i pythinker-code_0.35.0_arm64.deb
+sudo dpkg -i pythinker-code_0.36.0_arm64.deb
# Fedora / RHEL / openSUSE (x86_64)
-curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.35.0/pythinker-code-0.35.0.x86_64.rpm
-curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.35.0/pythinker-code-0.35.0.x86_64.rpm.sha256
-sha256sum -c pythinker-code-0.35.0.x86_64.rpm.sha256
+curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.36.0/pythinker-code-0.36.0.x86_64.rpm
+curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.36.0/pythinker-code-0.36.0.x86_64.rpm.sha256
+sha256sum -c pythinker-code-0.36.0.x86_64.rpm.sha256
# Fedora / RHEL:
-sudo dnf install ./pythinker-code-0.35.0.x86_64.rpm
+sudo dnf install ./pythinker-code-0.36.0.x86_64.rpm
# openSUSE:
-sudo zypper install ./pythinker-code-0.35.0.x86_64.rpm
+sudo zypper install ./pythinker-code-0.36.0.x86_64.rpm
# Fedora / RHEL (aarch64)
-curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.35.0/pythinker-code-0.35.0.aarch64.rpm
-curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.35.0/pythinker-code-0.35.0.aarch64.rpm.sha256
-sha256sum -c pythinker-code-0.35.0.aarch64.rpm.sha256
-sudo dnf install ./pythinker-code-0.35.0.aarch64.rpm
+curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.36.0/pythinker-code-0.36.0.aarch64.rpm
+curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.36.0/pythinker-code-0.36.0.aarch64.rpm.sha256
+sha256sum -c pythinker-code-0.36.0.aarch64.rpm.sha256
+sudo dnf install ./pythinker-code-0.36.0.aarch64.rpm
```
Both packages drop a small `/usr/bin/pythinker` launcher that execs the real
@@ -270,8 +270,8 @@ binary under `/usr/lib/pythinker/`, so your `$PATH` stays tidy.
**Verify before install:**
```sh
-sha256sum -c pythinker-code_0.35.0_amd64.deb.sha256 # Debian/Ubuntu
-sha256sum -c pythinker-code-0.35.0.x86_64.rpm.sha256 # Fedora/RHEL
+sha256sum -c pythinker-code_0.36.0_amd64.deb.sha256 # Debian/Ubuntu
+sha256sum -c pythinker-code-0.36.0.x86_64.rpm.sha256 # Fedora/RHEL
```
**Upgrade:** download the new `.deb`/`.rpm` from Releases and `dpkg -i` /
diff --git a/docs/en/guides/getting-started.md b/docs/en/guides/getting-started.md
index c9fc968c..e34c51fc 100644
--- a/docs/en/guides/getting-started.md
+++ b/docs/en/guides/getting-started.md
@@ -44,7 +44,7 @@ On Windows, run the PowerShell bootstrap. It downloads the native installer, ver
irm https://pythinker.com/install.ps1 | iex
```
-You can also download `PythinkerSetup-0.35.0.exe` manually from the [latest release](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
+You can also download `PythinkerSetup-0.36.0.exe` manually from the [latest release](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
Verify the installation:
diff --git a/docs/en/release-notes/breaking-changes.md b/docs/en/release-notes/breaking-changes.md
index 07f86020..8f7a3994 100644
--- a/docs/en/release-notes/breaking-changes.md
+++ b/docs/en/release-notes/breaking-changes.md
@@ -4,6 +4,10 @@ This page documents breaking changes in Pythinker Code releases and provides mig
## Unreleased
+## 0.36.0 (2026-06-05)
+
+No breaking changes. This release is compatible with 0.35.0 user configuration, native installs, and session data.
+
## 0.35.0 (2026-06-04)
## 0.34.0 (2026-06-03)
diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md
index 00ccdf3a..75295dbb 100644
--- a/docs/en/release-notes/changelog.md
+++ b/docs/en/release-notes/changelog.md
@@ -17,6 +17,14 @@ GitHub Releases page; `0.8.0` is the new starting line.
## Unreleased
+## 0.36.0 (2026-06-05)
+
+- **Alibaba DashScope multi-region fallback.** Logging in with a China-region key (`dashscope.aliyuncs.com`) against the default US Virginia endpoint now auto-detects the mismatch and reconfigures for the correct endpoint rather than failing with a misleading "API key is wrong" error.
+- **Alibaba Token Plan compatibility (`sk-ws-`).** `/login alibaba` now requires the dedicated workspace Base URL shown in the Token Plan console instead of accepting a public `/models` response as credential validation. Dedicated workspace endpoints hide Kimi K2.6 when Alibaba advertises it without a working route, and use non-streaming Chat Completions for DeepSeek V3.2 because those endpoints return an empty SSE stream. Kimi requests on other Alibaba routes use DashScope's `enable_thinking` parameter.
+- **Alibaba model catalog refresh.** Added Qwen3.7 Plus (1M context), Qwen3 Coder Plus, and Qwen3 Coder Flash. Removed `kimi-k2.5`, `glm-5`, and `MiniMax-M2.5` (absent from the live endpoint). Corrected Qwen3.7 Max context window to 1M tokens.
+
+Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.36.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
+
## 0.35.0 (2026-06-04)
- **Alibaba DashScope provider and MiniMax M3 catalog.** `/login alibaba` now configures Alibaba Cloud Model Studio / DashScope Token Plan models, including workspace-compatible endpoints and native GLM thinking behavior. MiniMax API-key login now defaults to MiniMax M3 with its larger context and multimodal capabilities.
diff --git a/packages/linux-installer/README.md b/packages/linux-installer/README.md
index d261dda4..b6dcce0a 100644
--- a/packages/linux-installer/README.md
+++ b/packages/linux-installer/README.md
@@ -8,19 +8,19 @@ End-user install from the current GitHub Release:
```sh
# Debian / Ubuntu
-curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.35.0/pythinker-code_0.35.0_amd64.deb
-curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.35.0/pythinker-code_0.35.0_amd64.deb.sha256
-sha256sum -c pythinker-code_0.35.0_amd64.deb.sha256
-sudo dpkg -i pythinker-code_0.35.0_amd64.deb
+curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.36.0/pythinker-code_0.36.0_amd64.deb
+curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.36.0/pythinker-code_0.36.0_amd64.deb.sha256
+sha256sum -c pythinker-code_0.36.0_amd64.deb.sha256
+sudo dpkg -i pythinker-code_0.36.0_amd64.deb
sudo apt-get install -f # only needed if dependencies fail to resolve
# Fedora / RHEL / openSUSE
-curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.35.0/pythinker-code-0.35.0.x86_64.rpm
-curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.35.0/pythinker-code-0.35.0.x86_64.rpm.sha256
-sha256sum -c pythinker-code-0.35.0.x86_64.rpm.sha256
-sudo dnf install ./pythinker-code-0.35.0.x86_64.rpm
+curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.36.0/pythinker-code-0.36.0.x86_64.rpm
+curl -LO https://github.com/Pythoughts-labs/pythinker-code/releases/download/v0.36.0/pythinker-code-0.36.0.x86_64.rpm.sha256
+sha256sum -c pythinker-code-0.36.0.x86_64.rpm.sha256
+sudo dnf install ./pythinker-code-0.36.0.x86_64.rpm
# or, on openSUSE:
-sudo zypper install ./pythinker-code-0.35.0.x86_64.rpm
+sudo zypper install ./pythinker-code-0.36.0.x86_64.rpm
```
The package drops a single executable at `/usr/bin/pythinker` and a license
@@ -36,17 +36,17 @@ file at `/usr/share/doc/pythinker-code/LICENSE`.
## Build
```sh
-bash packages/linux-installer/build.sh 0.35.0
+bash packages/linux-installer/build.sh 0.36.0
```
Outputs to `dist/`:
-- `pythinker-code_0.35.0_amd64.deb`
-- `pythinker-code-0.35.0.x86_64.rpm`
+- `pythinker-code_0.36.0_amd64.deb`
+- `pythinker-code-0.36.0.x86_64.rpm`
The portable tarball used by `scripts/install-native.sh` is published by
the existing `release-pythinker-cli.yml` workflow under the cargo-dist
-target-triple naming (e.g. `pythinker-0.35.0-x86_64-unknown-linux-gnu.tar.gz`).
+target-triple naming (e.g. `pythinker-0.36.0-x86_64-unknown-linux-gnu.tar.gz`).
## CI
diff --git a/pyproject.toml b/pyproject.toml
index 54fbf20b..3b45c02c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "pythinker-code"
-version = "0.35.0"
+version = "0.36.0"
description = "Pythinker — an agentic CLI developed by Pythoughts-labs."
readme = "README.md"
requires-python = ">=3.12"
diff --git a/src/pythinker_code/agents/default/code_reviewer.yaml b/src/pythinker_code/agents/default/code_reviewer.yaml
index d1ad020e..510423a2 100644
--- a/src/pythinker_code/agents/default/code_reviewer.yaml
+++ b/src/pythinker_code/agents/default/code_reviewer.yaml
@@ -26,6 +26,8 @@ agent:
- Build a review context packet: base ref/diff scope or Reviewflow feature IDs, changed behavior, likely tests, user-visible impact, valid evidence paths, omitted/truncated context, and validation evidence.
- Flag only issues introduced or made reachable by the diff.
- Prefer no finding over vague speculation. Every finding must cite concrete evidence and a failure mode.
+ - Run the production guardrail gate before finalizing: check for cache stampedes, connection/resource leaks, missing boundary schemas, unhandled race conditions, naive retry loops, unbounded event callbacks/listeners, and IDOR/tenant-scope mistakes.
+ - Treat missing `finally` cleanup, absent schema validation at trust boundaries, unprotected shared-state mutation, non-jittered immediate retries, or identity from mutable client parameters as reject-level findings when reachable in the changed code.
Freshness check (run BEFORE flagging third-party library or framework misuse):
- For every third-party API, SDK call, framework primitive, or "best practice" the diff turns on, verify the current canonical usage. Prefer a context7 MCP query (e.g. `mcp__context7__query-docs` with the library id) when registered with the parent runtime; otherwise use `SearchWeb` to locate the official documentation and `FetchURL` to read the current page.
diff --git a/src/pythinker_code/agents/default/judge.yaml b/src/pythinker_code/agents/default/judge.yaml
index 0be35baf..22cb6e25 100644
--- a/src/pythinker_code/agents/default/judge.yaml
+++ b/src/pythinker_code/agents/default/judge.yaml
@@ -14,6 +14,7 @@ agent:
- Fidelity: the draft summary matches the actual diff and changes, with no overclaiming.
- Verification: the checks the parent ran are relevant to the change and actually ran, not assumed.
- Safety and scope: no unsafe or destructive action, no secret or PII exposure, no scope creep beyond the request.
+ - Production guardrails: changed code that touches caches, resources, trust boundaries, shared state, outbound requests, long-lived listeners, or authorization context has explicit defenses for stampedes, cleanup, schemas, races, retry storms, leaks, and IDOR risks.
- Findings quality: for reports, each finding is actionable, correctly severity-ranked, and anchored to evidence.
Do not rubber-stamp, and do not pad: prefer a few concrete blockers over broad style notes.
diff --git a/src/pythinker_code/agents/default/review.yaml b/src/pythinker_code/agents/default/review.yaml
index c0d7ba27..b6569831 100644
--- a/src/pythinker_code/agents/default/review.yaml
+++ b/src/pythinker_code/agents/default/review.yaml
@@ -16,6 +16,8 @@ agent:
- If `.pythinker/review-guidelines.md` exists, read it before scoring findings.
- Read the diff or target files before scoring.
- Use Grep/Glob to check sibling call sites, similar patterns, and existing tests.
+ - Apply the production guardrail gate: look specifically for cache stampedes, connection/resource leaks, missing boundary schemas, unhandled race conditions, naive retry loops, unbounded event callbacks/listeners, and IDOR/tenant-scope mistakes.
+ - Reject happy-path code as BLOCKER or MAJOR when the changed path mutates shared state, crosses a trust boundary, acquires resources, retries outbound calls, or registers long-lived callbacks without the matching defensive pattern.
- Score each finding as BLOCKER, MAJOR, MINOR, or NIT.
- Order findings by severity, BLOCKER first.
- Do not request tests unless they cover a distinct behavior or risk introduced by the change.
diff --git a/src/pythinker_code/agents/default/security_reviewer.yaml b/src/pythinker_code/agents/default/security_reviewer.yaml
index 3d341012..c46315e2 100644
--- a/src/pythinker_code/agents/default/security_reviewer.yaml
+++ b/src/pythinker_code/agents/default/security_reviewer.yaml
@@ -9,6 +9,7 @@ agent:
Security review discipline:
- Build a threat context before judging: changed trust boundaries, inputs/outputs, authz/authn, filesystem/network access, secrets, serialization, command execution, and persistence.
+ - Apply the production guardrail gate to security-relevant changes: reject missing boundary schemas, IDOR/tenant-scope mistakes, unprotected shared-state mutations, unsafe retries for non-idempotent outbound calls, and resource leaks that can become denial-of-service vectors.
- Report only reachable or plausibly reachable vulnerabilities backed by evidence. Prefer no finding over speculative risk.
- For each finding, include exploit preconditions, impact, severity rationale, and the smallest safe mitigation.
- Treat secrets/PII carefully: never print raw secret values; redact if needed.
diff --git a/src/pythinker_code/agents/default/system.md b/src/pythinker_code/agents/default/system.md
index d7a13ff0..938facf3 100644
--- a/src/pythinker_code/agents/default/system.md
+++ b/src/pythinker_code/agents/default/system.md
@@ -186,6 +186,28 @@ Code quality defaults (unless project or domain rules override):
- Wrap error-prone I/O, API, network, and resource operations with appropriate error handling, timeouts/fallbacks, and cleanup.
- Adapt to domain standards when relevant (for example stricter MISRA-style practices for critical C/C++ systems).
+## Production Bug Guardrails
+
+When generating, changing, reviewing, or approving production-facing code, optimize for failure modes first: concurrency, resource cleanup, input boundaries, authorization context, data integrity, and retry behavior. Never assume single-threaded, trusted, or low-traffic execution when the code can run in a shared service.
+
+Mandatory defensive patterns:
+
+1. **Cache misses:** If adding cache-aside behavior, serialize identical misses with a local or distributed double-checked lock so concurrent misses do not stampede the backing store.
+2. **Resource acquisition:** For database clients, transactions, streams, sockets, files, and connection pools, acquire immediately before a `try` block and guarantee release/close in `finally`. Transactions that fail must explicitly roll back before release.
+3. **API and webhook boundaries:** Validate runtime inputs at the boundary with the project's schema/validation mechanism, strip or ignore unregistered fields, bound payload sizes/types where relevant, and never pass raw request bodies directly into persistence or business logic.
+4. **State mutations and counters:** For increments, decrements, toggles, balances, inventory, likes, and unique relationships, use atomic conflict handling plus row-level serialization (`FOR UPDATE`) or optimistic version checks inside transactions.
+5. **Outbound requests:** Use short explicit timeouts, exponential backoff with random jitter, and avoid retry storms. Non-idempotent outbound mutations need an idempotency key/header or an explicit reason they cannot safely be retried.
+6. **Long-lived listeners:** Every subscription, event listener, websocket, interval, timer, and background callback needs symmetric cleanup (`unsubscribe`, `off`, `close`, `clearInterval`, or equivalent). Clean up empty maps/registries to avoid leaks.
+7. **Authorization context:** Use verified cryptographic/session identity (`req.user`, validated token claims, server-side session) for user/account/tenant scope. Never trust mutable query/body/path parameters as the authority for identity when verified context exists.
+
+Self-correction pre-flight before calling code done:
+
+- **Concurrency:** If 1,000 requests hit this path simultaneously, what shared resource races or stampedes?
+- **Resources:** If an exception is raised after acquisition, is every socket/connection/stream/listener guaranteed to close?
+- **Security:** Is identity or tenant scope derived only from verified auth context, not mutable client parameters?
+- **Data integrity:** What happens with oversized strings, wrong types, duplicate submits, or malicious payload shape?
+- **Resilience:** If a dependency is slow or failing, do timeouts/retries prevent cascading load rather than amplify it?
+
DO NOT run `git commit`, `git push`, `git reset`, `git rebase` and/or do any other git mutations unless explicitly asked to do so. Ask for confirmation each time when you need to do git mutations, even if the user has confirmed in earlier conversations.
# General Guidelines for Research and Data Processing
diff --git a/src/pythinker_code/auth/alibaba.py b/src/pythinker_code/auth/alibaba.py
index 0c17d9af..5faf9cfd 100644
--- a/src/pythinker_code/auth/alibaba.py
+++ b/src/pythinker_code/auth/alibaba.py
@@ -4,6 +4,7 @@
from collections.abc import AsyncIterator
from dataclasses import dataclass
from typing import Any, cast
+from urllib.parse import urlparse
import aiohttp
from pydantic import SecretStr
@@ -17,6 +18,8 @@
from pythinker_code.utils.aiohttp import new_client_session
ALIBABA_BASE_URL = "https://dashscope-us.aliyuncs.com/compatible-mode/v1"
+ALIBABA_CHINA_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
+ALIBABA_CODING_PLAN_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1"
ALIBABA_PROVIDER_KEY = managed_provider_key(ALIBABA_PLATFORM_ID)
ALIBABA_DEFAULT_MODEL_ALIAS = f"{ALIBABA_PLATFORM_ID}/qwen3.6-plus"
ALIBABA_MODEL_DISCOVERY_TIMEOUT = aiohttp.ClientTimeout(total=15, sock_connect=8, sock_read=10)
@@ -41,8 +44,15 @@ def alias(self) -> str:
model_id="qwen3.7-max",
alias_suffix="qwen3.7-max",
display_name="Qwen3.7 Max",
- max_context_size=262_144,
- capabilities=frozenset[ModelCapability]({"thinking"}),
+ max_context_size=1_000_000,
+ capabilities=frozenset[ModelCapability]({"thinking", "image_in"}),
+ ),
+ AlibabaModel(
+ model_id="qwen3.7-plus",
+ alias_suffix="qwen3.7-plus",
+ display_name="Qwen3.7 Plus",
+ max_context_size=1_000_000,
+ capabilities=frozenset[ModelCapability]({"thinking", "image_in"}),
),
AlibabaModel(
model_id="qwen3.6-plus",
@@ -58,6 +68,20 @@ def alias(self) -> str:
max_context_size=1_000_000,
capabilities=frozenset[ModelCapability]({"thinking", "image_in"}),
),
+ AlibabaModel(
+ model_id="qwen3-coder-plus",
+ alias_suffix="qwen3-coder-plus",
+ display_name="Qwen3 Coder Plus",
+ max_context_size=1_000_000,
+ capabilities=frozenset[ModelCapability]({"thinking"}),
+ ),
+ AlibabaModel(
+ model_id="qwen3-coder-flash",
+ alias_suffix="qwen3-coder-flash",
+ display_name="Qwen3 Coder Flash",
+ max_context_size=1_000_000,
+ capabilities=frozenset[ModelCapability]({"thinking"}),
+ ),
AlibabaModel(
model_id="deepseek-v4-pro",
alias_suffix="deepseek-v4-pro",
@@ -86,13 +110,6 @@ def alias(self) -> str:
max_context_size=262_144,
capabilities=frozenset[ModelCapability]({"thinking", "image_in"}),
),
- AlibabaModel(
- model_id="kimi-k2.5",
- alias_suffix="kimi-k2.5",
- display_name="Kimi K2.5",
- max_context_size=262_144,
- capabilities=frozenset[ModelCapability]({"thinking", "image_in"}),
- ),
AlibabaModel(
model_id="glm-5.1",
alias_suffix="glm-5.1",
@@ -100,20 +117,6 @@ def alias(self) -> str:
max_context_size=262_144,
capabilities=frozenset[ModelCapability]({"always_thinking"}),
),
- AlibabaModel(
- model_id="glm-5",
- alias_suffix="glm-5",
- display_name="GLM-5",
- max_context_size=262_144,
- capabilities=frozenset[ModelCapability]({"always_thinking"}),
- ),
- AlibabaModel(
- model_id="MiniMax-M2.5",
- alias_suffix="minimax-m2.5",
- display_name="MiniMax M2.5",
- max_context_size=204_800,
- capabilities=frozenset[ModelCapability]({"thinking"}),
- ),
)
@@ -125,12 +128,18 @@ def get_alibaba_api_key_from_env() -> str | None:
return None
-def _normalize_alibaba_base_url(value: str) -> str:
+def _normalize_alibaba_base_url(value: str, is_coding_plan: bool = False) -> str:
base_url = value.strip().rstrip("/")
if not base_url:
- return ALIBABA_BASE_URL
+ return ALIBABA_CODING_PLAN_BASE_URL if is_coding_plan else ALIBABA_BASE_URL
if "://" not in base_url:
base_url = f"https://{base_url}"
+
+ if is_coding_plan:
+ if base_url.endswith("/v1"):
+ return base_url
+ return f"{base_url}/v1"
+
if base_url.endswith("/api/v1"):
return f"{base_url.removesuffix('/api/v1')}/compatible-mode/v1"
if base_url.endswith("/compatible-mode/v1"):
@@ -150,10 +159,11 @@ def _apply_alibaba_config(
config: Config,
api_key: SecretStr,
models: tuple[AlibabaModel, ...] = ALIBABA_MODELS,
+ base_url: str | None = None,
) -> None:
config.providers[ALIBABA_PROVIDER_KEY] = LLMProvider(
type="openai_legacy",
- base_url=get_alibaba_base_url_from_env(),
+ base_url=base_url if base_url is not None else get_alibaba_base_url_from_env(),
api_key=api_key,
)
@@ -226,11 +236,16 @@ def _parse_discovered_models(data: object) -> tuple[AlibabaModel, ...]:
return tuple(results)
-async def _discover_alibaba_models(api_key: str) -> tuple[AlibabaModel, ...]:
+def _is_workspace_endpoint(base_url: str) -> bool:
+ hostname = urlparse(base_url).hostname or ""
+ return hostname.startswith("ws-") and hostname.endswith(".maas.aliyuncs.com")
+
+
+async def _discover_alibaba_models(api_key: str, base_url: str) -> tuple[AlibabaModel, ...]:
async with (
new_client_session(timeout=ALIBABA_MODEL_DISCOVERY_TIMEOUT) as session,
session.get(
- f"{get_alibaba_base_url_from_env()}/models",
+ f"{base_url}/models",
headers={"Authorization": f"Bearer {api_key}"},
raise_for_status=True,
) as response,
@@ -240,7 +255,7 @@ async def _discover_alibaba_models(api_key: str) -> tuple[AlibabaModel, ...]:
async def login_alibaba_api_key(
- config: Config, api_key: str | None = None
+ config: Config, api_key: str | None = None, base_url: str | None = None
) -> AsyncIterator[OAuthEvent]:
if not config.is_from_default_location:
yield OAuthEvent(
@@ -254,26 +269,162 @@ async def login_alibaba_api_key(
yield OAuthEvent("error", "Alibaba API key is required.")
return
+ is_coding_plan = resolved_key.startswith("sk-sp-")
+ is_token_plan = resolved_key.startswith("sk-ws-")
+
+ default_url = ALIBABA_CODING_PLAN_BASE_URL if is_coding_plan else ALIBABA_BASE_URL
+ china_url = (
+ "https://coding.dashscope.aliyuncs.com/v1" if is_coding_plan else ALIBABA_CHINA_BASE_URL
+ )
+
+ env_base_url = os.getenv("DASHSCOPE_BASE_URL") or os.getenv("ALIBABA_BASE_URL")
+ is_env_explicitly_set = bool(env_base_url)
+
+ if is_token_plan and not (base_url and base_url.strip()) and not is_env_explicitly_set:
+ yield OAuthEvent(
+ "error",
+ "Alibaba Token Plan workspace keys require the dedicated Base URL shown in "
+ "the Token Plan console. Enter it during /login or set DASHSCOPE_BASE_URL.",
+ )
+ return
+
+ if base_url and base_url.strip():
+ primary_url = _normalize_alibaba_base_url(base_url, is_coding_plan=is_coding_plan)
+ elif is_env_explicitly_set:
+ primary_url = _normalize_alibaba_base_url(env_base_url, is_coding_plan=is_coding_plan)
+ else:
+ primary_url = default_url
+
+ active_url = primary_url
models = ALIBABA_MODELS
+
try:
- discovered = await _discover_alibaba_models(resolved_key)
+ discovered = await _discover_alibaba_models(resolved_key, primary_url)
if discovered:
models = discovered
except aiohttp.ClientResponseError as exc:
if exc.status in {401, 403}:
- yield OAuthEvent("error", "Invalid Alibaba API key; the key was not saved.")
- return
- yield OAuthEvent(
- "info",
- "Alibaba model listing is unavailable; using the built-in model list.",
- )
+ if is_token_plan:
+ yield OAuthEvent(
+ "error",
+ "Alibaba Token Plan API key was not accepted. Ensure the key is active "
+ "and assigned to a Token Plan seat. Set DASHSCOPE_BASE_URL only when "
+ "Alibaba provides a different endpoint for your plan.",
+ )
+ return
+
+ if is_coding_plan and primary_url == default_url:
+ try:
+ discovered = await _discover_alibaba_models(resolved_key, china_url)
+ active_url = china_url
+ if discovered:
+ models = discovered
+ yield OAuthEvent(
+ "info",
+ "Detected China-region Coding Plan key; configured for China endpoint.",
+ )
+ except aiohttp.ClientResponseError as china_exc:
+ if china_exc.status in {401, 403}:
+ yield OAuthEvent(
+ "error",
+ "Alibaba Coding Plan API key was not accepted. Ensure your key is "
+ "valid, active, and has the required model permissions in the "
+ "Coding Plan console.",
+ )
+ return
+ yield OAuthEvent(
+ "error",
+ "The default international endpoint rejected the key and the China "
+ "endpoint could not be reached. Check network access or set "
+ "DASHSCOPE_BASE_URL to the correct endpoint and try again.",
+ )
+ return
+ except (aiohttp.ClientError, TimeoutError, ValueError):
+ yield OAuthEvent(
+ "error",
+ "The default international endpoint rejected the key and the China "
+ "endpoint could not be reached. Check network access or set "
+ "DASHSCOPE_BASE_URL to the correct endpoint and try again.",
+ )
+ return
+ else:
+ if is_coding_plan:
+ if primary_url == china_url:
+ yield OAuthEvent(
+ "error",
+ "Alibaba Coding Plan API key was not accepted. Ensure your key is "
+ "valid, active, and has the required model permissions in the "
+ "Coding Plan console.",
+ )
+ else:
+ yield OAuthEvent(
+ "error",
+ "Alibaba Coding Plan API key was not accepted. Ensure your key is "
+ "valid, active, and has the required model permissions in the "
+ "Coding Plan console. If using the China region, set "
+ "DASHSCOPE_BASE_URL=https://coding.dashscope.aliyuncs.com/v1",
+ )
+ return
+ else:
+ if primary_url == default_url:
+ try:
+ discovered = await _discover_alibaba_models(resolved_key, china_url)
+ active_url = china_url
+ if discovered:
+ models = discovered
+ yield OAuthEvent(
+ "info",
+ "Detected China-region DashScope key; "
+ "configured for China (Beijing) endpoint.",
+ )
+ except aiohttp.ClientResponseError as china_exc:
+ if china_exc.status in {401, 403}:
+ yield OAuthEvent(
+ "error",
+ "Alibaba API key was not accepted. Ensure your key is valid "
+ "and "
+ "comes from the Alibaba Cloud Model Studio console "
+ "(https://bailian.console.aliyun.com). "
+ "Set DASHSCOPE_BASE_URL to override the endpoint if needed.",
+ )
+ return
+ yield OAuthEvent(
+ "error",
+ "The default US endpoint rejected the key and the China endpoint "
+ "could not be reached. Check network access or set "
+ "DASHSCOPE_BASE_URL to the correct endpoint and try again.",
+ )
+ return
+ except (aiohttp.ClientError, TimeoutError, ValueError):
+ yield OAuthEvent(
+ "error",
+ "The default US endpoint rejected the key and the China endpoint "
+ "could not be reached. Check network access or set "
+ "DASHSCOPE_BASE_URL to the correct endpoint and try again.",
+ )
+ return
+ else:
+ yield OAuthEvent(
+ "error",
+ "Alibaba API key was not accepted. Ensure your key is valid and "
+ "that DASHSCOPE_BASE_URL points to the correct endpoint.",
+ )
+ return
+ else:
+ yield OAuthEvent(
+ "info",
+ "Alibaba model listing is unavailable; using the built-in model list.",
+ )
except (aiohttp.ClientError, TimeoutError, ValueError):
yield OAuthEvent(
"info",
"Alibaba model listing is unavailable; using the built-in model list.",
)
- _apply_alibaba_config(config, SecretStr(resolved_key), models=models)
+ if _is_workspace_endpoint(active_url):
+ models = tuple(model for model in models if model.model_id != "kimi-k2.6")
+
+ _apply_alibaba_config(config, SecretStr(resolved_key), models=models, base_url=active_url)
save_config(config)
yield OAuthEvent("success", f"Alibaba configured with model {config.default_model}.")
diff --git a/src/pythinker_code/llm.py b/src/pythinker_code/llm.py
index 4a484b36..b9c9f235 100644
--- a/src/pythinker_code/llm.py
+++ b/src/pythinker_code/llm.py
@@ -225,10 +225,15 @@ def create_llm(
if provider.reasoning_key is not None
else "reasoning_content"
)
+ stream = not (
+ _is_alibaba_workspace_endpoint(provider.base_url)
+ and model.model.lower().replace("_", "-") == "deepseek-v3.2"
+ )
chat_provider = OpenAILegacy(
model=model.model,
base_url=provider.base_url,
api_key=resolved_api_key,
+ stream=stream,
reasoning_key=reasoning_key,
default_headers=dict(provider.custom_headers) if provider.custom_headers else None,
http_client=rl_http_client,
@@ -351,23 +356,32 @@ def create_llm(
effective_effort = "off" if requested_effort is not None else None
thinking_on = thinking_effort_enabled(effective_effort)
- is_kimi_openai_legacy = provider.type == "openai_legacy" and _is_kimi_k2_model(model.model)
+ # DashScope's routing layer rejects reasoning_effort entirely; use
+ # model-specific body fields instead.
+ is_dashscope_legacy = provider.type == "openai_legacy" and _is_dashscope_endpoint(
+ provider.base_url or ""
+ )
+ # Kimi K2.x uses the provider-specific thinking.type field on Moonshot-style
+ # endpoints, but Alibaba's DashScope-compatible routes use enable_thinking.
+ is_kimi_openai_legacy = (
+ provider.type == "openai_legacy"
+ and _is_kimi_k2_model(model.model)
+ and not is_dashscope_legacy
+ )
is_glm_openai_legacy = provider.type == "openai_legacy" and _is_glm_model(model.model)
if (
effective_effort is not None
and supports_thinking
and not is_kimi_openai_legacy
and not is_glm_openai_legacy
+ and not is_dashscope_legacy
):
# Only explicitly send thinking controls for models that advertise
# reasoning. Some OpenAI-compatible non-reasoning models reject even a
# null reasoning_effort field.
chat_provider = chat_provider.with_thinking(effective_effort)
- # Kimi K2.5/K2.6 and GLM models use OpenAI-compatible APIs but their thinking
- # toggle is the provider-specific `thinking.type` body field rather than
- # OpenAI's `reasoning_effort`. Send that field instead of `reasoning_effort`;
- # otherwise providers such as Z.ai/GLM ignore the generic reasoning knob.
+ # Kimi K2.x on Moonshot-style endpoints and GLM use thinking.type.
if (is_kimi_openai_legacy or is_glm_openai_legacy) and effective_effort is not None:
thinking_body: dict[str, object] = {"type": "enabled" if thinking_on else "disabled"}
if is_glm_openai_legacy and thinking_on:
@@ -379,6 +393,18 @@ def create_llm(
extra_body={"thinking": thinking_body}
)
+ # DashScope-compatible models use enable_thinking unless handled by a
+ # provider-specific format above.
+ if (
+ is_dashscope_legacy
+ and not is_kimi_openai_legacy
+ and not is_glm_openai_legacy
+ and effective_effort is not None
+ ):
+ chat_provider = cast(Any, chat_provider).with_generation_kwargs(
+ extra_body={"enable_thinking": thinking_on}
+ )
+
# Apply Pythinker AI-specific ``thinking.keep`` (preserved thinking) only when
# the model is actually in thinking mode; otherwise the API would see a
# ``thinking.keep`` without an accompanying ``thinking.type`` it honors.
@@ -467,6 +493,15 @@ def _is_glm_model(model_name: str) -> bool:
return model_name.lower().replace("_", "-").startswith("glm-")
+def _is_dashscope_endpoint(base_url: str) -> bool:
+ """True for any Alibaba DashScope endpoint (standard, intl, workspace)."""
+ return "aliyuncs.com" in base_url
+
+
+def _is_alibaba_workspace_endpoint(base_url: str) -> bool:
+ return "://ws-" in base_url and ".maas.aliyuncs.com" in base_url
+
+
def _load_scripted_echo_scripts() -> list[str]:
script_path = os.getenv("PYTHINKER_SCRIPTED_ECHO_SCRIPTS")
if not script_path:
diff --git a/src/pythinker_code/ui/shell/oauth.py b/src/pythinker_code/ui/shell/oauth.py
index c3ddc391..10541f44 100644
--- a/src/pythinker_code/ui/shell/oauth.py
+++ b/src/pythinker_code/ui/shell/oauth.py
@@ -137,6 +137,15 @@ async def _prompt_api_key(label: str) -> str | None:
return value.strip() or None
+async def _prompt_text(label: str) -> str | None:
+ session = PromptSession[str]()
+ try:
+ value = await session.prompt_async(f" {label}: ")
+ except (EOFError, KeyboardInterrupt):
+ return None
+ return value.strip() or None
+
+
_SELECTOR_PROVIDER_ENTRIES: list[OAuthProviderEntry] = [
OAuthProviderEntry(id="browser", name="OpenAI ChatGPT (browser)", auth_type="oauth"),
OAuthProviderEntry(id="headless", name="OpenAI ChatGPT (device code)", auth_type="oauth"),
@@ -284,7 +293,15 @@ async def login(app: Shell, args: str) -> None:
if not api_key:
console.print(f"[{_t.error}]No Alibaba API key entered.[/]")
return
- ok = await _render_oauth_events(login_alibaba_api_key(soul.runtime.config, api_key))
+ base_url: str | None = None
+ if api_key.startswith("sk-ws-"):
+ base_url = await _prompt_text("Token Plan OpenAI-compatible endpoint")
+ if not base_url:
+ console.print(f"[{_t.error}]No Token Plan endpoint entered.[/]")
+ return
+ ok = await _render_oauth_events(
+ login_alibaba_api_key(soul.runtime.config, api_key, base_url=base_url)
+ )
provider = ALIBABA_PLATFORM_ID
elif mode == "anthropic":
api_key = await _prompt_api_key("Anthropic")
diff --git a/tasks/todo.md b/tasks/todo.md
index 214918d0..03d38818 100644
--- a/tasks/todo.md
+++ b/tasks/todo.md
@@ -1,3 +1,40 @@
+# Alibaba Token Plan model compatibility fix
+
+## Plan
+
+- [x] Reproduce the Kimi-only request-shaping regression with a focused test.
+- [x] Preserve Moonshot/Z AI request formats for non-Alibaba providers.
+- [x] Confirm the generic Token Plan `/models` response does not validate `sk-ws-` credentials.
+- [x] Require the dedicated workspace Base URL for `sk-ws-` login.
+- [x] Re-test Kimi K2.6 and DeepSeek V3.2 request behavior for the dedicated endpoint.
+- [x] Fix the shell wire coroutine warning.
+- [x] Run focused Alibaba auth/LLM tests and `make check-pythinker-code`.
+
+## Acceptance criteria
+
+- Alibaba Token Plan Kimi K2.6 sends `extra_body.enable_thinking`, not
+ `extra_body.thinking.type` or `reasoning_effort`.
+- `sk-ws-` login requires and saves the dedicated Base URL shown in the Token Plan console.
+- Login cannot falsely succeed based only on the generic endpoint's public `/models` response.
+- DeepSeek V3.2 either produces a valid response or is excluded with evidence that the workspace
+ endpoint does not support it.
+- Existing generic, regional, Coding Plan, Moonshot, and GLM behavior remains covered.
+
+## Review
+
+- Token Plan keys now require an explicit dedicated endpoint and the shell passes that endpoint
+ directly to the login flow.
+- Regional fallback only saves a provider after the fallback endpoint authenticates successfully;
+ custom endpoints are never silently replaced.
+- Workspace Kimi entries are filtered when the advertised route is unusable, while DeepSeek V3.2
+ uses the verified non-streaming request path and DashScope `enable_thinking`.
+- Final focused agent-spec, auth, LLM, and shell tests: `122 passed`.
+- `make check-pythinker-code` passed.
+- Full Pythinker Code test target passed: `4427 passed, 6 skipped, 1 xfailed`; wire E2E
+ passed: `52 passed, 4 skipped`.
+
+---
+
# Plan: Full rename `pythinker-cli` → `pythinker-code`
**Status**: Planning — DO NOT execute yet. User to review and approve.
diff --git a/tests/auth/test_alibaba_auth.py b/tests/auth/test_alibaba_auth.py
index 85d60ae2..9a7d61a0 100644
--- a/tests/auth/test_alibaba_auth.py
+++ b/tests/auth/test_alibaba_auth.py
@@ -17,50 +17,53 @@ def test_alibaba_model_catalog_contains_current_models():
aliases = {model.alias for model in ALIBABA_MODELS}
assert aliases == {
"alibaba/qwen3.7-max",
+ "alibaba/qwen3.7-plus",
"alibaba/qwen3.6-plus",
"alibaba/qwen3.6-flash",
+ "alibaba/qwen3-coder-plus",
+ "alibaba/qwen3-coder-flash",
"alibaba/deepseek-v4-pro",
"alibaba/deepseek-v4-flash",
"alibaba/deepseek-v3.2",
"alibaba/kimi-k2.6",
- "alibaba/kimi-k2.5",
"alibaba/glm-5.1",
- "alibaba/glm-5",
- "alibaba/minimax-m2.5",
}
api_ids = {m.alias: m.model_id for m in ALIBABA_MODELS}
assert api_ids == {
"alibaba/qwen3.7-max": "qwen3.7-max",
+ "alibaba/qwen3.7-plus": "qwen3.7-plus",
"alibaba/qwen3.6-plus": "qwen3.6-plus",
"alibaba/qwen3.6-flash": "qwen3.6-flash",
+ "alibaba/qwen3-coder-plus": "qwen3-coder-plus",
+ "alibaba/qwen3-coder-flash": "qwen3-coder-flash",
"alibaba/deepseek-v4-pro": "deepseek-v4-pro",
"alibaba/deepseek-v4-flash": "deepseek-v4-flash",
"alibaba/deepseek-v3.2": "deepseek-v3.2",
"alibaba/kimi-k2.6": "kimi-k2.6",
- "alibaba/kimi-k2.5": "kimi-k2.5",
"alibaba/glm-5.1": "glm-5.1",
- "alibaba/glm-5": "glm-5",
- "alibaba/minimax-m2.5": "MiniMax-M2.5",
}
assert all(m.provider_key == "managed:alibaba" for m in ALIBABA_MODELS)
by_alias = {m.alias: m for m in ALIBABA_MODELS}
- assert by_alias["alibaba/qwen3.7-max"].capabilities == frozenset({"thinking"})
- assert by_alias["alibaba/qwen3.7-max"].max_context_size == 262_144
+ assert by_alias["alibaba/qwen3.7-max"].capabilities == frozenset({"thinking", "image_in"})
+ assert by_alias["alibaba/qwen3.7-max"].max_context_size == 1_000_000
+ assert by_alias["alibaba/qwen3.7-plus"].capabilities == frozenset({"thinking", "image_in"})
+ assert by_alias["alibaba/qwen3.7-plus"].max_context_size == 1_000_000
assert by_alias["alibaba/qwen3.6-plus"].capabilities == frozenset({"thinking", "image_in"})
assert by_alias["alibaba/qwen3.6-plus"].max_context_size == 1_000_000
assert by_alias["alibaba/qwen3.6-flash"].capabilities == frozenset({"thinking", "image_in"})
+ assert by_alias["alibaba/qwen3-coder-plus"].capabilities == frozenset({"thinking"})
+ assert by_alias["alibaba/qwen3-coder-plus"].max_context_size == 1_000_000
+ assert by_alias["alibaba/qwen3-coder-flash"].capabilities == frozenset({"thinking"})
+ assert by_alias["alibaba/qwen3-coder-flash"].max_context_size == 1_000_000
assert by_alias["alibaba/deepseek-v4-pro"].capabilities == frozenset({"thinking"})
assert by_alias["alibaba/deepseek-v4-flash"].capabilities == frozenset({"thinking"})
assert by_alias["alibaba/deepseek-v3.2"].capabilities == frozenset({"thinking"})
assert by_alias["alibaba/deepseek-v3.2"].max_context_size == 128_000
assert by_alias["alibaba/kimi-k2.6"].capabilities == frozenset({"thinking", "image_in"})
- assert by_alias["alibaba/kimi-k2.5"].capabilities == frozenset({"thinking", "image_in"})
assert by_alias["alibaba/glm-5.1"].capabilities == frozenset({"always_thinking"})
- assert by_alias["alibaba/glm-5"].capabilities == frozenset({"always_thinking"})
- assert by_alias["alibaba/minimax-m2.5"].capabilities == frozenset({"thinking"})
def test_alibaba_env_key_prefers_dashscope_api_key(monkeypatch):
@@ -127,7 +130,7 @@ def test_apply_alibaba_config_writes_provider_and_default():
assert "alibaba/qwen3.6-plus" in config.models
assert config.models["alibaba/qwen3.6-plus"].provider == ALIBABA_PROVIDER_KEY
assert config.models["alibaba/qwen3.6-plus"].model == "qwen3.6-plus"
- assert config.models["alibaba/qwen3.7-max"].capabilities == frozenset({"thinking"})
+ assert config.models["alibaba/qwen3.7-max"].capabilities == frozenset({"thinking", "image_in"})
assert config.models["alibaba/qwen3.6-plus"].capabilities == frozenset({"thinking", "image_in"})
assert config.models["alibaba/qwen3.6-flash"].capabilities == frozenset(
{"thinking", "image_in"}
@@ -135,7 +138,7 @@ def test_apply_alibaba_config_writes_provider_and_default():
assert config.models["alibaba/deepseek-v4-pro"].capabilities == frozenset({"thinking"})
assert config.models["alibaba/kimi-k2.6"].capabilities == frozenset({"thinking", "image_in"})
assert config.models["alibaba/glm-5.1"].capabilities == frozenset({"always_thinking"})
- assert config.models["alibaba/minimax-m2.5"].model == "MiniMax-M2.5"
+ assert "alibaba/minimax-m2.5" not in config.models
assert config.default_model == "alibaba/qwen3.6-plus"
assert config.default_thinking is True
assert config.default_thinking_effort == "high"
@@ -206,6 +209,23 @@ async def json(self, *, content_type: str | None = None) -> object:
return self._payload
+@pytest.mark.asyncio
+async def test_discover_alibaba_models_uses_provided_base_url(monkeypatch):
+ from pythinker_code.auth.alibaba import _discover_alibaba_models
+
+ seen_urls: list[str] = []
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ seen_urls.append(str(args[2]))
+ return _FakeAiohttpResponse({"data": []})
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ await _discover_alibaba_models("sk-test", "https://custom.example.com/compatible-mode/v1")
+ assert len(seen_urls) == 1
+ assert "custom.example.com" in seen_urls[0]
+
+
@pytest.mark.asyncio
async def test_login_alibaba_saves_static_models_when_discovery_fails(monkeypatch, tmp_path):
from pythinker_code.auth.alibaba import login_alibaba_api_key
@@ -277,6 +297,252 @@ async def fake_request(*args: object, **kwargs: object) -> object:
assert config.models == {}
+@pytest.mark.asyncio
+async def test_login_alibaba_china_key_auto_detected(monkeypatch, tmp_path):
+ """A China-region key that fails on US but succeeds on China is auto-configured."""
+ from pythinker_code.auth.alibaba import ALIBABA_CHINA_BASE_URL, login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ url = str(args[2])
+ if "dashscope-us" in url:
+ raise aiohttp.ClientResponseError(
+ _request_info(url), (), status=401, message="Unauthorized"
+ )
+ return _FakeAiohttpResponse({"data": [{"id": "qwen3.6-plus", "context_length": 1_000_000}]})
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [event async for event in login_alibaba_api_key(config, "sk-china-key")]
+
+ types = [e.type for e in events]
+ assert types == ["info", "success"], types
+ assert "China" in events[0].message
+ provider = next(iter(config.providers.values()))
+ assert provider.base_url == ALIBABA_CHINA_BASE_URL
+ assert config.models["alibaba/qwen3.6-plus"].max_context_size == 1_000_000
+ assert (tmp_path / "config.toml").exists()
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_china_probe_network_error_fails_closed(monkeypatch, tmp_path):
+ """When US rejects a key and China is unreachable, do not save an unverified endpoint."""
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ url = str(args[2])
+ if "dashscope-us" in url:
+ raise aiohttp.ClientResponseError(
+ _request_info(url), (), status=401, message="Unauthorized"
+ )
+ raise aiohttp.ClientConnectionError("China unreachable")
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [event async for event in login_alibaba_api_key(config, "sk-china-key")]
+
+ assert [event.type for event in events] == ["error"]
+ assert "China endpoint could not be reached" in events[-1].message
+ assert config.providers == {}
+ assert config.models == {}
+ assert not (tmp_path / "config.toml").exists()
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_custom_endpoint_401_does_not_probe_china(monkeypatch, tmp_path):
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ config = Config(is_from_default_location=True)
+ seen_urls: list[str] = []
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ url = str(args[2])
+ seen_urls.append(url)
+ raise aiohttp.ClientResponseError(
+ _request_info(url), (), status=401, message="Unauthorized"
+ )
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [
+ event
+ async for event in login_alibaba_api_key(
+ config,
+ "sk-custom-key",
+ base_url="https://custom.example.com/compatible-mode/v1",
+ )
+ ]
+
+ assert [event.type for event in events] == ["error"]
+ assert seen_urls == ["https://custom.example.com/compatible-mode/v1/models"]
+ assert config.providers == {}
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_primary_already_china_401_fails(monkeypatch, tmp_path):
+ """When DASHSCOPE_BASE_URL is already China and it returns 401, fail without probing again."""
+ from pythinker_code.auth.alibaba import ALIBABA_CHINA_BASE_URL, login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.setenv("DASHSCOPE_BASE_URL", ALIBABA_CHINA_BASE_URL)
+ config = Config(is_from_default_location=True)
+
+ call_count = 0
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ nonlocal call_count
+ call_count += 1
+ url = str(args[2])
+ raise aiohttp.ClientResponseError(
+ _request_info(url), (), status=401, message="Unauthorized"
+ )
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [event async for event in login_alibaba_api_key(config, "bad-key")]
+
+ assert events[-1].type == "error"
+ assert "Alibaba" in events[-1].message
+ assert call_count == 1, "should not probe a second endpoint when primary is already China"
+ assert config.providers == {}
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_token_plan_key_gives_targeted_error(monkeypatch, tmp_path):
+ """Rejected sk-ws- keys get Token Plan-specific recovery guidance."""
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ call_count = 0
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ nonlocal call_count
+ call_count += 1
+ url = str(args[2])
+ raise aiohttp.ClientResponseError(
+ _request_info(url), (), status=401, message="Unauthorized"
+ )
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [
+ event
+ async for event in login_alibaba_api_key(
+ config, "sk-ws-H.HXDYIP.c9u4.abcdefghijklmnopqrstuvwxyz"
+ )
+ ]
+
+ assert events[-1].type == "error"
+ assert "Token Plan" in events[-1].message
+ assert "DASHSCOPE_BASE_URL" in events[-1].message
+ assert call_count == 0, "should not probe any endpoint without the dedicated workspace URL"
+ assert config.providers == {}
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_workspace_key_with_correct_base_url_succeeds(monkeypatch, tmp_path):
+ """An explicit Token Plan endpoint takes precedence over the environment."""
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.setenv(
+ "DASHSCOPE_BASE_URL",
+ "ws-wrong.ap-southeast-1.maas.aliyuncs.com",
+ )
+ config = Config(is_from_default_location=True)
+ seen_urls: list[str] = []
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ seen_urls.append(str(args[2]))
+ return _FakeAiohttpResponse({"data": [{"id": "qwen3.6-plus"}]})
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [
+ event
+ async for event in login_alibaba_api_key(
+ config,
+ "sk-ws-H.HXDYIP.c9u4.abcdefghijklmnopqrstuvwxyz",
+ base_url="ws-kopy0du82ky7144q.ap-southeast-1.maas.aliyuncs.com",
+ )
+ ]
+
+ assert events[-1].type == "success"
+ provider = next(iter(config.providers.values()))
+ assert "ws-kopy0du82ky7144q" in provider.base_url
+ assert all("ws-wrong" not in url for url in seen_urls)
+ assert (tmp_path / "config.toml").exists()
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_workspace_hides_unroutable_kimi(monkeypatch, tmp_path):
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ return _FakeAiohttpResponse({"data": [{"id": "kimi-k2.6"}, {"id": "deepseek-v3.2"}]})
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [
+ event
+ async for event in login_alibaba_api_key(
+ config,
+ "sk-ws-H.HXDYIP.c9u4.abcdefghijklmnopqrstuvwxyz",
+ base_url="ws-example.ap-southeast-1.maas.aliyuncs.com",
+ )
+ ]
+
+ assert events[-1].type == "success"
+ assert "alibaba/kimi-k2.6" not in config.models
+ assert "alibaba/deepseek-v3.2" in config.models
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_workspace_key_requires_dedicated_base_url(monkeypatch, tmp_path):
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ pytest.fail("workspace login must not probe a generic endpoint")
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [
+ event
+ async for event in login_alibaba_api_key(
+ config, "sk-ws-H.HXDYIP.c9u4.abcdefghijklmnopqrstuvwxyz"
+ )
+ ]
+
+ assert events[-1].type == "error"
+ assert "dedicated Base URL" in events[-1].message
+ assert "DASHSCOPE_BASE_URL" in events[-1].message
+ assert config.providers == {}
+
+
@pytest.mark.asyncio
async def test_login_alibaba_uses_discovered_context_length(monkeypatch, tmp_path):
from pythinker_code.auth.alibaba import login_alibaba_api_key
@@ -295,7 +561,7 @@ async def fake_request(*args: object, **kwargs: object) -> object:
assert events[-1].type == "success"
assert config.models["alibaba/qwen3.7-max"].max_context_size == 512_000
- assert config.models["alibaba/qwen3.7-max"].capabilities == frozenset({"thinking"})
+ assert config.models["alibaba/qwen3.7-max"].capabilities == frozenset({"thinking", "image_in"})
@pytest.mark.asyncio
@@ -344,7 +610,7 @@ def test_parse_discovered_alibaba_models_overrides_context_length_only_for_posit
}
result = _parse_discovered_models(payload)
by_id = {m.model_id: m for m in result}
- assert by_id["qwen3.7-max"].max_context_size == 262_144
+ assert by_id["qwen3.7-max"].max_context_size == 1_000_000
assert by_id["qwen3.6-plus"].max_context_size == 1_000_000
assert by_id["deepseek-v3.2"].max_context_size == 512_000
@@ -408,3 +674,110 @@ async def test_logout_alibaba_rejects_non_default_config_location():
assert "default config file" in events[-1].message
assert config.providers == {}
assert config.models == {}
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_coding_plan_key_intl_succeeds(monkeypatch, tmp_path):
+ """A Coding Plan (sk-sp-) key succeeds on the intl endpoint."""
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ url = str(args[2])
+ assert "coding-intl.dashscope.aliyuncs.com/v1" in url
+ return _FakeAiohttpResponse({"data": [{"id": "kimi-k2.6"}]})
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [event async for event in login_alibaba_api_key(config, "sk-sp-test-key")]
+
+ assert events[-1].type == "success"
+ provider = next(iter(config.providers.values()))
+ assert provider.base_url == "https://coding-intl.dashscope.aliyuncs.com/v1"
+ assert (tmp_path / "config.toml").exists()
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_coding_plan_key_auto_detected(monkeypatch, tmp_path):
+ """A Coding Plan (sk-sp-) key that fails on intl but succeeds on China is auto-configured."""
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ url = str(args[2])
+ if "coding-intl" in url:
+ raise aiohttp.ClientResponseError(
+ _request_info(url), (), status=401, message="Unauthorized"
+ )
+ return _FakeAiohttpResponse({"data": [{"id": "kimi-k2.6", "context_length": 262_144}]})
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [event async for event in login_alibaba_api_key(config, "sk-sp-test-key")]
+
+ types = [e.type for e in events]
+ assert types == ["info", "success"], types
+ assert "China" in events[0].message
+ provider = next(iter(config.providers.values()))
+ assert provider.base_url == "https://coding.dashscope.aliyuncs.com/v1"
+ assert config.models["alibaba/kimi-k2.6"].max_context_size == 262_144
+ assert (tmp_path / "config.toml").exists()
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_coding_plan_key_rejects_401(monkeypatch, tmp_path):
+ """A Coding Plan (sk-sp-) key that fails on both intl and China endpoints is rejected."""
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ url = str(args[2])
+ raise aiohttp.ClientResponseError(
+ _request_info(url), (), status=401, message="Unauthorized"
+ )
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [event async for event in login_alibaba_api_key(config, "sk-sp-bad-key")]
+
+ assert events[-1].type == "error"
+ assert "Coding Plan" in events[-1].message
+ assert config.providers == {}
+
+
+@pytest.mark.asyncio
+async def test_login_alibaba_coding_plan_probe_network_error_fails_closed(monkeypatch, tmp_path):
+ from pythinker_code.auth.alibaba import login_alibaba_api_key
+
+ monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path))
+ monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
+ monkeypatch.delenv("ALIBABA_BASE_URL", raising=False)
+ config = Config(is_from_default_location=True)
+
+ async def fake_request(*args: object, **kwargs: object) -> object:
+ url = str(args[2])
+ if "coding-intl" in url:
+ raise aiohttp.ClientResponseError(
+ _request_info(url), (), status=401, message="Unauthorized"
+ )
+ raise aiohttp.ClientConnectionError("China unreachable")
+
+ monkeypatch.setattr(aiohttp.ClientSession, "_request", fake_request)
+
+ events = [event async for event in login_alibaba_api_key(config, "sk-sp-test-key")]
+
+ assert [event.type for event in events] == ["error"]
+ assert "China endpoint could not be reached" in events[-1].message
+ assert config.providers == {}
diff --git a/tests/core/test_agent_spec.py b/tests/core/test_agent_spec.py
index e2991b13..1c2a4b79 100644
--- a/tests/core/test_agent_spec.py
+++ b/tests/core/test_agent_spec.py
@@ -420,6 +420,26 @@ def test_load_default_agent_spec():
assert sub_subagents == snapshot({})
+def test_default_subagents_include_production_guardrail_gate():
+ subagent_specs = {
+ name: load_agent_spec(spec.path)
+ for name, spec in load_agent_spec(DEFAULT_AGENT_FILE).subagents.items()
+ }
+
+ assert (
+ "production guardrail gate"
+ in subagent_specs["review"].system_prompt_args["ROLE_ADDITIONAL"]
+ )
+ assert (
+ "cache stampedes" in subagent_specs["code-reviewer"].system_prompt_args["ROLE_ADDITIONAL"]
+ )
+ assert (
+ "IDOR/tenant-scope mistakes"
+ in subagent_specs["security-reviewer"].system_prompt_args["ROLE_ADDITIONAL"]
+ )
+ assert "Production guardrails" in subagent_specs["judge"].system_prompt_args["ROLE_ADDITIONAL"]
+
+
def test_load_agent_spec_basic(agent_file: Path):
"""Test loading a basic agent specification."""
spec = load_agent_spec(agent_file)
diff --git a/tests/core/test_create_llm.py b/tests/core/test_create_llm.py
index 6e5061d0..b9fdce74 100644
--- a/tests/core/test_create_llm.py
+++ b/tests/core/test_create_llm.py
@@ -718,6 +718,52 @@ def test_create_llm_openai_legacy_kimi_sends_enabled_thinking_body():
}
+def test_create_llm_alibaba_kimi_uses_dashscope_thinking_switch():
+ provider = LLMProvider(
+ type="openai_legacy",
+ base_url="https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
+ api_key=SecretStr("test-key"),
+ )
+ model = LLMModel(
+ provider="managed:alibaba",
+ model="kimi-k2.6",
+ max_context_size=262_144,
+ capabilities={"thinking"},
+ )
+
+ llm = create_llm(provider, model, thinking=True)
+
+ assert llm is not None
+ assert isinstance(llm.chat_provider, OpenAILegacy)
+ assert llm.chat_provider.thinking_effort is None
+ assert llm.chat_provider._generation_kwargs.get("extra_body") == { # pyright: ignore[reportPrivateUsage]
+ "enable_thinking": True
+ }
+
+
+def test_create_llm_alibaba_workspace_deepseek_disables_streaming():
+ provider = LLMProvider(
+ type="openai_legacy",
+ base_url=("https://ws-example.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"),
+ api_key=SecretStr("test-key"),
+ )
+ model = LLMModel(
+ provider="managed:alibaba",
+ model="deepseek-v3.2",
+ max_context_size=128_000,
+ capabilities={"thinking"},
+ )
+
+ llm = create_llm(provider, model, thinking=True)
+
+ assert llm is not None
+ assert isinstance(llm.chat_provider, OpenAILegacy)
+ assert llm.chat_provider.stream is False
+ assert llm.chat_provider._generation_kwargs.get("extra_body") == { # pyright: ignore[reportPrivateUsage]
+ "enable_thinking": True
+ }
+
+
def test_create_llm_openai_legacy_glm_sends_provider_thinking_body():
provider = LLMProvider(
type="openai_legacy",
diff --git a/tests/core/test_default_agent.py b/tests/core/test_default_agent.py
index d2c40675..22eb97ff 100644
--- a/tests/core/test_default_agent.py
+++ b/tests/core/test_default_agent.py
@@ -20,6 +20,17 @@ async def test_default_agent(runtime: Runtime):
assert "Pythoughts-labs" in agent.system_prompt
assert "Do not name or describe the underlying language model" in agent.system_prompt
+ # Production guardrails — keep defensive coding rules in the base prompt so root and
+ # subagent roles inherit the same failure-mode posture.
+ assert "## Production Bug Guardrails" in agent.system_prompt
+ assert "double-checked lock" in agent.system_prompt
+ assert "guarantee release/close in `finally`" in agent.system_prompt
+ assert "schema/validation mechanism" in agent.system_prompt
+ assert "row-level serialization (`FOR UPDATE`)" in agent.system_prompt
+ assert "exponential backoff with random jitter" in agent.system_prompt
+ assert "symmetric cleanup" in agent.system_prompt
+ assert "verified cryptographic/session identity" in agent.system_prompt
+
builtin_types = [
(
name,
diff --git a/tests/ui_and_conv/test_openai_shell_login.py b/tests/ui_and_conv/test_openai_shell_login.py
index a86071c3..73a0e377 100644
--- a/tests/ui_and_conv/test_openai_shell_login.py
+++ b/tests/ui_and_conv/test_openai_shell_login.py
@@ -220,6 +220,28 @@ async def test_shell_login_deepseek_routes_to_deepseek(monkeypatch):
assert login.call_args.args[1] == "ds-test"
+@pytest.mark.asyncio
+async def test_shell_login_alibaba_workspace_key_prompts_for_dedicated_endpoint(monkeypatch):
+ login = Mock(side_effect=_success_event)
+ monkeypatch.setattr(shell_oauth, "login_alibaba_api_key", login, raising=False)
+ monkeypatch.setattr(
+ shell_oauth,
+ "_prompt_api_key",
+ lambda label: _async_value("sk-ws-test"),
+ )
+ monkeypatch.setattr(
+ shell_oauth,
+ "_prompt_text",
+ lambda label: _async_value("ws-example.ap-southeast-1.maas.aliyuncs.com"),
+ raising=False,
+ )
+
+ with pytest.raises(Reload):
+ await cast(Any, shell_oauth.login)(_app(), "alibaba")
+
+ assert login.call_args.kwargs["base_url"] == ("ws-example.ap-southeast-1.maas.aliyuncs.com")
+
+
@pytest.mark.asyncio
async def test_shell_logout_deepseek_routes_to_deepseek(monkeypatch):
logout = Mock(side_effect=_success_event)
diff --git a/uv.lock b/uv.lock
index 66d2a1b5..2fde3077 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2515,7 +2515,7 @@ wheels = [
[[package]]
name = "pythinker-code"
-version = "0.35.0"
+version = "0.36.0"
source = { editable = "." }
dependencies = [
{ name = "agent-client-protocol" },