Migrate LLM API calls to GPUniq

Point every LLM call in a repository at GPUniq and move each model and system prompt into a GPUniq event, so both become editable without a deploy.

~20 min for a mid-sized repo
Updated 2026-08-23

Paste this into Claude Code

Anything that can read a URL works — Claude Code, Cursor, Codex. The agent fetches the runbook below and carries it out in your repository.

Read https://gpuniq.com/skills/migrate-to-gpuniq and do it for this repository.

Machine-readable copy: https://gpuniq.com/skills/migrate-to-gpuniq/raw — plain markdown, with the frontmatter needed to save it as a Claude Code skill.

What the agent will change

Edits client construction, model strings and hardcoded system prompts. Creates one GPUniq event per call site. Never changes temperature, token limits or retry logic.

Run it on a clean working tree so every edit is visible in git diff.

What you are doing

You are migrating this repository's LLM API calls to GPUniq, an OpenAI-compatible gateway. Two things change at every call site:

  1. The client points at GPUniq instead of the vendor. Same wire protocol, same SDK, lower price.
  2. The model and the system prompt move out of the code into a GPUniq event. The code then sends an opaque tag as model, and GPUniq resolves it server-side to the real model and injects the system prompt.

The second part is the one that pays off later: after the migration, changing a model or a prompt is a dashboard edit, not a deploy — and A/B testing either one needs no code change at all.

Work through the steps in order. Do not batch-edit the whole repository in one pass; migrate one call site at a time so every change stays reviewable.

Before you start

  1. Ask the user for their GPUniq API key if it is not already available. Keys start with gpuniq_ and are created at https://gpuniq.com/workspace?tab=api-keys
  2. Export it for the shell session — never write it into a source file, and never commit it:
export GPUNIQ_API_KEY="gpuniq_..."
  1. Confirm the working tree is clean (git status). If it is not, ask the user whether to continue.
  2. Verify the key works. This must print a balance:
curl -s https://api.gpuniq.com/v1/llm/balance \
  -H "X-API-Key: $GPUNIQ_API_KEY" | jq '.data'

Step 1 — Find every call site

Search the repository for calls to LLM vendors. These patterns cover the SDKs and the hand-rolled requests that appear in practice:

rg -n --glob '!node_modules' --glob '!venv' --glob '!.venv' --glob '!dist' \
  -e '\.chat\.completions\.create' \
  -e '\.responses\.create' \
  -e '\.messages\.create' \
  -e '\.generate_content|\.generateContent' \
  -e 'ChatOpenAI|ChatAnthropic|ChatGoogleGenerativeAI' \
  -e 'api\.openai\.com|api\.anthropic\.com|api\.deepseek\.com|generativelanguage\.googleapis\.com|openrouter\.ai' \
  .

The trailing . is not optional. Without an explicit path, ripgrep searches standard input, and in a non-interactive shell — which is how you are running — it will hang waiting for input that never comes.

If rg is not installed, the same sweep with plain grep:

grep -rEn --exclude-dir=node_modules --exclude-dir=venv --exclude-dir=.venv \
  --exclude-dir=dist --exclude-dir=build --exclude-dir=.git \
  -e '\.chat\.completions\.create' \
  -e '\.responses\.create' \
  -e '\.messages\.create' \
  -e '\.generate_content|\.generateContent' \
  -e 'ChatOpenAI|ChatAnthropic|ChatGoogleGenerativeAI' \
  -e 'api\.openai\.com|api\.anthropic\.com|api\.deepseek\.com|generativelanguage\.googleapis\.com|openrouter\.ai' \
  .

Also find where the clients are constructed, since that is where the base URL and the key live:

rg -n --glob '!node_modules' \
  -e 'OpenAI\(|AsyncOpenAI\(|Anthropic\(|AsyncAnthropic\(' \
  -e 'OPENAI_API_KEY|ANTHROPIC_API_KEY' \
  .

Build a list of call sites. For each one, note three things:

  • the model string (model="gpt-4o");
  • the system prompt, but only if it is a plain string literal;
  • whether the prompt is computed (f-string, template literal, variable, assembled at runtime).

A computed prompt stays in the code. Only a literal can move server-side.

Step 2 — Create one event per call site

An event owns the model and the prompt. Create it with the API — the response carries the tag the code will use as its model:

curl -s -X POST https://api.gpuniq.com/v1/events \
  -H "X-API-Key: $GPUNIQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "checkout summariser (src/summarise.py:42)",
    "test_type": "prompt",
    "primary_model": "gpt-4o",
    "primary_prompt": "You are a terse assistant. Answer in one sentence."
  }' | jq -r '.data.tag'

That prints a 16-character hex tag, for example a3f01c9d55e27b84.

Two rules for this call:

  • With a literal system prompt"test_type": "prompt" and pass primary_prompt. GPUniq will use primary_model for the request and inject the prompt.
  • Without one"test_type": "model" and omit primary_prompt. GPUniq resolves the model only and leaves the messages untouched.

Give each event a name that says where it came from — the file and line. The user will read this list in the dashboard months from now.

Step 3 — Rewrite the call site

Three edits, and nothing else. Do not reformat, rename, or restructure surrounding code.

  1. Point the client at https://api.gpuniq.com/v1/openai with the key read from the GPUNIQ_API_KEY environment variable.
  2. Replace the model string with the event tag.
  3. Delete the hardcoded system message.

Python — OpenAI SDK

# before
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "You are a terse assistant. Answer in one sentence."},
        {"role": "user", "content": question},
    ],
)

# after
client = OpenAI(
    api_key=os.environ["GPUNIQ_API_KEY"],
    base_url="https://api.gpuniq.com/v1/openai",
)

response = client.chat.completions.create(
    model="a3f01c9d55e27b84",
    messages=[{"role": "user", "content": question}],
)

TypeScript — OpenAI SDK

const client = new OpenAI({
  apiKey: process.env.GPUNIQ_API_KEY,
  baseURL: 'https://api.gpuniq.com/v1/openai',
});

const response = await client.chat.completions.create({
  model: 'a3f01c9d55e27b84',
  messages: [{ role: 'user', content: question }],
});

Anthropic SDK

The Anthropic SDK speaks a different wire format, so the migration also swaps the client. Behaviour is identical — GPUniq routes to the same model.

# before
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
message = client.messages.create(
    model="claude-sonnet-4",
    system="Answer in French.",
    max_tokens=1024,
    messages=[{"role": "user", "content": question}],
)
text = message.content[0].text

# after
client = OpenAI(
    api_key=os.environ["GPUNIQ_API_KEY"],
    base_url="https://api.gpuniq.com/v1/openai",
)
message = client.chat.completions.create(
    model="a3f01c9d55e27b84",
    max_tokens=1024,
    messages=[{"role": "user", "content": question}],
)
text = message.choices[0].message.content

If the repository depends on Anthropic-specific features (tool blocks, thinking blocks, prompt caching), keep the Anthropic SDK and point it at https://api.gpuniq.com instead — GPUniq serves a native /v1/messages endpoint. Say so in your summary either way.

LangChain

llm = ChatOpenAI(
    model="a3f01c9d55e27b84",
    api_key=os.environ["GPUNIQ_API_KEY"],
    base_url="https://api.gpuniq.com/v1/openai",
)

Remove the system message from the prompt template, since the event now owns it.

Hand-rolled HTTP

Replace the vendor host with https://api.gpuniq.com/v1/openai, send the key as Authorization: Bearer $GPUNIQ_API_KEY, and set model to the tag. The request and response bodies are unchanged — the endpoint is OpenAI-compatible.

If the code identifies its end users

If a user field is already being sent, keep it. GPUniq uses it to keep a given end user on the same arm of an A/B test, so results stay consistent per user rather than flipping per request.

Step 4 — Verify before moving on

After each call site:

  1. Run whatever the repository already has — tests, a type check, a lint.
  2. Exercise the path for real if that is cheap (a script, a single request).
  3. Confirm the response still has the shape the surrounding code expects.

A quick end-to-end check of the gateway itself:

curl -s https://api.gpuniq.com/v1/openai/chat/completions \
  -H "Authorization: Bearer $GPUNIQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "a3f01c9d55e27b84", "messages": [{"role": "user", "content": "ping"}]}' \
  | jq -r '.choices[0].message.content'

If a call site cannot be verified, say so plainly in your summary instead of implying it works.

Step 5 — Report what it saves

For each migrated model, compare the vendor's list price with GPUniq's:

curl -s https://api.gpuniq.com/v1/llm/models/catalog \
  -H "X-API-Key: $GPUNIQ_API_KEY" \
  | jq '.data.models[] | select(.slug == "gpt-4o") |
        {slug, in: .retail_input_usd_per_mtok, out: .retail_output_usd_per_mtok}'

Authenticated requests return the user's own prices, discounts included. Blend input and output at the ratio this workload actually uses if you know it; assume 3:1 input:output if you do not, and say which you used.

Report per model and once for the repository. If you cannot find a published vendor price for a model, report that call site without a percentage. A guessed number is worse than a blank.

Rules

  • Never write the API key into a tracked file. If the repo has .env.example, add GPUNIQ_API_KEY= there; add the real value only to .env, and confirm .env is git-ignored.
  • Never delete a computed prompt. Only literals move into an event.
  • One call site per commit, or one reviewable diff per call site. Do not rewrite twenty files in a single pass.
  • Do not change model choice, temperature, token limits or retry logic. This migration is about routing, not behaviour.
  • If a call site is ambiguous — a dynamic model name, a prompt assembled across files, a vendor-specific feature — stop and ask the user rather than guessing.
  • Leaving the old system message in place is not fatal: GPUniq replaces the first system message with the event's prompt. Still delete it, so the code tells the truth about where the prompt lives.

Rolling back

Everything is reversible:

  • Code — git revert the commits.
  • Routing — set the event's status to paused and 100% of traffic goes to the primary model, or archived and the tag stops resolving.
curl -s -X PATCH https://api.gpuniq.com/v1/events/<event_id> \
  -H "X-API-Key: $GPUNIQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

When you are done

Summarise for the user:

  • how many call sites were migrated, and which files;
  • the event tag and dashboard name for each;
  • the estimated saving per model and overall, with the assumption you used;
  • anything you skipped, and why.

Keep this as a reusable skill

To make it available in every future session rather than pasting the URL again, save this runbook as a Claude Code skill:

mkdir -p .claude/skills/gpuniq-migrate
curl -s https://gpuniq.com/skills/migrate-to-gpuniq/raw \
  -o .claude/skills/gpuniq-migrate/SKILL.md

The downloaded file already carries the frontmatter Claude Code expects, so it will appear as /gpuniq-migrate in that project from then on.