Engineering

Migrating OpenAI and Anthropic SDK calls to SchneeAI

The client-side migration is one base URL and one auth header. The actual diff in Python, TypeScript, and curl — plus the pitfalls that show up in real migrations.

SchneeAI2026-07-148 min read

If your backend calls chat.completions.create(...) from the OpenAI SDK, or client.messages.create(...) from the Anthropic SDK, migrating to SchneeAI is deliberately small. The client-side change is one base URL and one auth header. The request body — model, messages, tools, temperature — is unchanged. So is the response shape.

This is by design. SchneeAI’s Gateway is OpenAI-compatible at the wire level: same endpoint shape (POST /v1/chat/completions), same field names, same streaming SSE format. Your SDK does not know SchneeAI exists.

This post walks the actual diff, language by language, with the gotchas that show up in real migrations. If you are still weighing whether to migrate at all, read SchneeAI vs LiteLLM first; this post assumes you have decided to.

Design-partner phase. The public api.schneeai.com endpoint is activated at general availability. To run the examples below today, request design-partner access at [email protected] — we will issue a partner-specific endpoint and key. Replace https://api.schneeai.com in the examples with your partner endpoint.

1. What actually changes

Two things, and an optional third:

  1. Base URL: https://api.openai.com/v1https://api.schneeai.com/v1
  2. Auth header: Authorization: Bearer sk-...Authorization: Bearer <schneeai-key>
  3. Model (optional): "gpt-4o""auto" (or any alias in the Model Directory)

The request body is unchanged. Messages, tools, temperature, top_p, max_tokens, stream, stop, response_format — every field OpenAI’s API accepts, SchneeAI accepts at the same path with the same semantics.

This is the design philosophy: your SDK is the integration surface, not SchneeAI. We expose OpenAI’s wire shape, so the entire ecosystem of OpenAI-compatible clients — the official Python and TypeScript SDKs, LangChain’s OpenAI adapter, AutoGen, Continue, Aider, OpenWebUI — works against SchneeAI without source changes.

2. Before and after: the diff

Python — OpenAI SDK

# before — direct OpenAI call
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    # base_url defaults to https://api.openai.com/v1
)

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Hello"}],
)
# after — through SchneeAI
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["SCHNEEAI_API_KEY"],
    base_url="https://api.schneeai.com/v1",
)

resp = client.chat.completions.create(
    model="auto",  # or pin "gpt-4o-mini", "claude-haiku-4-5", etc.
    messages=[{"role": "user", "content": "Hello"}],
)

# response shape is unchanged
print(resp.choices[0].message.content)

# SchneeAI extensions live alongside
print(resp.model)        # "auto→gpt-4o-mini" — what the router picked
print(resp.request_id)   # trace handle for support

Same import. Same constructor. Same method. Two arguments change. The migration review is a 30-second diff.

TypeScript — openai-node

// before
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY!,
});

const resp = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Hello" }],
});
// after
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.SCHNEEAI_API_KEY!,
  baseURL: "https://api.schneeai.com/v1",
});

const resp = await client.chat.completions.create({
  model: "auto",
  messages: [{ role: "user", content: "Hello" }],
});

curl

# before — direct OpenAI call
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "messages": [{"role": "user", "content": "Hello"}]
  }'
# after — through SchneeAI
curl https://api.schneeai.com/v1/chat/completions \
  -H "Authorization: Bearer $SCHNEEAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "auto",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

Response shape

The response is OpenAI-shaped. SchneeAI extensions live alongside, not in place of:

{
  "id": "chatcmpl-...",
  "choices": [{
    "message": { "role": "assistant", "content": "Hi there." }
  }],
  "usage": { "prompt_tokens": 8, "completion_tokens": 4, "total_tokens": 12 },
  "model": "auto→gpt-4o-mini",
  "request_id": "req_01HQ..."
}

Existing code that reads resp.choices[0].message.content works unchanged. Code that reads resp.model will see auto→... instead of the literal gpt-4o-mini — this is a feature (you learn what the router picked), but be aware if you have assertions that compare model strings exactly.

3. Auth: Bearer header, SchneeAI credential

The header shape is identical to direct OpenAI calls: Authorization: Bearer .... What changes is what goes in the header — instead of an OpenAI-issued sk-... key, you send a SchneeAI-issued credential scoped to your service.

During the design-partner phase, that credential is a partner API key we issue when you register. At general availability it becomes a short-lived JWT minted from your service credentials via Ory Hydra — same Bearer header, signed token instead of long-lived key. The full token-acquisition flow is in the Tutorial.

The migration-relevant point: the header shape does not change. If your existing code stores the OpenAI key in OPENAI_API_KEY, the cleanest migration is to add SCHNEEAI_API_KEY alongside, switch the SDK init to read it, and remove the OpenAI env var once traffic has moved.

4. Model aliases: auto, or pin one

The "model" field accepts three kinds of values:

  • "auto" — SchneeAI’s router picks based on your routing policy (latency target, budget remaining, feature tag).
  • A SchneeAI alias — "gpt-4o", "claude-sonnet-4-5", "gemini-2-5-flash", "deepseek-v3", etc. (full list in the Model Directory).
  • A provider-specific name — SchneeAI maps and forwards it.

The pragmatic migration path: start with your existing model pinned during the migration window, then switch to "auto" for new traffic once you trust the router.

The reason to prefer "auto" over pinning: providers ship new models monthly, pricing shifts, and a hardcoded "gpt-4o" will outlive its cost-effectiveness. Routing policy is one config edit; code changes are a deploy.

5. What you get for free after the switch

Once traffic flows through SchneeAI, you inherit the eight-hop Gateway path without writing any of it:

  • Pre-call PII scanning — 17 categories; a leaked API key in a prompt never reaches the provider.
  • Budget enforcement — per-service, per-tenant, per-feature caps; over-budget calls return 429 with code: budget_exceeded before any provider spend.
  • Encrypted raw retention — every prompt and response lands in the Vault (AES-256 at rest), queryable for incident response.
  • Structured usage — service / tenant / user / model / tokens / cost in micro-USD, queryable via GET /v1/usage.
  • Audit trail — append-only record of every request, policy change, and operational action.
  • Provider abstraction — if OpenAI has an outage, your routing policy can fail over to Anthropic or DeepSeek without a code change.

These are the layers most teams build in their second or third quarter of operating AI in production. The point of SchneeAI is that you inherit them on day one — without changing your call sites.

6. Pitfalls

Migrations go wrong in predictable ways. The five we see most:

Hardcoded provider URLs

The OpenAI SDK lets you skip base_url because it defaults to https://api.openai.com/v1. Code that hardcodes the URL — bypassing the SDK — is easy to miss:

# broken — bypasses the SDK, hits OpenAI directly
res = requests.post(
    "https://api.openai.com/v1/chat/completions",  # hardcoded
    headers={"Authorization": f"Bearer {OPENAI_API_KEY}"},
    json={"model": "gpt-4o", "messages": messages},
)
# fixed — through SchneeAI
res = requests.post(
    "https://api.schneeai.com/v1/chat/completions",
    headers={"Authorization": f"Bearer {SCHNEEAI_API_KEY}"},
    json={"model": "auto", "messages": messages},
)

Audit for api.openai.com, api.anthropic.com, and generativelanguage.googleapis.com across your codebase before declaring the migration done.

Streaming SSE clients that buffer

The OpenAI SDK’s streaming mode works against SchneeAI unchanged. Hand-rolled SSE parsers sometimes assume the upstream buffers full events — SchneeAI streams the provider’s SSE through as it arrives, so partial-event buffering matters. If you see truncation, check your reader’s buffer handling.

Tool-calling shape parity

SchneeAI forwards the tools array to the provider as-is. OpenAI’s tool-call shape (choices[0].message.tool_calls) is preserved on the way back. If your code reads tool calls from the response, it works unchanged — but cross-check if you depend on provider-specific quirks (Anthropic’s tool-use differs in edge cases).

SDK version pinning

The OpenAI Python SDK at v1.x and the TypeScript SDK at v4.x are the versions we test against. Older SDK versions (Python 0.x, TypeScript 3.x) use a different shape and will not work without code changes. If you are on a legacy SDK, upgrade as part of the migration — it is a one-time cost.

Anthropic SDK is not OpenAI-shaped

The Anthropic SDK (@anthropic-ai/sdk in TypeScript, anthropic in Python) uses a different request shape — messages.create with max_tokens required, different system handling, different tool format. SchneeAI does not change the Anthropic SDK’s shape, so you cannot point it at SchneeAI.

If you are migrating from the Anthropic SDK specifically, you have two paths:

  1. Switch to the OpenAI SDK against SchneeAI — recommended. You get access to every provider SchneeAI routes to (including Anthropic models), with the OpenAI-shaped ergonomics.
  2. Use raw requests.post / fetch against POST /v1/chat/completions — if you want zero SDK dependency.

The Anthropic models (claude-sonnet-4-5, claude-haiku-4-5) are available through SchneeAI under those aliases. You do not lose model access by switching to the OpenAI shape.

7. Migration checklist

A practical pre-flight for cutting traffic over:

  • SCHNEEAI_API_KEY added to your secret manager; OPENAI_API_KEY (or ANTHROPIC_API_KEY) still in place for rollback.
  • All SDK initializations updated to read base_url / baseURL from the SchneeAI endpoint.
  • Codebase grep for api.openai.com, api.anthropic.com, generativelanguage.googleapis.com — only test fixtures or documentation should match.
  • Smoke test: one production-shaped request through SchneeAI, verify resp.choices[0].message.content parses correctly.
  • Verify resp.model shows auto→... (or the pinned alias) — confirms the router is in the path.
  • Verify resp.request_id is non-empty — your trace handle for support.
  • Streaming: confirm a stream: true request returns SSE chunks within expected latency.
  • Retries: add an Idempotency-Key header (UUIDv4) on retried requests so duplicates do not double-charge. See Error Handling.
  • Cut a low-stakes feature over first (internal tooling, batch job). Watch GET /v1/usage for a day.
  • Cut remaining features.
  • Remove OPENAI_API_KEY / ANTHROPIC_API_KEY from the secret manager once the rollback window closes.

For the full error model — retry rules, idempotency, status code semantics — see Error Handling.

What this buys you

The promise of an OpenAI-compatible API is that migration is a configuration change, not a rewrite. That is genuinely true for the call site: two arguments in the SDK init, one env var.

What you get on the other side is the part that is hard to build yourself: governance, billing, audit, PII scanning, encrypted retention, and a routing layer that lets you treat “which model” as a policy decision instead of a code change. The Gateway architecture walk-through covers what each hop does in detail.

If you want to try the migration on a real workload, the Tutorial has runnable examples in curl, Python, and TypeScript — and email us to request design-partner access.