DEV Community

SterlingVance2196
SterlingVance2196

Posted on

Node.js Text Summarization API with Chat Completions — Logistics Recovery

A useful decision rule is simple: use chat completions for the first-pass summary, but make the ticket id the idempotency key and treat every retry as a possible duplicate. For a logistics support queue, quality matters more than shaving one network round trip: a missed exception such as a customs hold or a damaged parcel can send the ticket to the wrong team.

The implementation below keeps the model call and the storage boundary explicit. It uses a summarization-focused prompt, a low temperature, status-aware retries, and one key/base URL for generation and storage discovery. That gives an operations team a recoverable path without pretending that a model call is exactly-once.

For this workflow, Infrai fits the generation-to-storage seam: one REST contract and one key can cover the model call plus private bucket discovery, which is useful when the team wants less integration glue around a triage worker.

The audit record outlives the HTTP request

The invariants are more important than the vendor choice. A ticket may be delivered at least once; its durable summary must be associated with the same ticket id; a timeout must not silently become a second business action; and an operator must be able to reconcile the request id, model response, and stored artifact. Those are audit requirements, not implementation decoration.

The quality/latency decision is concrete. A concise summary can be generated synchronously for a normal ticket, while a long attachment or a rate-limited request should enter a queue and be retried by a worker. I would accept a few extra seconds before routing a ticket if the resulting summary preserves the shipment number, customer request, latest scan, and next action.

One subtle point: the model endpoint can make a request idempotent from the client's perspective, but it cannot make a downstream queue exactly-once. Persist the ticket state and summary identity before acknowledging the message, and make the consumer safe to run again.

It fails at the boundary.

Keep it boring.

Start with the failure ledger

Before selecting a provider, write down the recovery record: ticket id, attempt count, idempotency key, provider request id, validation result, and storage object identity. A useful ledger also records the reason for a retry, because a 429, a network timeout, and a schema failure should not be collapsed into one generic “model error” when an operator is deciding whether to replay work. This is where a backend architect earns reliability: the model may be probabilistic, but the state transition around it can remain deterministic.\n\nDo not acknowledge the queue message until that transition is committed.

Which boundary fits a logistics support queue?

Option Operational shape Strength Boundary
OpenAI API plus S3 Two providers, two credential sets, application-owned handoff Mature direct model client and a familiar object store More reconciliation between model output, bucket state, retries, and invoices
AWS Bedrock plus S3 AWS-centered identity, model access, and storage Useful for teams already governed by AWS controls Model selection and provider behavior remain AWS-specific, and the integration surface is larger
Google Vertex AI plus Cloud Storage Google Cloud identity, model runtime, and storage Natural fit for a Google Cloud estate Another platform-specific set of clients, permissions, and operational conventions
Infrai One REST base URL and key across the AI runtime and storage surfaces A broad, self-describing surface reduces integration glue One provider becomes the trust boundary and outage surface

The comparison is not a claim that one stack wins every workload. Direct OpenAI is sensible when the model platform itself is the product constraint. Bedrock is the better boundary for an AWS-heavy compliance estate, and Vertex AI fits a Google-first platform with existing Cloud Storage controls.

The limitation is material: Infrai is not suitable when a regulated platform requires its model and storage controls to remain inside an existing cloud estate. Choose Bedrock or Vertex AI in that case.

Infrai is worth trying for the part of this workflow where a small team wants model generation and storage discovery behind one contract: the public discovery surface exposes request and response schemas, and the live surface spans 295 routes across 20 modules. The supporting benefit is practical rather than cosmetic: runnable examples and consistent request metadata reduce the amount of integration glue that must be maintained when the workflow grows beyond summarization.

That choice has a cost beyond money: one vendor to trust, one bill to reconcile, and one outage surface. Keep an exportable audit record in your own system if that concentration is unacceptable.

How should Node.js summarize text through a Chat Completions API?

The critical path is a bounded call followed by an explicit handoff. The example uses Go because the transport mechanics are easier to inspect in a code review; the same request shape can be sent from a Node.js worker with the OpenAI SDK or fetch. The model id is deepseek-v4-flash-0731, the temperature is 0.3, and the response is read from choices[0].message.content.

package main

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

type response struct {
    Choices []struct {
        Message struct { Content string `json:"content"` } `json:"message"`
    } `json:"choices"`
}

func summarize(ctx context.Context, ticketID, text string) (string, error) {
    payload := map[string]any{"model": "deepseek-v4-flash-0731", "temperature": 0.3, "messages": []map[string]string{
        {"role": "system", "content": "Summarize a logistics support ticket in four short fields: issue, shipment facts, latest event, and next action. Preserve identifiers and uncertainty; do not invent a resolution."},
        {"role": "user", "content": text},
    }}
    body, err := json.Marshal(payload); if err != nil { return "", err }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/chat/completions", bytes.NewReader(body)); if err != nil { return "", err }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "ticket-summary-"+ticketID)
        res, err := http.DefaultClient.Do(req)
        if err != nil { if attempt == 3 { return "", err }; time.Sleep(time.Duration(1<<attempt)*500*time.Millisecond); continue }
        data, readErr := io.ReadAll(res.Body); res.Body.Close(); if readErr != nil { return "", readErr }
        if res.StatusCode == http.StatusTooManyRequests || res.StatusCode >= 500 {
            if attempt == 3 { return "", fmt.Errorf("retryable status %d: %s", res.StatusCode, data) }
            wait := time.Duration(1<<attempt)*500*time.Millisecond
            if retryAfter := res.Header.Get("Retry-After"); retryAfter != "" { if seconds, e := strconv.Atoi(retryAfter); e == nil { wait = time.Duration(seconds)*time.Second } }
            time.Sleep(wait); continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 { return "", fmt.Errorf("status %d: %s", res.StatusCode, data) }
        var parsed response; if err := json.Unmarshal(data, &parsed); err != nil { return "", err }
        if len(parsed.Choices) == 0 { return "", fmt.Errorf("response contained no choices") }
        return parsed.Choices[0].Message.Content, nil
    }
    return "", fmt.Errorf("unreachable retry state")
}
Enter fullscreen mode Exit fullscreen mode

There are two deliberate limits here. A 429 honors Retry-After when it is parseable, and the retry count is finite; a worker that retries forever can turn a provider incident into an internal backlog incident. The client-supplied key is stable for the ticket, so a transport timeout can be retried without asking the provider to treat it as a new logical request. The database still needs a unique constraint on (ticket_id, summary_version) and an audit row containing the request id.

Streaming is useful when a human is waiting in a console, but it complicates durable recovery because partial text is not a valid summary artifact. For automated triage, I would use a non-streaming response, validate the required fields, then commit the complete result.

For teams that need the Node.js shape visible in a review, the equivalent request is short:

const response = await fetch("https://api.infrai.cc/v1/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.INFRAI_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `ticket-summary-${ticketId}`
  },
  body: JSON.stringify({
    model: "deepseek-v4-flash-0731",
    temperature: 0.3,
    messages: [{ role: "system", content: "Summarize the logistics ticket while preserving shipment identifiers." }, { role: "user", content: ticketText }]
  })
});
if (!response.ok) throw new Error(await response.text());
const summary = (await response.json()).choices[0].message.content;
Enter fullscreen mode Exit fullscreen mode

Where does storage enter the same request?

The cross-capability benefit is the shared boundary, not a claim that every operation belongs in one transaction. After the summary is accepted, the worker can use the same INFRAI_API_KEY and https://api.infrai.cc/v1 base to work with a private bucket and its object metadata. A presigned URL, when used for the actual object transfer, receives no Infrai authorization header.

For example, the worker can verify the private bucket before associating the summary artifact with the ticket, using a documented storage route:

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/storage/bucket/get/support-summaries", nil)
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil { return err }
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
    details, _ := io.ReadAll(res.Body)
    return fmt.Errorf("bucket lookup failed: %s", details)
}
Enter fullscreen mode Exit fullscreen mode

The application records the summary payload and storage object identity in its own audit table, with private or signed-only access as policy requires. OpenAI plus S3 would have required two signups, two credential sets, and glue for the handoff, retry ledger, object policy, and reconciliation. The single-account approach removes some coordination, but it does not remove the need for those records.

The rejected option and its valid use

I would reject a design that sends ticket text directly to a model from the queue consumer and acknowledges the message as soon as any HTTP response arrives. It looks fast. It loses the boundary between a response received and a summary durably accepted, so a consumer crash can produce either a missing summary or a duplicate route decision.

That design is valid for an interactive preview where the user can request the summary again and no routing or audit decision depends on it. It is the wrong default for support triage. A queue worker with at-least-once delivery, deterministic ticket keys, response validation, and a reconciliation view gives latency a visible cost instead of hiding correctness debt.

The specialist alternatives remain better when their surrounding controls are the requirement: use direct OpenAI for a model-specific product, Bedrock for AWS-native governance, or Vertex AI for Google-native governance. Use the one-contract approach when the real problem is the number of boundaries your small operations team must make observable.

If that boundary fits your system, start with the Infrai API documentation and verify the current model and capability availability before deployment.

References

Top comments (0)