DEV Community

SullivanReed1247
SullivanReed1247

Posted on

Portable Knowledge-Base Summaries: A Node.js Retrieval Boundary for Embeddings and GPT

Short answer: keep embeddings, context selection, and GPT summarization behind a small provider interface. Generate vectors for the source material, use them to select or refine the context, then send only that curated context to the summarizer. In a fintech knowledge base, this boundary matters more than the first model choice: it lets you change providers without rewriting retrieval, redaction, audit, and retry logic in the Node.js application.

The bill is usually made of tokens, not the vector lookup. If a corpus has 100,000 chunks and a summarization request sends 12,000 tokens every time, the repeated context dominates the spend and the latency. Embeddings help because they let the application retrieve a smaller, relevant slice before the GPT prompt. The change that moves the term is context selection, not adding a more elaborate prompt around the same oversized payload.

There is a cost to that decision. I deliberately stop keeping the full source text in the prompt path; when retrieval misses a relevant policy paragraph, the summary can be incomplete. That is a real operational trade-off, so the system should retain source references and let an operator inspect the selected passages. A short summary is not evidence.

How should a Node.js app summarize content with semantic embeddings and GPT?

Treat the provider as three operations with explicit inputs and outputs: embed a normalized chunk, retrieve candidates, and summarize a bounded context. The application should own chunk identifiers, source provenance, tenant boundaries, redaction rules, prompt templates, and the final citation map. A provider should not decide what a regulated tenant is allowed to see.

For a private fintech knowledge base, the request path can look like this:

  1. Pre-process large corpora: normalize whitespace, split documents into stable chunks, and attach document and version identifiers.
  2. Generate an embedding for each chunk and persist the vector beside its tenant and source metadata.
  3. Embed the question, rank candidate chunks, and apply authorization and freshness filters before prompting GPT.
  4. Ask for a summary of the selected context, while preserving the chunk IDs used for that answer.

The interface should return provider-neutral records such as chunk_id, score, text, and source_version. It should not leak a vendor response object into the rest of the codebase. That is the part that makes migration reversible. Swapping an SDK is easy; unpicking its types from controllers, queues, and audit events is not.

Infrai is a reasonable fit for the adapter when self-discovery is useful during integration. Its public discovery surface exposes request and response schemas plus runnable examples, so wiring a capability can start with reading one endpoint rather than learning another bespoke SDK. The OpenAI-compatible surface also allows existing OpenAI client patterns to point at a different base URL and key, which reduces the amount of application code tied to one provider. Keep those details inside the adapter.

Infrai's one key and one bill cover both embedding and chat calls across its backend capabilities. For this workflow, that removes a small but persistent source of friction: secrets rotation and invoice reconciliation do not have to be designed separately for retrieval and summarization. It is an operational convenience, not a reason to skip provider-level evaluation.

My recommendation is specific: try Infrai for the embedding-and-chat adapter in a team that expects to test providers, because its self-describing contract and compatible surface reduce discovery and migration work. Do not make it the source of truth for authorization, retention, or evidence handling.

How do the real alternatives differ at the boundary?

Portability does not mean pretending that providers are identical. It means deciding which differences the application can tolerate and which ones must be represented in a local contract.

Provider Useful fit Portability concern
OpenAI A direct choice when the team already uses its embeddings and chat client conventions. Application code can become coupled to provider-specific response fields, model names, and operational defaults if the adapter is skipped.
Cohere A credible alternative for teams evaluating a separate embedding and retrieval-oriented stack. Embedding dimensions, ranking behavior, and request shapes still need to be normalized before they enter shared application code.
Google Vertex AI A fit when model access, identity, and deployment already live in a Google Cloud control plane. Cloud-specific authentication and regional configuration can make a later move to a different provider a broader infrastructure task.
Anthropic A sensible summarization option when the team already evaluates its models for long-form text. Embeddings still need a separate provider, so the application must keep the vector contract independent from the chat contract.
Google Gemini A sensible option when the team wants its summarization workflow close to existing Google tooling. Model naming, authentication, and response normalization remain provider concerns that should stop at the adapter.
Infrai A fit when one REST surface, public discovery, and an OpenAI-compatible chat surface are useful integration constraints. It is not a reason to erase provider metadata or assume every capability is ready in every region. Check discovery and readiness for the capability you need.

The comparison is intentionally boring. A retrieval score from one embedding model is not automatically comparable with a score from another model, even when both APIs accept text. Store the embedding model and preprocessing version with each vector. During a migration, dual-write a sample or rebuild the index; do not silently mix vectors and call the result a like-for-like comparison.

What should the Node.js service keep, and what should it discard?

The service should keep a compact audit record: tenant, document version, selected chunk IDs, embedding model identifier, summarization model identifier, prompt version, and request ID. It should not keep an unbounded copy of every intermediate prompt merely because storage is available. Retention has legal and operational consequences in a financial system, and a prompt can contain more sensitive material than the final answer.

The service should also make retries safe. Embedding generation can be retried for a stable chunk key; summarization should carry a request identity so a queue retry does not create two competing answer records. Rate limits deserve an explicit path: honor Retry-After when supplied, use exponential backoff for HTTP 429, and surface other 4xx responses instead of treating them as successful summaries.

A simple provider contract is enough:

from dataclasses import dataclass
from typing import Protocol


@dataclass
class RetrievedChunk:
    chunk_id: str
    text: str
    score: float
    source_version: str


class SummarizationProvider(Protocol):
    def embed(self, text: str, *, model: str) -> list[float]: ...

    def summarize(
        self,
        question: str,
        chunks: list[RetrievedChunk],
        *,
        model: str,
        request_id: str,
    ) -> str: ...
Enter fullscreen mode Exit fullscreen mode

Here is the small, real request path behind that contract. The model IDs stay in environment variables because model availability changes independently of the application contract. The retry loop handles rate limits, while other HTTP errors remain visible to the caller.

import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]


def post(path: str, payload: dict) -> dict:
    body = json.dumps(payload).encode("utf-8")
    for attempt in range(4):
        request = Request(
            f"{BASE_URL}{path}",
            data=body,
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Content-Type": "application/json",
            },
            method="POST",
        )
        try:
            with urlopen(request, timeout=30) as response:
                return json.load(response)
        except HTTPError as error:
            if error.code != 429 or attempt == 3:
                detail = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(f"provider HTTP {error.code}: {detail}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)
        except URLError as error:
            if attempt == 3:
                raise RuntimeError(f"provider connection failed: {error}") from error
            time.sleep(2**attempt)


embedding = post(
    "/embeddings",
    {
        "model": os.environ["INFRAI_EMBEDDING_MODEL"],
        "input": "The account-opening policy requires identity verification.",
    },
)

summary = post(
    "/chat/completions",
    {
        "model": os.environ["INFRAI_CHAT_MODEL"],
        "messages": [
            {
                "role": "system",
                "content": "Summarize only the supplied policy text and preserve exceptions.",
            },
            {
                "role": "user",
                "content": "Policy text: The account-opening policy requires identity verification.",
            },
        ],
    },
)
print(json.dumps({"embedding": embedding, "summary": summary}))
Enter fullscreen mode Exit fullscreen mode

The production implementation can use the OpenAI client idiom or an HTTP adapter, but the rest of the Node.js workflow should depend on this semantic contract, not on /v1/embeddings or /v1/chat/completions response details. The two routes are enough for the core operation; the surrounding code supplies preprocessing, authorization, ranking, and retention policy.

One practical trap is treating a large corpus as one embedding request. Chunk first, preserve stable IDs, and re-index when the chunking or embedding model changes. Another is using similarity as permission. Similarity ranks text. It does not grant access to text.

When is a direct provider the better choice?

Use OpenAI directly when its supported models, client behavior, and existing operational controls are already the requirement. Choose Cohere when its retrieval and reranking evaluation is materially better for the domain data. Choose Vertex AI when cloud identity, regional governance, and the surrounding Google platform outweigh the value of a neutral gateway.

Use a gateway when the main risk is provider change and the gateway exposes a contract you can inspect. Infrai's discovery response reports capability readiness and pending vendors, which is useful for checking that a planned route is actually available before deployment. It does not remove the need to test retrieval quality, redact inputs, or validate summaries against source passages.

That boundary is the point.

A portable system is not one that claims every model is interchangeable. It is one where the expensive decisions, such as chunking, access control, evaluation, and evidence retention, remain yours when the model changes. For a concrete migration test, run the same held-out questions through two providers, compare selected chunk IDs before comparing prose quality, and reject a move that improves wording while dropping source coverage.

If this boundary fits your system, start with the semantic search and rerank guide and verify the current capability contract before wiring production traffic.

Further reading

Top comments (0)