Autofix API

Fix invalid OpenUI Lang from any model with one API call.

The Autofix API repairs invalid OpenUI Lang after your model has generated it. Keep your existing model provider, inference setup, and agent framework. Send the conversation with the generation as the last assistant turn, and the API returns the repair result. You do not need to generate through Gateway's Chat Completions or Responses API.

Use Autofix for correction after generation, or Gateway for model access with in-built correction.

Endpoint: POST https://api.thesys.dev/v1/autofix

OpenAI compatible. The request and the answer use the OpenAI chat completion shape. Point an OpenAI SDK at the base URL https://api.thesys.dev/v1/autofix and it posts to /v1/autofix/chat/completions. Both paths take the same body and answer the same way.

Use the server package

Import createAutofix from @openuidev/server/openai for Chat Completions or @openuidev/server/vercel for the Vercel AI SDK. Use the spec from openui generate --spec for the same library as your renderer. Keep the API key on the server.

lib/autofix.ts
import { createAutofix } from "@openuidev/server/openai";
import library from "./openui.spec.json";

export const autofix = createAutofix({
  apiKey: process.env.THESYS_API_KEY!,
  library,
});
app/api/chat/route.ts
import OpenAI from "openai";
import { autofix } from "../../../lib/autofix";

const model = new OpenAI();

export async function POST(request: Request) {
  const { messages } = await request.json();
  const source = await model.chat.completions.create(
    { model: "openai/gpt-5.5", messages, stream: true },
    { signal: request.signal },
  );

  return autofix.completions
    .stream({ stream: source, messages, signal: request.signal })
    .toResponse();
}

OpenAI: return SSE for the frontend openAIAdapter().

Vercel: pass toUIMessageStream({ stream: result.stream }). toResponse() is the UI message SSE protocol used by useChat.

Non-streaming: generation is the completed OpenUI text. Optional messages is the conversation before that generation. Use completions.fix or ai.fix to match the import.

Consume the stream

Use either chunks or toResponse() once. result settles after that consumer finishes.

MemberPurpose
chunksNative SDK events, including any correction.
toResponse()SSE Response for the matching frontend.
resultSettled Autofix result. Persist content when present, not joined text.

result is null when Autofix did not run. A failed repair throws with code: "fix_failed". Use fix() on the same helper when you already have completed text.

Call the API directly

Parse first, and only call Autofix when the generation is invalid.

lib/detect.ts
import type { ParseResult } from "@openuidev/lang-core";

export function isValid({ root, meta }: ParseResult): boolean {
  return (
    !meta.incomplete &&
    meta.errors.length === 0 &&
    meta.unresolved.length === 0 &&
    meta.orphaned.length === 0 &&
    root !== null
  );
}

Judge the generation once, after the stream ends. While it streams, incomplete and unresolved are normal: the parser has not seen the rest yet.

server.ts
import { createStreamingParser } from "@openuidev/lang-core";
import OpenAI from "openai";
import { autofix } from "./lib/autofix";
import { isValid } from "./lib/detect";
import library from "./openui.spec.json";

const model = new OpenAI(); // any provider that returns OpenUI Lang

export async function generate(messages: OpenAI.ChatCompletionMessageParam[]) {
  const parser = createStreamingParser(library.schema, library.root);
  let generation = "";
  let result = parser.getResult();

  const stream = await model.chat.completions.create({ model: "gpt-5.5", messages, stream: true });
  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta?.content ?? "";
    if (!delta) continue;
    generation += delta;
    result = parser.push(delta);
    send(delta); // the browser renders as the text arrives
  }

  if (isValid(result)) return;

  const completion = await autofix(messages, generation);
  if (completion.fix_summary.status === "fix_failed") {
    sendFallback(completion.fix_summary.unfixed_errors);
  } else {
    replace(completion.choices[0].message.content); // the whole valid generation
  }
}

send, replace, and sendFallback stand for your own transport to the browser: forward a delta, swap the rendered generation for the fixed one, and show a fallback.

parse and push never throw. They take fenced or bare OpenUI Lang and report prop errors in meta.errors, with the codes listed under Error codes. For a generation you already hold in full, createParser(library.schema, library.root).parse(generation) gives the same result.

Fix an invalid generation

Point the OpenAI client at /v1/autofix. Your component library goes first, as a system turn written by generateSystemPrompt({ cloud: true, library }); the generation goes last, as an assistant turn. Fenced or bare OpenUI Lang both work, and text outside the fence is ignored.

The API adds fix_summary to the chat completion, which the OpenAI types do not know about, so the example casts the request and the answer.

lib/autofix.ts
import { generateSystemPrompt } from "@openuidev/lang-core";
import OpenAI from "openai";
import type { ChatCompletion } from "openai/resources/chat/completions";
import library from "./openui.spec.json";

const client = new OpenAI({
  apiKey: process.env.THESYS_API_KEY,
  baseURL: "https://api.thesys.dev/v1/autofix",
});

// The library, in the form the API reads it: a system turn carrying the spec.
const libraryTurn = {
  role: "system",
  content: generateSystemPrompt({ cloud: true, library }),
} as const;

type FixError = { code: string; message: string; statementId?: string };
type FixSummary = {
  status: "already_valid" | "fixed" | "fix_failed";
  fixed_errors: FixError[];
  unfixed_errors: FixError[];
};
type AutofixCompletion = ChatCompletion & { fix_summary: FixSummary };

export async function autofix(messages: OpenAI.ChatCompletionMessageParam[], generation: string) {
  return (await client.chat.completions.create({
    model: "openui/autofix",
    messages: [libraryTurn, ...messages, { role: "assistant", content: generation }],
  } as OpenAI.ChatCompletionCreateParamsNonStreaming)) as AutofixCompletion;
}

openui.spec.json is the library spec that openui generate --spec writes for your components. See Generate OpenUI Lang for how it is created. generateSystemPrompt({ cloud: true, library }) turns it into a two-line ]]>openui:config block, the same one the Gateway reads; the API reads the library from the first message only, and counts no config block as conversation. The API always needs the library: a generation is only valid against the components your app renders, so there is no built-in default.

model is accepted so that an SDK client has something to send, and it is ignored: the fix always runs on the same correction model.

Without an SDK

The plain path takes the same body. Server calls use the API key described in Authentication.

lib/autofix-fetch.ts
import { generateSystemPrompt } from "@openuidev/lang-core";
import library from "./openui.spec.json";

type Message = { role: string; content: string | unknown[] };

const libraryTurn: Message = {
  role: "system",
  content: generateSystemPrompt({ cloud: true, library }),
};

export async function autofix(messages: Message[], generation: string) {
  const response = await fetch("https://api.thesys.dev/v1/autofix", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.THESYS_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      messages: [libraryTurn, ...messages, { role: "assistant", content: generation }],
    }),
  });

  if (!response.ok) {
    throw new Error(`Autofix failed: ${response.status}`);
  }

  return response.json();
}

Request

FieldTypeRequiredPurpose
messagesarrayYesThe conversation, in the OpenAI chat message shape. The first message is a system turn that carries your library as the ]]>openui:config block that generateSystemPrompt({ cloud: true, library }) writes; it is read as the library, not as conversation. The last turn must be an assistant turn whose text is the generation to fix, up to 100,000 characters. The turns in between are the context: only the text of user, assistant, system, and developer turns is read, and tool turns and non-text parts are dropped.
modelstringNoAccepted so that an SDK client can name a model, and ignored. The answer always reports openui/autofix.
streamboolNoStreaming is not supported. A fix has nothing to send until it is finished, so stream: true is a 400.

The other OpenAI fields, such as temperature and tools, are ignored.

Read the result

The answer is a chat completion. choices[0].message.content holds the complete generation, not a patch: replace the model output with it and render it. fix_summary.status says what happened.

statuscontentErrorsCharged
already_validThe input, unchangedfixed_errors is emptyNo
fixedThe corrected generationfixed_errors lists what was wrong and is now fixedYes
fix_failednullunfixed_errors lists what is still wrongYes

fixed_errors and unfixed_errors are always present, and one of them is usually empty.

A fixed generation:

{
  "id": "fix_P4TPtYimJFMpSgsViggie",
  "object": "chat.completion",
  "created": 1789019848,
  "model": "openui/autofix",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "root = Card([header])\nheader = Header(\"Q3 Results\")"
      }
    }
  ],
  "usage": { "prompt_tokens": 8014, "completion_tokens": 17, "total_tokens": 8031 },
  "fix_summary": {
    "status": "fixed",
    "fixed_errors": [
      {
        "code": "unresolved",
        "statementId": "followUp",
        "message": "reference \"followUp\" is never defined"
      }
    ],
    "unfixed_errors": []
  }
}

A generation that could not be fixed:

{
  "id": "fix_dRvfleg31F5g-Amn3v4-W",
  "object": "chat.completion",
  "created": 1789026253,
  "model": "openui/autofix",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": { "role": "assistant", "content": null }
    }
  ],
  "usage": { "prompt_tokens": 360, "completion_tokens": 146, "total_tokens": 506 },
  "fix_summary": {
    "status": "fix_failed",
    "fixed_errors": [],
    "unfixed_errors": [
      {
        "code": "null-required",
        "component": "B",
        "path": "/child",
        "statementId": "z",
        "message": "required field \"/child\" cannot be null"
      }
    ]
  }
}

When the fix fails, fall back to what your application does for any unusable model output, such as showing the text or a generic error state.

Error codes

Each entry in fixed_errors or unfixed_errors names one problem. code is always present. component, path, and statementId are present when the validator can name the component type, the property, and the statement that carried the problem.

CodeMeaning
unknown-componentA component name that is not in the library.
missing-requiredA required property was left out.
null-requiredA required property was set to null.
type-mismatchA property has the wrong type, or a value outside its allowed set.
excess-argsA component received more arguments than its signature has.
inline-reservedQuery() or Mutation() was used inside an expression instead of as a statement.
incompleteThe generation stopped in the middle of a statement.
unresolvedA statement is referenced but never defined.
orphanedA statement is defined but not reachable from root.
missing-rootThe generation has no valid root statement.

These are the same codes the OpenUI SDK reports in the browser, so an application can handle both with one path. See Reliability Monitoring for how they appear in the Thesys Console.

Framing

The generation may be bare OpenUI Lang, a fenced code block, or an assistant turn as the OpenUI SDK stores it after the user has touched a form inside it: ]]>openui:content, the program, ]]>openui:context with the form state, ]]>openui:end. Only the program inside is checked and repaired. A fixed generation comes back in the same wrapping it arrived in, header line, context section and end marker unchanged, so the SDK reads the answer the way it reads history.

Limits

  • The generation can be up to 100,000 characters.
  • The turns before it can hold up to 20 usable turns and 8,000 characters of text in total, counted after tool turns, non-text parts, and config blocks are removed.
  • Requests exceeding either context limit are rejected with 400. Trim older history before sending; the API does not drop it automatically.
  • Up to two attempts are made to fix one generation.
  • Only non-streaming responses are supported. stream: true is a 400.

Data

The conversation you send is passed to the model that fixes the generation, and is not stored beyond the request logs.

Pricing

Each call that runs a fix is charged a flat price from your credits. A generation that is already valid is free. See Pricing for the current rate.

usage in the answer reports the tokens the fix used, for your own tracking. The charge does not depend on it.

Errors

The endpoint returns the same error shape as the Chat Completions and Responses APIs.

StatusTypeWhen
400invalid_request_errorThe first message is not a library turn, or its block is malformed; the last turn is not an assistant turn with a generation; a limit is passed; or stream is true.
401authentication_errorThe API key is missing or invalid.
429rate_limit_errorThe organization has no credits, or billing is suspended.
500internal_server_errorThe fixing model could not be reached. Nothing is charged.

Reliability Monitoring

Autofix requests automatically appear in the Reliability Monitoring under the model name openui/autofix. No Observability SDK installation is required to track these requests. A successful fix counts as a sanitizer correction, with the same error codes as corrections made during Gateway generation.

On this page