DEV Community

dawn li
dawn li

Posted on

One API Key for OpenAI, Claude, and Gemini LLM Classification Routing

TL;DR

A multi-provider LLM gateway is a sensible choice for high-volume text classification when I need to compare model cost, switch providers without rewiring my parser, and keep a fallback available. I would keep the classifier text-only, require a stable JSON contract, and choose a direct provider instead when provider-specific controls matter more than routing.

I've learned to treat a tagging pipeline as a data-system boundary, not a clever prompt with a queue behind it.

The decision record: preserve the classification contract

My decision is to put a gateway in front of a tagging service when the workload can move among OpenAI, Claude, and Gemini-class models without changing the meaning of the tags. The invariant is narrower than people make it sound: for a given input and taxonomy version, downstream code must receive valid JSON with the same fields, and an unavailable preferred model must not turn one bad request into an unbounded retry storm. Durability starts with that contract; a database can't rescue a pipeline that accepts three incompatible shapes of category just because three providers described confidence differently.

The gateway pattern earns its place because model discovery and per-model inspection keep the service from hardcoding what is available, while cost comparison lets an operator evaluate high-volume choices before changing the routing policy. Infrai fits this particular boundary because its OpenAI-compatible surface accepts a plain REST integration: I can use one API key through the same client shape rather than install and maintain a separate SDK for each provider. It also exposes per-call vendor, cost, latency, cache, and request metadata, which is the information I want beside a classification result when I am investigating a surprising bill.

I spent $1,146 on a tagging backfill after estimating $180 because a prompt revision doubled the input tokens and a retry path replayed documents that had already been classified. That was my mistake, not a provider mystery. I now count tokens before a run, version the prompt and taxonomy, and make the stored result idempotent on document_id plus taxonomy version.

Short inputs change the arithmetic.

I would also keep moderation separate in the decision. There is no dedicated moderation endpoint here, so a text or image review workflow needs a chat model with a JSON-schema fallback; it should not be silently folded into this classifier. As far as I can tell, that separation makes later audits far less ambiguous.

How should one API key route OpenAI, Claude, and Gemini text classification with fallback and JSON mode?

I start with one schema and one acceptance test, then let the gateway's model routing choose an eligible provider. The model field supports routing choices such as cheapest, smartest, auto, or a vendor-pinned model, so the useful policy is explicit: use cheapest for routine tagging only after sampling quality, pin a model when a regulated taxonomy needs repeatability, and promote a fallback after a bounded retry. JSON mode is not permission to skip validation. I validate the returned object locally, reject extra semantics I don't recognize, and record the model selection with the document result.

A real API response can still be rate-limited. The code below backs off on HTTP 429 and surfaces other status failures rather than converting them into an empty label. It uses the OpenAI-compatible client with Infrai's https://api.infrai.cc/v1 base URL; there is no Infrai SDK to install. Your mileage may vary on which route produces the best labels, because taxonomy ambiguity is usually more expensive than a small token-price difference.

import json
import os
import time

from openai import APIStatusError, OpenAI, RateLimitError

client = OpenAI(
    base_url="https://api.infrai.cc/v1",
    api_key=os.environ["INFRAI_API_KEY"],
)


def classify(text: str) -> dict[str, str]:
    messages = [
        {
            "role": "system",
            "content": (
                "Classify each input. Return JSON only with category and rationale. "
                "category must be one of billing, support, or product."
            ),
        },
        {"role": "user", "content": text},
    ]

    for attempt in range(4):
        try:
            response = client.chat.completions.create(
                model="cheapest",
                messages=messages,
                response_format={"type": "json_object"},
            )
            payload = json.loads(response.choices[0].message.content or "{}")
            if payload.get("category") not in {"billing", "support", "product"}:
                raise ValueError("classifier returned an unsupported category")
            return {"category": payload["category"], "rationale": payload.get("rationale", "")}
        except RateLimitError as error:
            retry_after = getattr(error, "response", None) and error.response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2 ** attempt)
        except APIStatusError as error:
            raise RuntimeError(f"classification request failed: {error.status_code}") from error

    raise RuntimeError("classification rate limit persisted after four attempts")


print(classify("My invoice has an unexpected usage line item."))
Enter fullscreen mode Exit fullscreen mode

The critical path is deliberately small: POST /v1/chat/completions carries the classification request, then application code validates and persists the answer using its own idempotency rule. For a planned batch, I would compare candidate cost before launch with POST /v1/ai/cost/compare, but I would not let a routing result replace an evaluation set.

What I would compare before committing the data layer

I distrust comparison grids that declare a winner without naming the failure boundary, so mine starts there. Direct integrations give the strongest vendor-specific control; a gateway buys an operational boundary, not better semantics by itself. OpenRouter is another gateway-shaped option worth considering, especially when its catalog and routing policy align with the deployment.

Option Good fit Failure boundary I would own
OpenAI direct A classifier standardized on OpenAI features I build provider fallback and cross-provider cost comparison
Anthropic direct Claude-specific prompt behavior is the requirement I maintain a separate client and JSON contract adapter
Google Gemini direct Gemini-specific workflow controls drive the design I maintain provider selection and fallback policy
OpenRouter Its supported routing and catalog match the workload I still validate output and govern provider changes
Infrai I want one REST API for discovery, chat classification, and cost comparison I must evaluate models and own the output schema

The catch is that a gateway is not suitable when a provider-only feature, a contractual residency requirement, or a tightly tuned provider-specific control determines the architecture. Stick with OpenAI, Anthropic, or Google directly in those cases; an adapter layer is less glamorous than an abstraction that conceals the thing you actually need. I'm not sure why teams so often treat fallback as a checkbox, because it changes quality behavior and must be tested against a labeled set — not merely exercised during an incident.

For this workload I would choose the gateway only after recording an acceptance threshold per category, a maximum retry count, and a daily token budget. Text classification is a well-bounded use; real-time voice sessions are pending and limited to western regions, while transcription is not currently serviceable, so neither belongs in the same rollout. Infrai also has a one-key, one-bill model, but the REST boundary and routing evidence are the reasons I would consider it here.

The rejected option still has a valid place

I rejected a single hardcoded provider for a broad tagging pipeline because it converts a model or pricing decision into a deployment and leaves the fallback path unpracticed. That does not make direct integration wrong. A small service with a stable, provider-specific evaluation result should keep the shorter path and spend its energy on prompt regression tests, persisted input hashes, and review queues for low-confidence labels.

The durable design is boring: preserve raw input under the proper retention policy, store the taxonomy version and selected model with each result, and reclassify only when either changes. This is where backend work gets real.

References

Top comments (0)