Skip to main content
If you’re migrating from OpenAI, Anthropic, Gemini, or another provider, use one stable path to dottxt.

Shared migration pattern

  1. Point your client at dottxt (baseURL + dottxt API key).
  2. Send raw JSON Schema in response_format (avoid helper methods like parse that rewrite schemas).
This keeps the request shape explicit and avoids provider SDK transformations that can silently change optionality and constraints.

OpenAI-compatible example: change two lines

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": "Extract: John Smith <john@acme.com>, VP Engineering"}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                    "role": {"type": "string"}
                },
                "required": ["name", "email", "role"],
                "additionalProperties": False
            }
        }
    }
)

import json
contact = json.loads(response.choices[0].message.content)
Two changes:
  1. Set base_url and api_key to point at dottxt
  2. Swap the model name
Everything else, including response_format, messages, and response parsing, stays the same.

What changes when you switch

With OpenAI Structured Outputs, strict: true comes with a restricted JSON Schema subset. All object fields must be listed in required, objects must set additionalProperties: false, and some schema shapes are rejected outright. With dottxt, your schema is used as-is:
What you writeOpenAI behaviordottxt behavior
A field not in requiredNot allowed in strict modeField is genuinely optional
minLength: 3 on a stringSupportedEnforced during generation
pattern: "^[A-Z]{2}$"SupportedEnforced during generation
minimum: 0 on a numberSupportedEnforced during generation
anyOf at root levelRejectedSupported
if / then / elseRejectedSupported
Unsupported featureExplicit errorExplicit error
See the full provider comparison for details.

Code you can delete

When your provider does not enforce the schema you actually want, you compensate with application code. Here’s what that often looks like in practice: not the API call itself, but everything around it.
# Without full schema enforcement: the schema says minLength, pattern, optional fields...
# but your application still has to clean up and validate the result itself

import json
from datetime import datetime

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": text}],
    response_format={"type": "json_schema", "json_schema": { ... }}
)
result = json.loads(response.choices[0].message.content)

# Validate what the schema was supposed to enforce
if not result.get("vendor") or len(result["vendor"]) > 120:
    raise ValueError("vendor missing or too long")

# Normalize date to match the contract your application expects
raw_date = result.get("date", "")
for fmt in ("%Y-%m-%d", "%B %d, %Y", "%m/%d/%Y", "%b %d %Y"):
    try:
        result["date"] = datetime.strptime(raw_date, fmt).date().isoformat()
        break
    except ValueError:
        continue

# Normalize currency to the format your downstream code expects
currency = result.get("currency", "").upper().strip()
if currency in ("US DOLLARS", "USD$", "DOLLARS"):
    currency = "USD"
result["currency"] = currency

# Handle optional fields yourself when the response contract is not enforced directly
if result.get("notes") in ("", "N/A", "None", "n/a"):
    result["notes"] = None

# Retry if the output still doesn't conform
if not validate(result):
    # try again, hope for better luck
    response = client.chat.completions.create(...)
With dottxt, the same task:
import json
import os
from openai import OpenAI
from pydantic import BaseModel, Field

class Invoice(BaseModel):
    vendor: str = Field(min_length=1, max_length=120)
    date: str = Field(json_schema_extra={"format": "date"})
    currency: str = Field(pattern=r"^[A-Z]{3}$")
    line_items: list[dict] = Field(min_length=1, max_length=50)
    total: float = Field(ge=0)
    notes: str | None = Field(default=None, max_length=300)

client = OpenAI(
    base_url="https://api.dottxt.ai/v1",
    api_key=os.environ["DOTTXT_API_KEY"],
)

response = client.chat.completions.create(
    model="openai/gpt-oss-20b",
    messages=[{"role": "user", "content": text}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "invoice",
            "schema": Invoice.model_json_schema(),
        },
    },
)
invoice = Invoice.model_validate_json(response.choices[0].message.content)
# invoice.date is already "2026-02-12"
# invoice.currency is already "USD"
# invoice.notes is None when the source text doesn't contain any
# no validation, no normalization, no retry
The validation code, the date normalization, the currency cleanup, the empty-string-to-None conversion, and the retry loop all exist because the schema wasn’t enforced. Delete the workarounds, keep the schema.

Richer schemas that now work

Once you’re on dottxt, you can use JSON Schema patterns that OpenAI’s strict mode rejects:
response = client.chat.completions.create(
    model="openai/gpt-oss-20b",
    messages=[
        {"role": "user", "content": "Extract: John Smith <john@acme.com>, VP Engineering"}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "minLength": 1
                    },
                    "email": {
                        "type": "string",
                        "pattern": "^[^@]+@[^@]+$"
                    },
                    "role": {
                        "type": "string"
                    },
                    "tags": {
                        "type": "array",
                        "items": {"type": "string"},
                        "minItems": 1,
                        "maxItems": 5
                    }
                },
                "required": ["name", "email"],
                "additionalProperties": false
            }
        }
    }
)
name is always non-empty. email matches the pattern. tags has 1–5 items. role is optional; it may or may not appear in the output. None of this works with OpenAI’s strict mode.

TypeScript

The same approach works with any OpenAI-compatible TypeScript client:
import OpenAI from "openai";

const client = new OpenAI();

const response = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [
    { role: "user", content: "Extract: John Smith <john@acme.com>, VP Engineering" }
  ],
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "contact",
      strict: true,
      schema: {
        type: "object",
        properties: {
          name: { type: "string" },
          email: { type: "string" },
          role: { type: "string" }
        },
        required: ["name", "email", "role"],
        additionalProperties: false
      }
    }
  }
});

const contact = JSON.parse(response.choices[0].message.content!);