AISchemaGate

LLM SDK Reference — LLMGate

LLMGate is a thin wrapper over the shipped POST /egress/{contract_id} endpoint for validating LLM structured output. It extracts the JSON object from the common provider response shapes, calls egress against your deployed contract, and optionally runs a bounded re-prompt loop until the output passes or the retry budget exhausts.

contract_id (and prompt_contract_id) take either the contract's UUID or its YAML name. A name is looked up once on the API key's contract list (GET /contracts) and cached for the life of the gate; an unknown name raises an error listing the names on the key.

Design: see RFC-094. Egress semantics (dispositions, PII transforms, audit writes): see egress-validation-reference.md.


Install

LanguageCore SDKAISchemaGate alias
Pythonpip install "contractgate>=0.3.0"pip install aischemagate
TypeScript / Node ≥ 20npm install @nightmoose/contractgate-sdk@^0.3.0npm install @nightmoose/aischemagate

The alias package re-exports the same classes under the aischemagate / @nightmoose/aischemagate import name. Pick whichever fits your mental model; the behavior is identical.

# Both of these import the same object.
from contractgate.llm import LLMGate
from aischemagate       import LLMGate
// Both of these import the same object.
import { LLMGate } from '@nightmoose/contractgate-sdk/llm';
import { LLMGate } from '@nightmoose/aischemagate';

Constructor

The caller brings their own LLM client. The SDK takes no runtime dependency on openai / anthropic / any provider. vLLM and OpenAI-compatible Ollama endpoints work with the OpenAI-shaped .completion() call unchanged.

Python

from contractgate.llm import LLMGate
from openai import OpenAI

gate = LLMGate(
    api_key="cg_live_...",            # required — same key as the dashboard
    contract_id="extract_invoice_v1", # required — contract name or UUID
    llm=OpenAI(),                     # required — caller-bound client
    base_url="https://app.datacontractgate.com",  # default
    version=None,                     # pin to a specific contract version
    disposition=None,                 # None → gateway default (block)
    max_retries=2,                    # default 2; hard cap 5 (warns + clamps)
    repair_prompt=None,               # callable(violations) -> str; default below
    client=None,                      # inject an existing contractgate.Client
)

AsyncLLMGate has the identical constructor, except llm is an async-aware client (openai.AsyncOpenAI, anthropic.AsyncAnthropic, …) and client takes an AsyncClient.

TypeScript

import { LLMGate } from '@nightmoose/contractgate-sdk/llm';
import OpenAI from 'openai';

const gate = new LLMGate({
  apiKey: 'cg_live_...',           // required
  contractId: 'extract_invoice_v1',// required — contract name or UUID
  llm: new OpenAI(),               // required
  baseUrl: 'https://app.datacontractgate.com',  // default
  version: undefined,
  disposition: undefined,          // undefined → gateway default (block)
  maxRetries: 2,                   // hard cap 5
  repairPrompt: undefined,         // (violations) => string
  client: undefined,               // inject an existing Client
});

All TS methods are async. There is only one TS class (no sync/async split) because the TS Client is itself async-only.

Methods

completion — OpenAI protocol

Calls llm.chat.completions.create({...}) under the hood. Works for OpenAI, vLLM, and Ollama OpenAI-compat endpoints.

result = gate.completion(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Extract the invoice JSON..."}],
    auto_repair=True,
    response_format={"type": "json_object"},   # forwarded as-is
    temperature=0.1,                           # forwarded as-is
)
const result = await gate.completion({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: 'Extract the invoice JSON...' }],
  autoRepair: true,
  response_format: { type: 'json_object' },
  temperature: 0.1,
});

message — Anthropic protocol

Calls llm.messages.create({...}) under the hood. Caller supplies max_tokens (default 1024).

result = gate.message(
    model="claude-sonnet-4-5",
    messages=[{"role": "user", "content": "Extract the invoice JSON..."}],
    max_tokens=1024,
    auto_repair=True,
    tools=[{...}],                 # forwarded as-is
)
const result = await gate.message({
  model: 'claude-sonnet-4-5',
  messages: [{ role: 'user', content: 'Extract the invoice JSON...' }],
  maxTokens: 1024,
  autoRepair: true,
  tools: [{...}],
});

validate_only / validateOnly — bring your own response

Escape hatch for callers who already have the provider response (e.g. a LangChain / CrewAI chain output, or an MCP agent tool-call result). Extracts the JSON and runs egress; no provider call.

chain_output = await chain.ainvoke({"input": "..."})
result = gate.validate_only(chain_output, dry_run=False)
const chainOutput = await chain.invoke({ input: '...' });
const result = await gate.validateOnly(chainOutput, { dryRun: false });

mask_prompt / maskPrompt — RFC-004 prompt-side masking

Returns the RFC-004-transformed form of a prompt dict. One-line convenience over client.ingest(dry_run=True). Format your prompt template from the returned dict; never send the raw dict to the LLM.

clean = gate.mask_prompt(
    {"email": "alice@example.com", "ssn": "123-45-6789", "message": "..."},
    prompt_contract_id="my_prompt_shape_v1",
)
# clean == {"email": "hmac-sha256:...", "ssn": "<REDACTED>", "message": "..."}
# depending on the prompt contract's declared transforms (RFC-004).
const clean = await gate.maskPrompt(
  { email: 'alice@example.com', ssn: '123-45-6789', message: '...' },
  { promptContractId: 'my_prompt_shape_v1' },
);

Return shape — LLMGateResult

FieldPythonTypeScriptMeaning
success flagok: boolok: booleanTrue iff egress returned failed == 0.
sanitized payloadpayload: dictpayload: Record<string, unknown>The egress-sanitized object. Forward this downstream — never raw_response.
violationsviolations: list[Violation]violations: Violation[]Empty on success; otherwise the gateway's per-field violations.
attemptsattempts: intattempts: number1 on happy path; up to max_retries + 1 under auto_repair.
raw responseraw_response: AnyrawResponse: unknownThe final provider response object. For debugging only.
repair hintsrepair_hints: list[str]repairHints: string[]Hints the SDK appended during auto-repair. Empty on first-try pass.

Never forward raw_response.content to a downstream system after a block or fail disposition — the sanitized payload is the only safe object. See docs/egress-validation-reference.md.

Auto-repair

When auto_repair=True and egress returns failed > 0, the SDK:

  1. Formats a repair hint from the violation list (default formatter below; override with repair_prompt).
  2. Appends the hint as a new user message. The system message is preserved; the hint is never injected into the system role.
  3. Re-invokes the provider.
  4. Repeats until egress passes or max_retries + 1 provider calls have been made.

Budget: default max_retries = 2 → up to 3 provider calls total. Hard cap MAX_RETRIES_CAP = 5. Setting max_retries > 5 emits a warning and clamps.

Default hint:

The previous JSON was rejected by schema validation. Fix the violations
below and return ONLY the corrected JSON object. No prose, no markdown
fence.
- field 'amount': Field 'amount' value -3 is below minimum 0
- field 'event_type': Field 'event_type' value "purchse" not in allowed set: ["click","view","purchase","login"]

Override:

def my_hint(violations):
    return "Please fix: " + "; ".join(v.field for v in violations)

gate = LLMGate(..., repair_prompt=my_hint)
const gate = new LLMGate({
  ...,
  repairPrompt: (violations) =>
    'Please fix: ' + violations.map((v) => v.field).join('; '),
});

Exhausted budget: result.ok == False and result.attempts == max_retries + 1. Do not forward result.payload to production downstream systems in this case — surface to the human / log the failure.

Response-shape extraction (extract_json)

LLMGate calls this internally. It is public for callers who want to plug extraction into their own pipeline.

Supported shapes, in priority order:

#ShapeExample key path
1OpenAI chat tool-callresponse.choices[0].message.tool_calls[0].function.arguments
2OpenAI chat JSON moderesponse.choices[0].message.content
3Anthropic Messages tool_usefirst response.content[*] with type == "tool_use" → .input
4Anthropic Messages text (fallback)first response.content[*] with type == "text" → JSON.parse(.text)
5OpenAI Responses APIresponse.output_parsed (dict passthrough)
6Raw stringJSON.parse(s), with markdown-fence tolerance
7Plain dict / objectpassthrough
from contractgate.llm import extract_json
payload = extract_json(resp)      # resp from any shape above
import { extractJson } from '@nightmoose/contractgate-sdk/llm';
const payload = extractJson(resp);

Markdown-fence tolerance: strings wrapped in json\n...\n or \n...\n are unwrapped before parsing.

Unknown shape → LLMResponseError with the detected top-level type.

Streaming responses (iterators with no .choices / .content) → LLMResponseError. Streaming is not supported in v1 (RFC-094 §Non-goals). Use a non-streaming call, or buffer the stream yourself and pass the accumulated text to validate_only.

Error handling

LLMResponseError

Subclass of ContractGateError. Raised when:

  • The response is a streaming iterator (not supported in v1).
  • The response shape is unrecognized.
  • The response string is not valid JSON.
  • The response JSON is not an object (array, number, string).

ValueError / TypeError

  • LLMGate(..., llm=None) → ValueError (Python) / TypeError (TS).
  • gate.completion(...) on an LLM client missing chat.completions.create → TypeError.
  • gate.message(...) on an LLM client missing messages.create → TypeError.

Egress HTTP errors

The underlying Client.egress call can raise any HTTPError subclass (AuthError, BadRequestError, NotFoundError, ValidationFailedError, ServerError). Catch them where you catch your other contractgate HTTP errors.

Non-goals (v1)

  • Streaming output validation. v1 rejects streaming responses. See RFC-094 §Non-goals.
  • "Eliminate hallucinations." The contract catches only violations the contract can express (type, range, enum, pattern, undeclared field under compliance mode, RFC-004 transforms). Semantic truth outside the schema is a boundary problem.
  • Calling the LLM provider for you. The caller owns the provider call; LLMGate only inspects the response and runs egress. See RFC-094 §Decisions Q1 → B.
  • First-class LangChain / CrewAI adapters. v1 ships cookbook snippets in docs/llm-output-validation.md (shipped under RFC-095). Dedicated adapter packages would be a follow-up RFC.

See also

  • RFC-094 — LLMGate design.
  • RFC-095 — the LLM output validation playbook that uses this SDK.
  • RFC-093 — egress as a first-class door; the engine behind LLMGate.
  • RFC-004 — PII transforms applied on both ingest (for mask_prompt) and egress.
  • egress-validation-reference.md — dispositions, audit writes, PII masking semantics.