Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ GitHub Releases page; `0.8.0` is the new starting line.
## Unreleased

- **Parallel streamed tool calls are now correlated safely.** Interleaved argument chunks stay attached to their indexed calls, malformed or truncated call streams stop before tool execution, and failed attempts are not retried after output has already been shown.
- **Provider compatibility and Z.AI routing are now explicit.** Immutable compatibility profiles keep request-format quirks behind the chat-provider boundary, while independent Z.AI Coding Plan and API login routes use separate credentials, endpoints, model identities, catalog refresh, logout, and usage/rate-limit state. Curated GLM requests now apply exact context/output limits, thinking controls, reasoning replay, and tool-stream support without activating for local or unknown models.

## 0.58.0 (2026-07-11)

Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,21 @@ For hosted Pythinker models or ACP terminal auth:
pythinker login
```

Z.AI exposes two independent OpenAI-compatible routes. Choose the route that owns your key;
both may coexist, and Pythinker never infers, migrates, falls back, or retries across them:

```sh
# Coding Plan subscription
pythinker login --z-ai-coding # reads ZAI_CODING_API_KEY when set

# Standard pay-as-you-go API
pythinker login --z-ai-api # reads ZAI_API_KEY when set
```

These create distinct `z-ai-coding/*` and `z-ai-api/*` model identities. See the
[provider guide](./docs/en/configuration/providers.md#managed-zai-routes) for endpoints,
curated GLM capabilities, and route-scoped `/usage` behavior.

### 💬 Try it out

```sh
Expand Down
22 changes: 22 additions & 0 deletions docs/en/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,28 @@ Provides an OpenAI Admin API key for `/usage` cost data. If unset, `/usage` fall
export OPENAI_ADMIN_KEY="sk-admin-xxx"
```

## Z.AI environment variables

Z.AI Coding Plan and Z.AI API use separate credential variables because they are independent
routes:

| Environment Variable | Route | Base URL |
| --- | --- | --- |
| `ZAI_CODING_API_KEY` | Z.AI Coding Plan | `https://api.z.ai/api/coding/paas/v4` |
| `ZAI_API_KEY` | Z.AI API | `https://api.z.ai/api/paas/v4` |

```sh
export ZAI_CODING_API_KEY="your-coding-plan-key"
pythinker login --z-ai-coding

export ZAI_API_KEY="your-api-key"
pythinker login --z-ai-api
```

Each login reads only its route's variable. Pythinker does not infer the route from the key,
fall back to the other variable, migrate credentials, or retry requests across routes. Both
variables may be set when both routes are configured.

## Other environment variables

| Environment Variable | Description |
Expand Down
35 changes: 35 additions & 0 deletions docs/en/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ After configuration, Pythinker Code will automatically save settings to `~/.pyth
| OpenAI ChatGPT Codex | OpenAI managed account login |
| Pythinker AI Open Platform (pythinker-ai.cn) | China region API endpoint |
| Pythinker AI Open Platform (pythinker-ai.ai) | Global region API endpoint |
| Z.AI Coding Plan | Subscription route at `api.z.ai/api/coding/paas/v4` |
| Z.AI API | Pay-as-you-go route at `api.z.ai/api/paas/v4` |
| LM Studio | Local models served via LM Studio |
| Ollama | Local models served via Ollama |

Expand Down Expand Up @@ -64,6 +66,39 @@ base_url = "https://api.openai.com/v1"
api_key = "sk-xxx"
```

### Managed Z.AI routes

Z.AI Coding Plan and Z.AI API are independent managed routes. Configure the route that owns
your key; Pythinker does not infer a route from the credential, migrate credentials between
routes, or retry a request against the other endpoint.

| Route | Login | Provider key | Model prefix | Base URL | Environment variable |
| --- | --- | --- | --- | --- | --- |
| Coding Plan | `pythinker login --z-ai-coding` | `managed:z-ai-coding` | `z-ai-coding/` | `https://api.z.ai/api/coding/paas/v4` | `ZAI_CODING_API_KEY` |
| API | `pythinker login --z-ai-api` | `managed:z-ai-api` | `z-ai-api/` | `https://api.z.ai/api/paas/v4` | `ZAI_API_KEY` |

The same routes are available in the interactive selector as `/login z-ai-coding` and
`/login z-ai-api`. They may coexist in one config; login, catalog refresh, logout, default-model
repair, and cached rate-limit headers remain scoped to the selected route. `/usage` shows a
route-specific note because Z.AI does not document a route-wide usage endpoint; after a chat
request, captured rate-limit headers are displayed for that route when available.

Pythinker applies a provider compatibility profile to its curated GLM catalog:

| Model | Context tokens | Maximum output tokens | Thinking | Streamed tool calls |
| --- | ---: | ---: | --- | --- |
| `glm-5.2` | 1,000,000 | 131,072 | Tiered (`high` / `max`) | Yes |
| `glm-5.1` | 204,800 | 131,072 | Binary | Yes |
| `glm-5` | 204,800 | 131,072 | Binary | Yes |
| `glm-5-turbo` | 204,800 | 131,072 | Binary | Yes |
| `glm-4.7` | 204,800 | 131,072 | Binary | Yes |
| `glm-4.5-air` | 131,072 | 98,304 | Binary | No |

On these OpenAI-compatible routes, the full-context model id is plain `glm-5.2`;
`glm-5.2[1m]` is not an alias. Unknown Z.AI models keep conservative request defaults until
they are curated. Z.AI reasoning replay uses
only reasoning content the provider returned; Pythinker does not synthesize missing reasoning.

### `openai_responses`

For OpenAI Responses API (newer API format).
Expand Down
18 changes: 15 additions & 3 deletions docs/en/customization/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ The end-to-end flow when a session starts and processes a turn:
| `src/pythinker_code/cli/` | Typer command tree and UI-mode routing; lazy-loaded subcommands. | `cli`, `pythinker`, `login`, `logout`, `term`, `acp`, lazy group `info`, `export`, `mcp`, `plugin`, `skill`, `review`, `secscan`, `security-scan`, `debug`, `update`, `dashboard`, `web` |
| `src/pythinker_code/app.py` | Builds `PythinkerCLI`, `Runtime`, and `PythinkerSoul`; wires telemetry and frontends. | `PythinkerCLI.create`, `PythinkerCLI.run`, `run_shell` / `run_print` / `run_acp` / `run_wire_stdio` |
| `src/pythinker_code/config.py` | Three-scope config resolution (user → project → local TOML) with env overlay and JSON→TOML migration; `SecretStr` fields; scope locks on `api_key`/`providers`/`services`. | `Config`, `load_config`, `save_config`, `get_config_file` |
| `src/pythinker_code/llm.py` | Provider/model selection and capability derivation; wires `pythinker-core` backends. | `LLM`, `create_llm`, `augment_provider_with_env_vars`, `derive_model_capabilities` |
| `src/pythinker_code/llm.py` | Provider/model selection and capability derivation; resolves one compatibility profile and wires `pythinker-core` backends. | `LLM`, `create_llm`, `augment_provider_with_env_vars`, `derive_model_capabilities` |
| `src/pythinker_code/provider_compatibility.py` | Immutable provider/model compatibility profiles for request format, reasoning replay, generation overrides, output limits, and deferred-tool support. Resolution prefers managed identity, then normalized endpoint/API family. | `ProviderCompatibility`, `resolve_provider_compatibility`, `get_zai_model_policy` |
| `src/pythinker_code/agentspec.py` | Parses/validates agent YAML specs and resolves `extend`. | `load_agent_spec`, `ResolvedAgentSpec`, `DEFAULT_AGENT_FILE` |

## Soul: the agent loop
Expand Down Expand Up @@ -163,8 +164,19 @@ See `src/pythinker_code/tools/AGENTS.md`.
Provider modules in `auth/`: `openai`, `anthropic_direct`, `opencode_go`, `minimax`,
`deepseek`, `openrouter`, `z_ai`, `alibaba`, `lm_studio`, `ollama`, `moonshot`, and
`github_feedback`. Managed provider keys follow `managed:<platform_id>`; managed model ids
follow `<platform_id>/<model_id>`. Provider-aware code derives the provider from the active
model; `/usage` defaults to the active provider, with `/usage all` as the explicit aggregate.
follow `<platform_id>/<model_id>`. Z.AI has two explicit identities:
`managed:z-ai-coding` / `z-ai-coding/*` for the Coding Plan route and
`managed:z-ai-api` / `z-ai-api/*` for the standard API route. Their credentials, model
catalogs, lifecycle operations, usage notes, and rate-limit snapshots never cross route
boundaries. Provider-aware code derives the provider from the active model; `/usage` defaults
to the active provider, with `/usage all` as the explicit aggregate.

`src/pythinker_code/provider_compatibility.py` is the application-side source of provider
quirks. It resolves before `create_llm()` builds the `ChatProvider`; `PythinkerSoul` consumes
only generic profile fields (such as supported thinking levels) and stays free of provider-name
branches. `OpenAILegacy` owns transport
conversion, including explicit reasoning replay modes and copied per-request generation
parameters.

## Benchmark runner

Expand Down
12 changes: 11 additions & 1 deletion docs/en/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ first assembled request, the command reports that no manifest is available.
Log in or configure an API platform. After execution, first select a platform:

- **Pythinker**: Automatically opens a browser for OAuth authorization
- **Z.AI Coding Plan** (`/login z-ai-coding`): Configures the subscription route
- **Z.AI API** (`/login z-ai-api`): Configures the pay-as-you-go route
- **Other platforms**: Enter an API key, then select an available model

After configuration, settings are automatically saved to `~/.pythinker/config.toml` and reloaded. See [Providers](../configuration/providers.md) for details.
The two Z.AI entries are independent and may coexist. Each keeps its own credential, endpoint,
model prefix, catalog refresh, and logout lifecycle; Pythinker never retries one route through
the other. After configuration, settings are automatically saved to
`~/.pythinker/config.toml` and reloaded. See [Providers](../configuration/providers.md) for
details.

Alias: `/setup`

Expand Down Expand Up @@ -220,6 +226,10 @@ Usage:
- `/usage <provider-key>`: Show usage for a specific provider
- `/usage --json`: Output the report as JSON

For Z.AI, the Coding Plan and API reports are labeled separately. Z.AI has no documented
route-wide usage endpoint, so each report shows an explanatory note and adds cached rate-limit
headers only from requests made through that exact route.

Aliases: `/status`, `/cost`

### `/mcp`
Expand Down
1 change: 1 addition & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ GitHub Releases page; `0.8.0` is the new starting line.
## Unreleased

- **Parallel streamed tool calls are now correlated safely.** Interleaved argument chunks stay attached to their indexed calls, malformed or truncated call streams stop before tool execution, and failed attempts are not retried after output has already been shown.
- **Provider compatibility and Z.AI routing are now explicit.** Immutable compatibility profiles keep request-format quirks behind the chat-provider boundary, while independent Z.AI Coding Plan and API login routes use separate credentials, endpoints, model identities, catalog refresh, logout, and usage/rate-limit state. Curated GLM requests now apply exact context/output limits, thinking controls, reasoning replay, and tool-stream support without activating for local or unknown models.

## 0.58.0 (2026-07-11)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from typing import Literal

type ToolMessageConversion = Literal["extract_text"]
type ReasoningReplayMode = Literal["exact", "tool_calls", "strict_synthetic"]
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
thinking_effort_to_reasoning_effort,
tool_to_openai,
)
from pythinker_core.contrib.chat_provider.common import ToolMessageConversion
from pythinker_core.contrib.chat_provider.common import (
ReasoningReplayMode,
ToolMessageConversion,
)
from pythinker_core.message import ContentPart, Message, TextPart, ThinkPart, ToolCall, ToolCallPart
from pythinker_core.tooling import Tool

Expand Down Expand Up @@ -76,6 +79,9 @@ def __init__(
base_url: str | None = None,
stream: bool = True,
reasoning_key: str | None = None,
reasoning_replay_mode: ReasoningReplayMode | None = None,
auto_reasoning_effort: bool = True,
tool_stream: bool = False,
tool_message_conversion: ToolMessageConversion | None = None,
**client_kwargs: Any,
):
Expand All @@ -98,6 +104,11 @@ def __init__(
"""The underlying `AsyncOpenAI` client."""
self._reasoning_effort: ReasoningEffort | Omit = omit
self._reasoning_key = reasoning_key
self._reasoning_replay_mode: ReasoningReplayMode = reasoning_replay_mode or (
"strict_synthetic" if _is_strict_interleaved_model(model) else "tool_calls"
)
self._auto_reasoning_effort = auto_reasoning_effort
self._tool_stream = tool_stream
self._tool_message_conversion: ToolMessageConversion | None = tool_message_conversion
self._generation_kwargs: OpenAILegacy.GenerationKwargs = {}

Expand All @@ -124,14 +135,22 @@ async def generate(
messages.extend(self._convert_message(message) for message in history)

generation_kwargs: dict[str, Any] = {}
generation_kwargs.update(self._generation_kwargs)
generation_kwargs.update(copy.deepcopy(self._generation_kwargs))
if self._tool_stream and tools:
extra_body = dict(generation_kwargs.get("extra_body") or {})
extra_body["tool_stream"] = True
generation_kwargs["extra_body"] = extra_body

reasoning_effort = self._reasoning_effort
# Auto-enable reasoning_effort when the history contains ThinkPart but reasoning
# was not explicitly configured. This prevents server validation errors from APIs
# (e.g. One API) that require reasoning_effort when messages contain reasoning_content.
# See: https://github.com/Pythoughts-labs/pythinker-code/issues/1616
if isinstance(reasoning_effort, Omit) and self._reasoning_key:
if (
self._auto_reasoning_effort
and isinstance(reasoning_effort, Omit)
and self._reasoning_key
):
has_think_part = any(
isinstance(part, ThinkPart) for message in history for part in message.content
)
Expand Down Expand Up @@ -213,19 +232,15 @@ def _convert_message(self, message: Message) -> ChatCompletionMessageParam:
else:
message.content = content
dumped_message = message.model_dump(exclude_none=True)
if self._reasoning_key:
# Kimi-style interleaved-thinking providers require consistent
# reasoning replay metadata on assistant history. At a minimum
# tool-call turns need the field, and known strict models are
# safest when all assistant turns include it, even if empty.
has_tool_calls = message.role == "assistant" and bool(message.tool_calls)
strict_interleaved = message.role == "assistant" and _is_strict_interleaved_model(
self.model
)
if reasoning_content or has_tool_calls or strict_interleaved:
if strict_interleaved and not reasoning_content:
reasoning_content = message.extract_text() or "[reasoning unavailable]"
dumped_message[self._reasoning_key] = reasoning_content
if self._reasoning_key and reasoning_content:
dumped_message[self._reasoning_key] = reasoning_content
elif self._reasoning_key and message.role == "assistant":
if self._reasoning_replay_mode == "tool_calls" and message.tool_calls:
dumped_message[self._reasoning_key] = ""
elif self._reasoning_replay_mode == "strict_synthetic":
dumped_message[self._reasoning_key] = (
message.extract_text() or "[reasoning unavailable]"
)
return cast(ChatCompletionMessageParam, dumped_message)


Expand Down
Loading
Loading