Skip to main content

yapi

PyPI Python License: MIT

中文文档请见 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 unhyphenated yapi was taken by a 2018 project). Import path is still yapi.

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_choice parameter. yapi relies on PydanticAI's structured-output path, which forces the model to emit a tool call matching your response BaseModel. Models that lack tool_choice support — most notably "reasoning / thinking" variants such as deepseek-reasoner, deepseek-v4-flash, o1-preview / o1-mini, or any chat-only / completion-only checkpoint — will return HTTP 500 with a ModelHTTPError at 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) reject tool_choice and won't work here. Use deepseek-chat.

How a prompt route runs

For each request to a router.prompt.* route, yapi:

  1. parses path/query/header/cookie/body parameters via the function signature (FastAPI semantics, plus a single BaseModel request body),
  2. calls your function (sync or async def) to optionally produce a dynamic prompt (the function's return value, must be None or str),
  3. composes the final system prompt from: response-model docstring + function docstring + dynamic prompt,
  4. invokes the configured agent_runner (defaulting to a PydanticAI Agent) with a RunnerContext containing the prompt, request payload, injected fields, response model, path and method,
  5. 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 BaseModel subclass.
  • At most one parameter may be a BaseModel (the request body). Supports both req: WishIn and req: Annotated[WishIn, Body()].
  • Other parameters must be one of:
    • Depends(...) default or Annotated[T, Depends(...)]
    • Annotated[T, Query()/Header()/Cookie()/Path()/Form()/File()] or the equivalent = Query(...) default
  • *args / **kwargs are rejected at decoration time.
  • Function body must return None or a str (the dynamic prompt). Anything else raises at request time.
  • async def is 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)

Source distribution for pyyapi 0.3.2
File Size Uploaded Provenance
pyyapi-0.3.2.tar.gz 18.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pyyapi 0.3.2
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page