@@ -29,6 +29,21 @@ struct GatewayContext {
2929 endpoint : String ,
3030}
3131
32+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
33+ enum GpuCliRequest {
34+ DriverDefault ,
35+ Count ( u32 ) ,
36+ }
37+
38+ impl From < GpuCliRequest > for GpuResourceRequirements {
39+ fn from ( gpu : GpuCliRequest ) -> Self {
40+ match gpu {
41+ GpuCliRequest :: Count ( count) => Self { count : Some ( count) } ,
42+ GpuCliRequest :: DriverDefault => Self { count : None } ,
43+ }
44+ }
45+ }
46+
3247/// Resolve the gateway name to a [`GatewayContext`] with the gateway endpoint.
3348///
3449/// Resolution priority:
@@ -110,6 +125,21 @@ fn resolve_gateway(
110125 } )
111126}
112127
128+ fn parse_gpu_request ( value : & str ) -> std:: result:: Result < GpuCliRequest , String > {
129+ if value. is_empty ( ) {
130+ return Ok ( GpuCliRequest :: DriverDefault ) ;
131+ }
132+
133+ let count = value
134+ . parse :: < u32 > ( )
135+ . map_err ( |_| "GPU count must be a positive integer" . to_string ( ) ) ?;
136+ if count == 0 {
137+ return Err ( "GPU count must be greater than 0" . to_string ( ) ) ;
138+ }
139+
140+ Ok ( GpuCliRequest :: Count ( count) )
141+ }
142+
113143fn resolve_gateway_name ( gateway_flag : & Option < String > ) -> Option < String > {
114144 gateway_flag
115145 . clone ( )
@@ -1217,8 +1247,11 @@ enum SandboxCommands {
12171247 editor : Option < CliEditor > ,
12181248
12191249 /// Request GPU resources for the sandbox.
1220- #[ arg( long) ]
1221- gpu : bool ,
1250+ ///
1251+ /// Omit COUNT for the driver's default GPU selection, or pass COUNT
1252+ /// to request a specific number of GPUs.
1253+ #[ arg( long, num_args = 0 ..=1 , value_name = "COUNT" , default_missing_value = "" , value_parser = parse_gpu_request) ]
1254+ gpu : Option < GpuCliRequest > ,
12221255
12231256 /// CPU limit for the sandbox (for example: 500m, 1, 2.5).
12241257 #[ arg( long) ]
@@ -2626,7 +2659,7 @@ async fn main() -> Result<()> {
26262659 . map ( |s| openshell_core:: forward:: ForwardSpec :: parse ( & s) )
26272660 . transpose ( ) ?;
26282661 let keep = keep || !no_keep || editor. is_some ( ) || forward. is_some ( ) ;
2629- let gpu_requirements = gpu. then_some ( GpuResourceRequirements { } ) ;
2662+ let gpu_requirements: Option < GpuResourceRequirements > = gpu. map ( Into :: into ) ;
26302663
26312664 let ctx = resolve_gateway ( & cli. gateway , & cli. gateway_endpoint ) ?;
26322665 let endpoint = & ctx. endpoint ;
@@ -3636,6 +3669,27 @@ mod tests {
36363669 } ) ;
36373670 }
36383671
3672+ #[ test]
3673+ fn gpu_cli_request_option_maps_absent_gpu_to_no_requirements ( ) {
3674+ let gpu: Option < GpuResourceRequirements > = Option :: < GpuCliRequest > :: None . map ( Into :: into) ;
3675+
3676+ assert_eq ! ( gpu, None ) ;
3677+ }
3678+
3679+ #[ test]
3680+ fn gpu_cli_request_driver_default_converts_to_requirements ( ) {
3681+ let gpu = GpuResourceRequirements :: from ( GpuCliRequest :: DriverDefault ) ;
3682+
3683+ assert_eq ! ( gpu. count, None ) ;
3684+ }
3685+
3686+ #[ test]
3687+ fn gpu_cli_request_count_converts_to_requirements ( ) {
3688+ let gpu = GpuResourceRequirements :: from ( GpuCliRequest :: Count ( 2 ) ) ;
3689+
3690+ assert_eq ! ( gpu. count, Some ( 2 ) ) ;
3691+ }
3692+
36393693 #[ test]
36403694 fn apply_auth_uses_stored_token ( ) {
36413695 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
@@ -4507,7 +4561,23 @@ mod tests {
45074561 command : Some ( SandboxCommands :: Create { gpu, .. } ) ,
45084562 ..
45094563 } ) => {
4510- assert ! ( gpu) ;
4564+ assert_eq ! ( gpu, Some ( GpuCliRequest :: DriverDefault ) ) ;
4565+ }
4566+ other => panic ! ( "expected SandboxCommands::Create, got: {other:?}" ) ,
4567+ }
4568+ }
4569+
4570+ #[ test]
4571+ fn sandbox_create_gpu_count_parses_from_gpu_flag ( ) {
4572+ let cli = Cli :: try_parse_from ( [ "openshell" , "sandbox" , "create" , "--gpu" , "2" ] )
4573+ . expect ( "sandbox create --gpu 2 should parse" ) ;
4574+
4575+ match cli. command {
4576+ Some ( Commands :: Sandbox {
4577+ command : Some ( SandboxCommands :: Create { gpu, .. } ) ,
4578+ ..
4579+ } ) => {
4580+ assert_eq ! ( gpu, Some ( GpuCliRequest :: Count ( 2 ) ) ) ;
45114581 }
45124582 other => panic ! ( "expected SandboxCommands::Create, got: {other:?}" ) ,
45134583 }
@@ -4523,13 +4593,71 @@ mod tests {
45234593 command : Some ( SandboxCommands :: Create { gpu, command, .. } ) ,
45244594 ..
45254595 } ) => {
4526- assert ! ( gpu) ;
4596+ assert_eq ! ( gpu, Some ( GpuCliRequest :: DriverDefault ) ) ;
4597+ assert_eq ! ( command, vec![ "claude" . to_string( ) ] ) ;
4598+ }
4599+ other => panic ! ( "expected SandboxCommands::Create, got: {other:?}" ) ,
4600+ }
4601+ }
4602+
4603+ #[ test]
4604+ fn sandbox_create_gpu_count_allows_trailing_command ( ) {
4605+ let cli = Cli :: try_parse_from ( [
4606+ "openshell" ,
4607+ "sandbox" ,
4608+ "create" ,
4609+ "--gpu" ,
4610+ "2" ,
4611+ "--" ,
4612+ "claude" ,
4613+ ] )
4614+ . expect ( "sandbox create --gpu 2 -- claude should parse" ) ;
4615+
4616+ match cli. command {
4617+ Some ( Commands :: Sandbox {
4618+ command : Some ( SandboxCommands :: Create { gpu, command, .. } ) ,
4619+ ..
4620+ } ) => {
4621+ assert_eq ! ( gpu, Some ( GpuCliRequest :: Count ( 2 ) ) ) ;
45274622 assert_eq ! ( command, vec![ "claude" . to_string( ) ] ) ;
45284623 }
45294624 other => panic ! ( "expected SandboxCommands::Create, got: {other:?}" ) ,
45304625 }
45314626 }
45324627
4628+ #[ test]
4629+ fn sandbox_create_gpu_count_rejects_zero ( ) {
4630+ let result = Cli :: try_parse_from ( [ "openshell" , "sandbox" , "create" , "--gpu" , "0" ] ) ;
4631+
4632+ assert ! ( result. is_err( ) , "sandbox create --gpu 0 should be rejected" ) ;
4633+ }
4634+
4635+ #[ test]
4636+ fn sandbox_create_gpu_count_accepts_equals_syntax ( ) {
4637+ let cli = Cli :: try_parse_from ( [ "openshell" , "sandbox" , "create" , "--gpu=2" ] )
4638+ . expect ( "sandbox create --gpu=2 should parse" ) ;
4639+
4640+ match cli. command {
4641+ Some ( Commands :: Sandbox {
4642+ command : Some ( SandboxCommands :: Create { gpu, .. } ) ,
4643+ ..
4644+ } ) => {
4645+ assert_eq ! ( gpu, Some ( GpuCliRequest :: Count ( 2 ) ) ) ;
4646+ }
4647+ other => panic ! ( "expected SandboxCommands::Create, got: {other:?}" ) ,
4648+ }
4649+ }
4650+
4651+ #[ test]
4652+ fn sandbox_create_gpu_count_rejects_non_integer ( ) {
4653+ let result = Cli :: try_parse_from ( [ "openshell" , "sandbox" , "create" , "--gpu" , "many" ] ) ;
4654+
4655+ assert ! (
4656+ result. is_err( ) ,
4657+ "sandbox create --gpu many should be rejected"
4658+ ) ;
4659+ }
4660+
45334661 #[ test]
45344662 fn service_expose_accepts_positional_target_port_and_service ( ) {
45354663 let cli = Cli :: try_parse_from ( [
0 commit comments