@stigmer/react 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/billing/CreditLedgerTable.d.ts.map +1 -1
- package/billing/CreditLedgerTable.js +2 -7
- package/billing/CreditLedgerTable.js.map +1 -1
- package/composer/ComposerToolbar.d.ts +6 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +9 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +13 -2
- package/composer/SessionComposer.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +14 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +5 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +6 -5
- package/index.js.map +1 -1
- package/internal/Pagination.d.ts +15 -0
- package/internal/Pagination.d.ts.map +1 -0
- package/internal/Pagination.js +20 -0
- package/internal/Pagination.js.map +1 -0
- package/models/ModelSelector.d.ts +26 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +27 -6
- package/models/ModelSelector.js.map +1 -1
- package/models/index.d.ts +2 -0
- package/models/index.d.ts.map +1 -1
- package/models/index.js +1 -0
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +10 -0
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +3 -0
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +27 -0
- package/models/service-tier.d.ts.map +1 -0
- package/models/service-tier.js +34 -0
- package/models/service-tier.js.map +1 -0
- package/package.json +4 -4
- package/schedule/ScheduleDetailView.d.ts +31 -8
- package/schedule/ScheduleDetailView.d.ts.map +1 -1
- package/schedule/ScheduleDetailView.js +441 -29
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts +7 -4
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +93 -8
- package/schedule/ScheduleForm.js.map +1 -1
- package/schedule/ScheduleRunsTable.d.ts +72 -0
- package/schedule/ScheduleRunsTable.d.ts.map +1 -0
- package/schedule/ScheduleRunsTable.js +161 -0
- package/schedule/ScheduleRunsTable.js.map +1 -0
- package/schedule/index.d.ts +6 -0
- package/schedule/index.d.ts.map +1 -1
- package/schedule/index.js +3 -0
- package/schedule/index.js.map +1 -1
- package/schedule/useScheduleRuns.d.ts +44 -0
- package/schedule/useScheduleRuns.d.ts.map +1 -0
- package/schedule/useScheduleRuns.js +48 -0
- package/schedule/useScheduleRuns.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +23 -10
- package/schedule/useTriggerSchedule.d.ts.map +1 -1
- package/schedule/useTriggerSchedule.js +28 -9
- package/schedule/useTriggerSchedule.js.map +1 -1
- package/schedule/useUpdateScheduleSpec.d.ts +49 -0
- package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
- package/schedule/useUpdateScheduleSpec.js +70 -0
- package/schedule/useUpdateScheduleSpec.js.map +1 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +1 -0
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts +11 -0
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +1 -0
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/src/billing/CreditLedgerTable.tsx +3 -42
- package/src/composer/ComposerToolbar.tsx +9 -0
- package/src/composer/SessionComposer.tsx +24 -1
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
- package/src/execution/useCreateAgentExecution.ts +18 -0
- package/src/index.ts +9 -4
- package/src/internal/Pagination.tsx +72 -0
- package/src/models/ModelSelector.tsx +100 -3
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +16 -0
- package/src/models/service-tier.ts +49 -0
- package/src/schedule/ScheduleDetailView.tsx +1179 -111
- package/src/schedule/ScheduleForm.tsx +217 -7
- package/src/schedule/ScheduleRunsTable.tsx +424 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +431 -8
- package/src/schedule/__tests__/scheduleCreation.test.tsx +197 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +94 -7
- package/src/schedule/index.ts +12 -0
- package/src/schedule/useScheduleRuns.ts +93 -0
- package/src/schedule/useTriggerSchedule.ts +39 -14
- package/src/schedule/useUpdateScheduleSpec.ts +108 -0
- package/src/session/useNewSessionFlow.ts +1 -0
- package/src/session/useSessionConversation.ts +12 -0
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
- package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
- package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
- package/src/workflow/starter-workflow-yaml.ts +2 -1
- package/styles.css +1 -1
- package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
- package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
- package/workflow/inspector/forms/AgentCallForm.js +87 -9
- package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.js +31 -5
- package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
- package/workflow/starter-workflow-yaml.d.ts +1 -1
- package/workflow/starter-workflow-yaml.d.ts.map +1 -1
- package/workflow/starter-workflow-yaml.js +2 -1
- package/workflow/starter-workflow-yaml.js.map +1 -1
|
@@ -7,6 +7,7 @@ import { useStigmerPortalContainer } from "../portal-container.js";
|
|
|
7
7
|
import { useModelRegistry } from "./useModelRegistry.js";
|
|
8
8
|
import type { ModelInfo, CostTier, SpeedTier } from "./registry.js";
|
|
9
9
|
import { HARNESS_META, HARNESS_OPTIONS, type HarnessOption } from "./harness.js";
|
|
10
|
+
import { FAST_SERVICE_TIER, type ServiceTierOption } from "./service-tier.js";
|
|
10
11
|
|
|
11
12
|
const COST_TIER_LABEL: Record<CostTier, string> = {
|
|
12
13
|
economy: "$",
|
|
@@ -61,6 +62,31 @@ export interface ModelSelectorProps {
|
|
|
61
62
|
readonly className?: string;
|
|
62
63
|
/** When true, disables the selector. */
|
|
63
64
|
readonly disabled?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Trigger label shown while `value` is empty, instead of falling back
|
|
67
|
+
* to the registry's default model. Lets a form distinguish "nothing
|
|
68
|
+
* pinned — the platform default applies" from an actual selection
|
|
69
|
+
* (the schedule form's contract: an unset model inherits the
|
|
70
|
+
* surface's platform default). The harness prefix is hidden while
|
|
71
|
+
* the placeholder shows, since no engine is pinned either.
|
|
72
|
+
*/
|
|
73
|
+
readonly placeholderLabel?: string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Current service tier for the selected model (stigmer/stigmer#357).
|
|
77
|
+
* Defaults to "standard". Only meaningful together with
|
|
78
|
+
* {@link onServiceTierChange}.
|
|
79
|
+
*/
|
|
80
|
+
readonly serviceTier?: ServiceTierOption;
|
|
81
|
+
/**
|
|
82
|
+
* Called when the user toggles the fast tier. Providing this enables the
|
|
83
|
+
* tier toggle, which renders ONLY when the selected model prices a fast
|
|
84
|
+
* variant ({@link ModelInfo.serviceTiers}) — no dead controls. Selecting
|
|
85
|
+
* a model without a fast tier while "fast" is active resets the tier to
|
|
86
|
+
* "standard" through this callback, so an unsupported combination can
|
|
87
|
+
* never be submitted.
|
|
88
|
+
*/
|
|
89
|
+
readonly onServiceTierChange?: (tier: ServiceTierOption) => void;
|
|
64
90
|
|
|
65
91
|
/**
|
|
66
92
|
* @deprecated Use {@link onHarnessChange} instead.
|
|
@@ -104,6 +130,9 @@ export function ModelSelector({
|
|
|
104
130
|
compact = false,
|
|
105
131
|
className,
|
|
106
132
|
disabled,
|
|
133
|
+
placeholderLabel,
|
|
134
|
+
serviceTier = "standard",
|
|
135
|
+
onServiceTierChange,
|
|
107
136
|
}: ModelSelectorProps) {
|
|
108
137
|
const portalContainer = useStigmerPortalContainer();
|
|
109
138
|
|
|
@@ -216,9 +245,19 @@ export function ModelSelector({
|
|
|
216
245
|
const selectModel = useCallback(
|
|
217
246
|
(model: ModelInfo) => {
|
|
218
247
|
onValueChange(model.modelId);
|
|
248
|
+
// Fail-safe: the fast tier is per-model; carrying it onto a model
|
|
249
|
+
// that does not price it would be refused at create time (#357), so
|
|
250
|
+
// reset here instead of surfacing a dead selection.
|
|
251
|
+
if (
|
|
252
|
+
serviceTier === "fast"
|
|
253
|
+
&& onServiceTierChange
|
|
254
|
+
&& !model.serviceTiers.includes(FAST_SERVICE_TIER)
|
|
255
|
+
) {
|
|
256
|
+
onServiceTierChange("standard");
|
|
257
|
+
}
|
|
219
258
|
setOpen(false);
|
|
220
259
|
},
|
|
221
|
-
[onValueChange],
|
|
260
|
+
[onValueChange, serviceTier, onServiceTierChange],
|
|
222
261
|
);
|
|
223
262
|
|
|
224
263
|
const scrollHighlightIntoView = useCallback((idx: number) => {
|
|
@@ -265,8 +304,23 @@ export function ModelSelector({
|
|
|
265
304
|
|
|
266
305
|
const showShowAllButton = !isSearching && !showAll && featuredModels.length > 0 && featuredModels.length < models.length;
|
|
267
306
|
|
|
268
|
-
|
|
269
|
-
|
|
307
|
+
// The tier toggle exists only where it applies: a consumer opted in AND
|
|
308
|
+
// the selected model prices a fast variant (recognition over recall —
|
|
309
|
+
// never a dead control on models with no tier choice).
|
|
310
|
+
const showServiceTierToggle =
|
|
311
|
+
onServiceTierChange !== undefined
|
|
312
|
+
&& selectedModel !== undefined
|
|
313
|
+
&& selectedModel.serviceTiers.includes(FAST_SERVICE_TIER);
|
|
314
|
+
const fastActive = serviceTier === "fast";
|
|
315
|
+
|
|
316
|
+
const usingPlaceholder = !value && placeholderLabel !== undefined;
|
|
317
|
+
const triggerLabel = usingPlaceholder
|
|
318
|
+
? placeholderLabel
|
|
319
|
+
: (selectedModel?.displayName ?? "Select model");
|
|
320
|
+
const triggerHarness =
|
|
321
|
+
!isHarnessLocked && !usingPlaceholder
|
|
322
|
+
? HARNESS_META[activeHarness].label
|
|
323
|
+
: undefined;
|
|
270
324
|
|
|
271
325
|
return (
|
|
272
326
|
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
@@ -288,6 +342,16 @@ export function ModelSelector({
|
|
|
288
342
|
<span className="shrink-0 text-border" aria-hidden>·</span>
|
|
289
343
|
)}
|
|
290
344
|
<span className="truncate">{triggerLabel}</span>
|
|
345
|
+
{showServiceTierToggle && fastActive && (
|
|
346
|
+
<span
|
|
347
|
+
className={cn(
|
|
348
|
+
"shrink-0 rounded-sm border border-border px-1 py-px",
|
|
349
|
+
"text-[0.6rem] font-medium uppercase tracking-wider text-muted-foreground",
|
|
350
|
+
)}
|
|
351
|
+
>
|
|
352
|
+
Fast
|
|
353
|
+
</span>
|
|
354
|
+
)}
|
|
291
355
|
<ChevronIcon />
|
|
292
356
|
</Popover.Trigger>
|
|
293
357
|
|
|
@@ -471,6 +535,39 @@ export function ModelSelector({
|
|
|
471
535
|
</button>
|
|
472
536
|
)}
|
|
473
537
|
</div>
|
|
538
|
+
|
|
539
|
+
{/* Service tier toggle — only for models that price a fast variant (#357) */}
|
|
540
|
+
{showServiceTierToggle && (
|
|
541
|
+
<div className="flex items-center justify-between border-t border-border px-3 py-2">
|
|
542
|
+
<div className="flex flex-col">
|
|
543
|
+
<span className="text-xs text-foreground">Fast tier</span>
|
|
544
|
+
<span className="text-[0.65rem] text-muted-foreground">
|
|
545
|
+
Faster responses at higher per-token rates
|
|
546
|
+
</span>
|
|
547
|
+
</div>
|
|
548
|
+
<button
|
|
549
|
+
type="button"
|
|
550
|
+
role="switch"
|
|
551
|
+
aria-checked={fastActive}
|
|
552
|
+
aria-label="Fast tier"
|
|
553
|
+
className={cn(
|
|
554
|
+
"relative h-4.5 w-8 shrink-0 rounded-full transition-colors cursor-pointer",
|
|
555
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
556
|
+
fastActive ? "bg-primary" : "bg-muted",
|
|
557
|
+
)}
|
|
558
|
+
onClick={() =>
|
|
559
|
+
onServiceTierChange?.(fastActive ? "standard" : "fast")
|
|
560
|
+
}
|
|
561
|
+
>
|
|
562
|
+
<span
|
|
563
|
+
className={cn(
|
|
564
|
+
"absolute top-0.5 size-3.5 rounded-full bg-background shadow-sm transition-transform",
|
|
565
|
+
fastActive ? "translate-x-4" : "translate-x-0.5",
|
|
566
|
+
)}
|
|
567
|
+
/>
|
|
568
|
+
</button>
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
474
571
|
</Popover.Popup>
|
|
475
572
|
</Popover.Positioner>
|
|
476
573
|
</Popover.Portal>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeAll } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { ModelSelector } from "../ModelSelector";
|
|
4
|
+
import { ModelRegistryContext } from "../ModelRegistryContext";
|
|
5
|
+
import type { ModelInfo } from "../registry";
|
|
6
|
+
|
|
7
|
+
// Portal into document.body so the popover mounts under happy-dom, and shim
|
|
8
|
+
// ResizeObserver for Base UI's positioner (the scheduleCreation.test.tsx
|
|
9
|
+
// pattern).
|
|
10
|
+
vi.mock("../../portal-container", () => ({
|
|
11
|
+
useStigmerPortalContainer: () => document.body,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
if (!("ResizeObserver" in globalThis)) {
|
|
16
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
|
|
17
|
+
class {
|
|
18
|
+
observe() {}
|
|
19
|
+
unobserve() {}
|
|
20
|
+
disconnect() {}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Service-tier toggle gating (stigmer/stigmer#357).
|
|
27
|
+
*
|
|
28
|
+
* The toggle is capability-gated: it renders only when the consumer opted in
|
|
29
|
+
* (onServiceTierChange) AND the selected model prices a fast variant
|
|
30
|
+
* (ModelInfo.serviceTiers). Switching to a model without a fast tier resets
|
|
31
|
+
* an active fast selection — a stale tier would be refused at create time.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const FAST_CAPABLE: ModelInfo = {
|
|
35
|
+
modelId: "composer-2.5",
|
|
36
|
+
provider: "cursor",
|
|
37
|
+
displayName: "Composer 2.5",
|
|
38
|
+
shortDescription: "Fast agentic model",
|
|
39
|
+
speedTier: "fastest",
|
|
40
|
+
costTier: "economy",
|
|
41
|
+
harness: "cursor",
|
|
42
|
+
featured: true,
|
|
43
|
+
serviceTiers: ["fast"],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const NO_FAST_TIER: ModelInfo = {
|
|
47
|
+
modelId: "claude-haiku-4-5",
|
|
48
|
+
provider: "anthropic",
|
|
49
|
+
displayName: "Haiku 4.5",
|
|
50
|
+
shortDescription: "Small fast model",
|
|
51
|
+
speedTier: "fastest",
|
|
52
|
+
costTier: "economy",
|
|
53
|
+
harness: "cursor",
|
|
54
|
+
featured: true,
|
|
55
|
+
serviceTiers: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function renderSelector(
|
|
59
|
+
props?: Partial<React.ComponentProps<typeof ModelSelector>>,
|
|
60
|
+
) {
|
|
61
|
+
const onValueChange = vi.fn();
|
|
62
|
+
const onServiceTierChange = vi.fn();
|
|
63
|
+
|
|
64
|
+
const result = render(
|
|
65
|
+
<ModelRegistryContext.Provider
|
|
66
|
+
value={{
|
|
67
|
+
models: [FAST_CAPABLE, NO_FAST_TIER],
|
|
68
|
+
isLoading: false,
|
|
69
|
+
error: null,
|
|
70
|
+
refetch: vi.fn(),
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<ModelSelector
|
|
74
|
+
value="composer-2.5"
|
|
75
|
+
onValueChange={onValueChange}
|
|
76
|
+
harness="cursor"
|
|
77
|
+
serviceTier="standard"
|
|
78
|
+
onServiceTierChange={onServiceTierChange}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
</ModelRegistryContext.Provider>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return { ...result, onValueChange, onServiceTierChange };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Open the popover via its trigger and wait for the portaled popup to
|
|
89
|
+
* mount (Base UI portals render asynchronously in jsdom). The search
|
|
90
|
+
* input is the popup's always-present landmark.
|
|
91
|
+
*/
|
|
92
|
+
async function openPopover(triggerLabel: string) {
|
|
93
|
+
fireEvent.click(screen.getByRole("button", { name: new RegExp(triggerLabel) }));
|
|
94
|
+
await screen.findByPlaceholderText("Search models…");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A model row inside the popup list (never matches the trigger). */
|
|
98
|
+
function modelOption(displayName: string): HTMLElement {
|
|
99
|
+
const option = screen
|
|
100
|
+
.getAllByText(displayName)
|
|
101
|
+
.map((el) => el.closest<HTMLElement>("[data-model-option]"))
|
|
102
|
+
.find((el) => el != null);
|
|
103
|
+
if (!option) throw new Error(`No popup option for "${displayName}"`);
|
|
104
|
+
return option;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
afterEach(cleanup);
|
|
108
|
+
|
|
109
|
+
describe("ModelSelector — service tier toggle", () => {
|
|
110
|
+
it("renders the toggle for a fast-capable model", async () => {
|
|
111
|
+
renderSelector();
|
|
112
|
+
await openPopover("Composer 2.5");
|
|
113
|
+
|
|
114
|
+
const toggle = screen.getByRole("switch", { name: "Fast tier" });
|
|
115
|
+
expect(toggle.getAttribute("aria-checked")).toBe("false");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not render the toggle for a model without a fast tier", async () => {
|
|
119
|
+
renderSelector({ value: "claude-haiku-4-5" });
|
|
120
|
+
await openPopover("Haiku 4.5");
|
|
121
|
+
|
|
122
|
+
expect(screen.queryByRole("switch", { name: "Fast tier" })).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("does not render the toggle when the consumer did not opt in", async () => {
|
|
126
|
+
renderSelector({ onServiceTierChange: undefined });
|
|
127
|
+
await openPopover("Composer 2.5");
|
|
128
|
+
|
|
129
|
+
expect(screen.queryByRole("switch", { name: "Fast tier" })).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("toggling fires onServiceTierChange with the flipped tier", async () => {
|
|
133
|
+
const { onServiceTierChange } = renderSelector();
|
|
134
|
+
await openPopover("Composer 2.5");
|
|
135
|
+
|
|
136
|
+
fireEvent.click(screen.getByRole("switch", { name: "Fast tier" }));
|
|
137
|
+
expect(onServiceTierChange).toHaveBeenCalledOnce();
|
|
138
|
+
expect(onServiceTierChange).toHaveBeenCalledWith("fast");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("shows the Fast badge on the trigger when the fast tier is active", () => {
|
|
142
|
+
renderSelector({ serviceTier: "fast" });
|
|
143
|
+
|
|
144
|
+
expect(screen.getByText("Fast")).toBeTruthy();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("resets an active fast tier when selecting a model without one", async () => {
|
|
148
|
+
const { onValueChange, onServiceTierChange } = renderSelector({
|
|
149
|
+
serviceTier: "fast",
|
|
150
|
+
});
|
|
151
|
+
await openPopover("Composer 2.5");
|
|
152
|
+
|
|
153
|
+
fireEvent.click(modelOption("Haiku 4.5"));
|
|
154
|
+
|
|
155
|
+
expect(onValueChange).toHaveBeenCalledOnce();
|
|
156
|
+
expect(onValueChange).toHaveBeenCalledWith("claude-haiku-4-5");
|
|
157
|
+
expect(onServiceTierChange).toHaveBeenCalledOnce();
|
|
158
|
+
expect(onServiceTierChange).toHaveBeenCalledWith("standard");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("keeps an active fast tier when selecting another fast-capable model", async () => {
|
|
162
|
+
const { onServiceTierChange } = renderSelector({
|
|
163
|
+
serviceTier: "fast",
|
|
164
|
+
value: "claude-haiku-4-5",
|
|
165
|
+
});
|
|
166
|
+
await openPopover("Haiku 4.5");
|
|
167
|
+
|
|
168
|
+
fireEvent.click(modelOption("Composer 2.5"));
|
|
169
|
+
|
|
170
|
+
expect(onServiceTierChange).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -73,6 +73,9 @@ const TEST_REGISTRY_JSON = {
|
|
|
73
73
|
costTier: "standard",
|
|
74
74
|
featured: false,
|
|
75
75
|
pricing: { inputPricePerMillion: 3, outputPricePerMillion: 15, cacheWritePricePerMillion: 3.75, cacheReadPricePerMillion: 0.3 },
|
|
76
|
+
pricingVariants: {
|
|
77
|
+
fast: { inputPricePerMillion: 18, outputPricePerMillion: 90 },
|
|
78
|
+
},
|
|
76
79
|
},
|
|
77
80
|
{
|
|
78
81
|
id: "ollama-local",
|
|
@@ -103,6 +106,18 @@ function createWrapper(models: readonly ModelInfo[] = TEST_MODELS) {
|
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
describe("parseRegistryJson service tiers (#357)", () => {
|
|
110
|
+
it("surfaces pricingVariants keys as serviceTiers", () => {
|
|
111
|
+
const withVariants = TEST_MODELS.find((m) => m.modelId === "claude-4.6-sonnet");
|
|
112
|
+
expect(withVariants?.serviceTiers).toEqual(["fast"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("yields an empty serviceTiers list for models without variants", () => {
|
|
116
|
+
const withoutVariants = TEST_MODELS.find((m) => m.modelId === "claude-sonnet-4.6");
|
|
117
|
+
expect(withoutVariants?.serviceTiers).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
106
121
|
describe("useModelRegistry", () => {
|
|
107
122
|
describe("unified mode (no harness)", () => {
|
|
108
123
|
it("excludes DISABLED_PROVIDERS from models", () => {
|
package/src/models/index.ts
CHANGED
|
@@ -11,3 +11,5 @@ export { HarnessSelector } from "./HarnessSelector.js";
|
|
|
11
11
|
export type { HarnessSelectorProps } from "./HarnessSelector.js";
|
|
12
12
|
export { DEFAULT_HARNESS, HARNESS_LABELS, HARNESS_META, HARNESS_OPTIONS, toProtoHarness, fromProtoHarness } from "./harness.js";
|
|
13
13
|
export type { HarnessOption, HarnessDisplayInfo } from "./harness.js";
|
|
14
|
+
export { FAST_SERVICE_TIER, toProtoServiceTier, fromProtoServiceTier } from "./service-tier.js";
|
|
15
|
+
export type { ServiceTierOption } from "./service-tier.js";
|
package/src/models/registry.ts
CHANGED
|
@@ -89,6 +89,16 @@ export interface ModelInfo {
|
|
|
89
89
|
* before "Show All Models" is expanded or search is used).
|
|
90
90
|
*/
|
|
91
91
|
readonly featured: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Pricing-variant keys the registry prices for this model (e.g.
|
|
94
|
+
* `["fast"]`). A priced variant is a *selectable* service tier
|
|
95
|
+
* (stigmer/stigmer#357): the tier toggle renders only for models whose
|
|
96
|
+
* `serviceTiers` includes it. Empty for models with no variants.
|
|
97
|
+
*
|
|
98
|
+
* Distinct from {@link speedTier}, which is a static latency badge,
|
|
99
|
+
* not a selectable option.
|
|
100
|
+
*/
|
|
101
|
+
readonly serviceTiers: readonly string[];
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
/**
|
|
@@ -153,6 +163,8 @@ interface RegistryJsonEntry {
|
|
|
153
163
|
cacheWritePricePerMillion: number;
|
|
154
164
|
cacheReadPricePerMillion: number;
|
|
155
165
|
};
|
|
166
|
+
/** Variant-key → variant pricing block; only the key set matters here. */
|
|
167
|
+
pricingVariants?: Record<string, unknown>;
|
|
156
168
|
$comment?: string;
|
|
157
169
|
}
|
|
158
170
|
|
|
@@ -192,6 +204,10 @@ export function parseRegistryJson(data: unknown): ModelInfo[] {
|
|
|
192
204
|
costTier: m.costTier as CostTier,
|
|
193
205
|
harness: m.harness as HarnessOption,
|
|
194
206
|
featured: m.featured ?? false,
|
|
207
|
+
serviceTiers:
|
|
208
|
+
m.pricingVariants && typeof m.pricingVariants === "object"
|
|
209
|
+
? Object.keys(m.pricingVariants).sort()
|
|
210
|
+
: [],
|
|
195
211
|
}));
|
|
196
212
|
}
|
|
197
213
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* String form of the service tier for component props and hook inputs
|
|
5
|
+
* (stigmer/stigmer#357). Mirrors {@link HarnessOption}: components speak
|
|
6
|
+
* strings, execution creation speaks proto.
|
|
7
|
+
*
|
|
8
|
+
* "standard" is the model's base-priced configuration; "fast" is the
|
|
9
|
+
* provider's fast variant at the registry's fast rates — selectable only
|
|
10
|
+
* for models whose {@link ModelInfo.serviceTiers} includes "fast".
|
|
11
|
+
*/
|
|
12
|
+
export type ServiceTierOption = "standard" | "fast";
|
|
13
|
+
|
|
14
|
+
/** The tier key a model must price for the fast toggle to render. */
|
|
15
|
+
export const FAST_SERVICE_TIER = "fast";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert a {@link ServiceTierOption} string to the proto enum.
|
|
19
|
+
*/
|
|
20
|
+
export function toProtoServiceTier(tier: ServiceTierOption): ServiceTier {
|
|
21
|
+
switch (tier) {
|
|
22
|
+
case "fast":
|
|
23
|
+
return ServiceTier.FAST;
|
|
24
|
+
case "standard":
|
|
25
|
+
default:
|
|
26
|
+
return ServiceTier.STANDARD;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert a proto {@link ServiceTier} enum to its string form.
|
|
32
|
+
*
|
|
33
|
+
* `UNSPECIFIED` (and any unknown value) maps to `undefined` so callers can
|
|
34
|
+
* decide their own default — matching the proto contract (`UNSPECIFIED`
|
|
35
|
+
* resolves to `SERVICE_TIER_STANDARD` in the runner, never the provider
|
|
36
|
+
* account default).
|
|
37
|
+
*/
|
|
38
|
+
export function fromProtoServiceTier(
|
|
39
|
+
tier: ServiceTier | undefined,
|
|
40
|
+
): ServiceTierOption | undefined {
|
|
41
|
+
switch (tier) {
|
|
42
|
+
case ServiceTier.STANDARD:
|
|
43
|
+
return "standard";
|
|
44
|
+
case ServiceTier.FAST:
|
|
45
|
+
return "fast";
|
|
46
|
+
default:
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|