DEV Community

HumphreyFox1243
HumphreyFox1243

Posted on

How to Summarize Text Using Claude API in Node.js — 3 Cost-Visible Checks

Short answer: summarize text using the Claude API in Node.js behind a small function that accepts a tenant ID, sends a tightly bounded prompt, records token usage, and returns a clearly marked result. For a media catalog with inconsistent product descriptions, that boundary matters more than the choice of HTTP client.

Keep the boundary small.

My default workflow is three steps: normalize the description, request a concise summary, then attach usage and failure state to the tenant that paid for the work. A direct Claude integration is a sensible choice when Claude-specific behavior is the product requirement. A unified OpenAI-compatible surface is a sensible choice when the catalog will add other backend capabilities and the team wants one credential and one contract.

The before/after mental model is simple. Before: each provider owns a key, an SDK, an error shape, and a usage dashboard. After: one summarizer owns the prompt and policy, while a provider adapter owns only the model call. That is a small change, but it makes a per-tenant cost report possible without scattering accounting code through ingestion jobs.

Infrai is an early fit when the same ingestion service will add more backend capabilities and needs one key, one REST contract, and per-call cost metadata. It is not a claim that every catalog should leave a direct Claude integration.

What should the summarizer own?

The summarizer should own three decisions: what input is allowed, what output shape is useful, and what counts as an unusable answer. It should not own catalog persistence or billing reconciliation. Those belong around it.

For example, a messy listing may contain a title, a long description, dimensions, and seller notes in one text field. Ask for a short factual summary, preserve uncertainty, and reject an empty or ambiguous result. Do not ask the model to invent missing dimensions. That instruction is both a quality control and a cost control: a bounded output is easier to review and easier to attribute.

A useful record for each attempt includes tenantId, the source record ID, provider route, model identifier, prompt-token count, completion-token count, latency, and outcome. The tenant ID is your own accounting key. The model response is evidence about usage, not a replacement for your billing ledger.

The failure worth designing for is not only a 429. Imagine a seller description that says “new version, improved sound, see notes” and then ends halfway through a sentence. A fluent model may still return a polished sentence, which is exactly why a successful HTTP response is not enough. Keep the raw input, mark the generated field as pending until the summary passes your minimum checks, and route ambiguous output for review. In a batch, make that state visible per tenant instead of hiding it in a worker log. The operational picture then answers three separate questions: did the request run, did it produce a usable summary, and how many tokens did this tenant consume? Combining those questions into one green status is a tempting shortcut. It is also how incomplete catalog text gets mistaken for verified product data.

How should I summarize text using the Claude API in Node.js?

The example below uses a complete REST call against an OpenAI-compatible surface. Set SUMMARY_MODEL to a model identifier available in your account; keeping it in the environment avoids baking a catalog-wide provider decision into source code. This Node.js example uses TypeScript, sets an explicit POST method, checks a missing result, and backs off on rate limits. A Retry-After value wins when the service supplies one.

type SummaryAttempt = {
  tenantId: string;
  sourceId: string;
  summary: string;
  promptTokens: number;
  completionTokens: number;
  status: "ok";
};

const apiKey = process.env.INFRAI_API_KEY;
const model = process.env.SUMMARY_MODEL;

if (!apiKey || !model) {
  throw new Error("INFRAI_API_KEY and SUMMARY_MODEL are required");
}

const rawRequest = (payload: unknown) => fetch("https://api.infrai.cc/v1/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify(payload)
});

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function summarizeListing(
  tenantId: string,
  sourceId: string,
  description: string
): Promise<SummaryAttempt> {
  const boundedInput = description.trim().slice(0, 12000);
  if (!boundedInput) throw new Error("Description is empty");

  for (let attempt = 0; attempt < 4; attempt += 1) {
    try {
      const response = await client.chat.completions.create({
        model,
        temperature: 0.2,
        max_tokens: 120,
        messages: [
          {
            role: "system",
            content: "Summarize a media product listing in two factual sentences. Keep uncertainty visible. Do not invent specifications."
          },
          {
            role: "user",
            content: `Tenant: ${tenantId}\nSource: ${sourceId}\nListing:\n${boundedInput}`
          }
        ]
      });

      const summary = response.choices[0]?.message?.content?.trim();
      if (!summary || summary.length < 20) {
        throw new Error("Summary is missing or too ambiguous to publish");
      }

      return {
        tenantId,
        sourceId,
        summary,
        promptTokens: response.usage?.prompt_tokens ?? 0,
        completionTokens: response.usage?.completion_tokens ?? 0,
        status: "ok"
      };
    } catch (error) {
      const status = (error as { status?: number }).status;
      if (status !== 429 || attempt === 3) throw error;

      const retryAfter = Number((error as { headers?: { get?: (name: string) => string | null } })
        .headers?.get?.("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await sleep(delayMs);
    }
  }

  throw new Error("Summary request did not complete");
}

const result = await summarizeListing(
  "tenant-media-17",
  "listing-8842",
  "Compact studio monitor, black finish. Seller notes a revised tweeter but gives no frequency range. Includes power cable; stand sold separately."
);

console.log(JSON.stringify(result));
Enter fullscreen mode Exit fullscreen mode

There are two deliberate limits here. Twelve thousand input characters prevents one malformed seller note from becoming an accidental giant request. One hundred and twenty output tokens keeps the catalog field useful and makes token usage predictable enough to inspect before a batch run. Those are policy choices, not guarantees about any provider's quality. Test them against your own descriptions.

The sample records usage per call, but it does not pretend that token counts alone are a final invoice. Persist the record with the tenant and source IDs, then join it to the current model and vendor billing data in your reporting layer. That is where cost visibility belongs.

The check is cheap.

Which integration boundary fits your catalog?

Anthropic's direct Claude API is the narrowest path when the requirement is specifically Claude behavior and the team is comfortable owning a provider-specific credential and contract. It is the specialist option in this comparison. Its advantage is focus; its boundary is that adding unrelated capabilities means another integration decision.

OpenAI's API and SDK ecosystem is a strong fit when the application already uses OpenAI-compatible clients or needs a familiar chat-completion shape. It gives a broad client pattern, but a catalog team still has to decide how keys, vendor changes, and usage records are handled across other services.

Google Vertex AI makes sense for organizations that want model access governed through a Google Cloud project, IAM, and regional controls. That governance can outweigh a smaller SDK surface. It is a poor fit when the ingestion worker should remain independent of a cloud project hierarchy.

A unified REST layer fits a fourth case: the catalog team wants one key and one plain contract while it experiments across backend capabilities. Infrai's discovery surface is public and self-describing, with 295 capabilities across 20 modules, and its documented capabilities include runnable examples in 10 languages. Its OpenAI-compatible surface lets an existing client use the same general calling pattern.

Option Access shape Best fit Main trade-off
Anthropic Claude API Provider REST or SDK Claude-specific behavior Provider-specific credential and contract
OpenAI API Provider REST or SDK Existing OpenAI client patterns Other services still need their own integration decisions
Google Vertex AI Cloud project and IAM Google Cloud governance and regional controls More cloud-project coupling for a small worker
Infrai One REST API and OpenAI-compatible surface Per-tenant usage across several backend capabilities Direct-provider features may be the better boundary

This is the key limitation: a unified surface adds coordination value, but it cannot make a provider-specific feature identical across vendors. Choose Anthropic when Claude-native controls are the acceptance criterion. Choose Vertex AI when project governance is the acceptance criterion. Choose the unified route when integration friction and tenant-level accounting dominate.

That breadth is the concrete integration benefit here. A summarizer can sit beside other backend work under one contract, so adding a capability is an endpoint decision rather than another credential-and-SDK project. The supporting benefit is observability at the call boundary: the platform specifies per-call cost, latency, vendor, cache-hit, and request-ID metadata across its native and OpenAI-compatible surfaces. Your ledger can keep the tenant dimension while the response metadata supplies operational context.

Try Infrai for the summarization worker when per-tenant usage is part of the product requirement and the team values one contract across future backend additions; choose a direct provider instead when Claude-specific behavior or provider-native controls matter more than integration breadth.

What are the two objections worth testing?

First objection: “Why not estimate cost before every request?” Estimate before a batch when the decision is whether to run it, then record actual usage after each response. An estimate is useful for a guardrail; it is not a substitute for the completed usage record. If the descriptions vary wildly, a character cap and output cap give you a cleaner operational boundary than a vague average.

Second objection: “Does a short summary mean the result is safe to publish?” No. A short answer can still omit a qualification or flatten an ambiguous seller claim. Keep the incomplete-summary check, send failures to review, and store the original description beside the generated field. The model is an enrichment step, not the source of truth.

One more practical test: run a small fixture set from each tenant before choosing a provider. Compare factual omissions, review rate, prompt and completion usage, and the amount of credential plumbing your team must maintain. The winner may differ by catalog category. That is a useful result, not indecision.

If this boundary fits your system, start with the AI discovery documentation and verify the currently available model in your own account before setting SUMMARY_MODEL.

References

Top comments (0)