Lesson 2 — what a provider declares¶
What you will build¶
The difference between "this provider can do structured output" and "this call asked for structured output": a declaration on the provider, and a facade that refuses a request the provider never claimed it could serve. It is the cheapest correctness check in the AI stack, and it happens before a token is spent.
What you need¶
Nothing new.
"""Lesson 2 — providers declare what they can do, and the facade checks.
Run it with:
python -m learn_ai.ai02_providers
"""
from __future__ import annotations
import asyncio
from xyberos.ai import (
AIFacade,
ChatRequest,
FakeModelProvider,
Message,
ModelCapabilities,
ModelReply,
)
from xyberos.errors import AIUnsupportedCapabilityError
CATEGORY_SCHEMA = {
"type": "object",
"properties": {"category": {"type": "string", "enum": ["refund", "question", "complaint"]}},
"required": ["category"],
"additionalProperties": False,
}
async def describe(assistant: AIFacade, label: str) -> None:
"""Print what a facade's provider declares, then ask it for structured output."""
print(f"{label}: {assistant.name} {assistant.model}")
print(f"{label} capabilities:", assistant.capabilities)
request = ChatRequest(
messages=(Message.user("Classify this ticket."),),
response_schema=CATEGORY_SCHEMA,
)
try:
await assistant.structured(request)
except AIUnsupportedCapabilityError as error:
print(f"{label} refused:", error)
else:
print(f"{label} answered")
async def main() -> None:
"""One provider that declares structured output, one that does not."""
capable = FakeModelProvider(
[ModelReply.json_payload({"category": "complaint"})],
capabilities=ModelCapabilities(structured_output=True),
)
incapable = FakeModelProvider(capabilities=ModelCapabilities())
await describe(AIFacade(capable), "declares it")
await describe(AIFacade(incapable), "does not")
if __name__ == "__main__":
asyncio.run(main())
The provider's capabilities are not documentation: the facade reads them on every call. Asking a
provider without structured_output for a response schema fails immediately, with the reason, instead of
returning a string that the caller then tries to parse as JSON.
How to run it¶
What it prints¶
declares it: fake fake-model
declares it capabilities: ModelCapabilities(tools=False, structured_output=True, streaming=True, embeddings=False)
declares it answered
does not: fake fake-model
does not capabilities: ModelCapabilities(tools=False, structured_output=False, streaming=True, embeddings=False)
does not refused: the request asks for a response schema but this model does not claim structured output support; declare the capability on the provider before relying on it
| Line | What it establishes |
|---|---|
declares it capabilities: ModelCapabilities(...) |
what the provider claims, in one object |
declares it answered |
a claim that matches the request is served |
does not refused: the request asks for a response schema ... |
the mismatch is refused by the facade, not discovered later by the caller |
The four declared capabilities are the ones the facade can act on: structured_output, tools,
streaming, and embeddings. A provider that declares none of them is still a provider — it is a
provider you can call for chat and nothing else.
Common mistakes¶
- Declaring a capability you have not implemented. The declaration is a promise the facade trusts; a provider that lies turns a clear refusal into a confusing downstream failure.
- Skipping the declaration on a real adapter. A model that supports tools but is configured without
tools=Truewill refuse tool calls, and the exception will look like a facade bug rather than a configuration one. - Assuming every model has the same capabilities. Two models behind the same vendor differ; a capability belongs to the provider you configured, which is why the facade asks the provider instead of a table it keeps.
- Treating
streamingas free. A streamed answer cannot also ask for a schema or offer tools — the facade says so rather than dropping one of the two silently.
Next¶
Lesson 3 makes the first real call: a chat request, a chat response, and the metadata that comes back with it.