# LLM Provider Diversification Playbook **A runnable checklist for taking a single-vendor LLM pipeline to three or more providers without doubling your bill.** Version 1.1 (2026-08-05). All prices and URLs verified against official vendor documentation on **2026-08-05**. Prices move. Re-verify anything you are about to build a budget on, and see the staleness note at the bottom. *Changes since 1.0: corrected the payment-rails section (Kimi does accept US credit cards despite its docs listing only WeChat Pay and Alipay; Alibaba is the stricter one in practice); added the four self-healing layers, the bound-the-sum budget guard, the concurrency-exposed singleton race, and the self-auditing artifact pattern.* Written by J.A. Watte. Free to copy, fork, and hand to your team. --- ## 0. Read this before you change anything Three findings should reshape the plan you probably arrived with. **Your most likely cutoff is your own spend cap, not a ban.** Anthropic's rate limit tiers carry hard monthly spend ceilings: Start is **$500/month**, Build is **$1,000/month**, Scale is **$200,000/month**. The documentation is blunt about what happens next: "Once you reach your tier's spend cap, API usage pauses until the next month unless you request a higher limit." You cannot buy your way past it with prepaid credits. If you are a small shop, that $500 wall will stop your pipeline long before any account action does, and it will do it on a schedule you can predict. Check your tier today. **The two obvious diversification moves both cost you money.** The Anthropic Message Batches API gives you 50% off, and it exists only on the first-party API and Claude Platform on AWS. It is **not available on Amazon Bedrock and not available on Google Vertex AI**. It is also not expressible through an OpenAI-shaped `/chat/completions` gateway, because batching is a separate endpoint with its own verb, envelope, polling lifecycle, and out-of-order results. So "proxy everything through one gateway" and "fail over to Bedrock" both quietly convert your batch work to synchronous work at double the unit price. Whatever else you do, keep a native path to the first-party batch endpoint. **More vendors is not the same as more insurance.** Eight accounts serving eight proprietary models is eight separate migrations waiting to happen. What actually protects you is a small set of **open-weight models that several providers serve at comparable prices**, so failover is a base URL and a model string rather than a re-engineering project. Section 4 names them. You are not diversifying vendors so much as diversifying vendors across a fixed set of portable weights. --- ## 1. Preflight: measure before you move Do not start by opening accounts. Start by finding out what you actually spend and where. Every cost decision downstream depends on numbers you probably do not have yet, and roughly half the savings in any first pass comes from waste rather than from pricing. ### 1.1 Inventory every call site ```bash # Every place your code talks to a model, with line numbers. grep -rnE "anthropic|api\.moonshot|dashscope|openai|deepseek|generativelanguage|api\.z\.ai|groq|together|fireworks" \ --include="*.mjs" --include="*.js" --include="*.ts" --include="*.py" --include="*.go" . \ | grep -vE "node_modules|\.min\.|package-lock" > callsites.txt wc -l callsites.txt ``` For each hit, write down four things: the model string, the `max_tokens`, what triggers it, and how often that trigger fires. The last column is the one that finds money. ### 1.2 Find the duplicate pipeline This is the single most common leak and it survives migrations. A project moves to the batch API but never disables the old synchronous path, and both keep firing on cron. Both write the same date-keyed record, so the second one silently overwrites the first, and you paid for both. ```bash # Cron schedules and the code they fire, side by side. grep -rnE "schedule|cron" --include="*.yml" --include="*.yaml" --include="*.toml" . | grep -v node_modules ``` Look for two things specifically. Any endpoint that fires more than once a day and writes to the same key each time: every fire after the first is a paid no-op. And any synchronous fallback that was built as a manual emergency path but still has a schedule attached. ### 1.3 Pull your real batch usage You do not need an admin key for this. A normal workspace key can enumerate batches and read per-request token usage: ```bash curl -s 'https://api.anthropic.com/v1/messages/batches?limit=100' \ -H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' curl -s "https://api.anthropic.com/v1/messages/batches/$BATCH_ID/results" \ -H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' ``` The results are JSONL. Each line carries `result.message.usage` with `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, and the model. Multiply by batch rates and you have a bottom-up figure you can trust. **Read the usage object carefully.** On Anthropic, `input_tokens` is the **uncached remainder only**. Total prompt size is `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`. A cost dashboard that maps `input_tokens` to "prompt tokens" will report a well-cached agent that ran for hours as having sent four thousand tokens. On the OpenAI wire the same numbers are `prompt_tokens` and `completion_tokens`, with cached tokens nested under `prompt_tokens_details.cached_tokens`. Your accounting has to handle both spellings for the same underlying quantity. ### 1.4 Find the spend your own dashboard cannot see Any synchronous call outside your batch trail is invisible to a homegrown billing summary but very much present on the vendor invoice. In one pipeline I audited, a twice-daily "light refresh" running on the full-price synchronous path was the largest single avoidable line, and it did not appear in the project's own cost endpoint at all. Reconcile bottom-up token math against the vendor console total, and treat the gap as a to-do list rather than rounding error. ### 1.5 Verify caching is actually happening ``` assert usage.cache_read_input_tokens > 0 ``` Across repeated requests with an identical prefix, that number should be non-zero. If it is always zero you are paying the cache write premium for nothing. Grep your prompt assembly for the silent invalidators: `datetime.now()` or `Date.now()` interpolated into a system prompt, a UUID early in the content, `json.dumps` without `sort_keys=True`, iteration over a set, a session or user ID f-strung into the system prompt, conditional system sections, or `tools=build_tools(user)`. Caching is a **prefix byte match**. Render order is tools, then system, then messages. Any byte change anywhere in the prefix invalidates everything after it, which means tools sitting at position zero are the most destructive thing to touch. --- ## 2. Fund the accounts Open and fund accounts **before** you need them. A cold account is not a fallback; it is a form to fill out during an outage. Every provider below has a first-payment delay of some kind, whether that is card verification, a tier threshold, or a payment rail you do not have yet. ### 2.1 The top-up URLs | Provider | Where you load credit | Model | Notes | |---|---|---|---| | **Anthropic** | `https://platform.claude.com/settings/billing` | Prepaid credits plus auto-reload; invoicing for enterprise | `console.anthropic.com` now 301-redirects here. Auto-reload takes a minimum-balance trigger and a reload-to amount. Also set a spend limit while you are here, and check your tier's monthly cap. | | **Moonshot / Kimi** | `https://platform.kimi.ai/console/pay` | Prepaid balance, no subscription | **Read 2.2 before you plan on this one.** $1 minimum to activate, $10 is the number that matters. | | **Alibaba Qwen** | `https://billing-cost.console.alibabacloud.com/fortune/billing-account` | Cash balance plus postpaid settlement | Then: Asset Information, Cash Balance, "Top-up & Remittance". Sign up at `https://account.alibabacloud.com/register/intl_register.htm`. | | **DeepSeek** | `https://platform.deepseek.com/top_up` | Prepaid | | | **Z.ai (GLM)** | `https://z.ai/manage-apikey/billing` | Prepaid recharge | USD-native international product. Not the CNY `bigmodel.cn` product. | | **OpenRouter** | Credits page in the console, reached from `https://openrouter.ai/workspaces/default/keys` | Prepaid credits | 5.5% fee on card top-ups with a $0.80 minimum, so a $5 top-up effectively pays over 16%. Top up in larger increments. | | **Groq** | `https://console.groq.com/settings/billing/manage`, spend limits at `https://console.groq.com/settings/billing/limits` | Postpaid with spend limits | `/settings/billing` 307-redirects; use the direct paths. | | **Cerebras** | Console billing | Prepaid | Your **first** purchase moves you to the Developer tier. No dollar threshold is documented, so ignore any figure you read elsewhere. | | **Mistral** | `https://console.mistral.ai` billing | "Free mode" and "Pay-as-you-go" | Those are the current tier names. Anything calling it "Experiment" is out of date. Org-level monthly spending limit suspends the API when hit. | **Verify each of these yourself the day you use it.** Several vendor consoles are single-page apps that return HTTP 200 for any path, including paths that do not exist, so a link that loads is not proof a page is real. Two URLs printed inside Moonshot's own documentation were dead when checked. ### 2.2 Payment rails: where the docs and reality diverge **Kimi accepts US credit cards in practice, even though its documentation does not say so.** Moonshot's international billing page lists the rails verbatim as "Online top-ups support WeChat Pay and Alipay QR code payments," names **Beijing Moonshot AI Technology Co., Ltd.** as the invoicing entity, and quotes a "tax-inclusive rate is 6%" mainland VAT. Read cold, that page says a US developer cannot fund the account. Cards do work. The page is incomplete, which is consistent with the international billing doc being a partial translation of the mainland one. **Rule that generalizes: treat billing documentation as a lower bound on what is accepted, and confirm at the checkout screen.** Recharges are widely reported non-refundable with no published refund policy, so keep a small balance and treat a first payment as unrecoverable. **Alibaba is where a card is more likely to be declined, despite listing more of them.** Documented support is broad: Visa, Mastercard, American Express, UnionPay, JCB, Discover, Diners Club, plus Alipay, PayPal, Apple Pay, Google Pay. The restrictions are what bite: - The card must support **international transactions AND 3D Secure**. - **Rejected outright**: mainland-China-issued cards, prepaid cards, gift cards, virtual cards, and PayPal accounts registered in mainland China. - **One card binds to exactly one Alibaba Cloud account.** In practice that funnels most people onto a plain Visa or Mastercard, and the virtual or privacy card many developers reach for first will be refused. Alibaba is also not a prepaid API wallet: it is a cloud account with a payment method that settles pay-as-you-go against a threshold (about USD 1,000 for bank cards, USD 8 to 1,000 for digital wallets depending on the account, plus a monthly sweep below that). No minimum top-up is published. So "what is my credit balance" is the wrong mental model there. Two more things worth knowing. The international service is `api.moonshot.ai` with an account on `platform.kimi.ai`; the mainland service is `api.moonshot.cn` with a separate account, separate key, and separate balance. Older tutorials and a lot of GitHub examples hardcode the `.cn` host, and copying one produces an auth failure that looks exactly like a bad key. Pin `base_url` explicitly. And the contracting entity for the international platform is **Moonshot AI PTE. LTD.**, a Singapore company, with Singapore governing law and disputes going to SIAC arbitration in Singapore. There is no US entity, no US venue, and no small-claims path. ### 2.3 Two funding traps that will page you at 3am **Kimi returns HTTP 429 for an empty wallet, not 402.** When `available_balance` drops to zero or below, requests fail with `exceeded_current_quota_error` and a 429 status, message "Account balance is insufficient or the account has been disabled." A standard retry-with-backoff-on-429 policy will spin against an empty wallet forever, burning your latency budget while hiding the actual cause. Branch on `error.type`, never on the status code alone: - `exceeded_current_quota_error` means page a human. Do not retry. - `rate_limit_reached_error` means back off and retry. - `engine_overloaded_error` means back off and retry. Poll `GET /v1/users/me/balance` and alarm well above zero. Note that `cash_balance` can go negative, meaning you owe money, at which point `available_balance` collapses to whatever voucher balance remains. **Kimi rate tiers are gated on cumulative lifetime recharge, not monthly spend, and Tier0 is unusable.** At the $1 minimum you get concurrency 1 and 3 requests per minute. At $10 cumulative you get concurrency 50 and 200 RPM. Fund $10 immediately. Vouchers do not count toward the threshold; only cash does. The Console batch inference UI also requires Tier1 or above, so a $1 account cannot reach it. The same "buy your way up the ladder" shape appears at OpenRouter, but keyed differently: limits there are a function of **lifetime credit purchases**, not current balance. Under $10 lifetime you get 50 requests per day. At $10 or more you get 1,000 per day. Topping up a balance does nothing for your limit; cumulative spend is the lever. ### 2.4 Free quota worth collecting on the way through - **Alibaba Model Studio**: 1,000,000 tokens per eligible model, valid 90 days from activation. Hard restriction, quoted: "Only models in the Singapore region with the service deployment scope set to International are eligible for a free quota." Pick **Singapore**, not US Virginia. The grant covers real-time inference only and excludes batch, fine-tuning, and deployment. Turn on the per-model "Free quota only" switch while evaluating so exhaustion stops the service instead of rolling into paid usage. - **Z.ai**: `glm-4.7-flash`, `glm-4.5-flash`, and `glm-4.6v-flash` are genuinely $0 in and $0 out. Free only on Z.ai, so this is a break-glass tier rather than a portable one. - **Cerebras**: $5 in credits, but they **expire 30 days after being granted**. For a fallback leg that sits idle until a primary fails, expiring credits are close to worthless. Do not count them as insurance. - **Anthropic**: new accounts receive a small amount of free credits to test the API. - **Kimi**: no free tier and no trial credit. A $5 cumulative recharge earns a $5 voucher, and that is the only giveaway. --- ## 3. Route by job, not by vendor The instinct is to pick a cheaper vendor and move everything. The arithmetic almost never supports it. I measured this on a live news pipeline. Moving every workload off Claude to the only Kimi tier that had proven reliable saved about **$0.97 per day**, roughly $29 a month, because the cheaper model still has to do the work. Worse, the tier that the fallback logic actually selected under load was priced at $2 per million input tokens, which is **more than Claude Sonnet costs at batch rates**. The naive version of that migration had negative savings and the wrong sign, while breaking three product features that depended on model diversity. What works instead is matching each job to the cheapest model that can actually hold it, and being honest that some jobs are not movable. ### 3.1 The routing table | Job shape | Route to | Why | |---|---|---| | Bulk classification, tagging, extraction, dedupe | Cheapest adequate open-weight model, batched | High volume, low judgment, trivially verifiable. This is where the money is. | | Summarizing or condensing a large corpus before a smart model reads it | Cheap long-context model | You pay once to shrink the input, then the expensive model reads less. Compounds with volume. | | Final synthesis, published prose, anything a customer reads | Your best model | Quality is the product. Do not optimize here first. | | Grading, scoring, judging | Mid-tier, but never the model being graded | A model grading its own output produces a number with no information in it. | | Anything politically or jurisdictionally sensitive | A model whose jurisdiction you have deliberately chosen | See 3.3. | | Latency-critical interactive work | Fast tier, short context | Cheap models are also usually fast models. | ### 3.2 Make every routing decision a reversible flag This is the highest-value infrastructure change in the whole playbook, and it is not about cost directly. If every model string in your codebase is an inline literal, you cannot test a cheaper model without editing code, and you cannot revert during an incident without a deploy. Both of those facts make you route conservatively forever. Replace literals with per-job environment overrides and a kill switch: ``` MODEL_SUMMARIZE=deepseek-v4-flash MODEL_CLASSIFY=gpt-oss-120b MODEL_SYNTHESIZE=claude-opus-5 MODEL_JUDGE=glm-4.7 PILOT_SUMMARIZE=on # off reverts to the previous model, no deploy PROVIDER_SUMMARIZE=fireworks ``` Resolve those at call time with a null-safe fallback to the known-good model. The test for whether you did this right: can you move one job to a different provider, and back, by changing an environment variable and nothing else? If not, keep going. ### 3.3 Jurisdiction is a routing dimension, not a footnote Several of the best-value models come from PRC-jurisdiction labs. That is fine for a large class of work and disqualifying for another, and the line is not about quality. Two concrete mechanics. First, Chinese models measurably soften coverage of CCP-sovereignty topics, so if any of your output touches Taiwan, Xinjiang, Hong Kong, Tibet, or the South China Sea, routing it to one of those models is an editorial decision disguised as a cost decision. If you need that guard, implement it as a regex gate that forces the sensitive slice to a different provider **before** the cheap model is consulted, and write the regex in both Latin and native script. A Latin-only pattern is blind to a story that names its topic only in Chinese, and normalizing to strip diacritics stops `Taiwán` and `Taïwan` from slipping through. Second, Moonshot's terms make training on your content **opt-out via a negotiated contract**, not opt-in. The only carve-out is bespoke: customers requiring restrictions "may contact Moonshot AI to discuss available enterprise arrangements or separate written agreements." There is no published SOC 2, GDPR DPA, HIPAA BAA, or zero-retention option on the docs site. Do not send client PII, credentials, proprietary source, or anything under an NDA through the default self-serve tier. A China-origin model under Singapore law also fails many US enterprise, government, and defense procurement reviews outright regardless of price. --- ## 4. The portability layer: what actually protects you Here is the finding that should drive your architecture. Some open-weight models are served by many independent providers at prices close enough that moving between them is a config change. Those are the models to build on, because no single vendor can cut you off from them. ### 4.1 The three portable families **OpenAI gpt-oss (120B and 20B), Apache 2.0.** The most portable model available right now. Groq, Together, and Fireworks all serve it at an identical **$0.15 in / $0.60 out** per million tokens with a 131,072 token context. Cerebras adds a fourth first-party leg at $0.35/$0.75, which buys roughly 3,000 tokens per second. OpenRouter fronts 19 more upstreams with a floor around $0.03/$0.17. Groq and Fireworks both offer it at a 50% batch tier. If you standardize one model for portability, standardize this one. **DeepSeek V4 Flash, MIT weights.** Exactly **$0.14 / $0.28** at DeepSeek first-party, Together, and Fireworks, with 1M context and a cache-hit rate of $0.0028 that nobody else comes within an order of magnitude of. Two of those three providers are US entities, so this is simultaneously a vendor hedge and a jurisdiction hedge. OpenRouter has 21 endpoints for it, with a floor around $0.084/$0.168, which is 40% **below** DeepSeek's own price. Note that only Fireworks gives DeepSeek a batch discount; Together explicitly excludes DeepSeek from batch. **Z.ai GLM, MIT weights.** GLM-5.2 at exactly **$1.40 / $4.40** on Z.ai, Fireworks, and Together, plus roughly 30 more endpoints via OpenRouter with a floor near $0.28/$0.88. GLM-4.7 at $0.60/$2.20 has eight independent paths. GLM-4.5-Air at $0.20/$1.10 is the strategic sleeper: it is the one GLM on Together's batch-eligible list, so it is the GLM you can run at 50% off on a US provider. GLM-5.2 also exposes an Anthropic-shaped endpoint, making it the cheapest Claude-Code-compatible fallback available. **Context is the trap on all three.** The same model string can carry wildly different context windows per host. GLM-5.2 runs 1,048,576 tokens on Z.ai and Fireworks, 512,000 on Together, 262,144 on several OpenRouter upstreams, and as low as 96,890 on one. Pin your effective context to what the **smallest** leg can serve, or you get silent truncation the first time you fail over. ### 4.2 What is not insurance Be honest about these so you do not build on sand. - **Mistral open weights.** Downloadable, but Mistral is effectively the only serverless host among the major providers. Two paths, one of which is the same infrastructure. Mistral's real value here is EU jurisdiction and its batch-plus-cache discount stack, not portability. Licences also vary across their table, from Apache 2.0 to CC BY-NC, so check per model. - **Anything proprietary and single-sourced.** Groq's compound models, Cerebras preview models, Mistral's Premier line, and Alibaba's `qwen3.7-plus` tier. On that last one: route the open `qwen3.5` checkpoints if you want Qwen as insurance. "Plus" is Alibaba's proprietary tier with no open weights behind it. - **DeepSeek's cache pricing.** At $0.0028 per million cache-hit tokens, no other provider is within 10x. A cache-heavy pipeline is **economically** locked to DeepSeek even though it is technically portable. Design around that if PRC jurisdiction matters to you. - **Free tiers on a single provider.** Z.ai's free flash models are real and useful, and they exist in exactly one place. ### 4.3 Keep a break-glass account funded Put $50 of credit at OpenRouter and leave it there. One key reaches roughly 20 upstreams per model, it charges no per-token markup, and it is the only provider on this list that is itself a fallback layer with model arrays, provider ordering, and floor/nitro routing. Holding $50 costs $2.75 in top-up fees and is the difference between a bad afternoon and a dead pipeline. Set `provider.require_parameters: true` when you do. Without it, a request can land on an upstream that silently ignores parameters it does not support, which is how you get plausible JSON that intermittently fails validation with no error anywhere. --- ## 5. What not to proxy A gateway is the right answer for observability, key management, and cheap failover. It is the wrong answer for two things, and the two things are exactly the two biggest discounts on your bill. **Batch endpoints.** Batching is a separate endpoint with a separate verb, envelope, polling lifecycle, and out-of-order results keyed by `custom_id`. An OpenAI-shaped `/chat/completions` surface has nowhere to express it, so your batchable work runs synchronously at 2x. Anthropic's batch API also rejects `stream: true` outright. **Prompt caching.** This is four incompatible designs sharing one name. Anthropic is opt-in with explicit `cache_control` breakpoints, maximum four per request, 5-minute or 1-hour TTL, writes cost 1.25x or 2x, reads cost 0.1x. OpenAI is automatic above about 1024 tokens with no parameter. Gemini has implicit caching on by default plus a separate explicit API. DeepSeek and Kimi are automatic. The same request object cannot be optimal on all of them. Two caching details that cost people real money: *The minimum cacheable prefix is model-specific and not monotonic.* On Anthropic today it is 512 tokens on Opus 5 and Fable 5, 1024 on Opus 4.8 and Sonnet 5, 2048 on Opus 4.7, and 4096 on Opus 4.6 and Haiku 4.5. A 3,000 token prompt caches on Opus 5 and silently does not cache on Haiku 4.5, with no error at all: `cache_creation_input_tokens` simply comes back zero. So "route the cheap traffic to Haiku" can delete your caching discount and make the cheap model cost more per request. Kimi has a different flavour of the same trap: caching is automatic, but a request can only hit the prefix cache if the **previous** request's prompt exceeded 256 tokens. *Routing is itself a cache invalidator.* Switching models invalidates completely, because caches are model-scoped. Adding, removing, or reordering a single tool invalidates everything, because tools render at position zero. The two things a portability layer exists to do are precisely the two things that destroy the cache. Spawn a subagent on the cheap model rather than switching the main loop's model mid-conversation. **The design rule that follows:** do not model caching and batching as capabilities your abstraction exposes uniformly. Model them as per-provider adapters behind an interface that says "this workload is batchable" and "this prefix is stable," and let each adapter decide how, or whether, to express that. An abstraction that promises uniform caching and batching is either lying or leaving most of the discount on the table. **If you want a gateway anyway**, self-hosted LiteLLM preserves the most. It normalizes `cache_control` across seven providers, translating to Bedrock's `cachePoint` and Google's context-caching API, and it exposes an Anthropic passthrough at `{PROXY}/anthropic/...` that reaches `/v1/messages/batches` natively, so the 50% batch discount survives the hop. Its caveat is the same thing that makes it portable: it strips unsupported parameters silently. Assert on usage fields; do not trust that a parameter landed. --- ## 6. Gating: decide what fails open and what fails closed Every gate in your pipeline makes an implicit choice when it cannot reach the thing it is checking. Make that choice explicit, per gate, and write it down. **Fail closed** when proceeding would publish something wrong. A freshness gate that cannot read the corpus should skip the run, not run on stale input. **Fail open** when proceeding degrades gracefully and blocking would take down something large. One dead upstream should not take down twenty thousand pages. The failure I keep finding is a gate that fails open **and** overwrites its own known-good value. Here is the shape, from a real data pipeline: ```js // Do not do this. const [places, zips] = await Promise.all([ fetchPlaceCentroids().catch(e => { log(e); return {}; }), fetchZipCentroids().catch(e => { log(e); return {}; }), ]); writeFileSync('place-centroids.json', JSON.stringify(places)); // unconditional writeFileSync('zip-centroids.json', JSON.stringify(zips)); // unconditional ``` The soft-fail was added for availability. The write was never made conditional on having data. So one upstream outage replaces a good 1.7 MB artifact with `{}`, the map ships with zero markers, and the process exits 0. **A fallback that writes over the last known good value converts a transient outage into permanent data loss.** In an LLM pipeline this is the cached response overwritten with an empty completion. The fix is one line: only write when you have something. ```js if (Object.keys(places).length) writeFileSync('place-centroids.json', JSON.stringify(places)); else console.warn('places fetch empty, keeping previous artifact'); ``` Three more gating rules worth adopting: **Separate "credential missing" from "use the cheap path."** A pipeline I audited used to downgrade to synthetic numbers automatically when an API key was absent, which "silently published fake prices to production." The fix was to make the degraded mode opt-in by name, via a separate `USE_SAMPLE_DATA` flag that CI never sets. Missing key now nulls only the affected fields. Swap "synthetic numbers" for "a stub completion" and that is an LLM postmortem verbatim. If a missing key can route you to a cheaper model whose output still looks plausible, you have the same bug. **Retry the right status codes.** Retry 429, 408, and 5xx with exponential backoff and full jitter. Never retry other 4xx: a bad key or a malformed request will not fix itself, and retrying burns your budget while hiding the cause. Anthropic also has a **529 overloaded** status that does not exist in the OpenAI world, so a retry classifier keyed on `{429, 500, 502, 503}` gives up on it silently. Use full jitter, not plain exponential backoff, or several simultaneous keepalives will thunder-herd the same blip. **Treat a content filter as permanent, not transient.** Kimi returns HTTP 400 with type `content_filter` and the message "The request was rejected because it was considered high risk." It fires on both input and output, it is model-independent, and the platform will not publish the rules. The output-side trigger is the nasty one: an identical prompt can succeed and then fail nondeterministically, and on a streaming call it can kill a response mid-generation after you have already billed and rendered tokens. Do not retry it in a loop. Fail over to another provider. If your input is large and a content filter rejects it, one retry shape does work: resubmit **the same model with a smaller input slice**. I ship a descending ladder of full, half, quarter, keeping the top of the corpus, because significance is front-loaded and offending records cluster in the tail. Any other failure moves to the next model at the current size; a content filter moves to the next smaller size at the same model. When every size has been filtered, stop. Two details make it work: dedupe the rungs so a small input does not submit the identical payload three times, and if your payload is an object rather than an array, export the list of field names you shrink so a test can prove the serialized bytes actually differ between rungs. A ladder whose rungs are byte-identical is a dead retry that looks like a working one. --- ## 7. Watchdogs: ask the data, not the CI run This is the section that will save you the most grief, and it has nothing to do with which provider you pick. Per-step error isolation and honest job status are in direct conflict. `continue-on-error` suppresses a step's **conclusion** but preserves its **outcome**. So a run where every single step failed reports success, and looks byte-identical to a clean run. That is not hypothetical. On one pipeline, all eight ingest submits failed because the target site was unreachable from the runner for about 18 minutes. Each burned its five retries and exited 1. The run reported **success**. Seven sources self-healed at the next window. The eighth ran only in the failed window, sat 47 hours stale, and put Thursday's prices into a Saturday market brief. You need two independent checks, because they answer different questions. ### 7.1 Reconstruct honest job status from preserved outcomes Give each isolated step an `id`, then add a final gate that reads the outcomes and fails the job if any of them failed: ```yaml - name: Report submit failures if: always() env: OUTCOMES: | bea=${{ steps.bea.outcome }} fred=${{ steps.fred.outcome }} eia=${{ steps.eia.outcome }} run: | FAILED="" while IFS='=' read -r src out; do [ -z "$src" ] && continue [ "$out" = "failure" ] && FAILED="$FAILED $src" done <<< "$(echo "$OUTCOMES" | sed 's/^[[:space:]]*//')" if [ -n "$FAILED" ]; then echo "::error::submit failed for:$FAILED" exit 1 fi ``` Treat `skipped` as fine, not as failure. A source that deliberately runs in only one window will skip in the other, and a single-source manual dispatch skips the rest. **Order this gate after your artifact upload.** Step order is authoritative within a job, so an `exit 1` in the gate can never pre-empt an upload declared above it. That matters more than it sounds: if fetch and submit are separate steps, a submit-only blackout leaves a complete, replayable payload on disk. Failing before the upload converts a recoverable incident into permanent data loss, and some upstreams cannot be re-fetched at all. One source I depend on allows 25 calls a day and returns only the current snapshot, so a re-fetch can never recover an older value. The artifact is the only replay path. ### 7.2 Probe the published artifact, not the pipeline A green run tells you the pipeline executed. It cannot tell you the pipeline produced anything. Add a health endpoint that computes staleness from the **data**, with a per-source freshness budget: ```js // Budget in minutes, set to roughly 1.5x the producing cron's cadence: // one delayed run should not flap, two missed runs should alarm. const INGEST_BUDGETS_MIN = { 'sec-edgar': 360, 'gdelt': 360, 'local-news': 360, // 4h crons 'fed-register': 1080, 'openfec': 1080, // 12h 'bea': 2160, 'bls': 2160, 'fred': 2160, // daily }; ``` For each key, read the record, take its write timestamp, compute age in minutes, and mark it stale when the record is absent, the timestamp is unparseable, **or** the age exceeds the budget. Return 503 when anything is stale. Then have an hourly job read that endpoint and open or close a labeled issue with a per-source table. Three details that make this trustworthy rather than noisy: **Retry the probe and stay silent on total failure.** Three attempts, and if the endpoint is unreachable, emit a warning and exit 0 without filing anything. An unreachable site is indistinguishable from stale ingest, and a watchdog that files an issue during every deploy window trains its owner to ignore its own label. Require a parseable JSON body rather than just a 2xx, so an edge error page counts as a failed probe. **Assert your denominator before trusting your numerator.** This one bit me. If the health endpoint's storage read throws, the ingest fields are absent from the response entirely. A watchdog doing `jq -r '.ingestStaleCount // 0'` reads 0, concludes everything is fine, and **closes the open alarm**. A storage outage resolved the alarm instead of raising one. The `// 0` default is the standard defensive idiom, and here "field missing" and "field is zero" mean opposite things. Check `trackedCount > 0` before trusting `staleCount`, and treat a missing field as its own alarm state. **Self-heal before you escalate.** Three states keyed by hours since the last green run: under four hours, healthy, and close any open issue. At four hours, dispatch a recovery run, but only if nothing is already queued or in progress. At eight hours, also open or comment on an issue. That self-heals the common transient failure with no human involved and escalates only after two failed recovery windows. Put a `concurrency` group on the target that queues rather than cancels, or a watchdog dispatch will kill the run that was already recovering. --- ## 7A. The four self-healing layers, in firing order Each layer is idempotent and a no-op when the layer below already worked. 1. **In-process retry.** Exponential backoff with **full jitter** on 429, 408, 5xx and network errors. Never on other 4xx. Full jitter matters: without it, several simultaneous keepalives thunder-herd the same blip. 2. **Freshness-gated keepalive.** A second, independent scheduler fires the same idempotent endpoint every 30 minutes across a wide window, with a preceding step that skips when today's work is already filed. Ungated, that is a cost leak; gated, it is all-day insurance that costs one read per fire. Two independent schedulers matter because platform cron genuinely drops fires. I have watched a five-hour gap open in one scheduler's own timetable. 3. **Staleness watchdog that reads the published artifact** (section 7.2). Self-heals by dispatching at 4h, pages a human at 8h. 4. **Manual dispatch** taking the same parameters, so recovery is not improvised under pressure. ### 7A.1 Six mechanisms that make those layers real **Clear a wedged queue, do not merely detect it.** My drainer computed that a pending job was past its upstream expiry and did nothing with the fact, so the queue stayed pinned forever. **Computing a health signal and not acting on it is indistinguishable from not having it.** Clear the queue when the upstream job passes its documented expiry. **Give each job kind its own queue key and in-flight window.** Reusing one pending key across two job kinds lets either orphan the other's in-flight work. Separate keys plus a 409 guard make duplicate fires free and keep the jobs from corrupting each other. **Freeze the date at submission, not collection.** Async work that recomputes "today" when results arrive gets a different answer than at submit time, and the difference appears as an off-by-one on exactly the runs that straddle UTC midnight. **Make later re-fires conditional on the actual output.** First run of the day fires unconditionally; later runs read the artifact they would produce and re-fire only if it is missing or a stub. Healthy days cost two reads. Fail open on a read error so a storage blip cannot suppress real work. **Bound the SUM, not just each call.** A per-call timeout does nothing about N sequential calls that each have one. Concrete: moving to a slower model raised my per-call ceiling to 150s, and ten sequential enrichment calls made the pathological case ~25 minutes against a 15-minute platform cap. The per-call timeout was correct; the aggregate was fatal. Add a process-wide wall-clock budget after which remaining items skip the enrichment and take the documented degraded path: ```js const BUDGET_MS = Number(process.env.ENRICH_BUDGET_MS) || 420000; // 7 min let spentMs = 0; async function enrich(item) { if (spentMs >= BUDGET_MS) { console.warn(`[enrich] budget spent (${Math.round(spentMs/1000)}s): skipping, taking the degraded path`); return null; // caller already handles null } const t0 = Date.now(); try { return await callModel(item); } finally { spentMs += Date.now() - t0; } // charge timeouts too: they are the expensive case } ``` Charge it in `finally` so a timeout is counted. It should almost never fire; that is the point. **Cache before you call, cap what you call.** Resolve the cache first, then cap items per run, then enforce the wall-clock budget in **both** the cache loop and the work loop. Cache-first ordering is what converts a recurring per-item cost into a one-time per-item cost. ### 7A.2 Concurrency exposes bugs that were latent for years When you parallelize a loop that touched a lazily-initialized singleton, **guard the singleton**. Mine cached the resolved value: ```js if (!_browser) _browser = await launch(); // RACE: two workers both pass the check ``` Two workers each passed the check, each launched a browser, and cleanup could only reap one. The orphan kept the process alive **137 minutes after a 28-minute job**, so the symptom was "CI step hangs," not "job is slow." Cache the **promise** instead, so concurrent callers await the same in-flight initialization: ```js if (!_browserPromise) _browserPromise = (async () => launch())(); return _browserPromise; ``` Keep an explicit `process.exit(0)` after work completes as a backstop. "Exit when the event loop drains" is a liability when a handle you do not own is still registered. Two more bounds worth copying: a **two-level timeout** (a per-item hard ceiling inside a global soft timeout, where the soft timeout makes workers stop *claiming* new work rather than being killed mid-item, leaving headroom for the validate and upload steps to run on partial data), and an **incremental flush** that creates the output artifact *before* the first fetch and rewrites it on every exit path. I learned the second one when a job ran 75 minutes, the runner killed it, the output directory had never been created, and the entire run was unrecoverable. ## 7B. Make the artifact carry its own audit The best monitoring idea in any of my pipelines has no cron at all. One site recomputes its own quality metrics over the built output at build time and publishes them: record counts, how dates are distributed, how many records lag the newest date, which series contain duplicate dates. Regenerated every build, so it cannot go stale, and it inventories known defects in public instead of hiding them. Port it directly. Per run, publish counts computed **from the output artifact, not from logs**: | Field | Why it earns its place | |---|---| | `records_in` / `records_out` | A silent drop shows up as a gap | | `skipped` with a reason histogram | Turns "some failed" into an actionable list | | `fallback_used` per model id | The only way to see a ladder quietly carrying production | | `parse_failed` | Catches the malformed-JSON class before a consumer does | | `model_served` per record class | Proves routing did what the config claims | | `budget_exhausted` count | Tells you the guard above fired, and how often | Then the health question stops being "did the job exit zero" and becomes "does the artifact describe a run I would accept." A human answers that in five seconds; a script can gate on it. Same discipline at row level: **publish your exclusion counts**. When my leaderboards drop rows for being the wrong vintage or too thin, they render "N dropped for an older month, N for volume" into the page. A pipeline that silently skips records is hiding that same number. ## 8. Find your silent drops Freshness and correctness are different questions, and usually only one of them has a monitor. Here are the five shapes I keep finding, each with the check that catches it. **Green run, zero data.** Covered in section 7. Check: reconstruct job status from step outcomes. **`errors: []` published alongside null metrics.** A fetcher catches its own error into a local variable and returns normally with `article_count: 0` and `median_tone: null`. Because it never throws, the caller's catch never fires, so the payload ships with an empty errors array while most of its content is missing, and the narrative generator still announces full coverage. Check: an errors array is only a health signal if **every** degraded path writes to it. Build your summary text from the metrics you actually have, not from the metrics you asked for. **Shape-only validation refreshing the freshness clock.** Every ingest endpoint I audited validated shape and never value: `if (typeof body.narrative !== 'string') return 400`, then write the record with a fresh timestamp. So an all-null publish **refreshes** the clock and reads as fresh. Freshness monitoring detects an absent producer and is structurally blind to a broken one. Check: add a minimum-value assertion for at least the fields you would notice missing, and record counts alongside timestamps. **Build date stamped as data freshness.** One repo I read holds both the correct rule, "a fresh build should not make stale data look new," and its violation, pages stamping `new Date()` as the freshness signal regardless of whether the underlying data advanced. A rerun that changes nothing relabels the output as new, so "last updated" stops being evidence of anything. The LLM version is stamping `generated_at` on a cache hit. Check: render pipeline-run time and data-as-of time as two separately labeled values, and never conflate them. **A euphemism with no expiry.** A comment reads "never print a bare 0" because two data families are empty between runs, so a dead source renders as "not yet published in this build." It sounds transient. It had been true for roughly twelve consecutive green weekly runs, shipping two empty page families and a frozen search index, with the only evidence being four warning lines in logs nobody reads. Check: a degraded state framed as transient needs a check that escalates when it persists. Otherwise it becomes permanent and invisible. **The general rule:** publish your exclusion counts. If you dropped 40 records because they could not be parsed or were the wrong vintage, say so in the output, in numbers. The consumer can then see how much of the corpus was discarded, which is exactly what an LLM pipeline should expose when it silently skips records. --- ## 9. Attribute spend per call site You cannot manage what you cannot attribute, and a shared key across two projects is a bill nobody can read. I spent weeks believing one product was responsible for a Moonshot bill that turned out to be roughly 70% a different project sharing the same key. Write a usage record on every model call: ```js recordUsage({ callSite: 'summarize.tail', // the specific job, not the file provider: 'fireworks', model: resolved, // what actually ran, not what you asked for inputTokens: u.prompt_tokens ?? u.input_tokens, outputTokens: u.completion_tokens ?? u.output_tokens, cacheReadTokens: u.cache_read_input_tokens ?? u.prompt_tokens_details?.cached_tokens ?? 0, approxUsd, attempt, // so failed-then-fallback runs bill honestly ts: new Date().toISOString(), }); ``` Four things make this useful rather than decorative: - Record the model that **actually ran**, not the one you requested. With a fallback ladder those differ exactly when you most need to know. - Record failed attempts too, at the attempted model's rate. A ladder that fails twice before succeeding costs three calls. - Use race-free keys. `//-` rather than anything that can collide under concurrency. - Expose it behind an admin-token endpoint that aggregates per day, per call site, and per model. The first time you look at that table you will find something. Also record provenance **in the output record**, not just in the usage log: ```js record.source = record.source || 'fallback: glm-4.7 after claude timeout'; ``` That is the difference between degraded output you can identify after the fact and degraded output indistinguishable from healthy output. One pipeline I read does this well: its static-fallback path stamps its own name into the artifact and fills fields only if missing, so real data is never overwritten and the record declares what produced it. --- ## 10. Verify a cheaper model on your own workload Never trust a vendor benchmark for your workload. Build a golden set instead, and make it small enough that you actually run it. 1. Pull 50 to 100 real inputs from production, chosen to include the awkward ones: the longest, the shortest, the ones in a second language, the ones that have failed before. 2. Record your current model's output for each. That is your reference, not a ground truth. 3. Run the candidate model over the same inputs, same prompt, and store both outputs side by side. 4. Grade with a **third** model, and never with either of the two being compared. Ask for a per-item verdict plus a reason, not a score. Reasons are auditable; scores are not. 5. Read the disagreements yourself. All of them, if there are fewer than twenty. This is where you learn whether the cheaper model fails in a way that matters or in a way you can ignore. 6. Shadow the candidate on live traffic before you route to it: run both, serve the incumbent, log the delta. Two things to watch that a benchmark will not tell you. **JSON mode is three different guarantees sold under one parameter name.** `response_format: {type: 'json_object'}` guarantees syntactically valid JSON and nothing else, so fields you marked required can be missing. A `json_schema` with `strict: true` guarantees conformance but restricts which schema features you may use. And a third class of provider accepts the parameter and treats the schema as a strong hint, which gives you plausible JSON that fails validation intermittently under load. Test with a deliberately awkward schema: nested optionals, an enum, an array of objects. And **tool descriptions are not portable prompts.** The same tool schema gets different trigger rates per provider and per model generation. Recent Claude models under-trigger tools relative to older ones and respond well to prescriptive "call this when X" descriptions, while OpenAI-tuned descriptions written with "CRITICAL: you MUST" over-trigger on models that follow instructions literally. Swapping providers without re-tuning tool descriptions is the most common silent quality regression there is. --- ## 11. Parameter traps by provider These are the ones that produce a 400 on a working codebase, or worse, a silent no-op. **Kimi k2.x and k3 hard-fix sampling parameters and error on your values.** The docs are explicit: "Fixed means the parameter cannot be modified: passing any other value returns an error, so do not pass it explicitly." Strip `temperature` and `top_p` from the payload entirely. Do not send 1.0 to be safe, do not send null. `top_p` is fixed at 0.95. On `kimi-k2.7-code`, `tool_choice` accepts only `auto` or `none`, `n` must be 1, and both penalties are fixed at 0.0. The inverse trap matters too: `moonshot-v1-*` **does** accept temperature, so a shared code path that worked there will 400 the moment you switch to a k2.x model string. **Thinking cannot be disabled on the newest Kimi models.** `kimi-k3` has reasoning always on, with `reasoning_effort` in `{low, high, max}` defaulting to **max**. A naive port silently buys the most expensive reasoning setting on a $15 per Mtok output model. Set it to `low` explicitly unless you need depth. `kimi-k2.7-code` also cannot disable thinking. Only k2.6 and k2.5 accept `thinking.type = 'disabled'`. **The answer is in `message.content`; `message.reasoning_content` is separate.** Reasoning arrives first in a stream. Code that renders the first non-empty delta shows the user raw chain of thought. Code that reads only `delta.content` looks hung for the entire reasoning phase and then bursts, which is why people set aggressive timeouts and then wonder why calls fail. DeepSeek has the sharper version: with the `tools` parameter present, `reasoning_content` **must** be echoed back on every subsequent turn or you get a 400. Generic OpenAI-compatible wrappers drop unknown assistant fields, so every tool-calling conversation dies on turn two with a confusing error. **DeepSeek thinking mode accepts sampling parameters and ignores them.** `temperature`, `top_p`, and both penalties are accepted without error and have no effect. Your determinism knob becomes a no-op with no signal. **Current Anthropic models removed sampling parameters entirely.** On Opus 5, Opus 4.8, Opus 4.7, Sonnet 5, and Fable 5, sending `temperature`, `top_p`, or `top_k` is a 400. So is `thinking: {type: 'enabled', budget_tokens: N}`. Use `thinking: {type: 'adaptive'}` and `output_config: {effort: ...}`. A trailing assistant-turn prefill, the classic JSON-forcing trick, is also a 400 on all of them; use `output_config.format` instead. Assistant turns in the middle of the array for few-shot are still fine. **On Opus 5, thinking is on by default.** Omitting the `thinking` field enables it, unlike Opus 4.8 and 4.7 where omitting meant off. And `max_tokens` caps thinking **plus** response text together. A route that never set thinking and sized `max_tokens` tightly around its answer will now truncate mid-response. **Qwen tier bands reprice the entire call.** Several Qwen models are priced in context brackets, and the documentation confirms "All tokens in the request are billed at the unit price of the corresponding tier." A 40,000 token prompt does not pay the cheap rate for the first 32,000 and the expensive rate for the rest. It reprices the whole call. Budget from the bracket you will actually land in. **Qwen region keys are not interchangeable.** Singapore, US Virginia, and China Beijing keys are separate, and a key minted in one silently fails in another. Pinning a dated snapshot instead of the alias also drops your rate limit substantially and can forfeit a promotional price. **Stop sequence limits differ by 4x.** OpenAI Chat Completions accepts 4, Anthropic accepts 16, Bedrock's converse API caps at 4 regardless of the underlying model. OpenAI excludes the stop sequence from the returned text; Anthropic includes it. A prompt with six stop sequences works natively and 400s the moment you route it through a gateway, and your parser keeps or drops a delimiter depending on where the request landed. **A refusal is a 200 in one place and a 400 in another.** Claude Opus 5 and Fable 5 return HTTP 200 with `stop_reason: 'refusal'` and content that is either empty, which is unbilled, or partial, which **is** billed. Code doing `content[0].text` unconditionally crashes on a successful HTTP response. Branch on `stop_reason` before reading content, and branch on `stop_reason` rather than `stop_details`, because `stop_details` can be null even on a refusal. **Model IDs are three different strings for one model.** First-party bare `claude-opus-5`, Bedrock's `anthropic.claude-opus-5`, Vertex's `@`-separated dated snapshots, and Claude Platform on AWS which uses the bare ID despite being AWS. A config that stores one ID and swaps the base URL 404s on two of the four surfaces. **Streaming usage is opt-in and often unimplemented.** On the OpenAI wire you must send `stream_options: {include_usage: true}` to get a final usage chunk. Many compatible endpoints do not implement it, so your cost accounting silently reads zero for every streamed request. If your dashboard shows streaming traffic as free, that is why. --- ## 12. Watch your model IDs like a supply chain Model strings are dependencies with sunset dates and nobody sends you a deprecation PR. As of 2026-08-05, Moonshot has retired its entire k2 preview line and the whole `moonshot-v1` family plus `kimi-k2.5` sunset around **August 31, 2026**. If your fallback ladder resolves to a `moonshot-v1` tier, and many do because that family was the reliable fast tier for a long time, your fallback disappears this month. Both of Moonshot's documentation domains also moved. Anything you read about Kimi predating roughly June 2026, including third-party pricing aggregators, is likely quoting dead model IDs. Cerebras is deprecating its GLM-4.7 serving path on **August 17, 2026**. Anthropic's Sonnet 5 introductory pricing ends **August 31, 2026**, stepping from $2/$10 to $3/$15 per Mtok. Three habits that keep this from biting: 1. Never hardcode a model ID. Config-driven strings only, so a swap is an environment change. 2. Resolve the model list at runtime where the provider offers it, and pick from what actually exists. Filter to the family you trust rather than taking the first match, and never silently downsize below the context your job needs. I pin each job to a tier and let the resolver step **up** to the smallest sufficient one, never down. 3. Put a calendar reminder to re-read the pricing and models pages of every provider you depend on, once a month. It takes fifteen minutes. --- ## 13. The cutoff drill Run this once, on a normal Tuesday, before you need it. 1. **Pick a primary and revoke it in staging.** Not a fake 500. Actually remove the key. 2. **Time to first successful fallback response.** Write the number down. If it is longer than your users will wait, your fallback is decorative. 3. **Check what silently changed.** Did context truncate because the fallback leg serves a smaller window? Did structured output stop conforming because the upstream ignores `response_format`? Did your prompt cache go cold and triple your input cost? Did the fallback model's different tokenizer blow past a `max_tokens` you tuned elsewhere? 4. **Confirm the alarm fired.** If your monitoring did not notice a full provider outage, fix the monitoring before you add another provider. 5. **Confirm cost attribution followed the traffic.** Your usage records should show the fallback model, at its own rate, for the duration. 6. **Read the output.** Not the status codes. The actual text. Decide whether you would have shipped it. 7. **Restore and diff the bill.** Now you know what the drill costs per hour, which is the number that tells you how long you can run degraded. Write the results in a file next to this playbook. Re-run it when you change providers, and after any model deprecation. --- ## Quick reference: verified prices USD per million tokens, standard synchronous rates, checked 2026-08-05. Batch column is the discounted rate where a batch endpoint exists. ### Anthropic | Model | Input | Output | Batch in/out | Cache read | |---|---|---|---|---| | `claude-opus-5` | 5.00 | 25.00 | 2.50 / 12.50 | 0.50 | | `claude-sonnet-5` | 3.00 | 15.00 | 1.50 / 7.50 | 0.30 | | `claude-haiku-4-5` | 1.00 | 5.00 | 0.50 / 2.50 | 0.10 | | `claude-fable-5` | 10.00 | 50.00 | 5.00 / 25.00 | 1.00 | Sonnet 5 is at an introductory **$2.00 / $10.00** through 2026-08-31, batch $1/$5, cache read $0.20. The table shows the standard rate that takes effect 2026-09-01, because a budget built on the promo doubles without warning. Batch is first-party and Claude Platform on AWS only, never Bedrock or Vertex. `inference_geo: "us"` applies a 1.1x multiplier to every token category and stacks with batch and cache multipliers. ### Moonshot / Kimi | Model | Input | Output | Cache hit | Context | Batch | |---|---|---|---|---|---| | `kimi-k3` | 3.00 | 15.00 | 0.30 | 1,048,576 | **not eligible** | | `kimi-k2.7-code` | 0.95 | 4.00 | 0.19 | 262,144 | 0.57 / 2.40 | | `kimi-k2.6` | 0.95 | 4.00 | 0.16 | 262,144 | 0.57 / 2.40 | | `kimi-k2.7-code-highspeed` | 1.90 | 8.00 | 0.38 | 262,144 | not eligible | Batch is 60% of standard, so a 40% discount, and only three models are eligible. `kimi-k2.5` at $0.60/$3.00 and the entire `moonshot-v1` family **sunset around 2026-08-31**; do not architect on them. The built-in `$web_search` tool bills **$0.005 per successful call on top of tokens**, and search result tokens are billed too, which can dominate the bill on a cheap model. ### Alibaba Qwen, Singapore / International | Model | Input (list) | Output (list) | Batch | |---|---|---|---| | `qwen3.7-max` | 2.50 | 7.50 | not eligible | | `qwen3.7-plus` | 0.40 (to 256K) / 1.20 | 1.60 / 4.80 | not eligible | | `qwen3.6-flash` | 0.25 / 1.00 | 1.50 / 4.00 | not eligible | | `qwen3.5-flash` | 0.10 | 0.40 | not eligible | | `qwen-plus` | 0.40 / 1.20 | 1.20 / 3.60 | 50% off | | `qwen-flash` | 0.05 / 0.25 | 0.40 / 2.00 | 50% off | | `qwen-turbo` | 0.05 | 0.20 non-thinking / 0.50 thinking | 50% off | | `qwen-max` | 1.60 | 6.40 | 50% off | `qwen3.7-max` currently shows a limited-time 50% off and `qwen3.7-plus` a 20% off, with **no published end date**. List prices are shown, because that is what you will eventually pay. On the international endpoint, batch covers only `qwen-max`, `qwen-plus`, `qwen-flash`, and `qwen-turbo`: the entire qwen3.x line is Beijing-only for batch. Cache-hit pricing is a rule rather than a number, 10% of standard input for explicit and 20% for implicit, and it cannot be combined with batch. ### The portable open-weight tier | Model | Price | Providers at that price | Context | |---|---|---|---| | `gpt-oss-120b` | 0.15 / 0.60 | Groq, Together, Fireworks | 131,072 | | `deepseek-v4-flash` | 0.14 / 0.28 | DeepSeek, Together, Fireworks | 1M | | `glm-5.2` | 1.40 / 4.40 | Z.ai, Fireworks, Together | varies by host, pin it | | `glm-4.5-air` | 0.20 / 1.10 | Z.ai, and Together's batch list | 131,072 | | `deepseek-v4-pro` | 0.435 / 0.87 first-party | 1.74 / 3.48 at Together and Fireworks | 1M first-party, 512K at Together | OpenRouter fronts roughly 19 upstreams for gpt-oss-120b with a floor near $0.03/$0.17, 21 for deepseek-v4-flash with a floor near $0.084/$0.168, and about 30 for glm-5.2 with a floor near $0.28/$0.88. --- ## Staleness warning Every number and URL here was checked against official vendor documentation on **2026-08-05**, and several were wrong in the first draft before verification: a promotional rate presented as standard, two documentation-printed URLs that were dead, a batch eligibility list that applied to the wrong region, and a rate limit rule repeated from a retired policy. Vendor consoles that return HTTP 200 for nonexistent paths make this worse, because a link that loads is not evidence a page exists. Re-verify before you commit spend. Prices move, models sunset on weeks of notice, and third-party aggregators lag badly. *This document is informational, not legal, financial, or procurement advice. Provider names and trademarks are used nominatively. No affiliation or endorsement is implied.*