[build-system] requires = ["setuptools>=84.2.0", "wheel"] build-backend = "setuptools.build_meta" [project] # Distribution + import package stay `gateway`: the `otari` name on PyPI is the # Otari client SDK (a transitive dep via any-llm-sdk), so a top-level `otari` # package here would collide with it. User-facing names (CLI, env vars, docs) # are `otari`; only the internal import path remains `gateway`. The laptop-side # CLI is the workspace member at cli/ (distribution `otari-agent`, import # package `otari_agent`), which owns the `otari` console script. name = "gateway" version = "0.0.0" description = "otari, an OpenAI-compatible LLM gateway" requires-python = ">=3.13" dependencies = [ "any-llm-sdk[all]>=1.40.2", # The web search and web fetch libraries, workspace members at any-search/ # and any-fetch/. Each may not import the gateway back. "any-fetch", "any-search", # The built-in guardrail catalog (`services/guardrail_catalog.py`) and the # runner that builds an organization's definitions from it # (`services/tenancy/organization_guardrail_runner.py`). The extras are the # vendor SDKs of the guardrails that catalog lists, the ones reached over a # hosted API; alinia, lakera_guard and patronus need only `requests`, and # any_llm only any-llm. Not `all`: it adds torch and transformers for the # local-model guardrails, which run in the sidecar and never here. Bounded # below 0.8 rather than trusting a 0.x minor to keep the registry's shape. "any-guardrail[azure-content-safety,bedrock,openai,watsonx]>=0.7.7,<0.8.0", "alembic>=1.13.0", "aiosqlite>=0.19.0", # OAuth protocol mechanics for dashboard sign-in: provider endpoints, the # code exchange, and the userinfo fetch that normalizes a provider response # into an identity profile. The platform moved onto it in otari-ai#1740 and # this edition follows, so the two do not keep separate copies of a protocol # neither of us owns. What stays ours is which providers are configured, # which scopes are asked for, and how a fetched identity maps onto a user. "apron-auth>=0.18.0,<0.19.0", "asyncpg>=0.32.0", # Password hashing for the dashboard sign-in (docs/access-control.md). # The library directly rather than passlib, which the platform wraps it in: # passlib has been unmaintained since 2020 and its bcrypt backend breaks on # bcrypt 4.1+. The stored hash is the same "$2b$" string on either side, so an # identity imported from otari.ai signs in here and one created here signs in # there. 5.0 is the floor because that is the release that refuses a password # over 72 bytes instead of truncating it, which is the behavior # `gateway.services.password_service` documents and relies on. "bcrypt>=5.0.0", "click>=8.1.0", "cryptography>=50.0.0", # DNS lookups for the email-domain ownership proof # (`services.tenancy.domain_verification`). The async resolver specifically: # a claimed domain whose nameservers are gone takes the full timeout, and the # blocking resolver would hold the event loop for it. "dnspython>=2.9.0", "fastapi>=0.153.0", # Direct dependency for gateway-managed HTTP clients and the public # transport API used by pinned web retrieval. "httpx>=0.28.1", # The client the mcp SDK's transport takes (`http_client=`), which Otari # builds itself to keep MCP redirects and response sizes bounded. "httpx2>=2.13.1", # IDNA2008 for email-domain ownership and one UTS #46/STD3 identity for # URL policy and pinned retrieval. Declared rather than leaned on as a # transitive dependency, because the standard library's own "idna" codec # is IDNA2003 and folds `faß.de` into `fass.de`, which would let a proof # taken on one domain admit addresses at another. "idna>=3.10", # Pinned to one minor line, not just below the next major. The major # ceiling is #689: mcp 2.0 removed # `mcp.client.streamable_http.streamablehttp_client`, which # `services/mcp_client.py` and `services/mcp_stateless.py` import at module # scope, so a fresh resolution picking it up kills every gateway command. # The minor ceiling is a second, separate coupling: `/v1/mcp/execute` # answers with mcp's own `CallToolResult`, so ten mcp-owned schemas are # written into `docs/public/openapi.json` and from there into the Postman # collection and `web/src/client/schema.ts`, all three committed with drift # checks. An mcp release that touches any of those shapes therefore changes # three generated files here. Left open to the next major, a `uv lock` run for an # unrelated dependency would move mcp and those artifacts as a side effect, # and the drift checks would fail on a PR whose diff explained none of it. # Bumping this is deliberate work: raise the ceiling, then regenerate both # public artifacts and the dashboard client (see AGENTS.md, "Generated # Artifacts"). "mcp>=2.3.0,<2.4.0", "opentelemetry-api>=1.39.1", # The laptop-side CLI, a workspace member at cli/. The gateway needs it for # the Agent Gates evaluator the Hook Server runs (otari_agent.domain) and # so that `uv sync` installs the `otari` console script it owns. "otari-agent", "pillow>=13.0.0", "pypdfium2>=4.30.0", "psycopg2-binary>=2.9.9", "pydantic-settings>=2.16.1", "python-dotenv>=1.0.0", "python-multipart>=0.0.31", "pyyaml>=6.0", "sqlalchemy[asyncio]>=2.1.5", "sqlmodel>=0.0.24", "trafilatura>=2.0.0", "uvicorn[standard]>=0.54.0", "opentelemetry-proto>=1.39.1", "webauthn>=2.5.0", ] [project.optional-dependencies] # Optional OCR backend for image/scanned-PDF text extraction. Heavy (onnxruntime), # so it's opt-in; the gateway degrades to vision-describe / drop when absent. ocr = [ "rapidocr-onnxruntime>=1.3.0", "numpy>=2.5.4", ] # S3-compatible (AWS S3 or MinIO/any S3-compatible endpoint) files backend. # boto3 is already present transitively via any-llm-sdk[all] (Bedrock), but # this extra is what the S3 adapter checks for and documents, so the backend # stays decoupled from that implementation detail. s3 = [ "boto3>=1.43.115", ] # The generic files backend (`FsspecFileStore`): one URL reaches whichever # filesystem the operator has an fsspec implementation installed for. fsspec # is already in the tree through any-llm-sdk[all], but as with s3 the extra is # what the fsspec adapter names and documents, so the backend does not depend # on that implementation detail. The protocol's own package (gcsfs, adlfs, s3fs, # paramiko, ...) is the operator's to install. fsspec = [ "fsspec>=2026.6.0", ] # Hosted sandboxes through E2B, for a deployment that cannot run the reference # code-execution container with the isolation untrusted code needs. Selected # with `sandbox_provider: e2b`; the adapter imports the SDK only then. # The adapter walks a listing with `depth=`, and reads `size` and # `modified_time` off its entries, none of which the 1.x SDK has, so the floor # is the line it was written against rather than the first release to carry the # names. e2b = [ "e2b>=2.57.0", "e2b-code-interpreter>=2.11.1", ] # A rate-limit store every gateway process shares (`rate_limit_store: redis`), # so a limit holds across replicas rather than per process. The adapter imports # the client only when that store is selected. redis = [ "redis>=5.0.1", ] # Prometheus scrape at /metrics. Optional so a deployment that never scrapes # pays neither the import (prometheus_client.exposition pulls in http.server and # wsgiref.simple_server, which nothing else here needs) nor the collection; when # absent the metric objects in gateway.metrics are no-ops and `enable_metrics` # refuses to start. The Docker image installs it. metrics = [ "prometheus-client>=0.20.0", ] # Reads the country of a request's address from a MaxMind-format file # (`client_country_database`), for the provider country gate. Only that # setting imports it, and the gateway refuses to start without it when the # setting is on. The Docker image installs it. geoip = [ "maxminddb>=3.0", ] [dependency-groups] dev = [ "boto3-stubs[s3]", # The metrics extra, which the dev group installs unconditionally: the tests # that pin the exposed metric families need the real library, and mypy checks # gateway.metrics against it rather than against its no-op fallback. "prometheus-client>=0.20.0", # The geoip extra, for the same reason: the country lookup tests read a # real MaxMind-format file. "maxminddb>=3.0", # Validating payloads against the published code-execution contract # (docs/public/code-execution-openapi.yaml), in its conformance script and # the test that keeps the spec and the doc in agreement. Nothing at runtime # uses it: the gateway parses that contract with its own Pydantic models. "jsonschema>=4.26.0", "types-jsonschema", "moto[s3]>=5.2.4", "mypy", # Parses the mcp dependency spec straight out of this file's own # [project.dependencies], in the regression test for #689, instead of # relying on packaging being pulled in transitively. "packaging>=24.0", "pre-commit>=4.7.0", "pytest>=9.0.3,<10", "pytest-asyncio>=0.26.0", "pytest-cov>=6.2.1,<8", "pytest-rerunfailures>=16.0", "pytest-timeout", "pytest-xdist>=3.6.1", "ruff>=0.17.1", # The fsspec file store test reaches a real S3-compatible store through s3fs. # Its aiobotocore dependency pins botocore, so it also bounds the boto3 this lock can take. "s3fs>=2026.3.0", "testcontainers[postgres,redis]>=4.15.0", ] # The load test's tools container (loadtest/run.sh), which installs this group # alone from the lock, so a dependency release cannot move the gate between runs. loadtest = [ "httpx>=0.27", "psycopg[binary]>=3.3.6", "pytest>=8", "redis>=5", ] [tool.uv] # Otari is deployed as a Linux server and developed on Linux / macOS; it is # never packaged for Windows, so the lock resolves for those two platforms only. # # The lock also stops below Python 3.15. onnxruntime (the `ocr` extra) ships no cp315 wheel and # no sdist, so once an upgraded dependency forks the resolution at 3.15 (as # anyio, pandas and voyageai now do), that fork has no onnxruntime and uv # refuses the whole lock. Raise the bound when onnxruntime publishes cp315 # wheels; `requires-python` itself stays open. environments = [ "sys_platform == 'linux' and python_version < '3.15'", "sys_platform == 'darwin' and python_version < '3.15'", ] # authlib is imported nowhere here: apron-auth owns the OAuth mechanics and # pulls it in. It stays floored rather than listed as a direct dependency, # which would claim an import that does not exist. The floor is above # apron-auth's own >=1.6.12 because authlib has a dense advisory history, # including a signature-verification bypass fixed in 1.6.9. Matches the floor # the platform sets for the same dependency (otari-ai backend/pyproject.toml). # # **This binds uv, not every installer.** It governs `uv lock` and `uv sync`, so # it covers `uv.lock`, CI, and the Docker image, which is every way Otari is # actually built here. It does *not* reach a plain `pip install gateway`, where # apron-auth's own `>=1.6.12` is the only floor. That is accepted rather than # overlooked: promoting authlib to a direct dependency would claim an import # this tree does not have, and the advisory is in a transitive dependency whose # own maintainer sets the real floor. Revisit if Otari is ever published as a # wheel people pip-install directly. constraint-dependencies = [ "authlib>=1.7.2", # virtualenv clears its last advisory at 21.14.9. The override below lifts # the cap that blocked that upgrade. It does not hold this floor, because an # older virtualenv resolves against packaging 26.3 as well. "virtualenv>=21.14.9", ] # Every virtualenv from 21.14.6 on needs packaging>=26.3, and xai-sdk declares # packaging<26. xai-sdk arrives with any-llm-sdk[all], so that cap holds the # whole tree on an unpatched virtualenv. A constraint cannot loosen a declared # cap, so lifting it takes an override. xai-sdk reads packaging in one line, to # compare the major version of protobuf, so its cap guards a major rather than a # real incompatibility. An override replaces a declared specifier instead of # narrowing it, so the ceiling is named here. An open floor would strip a future # cap that does mean something. Remove this once xai-sdk allows packaging 26. override-dependencies = ["packaging>=26.3,<27"] [tool.uv.workspace] members = ["cli", "any-search", "any-fetch"] [tool.uv.sources] otari-agent = { workspace = true } any-search = { workspace = true } any-fetch = { workspace = true } [tool.setuptools] package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] include = ["gateway*"] [tool.setuptools.package-data] # The static/dashboard/* bundle is built, not committed (see .gitignore), so run # `make dashboard` before building a wheel. A pattern that matches nothing is # skipped silently, so a wheel built without it installs cleanly and the gateway # degrades to the tutorial page (gateway/dashboard.py). Patterns stay glob-only: # Vite decides the filenames, while pwa/ holds the manifest and icons that # index.html links and fonts/ the brand faces that globals.css asks for, each # with the SIL OFL text the license requires to travel with it (main.py mounts # them at /pwa and /fonts). A subdirectory needs its own line: these patterns do # not recurse, so a bundle directory missing here ships as a 404 at runtime. gateway = [ "py.typed", "templates/*.html", "templates/*.svg", "templates/email/*.html", "templates/email/*.txt", "static/dashboard/*", "static/dashboard/assets/*", "static/dashboard/pwa/*", "static/dashboard/fonts/*", "data/*.json", ] [tool.ruff] target-version = "py313" line-length = 120 src = ["src", "cli/src", "any-search/src", "any-fetch/src", "tests", "scripts"] # `demo/` is shell scripts; nothing for ruff to do there. extend-exclude = ["demo/"] [tool.ruff.lint] select = ["E", "F", "I", "PERF"] [tool.ruff.lint.per-file-ignores] "scripts/generate_openapi.py" = ["E402"] "tests/integration/conftest.py" = ["E402"] [tool.mypy] python_version = "3.13" strict = true # The libraries' tests and scripts are checked one library at a time (Makefile, # typecheck-python), because the two have files of the same names. files = ["src", "cli/src", "any-search/src", "any-fetch/src", "tests", "scripts"] # Standalone web-search adapters are independent services (own Dockerfile + # requirements), not part of the gateway package. They each ship a top-level # `app.py`, so type-checking them together trips mypy's duplicate-module guard. # Their behavior is covered by tests/unit/test_*_adapter.py. exclude = ["scripts/web-search-[^/]+-adapter/"] [[tool.mypy.overrides]] # setuptools-scm writes otari_agent/_version.py at build time, so it is absent # from a source tree nothing has built yet; __init__.py falls back when it is. module = ["otari_agent._version"] ignore_missing_imports = true [[tool.mypy.overrides]] module = ["testcontainers.*"] ignore_missing_imports = true [[tool.mypy.overrides]] module = ["trafilatura.*"] ignore_missing_imports = true [[tool.mypy.overrides]] # fsspec ships no stubs; the file store drives it through a handful of calls. module = ["fsspec", "fsspec.*"] ignore_missing_imports = true [[tool.mypy.overrides]] # The E2B SDK ships no stubs and is an optional extra, so it is absent from the # default environment the type check runs in. module = ["e2b", "e2b.*", "e2b_code_interpreter", "e2b_code_interpreter.*"] ignore_missing_imports = true [[tool.mypy.overrides]] # Optional/untyped extraction deps imported at the root (e.g. `import pypdfium2`). # Bare root names are required — a `foo.*` pattern alone does not match the # top-level `foo` module these are imported as. module = ["pypdfium2", "pypdfium2.*", "rapidocr_onnxruntime", "numpy", "PIL"] ignore_missing_imports = true [[tool.mypy.overrides]] # The OTLP proto messages and google.protobuf runtime helpers ship without stubs. module = ["google.protobuf.*", "opentelemetry.proto.*"] ignore_missing_imports = true