feat: route serving classes to their own upstreams
Summary
Serving classes become routable. A model that opts in with clay.routing_mode: class_routes serves model from its standard class and model:<class> from each other class it declares in clay.class_routes. Each class has its own upstream (endpoint and upstream model name), realtime prices and default realtime in-flight limit. Accounts can set priority, scheduling hints, toleration presets, limits and prices per class.
Nothing changes for a model until its catalogue entry sets routing_mode: class_routes. Every existing model stays on the legacy route.
What changes
Catalogue (model_provisioning)
- New
clay.routing_mode(legacydefault |class_routes). The provisioning writer setsdeployed_models.routing_modefrom it, so activation and rollback are catalogue edits. class_routesrequiresstandard. Other classes are optional (previouslystandardandfastwere both required).- New
class_routes.<class>.realtime_inflight_limit. When omitted, the class uses the model's limit. - Class synonyms (
aliases) are retired and must be empty. Class names are alwaysmodelandmodel:<class>. - Activation is refused for a model without
standardor with redirect traffic rules.
Migration (additive): model_serving_classes.realtime_inflight_limit, the realtime_inflight_class_overrides table (account Ć non-standard class limits), notify triggers, and effective_model_tariff updated to the billing rule below.
Gateway sync
- One target per class:
modelfor standard,model:<class>otherwise. - Each target forwards to its class's upstream, without the composite's members, fallback or stream continuation.
- Deny rules apply to every class. The model's batch capacity applies to standard only.
- The standard target keeps the key
model, so existing limiter keys and the batch lane are unchanged. - An activation the sync cannot serve falls back to the legacy route and logs
invalid_class_activation.
Account files
classesmay name any class the model declares, not onlystandard.- Limits, prices, priority, hints and toleration presets apply per class. Model-level priority and presets apply to every class.
Requests
- Class routes are realtime only. A flex or background request naming
model:<class>gets 400serving_class_realtime_only, and batch lines naming one are refused at upload. - Analytics records
model:<class>under the model's name, withrequested_serving_classandresolved_serving_classset to the class. The request is priced as that class. /v1/modelslistsmodel:<class>for each non-standard class.- A class route inherits its model's prompt-cache enablement. Standard keeps the model's cache scope.
Docs: model provisioning reference (class routing, account files per class) and the billing precedence guide.
Decisions to review
- An account's class-less price covers the
standardclass only. On another class, the account pays its price for that class if it has one, otherwise the model's public price for that class. A discount on the standard tier therefore never undercuts a premium class. Cache multipliers follow the same rule. - Discovery lists class entries without prices for now. Class prices in
/v1/modelsare a follow-up. self_hosted_onlydoes not apply to class routes. The class names its upstream directly.- An invalid activation keeps serving the legacy route instead of dropping the model.
- Rollback is
routing_mode: legacyplus a redeploy. Keep the legacydeployments/routing/tariffswhile a model is activated. Older releases reject therouting_modefield, so an image rollback needs the catalogue rolled back first.
Follow-ups (not in this PR)
- Class prices in
/v1/models. - A
classlabel on existing gateway metrics. - A dry activation of one model against a production-shaped database.
- A two-instance Redis check of per-class limits.
- A gateway performance re-run.
Testing
- Unit and integration tests for catalogue validation, activation and rollback, class limits and prices in account files, per-class targets, toleration presets per class, async/batch refusals, pricing precedence, discovery and continuation.
- End to end (
test::serving_classes): realtime requests reach each class's upstream and are billed at that class's price; flex and batch naming a class are refused; discovery lists the class. just lint rustandjust test rustpass locally.
š¤ Generated with Claude Code