feat(zai): add zai-anthropic binding — GLM models via the Claude Agent SDK - #86
Open
pofallon wants to merge 2 commits into
Open
Conversation
…t SDK Z.AI exposes an endpoint speaking Anthropic's Messages wire format, which is what lets the `claude` CLI drive it. This adapter reuses ClaudeCodeRuntime's entire harness and changes only the three things that make it a different binding: base URL, auth variable, feature surface. Its own PROVIDER_ID rather than a config flag on `claude`, per the binding rule: `supports()` is a ClassVar manifest, so one ID carries exactly one honest capability surface. `claude` declares 22 features against Anthropic's endpoint; several are unverified against Z.AI's. The ID is `zai-anthropic` rather than a bare `zai` because Z.AI also fronts an OpenAI-compatible surface, which belongs to a future `zai-openai` sibling. Credential handling: - `ZAI_API_KEY` → `ANTHROPIC_AUTH_TOKEN` in the subprocess env. No fallback to any Anthropic credential — those authenticate a different account at a different vendor. - `_subprocess_env()` shadows inherited ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / CLAUDE_CODE_OAUTH_TOKEN with empty strings. The Agent SDK merges ClaudeAgentOptions.env over os.environ and cannot unset a key, so shadowing is the only mechanism available. This is the subprocess-boundary counterpart to the HTTP-boundary fix in #84. Feature surface is deliberately understated. CLI-side features (streaming, resume, cancel, tools, hooks, permission, budget, slash commands) are protocol-independent and retained. Endpoint-dependent ones (thinking, vision, file input, count_tokens, rate-limit telemetry, request metadata) report False pending live verification, because a supports() that overstates produces a runtime failure the consumer was told couldn't happen. STRUCTURED_OUTPUT_JSON_SCHEMA stays True despite being equally unverified — the conformance suite makes it the floor, so declaring it False would yield a non-conforming adapter rather than a cautious one. examples/probe_zai.py verifies each flag against a real key. Supporting changes to shared code, all driven by this being the first adapter to decline features its harness implements: - Extract `ClaudeCodeRuntime._subprocess_env()` as an overridable hook. Previously the env dict was built inline in the session and hardcoded ANTHROPIC_API_KEY. - Add `_check_thinking_supported()` to sessions.py and call it before connect. The thinking= kwarg had no capability gate; ClaudeCodeRuntime never noticed because it declares both reasoning flags True. - Read VISION_INPUT / FILE_INPUT from the capability flags in `_split_prompt_parts` calls rather than hardcoding True. - Register `zai-anthropic` in the conformance suite's provider-options map. It reuses ClaudeOptions — a distinct binding does not imply a distinct options namespace. Overrides that would otherwise misbehave: `list_models()` returns a static catalog (no /v1/models to query) and `count_tokens()` raises UnsupportedFeatureError (no such route on Z.AI). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBTr6Q6kTGUMQiwwBHcdMj
…nesses The adapter shipped without registering in the credential maps the existing test tooling reads, so it was invisible to all of it: - `testing/integration.py::_PROVIDER_AUTH` had no entry, and `_has_credentials` returns False for any unlisted provider — every integration test would have skipped silently even with a valid key. - `examples/smoke_providers.py` printed "no known credential mapping" rather than exercising the adapter. - `.env.example` documented seven of eight bindings. Adds the missing entries plus `tests/test_zai_integration.py`, which imports a deliberately narrower contract set than the Claude suite: thinking is declined pending verification (the unit conformance suite already covers the decline), and `list_models()` is a static catalog with no network call, so a green result there would prove nothing about the endpoint. Both become worth importing if examples/probe_zai.py promotes the corresponding flags. The fixture skips on a missing `claude` CLI as well as a missing key — this binding needs both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBTr6Q6kTGUMQiwwBHcdMj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Z.AI exposes an endpoint that speaks Anthropic's Messages wire format, which is what lets the
claudeCLI drive it at all. This adapter reusesClaudeCodeRuntime's entire harness — subprocess lifecycle, sessions, streaming, tools, hooks — and changes only what makes it a different binding.Why its own
PROVIDER_IDPer the binding rule (#85):
supports()is a ClassVar manifest, so one ID carries exactly one honest capability surface.claudedeclares 22 features against Anthropic's endpoint; several are unverified against Z.AI's. Sharing the ID would makesupports()lie about one of the two.zai-anthropicrather than a barezaibecause Z.AI also fronts an OpenAI-compatible surface — that belongs to a futurezai-openaisibling onOpenAICompatibleRuntime. Same reasoning that kept the Kimi Agent SDK adapter from claimingmoonshot.Pointing
ClaudeCodeRuntimeat Z.AI viaANTHROPIC_BASE_URLstill works as an escape hatch. An escape hatch is not a supported binding.Credentials
ZAI_API_KEY→ANTHROPIC_AUTH_TOKENin the subprocess env, with no fallback to any Anthropic credential._subprocess_env()also shadows inheritedANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN/CLAUDE_CODE_OAUTH_TOKENwith empty strings. The Agent SDK mergesClaudeAgentOptions.envoveros.environand offers no way to unset a key, so shadowing is the only available mechanism. Without it, a developer withCLAUDE_CODE_OAUTH_TOKENin a shell profile would have the CLI carry their Anthropic subscription token to Z.AI — the same leak class as #84, one layer down.Feature surface: deliberately understated
count_tokens, rate-limit telemetry, request metadata)Falsepending live verificationA
supports()that overstates produces a runtime failure the consumer was told couldn't happen; one that understates just routes them elsewhere.STRUCTURED_OUTPUT_JSON_SCHEMAstaysTruedespite being equally unverified. The conformance suite makes it the floor (test_supports_structured_output_json_schema_is_true), so declaring itFalsewould produce a non-conforming adapter rather than a cautious one. If a live probe shows Z.AI doesn't honour--json-schema, that's a blocker on the adapter, not a bit to flip.examples/probe_zai.pychecks it first and says so explicitly.Shared-code changes
This is the first adapter to decline features its harness implements, which exposed three gaps:
ClaudeCodeRuntime._subprocess_env()extracted as an overridable hook. The env dict was built inline in the session and hardcodedANTHROPIC_API_KEY._check_thinking_supported()added tosessions.py, called before connect. Thethinking=kwarg had no capability gate at all —ClaudeCodeRuntimenever noticed because it declares both reasoning flagsTrue. Three conformance contracts caught this.VISION_INPUT/FILE_INPUTnow read from the capability flags in_split_prompt_partscalls instead of hardcodedTrue.Also registers
zai-anthropicin the conformance provider-options map — it reusesClaudeOptions, since a distinct binding doesn't imply a distinct options namespace.Overrides
list_models()→ static catalog. No/v1/modelsto query; per-token pricing isNonerather than invented, since the coding plan bills flat-fee.count_tokens()→UnsupportedFeatureError. Inheriting the parent would POST to/v1/messages/count_tokens, a route Z.AI doesn't serve.Tests
make cigreen: 1080 → 1113 passed, 40 skipped.tests/test_zai.py— 33 tests covering identity, auth resolution, the credential-shadowing regression (parametrized over all three leaky vars), capability surface, and both overridden endpoints.tests/test_zai_conformance.py— the full 38-contract suite. Running it here is the point: most contracts are "behaviour agrees with the capability flag", so they assert the opposite branch than they do for the parent.tests/test_discovery.py— updated. Installing[claude]now surfaces two providers, which is the binding rule made concrete:PROVIDER_IDis notREQUIRES_PACKAGE.Not verified
I have no Z.AI key, so every live behaviour is unverified — hence the conservative flags and the probe script. The model catalog (
glm-4.6,glm-4.5-air) and their context windows come from outside knowledge, not from the code; worth a check against Z.AI's current docs before release.🤖 Generated with Claude Code
https://claude.ai/code/session_01UBTr6Q6kTGUMQiwwBHcdMj