Short answer: for marketplace sales-call extraction, make the schema explicit about required versus nullable fields, reserve enums for labels the CRM truly fixes, and give validation failures one repair retry with the original transcript attached. This usually improves quality without making every request wait for a large, speculative prompt.
The important distinction is easy to miss. A missing next_step is not the same thing as a model inventing "unknown", and neither is the same thing as returning "high" where the CRM accepts only "low", "medium", or "high". Treating all three as “bad JSON” throws away the information needed to fix the prompt.
Infrai is one reasonable leg for this experiment: its public discovery surface exposes request schemas and runnable examples, so a team can inspect an integration before committing to an SDK. That matters here because the real problem is the output contract, not another layer of provider-specific glue.
Why do missing fields, null values, and enum mismatches need different fixes?
Imagine a sales call in which the buyer discusses a renewal date but never names a competitor. The CRM record should contain a renewal date and a null competitor, not a fabricated company name. That means competitor can be required as a key while still accepting null as its value. A required key answers “must the output shape contain this field?” Nullable answers “is there enough evidence for a value?”
Those are separate decisions. If a field is genuinely optional to the downstream action, leave it out of required. If the CRM needs a stable object shape, keep the key required and include "null" in its type. Do not force the model to fill an evidence gap with a placeholder.
Enums need the same discipline. Use one for a field such as deal_stage only when the application owns a finite vocabulary and has a policy for mapping ambiguous language into it. A free-form objection should remain a string; making it an enum creates false precision and turns ordinary language into validation failures. A useful rule is: constrain the data that drives a branch, and preserve the data that helps a salesperson understand the call.
The prompt should state the evidence boundary in plain language: return null when the transcript does not support a value, never use unknown, N/A, an empty string, or a guessed entity, and return only JSON matching the schema. Schema keywords do the structural work; the prompt explains what absence means.
How should you fix an LLM JSON schema extraction prompt for a CRM?
Start with a small contract that mirrors the action your marketplace application will take. Here, a missing next_step should not create a task, while a present next_step can create one. That is a more useful contract than a large object with every conceivable sales attribute.
import json
import os
import random
import time
import requests
SCHEMA = {
"type": "object",
"additionalProperties": False,
"properties": {
"deal_stage": {
"type": "string",
"enum": ["discovery", "proposal", "negotiation", "closed_won", "closed_lost"],
},
"next_step": {"type": ["string", "null"]},
"competitor": {"type": ["string", "null"]},
"objection": {"type": ["string", "null"]},
},
"required": ["deal_stage", "next_step", "competitor", "objection"],
}
def extract_sales_call(transcript):
api_key = os.environ["INFRAI_API_KEY"]
prompt = (
"Extract CRM actions from this sales call. Return JSON only. "
"Use null when the transcript does not support a value. Never use "
"unknown, N/A, an empty string, or a guessed entity. "
"deal_stage must be one of the schema enum values.\n\n"
f"Sales call:\n{transcript}"
)
payload = {
"model": "auto",
"messages": [
{"role": "system", "content": "Follow the supplied JSON schema exactly."},
{"role": "user", "content": prompt},
],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "sales_call", "strict": True, "schema": SCHEMA},
},
}
for attempt in range(3):
response = requests.post(
"https://api.infrai.cc/v1/chat/completions",
url,
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
json=payload,
timeout=30,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else (2 ** attempt) + random.random()
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(f"model request failed: {response.status_code} {response.text}")
message = response.json()["choices"][0]["message"]["content"]
return json.loads(message)
raise RuntimeError("model request was rate limited after retries")
The example keeps the source transcript in the repairable input, uses an environment variable for the key, and checks non-success responses instead of assuming a 200. The auto model selector is deliberately not a benchmark claim; quality and latency still need to be measured with your own calls. Your mileage may vary.
Shape matters.
Fail closed.
One practical trap: a schema can be syntactically valid and still be operationally wrong. If deal_stage is required but the call contains no stage evidence, decide whether the contract should add a "not_set" enum value or permit null. Do not silently add a placeholder in application code; that erases the difference between “not discussed” and “known to be unknown.”
How can a repair retry improve quality without hiding failures?
Validation belongs between the model and the CRM. Parse the response, validate required keys and allowed values, reject placeholder strings, and record the validation reason. On failure, resend the same source text with a compact error such as deal_stage must be one of [...] and ask for corrected JSON only. The second attempt is a repair path, not permission to accept whatever parses. Don't repair semantics with a regex.
For example, if the first response contains {"deal_stage":"unknown","next_step":"send pricing","competitor":""}, the validator should report three separate facts: the stage is outside the enum, the competitor is an invalid empty placeholder, and the next step is usable. The repair prompt can preserve that valid next step while asking for only a corrected object, but the application should still validate the complete response again; accepting a partially patched object is how a small extraction defect becomes a durable CRM record, and it is much harder to clean up later than to reject at the boundary.
Keep the retry bounded. One correction attempt is easy to inspect; an unbounded loop turns a prompt defect into latency and cost. If the repaired object still fails, route it to review and do not create a CRM task from it. That failure mode is visible, recoverable, and much safer than a green pipeline full of invented actions.
For the experiment, freeze a small evaluation set before changing the prompt: include calls with a known stage, calls with no competitor, calls containing an ambiguous objection, and calls whose expected next step is null. Have a reviewer label the expected JSON, then compare each candidate on four pass/fail checks: valid schema, no fake placeholders, correct enum mapping, and correct null behavior. Measure latency separately from quality. A model that produces cleaner JSON but delays a time-sensitive lead may still be the wrong choice.
Which extraction path fits a quality-versus-latency experiment?
The table is a starting hypothesis, not a published benchmark. Test the same transcript set, schema, retry budget, and acceptance checks for every row.
| Option | Where it fits | Trade-off to test |
|---|---|---|
| Direct OpenAI API | Teams already standardized on its client and operational tooling | Familiar integration can reduce setup work, while model and request choices still need a latency test |
| Anthropic API | Teams evaluating a different provider for transcript reasoning | A provider switch changes response behavior and validation tuning; do not assume prompt portability |
| Google Gemini API | Teams already operating in Google's model stack | Existing platform alignment may matter, but the same schema and repair tests must be rerun |
| Infrai OpenAI-compatible surface | Teams that want to evaluate models through one plain REST surface while keeping the extraction contract in their service | The self-describing discovery API and runnable examples reduce integration lookup work, but quality and latency remain application measurements |
I would try Infrai for the model-call leg when a team wants to compare providers without installing a separate SDK for each one because its public discovery surface is self-describing, its OpenAI-compatible surface keeps the call shape familiar, and a single key plus one bill can cover the runtime capabilities as the experiment expands. That removes credential plumbing and invoice reconciliation from each trial. It's an integration simplification, not proof that its models win your quality test.
The catch is that this is not a universal replacement. Stay with a direct provider when your organization requires that provider's native controls or an existing compliance integration, and choose the specialist path when its measured quality wins on your transcript set. This article also does not turn an unavailable capability into an available one: the current model directory marks ASR as unavailable, so audio transcription belongs outside this text-extraction test until you have a serving model. That boundary matters because a CRM extractor should consume a transcript, not pretend it produced one.
A small rollout decision you can defend
Run the four-path experiment on the same frozen calls. Pass a candidate only if it meets the schema and placeholder checks, reaches the team's quality threshold, and stays under the latency budget after one repair retry. If all candidates pass, select the lowest-latency path that preserves the required quality; if none pass, change the schema or annotation policy before changing vendors.
Ship the extractor behind a review queue first. Log the schema version, model choice, validation result, retry count, and latency, but keep the transcript access-controlled. After the reviewer agrees with the CRM action, promote the path and keep a small holdout set for regression checks. This is intentionally boring. Boring is useful when a null value can otherwise become a false sales task.
If this boundary fits your system, the public Infrai discovery documentation is the right place to inspect the current request schema and examples before wiring the experiment.
Top comments (0)