# `Nous.OutputSchema`
[🔗](https://github.com/nyo16/nous/blob/v0.17.1/lib/nous/output_schema.ex#L1)

Structured output support for Nous agents.

Converts Ecto schemas and other type specifications into JSON Schema,
generates provider-specific settings, and validates LLM responses against
the declared output type.

Inspired by [instructor_ex](https://github.com/thmsmlr/instructor_ex).

## Output Type Variants

- `:string` — raw text (default, no processing)
- `module()` — Ecto schema → JSON schema + changeset validation
- `%{atom() => atom()}` — schemaless Ecto types (e.g. `%{name: :string}`)
- `%{String.t() => map()}` — raw JSON schema (string keys, passed through)
- `{:regex, pattern}` — regex-constrained output (vLLM/SGLang)
- `{:grammar, ebnf}` — grammar-constrained output (vLLM)
- `{:choice, choices}` — choice-constrained output (vLLM/SGLang)

## Modes

| Mode | Mechanism | Providers |
|------|-----------|-----------|
| `:auto` | Pick best for provider | All |
| `:tool_call` | Synthetic tool + tool_choice | All (native for Anthropic) |
| `:json_schema` | `response_format: {type: "json_schema", ...}` | OpenAI, vLLM, SGLang |
| `:json` | `response_format: {type: "json_object"}` | OpenAI-compatible |
| `:md_json` | Prompt + markdown fence + stop token | All (fallback) |

## Examples

Describe the shape you want with schemaless Ecto types, turn it into a JSON
Schema, and validate a raw model response against it:

    iex> shape = %{sentiment: :string, score: :float}
    iex> json_schema = Nous.OutputSchema.to_json_schema(shape)
    iex> json_schema["type"]
    "object"
    iex> Enum.sort(json_schema["required"])
    ["score", "sentiment"]
    iex> {:ok, parsed} = Nous.OutputSchema.parse_and_validate(~s({"sentiment": "positive", "score": 0.92}), shape)
    iex> {parsed.sentiment, parsed.score}
    {"positive", 0.92}

Constrained decoding needs no schema at all — the constraint is handed to
the provider as model settings, and the response is checked on the way back:

    iex> Nous.OutputSchema.to_provider_settings({:choice, ["positive", "negative"]}, :vllm)
    %{guided_choice: ["positive", "negative"]}
    iex> Nous.OutputSchema.parse_and_validate("  positive  ", {:choice, ["positive", "negative"]})
    {:ok, "positive"}

In practice you rarely call this module directly — pass `output_type:` to
`Nous.Agent.new/2` and the runner wires the schema, the provider settings
and the validation for you:

    agent = Nous.Agent.new("openai:gpt-4o-mini", output_type: %{sentiment: :string, score: :float})
    {:ok, result} = Nous.run(agent, "Rate the sentiment of: 'I love this library'")
    result.output.sentiment
    #=> "positive"

# `cast_and_validate`

```elixir
@spec cast_and_validate(map(), Nous.Types.output_type()) ::
  {:ok, any()} | {:error, Nous.Errors.ValidationError.t()}
```

Cast parsed JSON data and validate it against the output type.

For Ecto schemas, uses `Ecto.Changeset.cast/3` + `validate_changeset/1`.
For schemaless types, uses `Ecto.Changeset.cast/4` with `{data, types}`.
For raw JSON schema maps, returns the parsed data as-is.

# `extract_response_for_one_of`

```elixir
@spec extract_response_for_one_of(Nous.Message.t(), [module()]) ::
  {String.t(), module() | nil}
```

Extract the response text and matched schema from a `{:one_of, schemas}` message.

If the message contains a synthetic tool call matching one of the schemas,
returns `{json_text, schema_module}`. Otherwise returns `{text, nil}`.

# `extract_response_text`

```elixir
@spec extract_response_text(Nous.Message.t(), atom()) :: String.t()
```

Extract the response text from a provider response message.

For `:tool_call` mode on Anthropic, extracts the `__structured_output__`
tool call arguments. For all other modes, extracts the text content.

# `find_schema_for_tool_name`

```elixir
@spec find_schema_for_tool_name(String.t(), [module()]) :: module() | nil
```

Find the schema module whose synthetic tool name matches `tool_name`.

Returns `nil` if no schema matches.

# `format_errors`

```elixir
@spec format_errors(Nous.Errors.ValidationError.t()) :: String.t()
```

Format a validation error into a human-readable string for LLM retry.

# `parse_and_validate`

```elixir
@spec parse_and_validate(String.t(), Nous.Types.output_type()) ::
  {:ok, any()} | {:error, Nous.Errors.ValidationError.t()}
```

Parse raw LLM response text and validate it against the output type.

Returns `{:ok, result}` where result is a struct, map, or string,
or `{:error, %ValidationError{}}` on failure.

# `schema_name`

```elixir
@spec schema_name(Nous.Types.output_type()) :: String.t()
```

Return a snake_case name for the given output type.

For Ecto schema modules, returns the last segment of the module name
underscored (e.g. `MyApp.SentimentResult` → `"sentiment_result"`).
For maps or other types, returns `"output"`.

# `synthetic_tool_name?`

```elixir
@spec synthetic_tool_name?(String.t()) :: boolean()
```

Return `true` if `name` is a synthetic structured output tool name.

Matches both the standard `"__structured_output__"` and per-schema
names like `"__structured_output_sentiment_result__"`.

# `system_prompt_suffix`

```elixir
@spec system_prompt_suffix(
  Nous.Types.output_type(),
  keyword()
) :: String.t() | nil
```

Generate system prompt suffix describing the expected output schema.

# `to_json_schema`

```elixir
@spec to_json_schema(Nous.Types.output_type()) :: map() | nil
```

Convert an output type specification to a JSON Schema map.

Returns `nil` for types that don't produce JSON schema (`:string`, tuples).

# `to_provider_settings`

```elixir
@spec to_provider_settings(Nous.Types.output_type(), atom(), keyword()) :: map()
```

Generate provider-specific model settings for structured output.

Returns a map of settings to merge into the model request. The map may
contain special keys prefixed with `__structured_output` that the
AgentRunner handles separately (e.g. synthetic tool injection).

## Options

- `:mode` — output mode (`:auto`, `:tool_call`, `:json_schema`, `:json`, `:md_json`)
- `:has_other_tools` — whether the agent has non-synthetic tools

# `tool_name_for_schema`

```elixir
@spec tool_name_for_schema(module()) :: String.t()
```

Build the synthetic tool name for a given schema module.

## Example

    iex> OutputSchema.tool_name_for_schema(SentimentResult)
    "__structured_output_sentiment_result__"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
