OpenAI SDK

Written for openai 3.22.1 (Python) and 7.25.0 (npm), from their documentation and source. Both SDKs work against zdr.sh by changing the base URL to https://api.zdr.sh/v1; the key is a duck- key.

Python#

python
import osfrom openai import OpenAIclient = OpenAI(base_url="https://api.zdr.sh/v1", api_key=os.environ["ZDR_API_KEY"])stream = client.chat.completions.create(    model="glm-5.3",    messages=[{"role": "user", "content": "hi"}],    stream=True,)reasoning, text = [], []for chunk in stream:    if not chunk.choices:        continue    delta = chunk.choices[0].delta    rc = getattr(delta, "reasoning_content", None)  # extra field, not in the typed model    if rc:        reasoning.append(rc)    if delta.content:        text.append(delta.content)

reasoning_content is not a declared field, so read it with getattr or through model_extra. Not every chunk carries it.

JavaScript and TypeScript#

typescript
import OpenAI from 'openai';const client = new OpenAI({ baseURL: 'https://api.zdr.sh/v1', apiKey: process.env.ZDR_API_KEY });const stream = await client.chat.completions.create({  model: 'glm-5.3',  messages: [{ role: 'user', content: 'hi' }],  stream: true,});let reasoning = '';let text = '';for await (const chunk of stream) {  const delta = chunk.choices[0]?.delta as { reasoning_content?: string; content?: string };  if (delta?.reasoning_content) reasoning += delta.reasoning_content;  if (delta?.content) text += delta.content;}

Use the raw chunk loop rather than the .stream() helper: the helper copies unknown delta fields onto a snapshot and overwrites instead of appending, so reasoning_content would keep only the last fragment.

Migrating an OpenAI codebase#

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

  • chat.completions.create, streaming and not, with messages, tools, tool_choice, temperature, top_p, stop, response_format, max_tokens and max_completion_tokens
  • reasoning_effort on the models that publish efforts, and reasoning_content on the answers
  • usage, with prompt_tokens_details.cached_tokens and completion_tokens_details.reasoning_tokens filled in
  • responses.create, with input, instructions, tools, reasoning and text.format; the endpoint is stateless, so keep sending the conversation
  • models.list, and the errors, which arrive as the SDK's own error type with the zdr.sh code and message

What differs:

  • The model ids are the zdr.sh ones, and models.list is the source of truth. A key can map a name your code already sends to a model here, in its settings. Models lists the ids.
  • Hosted tools (web_search, file_search, code_interpreter) are dropped from a request, with x-token-notice naming them; the model answers without them.
  • A previous_response_id, a background: true or a stored prompt is a 400 stateless_only.
  • There are no audio or file APIs, and no batch or fine-tuning endpoints. Embeddings are served 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#

  • Hard-code the base URL. Both SDKs fall back to the vendor when the variable is unset, and an empty string counts as unset.
  • Environment route. OPENAI_BASE_URL=https://api.zdr.sh/v1 with OPENAI_API_KEY=duck-... works too, for code you would rather not touch.
  • Extra fields. Undocumented request fields travel as-is: extra_body and extra_headers in Python, or the body and headers options in Node.
  • Counting. The input counters are on the other two formats, Responses and Messages.
  • Errors. A non-2xx answer raises the SDK's own error type with our code and message; Errors lists the classes.