diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx index 876843dd1..b0421ccc1 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx @@ -25,9 +25,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ // would be wrong; we bump it via the test rate to demonstrate the // boundary). Real gateway rates here. costDetails: { - inputTokenCost: 3.0, - outputTokenCost: 15.0, - currency: 'USD', + flatCosts: { inputTokenCost: 300, outputTokenCost: 1500 }, }, }, }, @@ -40,9 +38,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ modelDetails: { contextWindowTokens: 200000, costDetails: { - inputTokenCost: 3.0, - outputTokenCost: 15.0, - currency: 'USD', + flatCosts: { inputTokenCost: 300, outputTokenCost: 1500 }, }, }, }, @@ -56,9 +52,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ modelDetails: { contextWindowTokens: 1000000, costDetails: { - inputTokenCost: 0.35, - outputTokenCost: 1.5, - currency: 'USD', + flatCosts: { inputTokenCost: 35, outputTokenCost: 150 }, }, }, }, @@ -74,9 +68,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ maxOutputTokens: 128000, // gpt-5 large: premium costDetails: { - inputTokenCost: 6.0, - outputTokenCost: 18.0, - currency: 'USD', + flatCosts: { inputTokenCost: 600, outputTokenCost: 1800 }, }, }, }, @@ -94,9 +86,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ contextWindowTokens: 128000, maxOutputTokens: 16384, costDetails: { - inputTokenCost: 2.5, - outputTokenCost: 10.0, - currency: 'USD', + flatCosts: { inputTokenCost: 250, outputTokenCost: 1000 }, }, }, }, @@ -189,9 +179,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ maxOutputTokens: 32768, // gpt-4.1-mini: basic costDetails: { - inputTokenCost: 0.4, - outputTokenCost: 1.6, - currency: 'USD', + flatCosts: { inputTokenCost: 40, outputTokenCost: 160 }, }, }, }, @@ -213,9 +201,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ contextWindowTokens: 400000, maxOutputTokens: 128000, costDetails: { - inputTokenCost: 2.5, - outputTokenCost: 10.0, - currency: 'USD', + flatCosts: { inputTokenCost: 250, outputTokenCost: 1000 }, }, }, }, @@ -377,6 +363,30 @@ export const AdminCanManageByo: Story = { }, }; +export const DeleteWithConfirmation: Story = { + name: 'Admin: delete asks for confirmation', + render: Controlled, + args: { + variant: 'searchable', + models: MOCK_MODELS, + canManageByo: true, + onDeleteModel: (model: DiscoveryModel) => { + // eslint-disable-next-line no-console + console.log('[story] confirmed delete of', model.modelId); + }, + }, + parameters: { + docs: { + description: { + story: + 'Pass `onDeleteModel` and BYO rows gain a delete action. The picker ' + + 'always shows a confirmation dialog naming the configuration before ' + + 'invoking the handler — deletion affects every consumer in the tenant.', + }, + }, + }, +}; + export const ViewerCannotManageByo: Story = { name: 'Viewer: read-only BYO', render: Controlled, diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.test.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.test.tsx index 2c97bba1e..2366e3c98 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.test.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.test.tsx @@ -253,7 +253,7 @@ describe('', () => { } }); - it('renders a delete row action on BYO rows when onDeleteModel is provided', async () => { + it('delete row action asks for confirmation before invoking onDeleteModel', async () => { const user = userEvent.setup(); const onDeleteModel = vi.fn(); renderPicker( @@ -267,9 +267,18 @@ describe('', () => { ); await user.click(screen.getByRole('button', { expanded: false })); const deleteButton = await screen.findByRole('button', { name: /delete configuration/i }); + + // Cancel: nothing happens. await user.click(deleteButton); + await user.click(await screen.findByRole('button', { name: /cancel/i })); + expect(onDeleteModel).not.toHaveBeenCalled(); + + // Confirm: the host handler runs with the BYO row. Opening the dialog + // closes the popup, so reopen it once the dialog's aria-hidden lifts. + await user.click(await screen.findByRole('button', { expanded: false })); + await user.click(await screen.findByRole('button', { name: /delete configuration/i })); + await user.click(await screen.findByRole('button', { name: /^delete$/i })); expect(onDeleteModel).toHaveBeenCalledTimes(1); - // Called with the BYO model row. expect(onDeleteModel.mock.calls[0][0]).toMatchObject({ modelSubscriptionType: 'BYOMAdded' }); }); diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx index fc6540554..af23b2142 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx @@ -1,11 +1,18 @@ import AddIcon from '@mui/icons-material/Add'; import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; import ButtonBase from '@mui/material/ButtonBase'; import CircularProgress from '@mui/material/CircularProgress'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; import Typography from '@mui/material/Typography'; import { Colors, FontFamily } from '@uipath/apollo-core'; import React from 'react'; import { useSafeLingui } from '../../../i18n'; +import { DELETE_CONFIRM } from './i18n'; import type { ModelBadgeKind } from './badges'; import { FolderSwitcher, type FolderSwitcherFolder } from './primitives/FolderSwitcher'; import { defaultRowActions } from './primitives/ModelOptionRow'; @@ -424,14 +431,20 @@ export const ModelPicker = React.forwardRef // In self-fetch mode a BYO delete refetches the catalog once the // host's handler resolves; with host-owned models the host refreshes. + // Deleting a configuration affects every consumer in the tenant, so the + // picker always confirms before invoking the host's handler. + const [pendingDelete, setPendingDelete] = React.useState(null); const handleDeleteModel = React.useMemo(() => { if (!onDeleteModel) return undefined; - if (!selfFetchCtx) return onDeleteModel; - return async (m: DiscoveryModel) => { - await onDeleteModel(m); - refetchDiscovery(); - }; - }, [onDeleteModel, selfFetchCtx, refetchDiscovery]); + return (m: DiscoveryModel) => setPendingDelete(m); + }, [onDeleteModel]); + const confirmPendingDelete = React.useCallback(async () => { + const model = pendingDelete; + setPendingDelete(null); + if (!model) return; + await onDeleteModel?.(model); + if (selfFetchCtx) refetchDiscovery(); + }, [pendingDelete, onDeleteModel, selfFetchCtx, refetchDiscovery]); // BYO rows without a host-supplied `byoConnectionLabel` get their // Integration Service connection name resolved by the picker itself. @@ -896,6 +909,56 @@ export const ModelPicker = React.forwardRef )} + + setPendingDelete(null)} + container={popupContainer} + disablePortal={disablePortal} + maxWidth="xs" + // Destructive interrupt: announce as an alert dialog wired to its + // own title and description. Visuals are deliberately left to the + // host's MUI theme so the dialog matches the product's other + // confirmation dialogs. + role="alertdialog" + aria-labelledby={`${id}-delete-title`} + aria-describedby={`${id}-delete-description`} + sx={{ zIndex: 1500 }} + PaperProps={{ sx: { fontFamily: FontFamily.FontNormal } }} + > + + {_(DELETE_CONFIRM.title)} + + + + {_({ + ...DELETE_CONFIRM.message, + values: { + name: pendingDelete ? (pendingDelete.displayName ?? pendingDelete.modelName) : '', + }, + })} + + + + {/* Focus lands on the safe action for a destructive confirm. */} + + + + ); } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/README.md b/packages/apollo-react/src/material/components/ap-model-picker/README.md index 4729288f4..75cc709dc 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/README.md +++ b/packages/apollo-react/src/material/components/ap-model-picker/README.md @@ -154,7 +154,7 @@ Resolution order: `recommendedModelIds` prop (test/storybook override) → DTO ` ### 5. Cost badges -Cost badges are **on by default**: whenever the Discovery DTO carries cost data, each row gets its tier from the Apollo pool (`cost-basic` / `cost-standard` / `cost-premium`) via `defaultCostTier`, which bins `modelDetails.costDetails.inputTokenCost` (USD per million input tokens) at `$1` / `$5`. No wiring needed. +Cost badges are **on by default**: whenever the Discovery DTO carries cost data, each row gets its tier from the Apollo pool (`cost-basic` / `cost-standard` / `cost-premium`) via `defaultCostTier`, which bins `modelDetails.costDetails.flatCosts.inputTokenCost` (cents per million input tokens, the gateway's unit; the first tier is used for tiered pricing) at `$1`/M and `$5`/M. No wiring needed. A host-supplied `badgesFor` takes over entirely — return your own pool kinds to reclassify, or `[]` to suppress badges: @@ -185,7 +185,7 @@ Under the hood: `GET {baseUrl}/portal_/api/organization/UserOrganizationInfo` - The **"Use custom model" footer** opens the *add configuration* form, deep-linked to `/{tenantId}/{folderId}/add` and pre-populated via `?product=&feature=` from `requestingProduct`/`requestingFeature`. The folder id is the switcher's current selection, or — when none is selected ("All folders") — the first available folder, matching the configurations page's own default. Only when no folders exist does it fall back to the configurations list. - The **edit row action** (BYO rows only) opens the configuration's *edit* form when the model carries `byomDetails.byoConfigurationId` (served by Discovery on UiPath/Arima#2659); when absent it lands on the configurations list scoped to the tenant + folder. -- The **delete row action** (BYO rows only) is opt-in: pass `onDeleteModel` and the picker renders a delete icon next to edit, calling your handler (which performs the deletion and refreshes `models`). Omit it to keep removal on the configurations page only. +- The **delete row action** (BYO rows only) is opt-in: pass `onDeleteModel` and the picker renders a delete icon next to edit. The picker always asks for confirmation first (a built-in dialog naming the configuration), then calls your handler; in self-fetch mode it refetches Discovery once your handler resolves. Omit it to keep removal on the configurations page only. These navigations always open the AI Trust Layer pages in a new browser tab - the picker is embedded in a product surface, and navigating it away would unload the user's in-progress work. `onUseCustomModel` overrides the footer's default navigation (e.g. an in-app wizard), and `slots.optionActions` overrides the row actions. Products with their own authorization model can pass `canManageByo` (`true`/`false`); when set, no admin check request is made. diff --git a/packages/apollo-react/src/material/components/ap-model-picker/i18n.ts b/packages/apollo-react/src/material/components/ap-model-picker/i18n.ts index 56a52e9a5..4ffe4fb31 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/i18n.ts +++ b/packages/apollo-react/src/material/components/ap-model-picker/i18n.ts @@ -160,6 +160,30 @@ export const ROW_ACTIONS = { }), } as const; +/* ────────────────────────────────────────────────────────────────────── + * Delete confirmation + * ─────────────────────────────────────────────────────────────────── */ + +export const DELETE_CONFIRM = { + title: msg({ + id: 'modelPicker.deleteConfirm.title', + message: 'Delete custom model', + }), + message: msg({ + id: 'modelPicker.deleteConfirm.message', + message: + 'This permanently deletes the "{name}" configuration for everyone in this tenant. This action cannot be undone.', + }), + cancel: msg({ + id: 'modelPicker.deleteConfirm.cancel', + message: 'Cancel', + }), + confirm: msg({ + id: 'modelPicker.deleteConfirm.confirm', + message: 'Delete', + }), +} as const; + /* ────────────────────────────────────────────────────────────────────── * Footer CTA * ─────────────────────────────────────────────────────────────────── */ diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/de.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/de.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/de.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/de.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/en.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/en.json index 3a951a395..bfcffc4ba 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/en.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/en.json @@ -42,5 +42,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/es-MX.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/es-MX.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/es-MX.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/es-MX.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/es.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/es.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/es.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/es.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/fr.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/fr.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/fr.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/fr.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/ja.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/ja.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/ja.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/ja.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/ko.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/ko.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/ko.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/ko.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/pt-BR.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/pt-BR.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/pt-BR.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/pt-BR.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/pt.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/pt.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/pt.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/pt.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/ro.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/ro.json index 0967ef424..1d74dc163 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/ro.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/ro.json @@ -1 +1,6 @@ -{} +{ + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" +} diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/ru.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/ru.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/ru.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/ru.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/tr.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/tr.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/tr.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/tr.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-CN.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-CN.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-CN.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-CN.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-TW.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-TW.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-TW.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-TW.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/types.ts b/packages/apollo-react/src/material/components/ap-model-picker/types.ts index 2a5d6096d..f9aaaa7d9 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/types.ts +++ b/packages/apollo-react/src/material/components/ap-model-picker/types.ts @@ -57,10 +57,24 @@ export interface ByomDetails { byoConfigurationId?: string; } -export interface ModelCostDetails { +/** Token costs in cents per million tokens (the gateway's unit). */ +export interface ModelFlatCosts { inputTokenCost?: number; outputTokenCost?: number; - currency?: string; + cacheReadInputTokenCost?: number; + cacheWriteInputTokenCost?: number; +} + +export interface ModelTieredCost extends ModelFlatCosts { + minimumTokens?: number; + maximumTokens?: number; +} + +export interface ModelCostDetails { + /** `Flat` | `Tiered` */ + pricingType?: string; + flatCosts?: ModelFlatCosts; + tieredCosts?: ModelTieredCost[]; } export interface ModelDetails { diff --git a/packages/apollo-react/src/material/components/ap-model-picker/utils.test.ts b/packages/apollo-react/src/material/components/ap-model-picker/utils.test.ts index e443720e2..271ceb18e 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/utils.test.ts +++ b/packages/apollo-react/src/material/components/ap-model-picker/utils.test.ts @@ -246,7 +246,7 @@ describe('deriveModelTags', () => { const tags = deriveModelTags( model({ modelId: 'a', - modelDetails: { costDetails: { inputTokenCost: 6 } }, + modelDetails: { costDetails: { flatCosts: { inputTokenCost: 600 } } }, }) ); expect(tags.find((t) => t.kind === 'cost-premium')).toBeTruthy(); @@ -259,7 +259,7 @@ describe('deriveModelTags', () => { const suppressed = deriveModelTags( model({ modelId: 'a', - modelDetails: { costDetails: { inputTokenCost: 6 } }, + modelDetails: { costDetails: { flatCosts: { inputTokenCost: 600 } } }, }), { badgesFor: () => [] } ); @@ -274,7 +274,7 @@ describe('deriveModelTags', () => { const tags = deriveModelTags( model({ modelId: 'a', - modelDetails: { costDetails: { inputTokenCost: 6 } }, + modelDetails: { costDetails: { flatCosts: { inputTokenCost: 600 } } }, }), { customTagsFor: costBadges } ); @@ -305,15 +305,21 @@ describe('defaultCostTier', () => { }); it('bins at < $1 / < $5 / >= $5 thresholds', () => { - expect(defaultCostTier(model({ modelDetails: { costDetails: { inputTokenCost: 0.4 } } }))).toBe( - 'basic' - ); - expect(defaultCostTier(model({ modelDetails: { costDetails: { inputTokenCost: 3 } } }))).toBe( - 'standard' - ); - expect(defaultCostTier(model({ modelDetails: { costDetails: { inputTokenCost: 6 } } }))).toBe( - 'premium' - ); + expect( + defaultCostTier( + model({ modelDetails: { costDetails: { flatCosts: { inputTokenCost: 40 } } } }) + ) + ).toBe('basic'); + expect( + defaultCostTier( + model({ modelDetails: { costDetails: { flatCosts: { inputTokenCost: 300 } } } }) + ) + ).toBe('standard'); + expect( + defaultCostTier( + model({ modelDetails: { costDetails: { flatCosts: { inputTokenCost: 600 } } } }) + ) + ).toBe('premium'); }); }); diff --git a/packages/apollo-react/src/material/components/ap-model-picker/utils.ts b/packages/apollo-react/src/material/components/ap-model-picker/utils.ts index ad4659229..f4ce32000 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/utils.ts +++ b/packages/apollo-react/src/material/components/ap-model-picker/utils.ts @@ -250,15 +250,18 @@ export function deriveModelTags( * return tier ? [`cost-${tier}` as const] : []; * }} * - * Bins Discovery's `inputTokenCost` (USD per million input tokens) at + * Bins Discovery's `costDetails.flatCosts.inputTokenCost` (cents per million input tokens) at * $1 / $5. Copy it and change the thresholds, or classify on something * else entirely — the pool badge kinds are the shared contract, the * classifier is yours. */ -const DEFAULT_BASIC_THRESHOLD = 1.0; -const DEFAULT_PREMIUM_THRESHOLD = 5.0; +// Cents per million input tokens: $1/M and $5/M. +const DEFAULT_BASIC_THRESHOLD = 100; +const DEFAULT_PREMIUM_THRESHOLD = 500; export function defaultCostTier(model: DiscoveryModel): CostTier | null { - const inputCost = model.modelDetails?.costDetails?.inputTokenCost; + const costDetails = model.modelDetails?.costDetails; + const inputCost = + costDetails?.flatCosts?.inputTokenCost ?? costDetails?.tieredCosts?.[0]?.inputTokenCost; if (inputCost == null) return null; if (inputCost < DEFAULT_BASIC_THRESHOLD) return 'basic'; if (inputCost < DEFAULT_PREMIUM_THRESHOLD) return 'standard';