Autofix API

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

The Autofix API repairs invalid OpenUI Lang after a model has generated it. Send the conversation with the generation as the last assistant turn, and the answer carries valid OpenUI Lang. Use it when your application calls a model directly.

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.

Detect an invalid generation

Parse the generation with @openuidev/lang-core before you call the API. It is the same check the API runs first, so a generation that passes here would come back as already_valid. Skip the call when nothing is wrong.

lib/detect.ts
import { createParser } from "@openuidev/lang-core";
import library from "./openui.spec.json";

const parser = createParser(library.schema, library.root);

/** Everything that stops the generation from rendering. Empty means valid. */
export function findErrors(generation: string): string[] {
  const { root, meta } = parser.parse(generation);
  return [
    ...meta.errors.map((e) => `${e.code} ${e.component}${e.path}: ${e.message}`),
    ...meta.unresolved.map((name) => `unresolved: "${name}" is used but never defined`),
    ...meta.orphaned.map((name) => `orphaned: "${name}" is defined but never used`),
    ...(meta.incomplete ? ["incomplete: the generation ends mid-statement"] : []),
    ...(root === null ? ["missing-root: no root element"] : []),
  ];
}
server.ts
import { findErrors } from "./lib/detect";

const errors = findErrors(generation);
if (errors.length === 0) {
  render(generation);
} else {
  console.warn("invalid generation", errors);
  // Send it to the Autofix API, below.
}

parse never throws. It takes fenced or bare OpenUI Lang and reports prop errors in meta.errors, with the codes listed under Error codes.

Fix an invalid generation

Point the OpenAI client at /v1/autofix and send the generation as the last 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 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",
});

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: [...messages, { role: "assistant", content: generation }],
    library,
  } as OpenAI.ChatCompletionCreateParamsNonStreaming)) as AutofixCompletion;
}
server.ts
import { autofix } from "./lib/autofix";

// Pass the same messages you sent to your model.
const completion = await autofix(messages, generation);

if (completion.fix_summary.status === "fix_failed") {
  renderFallback(generation);
} else {
  render(completion.choices[0].message.content);
}

openui.spec.json is the library spec that openui generate --spec writes for your components. See Generate OpenUI Lang for how it is created. Omit library to check the generation against the built-in OpenUI chat library.

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 library from "./openui.spec.json";

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

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: [...messages, { role: "assistant", content: generation }],
      library,
    }),
  });

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

  return response.json();
}

Request

FieldTypeRequiredPurpose
messagesarrayYesThe conversation, in the OpenAI chat message shape. The last turn must be an assistant turn whose text is the generation to fix, up to 100,000 characters. The turns before it are the context: only the text of user, assistant, system, and developer turns is read, and tool turns and non-text parts are dropped.
libraryobjectNoYour library spec, as written by openui generate --spec. Without it, the built-in chat library is used.
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 Observability for how they appear in the Thesys Console.

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 and non-text parts are dropped.
  • Only the most recent turns are used. Older turns are dropped first.
  • 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 last turn is not an assistant turn with a generation, a limit is passed, library is malformed, 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.

Observability

Autofix calls appear in the Thesys Console under the model name openui/autofix. A successful fix counts as a sanitizer correction on the Reliability page, with the same error codes as corrections made during generation.

On this page