Anthropic SDK

Written for anthropic 1.11.0 (Python) and @anthropic-ai/sdk 0.131.0 (npm), from their documentation and source. Both SDKs work against zdr.sh by changing the base URL to the origin only: they append /v1/messages themselves, so https://api.zdr.sh/v1 would post to /v1/v1/messages.

Python#

python
import osfrom anthropic import Anthropicclient = Anthropic(base_url="https://api.zdr.sh", api_key=os.environ["ZDR_API_KEY"])# or: Anthropic(base_url="https://api.zdr.sh", auth_token=os.environ["ZDR_API_KEY"])with client.messages.stream(    model="glm-5.3",    max_tokens=8192,    messages=[{"role": "user", "content": "hi"}],) as stream:    for event in stream:        if event.type == "thinking":            print(event.thinking, end="")        elif event.type == "text":            print(event.text, end="")    message = stream.get_final_message()

JavaScript and TypeScript#

typescript
import Anthropic from '@anthropic-ai/sdk';const client = new Anthropic({ baseURL: 'https://api.zdr.sh', apiKey: process.env.ZDR_API_KEY });const stream = client.messages  .stream({ model: 'glm-5.3', max_tokens: 8192, messages: [{ role: 'user', content: 'hi' }] })  .on('thinking', (delta) => process.stdout.write(delta))  .on('text', (text) => process.stdout.write(text));const message = await stream.finalMessage();

Migrating an Anthropic codebase#

What works with no change beyond the base URL and the key:

  • messages.create and messages.stream, with system, messages, tools, tool_choice, stop_sequences, temperature, top_p and max_tokens
  • Extended thinking: thinking blocks with thinking_delta and signature_delta, with the signature issued by zdr.sh
  • messages.count_tokens, answered by the service
  • anthropic-version and anthropic-beta headers; ?beta=true accepted
  • cache_control breakpoints, and the usage fields cache_read_input_tokens and cache_creation_input_tokens
  • The errors, in the Anthropic envelope with the request id

What differs:

  • The model ids are the zdr.sh ones. A claude- id maps through the key's Claude mapping, which is on by default, and the response names the mapped id. models.list never lists a claude- id.
  • Server tools such as web_search_20250305 are dropped when the model cannot run them, with x-token-notice naming them.
  • Token counting happens on the server; there is no client-side count to fall back to.
  • There are no audio or file APIs. Embeddings are served in the OpenAI format at POST /v1/embeddings: Embeddings.
  • Idempotency-Key makes a retry bill once. Chat completions has the rules.
  • x-token-cost is extra, and a trailer on a stream.

Notes#

  • Credential. api_key is sent as X-Api-Key, auth_token as Authorization: Bearer. zdr.sh accepts either. With no credential the request throws at request time, not at construction.
  • Base URL. Hard-code it; both SDKs fall back to their vendor when the value is empty or absent. ANTHROPIC_BASE_URL and the matching key variables work as fallbacks when you do not pass arguments.
  • Counting. client.messages.count_tokens(...) reaches /v1/messages/count_tokens, so no token counting happens on the client.
  • Extra fields. extra_body, extra_query and extra_headers in Python, or the same options in TypeScript, carry anything the typed parameters do not.
  • Streaming without the helper. client.messages.create({..., stream: true}) with for await gives raw events; breaking out of the loop or calling stream.controller.abort() cancels the request, and what was produced is what you pay for.