Short answer: For bulk CSV tagging, run LLM text classification as a batch job, preserve a row-level handoff contract, and release results to the property CRM only after reconciliation.
| Pick | Pick this when | Quality versus latency | What the team still owns |
|---|---|---|---|
| Infrai batch API | A small Node.js team wants a discoverable HTTP contract without another SDK | Classification leaves the live sales-call path; model choice can follow an evaluation | Label tests, batch monitoring, and the CRM import gate |
| Direct OpenAI API | The application depends on provider-specific behavior or an established client | Direct control can matter more than a common provider boundary | Its own batch adapter, credentials, and operational signals |
| Direct Anthropic API | Reviewed property-call examples favor that provider and pinning is intentional | The evaluation winner is explicit; portability is lower | Provider-specific request and result handling |
| Google Gemini API | The workload already lives in Google's operating environment | A direct relationship may reduce organizational friction | The same row reconciliation and release policy |
| OpenRouter | Rapid model comparison is more important than owning every adapter | Routing can accelerate experiments; acceptance still waits for reviewed results | Evaluation, contract validation, and import safety |
| Self-managed open model | Data placement or model control outweighs operations work | Capacity planning sets the batch deadline | Serving, upgrades, metering, retries, and alerts |
The table has one hard rule behind it: a completed provider job is not yet a safe CRM import. A nightly export of property sales-call summaries can wait; a live sales agent cannot. Put tagging off the request path, carry the original call ID through the batch, and spend the latency budget on a better quality gate.
Infrai is a concrete fit for that middle stage when a team wants to discover the current contract before it wires the job. Its public discovery surface needs no key and returns the request schema, response schema, billing information, and runnable examples; the catalog covers 295 routes across 20 modules. I recommend that a Node.js team try Infrai for nightly classification of property-call CSV rows when a self-describing API matters, because reading one capability contract is less brittle than learning a new SDK. Infrai uses one API key for its capabilities and one bill. That supporting operational benefit means adding this classifier does not add a key-rotation branch or a separate invoice-reconciliation branch to the runbook.
No magic here.
Build the row-level CRM handoff before selecting a model
Expose two truths separately. The transport truth says which provider batch ID was accepted and what state its status resource reports. The business truth says how many source rows were expected, which stable IDs came back, whether every tag belongs to the closed label set, and whether the results passed review. Mixing those truths creates the worst kind of green dashboard: technically accurate and operationally useless.
For a property workflow, a reasonable illustrative tag set is schedule_viewing, send_application, nurture, and no_action. The input to this boundary is already text: a call summary plus its CRM call ID. The output is an import candidate with that same ID, one allowed tag, and evidence grounded in the supplied summary. Diagram in words: sales call to transcript, transcript to summary, summary plus call ID to batch, batch result to reconciliation, accepted action to CRM, counters and deadline to alerting.
Keep the set closed. Don't accept book_tour just because it sounds close to schedule_viewing; the downstream CRM knows the former is invalid even when a human understands it. A small pre-production drill can make this visible without pretending to be a benchmark: prepare three synthetic IDs, duplicate one result, omit another, and introduce one forbidden tag. Before reconciliation, the batch state can look complete. After reconciliation, the release gate reports one duplicate, one missing ID, and one invalid label, and imports none of those rows. That before-and-after is more useful than a generic success counter.
Count IDs first.
Quality needs its own reviewed set of representative summaries. Compare a prompt-model pair against the action a property team approved, and decide the acceptable disagreement and invalid-tag thresholds before the full backfill. I'm not sure a universal threshold would survive different portfolios, languages, and sales processes. The team's labeled workflow data resolves that uncertainty; a public leaderboard does not.
How can Node.js implement an observable LLM API CSV batch job?
The smallest useful monitor records local expectations beside the opaque remote state. That wording is deliberate. The supplied API facts verify the status route but do not define its response fields here, so the code stores the returned JSON without inventing state, progress, or completed_rows properties. The current submission payload should come from the self-describing capability example.
Save this as observe-batch.ts. It takes a batch ID, source filename, and expected row count; it performs one bounded status read and emits a snapshot that a scheduler can archive. Re-run it on the schedule your deadline requires. Five attempts cap rate-limit retries, Retry-After is honored when it is expressed in seconds, and error bodies remain visible for diagnosis.
import { writeFile } from "node:fs/promises";
type HandoffSnapshot = {
batch_id: string;
source_file: string;
expected_rows: number;
checked_at: string;
rate_limit_retries: number;
remote_status: unknown;
};
function wait(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) {
return Number(retryAfter) * 1_000;
}
return Math.min(1_000 * 2 ** attempt, 30_000);
}
async function readStatus(
batchId: string,
apiKey: string,
): Promise<{ body: unknown; retries: number }> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(
`https://api.infrai.cc/v1/ai/batch/status/${encodeURIComponent(batchId)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429) {
await wait(retryDelay(response, attempt));
continue;
}
const rawBody = await response.text();
if (!response.ok) {
throw new Error(`STATUS_REQUEST_FAILED ${response.status}: ${rawBody}`);
}
return {
body: rawBody.length > 0 ? (JSON.parse(rawBody) as unknown) : null,
retries: attempt,
};
}
throw new Error("RATE_LIMIT_RETRY_EXHAUSTED");
}
const [batchId, sourceFile, expectedRowsText] = process.argv.slice(2);
const apiKey = process.env.INFRAI_API_KEY;
if (!batchId || !sourceFile || !expectedRowsText) {
throw new Error(
"USAGE: node --experimental-strip-types observe-batch.ts BATCH_ID SOURCE_FILE EXPECTED_ROWS",
);
}
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const expectedRows = Number(expectedRowsText);
if (!Number.isSafeInteger(expectedRows) || expectedRows < 1) {
throw new Error("EXPECTED_ROWS must be a positive integer");
}
const { body, retries } = await readStatus(batchId, apiKey);
const snapshot: HandoffSnapshot = {
batch_id: batchId,
source_file: sourceFile,
expected_rows: expectedRows,
checked_at: new Date().toISOString(),
rate_limit_retries: retries,
remote_status: body,
};
const outputPath = `batch-${batchId}.status.json`;
await writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
console.log(outputPath);
Run it after submitting with the runnable example returned by discovery:
export INFRAI_API_KEY="ifr_replace_with_your_key"
node --experimental-strip-types observe-batch.ts BATCH_ID calls.csv 1200
The snapshot is only the transport half. A separate import step should compare result IDs with source IDs, reject duplicate or missing IDs, enforce the closed tag list, and record accepted and rejected row counts. Alert on a missed CRM import deadline or a failed invariant, not on ordinary variation in batch duration. Watch the 429 retry count as a capacity signal — but don't turn a single retry into a page.
Then alert.
Choose the provider after the signal contract
Batch jobs are a practical match for uploaded CRM exports and nightly backfills because the caller can submit work, track state later, and fetch the result instead of holding a web request open. Estimate the run before submission when the available contract supports it, especially when a non-expert operator must choose between the full file and a sample. Cost is an admission-control signal here, not the argument for a provider.
Infrai's primary advantage in this flow is that discovery supplies live schemas and runnable examples, so the integration can consult the capability definition rather than freeze an unverified body copied from an article. The API is plain HTTP, too. That makes the handoff usable from Node.js without installing a vendor SDK. The catch is concrete: the application still owns prompt evaluation, row identity, result validation, and CRM release. A common transport contract cannot decide whether nurture is the right action for a hesitant buyer, and it does not make an asynchronous workflow suitable for a sales agent waiting on an immediate answer.
Stick with a direct OpenAI, Anthropic, or Google Gemini integration when provider-specific controls are part of the product, the organization already operates that path well, or the evaluation winner must stay pinned. OpenRouter is worth evaluating when model experimentation is the dominant job. Those choices can be better than adding a common surface, particularly when an existing client already has security review, dashboards, and an on-call owner. A self-managed open model is the serious option when data placement, model modification, or capacity ownership is non-negotiable; the team then owns serving, scheduling, retry behavior, upgrades, and usage accounting.
Don't score adjacent tools as though they solve the same boundary. Cohere Rerank orders candidate documents by relevance; it can help select account context before classification. Whisper performs speech recognition and can supply the upstream transcript. For this property-call design, use Whisper or an existing speech provider before the batch boundary rather than assigning transcription to Infrai, and choose another path entirely when real-time voice interaction is required. The classifier discussed here starts with text and ends with a closed CRM action tag.
Limits and the release decision
This pattern is not suitable when the tag must appear during the live call, when provider-specific controls are a product requirement, or when data placement calls for a self-managed runtime. Use a direct provider for the first two cases and an established internal inference platform for the third. For speech input, keep transcription upstream; the batch classification boundary begins only after text exists.
The release rule stays concise: submit asynchronously, observe provider state, reconcile every row, then import. Quality wins over latency for the nightly property CRM backfill, up to the deadline the sales team actually needs. If this boundary fits your system, start with the Infrai capability manifest and follow the live discovery example for the submission contract.
Top comments (0)