Software Engineering Wiki

Practice

LLM APIs

Calling the Anthropic and OpenAI APIs, streaming and tool use, retries and rate limits, and keeping cost and secrets under control.

Cheatsheet #

TaskSnippet
Anthropic keyexport ANTHROPIC_API_KEY=...
OpenAI keyexport OPENAI_API_KEY=...
Anthropic SDK callclient.messages.create(model=..., max_tokens=1024, messages=[...])
System prompt (Anthropic)top-level system= parameter, not a message
Streamwith client.messages.stream(...) as s: for t in s.text_stream:
Force JSONtool use with a schema, or a prefilled assistant turn
Count tokens before sendingclient.messages.count_tokens(...)
Retryexponential backoff on 429 and 5xx; SDKs retry by default
Rate limit headersanthropic-ratelimit-*, x-ratelimit-*
Cost driverinput tokens × rate + output tokens × rate
Cheap repeated contextprompt caching (cache_control)
Long batch workthe Batch API, at roughly half price

Request shape #

Both APIs take a list of alternating user and assistant messages plus generation parameters. The model is stateless: every request must carry whatever history matters, and that history is what you pay for.

import anthropic

client = anthropic.Anthropic()          # reads ANTHROPIC_API_KEY

resp = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    system="You are a precise assistant. Answer with commands, not prose.",
    messages=[
        {"role": "user", "content": "How do I list pods that are not running?"},
    ],
    temperature=0,
)
print(resp.content[0].text)
print(resp.usage.input_tokens, resp.usage.output_tokens)
from openai import OpenAI

client = OpenAI()                       # reads OPENAI_API_KEY
resp = client.responses.create(
    model="gpt-4.1",
    input="How do I list pods that are not running?",
)
print(resp.output_text)
ParameterEffect
max_tokensHard cap on output; a truncated answer means this was too low
temperature0 for extraction and classification, higher for drafting
stop_sequencesEnd generation at a marker
systemRole and constraints — Anthropic takes it as a parameter, not a message
streamToken-by-token output; required for anything user-facing

Check stop_reason: max_tokens means the response was cut off, end_turn means it finished, tool_use means it wants a tool result back.

Streaming #

with client.messages.stream(
    model="claude-sonnet-5", max_tokens=1024,
    messages=[{"role": "user", "content": prompt}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final = stream.get_final_message()

Streaming changes perceived latency far more than any model choice. It also lets a caller abort early, which is the only way to bound cost on a long generation.

Tool use and structured output #

Give the model a schema and it returns arguments that match it — this is the reliable way to get JSON, rather than asking politely in the prompt.

tools = [{
    "name": "record_incident",
    "description": "Record a structured incident summary.",
    "input_schema": {
        "type": "object",
        "properties": {
            "severity": {"type": "string", "enum": ["sev1", "sev2", "sev3"]},
            "service": {"type": "string"},
            "summary": {"type": "string"},
        },
        "required": ["severity", "service", "summary"],
    },
}]

resp = client.messages.create(
    model="claude-sonnet-5", max_tokens=1024, tools=tools,
    tool_choice={"type": "tool", "name": "record_incident"},
    messages=[{"role": "user", "content": transcript}],
)
block = next(b for b in resp.content if b.type == "tool_use")
incident = block.input          # already parsed and schema-checked

For a real tool loop, execute the call and send the result back as a tool_result content block in a new user message, then continue until stop_reason is end_turn. Validate every tool input server-side: a schema constrains shape, not intent.

Prompt patterns that earn their place #

PatternUse
Explicit output contractState the exact format; show one example of it
Few-shot examplesClassification and extraction, where the boundary is fuzzy
Delimiters around data<document>…</document> so instructions and data cannot be confused
Prefilled assistant turnStart the reply with { to force JSON, or with a heading to fix structure
Chain of thoughtHard reasoning; ask for the answer last so it is easy to parse
Refusal pathTell it what to output when the input does not fit

Put static context first and the variable part last: that ordering is what makes prompt caching effective.

Treat model output as untrusted input

Never execute generated commands, SQL or code without review or a sandbox, and never interpolate model output into a shell. Content fetched from the web or from a document can carry instructions aimed at your system — that is prompt injection, and the defence is not trusting the output, not a better prompt.

Cost #

Cost is input tokens plus output tokens, each at its own rate, and output is several times more expensive. Roughly four characters per token for English text.

count = client.messages.count_tokens(
    model="claude-sonnet-5",
    messages=[{"role": "user", "content": prompt}],
)
print(count.input_tokens)
LeverEffect
Smaller model for easy workOften an order of magnitude cheaper
Prompt cachingRepeated prefix billed at a fraction after the first call
Batch APIAround half price for work that can wait
Trim historySummarise old turns instead of resending them
Cap max_tokensBounds the expensive half of the bill
Stream and abortStop paying as soon as the answer is sufficient
system=[{
    "type": "text",
    "text": long_static_context,
    "cache_control": {"type": "ephemeral"},   # cached prefix
}]

Errors and rate limits #

StatusMeaningResponse
400Malformed requestFix it; retrying will not help
401Bad keyCheck the environment, not the code
413Payload too largeTrim context
429Rate limitedBack off with jitter; read retry-after
500 / 529Server side or overloadedRetry with backoff
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_random_exponential
import anthropic

@retry(
    retry=retry_if_exception_type((anthropic.RateLimitError, anthropic.APIStatusError)),
    wait=wait_random_exponential(min=1, max=30),
    stop=stop_after_attempt(5),
    reraise=True,
)
def ask(prompt: str) -> str:
    r = client.messages.create(model="claude-sonnet-5", max_tokens=512,
                               messages=[{"role": "user", "content": prompt}])
    return r.content[0].text

Limits apply to requests per minute and tokens per minute independently; a job can be throttled on tokens while nowhere near the request limit. Read the anthropic-ratelimit-tokens-remaining header and pace rather than discovering it at 429.

Keys and safety #

export ANTHROPIC_API_KEY=$(vault kv get -mount=secret -field=key llm/anthropic)

Keys belong in a secret manager, injected at runtime. Never in a repository, an image layer, a client-side bundle or a CI variable that gets echoed. Rotate on a schedule and scope keys per workload so one leak is one revocation.

Never send credentials, customer personal data or production secrets in a prompt unless the data handling agreement covers it. Redact before the call, not after.

Oneliners #

# Smoke test a key
curl -s https://api.anthropic.com/v1/messages -H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' -H 'content-type: application/json' -d '{"model":"claude-sonnet-5","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}' | jq -r '.content[0].text'

# Token usage of a single call
curl -s ... | jq '.usage'

# Stream from the shell
curl -sN https://api.anthropic.com/v1/messages -H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' -H 'content-type: application/json' -d '{"model":"claude-sonnet-5","max_tokens":256,"stream":true,"messages":[{"role":"user","content":"list three checks"}]}' | grep -o '"text":"[^"]*"'

# Check rate limit headroom
curl -sD - -o /dev/null https://api.anthropic.com/v1/messages -H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' -H 'content-type: application/json' -d '{"model":"claude-sonnet-5","max_tokens":1,"messages":[{"role":"user","content":"x"}]}' | grep -i ratelimit

# Estimate tokens in a file, roughly
wc -c < prompt.txt | awk '{printf "~%d tokens\n", $1/4}'

# Redact obvious secrets before sending a log as context
sed -E 's/(AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{20,})/[REDACTED]/g' app.log > safe.log

Last updated 15 September 2026 · Edit this page