Disclosure: I work on nRouter. This post was drafted with AI assistance and reviewed by me before publishing.
Most SDK guides stop at "install it, call it, good luck." The parts that decide whether an integration survives production — what comes back with the response, how streaming behaves, what a failure looks like — get left for you to discover at 2 a.m. So I went through our own Python SDK guide the way I'd read a vendor's: looking for the operational details. Here's what it actually specifies.
Install, and one naming trap
pip install nrouter-sdk
The trap: the package installs as nrouter-sdk, but you import it as nroutersdk — no hyphen. The client class is nRouter, and it reads your key from the environment:
export NROUTER_API_KEY="sk-nrouter-xxxxxxxxxxxx"
No key handling in code, no config object to thread through your app. The client targets https://api.nrouter.ai/v1 by default.
The first call hands you the receipt
from nroutersdk import nRouter # Reads NROUTER_API_KEY and targets https://api.nrouter.ai/v1
with nRouter() as client:
response = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[{"role": "user", "content": "Hello, nRouter!"}],
)
print(response.choices[0].message.content)
if client.last_response:
print(f"Request ID: {client.last_response.request_id}")
That last_response handle is the detail I'd steal for any client library. The gateway parses its own x-nr-* response headers and exposes the metadata on the client — request ID, cost figure, cache status — instead of leaving you to dig through raw headers when support asks "which request was that?" Anyone who has grepped logs for a request ID that was never logged knows why this matters.
The client is fully wire-compatible with both OpenAI Chat Completions and the native Anthropic Messages format, so the model string in that call is the only real decision on the page.
Streaming is a loop, not a callback maze
with nRouter() as client:
stream = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[{"role": "user", "content": "Summarize distributed consensus."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
An iterator of chunks with a delta you print as it arrives. No event emitter, no handler registration. In a notebook or a CLI tool this is exactly the shape you want.
Errors are typed, and the order matters
This is the section most SDK docs skip entirely. The guide gives four exception types, and the handling order in its example is specific-before-generic:
from nroutersdk import (
nRouter,
nRouterError,
nRouterAuthenticationError,
nRouterRateLimitError,
nRouterGuardrailBlockedError,
)
try:
# ... a chat completion call ...
pass
except nRouterGuardrailBlockedError as e:
print(f"Blocked: {e.message}")
except nRouterRateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after} seconds")
except nRouterAuthenticationError as e:
print(f"Authentication failed: {e.message}")
except nRouterError as e:
print(f"nRouter API error [{e.status_code}]: {e.message}")
Three things worth noticing. A guardrail block is its own type, so policy rejections never get mistaken for outages and retried. The rate-limit error carries retry_after, so your backoff reads a number instead of parsing a header. And the generic nRouterError still gives you status_code and message, so the fallthrough case is debuggable rather than a bare exception. The guide frames this as recovering cleanly from network timeouts, quota limits and guardrail redactions — typed errors are how that stops being a slogan.
The guide
Full page, with the complete examples: https://dev.nrouter.ai/docs/sdks/python
If you write Python against LLM APIs: what does your current client hand back after a call — and how many of its failures can you actually tell apart in code? That's the gap this guide is aiming at, and I'm curious how other SDKs compare.
Top comments (0)