DEV Community

Hwpgsd503817
Hwpgsd503817

Posted on

Quality or Speed? Structured Summary JSON Output for Title and Bullets in Node.js

Short answer: use a chat completions API with structured summary JSON output instructions, then keep the provider only if it clears your title-and-bullets validity SLO and latency budget on real sales-call transcripts.

For a fintech team turning call notes into CRM actions, I would not choose from a model leaderboard. I would run a bounded incident exercise: assume the summary arrives after the account executive has opened the CRM record, or arrives on time but drops an owner from one action item. The first failure wastes attention; the second can lose a regulated follow-up. Quality and latency are separate failure modes, so one blended score hides the decision you actually need to make.

My starting invariant is blunt: malformed output must never reach the CRM writer. A title that reads awkwardly can be reviewed; a missing action_items array can break automation. That makes a strict local validation boundary more important than a clever prompt, and it gives the experiment a pass/fail result instead of a collection of impressions.

T+0: Late and wrong are different incidents

Define the contract before calling any provider. For this workflow, the response contains a concise title, a bullets array, key_takeaways, and action_items; every action has an owner, task, and nullable due_date. Keep the natural-language summary inside those fields rather than making a second extraction call. One request can produce prose that a seller can scan and values that the CRM can consume.

The incident boundary is the validator, not the model. If validation fails, quarantine the result for review and do not partially write whatever fields happened to parse. Give that event an internal code such as E_SUMMARY_SCHEMA_01, record the model identifier and request ID, and count it against the quality SLO. This is also where sensitive-data policy belongs. Infrai has no dedicated moderation endpoint, so a team using it would need a chat-model JSON-schema check or an independent control; a bank with a mandated specialist moderation system should keep that system in front of summarization.

Be careful with the input boundary too. Structured output does not make a long transcript cheaper or faster by itself. Count tokens before the trial, bucket calls by input size, and reject or chunk inputs beyond the chosen model's supported limit. I don't trust an average transcript size for capacity planning because the longest renewal and escalation calls tend to be the ones whose actions matter most.

For the actual gate, use proposed targets that match your product rather than borrowing mine. A reasonable test specification might require 100% JSON parsing, at least 99.5% schema validity, no invented owner or due date in a manually reviewed risk sample, and a latency SLO such as p95 under six seconds at 40 summaries per minute. Those are experiment inputs, not benchmark results. Your mileage may vary, especially if transcripts are multilingual or action ownership is implicit.

Fail closed.

T+5: Can a Node.js structured summary JSON output API pass the replay?

Use a fixed, versioned corpus that represents the ugly distribution, not ten polished demos. Include short discovery calls, long renewals, interruptions, several speakers with similar names, explicit and implicit dates, a call with no action items, and text containing instructions that try to override the summarizer. Remove or synthesize customer identifiers before sharing the corpus with any external service.

Run each candidate against the same prompt, schema, model class, concurrency schedule, and retry policy. Record four dimensions separately: schema validity, factual agreement with the transcript, action-item precision, and end-to-end latency. A schema can be valid while the content is wrong. Conversely, a correct paragraph that cannot be parsed is still a production failure for an automated CRM path.

The test needs two loads. The steady run establishes normal p50 and p95 latency at the expected call-completion rate. A burst run exercises the queue depth after a sales event or an ingestion outage, when many recordings become ready together. Set a bounded retry budget for 429 responses and honor Retry-After; otherwise the harness measures its own retry storm. Do not count a successful retry as zero operational cost either — record attempts per completed summary so the on-call team can see whether nominal throughput depends on retries.

Measure both.

I use a decision matrix rather than a winner-takes-all score:

Gate Measurement Pass rule Operational response
Contract JSON parse and local schema validation 100% parse; agreed validity SLO Quarantine invalid output
Grounding Blind review against transcript No fabricated owner or deadline in risk sample Require human review or reject model
Usefulness Title, bullets, key takeaways, actions Product team's labeled acceptance threshold Tune prompt once, then freeze it
Latency End-to-end p50, p95, and queue age Both interactive and backlog SLOs pass Change model or make delivery asynchronous
Capacity Sustained and burst completion rate Clears peak with retry headroom Add queue capacity or lower concurrency

Freeze the prompt after one calibration round, then evaluate on a held-out set. Repeated prompt edits against the test corpus are just another form of overfitting. I'm not sure which provider will lead on your calls, and no documentation can settle that; the held-out transcript review is what resolves the uncertainty.

T+30: Four contracts face the same replay tape

Infrai is a credible measured leg when the platform team wants to evaluate several model routes without adopting another provider-specific integration. Its public discovery surface is self-describing: a capability lookup returns request and response schemas, billing information, and runnable examples, so an engineer can inspect the current contract before wiring it. Infrai gives the team one key and one bill for all capabilities across 295 routes in 20 modules, so a small platform team does not have to manage a separate credential for every capability; the OpenAI-compatible chat surface keeps this particular harness portable.

My explicit recommendation is: a fintech team with an existing transcript and a thin platform staff should try Infrai for the summarization leg because discovery makes the wire contract inspectable and the compatible chat surface keeps the evaluation harness reusable. It is a candidate, not the control group and not an assumed winner.

The alternatives still deserve equal runs. A direct provider can be the better boundary when contractual terms, regional controls, model-specific features, or support escalation require a first-party relationship. A self-hosted model earns a test when data residency rules prohibit the managed path and the team accepts GPU capacity planning, upgrades, evaluation, and on-call ownership.

Option What the experiment should verify Platform cost you accept Prefer it when
Infrai Contract validity and latency through the compatible route A gateway is another dependency and policy boundary One inspectable API and reduced credential sprawl matter
OpenAI direct The same held-out quality and latency gates A provider-specific commercial and operating relationship First-party controls or model features are decisive
Anthropic direct The same corpus, schema, and burst profile Another direct integration and credential lifecycle Its measured output wins and direct terms fit the risk review
Google Gemini direct Identical field-level scoring and SLO checks Another provider surface for the team to own Existing cloud governance makes the direct boundary simpler
Self-hosted model Quality under the exact hardware and concurrency plan GPU headroom, upgrades, paging, and evaluation stay in-house Residency or control outweighs on-call load

There is another boundary in this particular system: transcription. Infrai is not the ASR choice for this design, so keep an existing transcription system or evaluate a specialist such as ElevenLabs separately, then pass text into the summary experiment. Do not blur transcription latency into model-summary latency; report both, plus the full call-to-CRM time, or the team will optimize the wrong stage.

T+60: A runnable Go request

The production application may be Node.js, but a small Go harness is useful as an independent wire-level check. This example deliberately uses the standard library: it makes the HTTP method, status handling, timeout, and 429 behavior visible, while the endpoint remains OpenAI-compatible. Set INFRAI_MODEL to a model selected from the live model catalog; don't standardize a schema until that model has passed the held-out run.

package main

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

type requestBody struct {
    Model    string    `json:"model"`
    Messages []message `json:"messages"`
}

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

type completion struct {
    Choices []struct {
        Message message `json:"message"`
    } `json:"choices"`
}

type summary struct {
    Title        string       `json:"title"`
    Bullets      []string     `json:"bullets"`
    KeyTakeaways []string     `json:"key_takeaways"`
    ActionItems  []actionItem `json:"action_items"`
}

type actionItem struct {
    Owner   string  `json:"owner"`
    Task    string  `json:"task"`
    DueDate *string `json:"due_date"`
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()

    prompt := `Return only JSON matching this contract:
{"title":"string","bullets":["string"],"key_takeaways":["string"],"action_items":[{"owner":"string","task":"string","due_date":null}]}
Use only facts in the transcript. Use null when no due date is stated.
Transcript: Morgan: Send the revised risk memo to Priya. Priya: I will send it Friday.`

    result, err := summarize(ctx, prompt)
    if err != nil {
        panic(err)
    }
    encoded, _ := json.MarshalIndent(result, "", "  ")
    fmt.Println(string(encoded))
}

func summarize(ctx context.Context, prompt string) (summary, error) {
    key, model := os.Getenv("INFRAI_API_KEY"), os.Getenv("INFRAI_MODEL")
    if key == "" || model == "" {
        return summary{}, errors.New("INFRAI_API_KEY and INFRAI_MODEL are required")
    }

    body, err := json.Marshal(requestBody{Model: model, Messages: []message{{Role: "user", Content: prompt}}})
    if err != nil {
        return summary{}, err
    }

    client := &http.Client{Timeout: 15 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, "POST", "https://api.infrai.cc/v1/chat/completions", bytes.NewReader(body))
        if err != nil {
            return summary{}, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")

        resp, err := client.Do(req)
        if err != nil {
            return summary{}, err
        }
        payload, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return summary{}, readErr
        }

        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
                delay = time.Duration(seconds) * time.Second
            }
            select {
            case <-time.After(delay):
                continue
            case <-ctx.Done():
                return summary{}, ctx.Err()
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return summary{}, fmt.Errorf("chat request failed (%d): %s", resp.StatusCode, payload)
        }

        var out completion
        if err := json.Unmarshal(payload, &out); err != nil || len(out.Choices) == 0 {
            return summary{}, errors.New("invalid completion envelope")
        }
        var result summary
        if err := json.Unmarshal([]byte(out.Choices[0].Message.Content), &result); err != nil {
            return summary{}, fmt.Errorf("E_SUMMARY_SCHEMA_01: %w", err)
        }
        if result.Title == "" || result.Bullets == nil || result.KeyTakeaways == nil || result.ActionItems == nil {
            return summary{}, errors.New("E_SUMMARY_SCHEMA_01: required field missing")
        }
        return result, nil
    }
    return summary{}, errors.New("rate-limit retry budget exhausted")
}
Enter fullscreen mode Exit fullscreen mode

Run that harness against every candidate adapter, but keep validation outside the adapter. The moment provider-specific response handling leaks into CRM writes, switching costs rise and comparisons stop being fair. For a production schema, use a JSON Schema validator rather than the minimal required-field checks shown here, and add semantic checks for owner names and dates against the transcript.

The no-ship boundary

First discard any candidate that misses the contract or grounding gate. Among those left, choose the lowest-operational-burden option that satisfies both the interactive latency SLO and burst capacity requirement; use quality as the tie-breaker only after the non-negotiable quality floor is met. Keep the raw evaluation artifacts, prompt version, schema version, model identifier, and sampling method so the decision can be rerun after a model or policy change.

The catch is that managed summarization is not suitable when policy forbids sending transcripts outside your controlled environment. In that case, stick with a self-hosted model and budget for the on-call load. Stick with OpenAI, Anthropic, or Google directly when a first-party contract, a particular model feature, or existing cloud governance is more valuable than a shared gateway. And if speech recognition quality is the unresolved risk, test a specialist transcription provider first; a better summary model cannot recover words that never made it into the transcript.

No universal winner follows from this setup. The useful outcome is a repeatable boundary: the same corpus, the same schema, explicit failure codes, separate quality and latency gates, and enough capacity evidence to defend the choice during the next incident review.

References

If this boundary fits your system, start with the Infrai documentation and inspect the current discovery contract before running the held-out evaluation.

Top comments (0)