cli-one-liner-model-switch · EN · 2026-10-24

Switching Models Mid-Script With curl on One Aggregator Key: A Copy-Paste Cheatsheet

A shell-focused cheatsheet for calling multiple LLM backends through one aggregator API key with curl, showing how to swap the model field, adjust max_tokens, and toggle tools inside a plain bash script without an SDK.

Why curl and one key

Most scripting and CI environments already have curl and bash. If every model vendor needed its own SDK, auth flow, and endpoint, a build script would fill up with dependencies. With a single aggregator key you keep one base URL and one Authorization header, and change only the request body when you want a different backend.

This cheatsheet stays conceptual: it shows the shape of each request and the fields you flip, not a fixed price list or model catalog. Check the aggregator docs for current model names and context limits.

The one environment variable to export

Keep the key out of your scripts and in the environment:

export AGG_KEY="your-key-here"
export AGG_URL="https://your-aggregator.example/v1/chat/completions"

In CI, set AGG_KEY as a masked secret rather than committing it. Every recipe below reads from these two variables.

A minimal request body

A chat request is a JSON object with a model field and a messages array. The model string is the only thing you change to switch backends.

curl -sS "$AGG_URL" \
  -H "Authorization: Bearer $AGG_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "MODEL_NAME",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

Because the endpoint and auth are constant, you can wrap this in a function and pass the model name as an argument.

A reusable shell function

Define it once at the top of your script:

call_model() {
  local model="$1"
  local prompt="$2"
  curl -sS "$AGG_URL" \
    -H "Authorization: Bearer $AGG_KEY" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg m "$model" --arg p "$prompt" \
      '{model: $m, messages: [{role: "user", content: $p}]}')"
}

Using jq -n to build the body avoids quoting bugs when your prompt contains quotes, newlines, or backslashes. If jq is not available, build the JSON with a heredoc and be careful with escaping.

Switching backends inside a script

With the function above, flipping models is just a variable change:

for m in "MODEL_A" "MODEL_B" "MODEL_C"; do
  echo "== $m =="
  call_model "$m" "Summarize this changelog in one line."
done

This pattern is useful for comparing outputs, running a fallback chain, or routing different steps of a pipeline to different models (for example, a cheaper model for bulk classification and a stronger one for final review).

Controlling output length with max_tokens

max_tokens caps the generated response length. It is a per-request field, so you can set it differently per model in the same script. Add it to the body alongside model:

{
  "model": "MODEL_NAME",
  "max_tokens": 256,
  "messages": [{"role": "user", "content": "Give one sentence."}]
}

Notes on max_tokens:

  • It limits output, not input. Long prompts are bounded by the model's context window, not by this field.
  • Some backends use different defaults, so set it explicitly when you need predictable output sizes.
  • If a response stops mid-sentence, check whether you hit the cap.

Toggling tools

Tool/function calling is also a body field. You pass a tools array describing functions the model may call, and the model decides whether to invoke them. The same array shape works across compatible backends, but not every model supports tools, and behavior varies.

{
  "model": "MODEL_NAME",
  "messages": [{"role": "user", "content": "What is the weather in Paris?"}],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Look up current weather for a city",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"]
        }
      }
    }
  ]
}

Practical tips:

  • Feature-detect: if you loop over several models with tools, some may ignore or reject them. Keep a small per-model allowlist in your script.
  • If the model returns a tool call, your script is responsible for executing the function and sending the result back in a follow-up request.
  • Models that do not support tools generally still work for plain chat; just omit the tools field.

Checking responses safely in bash

Raw curl output is JSON. Parse it instead of grepping:

resp=$(call_model "MODEL_NAME" "Say hi")
text=$(printf '%s' "$resp" | jq -r '.choices[0].message.content // empty')
if [ -z "$text" ]; then
  echo "No content. Full response:" >&2
  printf '%s\n' "$resp" | jq . >&2
  exit 1
fi
printf '%s\n' "$text"

Checking for an empty field catches auth errors, rate limits, and malformed requests early, which matters in CI where a silent failure can pass a job.

Handling errors and retries

  • Use curl -sS so transport errors surface on stderr while the body stays clean on stdout.
  • Add --max-time to prevent a hung request from stalling a pipeline.
  • On HTTP 429 or 5xx, retry with backoff; on 4xx from a bad body, fix the request instead of retrying.
  • Capture the HTTP status with -w '%{http_code}' if you need to branch on it.
status=$(curl -sS -o /tmp/out.json -w '%{http_code}' "$AGG_URL" \
  -H "Authorization: Bearer $AGG_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_NAME","messages":[{"role":"user","content":"ping"}]}')

Billing and key hygiene for scripted use

Two things worth knowing before you wire this into automation:

  • Usage is billed at the official model price times 1.3, settled in USDC on Base with no KYC. Top up before long CI runs so jobs do not fail on an empty balance.
  • If you contribute keys, credit is returned at the official price times 1.1 (premium keys at 1.2) in USDC. Keep contributor keys separate from the key your scripts use so accounting stays clear.

Store AGG_KEY in your secret manager, scope it to the job that needs it, and rotate it if a log ever prints it. The same one-key pattern applies whether you run locally or in a container.

Quick reference

  • Switch model: change the model string; keep URL and headers fixed.
  • Cap output: add max_tokens per request.
  • Enable tools: add a tools array; verify the model supports it.
  • Build JSON safely: use jq -n instead of string concatenation.
  • Fail loudly: parse .choices[0].message.content and check for empty.
  • Retry wisely: backoff on 429/5xx, fix 4xx.