Prompt-first declarative HTTP framework on top of FastAPI and PydanticAI
Project description
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.
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
DeepSeek's "thinking" models (
deepseek-reasoner,deepseek-v4-flash) currently reject OpenAI Function Calling'stool_choiceparameter, which PydanticAI uses by default for structured output. Usedeepseek-chatfor now.
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.
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyyapi-0.2.0.tar.gz.
File metadata
- Download URL: pyyapi-0.2.0.tar.gz
- Upload date:
- Size: 15.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d83a78defef3445ef26f1e049bce91460def18d8ef0835f2bdc1e3c274bc64f
|
|
| MD5 |
fd0c705542328c92fcae46b1973a0320
|
|
| BLAKE2b-256 |
ddb0b7dd44412bc2b59d27b6bb5773f5d63e8dabaff462e9235494db7094ee50
|
Provenance
The following attestation bundles were made for pyyapi-0.2.0.tar.gz:
Publisher:
release.yml on TokenRollAI/yapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyyapi-0.2.0.tar.gz -
Subject digest:
3d83a78defef3445ef26f1e049bce91460def18d8ef0835f2bdc1e3c274bc64f - Sigstore transparency entry: 1615730513
- Sigstore integration time:
-
Permalink:
TokenRollAI/yapi@0bed22a452ff127bd7f4aa0e0ec5e5963679de92 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/TokenRollAI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0bed22a452ff127bd7f4aa0e0ec5e5963679de92 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyyapi-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pyyapi-0.2.0-py3-none-any.whl
- Upload date:
- Size: 12.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c29d7e8b2f63b5d338f98c58f0fd4c8bb76fd583da1884c89cdb134de38c288
|
|
| MD5 |
64c8fd6a4778e2b1fe2e64ea8f8021e6
|
|
| BLAKE2b-256 |
860971009df46f9ef17a4a17403e8b4dd5c1d9fce22a2057f31d6b39520d4614
|
Provenance
The following attestation bundles were made for pyyapi-0.2.0-py3-none-any.whl:
Publisher:
release.yml on TokenRollAI/yapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyyapi-0.2.0-py3-none-any.whl -
Subject digest:
9c29d7e8b2f63b5d338f98c58f0fd4c8bb76fd583da1884c89cdb134de38c288 - Sigstore transparency entry: 1615730532
- Sigstore integration time:
-
Permalink:
TokenRollAI/yapi@0bed22a452ff127bd7f4aa0e0ec5e5963679de92 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/TokenRollAI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0bed22a452ff127bd7f4aa0e0ec5e5963679de92 -
Trigger Event:
push
-
Statement type: