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
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const serving: OrganizationServing = {
}),
tariff("custom", {
organization_id: "org-a",
serving_class: "custom",
serving_class: "standard",
input_price_per_token: "0",
output_price_per_token: "0",
}),
Expand All @@ -62,7 +62,7 @@ const serving: OrganizationServing = {
{
deployed_model_id: "model",
alias: "example-model",
serving_class: "custom",
serving_class: "standard",
read_multiplier: "0.25",
write_multiplier_5m: "1",
write_multiplier_1h: "2",
Expand Down Expand Up @@ -188,9 +188,10 @@ describe("model pricing selector", () => {
it("shows effective playground prices when only realtime is configured", () => {
const { container } = mount(true, "org-a");
const playground = within(within(container).getByRole("region", { name: "Playground prices" }));
expect(playground.getByText("Bespoke · custom · realtime fallback")).toBeInTheDocument();
expect(playground.getByText("Bespoke · standard · realtime fallback")).toBeInTheDocument();
expect(playground.getAllByText("$0")).toHaveLength(2);
expect(playground.getAllByText("$2.00")).toHaveLength(1);
// The standard-class deal wins over the organisation's all-class price.
expect(playground.queryByText("$2.00")).not.toBeInTheDocument();
});

it("starts general then shows effective class prices and inherited batch/cache values", async () => {
Expand All @@ -212,14 +213,14 @@ describe("model pricing selector", () => {
}),
);
const rt = within(page.getByRole("region", { name: "Realtime prices" }));
expect(rt.getByText("Bespoke · custom")).toBeInTheDocument();
expect(rt.getByText("Bespoke · standard")).toBeInTheDocument();
expect(rt.getAllByText("$0")).toHaveLength(2);
expect(rt.getAllByText("$2.00")).toHaveLength(1);
expect(rt.queryByText("$2.00")).not.toBeInTheDocument();
const batch = within(
page.getByRole("region", { name: "Batch and flex prices" }),
);
expect(batch.getByText("Inherited · general model")).toBeInTheDocument();
expect(batch.getByText("Bespoke · all classes · realtime fallback")).toBeInTheDocument();
expect(batch.getByText("Bespoke · standard · realtime fallback")).toBeInTheDocument();
expect(
page.queryByRole("button", { name: "Manage Tariffs" }),
).not.toBeInTheDocument();
Expand All @@ -228,7 +229,7 @@ describe("model pricing selector", () => {
).not.toBeInTheDocument();
const cache = within(page.getByRole("region", { name: "Cache pricing" }));
expect(cache.getByText("0.25×")).toBeInTheDocument();
expect(cache.getAllByText("Inherited · general model")).toHaveLength(1);
expect(cache.queryByText("Inherited · general model")).not.toBeInTheDocument();
const sections = [...container.querySelectorAll("section[aria-label]")].map(
(x) => x.getAttribute("aria-label"),
);
Expand Down Expand Up @@ -271,7 +272,7 @@ describe("model pricing selector", () => {
expect(rt.queryByText("interactive")).not.toBeInTheDocument();
expect(rt.getByText("standard")).toBeInTheDocument();
});
it("shows custom target pricing even without a class-specific tariff", () => {
it("never adds a pricing class for overlay targets", () => {
vi.mocked(useOrganizationServing).mockReturnValue({
data: {
...serving,
Expand All @@ -286,11 +287,11 @@ describe("model pricing selector", () => {
isError: false,
} as unknown as ReturnType<typeof useOrganizationServing>);
const { container } = mount(true, "org-a");
expect(
within(
within(container).getByRole("region", { name: "Realtime prices" }),
).getByText("custom"),
).toBeInTheDocument();
const rt = within(
within(container).getByRole("region", { name: "Realtime prices" }),
);
expect(rt.queryByText("custom")).not.toBeInTheDocument();
expect(rt.getByText("standard")).toBeInTheDocument();
});
it.each(["loading", "error"])(
"never disguises %s organisation data as general prices",
Expand Down
22 changes: 3 additions & 19 deletions dashboard/src/components/features/models/manage/ModelPricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,25 +89,9 @@ export function ModelPricing({
const orgCache = (organization ? (own.data?.cache_tariffs ?? []) : []).filter(
(row) => row.deployed_model_id === model.id,
);
const selectedOverlay = own.data?.overlays.find(
(row) => row.deployed_model_id === model.id,
);
// Billing resolves to `standard`, or `custom` when the overlay's explicit
// targets apply; prices under any other class name are never selected.
const classes = organization
? [
"standard",
...(selectedOverlay?.targets ||
rows.some(
(row) =>
row.organization_id === organization &&
row.serving_class === "custom",
) ||
orgCache.some((row) => row.serving_class === "custom")
? ["custom"]
: []),
]
: ["standard"];
// Requests are priced by the class they are routed to; every model has
// `standard`, and prices under any other class name are never selected.
const classes = ["standard"];
const windows = [
...new Set([
...(model.allowed_batch_completion_windows ?? []),
Expand Down
6 changes: 3 additions & 3 deletions dashboard/src/components/features/serving/OverlayDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export function OverlayDetails({
<div className="space-y-4 text-sm">
<dl className="grid gap-3 sm:grid-cols-2">
<div>
<dt className="text-muted-foreground">Targets</dt>
<dd>{overlay?.targets ? "Custom targets" : "Standard"}</dd>
<dt className="text-muted-foreground">Scheduling hints</dt>
<dd>{overlay?.targets ? "Set (see below)" : "None"}</dd>
</div>
<div>
<dt className="text-muted-foreground">Self-hosted only</dt>
Expand All @@ -43,7 +43,7 @@ export function OverlayDetails({
</div>
{overlay?.targets && (
<div>
<dt className="text-muted-foreground">Explicit targets</dt>
<dt className="text-muted-foreground">Priority and latency targets</dt>
<dd>
TTFT {overlay.targets.ttft_ms} ms · ITL {overlay.targets.itl_ms}{" "}
ms · priority {overlay.targets.priority ?? 0}
Expand Down
2 changes: 1 addition & 1 deletion docs/model-serving-class-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ resolvers indefinitely:
| --- | --- |
| `deployed_models.serving_classes` JSON presets; `model_provisioning.rs` | Class catalog configuration replaces numeric target presets; retain shared model metadata. |
| `users.granted_serving_classes`, `default_serving_class`; sync account snapshots | Explicit class selection replaces grants and implicit defaults. Preserve routing preferences such as `self_hosted_only`. |
| `model_overlays.default_serving_class`, `targets`; `org_overlays.rs`, serving API | Replace target/default selection with class overrides. Retain account/model ownership, catalog versioning and prices. |
| `model_overlays.default_serving_class`, `targets`; `org_overlays.rs`, serving API | Default selection is retired. `targets` are scheduling hints (priority, latency targets) that never select a class; overlay and account prices override one of the model's classes. Retain account/model ownership, catalog versioning and prices. |
| `inference/middleware.rs`, `onwards/src/serving.rs`, `onwards/src/handlers.rs` | Replace suffix/default resolution and numeric target injection; preserve ingress scrubbing and trusted priority handling. |
| `sync/onwards_config` and composite member rows | Keep legacy routing until all live consumers have migrated; new classes remain dormant until explicit activation. |
| Tariff ledgers, analytics, queued billing records | Retain historical class keys and compatible readers; introducing class UUIDs does not rewrite history. |
Expand Down
22 changes: 14 additions & 8 deletions docs/src/how-to/organization-model-overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ retires all catalog-owned entries; a missing/unconfigured catalog is not a clean
request. Future-dated prices must be resolved before catalog adoption or editing:
startup reconciliation applies immediately and does not author schedules.

## Example: batch/flex and custom-target prices
## Example: batch/flex prices, a standard-class deal and scheduling hints

The organization, model alias and prices below are fictional. Token prices are
expressed per million tokens:
Expand Down Expand Up @@ -104,9 +104,9 @@ models:
write_multiplier_24h: "2"
read_multiplier: "0.10"
class_pricing:
custom:
standard:
tariffs:
- name: custom-realtime
- name: standard-realtime
purpose: realtime
input_per_million_tokens: "3.00"
output_per_million_tokens: "9.00"
Expand All @@ -117,11 +117,17 @@ models:
read_multiplier: "0.15"
```

Realtime requests to which the overlay's explicit `targets` apply resolve as
`custom`; everything else resolves as `standard`. Batch and flex use standard;
flex uses the batch `1h` tariff. Prefer putting batch prices in the all-class
`tariffs` list. `class_pricing.standard` also supports batch; `custom` batch
prices are rejected by validation.
A request's serving class is where it is routed, and `class_pricing` prices one
of the model's classes (`standard` until models carry more). A class price wins
over the all-class `tariffs` for requests in that class. Batch and flex use
standard; flex uses the batch `1h` tariff. Prefer putting batch prices in the
all-class `tariffs` list.

`targets` are scheduling hints for this organisation on this model: the priority
and the time-to-first-token and inter-token-latency targets are sent on realtime
requests to members of kind `dynamo` that accept scheduling fields. They change neither the
request's class nor its price. The targets are labels the serving stack may use
for alerting; nothing routes on them.

## Billing, display and admission

Expand Down
2 changes: 1 addition & 1 deletion docs/src/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ model_provisioning:
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Apply the declarative model catalog during startup. |
| `directory` | path | `/app/model-provisioning.d` | Directory containing one `.yaml` or `.yml` document per canonical model. |
| `org_overlays_directory` | path | `/app/org-overlays.d` | Directory containing one `.yaml` or `.yml` document per organisation with per-model serving overrides (explicit `targets`, `self_hosted_only`). Applied after the model catalog when `enabled` is set. A missing directory is an empty catalog, so a deployment that does not mount one applies no overlays; an existing path that is not a directory fails startup. |
| `org_overlays_directory` | path | `/app/org-overlays.d` | Directory containing one `.yaml` or `.yml` document per organisation with per-model serving overrides (`targets`, which are scheduling hints and never change the class or price; `self_hosted_only`; prices). Applied after the model catalog when `enabled` is set. A missing directory is an empty catalog, so a deployment that does not mount one applies no overlays; an existing path that is not a directory fails startup. |
| `account_limits_directory` | path | `/app/account-limits.d` | Directory containing one `.yaml` or `.yml` document per account with its own realtime in-flight limits, prices, scheduling priority and latency targets on virtual models. Applied after the model catalog and organisation overlays when `enabled` is set; limits replace every stored per-account limit, and the other settings are owned row by row. A missing directory changes nothing; an empty one clears what the files own. See [Account limits](./model-provisioning.md#account-limits). |

When enabled, the directory must exist. Startup fails before any provisioning
Expand Down
1 change: 0 additions & 1 deletion dwctl/src/account_limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,7 +1095,6 @@ mod tests {
(overlay.priority, overlay.ttft_ms, overlay.itl_ms),
(Some(300), Some(1000), Some(30))
);
assert!(overlay.targets.is_none(), "policy never becomes billed custom targets");
assert_eq!(gateway_limit(&pool, "org/model", org).await, 900);

// Removing the files clears what this catalog owns, keeping history.
Expand Down
Loading
Loading