---
name: gpuniq-migrate
description: 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.
---

# Migrate LLM API calls to GPUniq

_Source: https://gpuniq.com/skills/migrate-to-gpuniq · updated 2026-08-23_

## 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:

```bash
export GPUNIQ_API_KEY="gpuniq_..."
```

3. Confirm the working tree is clean (`git status`). If it is not, ask the user
   whether to continue.
4. Verify the key works. This must print a balance:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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

```python
# 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

```typescript
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.

```python
# 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

```python
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:

```bash
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:

```bash
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.

```bash
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:

```bash
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.
