fix(site): fix price threshold display and debounce model price lookup - #28298
Conversation
d3041d1 to
a41ae52
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a41ae52ae9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const debouncePending = debouncedModel !== trimmedModel; | ||
| const livePriceLoading = | ||
| livePricesQuery.fetchStatus !== "idle" && !livePricesQuery.isSuccess; | ||
| const livePrice = livePricesQuery.data?.[0]; | ||
| debouncePending || | ||
| (livePricesQuery.fetchStatus !== "idle" && !livePricesQuery.isSuccess); |
There was a problem hiding this comment.
Gate debounce loading on the live lookup
On deployments without the aibridge entitlement, livePricesQuery is disabled, but changing the model still makes debouncePending set livePriceLoading to true. As a result, selecting or typing a catalog model hides its synchronously available prices behind four loading skeletons for 500 ms even though no request can run. Gate this pending state on the live-price query being enabled so catalog-only deployments continue to update immediately.
AGENTS.md reference: site/AGENTS.md:L16-L17
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 3621dfb. The debounce-pending state is now gated on the live price query being enabled, so catalog-only deployments keep updating immediately without flashing loading placeholders. Added a story (CostEstimateCatalogNotHiddenByDebounce) asserting catalog prices render with no skeletons when the entitlement is absent.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
a41ae52 to
3621dfb
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3621dfbaba
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const debouncePending = | ||
| livePriceQueryEnabled && debouncedModel !== trimmedModel; |
There was a problem hiding this comment.
Treat the initial model debounce as loading
On an AI Bridge-entitled add form, or after an empty identifier has settled, typing a known catalog model leaves debouncedModel empty, so livePriceQueryEnabled and therefore debouncePending are false for 500 ms. During that window the catalog price renders even though the deployment price lookup has not run; if the deployment has an override, users briefly see an incorrect estimate before it changes to a skeleton and then the live price. Derive the pending state from the current model's eligibility rather than the old debounced model's query-enabled flag.
AGENTS.md reference: site/AGENTS.md:L16-L17
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ec8aeee. The pending state now derives from the current model's lookup eligibility (entitled, provider set, non-empty identifier) rather than the debounced model's, so typing into an empty identifier on an entitled form shows the loading placeholder immediately instead of briefly flashing catalog prices before the lookup runs.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
| const livePriceLoading = | ||
| livePricesQuery.fetchStatus !== "idle" && !livePricesQuery.isSuccess; | ||
| const livePrice = livePricesQuery.data?.[0]; | ||
| debouncePending || | ||
| (livePricesQuery.fetchStatus !== "idle" && !livePricesQuery.isSuccess); | ||
| const livePrice = debouncePending ? undefined : livePricesQuery.data?.[0]; |
There was a problem hiding this comment.
Hide errors belonging to the previous model
When the current live-price lookup has failed and the user changes to another nonempty model, debouncePending becomes true but the query remains keyed to the old model for 500 ms. Its isError state therefore wins at the return below, showing "Couldn't load pricing" for the newly typed model even though its request has not started. Suppress the old query's error while the debounce is pending so this interval uses the intended loading state.
AGENTS.md reference: site/AGENTS.md:L16-L17
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ec8aeee. The previous model's error state is now suppressed while the debounce is pending, so changing the identifier after a failure shows the loading placeholder instead of "Couldn't load pricing." until the new request fires. Covered by the CostEstimateErrorClearsWhileDebouncePending story.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
ec8aeee to
90afc64
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 90afc64194
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }); | ||
| const debouncePending = | ||
| entitledWithProvider && | ||
| trimmedModel !== "" && |
There was a problem hiding this comment.
Discard stale pricing when the identifier is cleared
On an entitled form with a loaded live price, clearing the model identifier leaves debouncedModel pointing at the old model for 500 ms, but this nonempty guard makes debouncePending false. The old query data or error is therefore rendered against the empty identifier until the debounce settles instead of showing the empty state. Fresh evidence beyond the previously fixed nonempty-model case is this explicit trimmedModel !== "" exclusion.
AGENTS.md reference: site/AGENTS.md:L16-L17
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in dc6148b. The staleness check is now symmetric: debouncePending is true whenever the debounced identifier differs from the current one, so clearing the field immediately discards the settled live price (or error) and shows the loading placeholder until the debounce settles, at which point the empty state renders. Covered by the CostEstimateClearingModelDiscardsStalePrices story.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
90afc64 to
dc6148b
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc6148bb88
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ); | ||
| }; | ||
|
|
||
| const mockGPT5LivePriceBookQueries = [ |
There was a problem hiding this comment.
Inline each story's query wiring
This pre-wired query array is shared by CostEstimateFromLivePriceBook and CostEstimateClearingModelDiscardsStalePrices, so changing one story's server setup implicitly changes the other. Keep only reusable entity data in a fixture and compose each story's { key, data } query entry inline as required by the frontend story-fixture contract.
AGENTS.md reference: site/AGENTS.md:L24-L25
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 449103e. The shared query array is replaced by an entity fixture (MockGPT5PriceBookEntry), and each story composes its own { key, data } query entry inline per the FE9 story-fixture contract.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
| <InputGroupAddon align="inline-start"> | ||
| {price?.belowThreshold ? "<$" : "$"} | ||
| </InputGroupAddon> |
There was a problem hiding this comment.
Expose the threshold marker to assistive technology
For a price below the threshold, <$ is rendered only as a sibling of the read-only input and is not part of that input's accessible name or value. A screen-reader user focusing the field therefore hears an exact value such as Input, 0.0001 rather than “less than $0.0001”; associate the marker with the input, for example through an accessible description, so the estimate retains its meaning.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 449103e. Below-threshold price inputs now carry aria-valuetext="less than $0.0001", so screen readers announce the threshold semantics instead of an exact value. Normal prices rely on the default value announcement (the $ addon is read as part of the group); only the < marker needed the override. Asserted in the CostEstimateBelowThresholdPrice story.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
dc6148b to
449103e
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 449103e9f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| { | ||
| provider: "openai", | ||
| model: "gpt-5", | ||
| input_price: 1250000, | ||
| output_price: 10000000, | ||
| cache_read_price: 125000, | ||
| input_price: 50, | ||
| output_price: null, |
There was a problem hiding this comment.
Move the inline price entity into a shared fixture
Fresh evidence after the earlier query-wiring fix is this complete inline AIModelPrice variant, while the new base price entry also remains module-local. Provider, model, and timestamps are consequently duplicated and can drift between pricing stories; define the base as a shared Mock* entity under testHelpers, spread it into a named below-threshold variant, and keep only the { key, data } wiring inline.
AGENTS.md reference: site/AGENTS.md:L24-L25
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in f65a309. The base entry and below-threshold variant now live in site/src/testHelpers/chatModels.ts as MockGPT5ModelPrice and MockGPT5BelowThresholdModelPrice (spread variant, shared timestamps via MOCK_TIMESTAMP); each story keeps only its { key, data } wiring inline.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
| aria-valuetext={ | ||
| price?.belowThreshold | ||
| ? `less than $${price.value}` | ||
| : undefined |
There was a problem hiding this comment.
Use a supported description for the threshold value
Fresh evidence after the earlier accessibility fix is that this remains an implicit textbox, for which aria-valuetext is not a supported state, so assistive technology may ignore it and announce the raw value 0.0001; the visible <$ marker is still only a sibling. Associate the threshold wording through a supported mechanism such as aria-describedby and visually hidden descriptive text.
AGENTS.md reference: site/AGENTS.md:L18-L19
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in f65a309. aria-valuetext is dropped; below-threshold inputs now use aria-describedby pointing at visually hidden text ("less than $0.0001 USD per million tokens", rendered only while a threshold price is displayed), which is a supported description mechanism for textboxes. Asserted via toHaveAccessibleDescription in the CostEstimateBelowThresholdPrice story.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
449103e to
f65a309
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f65a30935c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| within, | ||
| } from "storybook/test"; | ||
| import { reactRouterParameters } from "storybook-addon-remix-react-router"; | ||
| import { vi } from "vitest"; |
There was a problem hiding this comment.
Replace the runtime Vitest dependency in the story
When these stories run in the normal Storybook preview rather than through the Vitest addon, the runtime vi.waitFor and vi.mocked calls have no active Vitest worker and can fail instead of executing the play functions. This also prevents the newly added interaction coverage from serving as usable Storybook documentation. Use the already imported waitFor and assertions from storybook/test so the stories work in both preview and test-runner contexts.
AGENTS.md reference: site/AGENTS.md:L9-L10
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 78452ff. The runtime vitest import is gone; the stories use waitFor and mocked from storybook/test, so the play functions run in the plain Storybook preview as well as under the Vitest addon.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
| ...aiModelPrices(normalizedProvider, trimmedModel), | ||
| enabled: | ||
| aibridgeEntitled && normalizedProvider !== "" && trimmedModel !== "", | ||
| ...aiModelPrices(normalizedProvider, debouncedModel), |
There was a problem hiding this comment.
Avoid querying a stale model after a provider change
If an admin edits the identifier and switches providers within the 500 ms debounce window, this query combines the new normalizedProvider with the previous debouncedModel, issuing a lookup for a provider/model pair the form never represented. The pending gate hides the result, but the unnecessary request still defeats the debounce's request-reduction goal; debounce the provider/model pair together or disable the query while the model values differ.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 78452ff. The debounce now wraps the { provider, model } pair as a unit, so the query key only ever represents a settled pair; a provider change within the debounce window counts as pending and never issues a lookup mixing the new provider with the previous identifier.
Reply generated by Coder Agents on behalf of @DanielleMaywood.
Post-merge feedback on #28290: - A tiny positive price formats as a threshold, but the estimate field sliced the first character assuming a leading $\, which dropped the less-than marker and rendered $$0.0001. formatPricePerMillionTokens now returns the display value without a currency symbol plus a belowThreshold flag, so the field renders the marker ahead of the dollar addon. - In edit mode the model identifier is a plain input, so each keystroke keyed a new live price query and fired a request. The lookup now runs on a 500ms debounced identifier, and a pending debounce reads as loading so stale prices are not shown for identifiers the user has typed past.
f65a309 to
78452ff
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
ssncferreira
left a comment
There was a problem hiding this comment.
LGTM
thank you for the follow-up 🏆
Follow-up to post-merge review feedback on #28290 from @ssncferreira.
<dropped from threshold prices: a tiny positive price formats as a threshold (<$0.0001), but the estimate field sliced the first character assuming a leading$, which dropped the less-than marker and rendered$$0.0001.formatPricePerMillionTokensnow returns the display value without a currency symbol plus abelowThresholdflag, so the field renders the<ahead of the$addon.Per-keystroke API calls in edit mode: in edit mode the model identifier is a plain input, so each keystroke keyed a new live price query and fired a request. The lookup now runs on a 500ms debounced identifier (existing
useDebouncedValuehook), and a pending debounce reads as loading so stale prices are not shown for identifiers the user has typed past. Catalog lookups stay synchronous on the live value.Fix approaches confirmed with Qwen 3.8 Max before implementation.
Implementation notes for reviewers
formatPricePerMillionTokenshas exactly one caller (PricingEstimateFields), which previously re-parsed the display string with.slice(1). Returning{ belowThreshold, value }removes the string round-trip instead of adding a second parsing helper; unit tests updated to match.PricingEstimateFieldsso edit mode, duplicate mode, and add-without-catalog all benefit.staleTimecould not fix this: each keystroke mints a new query key, so there is never a cached entry to respect.debouncePendinggate is needed because a previously fetched model can be cached: without it, typing back to a cached prefix then continuing would render stale prices with no loading indicator. While pending, the existing skeleton renders.CostEstimateBelowThresholdPrice(seeds a 50-micro input price, asserts<$marker and0.0001value) andCostEstimateDebouncesLivePriceLookup(spies ongetAIModelPrices, typesgpt-5.5character by character, asserts only the mount lookup and one lookup for the settled value fire).Generated by Coder Agents on behalf of @DanielleMaywood.