Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/skills/backend-standards/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: Backend conventions for the otari gateway (`src/gateway/`), async S
# Backend Standards: otari gateway (`src/gateway/`)

The gateway is an async FastAPI service: request handlers in `api/routes/`, business logic in
`services/`, ORM in `models/` (`entities.py` plus `tenancy.py`), migrations in
`services/`, ORM in `models/` (one module per domain), migrations in
`alembic/versions/`. This guide is the backend counterpart to the frontend skill and to the
path-scoped review instructions in
`.github/instructions/` (performance and security). `AGENTS.md` is the source of truth for
Expand Down Expand Up @@ -40,7 +40,7 @@ count = (await db.execute(select(func.count()).select_from(ModelPricing))).scala
## The SQLModel half: the reconciled control plane's tables

`models/tenancy.py` (organizations, workspaces, identities, memberships) is SQLModel rather
than `entities.py`'s declarative style, because its `Create`/`Update`/`Public` schemas are the
than the declarative `Base` style of the other domain modules, because its `Create`/`Update`/`Public` schemas are the
endpoint contracts the generated dashboard client is built from. Same session, same chain, three
extra rules:

Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Provider resolution is the seam on the request hot path, but it is not the only

Today the control plane resolves two ways, selected by [mode](docs/modes.md):

- **Standalone** (default): the gateway resolves against its own local database (users, keys, budgets, usage in `src/gateway/models/entities.py`). This is the open-source control plane in its simplest form.
- **Standalone** (default): the gateway resolves against its own local database (users, keys, budgets, usage in `src/gateway/models/`). This is the open-source control plane in its simplest form.
- **Hybrid** (`OTARI_AI_TOKEN` set): the gateway delegates resolution to a peer over HTTP (`src/gateway/api/routes/_platform.py`). Any service that implements the protocol can answer; otari.ai is the reference peer.

Hybrid mode is a *network* form of this seam, and it is worth not conflating it with an overlay. In hybrid mode a remote control plane answers the resolve protocol over HTTP, out of the gateway's process; the peer can be any service that implements the protocol. An overlay, by contrast, is an *in-process* build that binds its own adapters into the composition container (see [How a port is resolved](#how-a-port-is-resolved)) and runs in the same process as the core. Both let something other than the plain local logic answer; the difference is whether that something runs over the network or in the same process. So a hybrid peer and an overlay are two ways to reach the seam, not the same thing.
Expand Down
7 changes: 3 additions & 4 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@

from gateway.core.database import to_sync_url

# Importing anything from gateway.models registers every model module on this
# metadata (see gateway/models/__init__.py), which is what makes the comparison
# below cover the whole schema rather than the half this file names.
from gateway.models.entities import Base
# Importing any gateway.models module registers every table, so autogenerate
# compares against the whole schema.
from gateway.models.base import Base

logger = logging.getLogger("alembic")

Expand Down
6 changes: 3 additions & 3 deletions docs/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -7740,7 +7740,7 @@
"type": "object"
},
"OrgProviderKeyCreateRequest": {
"description": "What a caller sends to create a key.\n\nThe plaintext key is never stored as sent: the service encrypts it\n(`services/secret_box.py`) and keeps only the ciphertext and ``last4``,\nthe same convention `entities.ProviderCredential` already uses.",
"description": "What a caller sends to create a key.\n\nThe plaintext key is never stored as sent: the service encrypts it\n(`services/secret_box.py`) and keeps only the ciphertext and ``last4``,\nthe same convention `providers.ProviderCredential` already uses.",
"properties": {
"api_base": {
"anyOf": [
Expand Down Expand Up @@ -8472,7 +8472,7 @@
"type": "object"
},
"OrganizationGuardrailCreate": {
"description": "Request body for mandating a guardrail across an organization.\n\n``credential`` is never stored as sent: it is encrypted with\n``OTARI_SECRET_KEY`` and only the ciphertext is kept, the same convention\n`entities.WorkspaceMcpServer` and `entities.ProviderCredential` use. It is\nsent to the endpoint as ``Authorization: Bearer`` when the guardrail runs,\nso it authenticates this gateway to the guardrails service the entry names.\nA guardrail *vendor's* own key is not this: the guardrails service builds\nits guardrails from the operator's YAML and holds those itself.\n\nA credential therefore requires ``url``. The deployment's ``guardrails_url``\nis not necessarily encrypted (the shipped compose file makes it a same-host\n``http://`` sidecar) and this row cannot see what it is set to, so an entry\nthat fell back to it could not promise the bearer was sent over https. See\n`_require_url_for_credential`.\n\n``on`` is not offered. This plane mandates input-direction checks, which is\nthe only direction the request path enforces\n(`services.guardrails.run_input_guardrails`); an organization that could\nstore an output-direction mandate would be storing one nothing runs.",
"description": "Request body for mandating a guardrail across an organization.\n\n``credential`` is never stored as sent: it is encrypted with\n``OTARI_SECRET_KEY`` and only the ciphertext is kept, the same convention\n`tools.WorkspaceMcpServer` and `providers.ProviderCredential` use. It is\nsent to the endpoint as ``Authorization: Bearer`` when the guardrail runs,\nso it authenticates this gateway to the guardrails service the entry names.\nA guardrail *vendor's* own key is not this: the guardrails service builds\nits guardrails from the operator's YAML and holds those itself.\n\nA credential therefore requires ``url``. The deployment's ``guardrails_url``\nis not necessarily encrypted (the shipped compose file makes it a same-host\n``http://`` sidecar) and this row cannot see what it is set to, so an entry\nthat fell back to it could not promise the bearer was sent over https. See\n`_require_url_for_credential`.\n\n``on`` is not offered. This plane mandates input-direction checks, which is\nthe only direction the request path enforces\n(`services.guardrails.run_input_guardrails`); an organization that could\nstore an output-direction mandate would be storing one nothing runs.",
"example": {
"applies_to_all_workspaces": true,
"credential": "sk-guardrails-...",
Expand Down Expand Up @@ -16484,7 +16484,7 @@
"type": "object"
},
"WorkspaceMcpServerCreate": {
"description": "Request body for registering a server.\n\n``authorization_token`` is never stored as sent: it is encrypted with\n``OTARI_SECRET_KEY`` and only the ciphertext is kept, the same convention\n`entities.ProviderCredential` and `OrgProviderKey` already use.",
"description": "Request body for registering a server.\n\n``authorization_token`` is never stored as sent: it is encrypted with\n``OTARI_SECRET_KEY`` and only the ciphertext is kept, the same convention\n`providers.ProviderCredential` and `OrgProviderKey` already use.",
"properties": {
"allowed_tools": {
"anyOf": [
Expand Down
14 changes: 6 additions & 8 deletions scripts/demo_gif/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,14 @@
from sqlalchemy.orm import sessionmaker
from sqlmodel import col

from gateway.models.entities import (
APIKey,
Budget,
ModelAlias,
ModelPricing,
UsageLog,
User,
)
from gateway.models.api_keys import APIKey
from gateway.models.budgets import Budget
from gateway.models.money import to_usd
from gateway.models.pricing import ModelPricing
from gateway.models.providers import ModelAlias
from gateway.models.tenancy import Organization, Workspace
from gateway.models.usage import UsageLog
from gateway.models.users import User

URL = sys.argv[1] if len(sys.argv) > 1 else "sqlite:///./scripts/demo_gif/demo.db"
rng = random.Random(4242) # deterministic values across runs
Expand Down
5 changes: 4 additions & 1 deletion scripts/seed_usage_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
from sqlalchemy.orm import sessionmaker
from sqlmodel import col

from gateway.models.entities import APIKey, ModelPricing, UsageLog, User
from gateway.models.api_keys import APIKey
from gateway.models.pricing import ModelPricing
from gateway.models.tenancy import Organization, Workspace
from gateway.models.usage import UsageLog
from gateway.models.users import User
from gateway.services.pricing_service import gateway_tool_pricing_key
from gateway.services.tool_usage import TOOL_METER_NAMESPACE

Expand Down
16 changes: 10 additions & 6 deletions src/gateway/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,16 @@ endpoints.

## Data and migrations

Gateway ORM entities live in `models/entities.py`. Reconciled control-plane
SQLModel tables live in `models/tenancy.py`, and the newer tenancy-scoped
gateway tables whose `Public` schemas are endpoint contracts follow its style in
their own modules (`models/provider_keys.py`, `models/playground.py`). All of
them share `SQLModel.metadata`; `models/__init__.py` imports every table module
before Alembic uses it.
Put a table in its domain's model module (`models/budgets.py`,
`models/tenancy.py`, and so on). `models/base.py` holds `Base` and the shared
column types and mixins. Tables use the declarative `Base`, except those whose
`Public` schemas are endpoint contracts, which use SQLModel (`models/tenancy.py`,
`models/provider_keys.py`, `models/playground.py`). A new table module must join
the import list in `models/__init__.py`, or Alembic proposes dropping its tables.

Two classes are named `User`: `models/users.py` is the billing identity that
keys, budgets, and usage attach to; `models/tenancy.py` is the dashboard sign-in
identity.

Request code gets a session through `get_db`; non-request code uses
`create_session()`; the usage-log writer uses `create_log_session()`, which
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/adapters/telemetry_storage_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from gateway.core.sql import bucket_expr, canonical_bucket, dialect_name, match_any, utc_bound
from gateway.log_config import logger
from gateway.models.entities import AgentTelemetry
from gateway.models.usage import AgentTelemetry
from gateway.ports.telemetry_storage_port import (
BehaviorCounts,
BehaviorGroup,
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from gateway.core.feature import CoreFeature
from gateway.log_config import logger
from gateway.metrics import REGISTRY, Counter
from gateway.models.entities import APIKey
from gateway.models.api_keys import APIKey
from gateway.models.tenancy import User as TenancyUser
from gateway.ports.billing_port import BillingPort
from gateway.ports.entitlement_port import EntitlementPort
Expand Down
4 changes: 3 additions & 1 deletion src/gateway/api/routes/_passthrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
from gateway.inflight import track_request
from gateway.log_config import logger
from gateway.model_labeling import relabel_model
from gateway.models.entities import APIKey, ModelPricing, UsageLog
from gateway.models.api_keys import APIKey
from gateway.models.pricing import ModelPricing
from gateway.models.usage import UsageLog
from gateway.rate_limit import check_rate_limit
from gateway.services.budget_service import (
ZERO,
Expand Down
4 changes: 3 additions & 1 deletion src/gateway/api/routes/_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@
from gateway.metrics import REGISTRY, Histogram
from gateway.metrics import Counter as PrometheusCounter
from gateway.model_labeling import relabel_model
from gateway.models.entities import APIKey, ModelPricing, UsageLog
from gateway.models.api_keys import APIKey
from gateway.models.guardrails import GuardrailConfig
from gateway.models.mcp import McpServerConfig
from gateway.models.money import to_usd
from gateway.models.pricing import ModelPricing
from gateway.models.usage import UsageLog
from gateway.ports.model_provider_port import HostedAccessDeniedError, ModelProviderPort
from gateway.rate_limit import RateLimitInfo, check_rate_limit
from gateway.services.budget_service import (
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/api/routes/agent_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
_resolve_window,
)
from gateway.core.sql import MAX_FILTER_VALUES, bucket_expr, canonical_bucket, dialect_name, match_any
from gateway.models.entities import UsageLog
from gateway.models.usage import UsageLog
from gateway.ports.telemetry_storage_port import (
BehaviorCounts,
TelemetryFilter,
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/api/routes/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from gateway.api.routes._helpers import resolve_managed_workspace_id
from gateway.core.config import GatewayConfig
from gateway.log_config import logger
from gateway.models.entities import ModelAlias
from gateway.models.providers import ModelAlias
from gateway.repositories.users_repository import get_active_user
from gateway.services.alias_service import all_alias_names, refresh_alias_cache
from gateway.services.policy_store import all_policy_names
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
from gateway.api.routes._schema_derive import derive_request_base
from gateway.api.routes._tools import _strip_gateway_fields
from gateway.core.config import GatewayConfig
from gateway.models.entities import APIKey, ModelPricing
from gateway.models.api_keys import APIKey
from gateway.models.pricing import ModelPricing
from gateway.services.log_writer import LogWriter
from gateway.services.pricing_service import flat_request_cost, per_request_meters
from gateway.services.provider_kwargs import ResolvedProvider
Expand Down
4 changes: 3 additions & 1 deletion src/gateway/api/routes/batches.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
from gateway.core.metered_pricing import calculate_token_cost, quantize_cost
from gateway.core.usage import cache_read_tokens_of
from gateway.log_config import logger
from gateway.models.entities import APIKey, BatchRecord, UsageLog
from gateway.models.api_keys import APIKey
from gateway.models.inference import BatchRecord
from gateway.models.usage import UsageLog
from gateway.rate_limit import check_rate_limit
from gateway.services.batch_service import (
claim_batch_accounting,
Expand Down
10 changes: 2 additions & 8 deletions src/gateway/api/routes/budgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,10 @@
from sqlmodel import col

from gateway.api.deps import get_db, require_deployment_operator
from gateway.models.entities import (
MAX_COUNT_LIMIT,
Budget,
BudgetResetLog,
ScopedBudget,
User,
WorkspaceBudgetDefault,
)
from gateway.models.budgets import MAX_COUNT_LIMIT, Budget, BudgetResetLog, ScopedBudget, WorkspaceBudgetDefault
from gateway.models.money import MAX_USD_LIMIT, as_float, to_usd, to_usd_or_none
from gateway.models.tenancy import Workspace
from gateway.models.users import User
from gateway.services.budget_retiming import cadence_of, retime_ceilings_for_budget
from gateway.services.scoped_budget_service import ResetAlignment

Expand Down
4 changes: 3 additions & 1 deletion src/gateway/api/routes/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@
)
from gateway.core.config import HOSTED_OFFERING_INSTANCE, GatewayConfig
from gateway.core.metered_pricing import effective_rates
from gateway.models.entities import APIKey, PricingSnapshot, UsageLog
from gateway.models.api_keys import APIKey
from gateway.models.pricing import PricingSnapshot
from gateway.models.tenancy import User as TenancyUser
from gateway.models.tenancy import Workspace
from gateway.models.usage import UsageLog
from gateway.services.catalog_selectors import (
current_selector_index,
model_selector_for_slug,
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from gateway.api.deps import get_config, get_db, get_log_writer, verify_api_key_or_master_key
from gateway.api.routes._passthrough import BillingMeters, run_passthrough
from gateway.core.config import GatewayConfig
from gateway.models.entities import APIKey, ModelPricing
from gateway.models.api_keys import APIKey
from gateway.models.pricing import ModelPricing
from gateway.services.budget_service import estimate_cost
from gateway.services.log_writer import LogWriter
from gateway.services.pricing_service import input_token_cost
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
from gateway.api.routes._helpers import resolve_user_id
from gateway.core.config import GatewayConfig
from gateway.log_config import logger
from gateway.models.entities import APIKey, FileObject
from gateway.models.api_keys import APIKey
from gateway.models.tools import FileObject
from gateway.services.file_service import fetch_file
from gateway.services.file_store import FileStore
from gateway.services.workspace_scope import default_workspace_id
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from gateway.api.routes._schema_derive import derive_request_base
from gateway.api.routes._tools import _strip_gateway_fields
from gateway.core.config import GatewayConfig
from gateway.models.entities import APIKey, ModelPricing
from gateway.models.api_keys import APIKey
from gateway.models.pricing import ModelPricing
from gateway.services.log_writer import LogWriter
from gateway.services.pricing_service import per_image_cost
from gateway.services.provider_kwargs import ResolvedProvider
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
from gateway.api.deps import CallerOrganization, get_config, get_db, require_deployment_operator
from gateway.auth.models import generate_api_key, hash_key, key_prefix, key_suffix
from gateway.core.config import GatewayConfig
from gateway.models.entities import APIKey, User
from gateway.models.api_keys import APIKey
from gateway.models.tenancy import Workspace
from gateway.models.users import User
from gateway.repositories.users_repository import get_or_create_default_user, owned_by_organization
from gateway.services.model_access import is_allowlist_subset, validate_allowed_models
from gateway.services.workspace_scope import organization_default_workspace_id
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/api/routes/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
from gateway.core.database import release_session
from gateway.inflight import track_request
from gateway.log_config import logger
from gateway.models.entities import APIKey
from gateway.models.api_keys import APIKey
from gateway.rate_limit import check_rate_limit
from gateway.repositories.users_repository import get_active_user

Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
verify_catalog_reader,
)
from gateway.core.config import GatewayConfig
from gateway.models.entities import APIKey, ModelPricing
from gateway.models.api_keys import APIKey
from gateway.models.pricing import ModelPricing
from gateway.models.tenancy import User as TenancyUser
from gateway.services.merged_catalog_service import (
ModelObject,
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/moderations.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from gateway.api.deps import get_config, get_db, get_log_writer, verify_api_key_or_master_key
from gateway.api.routes._passthrough import BillingMeters, run_passthrough
from gateway.core.config import GatewayConfig
from gateway.models.entities import APIKey, ModelPricing
from gateway.models.api_keys import APIKey
from gateway.models.pricing import ModelPricing
from gateway.services.log_writer import LogWriter
from gateway.services.pricing_service import flat_request_cost, per_request_meters
from gateway.services.provider_kwargs import ResolvedProvider
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/organization_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@
)
from gateway.auth.models import generate_api_key, hash_key, key_prefix, key_suffix
from gateway.core.config import GatewayConfig
from gateway.models.entities import APIKey, User
from gateway.models.api_keys import APIKey
from gateway.models.tenancy import User as TenancyUser
from gateway.models.tenancy import Workspace
from gateway.models.users import User
from gateway.ports.growth_signal_port import GrowthActivationEvent
from gateway.repositories.users_repository import get_or_create_attribution_user
from gateway.services.model_access import is_allowlist_subset, validate_allowed_models
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/api/routes/organization_pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@

from gateway.api.deps import CurrentIdentity, ModelProviderPortDep, get_config, get_db, verify_master_key
from gateway.core.config import GatewayConfig
from gateway.models.entities import OrganizationModelPricing
from gateway.models.money import as_float
from gateway.models.pricing import OrganizationModelPricing

# The tier shape comes from the deployment pricing route rather than a second
# copy here. An override resolves into a transient ``ModelPricing`` and is read by
Expand Down
4 changes: 2 additions & 2 deletions src/gateway/api/routes/organization_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@
)
from gateway.core.config import GatewayConfig
from gateway.log_config import logger
from gateway.models.entities import ModelAlias, RoutingPolicy
from gateway.models.routing import PolicySpec
from gateway.models.providers import ModelAlias
from gateway.models.routing import PolicySpec, RoutingPolicy
from gateway.models.tenancy import User as TenancyUser
from gateway.models.tenancy import Workspace
from gateway.services.alias_service import all_alias_names
Expand Down
Loading
Loading