OpenAI SDK Migration Guide: Switch to EXAIV by Changing One Line
OpenAI SDK Migration Guide: Switch to EXAIV by Changing One Line
You already have an application running on the OpenAI SDK and you want to move it to EXAIV's model pool. The question that matters is not "does it work" but "how much code do I have to change". The answer: for most applications, exactly two values β the base URL and the API key. Request format, message structure, streaming handling, and error types all stay the same, because EXAIV provides an OpenAI Chat Completions compatible endpoint.
This guide walks through the migration in the order you would actually do it: what to change first, how to verify the change, how to map the model field, which behaviors to re-test, and how to roll back if something breaks. Your business logic never needs to change.
The 30-second version
In Python, swap two constructor arguments β OpenAI(api_key=..., base_url="https://exaiv.com/api/ai/v1") β set the model field to a model ID from the catalog, and keep everything else as-is.
The change list at a glance
| Location | Before (OpenAI) | After (EXAIV) |
|---|---|---|
| base_url | https://api.openai.com/v1 | https://exaiv.com/api/ai/v1 |
| API key | sk-... (OpenAI key) | A key generated in the platform console |
| model | OpenAI model name | A catalog model ID (e.g. kimi-k3) |
| Request/response shape | β | Unchanged |
| SDK version | β | Unchanged |
Note that the base URL ends with /v1: the SDK appends /chat/completions itself. If you paste the full path into base_url, the actual request becomes /chat/completions/chat/completions and returns 404. This is the most common first mistake in a migration.
Python: the openai SDK
First make sure the SDK is reasonably current (v1.x or later); older versions have quite different call signatures:
pip install --upgrade openai
Before and after:
# Before
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY, points at api.openai.com
# After
const client = new OpenAI({
apiKey: process.env.EXAIV_API_KEY,
baseURL: "https://exaiv.com/api/ai/v1",
});
const resp = await client.chat.completions.create({
model: "qwen3.8-max",
messages: [{ role: "user", content: "Introduce yourself in one sentence." }],
max_tokens: 128,
});
console.log(resp.choices[0].message.content);
One naming trap: the Python argument is base_url, the npm package is baseURL. Mixing them up is the most common typo in this migration.
Verify with curl before touching code
Before editing code, confirm the key and endpoint work with curl. If anything fails later, you will know the endpoint side is fine and the problem is in your code:
curl -sS https://exaiv.com/api/ai/v1/chat/completions \
-H "Authorization: Bearer ${EXAIV_API_KEY}" \
-H "content-type: application/json" \
-d '{
"model": "kimi-k3",
"messages": [{"role": "user", "content": "Reply with OK."}],
"max_tokens": 8
}'
A 200 with content in choices[0].message.content completes the endpoint-side verification.
Environment variables without the foot-guns
In practice, most migration breakage comes from environment plumbing rather than code. Three rules prevent the common failures. First, name your variables explicitly (EXAIV_API_KEY, EXAIV_BASE_URL) and pass them into the client constructor; implicit OPENAI_* variables are how a staging service quietly keeps calling the old provider after everyone believes it was migrated. Second, never let two providers share one variable name in the same process β if one part of your system still calls OpenAI directly, give it its own variables and construct a separate client. Third, print the resolved base URL once at startup (without the key) so that logs show which endpoint each process actually uses; a surprising number of "the migration does not work" reports end with the log showing the old URL.
For local development, a .env file loaded by your tooling is fine as long as it is gitignored. For deployed services, prefer the platform's native secret mechanism over committed files, and rotate keys by generating a new one in the console and updating the secret store β not by editing source.
Tests and CI: keep the suite provider-agnostic
Your unit and integration tests should not depend on which endpoint the client points at. The pattern that survives migrations is a thin factory plus injected responses:
# production.py
client = OpenAI(api_key=settings.key, base_url=settings.base_url)
# test.py
client = FakeClient(responses={"Introduce yourself": "A fixed reply."})
answer = ask(client, model="kimi-k3", prompt="Introduce yourself")
assert answer == "A fixed reply."
Assertions target the prompt and the model ID β not live network calls β so the suite runs in seconds and never flakes on upstream latency. Reserve real-endpoint tests for a small scheduled job that pings the minimal request once a day; it will catch credential expiry, route changes, and silent catalog moves far earlier than your users will.
In CI, set the EXAIV variables as secrets, not plain environment entries, and make sure the suite fails loudly if they are missing rather than silently falling back to OPENAI_* names. A missing-variable error at test time is annoying for a minute; a wrong-endpoint deploy is annoying for a lot longer.
The model field: the only "translation" work
OpenAI model names (deepseek-v4.1-flash, deepseek-v4.1-flash-mini, and so on) do not resolve on EXAIV; they must be mapped to real catalog IDs. Choose by the original model's role:
| Original use | Suggested starting ID | Notes |
|---|---|---|
| Everyday chat / light tasks | qwen3.7-flash, glm-5.3-flash | Fast responses, low cost |
| Main generation | kimi-k3, gpt-6-sol | 1M context, the quality workhorses |
| Reasoning / complex tasks | glm-5.3-flash | Fast orchestration and tool calling |
| Coding tasks | gpt-6-sol, glm-5.3 | Code understanding and generation |
Keep the mapping in configuration rather than scattered through code:
MODEL_MAP = {
"deepseek-v4.1-flash-mini": "qwen3.7-flash",
"deepseek-v4.1-flash": "kimi-k3",
"o1": "glm-5.3-flash",
}
model = MODEL_MAP.get(requested_model, "kimi-k3")
The ID list is defined by the live model catalog; newly listed and delisted models both show up there.
Five things to re-test after migration
Once base_url changes, five behaviors deserve a fresh pass β each one can expose a protocol-level difference:
- Basic request: short prompt, small max_tokens, confirm a 200 and readable text.
- Streaming: SSE framing with
stream=True, thedata: [DONE]terminator, and disconnect behavior. - max_tokens limits: default output caps differ per model; confirm long-output tasks are not silently truncated.
- Error handling: confirm your except branches catch the new endpoint's error structures (401/404/429 response bodies).
- Concurrency and rate limits: load-test at your real concurrency level and confirm the limits cover your peak.
"Passing" does not mean "it runs". It means the error paths are verified too: deliberately use a wrong key to exercise the 401 path, a wrong model ID to read the error message, and max_tokens=1 to observe truncation behavior.
One difference to know: reasoning fields
Some models expose a thinking process in a separate field. When the model you map to supports it, the thinking arrives in message.reasoning_content while the final answer stays in message.content β but not every model populates that field, so treat it as optional and check the model card before relying on it:
msg = resp.choices[0].message
print(msg.content) # Final answer
# print(msg.reasoning_content) # Thinking process, reasoning models only
If your UI only reads content, nothing changes; but when you meter token usage, count the reasoning portion too β it can be substantial.
How to roll back
Rollback is a configuration change: point base_url and the key back at their original values. The model mapping table can stay; it does nothing until referenced. To keep rollback fast:
- Drive base_url and the key from environment variables, never hard-coded
- Keep the original OpenAI config as a commented block or a spare env file
- After rolling back, run one basic request to verify
This also means the migration can be canaried: move one non-critical endpoint to EXAIV first, watch error rate and latency for a week or two, then move the core paths.
Common errors, quick reference
| Error | Cause | Fix |
|---|---|---|
| 404 | Full path pasted into base_url, or a typo in the domain | Confirm it is exactly https://exaiv.com/api/ai/v1; the SDK appends the rest |
| 401 | Wrong key, missing Bearer prefix, or revoked key | Regenerate the key; confirm the header is Authorization: Bearer <key> |
| model not found | OpenAI name passed through, or a typo | Copy the real ID from the catalog |
| 400 | Request contains fields the endpoint does not support | Strip optional fields; rebuild from a minimal request |
| 429 | Rate limit hit | Reduce concurrency and back off per the 429 response |
Streaming: one behavior worth a dedicated test
Streaming is where migrations most often surprise teams, because it works differently at the transport layer even when the protocol matches. After switching base_url, run a dedicated streaming check before declaring the migration done: enable stream=True, print each chunk as it arrives, and confirm you see incremental deltas, an empty-delta case handled gracefully, and the data: [DONE] terminator at the end. Then kill the connection mid-stream once and observe what your code does β a retry loop that re-sends the whole conversation is a common and expensive default.
If your application aggregates chunks into a full answer, confirm the aggregation still produces identical text to a non-streaming call of the same prompt; small differences here usually mean your client is double-counting or dropping a partial delta. None of these checks take more than a few minutes, and each one has caught a real production bug somewhere.