# Instructor

[Instructor](https://python.useinstructor.com) does one thing: you hand it a
Pydantic model, it hands you back a validated instance. It wraps an OpenAI
client, so pointing it at Hyphen is the same base URL change as everywhere
else.

```bash
pip install "instructor>=1.15.4" openai pydantic
export HYPHEN_API_KEY="sk-..."
```

:::warning[Pin `instructor>=1.15.4`]
Before 1.15.4, `from_provider(...)` silently dropped `base_url` and sent your
traffic to `api.openai.com` with a Hyphen key. It fails as a confusing auth
error rather than a routing error. Either pin the version, or use the
`from_openai` form further down, which was never affected.
:::

*Setup guide: this config comes from Instructor's official documentation and
has not been run end to end against the gateway. Corrections to [support@hyphen-solution.com](mailto:support@hyphen-solution.com).*

## Minimal

```python
import os
import instructor
from instructor import Mode
from pydantic import BaseModel


class Person(BaseModel):
    name: str
    age: int
    occupation: str


client = instructor.from_provider(
    "openai/minimax-m3",
    base_url="https://api.hyphen-solution.com/v1",
    api_key=os.environ["HYPHEN_API_KEY"],
    mode=Mode.TOOLS,
)

person = client.create(
    response_model=Person,
    messages=[
        {"role": "user", "content": "Extract: John is a 30-year-old software engineer"}
    ],
    max_tokens=4000,
    max_retries=3,
)
print(person)
# Person(name='John', age=30, occupation='software engineer')
```

## Use `Mode.TOOLS`

This is the setting that matters. Instructor can extract structured data
several ways, and only one of them works against the M-series.

| Mode | Mechanism | Expected on Hyphen |
| --- | --- | --- |
| `Mode.TOOLS` | Tool calling | **Yes. Use this.** |
| `Mode.MD_JSON` | Prompt only, parses fenced JSON | Yes, fallback |
| `Mode.JSON` | Needs `response_format: json_object` | No |
| `Mode.JSON_SCHEMA` | Needs `response_format: json_schema` | No |
| `Mode.TOOLS_STRICT` | Needs strict schema support | No |

`Mode.TOOLS` is already the default for the openai provider, so you can omit
it. Setting it explicitly documents the intent and stops a future default
change from breaking you.

If tool calling ever misbehaves on a prompt, `Mode.MD_JSON` is the safe
fallback because it needs no provider features at all:

```python
client = instructor.from_provider(
    "openai/minimax-m3",
    base_url="https://api.hyphen-solution.com/v1",
    api_key=os.environ["HYPHEN_API_KEY"],
    mode=Mode.MD_JSON,
)
```

## The classic form

Wrapping an OpenAI client yourself. Slightly more verbose, stable across
versions, and makes the base URL impossible to lose.

```python
import os
import instructor
from instructor import Mode
from openai import OpenAI
from pydantic import BaseModel


class Person(BaseModel):
    name: str
    age: int
    occupation: str


client = instructor.from_openai(
    OpenAI(
        base_url="https://api.hyphen-solution.com/v1",
        api_key=os.environ["HYPHEN_API_KEY"],
    ),
    mode=Mode.TOOLS,
)

person = client.chat.completions.create(
    model="minimax-m3",
    response_model=Person,
    messages=[
        {"role": "user", "content": "Extract: John is a 30-year-old software engineer"}
    ],
    max_tokens=4000,
)
print(person)
```

Note the model name has **no** `openai/` prefix in this form. The prefix only
exists in `from_provider`'s combined `provider/model` string.

## Validation and retries

This is why Instructor is worth using. `max_retries` feeds validation errors
back to the model so it can repair its own output.

```python
from pydantic import BaseModel, Field, field_validator


class Issue(BaseModel):
    title: str = Field(max_length=80)
    severity: str

    @field_validator("severity")
    @classmethod
    def known_severity(cls, v: str) -> str:
        allowed = {"low", "medium", "high", "critical"}
        if v not in allowed:
            raise ValueError(f"severity must be one of {allowed}")
        return v


issue = client.create(
    response_model=Issue,
    messages=[{"role": "user", "content": "CSV exporter drops the last row. Blocking."}],
    max_tokens=4000,
    max_retries=3,
)
```

Each retry costs tokens against your monthly cap, so keep `max_retries` small.
Three is plenty.

## Lists and nested models

```python
from typing import List


class Team(BaseModel):
    name: str
    members: List[Person]


team = client.create(
    response_model=Team,
    messages=[{"role": "user", "content": "The Platform team is Ana (32, SRE) and Bo (28, dev)."}],
    max_tokens=4000,
)
```

## Gotchas

- **`max_tokens` of 4000.** Reasoning runs before the tool call carrying your
  object is emitted. Too low and you get a retry loop that never succeeds. See
  [Choosing a model](/choosing-a-model#the-max_tokens-gotcha).
- **Never `Mode.JSON` or `Mode.JSON_SCHEMA`.** The M-series ignore
  `response_format`.
- **Pin `instructor>=1.15.4`** if you use `from_provider` with `base_url`.
- **`429`** means the monthly budget is spent. Note that retries make this
  arrive sooner. See [Handling the 429 cap](/recipes/handling-429).

## Related

- [Instructor docs](https://python.useinstructor.com/)
- [Mode comparison](https://python.useinstructor.com/modes-comparison/)
- [Structured JSON output](/recipes/structured-output): the same job without a library.
- [Pydantic AI](/build/pydantic-ai): if you want agents as well as extraction.
