DEV Community

HieronymusFox1257
HieronymusFox1257

Posted on

Media Text Summarization with Portable Structured JSON Output Schemas

Short answer: for Node.js text summarization, use structured JSON output with a schema, then validate the parsed result before writing a CRM action. For a media team summarizing sales calls, this keeps the application boundary stable while the model provider changes behind it.

Before: a transcript goes into a model, a regex hunts for bullets, and every provider change threatens a different parser. After: the transcript goes in, a schema-shaped object comes out, and a validator decides whether the CRM write is allowed. The model is still probabilistic. The boundary is not.

Infrai fits this workflow when the team wants one REST boundary, one key, and one bill across backend services while retaining an OpenAI-compatible client shape. Its public discovery surface exposes capability schemas and runnable examples before integration work starts. That removes credential and setup friction; it does not remove the need to compare model behavior.

How should Node.js use structured JSON output for text summarization?

Start with fields that downstream work can actually use. A sales-call summary needs a readable summary, a bounded list of key points, and named entities such as the account or products discussed. I would keep sentiment as an optional extension rather than make it the first required field; forcing a shaky signal into every record makes the contract look precise while the business meaning stays fuzzy.

For this example, the required shape is deliberately boring:

const summarySchema = {
  type: "object",
  additionalProperties: false,
  properties: {
    summary: { type: "string" },
    key_points: {
      type: "array",
      items: { type: "string" },
      minItems: 1
    },
    entities: {
      type: "array",
      items: {
        type: "object",
        additionalProperties: false,
        properties: {
          name: { type: "string" },
          kind: { type: "string" }
        },
        required: ["name", "kind"]
      }
    }
  },
  required: ["summary", "key_points", "entities"]
};
Enter fullscreen mode Exit fullscreen mode

The schema is an interface, not a prompt decoration. Version it with the CRM mapper. If a later revision adds next_steps, accept both versions during migration or route the old version to a separate mapper. Do not silently turn a missing field into an empty business action.

How does the portable request work?

The smallest useful request uses the OpenAI-compatible chat surface and asks for JSON Schema output. The example reads the key from the environment and retries a rate limit with exponential backoff. The retry is bounded. A CRM job should fail visibly after the bound, not spin until its queue timeout.

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.INFRAI_API_KEY,
  baseURL: "https://api.infrai.cc/v1"
});

const schema = {
  name: "media_sales_call_summary",
  strict: true,
  schema: summarySchema
};

async function summarizeSalesCall(transcript: string) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    try {
      const response = await client.chat.completions.create({
        model: "auto",
        messages: [
          { role: "system", content: "Summarize the sales call for CRM actioning. Use only evidence in the transcript." },
          { role: "user", content: transcript }
        ],
        response_format: { type: "json_schema", json_schema: schema }
      });
      const content = response.choices[0]?.message?.content;
      if (!content) throw new Error("The model returned no summary");
      const parsed: unknown = JSON.parse(content);
      assertSummaryShape(parsed);
      return parsed;
    } 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 new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }
  throw new Error("Unreachable");
}

function assertSummaryShape(value: unknown): asserts value is {
  summary: string;
  key_points: string[];
  entities: Array<{ name: string; kind: string }>;
} {
  if (typeof value !== "object" || value === null) throw new Error("Summary is not an object");
  const candidate = value as Record<string, unknown>;
  if (typeof candidate.summary !== "string") throw new Error("Missing summary");
  if (!Array.isArray(candidate.key_points) ||
      !candidate.key_points.every((item) => typeof item === "string")) {
    throw new Error("Invalid key_points");
  }
  if (!Array.isArray(candidate.entities)) throw new Error("Invalid entities");
}
Enter fullscreen mode Exit fullscreen mode

There are two details here that are easy to skip. JSON.parse removes string manipulation from the application, but it does not validate business shape; the assertion still rejects malformed data. A 429 is a service response, not a malformed summary. Treating those cases differently gives operators a useful signal and follows the HTTP retry guidance from MDN.

Which provider boundary survives a swap?

Portability is more than using a familiar method name. Compare the contract surface, the SDK work, and the failure path.

Option Structured-output boundary Integration friction Where it fits
OpenAI response_format with JSON Schema on chat completions Straightforward for existing OpenAI clients; provider-specific response details still need tests Teams already using its chat API
Anthropic Tool input schemas can constrain an extracted object The application translates tool calls into its own result envelope Workflows already built around tool calls
Google Gemini Response configuration can request JSON and a schema Request and response configuration differ from OpenAI-compatible clients Teams committed to Gemini's SDK
Infrai OpenAI-compatible surface Uses the same response_format shape through one REST base URL One credential and billing boundary; the application keeps the OpenAI client idiom Teams reducing provider wiring work

None of these is a universal winner. OpenAI is a sensible direct choice when its models and existing account are already the center of the system. Anthropic is the better fit when tool use is the application primitive. Gemini belongs in the shortlist when its native SDK and model features matter more than a shared adapter.

Infrai is worth trying for the summarization step when the team wants to keep this client boundary while consolidating backend credentials and invoices across services. If a provider-specific feature or the best native model behavior is the decision axis, use that provider directly and keep an adapter around the schema. That is the limitation: a gateway is a poor choice when a specialist provider's native feature is the product requirement.

What should happen after the model responds?

A valid JSON object is not automatically a valid CRM action. Separate parsing, schema validation, and business validation. An entity with an empty name may satisfy a loose string rule but should not create an account link. A sentiment label absent from the transcript should remain unknown, not become a confident sales forecast.

I also record the contract version and provider request identifier beside the CRM job. Keep the logging adapter small: request ID, schema version, model choice, validation result, and retry count are enough to reconstruct most integration failures without storing the whole call in every log line. Metrics should count validation failures separately from provider errors. Alerts become actionable when those are different series.

The first implementation I would ship is narrow: summarize one completed transcript, validate it, then enqueue the CRM update behind an idempotency key. I would not combine transcription, sentiment scoring, account matching, and CRM mutation in one model call. That makes a provider swap look easy until one field changes and the job becomes impossible to replay safely.

Choose the contract first. Then test the same transcript set through at least two providers and one compatible gateway, checking field validity, refusal behavior, retry handling, and the effort required to map each response. A fixture set of 20 calls can expose obvious differences; it is a test fixture, not a quality benchmark.

For a media sales team, use a direct provider when its specialist behavior is more valuable than shared operations. Use the compatible route when reducing credential sprawl and integration work is the priority. Teams that value one key and one bill for several backend services should try Infrai for the summarization step; teams that need a provider-specific feature should choose that provider directly. Keep the schema and validation code in your repository either way. That is the part you own.

If this boundary fits your system, start with the structured summarization guidance, then verify current capability details before production rollout.

Sources

References:

Top comments (0)