# Files changed · PR \#9861 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/9861/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(web\): edit usage prices across selected environments](https://git.cafe/pingdotgg/t3code/pull/9861.md)

Version: 12

Head commit: 6afb81c2f22d14c086b2605ada8a0b053a7e287e

Base commit: ed79de694090076e4b038081658b0b5d9e709411

Comparison base: ed79de694090076e4b038081658b0b5d9e709411

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/web/src/components/usage/UsagePage.tsx

Status: modified; +28; −7

```diff
@@ -4,7 +4,13 @@
   type EnvironmentId,
   type UsageProviderKind,
 } from "@t3tools/contracts";
+import {
+  CircleAlertIcon,
+  ChevronDownIcon,
+  CircleDashedIcon,
+  RefreshCwIcon,
-import { CircleAlertIcon, ChevronDownIcon, CircleDashedIcon, RefreshCwIcon } from "lucide-react";
+  SlidersHorizontalIcon,
+} from "lucide-react";
 import { useMemo, useState } from "react";
 
 import {
@@ -32,7 +38,14 @@
   makeWindow,
 } from "@t3tools/shared/usageFormat";
 import { Button } from "../ui/button";
-import { Menu, MenuCheckboxItem, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu";
+import {
+  Menu,
+  MenuCheckboxItem,
+  MenuItem,
+  MenuPopup,
+  MenuSeparator,
+  MenuTrigger,
+} from "../ui/menu";
 import { ScrollArea } from "../ui/scroll-area";
 import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
 import { SidebarInset } from "../ui/sidebar";
@@ -287,11 +300,6 @@
 
         <ScrollArea className="min-h-0 flex-1">
           <WorkspacePageContainer width="wide">
-            {!showingLimits ? (
-              <div className="flex justify-end">
-                <UsagePriceOverrides usage={environments} />
-              </div>
-            ) : null}
             {selectedEnvironments.length === 0 ? (
               <p className="text-sm text-muted-foreground">
                 {environments.length === 0
@@ -629,6 +637,7 @@
   readonly duplicateSources: readonly string[];
   readonly staleEnvironments: readonly string[];
 }) {
+  const [modelPricesOpen, setModelPricesOpen] = useState(false);
   const allSelected = selectedEnvironmentIds === null;
   const label = allSelected
     ? "All environments"
@@ -742,8 +751,20 @@
               staleEnvironments={staleEnvironments}
             />
           ) : null}
+          <MenuSeparator />
+          <MenuItem onClick={() => setModelPricesOpen(true)}>
+            <SlidersHorizontalIcon aria-hidden />
+            Model prices
+          </MenuItem>
         </MenuPopup>
       </Menu>
+      {modelPricesOpen ? (
+        <UsagePriceOverrides
+          usage={environments}
+          initialSelectedEnvironmentIds={selectedEnvironmentIds}
+          onOpenChange={setModelPricesOpen}
+        />
+      ) : null}
     </>
   );
 }
```

## apps/web/src/components/usage/UsagePriceOverrides.tsx

Status: modified; +507; −271

```diff
@@ -1,15 +1,15 @@
 import { useAtomValue } from "@effect/atom-react";
+import type { EnvironmentId } from "@t3tools/contracts";
+import { ChevronDownIcon, PlusIcon, RotateCcwIcon, XIcon } from "lucide-react";
+import * as Option from "effect/Option";
+import { AsyncResult, Atom } from "effect/unstable/reactivity";
-import type { EnvironmentId, UsageModelPriceOverride } from "@t3tools/contracts";
+import { useRef, useState } from "react";
-import { useState } from "react";
 
 import { isElectron } from "../../env";
-import {
-  type EnvironmentPresentation,
-  useEnvironments,
+import { cn } from "../../lib/utils";
+import { environmentPresentations } from "../../state/presentation";
-  usePrimaryEnvironmentId,
-} from "../../state/environments";
 import { serverEnvironment } from "../../state/server";
+import { environmentSession } from "../../state/session";
-import { useEnvironmentSessionState } from "../../state/session";
 import { useAtomCommand } from "../../state/use-atom-command";
 import type { EnvironmentUsageStatus } from "../../state/usage";
 import {
@@ -20,6 +20,7 @@
 import {
   Dialog,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogPanel,
   DialogPopup,
@@ -27,292 +28,527 @@
 } from "../ui/dialog";
 import { Input } from "../ui/input";
 import { Label } from "../ui/label";
+import { Menu, MenuCheckboxItem, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
+import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";
+import { USAGE_PRICE_FIELDS } from "./usagePriceForm";
+import {
+  isEmptyUsagePriceDraft,
+  usagePriceCell,
+  usagePriceTableChanges,
-import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
+  usagePriceTableErrors,
-import { parseUsagePriceForm, USAGE_PRICE_FIELDS, usagePriceForm } from "./usagePriceForm";
+  type UsagePriceDraft,
+  type UsagePriceField,
+} from "./usagePriceTable";
+import {
+  writeUsagePrices,
+  type UsagePriceTarget,
+  type UsagePriceWriteResult,
+} from "./usagePriceTargets";
 
-export function UsagePriceOverrides({
-  usage,
-}: {
-  readonly usage: readonly EnvironmentUsageStatus[];
-}) {
-  const [open, setOpen] = useState(false);
-  return (
-    <>
-      <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
-        Model prices
-      </Button>
-      {open ? <ModelPricesDialog usage={usage} onOpenChange={setOpen} /> : null}
-    </>
+const priceTargetsAtom = Atom.make((get): readonly UsagePriceTarget[] =>
+  [...get(environmentPresentations.presentationsAtom)].map(([environmentId, environment]) => {
+    const settings = get(serverEnvironment.settingsValueAtom(environmentId));
+    const session = get(environmentSession.sessionStateAtom(environmentId));
+    const sessionAccess = {
+      session: Option.getOrNull(AsyncResult.value(session)),
+      isPending: session.waiting,
+      hasError: session._tag === "Failure",
+    };
+    const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
+    const access = isPrimary
+      ? resolvePrimaryOperateAccess({ ...sessionAccess, isPrimary, hasDesktopBridge: isElectron })
+      : resolveRemoteOperateAccess(sessionAccess);
+    return {
+      environmentId,
+      label: environment.entry.target.label,
+      prices: settings?.usagePriceOverrides ?? null,
+      unavailable:
+        environment.connection.phase !== "connected"
+          ? "Offline"
+          : settings === null
+            ? "Prices not loaded"
+            : environment.serverConfig?.environment.capabilities.usagePriceOverrides !== true
+              ? "Update server to edit prices"
+              : access === "pending"
-  );
+                ? "Checking permissions…"
-}
+                : access === "denied"
+                  ? "Read-only access"
+                  : null,
+    };
+  }),
+);
 
+type SaveAttempt = {
+  readonly drafts: readonly UsagePriceDraft[];
-function ModelPricesDialog({
+  readonly destinations: readonly {
+    readonly environmentId: EnvironmentId;
+    readonly label: string;
+  }[];
+  readonly results: ReadonlyMap<
+    EnvironmentId,
+    UsagePriceWriteResult | { readonly status: "saving" }
+  >;
+};
+
+export function UsagePriceOverrides({
   usage,
+  initialSelectedEnvironmentIds,
   onOpenChange,
 }: {
   readonly usage: readonly EnvironmentUsageStatus[];
+  readonly initialSelectedEnvironmentIds: ReadonlySet<EnvironmentId> | null;
   readonly onOpenChange: (open: boolean) => void;
 }) {
+  const environments = useAtomValue(priceTargetsAtom);
+  const [selectedIds, setSelectedIds] = useState(initialSelectedEnvironmentIds);
+  const selected = environments.filter(
+    (environment) => selectedIds === null || selectedIds.has(environment.environmentId),
+  );
+  const [drafts, setDrafts] = useState<readonly UsagePriceDraft[]>([]);
+  const [pending, setPending] = useState(false);
+  const [attempt, setAttempt] = useState<SaveAttempt | null>(null);
+  const focusRowRef = useRef<string | null>(null);
+  const nextRowId = useRef(0);
+  const updateSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
+  const customModels = [
+    ...new Set(selected.flatMap((environment) => Object.keys(environment.prices ?? {}))),
+  ].sort();
+  const models = [
+    ...new Set([
+      ...customModels,
+      ...usage
+        .filter((environment) =>
+          selected.some((target) => target.environmentId === environment.environmentId),
+        )
+        .flatMap((environment) => environment.summary?.buckets.map((bucket) => bucket.model) ?? []),
+    ]),
+  ].sort();
+  // Keep new rows in place while successful environments publish their updated settings.
+  const newModels = new Set(
+    drafts.filter((draft) => draft.isNew).map((draft) => draft.model.trim()),
+  );
+  const rows: readonly UsagePriceDraft[] = [
+    ...customModels
+      .filter((model) => attempt === null || !newModels.has(model))
+      .map(
+        (model) =>
+          drafts.find((draft) => draft.id === `model:${model}`) ?? {
+            id: `model:${model}`,
+            model,
+            isNew: false,
+            values: {},
+          },
+      ),
+    ...drafts.filter((draft) => draft.isNew),
+  ];
+  const stagedDrafts = drafts.filter((draft) => !isEmptyUsagePriceDraft(draft));
+  const errors = usagePriceTableErrors(selected, stagedDrafts);
+  for (const draft of stagedDrafts) {
+    if (!draft.isNew) continue;
+    if (draft.model.trim() === "") errors.set(draft.id, "Enter a model ID.");
+    else if (
+      attempt === null &&
+      (customModels.includes(draft.model.trim()) ||
+        drafts.some((other) => other.id !== draft.id && other.model.trim() === draft.model.trim()))
+    )
+      errors.set(draft.id, "This model already has a row. Edit its prices there.");
+  }
+  const failedDestinations =
+    attempt?.destinations.filter(
+      (destination) => attempt.results.get(destination.environmentId)?.status === "failed",
+    ) ?? [];
+  const locked = pending || failedDestinations.length > 0;
+  const hasChanges = stagedDrafts.length > 0;
+  const destinationLabel =
+    selected.length === 1 ? selected[0]!.label : `${selected.length} environments`;
+  const selectionLabel = selectedIds === null ? "All environments" : destinationLabel;
+  const discard = () => {
+    setDrafts([]);
+    setAttempt(null);
+  };
+  const selectEnvironments = (ids: ReadonlySet<EnvironmentId> | null) => {
+    setSelectedIds(ids);
+    setAttempt(null);
+  };
+  const updateDraft = (draft: UsagePriceDraft) => {
+    setDrafts((previous) =>
+      previous.some((entry) => entry.id === draft.id)
+        ? previous.map((entry) => (entry.id === draft.id ? draft : entry))
+        : [...previous, draft],
+    );
+    setAttempt(null);
+  };
+  const editCell = (row: UsagePriceDraft, field: UsagePriceField, value: string) => {
+    const values = { ...row.values, [field]: value };
+    const original = usagePriceCell(selected, row.model, field);
+    if (
+      !row.isNew &&
+      original.placeholder !== "Mixed" &&
+      original.placeholder !== "Unavailable" &&
+      value === original.value
+    )
+      delete values[field];
+    if (!row.isNew && Object.keys(values).length === 0)
+      setDrafts((previous) => previous.filter((entry) => entry.id !== row.id));
+    else updateDraft({ ...row, values });
+  };
+  const save = async (retry = false) => {
+    if (pending) return;
+    const destinations = retry ? failedDestinations : selected;
+    const edits = retry && attempt ? attempt.drafts : stagedDrafts;
+    if (destinations.length === 0 || edits.length === 0) return;
+    const targets = destinations.map(
+      (destination) =>
+        environments.find(
+          (environment) => environment.environmentId === destination.environmentId,
+        ) ?? { ...destination, prices: null, unavailable: "Environment removed" },
+    );
+    const changes = new Map(
+      targets.map((target) => [target.environmentId, usagePriceTableChanges(target, edits)]),
+    );
+    setPending(true);
+    setAttempt((previous) => ({
+      drafts: edits,
+      destinations: retry && previous ? previous.destinations : targets,
+      results: new Map([
+        ...(retry && previous ? previous.results : []),
+        ...targets.map((target) => [target.environmentId, { status: "saving" } as const] as const),
+      ]),
+    }));
+    let failed = false;
+    await writeUsagePrices({
+      targets: targets.map((target) => ({
+        ...target,
+        unavailable:
-  const { environments } = useEnvironments();
-  const primaryEnvironmentId = usePrimaryEnvironmentId();
-  const [selectedId, setSelectedId] = useState<EnvironmentId | null>(primaryEnvironmentId);
-  const environment =
-    environments.find((entry) => entry.environmentId === selectedId) ?? environments[0];
-  const summary = usage.find(
-    (entry) => entry.environmentId === environment?.environmentId,
+          target.unavailable ?? [...changes.get(target.environmentId)!.errors.values()][0] ?? null,
-  )?.summary;
+      })),
+      changes: new Map([...changes].map(([id, plan]) => [id, plan.changes])),
+      write: updateSettings,
+      onResult: (environmentId, result) => {
+        if (result.status === "failed") failed = true;
+        setAttempt((previous) =>
+          previous === null
+            ? null
+            : { ...previous, results: new Map(previous.results).set(environmentId, result) },
+        );
+      },
+    });
+    setPending(false);
+    if (!failed) setDrafts([]);
+  };
-  const models = [...new Set(summary?.buckets.map((bucket) => bucket.model) ?? [])].sort();
 
   return (
-    <Dialog open onOpenChange={onOpenChange}>
-      <DialogPopup className="max-w-xl">
+    <Dialog
+      open
+      onOpenChange={(open) => {
+        if (!pending) onOpenChange(open);
+      }}
+    >
+      <DialogPopup className="max-w-3xl">
         <DialogHeader>
+          <DialogTitle>Custom model prices</DialogTitle>
-          <DialogTitle>Model prices</DialogTitle>
           <DialogDescription>
-            Override estimated costs using USD per million tokens. Prices apply to all saved history
+            Prices apply to all past and future usage on the environments you select.
-            on the selected environment.
           </DialogDescription>
         </DialogHeader>
-        <DialogPanel className="grid gap-5 pb-6">
-          {environment ? (
-            <>
-              <div className="grid gap-1.5">
-                <Label htmlFor="usage-prices-environment">Environment</Label>
-                <Select
-                  value={environment.environmentId}
-                  onValueChange={(value) => {
-                    const next = environments.find((entry) => entry.environmentId === value);
-                    if (next) setSelectedId(next.environmentId);
+        <DialogPanel className="grid gap-4">
-                  }}
+          <div className="flex flex-wrap items-center justify-between gap-2">
+            <div className="flex min-w-0 items-center gap-2">
+              <Label id="usage-prices-apply-label" className="shrink-0">
+                Apply to
+              </Label>
+              <Menu>
+                <MenuTrigger
+                  render={<Button variant="outline" size="sm" className="min-w-0" />}
+                  aria-labelledby="usage-prices-apply-label usage-prices-selection"
+                  disabled={pending || hasChanges}
                 >
-                  <SelectTrigger id="usage-prices-environment">
-                    <SelectValue>{environment.label}</SelectValue>
-                  </SelectTrigger>
-                  <SelectPopup>
-                    {environments.map((entry) => (
-                      <SelectItem key={entry.environmentId} value={entry.environmentId}>
-                        {entry.label}
-                      </SelectItem>
+                  <span id="usage-prices-selection" className="truncate">
+                    {selectionLabel}
+                  </span>
+                  <ChevronDownIcon className="size-3.5 shrink-0" aria-hidden />
+                </MenuTrigger>
+                <MenuPopup align="start" className="w-80 max-w-[calc(100vw-2rem)]">
+                  <MenuCheckboxItem
+                    checked={selectedIds === null}
+                    closeOnClick={false}
+                    onCheckedChange={(checked) => selectEnvironments(checked ? null : new Set())}
+                  >
+                    All environments
+                  </MenuCheckboxItem>
+                  <MenuSeparator />
+                  {environments.map((environment) => (
+                    <MenuCheckboxItem
+                      key={environment.environmentId}
+                      checked={selectedIds === null || selectedIds.has(environment.environmentId)}
+                      closeOnClick={false}
+                      onCheckedChange={(checked) => {
+                        const next = new Set(selected.map((target) => target.environmentId));
+                        if (checked) next.add(environment.environmentId);
+                        else next.delete(environment.environmentId);
+                        selectEnvironments(next.size === environments.length ? null : next);
+                      }}
+                    >
+                      <span className="flex min-w-0 items-center gap-3">
+                        <span className="min-w-0 flex-1 truncate">{environment.label}</span>
+                        {environment.unavailable ? (
+                          <span className="text-xs text-muted-foreground">
+                            {environment.unavailable}
+                          </span>
+                        ) : null}
+                      </span>
+                    </MenuCheckboxItem>
+                  ))}
+                </MenuPopup>
+              </Menu>
+            </div>
+            <span className="text-xs text-muted-foreground">USD / million tokens</span>
+          </div>
+          {selected.length === 0 ? (
+            <p className="text-sm text-muted-foreground">
+              {environments.length === 0
+                ? "Connect an environment to set model prices."
+                : "Select an environment to see and change its model prices."}
+            </p>
+          ) : (
+            <>
+              <div className="min-w-0 overflow-hidden rounded-lg border border-border">
+                <Table className="min-w-160 table-fixed" aria-label="Custom model prices">
+                  <colgroup>
+                    <col className="w-[34%]" />
+                    {USAGE_PRICE_FIELDS.map((field) => (
+                      <col key={field.key} />
                     ))}
-                  </SelectPopup>
+                    <col className="w-10" />
-                </Select>
+                  </colgroup>
+                  <TableHeader>
+                    <TableRow className="hover:bg-transparent">
+                      <TableHead className="pl-3">Model ID</TableHead>
+                      {USAGE_PRICE_FIELDS.map((field) => (
+                        <TableHead key={field.key}>{field.label}</TableHead>
+                      ))}
+                      <TableHead className="px-1">
+                        <Button
+                          size="icon-xs"
+                          variant="ghost"
+                          aria-label="Add model price"
+                          disabled={locked}
+                          onClick={() => {
+                            const id = `new:${nextRowId.current++}`;
+                            focusRowRef.current = id;
+                            updateDraft({ id, model: "", isNew: true, values: {} });
+                          }}
+                        >
+                          <PlusIcon aria-hidden />
+                        </Button>
+                      </TableHead>
+                    </TableRow>
+                  </TableHeader>
+                  <TableBody>
+                    {rows.length === 0 ? (
+                      <TableRow className="hover:bg-transparent">
+                        <TableCell
+                          colSpan={6}
+                          className="py-8 text-center whitespace-normal text-muted-foreground"
+                        >
+                          {selected.some((environment) => environment.prices === null)
+                            ? "Some environment prices are unavailable."
+                            : "No custom prices. Add a row to override automatic pricing."}
+                        </TableCell>
+                      </TableRow>
+                    ) : (
+                      rows.map((row) => (
+                        <TableRow
+                          key={row.id}
+                          data-row-id={row.id}
+                          className="hover:bg-transparent"
+                        >
+                          <TableCell className="pl-3 whitespace-normal">
+                            {row.isNew ? (
+                              <Input
+                                size="compact"
+                                value={row.model}
+                                ref={(node) => {
+                                  if (node && focusRowRef.current === row.id) {
+                                    node.focus();
+                                    focusRowRef.current = null;
+                                  }
+                                }}
+                                aria-label="New model ID"
+                                aria-invalid={
+                                  (row.model.trim() !== "" && errors.has(row.id)) || undefined
+                                }
+                                list="usage-price-models"
+                                placeholder="Model ID"
+                                autoComplete="off"
+                                spellCheck={false}
+                                disabled={locked}
+                                onChange={(event) =>
+                                  updateDraft({ ...row, model: event.target.value })
+                                }
+                              />
+                            ) : (
+                              <span
+                                className={cn(
+                                  "block break-all",
+                                  row.removed && "text-muted-foreground line-through",
+                                )}
+                              >
+                                {row.model}
+                              </span>
+                            )}
+                            {errors.has(row.id) &&
+                            (row.model.trim() !== "" ||
+                              Object.values(row.values).some((value) => value !== "")) ? (
+                              <p role="alert" className="mt-1 text-xs text-destructive">
+                                {errors.get(row.id)}
+                              </p>
+                            ) : null}
+                          </TableCell>
+                          {row.removed ? (
+                            <TableCell colSpan={4} className="text-muted-foreground">
+                              Automatic pricing after saving
+                            </TableCell>
+                          ) : (
+                            USAGE_PRICE_FIELDS.map((field) => {
+                              const cell = usagePriceCell(selected, row.model, field.key);
+                              return (
+                                <TableCell key={field.key} className="px-1">
+                                  <Input
+                                    size="compact"
+                                    inputMode="decimal"
+                                    aria-label={`${field.label} price for ${row.model || "new model"}`}
+                                    value={row.values[field.key] ?? (row.isNew ? "" : cell.value)}
+                                    placeholder={
+                                      row.isNew
+                                        ? field.optional
+                                          ? "Input rate"
+                                          : "0.00"
+                                        : cell.placeholder
+                                    }
+                                    autoComplete="off"
+                                    disabled={locked}
+                                    className="tabular-nums"
+                                    onChange={(event) =>
+                                      editCell(row, field.key, event.target.value)
+                                    }
+                                  />
+                                </TableCell>
+                              );
+                            })
+                          )}
+                          <TableCell className="px-1">
+                            <Tooltip>
+                              <TooltipTrigger
+                                render={<Button size="icon-xs" variant="ghost" />}
+                                disabled={locked}
+                                aria-label={
+                                  row.removed
+                                    ? `Undo reset for ${row.model}`
+                                    : row.isNew
+                                      ? "Remove new model"
+                                      : `Reset price for ${row.model} to automatic`
+                                }
+                                onClick={() => {
+                                  if (row.isNew)
+                                    setDrafts((previous) =>
+                                      previous.filter((entry) => entry.id !== row.id),
+                                    );
+                                  else if (row.removed) {
+                                    if (Object.keys(row.values).length === 0)
+                                      setDrafts((previous) =>
+                                        previous.filter((entry) => entry.id !== row.id),
+                                      );
+                                    else updateDraft({ ...row, removed: false });
+                                  } else updateDraft({ ...row, removed: true });
+                                }}
+                              >
+                                {row.isNew ? <XIcon aria-hidden /> : <RotateCcwIcon aria-hidden />}
+                              </TooltipTrigger>
+                              <TooltipPopup>
+                                {row.removed
+                                  ? "Undo reset"
+                                  : row.isNew
+                                    ? "Remove row"
+                                    : "Reset to automatic"}
+                              </TooltipPopup>
+                            </Tooltip>
+                          </TableCell>
+                        </TableRow>
+                      ))
+                    )}
+                  </TableBody>
+                </Table>
               </div>
+              <datalist id="usage-price-models">
+                {models
+                  .filter((model) => !customModels.includes(model))
+                  .map((model) => (
+                    <option key={model} value={model} />
+                  ))}
-              <EnvironmentModelPrices
-                key={environment.environmentId}
-                environment={environment}
-                models={models}
+              </datalist>
-              />
+              <p className="text-xs text-muted-foreground">
+                Blank cache rates use the input price. Enter 0 for free tokens.
+                {selected.length > 1
+                  ? " Mixed cells keep each environment’s rate until you edit them."
+                  : ""}
+              </p>
             </>
-          ) : (
-            <p className="text-sm text-muted-foreground">
-              Connect an environment to set model prices.
-            </p>
           )}
+          {attempt ? (
+            <div role="status" className="grid gap-1 text-xs">
+              {attempt.destinations.map((destination) => {
+                const result = attempt.results.get(destination.environmentId);
+                return (
+                  <div key={destination.environmentId} className="flex justify-between gap-3">
+                    <span className="truncate text-muted-foreground">{destination.label}</span>
+                    <span
+                      className={
+                        result?.status === "failed"
+                          ? "text-right text-destructive"
+                          : "text-muted-foreground"
+                      }
+                    >
+                      {result?.status === "failed"
+                        ? `Not saved · ${result.error}`
+                        : result?.status === "saved"
+                          ? "Saved"
+                          : "Saving…"}
+                    </span>
+                  </div>
+                );
+              })}
+            </div>
+          ) : null}
         </DialogPanel>
-      </DialogPopup>
-    </Dialog>
-  );
-}
-
-function EnvironmentModelPrices({
-  environment,
-  models,
-}: {
-  readonly environment: EnvironmentPresentation;
-  readonly models: readonly string[];
-}) {
-  const settings = useAtomValue(serverEnvironment.settingsValueAtom(environment.environmentId));
-  const session = useEnvironmentSessionState(environment.environmentId);
-  const updateSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
-  const [form, setForm] = useState(usagePriceForm());
-  const [editingModel, setEditingModel] = useState<string | null>(null);
-  const [pending, setPending] = useState(false);
-  const [error, setError] = useState<string | null>(null);
-  const prices = settings?.usagePriceOverrides ?? {};
-  const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
-  const access = isPrimary
+        <DialogFooter variant="bare" className="items-center sm:justify-between">
+          <span className="text-xs text-muted-foreground">
-    ? resolvePrimaryOperateAccess({
+            {hasChanges ? `Changes apply to ${destinationLabel}` : ""}
+          </span>
+          <div className="flex w-full flex-wrap justify-end gap-2 sm:w-auto">
+            {hasChanges ? (
+              <Button variant="ghost" disabled={pending} onClick={discard}>
+                {failedDestinations.length > 0 ? "Discard pending changes" : "Discard changes"}
-        isPrimary,
-        hasDesktopBridge: isElectron,
-        session: session.data,
-        isPending: session.isPending,
-        hasError: session.hasError,
-      })
-    : resolveRemoteOperateAccess({
-        session: session.data,
-        isPending: session.isPending,
-        hasError: session.hasError,
-      });
-  const supportsOverrides =
-    environment.serverConfig?.environment.capabilities.usagePriceOverrides === true;
-  const unavailable =
-    environment.connection.phase !== "connected"
-      ? "Connect this environment to change its model prices."
-      : settings === null
-        ? "Loading model prices..."
-        : !supportsOverrides
-          ? "Update this environment's server to set model prices."
-          : access === "pending"
-            ? "Checking permissions..."
-            : access === "denied"
-              ? "This session can view model prices but cannot change them."
-              : null;
-  const readOnly = unavailable !== null || pending;
-  const parsed = parseUsagePriceForm(form);
-  const duplicate = editingModel === null && parsed !== null && Object.hasOwn(prices, parsed.model);
-
-  const reset = () => {
-    setEditingModel(null);
-    setForm(usagePriceForm());
-    setError(null);
-  };
-  const save = async (model: string, price: UsageModelPriceOverride | null) => {
-    if (readOnly) return;
-    setPending(true);
-    setError(null);
-    try {
-      const result = await updateSettings({
-        environmentId: environment.environmentId,
-        input: { patch: { usagePriceOverrides: { [model]: price } } },
-      });
-      if (result._tag === "Failure") {
-        setError("Could not save model prices. Try again.");
-      } else if (price !== null || editingModel === model) {
-        reset();
-      }
-    } finally {
-      setPending(false);
-    }
-  };
-
-  return (
-    <>
-      {Object.keys(prices).length > 0 ? (
-        <ul className="divide-y divide-border rounded-lg border border-border">
-          {Object.entries(prices)
-            .sort(([left], [right]) => left.localeCompare(right))
-            .map(([model, price]) => (
-              <li key={model} className="flex flex-wrap items-center gap-3 p-3">
-                <div className="min-w-0 flex-1">
-                  <p className="break-all text-sm font-medium">{model}</p>
-                  <p className="text-xs text-muted-foreground">
-                    Input ${price.inputCostPerMillionTokens} · Output $
-                    {price.outputCostPerMillionTokens}
-                    <br />
-                    Cache read $
-                    {price.cacheReadCostPerMillionTokens ?? price.inputCostPerMillionTokens}
-                    {" · "}Cache write $
-                    {price.cacheWriteCostPerMillionTokens ?? price.inputCostPerMillionTokens}
-                  </p>
-                </div>
-                <Button
-                  variant="ghost"
-                  size="xs"
-                  disabled={readOnly}
-                  aria-label={`Edit price for ${model}`}
-                  onClick={() => {
-                    setEditingModel(model);
-                    setForm(usagePriceForm(model, price));
-                    setError(null);
-                  }}
-                >
-                  Edit
-                </Button>
-                <Button
-                  variant="ghost"
-                  size="xs"
-                  disabled={readOnly}
-                  aria-label={`Remove price for ${model}`}
-                  onClick={() => void save(model, null)}
-                >
-                  Remove
-                </Button>
-              </li>
-            ))}
-        </ul>
-      ) : (
-        <p className="text-sm text-muted-foreground">
-          No custom prices. Public model pricing is used when available.
-        </p>
-      )}
-      {unavailable ? <p className="text-sm text-muted-foreground">{unavailable}</p> : null}
-      <form
-        className="grid gap-4"
-        onSubmit={(event) => {
-          event.preventDefault();
-          if (parsed && !duplicate) void save(parsed.model, parsed.price);
-        }}
-      >
-        <fieldset disabled={readOnly} className="grid min-w-0 gap-4">
-          <legend className="mb-3 text-sm font-medium">
-            {editingModel === null ? "Add model price" : "Edit model price"}
-          </legend>
-          <div className="grid gap-1.5">
-            <Label htmlFor="usage-price-model">Model ID</Label>
-            <Input
-              id="usage-price-model"
-              list="usage-price-models"
-              value={form.model}
-              disabled={editingModel !== null}
-              placeholder="Exact model ID from usage"
-              autoComplete="off"
-              spellCheck={false}
-              required
-              onChange={(event) => setForm({ ...form, model: event.target.value })}
-            />
-            <datalist id="usage-price-models">
-              {models.map((model) => (
-                <option key={model} value={model} />
-              ))}
-            </datalist>
-            {duplicate ? (
-              <p role="alert" className="text-xs text-destructive">
-                This model already has a price. Use Edit to change it.
-              </p>
-            ) : null}
-          </div>
-          <div className="grid grid-cols-2 gap-3">
-            {USAGE_PRICE_FIELDS.map((field) => (
-              <div key={field.key} className="grid gap-1.5">
-                <Label htmlFor={`usage-price-${field.key}`}>
-                  {field.label}
-                  {field.optional ? " (optional)" : ""}
-                </Label>
-                <Input
-                  id={`usage-price-${field.key}`}
-                  type="number"
-                  min="0"
-                  step="any"
-                  inputMode="decimal"
-                  required={!field.optional}
-                  value={form[field.key]}
-                  placeholder={field.optional ? "Use input rate" : "0.00"}
-                  onChange={(event) => setForm({ ...form, [field.key]: event.target.value })}
-                />
-              </div>
-            ))}
-          </div>
-          <p className="text-xs text-muted-foreground">
-            Blank cache rates use the input price. Enter 0 for free tokens. Model IDs match exactly.
-            Remove a price to restore automatic pricing.
-          </p>
-          {error ? (
-            <p role="alert" className="text-sm text-destructive">
-              {error}
-            </p>
-          ) : null}
-          <div className="flex justify-end gap-2">
-            {editingModel !== null ? (
-              <Button variant="outline" onClick={reset}>
-                Cancel
               </Button>
             ) : null}
-            <Button type="submit" disabled={parsed === null || duplicate || readOnly}>
+            <Button
-              {pending ? "Saving..." : editingModel === null ? "Add price" : "Save price"}
+              disabled={
+                pending ||
+                (!hasChanges && failedDestinations.length === 0) ||
+                (failedDestinations.length === 0 && (errors.size > 0 || selected.length === 0))
+              }
+              onClick={() => void save(failedDestinations.length > 0)}
+            >
+              {pending
+                ? "Saving…"
+                : failedDestinations.length > 0
+                  ? "Retry failed saves"
+                  : "Save changes"}
             </Button>
           </div>
-        </fieldset>
-      </form>
-    </>
+        </DialogFooter>
+      </DialogPopup>
+    </Dialog>
   );
 }
```

## apps/web/src/components/usage/usagePriceTable.test.ts

Status: added; +170; −0

```diff
@@ -1,0 +1,170 @@
+import { EnvironmentId } from "@t3tools/contracts";
+import { describe, expect, it } from "vite-plus/test";
+import {
+  usagePriceCell,
+  usagePriceTableChanges,
+  usagePriceTableErrors,
+  type UsagePriceDraft,
+} from "./usagePriceTable";
+import type { UsagePriceTarget } from "./usagePriceTargets";
+
+const price = { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 };
+const target = (name: string, prices: UsagePriceTarget["prices"]): UsagePriceTarget => ({
+  environmentId: EnvironmentId.make(name),
+  label: name,
+  prices,
+  unavailable: null,
+});
+const draft = (values: UsagePriceDraft["values"]): UsagePriceDraft => ({
+  id: "model:example",
+  model: "example",
+  isNew: false,
+  values,
+});
+
+describe("price table edits", () => {
+  it("shows shared and mixed cells separately, and never treats missing settings as automatic", () => {
+    const targets = [
+      target("a", { example: price }),
+      target("b", { example: { ...price, outputCostPerMillionTokens: 10 } }),
+    ];
+    expect(usagePriceCell(targets, "example", "inputCostPerMillionTokens").value).toBe("2");
+    expect(usagePriceCell(targets, "example", "outputCostPerMillionTokens")).toEqual({
+      value: "",
+      placeholder: "Mixed",
+    });
+    expect(usagePriceCell(targets, "example", "cacheReadCostPerMillionTokens")).toEqual({
+      value: "",
+      placeholder: "Input rate",
+    });
+    expect(
+      usagePriceCell([target("a", {})], "example", "inputCostPerMillionTokens").placeholder,
+    ).toBe("Automatic");
+    expect(
+      usagePriceCell([targets[0]!, target("b", null)], "example", "inputCostPerMillionTokens")
+        .placeholder,
+    ).toBe("Unavailable");
+  });
+
+  it.each(["constructor", "toString", "__proto__"])(
+    "handles an exact model ID named %s",
+    (model) => {
+      const environment = target("a", {});
+      expect(usagePriceCell([environment], model, "inputCostPerMillionTokens")).toEqual({
+        value: "",
+        placeholder: "Automatic",
+      });
+      const result = usagePriceTableChanges(environment, [
+        {
+          ...draft({ inputCostPerMillionTokens: "2", outputCostPerMillionTokens: "8" }),
+          model,
+          isNew: true,
+        },
+      ]);
+      expect(result.changes).toEqual([{ model, price }]);
+      expect(
+        usagePriceCell([target("a", { [model]: price })], model, "inputCostPerMillionTokens").value,
+      ).toBe("2");
+    },
+  );
+
+  it("changes only edited columns while preserving each environment's other prices and models", () => {
+    const edits = [draft({ inputCostPerMillionTokens: "3" })];
+    const a = usagePriceTableChanges(
+      target("a", { example: { ...price, cacheReadCostPerMillionTokens: 0 }, untouched: price }),
+      edits,
+    );
+    const b = usagePriceTableChanges(
+      target("b", { example: { ...price, outputCostPerMillionTokens: 10 } }),
+      edits,
+    );
+    expect(a.changes).toEqual([
+      {
+        model: "example",
+        price: { ...price, inputCostPerMillionTokens: 3, cacheReadCostPerMillionTokens: 0 },
+      },
+    ]);
+    expect(b.changes).toEqual([
+      { model: "example", price: { inputCostPerMillionTokens: 3, outputCostPerMillionTokens: 10 } },
+    ]);
+  });
+
+  it("batches new rows and resets, and distinguishes blank cache rates from explicit zero", () => {
+    const plan = usagePriceTableChanges(target("a", { example: price }), [
+      { ...draft({}), removed: true },
+      {
+        id: "new:1",
+        model: "  vendor/Model  ",
+        isNew: true,
+        values: {
+          inputCostPerMillionTokens: "2",
+          outputCostPerMillionTokens: "8",
+          cacheReadCostPerMillionTokens: "0",
+          cacheWriteCostPerMillionTokens: "",
+        },
+      },
+    ]);
+    expect(plan.errors.size).toBe(0);
+    expect(plan.changes).toEqual([
+      { model: "example", price: null },
+      { model: "vendor/Model", price: { ...price, cacheReadCostPerMillionTokens: 0 } },
+    ]);
+  });
+
+  it("requires complete prices before turning automatic pricing into an override", () => {
+    const edits = [draft({ inputCostPerMillionTokens: "3" })];
+    expect(usagePriceTableChanges(target("a", { example: price }), edits).errors.size).toBe(0);
+    const missing = usagePriceTableChanges(target("b", {}), edits);
+    expect(missing.changes).toEqual([]);
+    expect(missing.errors.has("model:example")).toBe(true);
+  });
+
+  it("ignores empty new rows without blocking other edits", () => {
+    const environment = target("a", { example: price });
+    const empty: UsagePriceDraft = {
+      id: "new:1",
+      model: " ",
+      isNew: true,
+      values: { inputCostPerMillionTokens: " " },
+    };
+    const plan = usagePriceTableChanges(environment, [
+      draft({ inputCostPerMillionTokens: "3" }),
+      empty,
+    ]);
+    expect(plan.errors.size).toBe(0);
+    expect(plan.changes).toEqual([
+      { model: "example", price: { ...price, inputCostPerMillionTokens: 3 } },
+    ]);
+    expect(usagePriceTableChanges(environment, [empty]).changes).toEqual([]);
+    expect(
+      usagePriceTableChanges(environment, [
+        { ...empty, values: { inputCostPerMillionTokens: "0" } },
+      ]).errors.get(empty.id),
+    ).toBe("Enter a model ID.");
+  });
+
+  it.each(["Offline", "Read-only access", "Update server to edit prices"])(
+    "does not let %s destinations block a valid edit elsewhere",
+    (unavailable) => {
+      const edits = [draft({ inputCostPerMillionTokens: "3" })];
+      const writable = target("writable", { example: price });
+      const other = target("other", {});
+      expect(usagePriceTableErrors([writable, { ...other, unavailable }], edits).size).toBe(0);
+      expect(usagePriceTableErrors([writable, other], edits).has("model:example")).toBe(true);
+    },
+  );
+
+  it("does not write unchanged rates and rejects invalid edited cells", () => {
+    const environment = target("a", { example: price });
+    expect(
+      usagePriceTableChanges(environment, [draft({ inputCostPerMillionTokens: "2.00" })]).changes,
+    ).toEqual([]);
+    expect(
+      usagePriceTableChanges(environment, [draft({ cacheReadCostPerMillionTokens: "-1" })]).errors
+        .size,
+    ).toBe(1);
+    expect(
+      usagePriceTableChanges(environment, [draft({ outputCostPerMillionTokens: "" })]).errors.size,
+    ).toBe(1);
+  });
+});
```

## apps/web/src/components/usage/usagePriceTable.ts

Status: added; +97; −0

```diff
@@ -1,0 +1,97 @@
+import {
+  parseUsagePriceForm,
+  usagePriceForm,
+  USAGE_PRICE_FIELDS,
+  type UsagePriceForm,
+} from "./usagePriceForm";
+import type { UsagePriceChange, UsagePriceTarget } from "./usagePriceTargets";
+
+export type UsagePriceField = (typeof USAGE_PRICE_FIELDS)[number]["key"];
+
+export interface UsagePriceDraft {
+  readonly id: string;
+  readonly model: string;
+  readonly isNew: boolean;
+  readonly values: Partial<Pick<UsagePriceForm, UsagePriceField>>;
+  readonly removed?: boolean;
+}
+
+export function isEmptyUsagePriceDraft(draft: UsagePriceDraft) {
+  return (
+    draft.isNew &&
+    draft.model.trim() === "" &&
+    Object.values(draft.values).every((value) => value.trim() === "")
+  );
+}
+
+function modelPrice(target: UsagePriceTarget, model: string) {
+  return target.prices && Object.hasOwn(target.prices, model) ? target.prices[model] : undefined;
+}
+
+export function usagePriceCell(
+  targets: readonly UsagePriceTarget[],
+  model: string,
+  field: UsagePriceField,
+) {
+  const optional = USAGE_PRICE_FIELDS.find((entry) => entry.key === field)!.optional;
+  const values = targets.map((target) =>
+    modelPrice(target, model) ? usagePriceForm(model, modelPrice(target, model))[field] : null,
+  );
+  if (targets.some((target) => target.prices === null))
+    return { value: "", placeholder: "Unavailable" };
+  if (values.some((value) => value !== values[0])) return { value: "", placeholder: "Mixed" };
+  return {
+    value: values[0] ?? "",
+    placeholder: values[0] === null ? "Automatic" : optional ? "Input rate" : "0.00",
+  };
+}
+
+/** Only edited cells replace rates; untouched cells retain each environment's own values. */
+export function usagePriceTableChanges(
+  target: UsagePriceTarget,
+  drafts: readonly UsagePriceDraft[],
+) {
+  const changes: UsagePriceChange[] = [];
+  const errors = new Map<string, string>();
+  for (const draft of drafts) {
+    if (isEmptyUsagePriceDraft(draft)) continue;
+    const model = draft.model.trim();
+    if (draft.removed) {
+      if (modelPrice(target, model)) changes.push({ model, price: null });
+      continue;
+    }
+    const original = usagePriceForm(model, modelPrice(target, model));
+    const form = { ...original, ...draft.values };
+    const parsed = parseUsagePriceForm(form);
+    if (parsed === null) {
+      const missing = USAGE_PRICE_FIELDS.find(
+        (field) => !field.optional && form[field.key].trim() === "",
+      );
+      errors.set(
+        draft.id,
+        model === ""
+          ? "Enter a model ID."
+          : missing
+            ? `${missing.label} is required on ${target.label}.`
+            : "Use non-negative numbers for prices.",
+      );
+      continue;
+    }
+    const next = usagePriceForm(model, parsed.price);
+    if (USAGE_PRICE_FIELDS.some((field) => original[field.key] !== next[field.key]))
+      changes.push(parsed);
+  }
+  return { changes, errors };
+}
+
+/** Unavailable destinations report a save failure without blocking writable environments. */
+export function usagePriceTableErrors(
+  targets: readonly UsagePriceTarget[],
+  drafts: readonly UsagePriceDraft[],
+) {
+  return new Map(
+    targets
+      .filter((target) => target.unavailable === null && target.prices !== null)
+      .flatMap((target) => [...usagePriceTableChanges(target, drafts).errors]),
+  );
+}
```

## apps/web/src/components/usage/usagePriceTargets.test.ts

Status: added; +143; −0

```diff
@@ -1,0 +1,143 @@
+import { EnvironmentId } from "@t3tools/contracts";
+import { describe, expect, it, vi } from "vite-plus/test";
+
+import {
+  writeUsagePrices,
+  type UsagePriceTarget,
+  type UsagePriceWriteResult,
+} from "./usagePriceTargets";
+
+const price = { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 };
+const target = (id: string, overrides: Partial<UsagePriceTarget> = {}): UsagePriceTarget => ({
+  environmentId: EnvironmentId.make(id),
+  label: id,
+  prices: { example: price },
+  unavailable: null,
+  ...overrides,
+});
+
+describe("model price writes", () => {
+  it("sends all table changes in one patch per environment and skips unchanged destinations", async () => {
+    const write = vi.fn(async () => ({ _tag: "Success" as const }));
+    const onResult = vi.fn();
+    await writeUsagePrices({
+      targets: [target("edited"), target("unchanged")],
+      changes: new Map([
+        [
+          EnvironmentId.make("edited"),
+          [
+            { model: "new-model", price },
+            { model: "example", price: null },
+          ],
+        ],
+      ]),
+      write,
+      onResult,
+    });
+    expect(write).toHaveBeenCalledExactlyOnceWith({
+      environmentId: "edited",
+      input: { patch: { usagePriceOverrides: { "new-model": price, example: null } } },
+    });
+    expect(onResult).toHaveBeenCalledWith("unchanged", { status: "saved" });
+  });
+
+  it("saves independently, skips unavailable targets, and retries failures without rewriting successes", async () => {
+    let resolveSlow!: (result: { _tag: "Success" | "Failure" }) => void;
+    const slow = new Promise<{ _tag: "Success" | "Failure" }>((resolve) => {
+      resolveSlow = resolve;
+    });
+    let resolveFastSaved!: () => void;
+    const fastSaved = new Promise<void>((resolve) => {
+      resolveFastSaved = resolve;
+    });
+    const targets = [target("fast"), target("slow"), target("offline", { unavailable: "Offline" })];
+    const results = new Map<EnvironmentId, UsagePriceWriteResult>();
+    const write = vi.fn(async ({ environmentId }: { environmentId: EnvironmentId }) =>
+      environmentId === "slow" ? slow : { _tag: "Success" as const },
+    );
+    const change = { model: "example", price };
+    const running = writeUsagePrices({
+      targets,
+      changes: new Map(targets.map((target) => [target.environmentId, [change]])),
+      write,
+      onResult: (id, result) => {
+        results.set(id, result);
+        if (id === "fast") resolveFastSaved();
+      },
+    });
+    await fastSaved;
+    expect(results.get(EnvironmentId.make("fast"))).toEqual({ status: "saved" });
+    expect(results.get(EnvironmentId.make("offline"))).toEqual({
+      status: "failed",
+      error: "Offline",
+    });
+    expect(results.has(EnvironmentId.make("slow"))).toBe(false);
+    resolveSlow({ _tag: "Failure" });
+    await running;
+    expect(write).toHaveBeenCalledTimes(2);
+    expect(write).toHaveBeenCalledWith({
+      environmentId: "fast",
+      input: { patch: { usagePriceOverrides: { example: price } } },
+    });
+    const retry = vi.fn(async () => ({ _tag: "Success" as const }));
+    await writeUsagePrices({
+      targets: targets
+        .filter((entry) => results.get(entry.environmentId)?.status === "failed")
+        .map((entry) => ({ ...entry, unavailable: null })),
+      changes: new Map(targets.map((target) => [target.environmentId, [change]])),
+      write: retry,
+      onResult: (id, result) => {
+        results.set(id, result);
+      },
+    });
+    expect(retry.mock.calls).toHaveLength(2);
+    expect(retry).not.toHaveBeenCalledWith(expect.objectContaining({ environmentId: "fast" }));
+    expect([...results.values()].every((result) => result.status === "saved")).toBe(true);
+  });
+
+  it("resets only the chosen model on the selected destination", async () => {
+    const write = vi.fn(async () => ({ _tag: "Success" as const }));
+    const onResult = vi.fn();
+    await writeUsagePrices({
+      targets: [target("selected")],
+      changes: new Map([[EnvironmentId.make("selected"), [{ model: "example", price: null }]]]),
+      write,
+      onResult,
+    });
+    expect(write).toHaveBeenCalledExactlyOnceWith({
+      environmentId: "selected",
+      input: { patch: { usagePriceOverrides: { example: null } } },
+    });
+    expect(onResult).toHaveBeenCalledWith("selected", { status: "saved" });
+  });
+
+  it("reports thrown writes and permission/version restrictions without aborting other saves", async () => {
+    const onResult = vi.fn();
+    const write = vi.fn(async () => {
+      throw new Error("connection lost");
+    });
+    await writeUsagePrices({
+      targets: [
+        target("lost"),
+        target("denied", { unavailable: "Read-only access" }),
+        target("old", { unavailable: "Update server to edit prices" }),
+      ],
+      changes: new Map([[EnvironmentId.make("lost"), [{ model: "example", price }]]]),
+      write,
+      onResult,
+    });
+    expect(write).toHaveBeenCalledTimes(1);
+    expect(onResult).toHaveBeenCalledWith("lost", {
+      status: "failed",
+      error: "Could not save. Try again.",
+    });
+    expect(onResult).toHaveBeenCalledWith("denied", {
+      status: "failed",
+      error: "Read-only access",
+    });
+    expect(onResult).toHaveBeenCalledWith("old", {
+      status: "failed",
+      error: "Update server to edit prices",
+    });
+  });
+});
```

## apps/web/src/components/usage/usagePriceTargets.ts

Status: added; +65; −0

```diff
@@ -1,0 +1,65 @@
+import type {
+  EnvironmentId,
+  ServerSettingsPatch,
+  UsageModelPriceOverride,
+} from "@t3tools/contracts";
+
+export interface UsagePriceTarget {
+  readonly environmentId: EnvironmentId;
+  readonly label: string;
+  readonly prices: Readonly<Record<string, UsageModelPriceOverride>> | null;
+  readonly unavailable: string | null;
+}
+
+export interface UsagePriceChange {
+  readonly model: string;
+  readonly price: UsageModelPriceOverride | null;
+}
+
+export type UsagePriceWriteResult =
+  | { readonly status: "saved" }
+  | { readonly status: "failed"; readonly error: string };
+
+/** Each destination settles independently; retry callers pass only the failed destinations. */
+export async function writeUsagePrices(input: {
+  readonly targets: readonly UsagePriceTarget[];
+  readonly changes: ReadonlyMap<EnvironmentId, readonly UsagePriceChange[]>;
+  readonly write: (input: {
+    environmentId: EnvironmentId;
+    input: { patch: ServerSettingsPatch };
+  }) => Promise<{ readonly _tag: "Success" | "Failure" }>;
+  readonly onResult: (environmentId: EnvironmentId, result: UsagePriceWriteResult) => void;
+}) {
+  await Promise.all(
+    input.targets.map(async (target) => {
+      let result: UsagePriceWriteResult;
+      if (target.unavailable !== null) {
+        result = { status: "failed", error: target.unavailable };
+      } else {
+        try {
+          const changes = input.changes.get(target.environmentId) ?? [];
+          const saved =
+            changes.length === 0
+              ? { _tag: "Success" as const }
+              : await input.write({
+                  environmentId: target.environmentId,
+                  input: {
+                    patch: {
+                      usagePriceOverrides: Object.fromEntries(
+                        changes.map((change) => [change.model, change.price]),
+                      ),
+                    },
+                  },
+                });
+          result =
+            saved._tag === "Success"
+              ? { status: "saved" }
+              : { status: "failed", error: "Could not save. Try again." };
+        } catch {
+          result = { status: "failed", error: "Could not save. Try again." };
+        }
+      }
+      input.onResult(target.environmentId, result);
+    }),
+  );
+}
```

## docs/user/usage.md

Status: modified; +14; −5

```diff
@@ -18,15 +18,24 @@
 
 ## Set custom model prices
 
+On web or desktop, open the environment dropdown on **Usage**, then choose **Model prices** to add,
+edit, or reset a model's estimated price. **Apply to** starts with your current Usage filter;
-On web or desktop, open **Usage → Model prices** to add, edit, or remove a model's estimated
-price. Choose the environment whose history you want to price, then enter the exact model ID and
-USD rates per million input and output tokens. You can enter any model ID, including models
+choose all environments or select individual destinations. Enter the exact model ID and USD
+rates per million input and output tokens. You can enter any model ID, including models
 without public pricing.
 
 Cache read and cache write rates are optional and use the input rate when blank. Enter `0` for
 tokens that are free. Saved prices replace automatic pricing for all of that environment's
+history and are shared with clients connected to it. When environments have different prices,
+cells show **Mixed**. Edit rates directly in the table, then choose **Save changes** to apply all
+edited rows. Untouched cells keep each environment's rate. Select one environment to inspect its
+prices. **Reset to automatic** marks a model's override for removal when you save; you can undo
+it before saving.
+
+Each destination reports whether the change saved. Offline or unavailable environments are
+marked **Not saved**. Reconnect them and choose **Retry failed saves** to finish the same change
-history and are shared with clients connected to it. Set prices on each environment that needs
-them. Removing a price restores automatic pricing.
+without writing again to environments that already saved. Changes are not queued after you close
+the dialog.
 
 ## Track subscription limits
 
```
