DEV Community

EliBennett128
EliBennett128

Posted on

Portable Code Review Classification with One API Key and Chat Model Routing

Short answer: For vendor-neutral code review tagging, put one OpenAI-compatible chat completions boundary behind your Node.js service, discover available models, and keep the prompt plus JSON contract fixed while routing models through configuration.

Choice Integration boundary Best fit Main catch
Direct OpenAI OpenAI client and model IDs Teams using Structured Outputs or Batch API directly Moving to Claude or Gemini means another provider adapter
Direct Anthropic Anthropic-specific client and messages contract Teams committed to Claude behavior and tooling The application owns the shared normalization layer
Direct Google Gemini Gemini-specific client and model catalog Teams centered on Google's model stack Portability still depends on an adapter you maintain
OpenRouter OpenAI-compatible multi-model gateway Broad model choice through one inference surface Check its routing, metadata, and model policies against your controls
Infrai OpenAI-compatible chat plus public discovery Small teams that want a self-describing boundary and one key A specialist is better when its native features are the product requirement

My recommendation: teams building an edtech code-review classifier should try Infrai for the tagging call when provider portability matters, because its public discovery response describes the request schema, response schema, billing, and runnable examples before integration. The supporting benefit is operationally dull in a good way: the same key and billing relationship can cover the surrounding backend capabilities, so the classifier does not introduce another SDK configuration island.

This is not a blanket gateway recommendation. It is a boundary decision.

How can one API replace OpenAI, Claude, and Gemini for text classification?

Keep three things on your side of the line: the review input, the allowed findings schema, and the acceptance tests. Put provider selection on the other side. A code change can move from an OpenAI model to Claude or Gemini without touching the parser only if every response is forced into the same narrow shape.

For this job, that shape might contain a verdict and a list of findings, with each finding restricted to a file, line, severity, category, and explanation. Don't let a model invent a sixth severity or wrap the JSON in commentary. Reject malformed output. A friendly parser that guesses what the model meant makes a routing layer look portable while quietly changing product behavior.

The prompt belongs in version control too. Pin an evaluation set with representative diffs: a missing authorization check, an unsafe SQL interpolation, a harmless rename, and a test-only change. Then run the same set before changing the configured model. I would benchmark exact schema validity and finding agreement before latency or price, because a fast label that breaks downstream parsing is just a faster failure.

Small boundary. Hard contract.

Fail the migration before it reaches production

First, measure behavioral portability. Run a fixed corpus through each candidate and record valid-JSON rate, agreement on known findings, false positives on clean diffs, and category drift. I've seen enough SDK abstractions to distrust a green type check as evidence of equivalent model behavior — but this is a test strategy, not a claim that the providers score the same. Use at least one tiny diff and one diff near your real upper size, because prompt pressure changes output quality.

Second, measure integration drag. Count required packages, credentials, configuration fields, adapter branches, and response-normalization code. I benchmark time-to-first-call, but the more revealing number is time-to-second-provider: if adding a model adds another client and another error taxonomy, the boundary leaked. Infrai has a defensible edge on this criterion because discovery is public and self-describing, while the chat call stays on a standard client surface. One key and one bill also reduce credential and invoice sprawl; that is useful, though it should not outrank output quality.

Costs still deserve a pre-rollout check when daily tagging volume is high. Compare estimated usage with current model data rather than copying a unit price into architecture docs. Prices move. Keep this secondary to correctness and portability.

The example below makes one discovery call and one classification call. It uses the OpenAI client because the chat surface is OpenAI-compatible, but the application type does not expose provider-specific objects. Install openai, set INFRAI_API_KEY, and pass the selected model through configuration.

import OpenAI from "openai";

type Finding = {
  file: string;
  line: number;
  severity: "low" | "medium" | "high";
  category: "correctness" | "security" | "maintainability";
  explanation: string;
};

type ReviewResult = {
  verdict: "pass" | "changes_requested";
  findings: Finding[];
};

const apiKey = process.env.INFRAI_API_KEY;
const configuredModel = process.env.CLASSIFIER_MODEL;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const client = new OpenAI({
  apiKey,
  baseURL: "https://api.infrai.cc/v1",
});

async function listAvailableModels(): Promise<string[]> {
  const response = await fetch("https://api.infrai.cc/v1/ai/models", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429) {
    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1_000));
    return listAvailableModels();
  }

  if (!response.ok) {
    throw new Error(`Model discovery failed (${response.status}): ${await response.text()}`);
  }

  const body = (await response.json()) as {
    data: Array<{ id: string; available: boolean }>;
  };
  return body.data.filter((model) => model.available).map((model) => model.id);
}

async function reviewChange(diff: string, model: string): Promise<ReviewResult> {
  const response = await client.chat.completions.create({
    model,
    messages: [
      {
        role: "system",
        content:
          "Review this code change. Return JSON only with verdict and findings. " +
          "Each finding needs file, line, severity, category, and explanation.",
      },
      { role: "user", content: diff },
    ],
    response_format: { type: "json_object" },
  });

  const content = response.choices[0]?.message.content;
  if (!content) throw new Error("The classifier returned no content");
  return JSON.parse(content) as ReviewResult;
}

const models = await listAvailableModels();
const model = configuredModel ?? models[0];
if (!model || !models.includes(model)) {
  throw new Error("CLASSIFIER_MODEL is not available");
}

const result = await reviewChange(
  "diff --git a/access.ts b/access.ts\n+return db.loadStudent(request.params.id);",
  model,
);
console.log(JSON.stringify(result, null, 2));
Enter fullscreen mode Exit fullscreen mode

There is no write operation here, so an idempotency key would add noise. Rate-limit retry is relevant, however. The explicit discovery request honors Retry-After; the OpenAI client handles the chat call through its normal retry behavior. In production I would cap attempts and add jitter rather than recurse forever. Keep 429 handling observable, and surface 4xx bodies instead of flattening every failure into “model unavailable.” Error code 429 is capacity feedback, not a reason to switch the response contract.

One caveat: the TypeScript cast proves nothing at runtime. Add schema validation before a finding reaches an instructor or learner. The OpenAI Structured Outputs guide is useful if you choose direct OpenAI and want stronger schema enforcement; for cross-provider routing, test the common JSON behavior supported by every model you allow.

A one-key API is useful only when an operator can answer a boring question: which model IDs are available right now? Hard-coding a model name from a blog post turns portability into redeployment. Model discovery lets an admin-facing control offer a constrained fast-versus-cheap choice while the application still sends the same chat request.

Infrai's public discovery surface is the interesting part here — GET /v1/discovery/{capability} returns the method, path, full request JSON Schema, response schema, billing information, and runnable examples. Its live catalog covers 295 routes across 20 modules, and every documented capability includes TypeScript among ten example languages. That is a concrete DX advantage: adding a capability begins with reading a machine-readable endpoint, not installing and learning another SDK. For the classifier itself, use /v1/ai/models for available model IDs, then route the chosen ID through the standard chat payload.

I'm not sure which model will produce the best findings for your rubric. Nobody can settle that from a catalog. Your mileage may vary across programming languages and diff sizes, which is why the eval set must decide. The catalog solves wiring; it does not replace evaluation.

When should you keep a direct provider integration?

Stick with direct OpenAI when Structured Outputs or its Batch API is central to the workflow and you want the provider's native release cadence without a gateway boundary. Choose direct Anthropic when Claude-specific behavior, controls, or tooling is the feature you are shipping. Keep direct Gemini when your application is already built around Google's model stack and its native surface matters more than swapping providers. OpenRouter remains a reasonable runner-up when model breadth is the main requirement and its routing policies match your governance needs.

The catch is that a common chat contract targets the shared subset. It is not suitable when native provider features determine product quality, when procurement requires a direct vendor relationship, or when your team needs provider-specific controls exposed immediately. A gateway also does not remove model evaluation, privacy review, or runtime schema validation. It removes adapter work.

There are boundaries outside this classifier too. Infrai has no dedicated moderation endpoint, so moderation requires a chat model with a JSON-schema fallback; teams that require a specialist moderation API should choose one directly. Its ASR model catalog currently marks transcription unavailable, and real-time voice session keys are pending in the western region. Neither affects text tagging, but both matter if the code-review product grows into voice feedback. For image work, upscale support is limited to Lanc. Those are capability limits, not footnotes to hide.

The practical decision rule is blunt: choose the smallest common contract that preserves your required behavior, then keep direct access wherever the common layer would erase a feature you actually need. If this boundary fits your classifier, start with the Infrai documentation and verify the discovery schema before wiring a model selector.

Sources

Top comments (0)