AIWave API

Migrate from OpenAI to Chinese Models in One Line

Migrating from the OpenAI API to DeepSeek, GLM, Kimi or Qwen changes one line of your code: the base_url. Because the gateway speaks the OpenAI Chat Completions wire format, your SDK, your request bodies, your streaming parser and your tests all keep working. This guide covers the exact change, the four things that genuinely differ, how to validate the swap before you ship it, and how to roll back in one deploy if you do not like the result.

Get an API key →Compare live pricing →

The change itself

Here is the entire migration in Python. The SDK is the official openai-python package, unchanged:

# before
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
)

# after
from openai import OpenAI
client = OpenAI(
    api_key=os.environ["AIWAVE_API_KEY"],
    base_url="https://aiwave.live/v1",     # ← the migration
)
resp = client.chat.completions.create(
    model="deepseek-v4-pro",               # ← and a model name
    messages=[{"role": "user", "content": "Hello"}],
)

Node.js is the same shape with openai-node:

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

// after
const client = new OpenAI({
  apiKey: process.env.AIWAVE_API_KEY,
  baseURL: "https://aiwave.live/v1",
});

Get a key from the console. Email or GitHub signup, no Chinese phone number, no real-name verification, billing in USD. The endpoint contract is documented in the API reference.

Do it without touching code at all

The official SDKs read OPENAI_BASE_URL and OPENAI_API_KEY from the environment. If your application constructs its client with no arguments, you can migrate by changing deployment configuration alone:

export OPENAI_BASE_URL="https://aiwave.live/v1"
export OPENAI_API_KEY="sk-YOUR_AIWAVE_KEY"

This is the ideal rollout mechanism: the change is a config value, so switching back is a config value too. No deploy, no revert, no code review.

Step 1: Pick a target model

There is no automatic mapping from a GPT model to a Chinese one, because they are not the same product. What you can do is choose a starting point by role, then validate.

Current rates for the models teams most often migrate to:

Model IDInput / 1M tokensOutput / 1M tokens
deepseek-v4-flash$0.206$0.412
deepseek-v4-pro$1.09$2.17
glm-4.7-flashfreefree
glm-5$1.55$4.96
kimi-k2.5$0.66$3.30
qwen3-coder-480b-a35b-instruct$0.12$0.36
ernie-4.0-turbo-8k$0.0012$0.0012

Rates read from the AIWave pricing endpoint on 2026-07-26. Check live pricing before budgeting — providers revise rates.

Step 2: Build an evaluation set before you change anything

This is the step teams skip and then regret. Capture 50–100 real requests from your current traffic along with the responses you are happy with. Without this you have no way to tell whether a migration degraded anything, and you will be arguing from impressions.

import json

# log a sample of production traffic first
with open("eval.jsonl", "a") as f:
    f.write(json.dumps({
        "input": messages,
        "current_output": resp.choices[0].message.content,
        "model": "gpt-4o",
    }) + "\n")

Step 3: Run both models over the same inputs

import json
from openai import OpenAI

old = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
new = OpenAI(api_key=os.environ["AIWAVE_API_KEY"],
             base_url="https://aiwave.live/v1")

cases = [json.loads(l) for l in open("eval.jsonl")]

for c in cases:
    a = old.chat.completions.create(model="gpt-4o",
                                    messages=c["input"], temperature=0)
    b = new.chat.completions.create(model="deepseek-v4-pro",
                                    messages=c["input"], temperature=0)
    print("---")
    print("OLD:", a.choices[0].message.content[:300])
    print("NEW:", b.choices[0].message.content[:300])
    print("tokens:", a.usage.total_tokens, "vs", b.usage.total_tokens)

Read the diffs yourself for the first twenty. You are looking for failure modes, not stylistic differences: wrong facts, ignored instructions, malformed JSON, refusals. Style differences are usually fixable with a prompt tweak; capability gaps are not.

Step 4: Ship behind a flag

import os
from openai import OpenAI

USE_AIWAVE = os.getenv("USE_AIWAVE") == "1"

client = OpenAI(
    api_key=os.environ["AIWAVE_API_KEY" if USE_AIWAVE else "OPENAI_API_KEY"],
    base_url="https://aiwave.live/v1" if USE_AIWAVE else None,
)
MODEL = "deepseek-v4-pro" if USE_AIWAVE else "gpt-4o"

Roll out to a fraction of traffic, watch your quality metrics and error rates for a few days, then increase. Rollback is an environment variable rather than a revert.

The four things that actually differ

1. Model IDs are versioned and change

A hard-coded model string will eventually 404. Read the list at startup and fail loudly rather than discovering it on a user request:

ids = {m.id for m in client.models.list().data}
assert MODEL in ids, f"{MODEL} is no longer served"

2. Optional parameters vary by model

JSON mode, tool calling and vision are upstream capabilities. If a model ignores response_format, that is the provider rather than the gateway. Verify on the model directory, and keep a prompt-level fallback for structured output.

3. Error codes need real handling

You will see 429 and 503. They are transient and deserve exponential backoff with jitter, plus a fallback model. Because the request body is identical across models, that fallback is a one-word change:

import time, random
from openai import APIStatusError

RETRYABLE = {408, 429, 500, 502, 503}

def chat(messages, model="deepseek-v4-pro", fallback="deepseek-v4-flash", attempts=5):
    for i in range(attempts):
        try:
            return client.chat.completions.create(model=model, messages=messages)
        except APIStatusError as e:
            if e.status_code not in RETRYABLE:
                raise
            if i == attempts - 2:
                model = fallback
            time.sleep((2 ** i) + random.random())
    raise RuntimeError("retries exhausted")

Full reference in error codes and rate limits.

4. Prompts may need small adjustments

Prompts tuned against one model family carry assumptions. Usually the fix is being more explicit — stating the output format rather than relying on the model to infer it, and giving one worked example. This is a prompt-engineering afternoon, not a rewrite.

What migration actually saves

Compute it rather than estimating. Pull your monthly input and output token totals from your current provider’s usage dashboard, then apply the rates above: (input tokens ÷ 1,000,000) × input price + (output tokens ÷ 1,000,000) × output price. Compare against your current invoice.

Two things to note honestly. Your token counts may shift slightly because tokenisers differ between model families, so treat the projection as approximate until you have a week of real usage. And the saving is largest on high-volume simple work — if your entire spend is on a small number of genuinely hard requests, the case is weaker. See AI API cost comparison 2026 for the broader picture.

Frameworks migrate the same way

Anything that accepts a custom base URL needs the same one-line change:

Running both providers side by side in production

A flag that flips the whole application is fine for a small service. For anything with mixed workloads, routing per request is better: send the cheap high-volume work to a cheap model immediately and leave the hard requests where they are until you have evidence.

from openai import OpenAI

openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
aiwave_client = OpenAI(api_key=os.environ["AIWAVE_API_KEY"],
                       base_url="https://aiwave.live/v1")

ROUTES = {
    "classify":  (aiwave_client, "deepseek-v4-flash"),   # migrated
    "summarise": (aiwave_client, "deepseek-v4-flash"),   # migrated
    "extract":   (aiwave_client, "deepseek-v4-pro"),     # migrated
    "reason":    (openai_client, "gpt-4o"),              # still evaluating
}

def call(task, messages, **kw):
    client, model = ROUTES[task]
    return client.chat.completions.create(model=model, messages=messages, **kw)

This shape has two advantages over an all-or-nothing switch. You capture the savings on the majority of traffic within a day, and each remaining route becomes a separate, small decision rather than one large risky one. Most teams find the classification and extraction routes migrate with no measurable quality change, which is also where the token volume tends to concentrate.

Shadow traffic: measuring without risking anything

If you would rather not put new output in front of users at all during evaluation, mirror the requests and compare offline. The user still sees the old response; you accumulate a real comparison set.

import asyncio, json, logging
from openai import AsyncOpenAI

primary = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
shadow  = AsyncOpenAI(api_key=os.environ["AIWAVE_API_KEY"],
                      base_url="https://aiwave.live/v1")

async def handle(messages):
    result = await primary.chat.completions.create(
        model="gpt-4o", messages=messages, temperature=0)
    asyncio.create_task(_shadow(messages, result))   # fire and forget
    return result

async def _shadow(messages, primary_result):
    try:
        alt = await shadow.chat.completions.create(
            model="deepseek-v4-pro", messages=messages, temperature=0)
        logging.info(json.dumps({
            "input": messages[-1]["content"][:500],
            "primary": primary_result.choices[0].message.content[:1000],
            "candidate": alt.choices[0].message.content[:1000],
            "primary_tokens": primary_result.usage.total_tokens,
            "candidate_tokens": alt.usage.total_tokens,
        }))
    except Exception as e:                 # shadow failures must never affect users
        logging.warning("shadow failed: %s", e)

Two rules make this safe. The shadow call must be fire-and-forget so it cannot add latency, and its failures must be swallowed entirely — a shadow that can break the request path defeats the purpose. Run it over a sampled percentage of traffic rather than everything; a few thousand comparisons is plenty.

Scoring the comparison without reading everything

Reading a few hundred pairs by hand is valuable and does not scale. A model-as-judge pass narrows it to the disagreements worth your attention.

import json, re, collections

judge = OpenAI(api_key=os.environ["AIWAVE_API_KEY"],
               base_url="https://aiwave.live/v1")

def score(reference, candidate):
    r = judge.chat.completions.create(
        model="deepseek-v4-pro",
        temperature=0,
        messages=[{"role": "user", "content":
            f"Reference answer:\n{reference}\n\nCandidate answer:\n{candidate}\n\n"
            "Does the candidate convey the same substantive information? "
            "Reply with a single integer 1-5. Ignore differences in style or length."}],
    )
    m = re.search(r"[1-5]", r.choices[0].message.content)
    return int(m.group()) if m else 0

buckets = collections.Counter()
for row in map(json.loads, open("shadow.jsonl")):
    s = score(row["primary"], row["candidate"])
    buckets[s] += 1
    if s <= 2:
        print("REVIEW:", row["input"][:200])   # only these need a human

Be honest about the limits of this. A judge model is biased toward its own phrasing and cannot verify facts it does not know. Its actual value is triage: it surfaces the cases scoring 1–2 so you read those instead of everything. Treat the score distribution as a signal, not a certificate.

Handling the tokeniser difference

Different model families tokenise differently, so the same text does not produce the same token count everywhere. This matters in three places, and each has a straightforward fix.

resp = client.chat.completions.create(model=MODEL, messages=messages)
logging.info("tokens in=%d out=%d total=%d",
             resp.usage.prompt_tokens,
             resp.usage.completion_tokens,
             resp.usage.total_tokens)

Logging usage from day one turns cost from a monthly surprise into a metric you can alert on. Streaming responses do not include it per chunk, so if you stream, measure with a sampled non-streaming call.

Adjusting prompts that were tuned for another model

Most prompt portability problems come from implicit expectations. Three adjustments resolve the majority:

State the output format explicitly. If the prompt says “list the issues” and your parser expects JSON, you were relying on the previous model’s habits. Say “Return a JSON array of objects with keys issue and severity. Return nothing else.”

Add one worked example. A single input-output pair is worth several paragraphs of instruction and works across model families.

Move constraints to the system message. Rules buried mid-prompt get less weight than rules stated up front. Put the non-negotiables in the system role.

messages = [
    {"role": "system", "content":
     "You extract structured data. Always return valid JSON matching the schema. "
     "Never include commentary, markdown fences, or explanation."},
    {"role": "user", "content":
     'Schema: {"name": string, "amount": number, "currency": string}\n\n'
     'Example input: "Invoice 4471 for 250 EUR from Acme"\n'
     'Example output: {"name": "Acme", "amount": 250, "currency": "EUR"}\n\n'
     f'Input: {text}'},
]

A migration timeline that works

  1. Day 1. Create a key, change base_url in a scratch script, confirm a request succeeds. This takes minutes and removes all abstract uncertainty.
  2. Day 2–3. Capture 50–100 real requests into an evaluation file.
  3. Day 4. Run both providers over the set. Read the first twenty diffs yourself.
  4. Week 2. Migrate the cheapest, highest-volume route behind a flag. Watch error rates and quality metrics.
  5. Week 3. Expand route by route. Keep the flag.
  6. Week 4. Compare the invoice against your projection and decide about the remaining routes.

The pattern to avoid is migrating everything at once on a Friday. Not because the change is difficult — it is one line — but because without an evaluation set you have no way to tell a real regression from a normal week.

What to keep from your existing setup

Migration is not a rewrite, and treating it as one creates risk that the change itself does not carry. Keep your retry logic, your logging, your prompt templates in version control, your evaluation harness and your caching layer. All of it is provider-agnostic. The only genuinely provider-specific pieces are the base URL, the key and the model string — which is precisely why this is a one-line change and not a project.

Frequently asked questions

Do I need to rewrite my code to switch from OpenAI?

No. Change base_url and the model name. The official OpenAI SDKs work unchanged, and if your client reads OPENAI_BASE_URL from the environment you can migrate with configuration alone.

Will my prompts still work?

Mostly. Expect small adjustments where a prompt relied on implicit behaviour. Being explicit about output format resolves most differences.

What about function calling and JSON mode?

Supported by models that implement them upstream. Check the model directory before depending on it, and keep a prompt-level fallback for structured output.

How do I roll back?

Keep the previous configuration behind an environment variable. Rollback is then a config change rather than a deploy.

Do I need a Chinese phone number?

No. Email or GitHub signup, billing in USD. See accessing Chinese AI models without a Chinese phone number.

Get an API key →Compare live pricing →

Sources

Every claim above traces back to one of these. Pricing figures come from the AIWave pricing endpoint on 2026-07-26; capability claims come from the vendors’ own documentation.