PhoenixKitAI.StructuredOutput (PhoenixKitAI v0.23.1)

Copy Markdown View Source

Machine-readable answers from chat models, shared by PhoenixKitAI.ask/3, PhoenixKitAI.complete/3 and PhoenixKitAI.describe_image/3.

Two options turn it on:

  • schema: — a JSON Schema map; the answer is requested as that object (response_format: json_schema, strict) and parsed.
  • json: true — any JSON object (response_format: json_object).

The schema also rides in the prompt, so a model that rejects response_format (the request is retried once without it on 400/422 — any 400, so an unrelated bad request costs one extra call) still knows the shape. The parsed object comes back under "json" on a chat response and :json on a vision result; an answer that does not parse is {:error, {:no_json_in_response, text}} and is never cached, though the provider call is logged with its usage. Nothing validates the object against the schema locally: strict schemas are enforced by the provider on the first attempt and advisory on the retry, so check the keys you depend on.

Summary

Functions

Appends suffix to the last user message (string content or content parts). Messages with atom or string keys are both handled.

Parses the model's text as a JSON object (or array): the whole text first, then a fenced block anywhere in it, then the outermost {…} — models wrap answers in prose and fences however they were told. With requested? false returns {:ok, nil}.

The sentence to append to the user's prompt and the response_format to send, or {nil, nil} when no JSON was asked for.

Whether the caller asked for a JSON answer.

Runs fun.(response_format); when a JSON answer was requested and the provider answers 400/422 (a model that takes no response_format), runs it once more with nil.

Types

format()

@type format() :: %{required(String.t()) => term()} | nil

Functions

attach(messages, suffix)

@spec attach(list(), String.t() | nil) :: list()

Appends suffix to the last user message (string content or content parts). Messages with atom or string keys are both handled.

parse(text, bool)

@spec parse(String.t() | nil, boolean()) ::
  {:ok, map() | list() | nil}
  | {:error, {:no_json_in_response, String.t() | nil}}

Parses the model's text as a JSON object (or array): the whole text first, then a fenced block anywhere in it, then the outermost {…} — models wrap answers in prose and fences however they were told. With requested? false returns {:ok, nil}.

request(opts)

@spec request(keyword()) :: {String.t() | nil, format()}

The sentence to append to the user's prompt and the response_format to send, or {nil, nil} when no JSON was asked for.

requested?(opts)

@spec requested?(keyword()) :: boolean()

Whether the caller asked for a JSON answer.

with_fallback(format, fun)

@spec with_fallback(format(), (format() -> {:ok, term()} | {:error, term()})) ::
  {:ok, term()} | {:error, term()}

Runs fun.(response_format); when a JSON answer was requested and the provider answers 400/422 (a model that takes no response_format), runs it once more with nil.