yapi
中文文档请见 README.zh-CN.md
Prompt-first declarative HTTP framework — write a normal Python function with a docstring, get an LLM-powered HTTP endpoint with structured JSON responses.
yapi is a thin layer on top of FastAPI and PydanticAI. PromptRouter is a true superset of fastapi.APIRouter: native routes work as-is, and prompt routes live in the router.prompt.* namespace.
Package name on PyPI is
pyyapi(the unhyphenatedyapiwas taken by a 2018 project). Import path is stillyapi.
Install
pip install pyyapi
Python 3.12+ required.
Quick start
from fastapi import FastAPI
from pydantic import BaseModel
from yapi import PromptRouter
class WishIn(BaseModel):
user_id: str
wish: str
class WishOut(BaseModel):
"""You are a wish-granting entity. Decide whether to grant the wish."""
granted: bool
message: str
app = FastAPI(title="yapi showcase")
router = PromptRouter()
@router.prompt.post("/wish")
def make_a_wish(req: WishIn) -> WishOut:
"""Decide whether to grant the user's wish."""
app.include_router(router)
Run it:
YAPI_MODEL=test uvicorn examples.wish_api:app --reload
YAPI_MODEL=test activates PydanticAI's built-in TestModel — no API key, no network, perfect for offline smoke tests. For real models, set e.g. YAPI_MODEL=openai:gpt-4o or YAPI_MODEL=anthropic:claude-3-5-sonnet.
Open http://localhost:8000/docs for the auto-generated OpenAPI UI.
Mixing native FastAPI routes with prompt routes
PromptRouter is now a real APIRouter superset. .get/.post/... keep their FastAPI semantics; only router.prompt.* enters the LLM pipeline.
router = PromptRouter(prefix="/v1", tags=["wishes"])
@router.get("/health")
def health() -> dict:
return {"status": "ok"}
@router.prompt.post("/wish")
def make_a_wish(req: WishIn) -> WishOut:
"""Decide whether to grant the user's wish."""
Configuration
yapi is configured entirely through environment variables — the package never reads .env files itself. Use a launcher that injects them (recommended: uvicorn --env-file .env; alternatives: set -a; source .env; set +a in your shell, Docker --env-file, Kubernetes secrets, etc.).
YAPI_MODEL (required for the default runner)
PydanticAI model string in provider:model form. Read once when PromptRouter() is constructed without an explicit agent_runner.
YAPI_MODEL=openai:gpt-4o # OpenAI
YAPI_MODEL=anthropic:claude-3-5-sonnet # Anthropic
YAPI_MODEL=openai:deepseek-chat # DeepSeek (OpenAI-compatible)
YAPI_MODEL=test # PydanticAI TestModel, no key, no network
Unset → constructor emits a YapiUsageWarning, first request returns HTTP 500.
⚠️ The model must support OpenAI Function Calling's
tool_choiceparameter.yapirelies on PydanticAI's structured-output path, which forces the model to emit a tool call matching your responseBaseModel. Models that lacktool_choicesupport — most notably "reasoning / thinking" variants such asdeepseek-reasoner,deepseek-v4-flash,o1-preview/o1-mini, or any chat-only / completion-only checkpoint — will return HTTP 500 with aModelHTTPErrorat the first request. Pick a model whose API docs explicitly support function calling (gpt-4o,gpt-4o-mini,claude-3-5-sonnet,deepseek-chat, …).
Provider credentials (read directly by PydanticAI)
yapi does not validate or even look at these — they are consumed by the underlying PydanticAI provider via os.environ:
| Provider | Env vars |
|---|---|
| OpenAI | OPENAI_API_KEY |
| OpenAI-compatible endpoints (DeepSeek, Azure OpenAI, OneAPI, local servers, …) | OPENAI_API_KEY + OPENAI_BASE_URL (e.g. https://api.deepseek.com/v1) |
| Anthropic | ANTHROPIC_API_KEY |
| Others (Google, Groq, Mistral, …) | See PydanticAI providers docs |
Example .env (DeepSeek)
YAPI_MODEL=openai:deepseek-chat
OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://api.deepseek.com/v1
uv run uvicorn examples.wish_api:app --reload --env-file .env
Same caveat as the warning above: DeepSeek's "thinking" models (
deepseek-reasoner,deepseek-v4-flash) rejecttool_choiceand won't work here. Usedeepseek-chat.
How a prompt route runs
For each request to a router.prompt.* route, yapi:
- parses path/query/header/cookie/body parameters via the function signature (FastAPI semantics, plus a single
BaseModelrequest body), - calls your function (sync or
async def) to optionally produce a dynamic prompt (the function'sreturnvalue, must beNoneorstr), - composes the final system prompt from: response-model docstring + function docstring + dynamic prompt,
- invokes the configured
agent_runner(defaulting to a PydanticAIAgent) with aRunnerContextcontaining the prompt, request payload, injected fields, response model, path and method, - validates the agent's output against your return annotation and serializes via FastAPI.
Contract (hard rules)
Applies inside router.prompt.*:
- Return annotation must be a
BaseModelsubclass. - At most one parameter may be a
BaseModel(the request body). Supports bothreq: WishInandreq: Annotated[WishIn, Body()]. - Other parameters must be one of:
Depends(...)default orAnnotated[T, Depends(...)]Annotated[T, Query()/Header()/Cookie()/Path()/Form()/File()]or the equivalent= Query(...)default
*args/**kwargsare rejected at decoration time.- Function body must
returnNoneor astr(the dynamic prompt). Anything else raises at request time. async defis supported.
Decoration kwargs:
- Passed through to FastAPI:
tags,summary,description,status_code,deprecated,operation_id,name,include_in_schema,responses,openapi_extra. - Rejected at decoration time with
YapiDeclarationError:response_model,response_class,dependencies. - Any other unknown kwarg emits a
YapiUsageWarning.
Violations are raised as YapiDeclarationError at decoration time — broken routes fail at import, not at request time.
Prompt context
Use PromptContext to inject structured facts into the system prompt without returning a string. Declare a parameter typed PromptContext and yapi auto-injects a per-request instance:
from yapi import PromptContext, PromptRouter
router = PromptRouter()
@router.prompt.post("/wish")
def make_a_wish(req: WishIn, ctx: PromptContext) -> WishOut:
"""Decide whether to grant the user's wish."""
ctx.add_section("User Profile", {"vip": req.user_id.startswith("vip-")})
ctx.add_kv("user_id", req.user_id)
ctx.add(req.wish)
yapi collects all segments and wraps them in <context>…</context> at the end of the system prompt:
You are the execution engine…
Decide whether to grant the user's wish.
<context>
# User Profile
{"vip": true}
user_id: vip-1
moon
</context>
Three methods:
| Method | Produces |
|---|---|
ctx.add(value) |
<serialized value> |
ctx.add_kv(key, value) |
{key}: <serialized value> |
ctx.add_section(name, body) |
# {name}\n<serialized body> |
Value serialization: str → pass-through; BaseModel → model_dump_json(); dict/list/tuple → json.dumps(..., ensure_ascii=False); anything else → str(). None is rejected — use "" if you want an empty segment.
PromptContext is append-only — no clear / pop. Use Python if for conditional adds. At most one PromptContext parameter per route; the parameter must not carry FastAPI markers (Annotated[PromptContext, Body()/Query()/Depends()] is a declaration error).
State retrieval is out of scope for yapi. Fetch your data via Depends(...) and pass it to ctx.*. See examples/state_via_depends.py.
Dependency injection
from fastapi import Depends
from typing import Annotated
def get_db():
...
@router.prompt.post("/wish")
def make_a_wish(
req: WishIn,
db: Annotated[Database, Depends(get_db)],
) -> WishOut:
"""..."""
return f"user has {db.balance(req.user_id)} wishes left"
Custom agent runner
Implement the AgentRunner Protocol — any object with a .run(ctx: RunnerContext) -> dict | BaseModel method is accepted:
from yapi import AgentRunner, PromptRouter, RunnerContext
class MockRunner:
def run(self, ctx: RunnerContext) -> dict:
return {
"granted": "moon" not in ctx.request["wish"].lower(),
"message": f"path={ctx.path}",
}
router = PromptRouter(agent_runner=MockRunner())
The legacy v2-style (*, prompt, request, injected, response_model) -> dict callable is still accepted (auto-adapted).
You can also inject a custom prompt_composer= to customize how the system prompt is assembled.
Development
uv sync --extra dev
uv run pytest
uv run uvicorn examples.wish_api:app --reload
License
MIT
Metadata
Release files for pyyapi 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pyyapi-0.3.2.tar.gz | 18.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyyapi-0.3.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.7 kB
Release files / pyyapi-0.3.2.tar.gz
| Download URL | pyyapi-0.3.2.tar.gz |
|---|---|
| Size | 18.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
03d7b34717d49741d1ca5fd81d4bf8fbdd85b7920afc4a4ffc82e6669c67fedb
|
|
BLAKE2b-256 checksum How to use checksums |
2565f99ec0b88d691e6a28e8f93737b52bc64d71b97c130c4b76acb5b3930fac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 23, 2026.
Transparency logRelease files / pyyapi-0.3.2-py3-none-any.whl
| Download URL | pyyapi-0.3.2-py3-none-any.whl |
|---|---|
| Size | 14.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b8a857bc3828859a8541c6036315bbe9796b7ece7b1a48c5a843b48cfa3ce375
|
|
BLAKE2b-256 checksum How to use checksums |
7645ded6288df812f68cd0625f89b2489be2397a829d453b5ef8845be3c7b624
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 23, 2026.
Transparency logSecurity for pyyapi 0.3.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pyyapi-0.3.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.