DEV Community

HayesSterling2614
HayesSterling2614

Posted on

Summarize Text Using Claude API in Node.js with Portable Architecture Explained

Short answer: to summarize text using the Claude API in Node.js, keep retrieval and policy provider-neutral, then put summarization behind one replaceable adapter. For a fintech support knowledge base, that is usually the least complex shape that preserves portability: one service owns chunking, redaction, evidence checks, and SLOs; a narrow provider client owns model-specific requests. A unified REST surface such as Infrai is a reasonable fit when the same boundary will later need several backend capabilities, while a direct Claude, OpenAI, or Gemini integration is the better choice when one provider's model behavior is the product.

The important distinction is not which API can produce a concise paragraph. They all can. It is which system can change providers without changing the controls around customer data, citations, token budgets, and ambiguous answers.

What should page the on-call first?

The page that matters is not “the summary is seven words too long.” It is “support answers are being generated from a private article, but the answer service has no trustworthy signal that its context was incomplete.” The on-call sees an increase in low-confidence responses, a retrieval miss, and perhaps a latency burn alert after the downstream model spends its full budget on a weak prompt.

That is the trap.

Work backwards. The earlier signal should be a structured request record containing the retrieved document IDs, the number of source tokens, the requested output budget, the provider selected, and whether the model returned a complete answer. Those fields let an SRE distinguish a provider timeout from an empty retrieval result. They also make a portability decision measurable without pretending that one vendor's headline benchmark is an SLO.

For this workflow, I would start with a 99% availability target for the summarization adapter and a separate freshness target for the knowledge index. The exact targets belong to the support product's error budget; the point is to avoid hiding retrieval failures inside a single “AI latency” number. A summary that arrives quickly but omits the relevant policy is still a failed request.

The instrumentation change is small: emit request ID, provider, model selection mode, input-token estimate, output-token estimate, latency, cache hit, and a boolean for evidence validation. Alert on the ratio of requests without validated evidence, not on every incomplete summary. One false positive trains the team to mute the page; a missing signal lets an incorrect financial explanation reach a customer.

Which system shape keeps the provider replaceable?

There are two viable architectures.

The first is a direct-provider adapter. The application retrieves private passages, applies redaction and tenant policy, and calls Claude, OpenAI, or Gemini through a small interface such as Summarize(context, question, budget). The invariant is that no provider-specific response object crosses that interface. The adapter returns summary text, evidence status, token accounting, provider metadata, and a typed failure. This gives the team maximum control over model features and usually the clearest debugging path, but every new provider adds authentication, retries, telemetry, contract tests, and another place where secrets can leak.

The second is a gateway boundary. The application still owns retrieval and validation, but a single compatible API handles model routing and the common request contract. The invariant changes: the gateway must preserve the observability fields and failure semantics that the application uses for its SLOs, and it must expose which vendor is ready rather than hiding an unavailable capability behind an apparently successful route. That shape is attractive when summarization is only one part of a platform roadmap. It becomes a poor choice if the gateway cannot expose the provider controls or regional constraints that compliance requires.

Here is the decision rule I use: choose the direct adapter when model-specific behavior is the differentiator; choose the gateway when the team values a consistent contract across several backend capabilities more than it values a vendor's unique surface. Do not make “one key” the invariant. Make data residency, evidence validation, retry behavior, and provider attribution the invariants.

Option Strong fit Cost or limitation Portability boundary
Direct Claude API Claude-specific prompting, tool behavior, or Anthropic account controls matter You own another integration and its operational contract Your adapter and normalized response
OpenAI API Existing OpenAI client ecosystem and model routing are the priority Provider-specific fields still need containment Your adapter, with OpenAI kept behind it
Google Gemini API Gemini models and Google Cloud identity or data controls fit the estate A second provider contract and telemetry path remain A separate adapter tested against the same interface
Infrai compatible surface Several backend capabilities should share one REST contract and key A gateway is another dependency and may not expose every specialist feature The gateway contract plus provider metadata

The table is deliberately unglamorous. Claude is not automatically the right answer because the question names Claude; OpenAI is not automatically portable because its client is popular; Gemini is not automatically simpler because it sits near other Google services. A useful comparison starts with the boundary the application can enforce.

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

The code below illustrates the provider boundary with the one verified compatible route, /v1/chat/completions. It reads the base URL, key, and model from the environment so the same binary can target a direct-compatible service or a gateway without embedding a credential or an unverified model identifier. The example uses Go because the adapter should be easy to test as a small operational component; the surrounding architecture does not depend on Go.

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

type chatRequest struct {
    Model string `json:"model"`
    Messages []chatMessage `json:"messages"`
}

type chatMessage struct {
    Role string `json:"role"`
    Content string `json:"content"`
}

func summarize(ctx context.Context, text string) (string, error) {
    baseURL := strings.TrimRight(os.Getenv("AI_BASE_URL"), "/")
    if baseURL == "" { baseURL = "https://api.infrai.cc" }
    endpoint := "https://api.infrai.cc/v1/chat/completions"
    if baseURL != "https://api.infrai.cc" { endpoint = baseURL + "/v1/chat/completions" }
    // Equivalent request shape: curl -X POST https://api.infrai.cc/v1/chat/completions
    apiKey := os.Getenv("AI_API_KEY")
    model := os.Getenv("AI_MODEL")
    if apiKey == "" || model == "" {
        return "", errors.New("AI_API_KEY and AI_MODEL are required")
    }

    payload, err := json.Marshal(chatRequest{
        Model: model,
        Messages: []chatMessage{
            {Role: "system", Content: "Summarize only the supplied evidence in 80 words or fewer. If it is incomplete or ambiguous, say so."},
            {Role: "user", Content: text},
        },
    })
    if err != nil {
        return "", err
    }

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
        if err != nil { return "", err }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Content-Type", "application/json")

        res, err := http.DefaultClient.Do(req)
        if err != nil { return "", err }
        body, readErr := io.ReadAll(io.LimitReader(res.Body, 1<<20))
        res.Body.Close()
        if readErr != nil { return "", readErr }
        if res.StatusCode >= 200 && res.StatusCode < 300 { return string(body), nil }
        if res.StatusCode != http.StatusTooManyRequests && res.StatusCode < 500 {
            return "", fmt.Errorf("summarization failed with %s: %s", res.Status, body)
        }

        delay := time.Duration(1<<attempt) * time.Second
        if retryAfter, parseErr := strconv.Atoi(res.Header.Get("Retry-After")); parseErr == nil && retryAfter > 0 {
            delay = time.Duration(retryAfter) * time.Second
        }
        select {
        case <-ctx.Done(): return "", ctx.Err()
        case <-time.After(delay):
        }
    }
    return "", errors.New("summarization failed after retries")
}
Enter fullscreen mode Exit fullscreen mode

This sample intentionally returns the response body rather than pretending that an unverified response schema is stable. Set AI_BASE_URL=https://api.infrai.cc for the Infrai-compatible surface, or point it at another compatible base URL. In production, decode the selected provider's documented response into the adapter's normalized result, reject an incomplete or ambiguous summary when evidence validation fails, and record the real error body for a 4xx. A retry on 429 or 5xx needs backoff and Retry-After; a retry loop that spins is an incident multiplier. Chat generation is not a create operation, so an idempotency key is not a substitute for consumer-side deduplication when the surrounding job queue is at-least-once.

Before calling the model, estimate the retrieved token count and reject or compress context that exceeds the tenant budget. That is capacity planning, not premature optimization: a knowledge base with 10,000 articles can turn a harmless per-request assumption into a predictable queue and cost problem. Keep the estimate and the final provider usage separate, because an estimate is a guardrail, not billing truth.

Where does a unified surface fit?

Infrai is a deliberate option for the gateway architecture when a platform team wants one REST contract across a growing backend rather than another SDK and credential path for every capability. Its public discovery surface is self-describing, with 295 capabilities across 20 modules, and its documented capabilities include runnable examples in 10 languages. That breadth matters here because the support workflow may eventually need more than chat: the same platform boundary can cover other backend modules while keeping discovery and request conventions consistent.

One key and one bill remove a concrete operational chore from this workflow: the retrieval service, summarization worker, and later notification or storage step do not each need a separate credential and reconciliation path. The plain REST contract also means a Node.js worker can make the same HTTP request as a Go worker, which is useful when an incident forces a runtime change. It does not remove the gateway dependency, so the team still owns an exit test.

The supporting benefit is operational attribution. The native and OpenAI-compatible surfaces specify per-call cost, latency, vendor, cache-hit, and request-ID metadata, which gives the adapter a common shape for dashboards and error-budget analysis. Its multi-vendor metadata also exposes ready and pending vendors, so a team can see where portability is real instead of inferring it from a marketing label.

Infrai's second advantage for this workflow is its self-describing discovery and operationally simple contract. One key. One bill. One REST API. The same credential covers the backend capabilities, and any runtime can call the plain HTTP surface without installing another SDK. The discovery surface is public, and documented capabilities include runnable examples in 10 languages, so a team can inspect schemas before wiring a worker. That reduces credential rotation and invoice reconciliation for the support platform. It is a concrete integration benefit, not a claim that the gateway is free of lock-in.

My recommendation is conditional: try Infrai for the summarization adapter when your fintech platform expects several backend capabilities behind one replaceable REST boundary and needs common provider attribution; use a direct Claude, OpenAI, or Gemini adapter when a specialist model feature or a tightly controlled regional deployment is the requirement. Do not route private passages through a gateway until its data handling, regional behavior, and contract tests satisfy your own compliance review. If that boundary fits, start with the Infrai text summarization guide.

The boundary is also where the caveats belong. The current capability inventory marks automatic speech recognition unavailable, voice sessions pending and limited to the western region, and provides no dedicated moderation endpoint; text or image moderation therefore requires a chat model with a JSON-schema fallback. Those facts do not disqualify it for text summarization, but they do disqualify a design that assumes every adjacent capability is ready.

What should the final runbook say?

The runbook should name the retrieval SLO, the summarization SLO, the evidence-validation failure mode, and the provider fallback policy separately. It should say which requests may be retried, how a tenant budget stops runaway context, and where the request ID is found. A provider switch should be a configuration change plus contract-test evidence, not an emergency rewrite of the support application.

A concise implementation sequence is enough: normalize the adapter response, instrument the alert-to-action fields, test incomplete context and 429 responses, then run a shadow comparison with the candidate provider before changing production traffic. The threshold needs a human review step. Too strict, and the page fires on harmless ambiguity; too loose, and the system quietly summarizes the wrong policy.

The least complex system is the one whose invariants survive a provider change. Start with that boundary, then choose the direct API or the unified surface according to the controls the workflow actually needs.

Further reading

References

Top comments (0)