44 */
55import { DeploymentLauncherPageContentPresetSummaryQuery } from '../__generated__/DeploymentLauncherPageContentPresetSummaryQuery.graphql' ;
66import { DeploymentLauncherPageContent_deployment$key } from '../__generated__/DeploymentLauncherPageContent_deployment.graphql' ;
7+ import { convertToBinaryUnit } from '../helper' ;
78import { parseCliCommand } from '../helper/parseCliCommand' ;
89import {
910 mergeExtraArgs ,
1011 reverseMapExtraArgs ,
1112} from '../helper/runtimeExtraArgsParser' ;
13+ import { useSuspendedBackendaiClient } from '../hooks' ;
1214import { ResourceSlotName , useResourceSlots } from '../hooks/backendai' ;
1315import { useBAISettingUserState } from '../hooks/useBAISetting' ;
1416import { useCurrentProjectValue } from '../hooks/useCurrentProject' ;
@@ -20,6 +22,7 @@ import {
2022 getExtraArgsEnvVarName ,
2123 type RuntimeParameterGroup ,
2224} from '../hooks/useRuntimeParameterSchema' ;
25+ import { ResourceNumbersOfSession } from '../pages/SessionLauncherPage' ;
2326import ErrorBoundaryWithNullFallback from './ErrorBoundaryWithNullFallback' ;
2427import ImageEnvironmentSelectFormItems , {
2528 ImageEnvironmentFormInput ,
@@ -28,6 +31,9 @@ import ResourcePresetSelect from './ResourcePresetSelect';
2831import RuntimeParameterFormSection , {
2932 RuntimeParameterValues ,
3033} from './RuntimeParameterFormSection' ;
34+ import ResourceAllocationFormItems , {
35+ type ResourceAllocationFormValue ,
36+ } from './SessionFormItems/ResourceAllocationFormItems' ;
3137import SourceCodeView from './SourceCodeView' ;
3238import VFolderLazyView from './VFolderLazyView' ;
3339import VFolderSelect from './VFolderSelect' ;
@@ -133,6 +139,11 @@ export type DeploymentLauncherFormValue = ImageEnvironmentFormInput & {
133139 // Step 3 — Resources & Replicas
134140 resourceGroup : string ;
135141 resourcePresetId ?: string ;
142+ /** Mirrors resourcePresetId so ResourceAllocationFormItems can track custom state. */
143+ allocationPreset ?: string ;
144+ /** Custom resource slots — populated when resourcePresetId === 'custom'. */
145+ resource ?: ResourceAllocationFormValue [ 'resource' ] ;
146+ enabledAutomaticShmem ?: boolean ;
136147 desiredReplicaCount : number ;
137148} ;
138149
@@ -146,6 +157,15 @@ export interface DeploymentLauncherPageContentProps {
146157 deploymentFrgmt ?: DeploymentLauncherPageContent_deployment$key | null ;
147158 /** Available runtime variants fetched by the parent page layout. */
148159 runtimeVariants ?: ReadonlyArray < { name : string ; id : string } > ;
160+ /** Available resource presets — used in edit mode to detect custom allocation. */
161+ resourcePresets ?: ReadonlyArray <
162+ | {
163+ name ?: string | null ;
164+ resource_slots ?: string | null ;
165+ }
166+ | null
167+ | undefined
168+ > ;
149169 /**
150170 * Optional change observer forwarded to the underlying antd `<Form>`.
151171 * Useful for parent pages that want to persist the draft state
@@ -173,6 +193,95 @@ export interface DeploymentLauncherPageContentProps {
173193 } | null > ;
174194}
175195
196+ /**
197+ * Compare revision resourceSlots against available presets to detect custom
198+ * allocation, and return the matching form fields. Returns `resourcePresetId:
199+ * 'custom'` with populated `resource.*` values when no preset matches.
200+ */
201+ const resolveEditModeResourcePreset = (
202+ revisionSlots : ReadonlyArray < {
203+ slotName : string ;
204+ quantity : string ;
205+ } | null > | null ,
206+ resourceOptsEntries : ReadonlyArray < {
207+ name : string ;
208+ value : string ;
209+ } | null > | null ,
210+ presets : ReadonlyArray <
211+ | {
212+ name ?: string | null ;
213+ resource_slots ?: string | null ;
214+ }
215+ | null
216+ | undefined
217+ > | null ,
218+ ) : Partial < DeploymentLauncherFormValue > => {
219+ if ( ! revisionSlots || revisionSlots . length === 0 ) return { } ;
220+
221+ const slots = revisionSlots . filter (
222+ ( s ) : s is { slotName : string ; quantity : string } => s !== null ,
223+ ) ;
224+
225+ // Normalize a slot quantity to bytes string for comparison.
226+ // Mem values may come as "4g" (preset) or raw bytes "4294967296" (revision).
227+ const toComparableBytes = ( slotName : string , value : string ) : string => {
228+ if ( slotName !== 'mem' ) return value ;
229+ return String ( convertToBinaryUnit ( value , '' ) ?. number ?? value ) ;
230+ } ;
231+
232+ const matchingPreset = presets ?. find ( ( p ) => {
233+ if ( ! p ?. name || ! p ?. resource_slots ) return false ;
234+ let presetSlots : Record < string , string > ;
235+ try {
236+ presetSlots = JSON . parse ( p . resource_slots ) ;
237+ } catch {
238+ return false ;
239+ }
240+ const presetKeys = Object . keys ( presetSlots ) ;
241+ if ( presetKeys . length !== slots . length ) return false ;
242+ return presetKeys . every ( ( key ) => {
243+ const revSlot = slots . find ( ( s ) => s . slotName === key ) ;
244+ if ( ! revSlot ) return false ;
245+ return (
246+ toComparableBytes ( key , String ( revSlot . quantity ) ) ===
247+ toComparableBytes ( key , String ( presetSlots [ key ] ) )
248+ ) ;
249+ } ) ;
250+ } ) ;
251+
252+ if ( matchingPreset ?. name ) {
253+ return {
254+ resourcePresetId : matchingPreset . name ,
255+ allocationPreset : matchingPreset . name ,
256+ } ;
257+ }
258+
259+ // No preset matches — custom allocation. Reconstruct resource.* fields.
260+ const cpu = Number ( slots . find ( ( s ) => s . slotName === 'cpu' ) ?. quantity ?? 0 ) ;
261+ const rawMem = slots . find ( ( s ) => s . slotName === 'mem' ) ?. quantity ?? '0g' ;
262+ // Server returns mem in bytes; convert to "Xg" format for the slider.
263+ const mem = convertToBinaryUnit ( rawMem , 'g' , 4 ) ?. value ?? rawMem ;
264+ const shmemEntry = resourceOptsEntries
265+ ?. filter ( ( e ) : e is { name : string ; value : string } => e !== null )
266+ . find ( ( e ) => e . name === 'shmem' ) ;
267+ const acceleratorSlot = slots . find (
268+ ( s ) => s . slotName !== 'cpu' && s . slotName !== 'mem' ,
269+ ) ;
270+
271+ return {
272+ resourcePresetId : 'custom' ,
273+ allocationPreset : 'custom' ,
274+ resource : {
275+ cpu,
276+ mem,
277+ shmem : shmemEntry ?. value ?? '0g' ,
278+ accelerator : acceleratorSlot ? Number ( acceleratorSlot . quantity ) : 0 ,
279+ acceleratorType : acceleratorSlot ?. slotName ,
280+ } ,
281+ enabledAutomaticShmem : ! shmemEntry ,
282+ } ;
283+ } ;
284+
176285/**
177286 * Default values applied when neither `deploymentFrgmt` nor
178287 * `preFilledModel` is provided. Also used as the merge base for
@@ -197,6 +306,9 @@ const DEFAULT_FORM_VALUES: DeploymentLauncherFormValue = {
197306 clusterSize : 1 ,
198307 resourceGroup : '' ,
199308 resourcePresetId : undefined ,
309+ allocationPreset : undefined ,
310+ resource : { cpu : 0 , mem : '0g' , shmem : '0g' , accelerator : 0 } ,
311+ enabledAutomaticShmem : true ,
200312 desiredReplicaCount : 1 ,
201313 environments : {
202314 environment : '' ,
@@ -227,6 +339,7 @@ const DeploymentLauncherPageContent: React.FC<
227339 form,
228340 deploymentFrgmt,
229341 runtimeVariants = [ ] ,
342+ resourcePresets,
230343 onValuesChange,
231344 onSubmit,
232345 isSubmitting,
@@ -237,6 +350,7 @@ const DeploymentLauncherPageContent: React.FC<
237350 const { t } = useTranslation ( ) ;
238351 const { token } = theme . useToken ( ) ;
239352 const screens = Grid . useBreakpoint ( ) ;
353+ const baiClient = useSuspendedBackendaiClient ( ) ;
240354 // Debounced CLI command parser — auto-fills port/health/mount from the
241355 // pasted command, mirroring ServiceLauncherPageContent behaviour.
242356 const { run : parseCommandWithDebounce } = useDebounceFn (
@@ -388,6 +502,16 @@ const DeploymentLauncherPageContent: React.FC<
388502 }
389503 resourceConfig {
390504 resourceGroupName
505+ resourceOpts {
506+ entries {
507+ name
508+ value
509+ }
510+ }
511+ }
512+ resourceSlots @since(version: "26.4.2") {
513+ slotName
514+ quantity
391515 }
392516 modelRuntimeConfig {
393517 runtimeVariantId
@@ -468,18 +592,26 @@ const DeploymentLauncherPageContent: React.FC<
468592 revision ?. clusterConfig ?. size ?? DEFAULT_FORM_VALUES . clusterSize ,
469593 resourceGroup : revision ?. resourceConfig ?. resourceGroupName ?? '' ,
470594 desiredReplicaCount : deployment . replicaState . desiredReplicaCount ,
471- // TODO(needs-backend): FR-2787 — resourcePresetId is not exposed in the
472- // ModelRevision schema, so the preset selector cannot be pre-populated in
473- // edit mode. Once the backend exposes resourcePresetName/id on the
474- // revision, add it here.
595+ ...resolveEditModeResourcePreset (
596+ revision ?. resourceSlots ?? null ,
597+ revision ?. resourceConfig ?. resourceOpts ?. entries ?? null ,
598+ resourcePresets ?? null ,
599+ ) ,
475600 } satisfies Partial < DeploymentLauncherFormValue > ) ;
476601 }
477602 return _ . merge ( { } , DEFAULT_FORM_VALUES , {
478603 ...( urlModel && { modelFolderId : urlModel } ) ,
479604 ...( urlResourceGroup && { resourceGroup : urlResourceGroup } ) ,
480605 ...( urlResourcePresetId && { resourcePresetId : urlResourcePresetId } ) ,
481606 } ) ;
482- } , [ mode , deployment , urlModel , urlResourceGroup , urlResourcePresetId ] ) ;
607+ } , [
608+ mode ,
609+ deployment ,
610+ urlModel ,
611+ urlResourceGroup ,
612+ urlResourcePresetId ,
613+ resourcePresets ,
614+ ] ) ;
483615
484616 // Apply initial values to the parent-owned form instance exactly once
485617 // per mount / deployment change. Using `useEffectEvent` keeps the
@@ -866,6 +998,13 @@ const DeploymentLauncherPageContent: React.FC<
866998 display : currentStepKey === 'resources' ? 'block' : 'none' ,
867999 } }
8681000 >
1001+ < Form . Item
1002+ name = "desiredReplicaCount"
1003+ label = { t ( 'deployment.DesiredReplicas' ) }
1004+ rules = { [ { required : true , type : 'number' , min : 1 } ] }
1005+ >
1006+ < InputNumber min = { 1 } style = { { width : '100%' } } />
1007+ </ Form . Item >
8691008 < Form . Item
8701009 name = "resourceGroup"
8711010 label = { t ( 'session.ResourceGroup' ) }
@@ -882,21 +1021,35 @@ const DeploymentLauncherPageContent: React.FC<
8821021 < Form . Item
8831022 name = "resourcePresetId"
8841023 label = { t ( 'resourcePreset.ResourcePresets' ) }
1024+ required
8851025 >
8861026 < ResourcePresetSelect
8871027 resourceGroup = { getFieldValue ( 'resourceGroup' ) }
8881028 autoSelectDefault
8891029 showSearch
1030+ showCustom = { baiClient . _config . allowCustomResourceAllocation }
1031+ showMinimumRequired = {
1032+ baiClient . _config . allowCustomResourceAllocation
1033+ }
1034+ onChange = { ( value ) => {
1035+ form . setFieldValue ( 'allocationPreset' , value ) ;
1036+ } }
8901037 />
8911038 </ Form . Item >
8921039 ) }
8931040 </ Form . Item >
894- < Form . Item
895- name = "desiredReplicaCount"
896- label = { t ( 'deployment.DesiredReplicas' ) }
897- rules = { [ { required : true , type : 'number' , min : 1 } ] }
898- >
899- < InputNumber min = { 1 } style = { { width : '100%' } } />
1041+ < Form . Item dependencies = { [ 'resourcePresetId' ] } noStyle >
1042+ { ( { getFieldValue } ) =>
1043+ getFieldValue ( 'resourcePresetId' ) === 'custom' && (
1044+ < Suspense >
1045+ < ResourceAllocationFormItems
1046+ enableResourcePresets = { false }
1047+ hideClusterFormItems
1048+ hideResourceGroup
1049+ />
1050+ </ Suspense >
1051+ )
1052+ }
9001053 </ Form . Item >
9011054 </ Card >
9021055
@@ -1301,7 +1454,22 @@ const DeploymentReviewSummary: React.FC<{
13011454 { values . resourceGroup || '-' }
13021455 </ Descriptions . Item >
13031456 < Descriptions . Item label = { t ( 'resourcePreset.ResourcePresets' ) } >
1304- { values . resourcePresetId ? (
1457+ { values . resourcePresetId === 'custom' ? (
1458+ < BAIFlex direction = "column" gap = "xs" align = "start" >
1459+ < Typography . Text >
1460+ { t ( 'session.launcher.CustomAllocation' ) }
1461+ </ Typography . Text >
1462+ { values . resource && (
1463+ < BAIFlex
1464+ direction = "row"
1465+ gap = "xxs"
1466+ style = { { flexWrap : 'wrap' } }
1467+ >
1468+ < ResourceNumbersOfSession resource = { values . resource } />
1469+ </ BAIFlex >
1470+ ) }
1471+ </ BAIFlex >
1472+ ) : values . resourcePresetId ? (
13051473 < Suspense fallback = { < > { values . resourcePresetId } </ > } >
13061474 < ErrorBoundaryWithNullFallback >
13071475 < ResourcePresetReviewDisplay
0 commit comments