1818
1919# qrmi_type values whose ResourceType name cannot be derived from them
2020_RESOURCE_TYPE_ALIASES = {
21+ "qiskit-runtime-service" : "IBMQiskitRuntimeService" ,
2122 "direct-access" : "IBMQuantumSystem" , # legacy name for ibm-quantum-system
2223 "ibm-direct-access" : "IBMQuantumSystem" ,
2324}
2425
26+ # the infix QRMI puts in its variable names for each IBM type, as in
27+ # <backend>_QRMI_IBM_QRS_IAM_APIKEY
28+ _ENV_INFIX = {
29+ "IBMQiskitRuntimeService" : "_QRMI_IBM_QRS" ,
30+ "IBMQuantumSystem" : "_QRMI_IBM_QS" ,
31+ }
32+
33+ # what the infix variables default to, or are derived from, when only the
34+ # IBM_CLOUD_TOKEN and IBM_CLOUD_CRN a workload Secret provides are present.
35+ # The sampler derives the same set for the scout.
36+ _IBM_DEFAULTS = {
37+ "_ENDPOINT" : "https://quantum.cloud.ibm.com/api/v1" ,
38+ "_IAM_ENDPOINT" : "https://iam.cloud.ibm.com" ,
39+ }
40+
2541# kept upper case when deriving a ResourceType name
2642_ACRONYMS = {"ibm" , "iqm" , "qrmi" , "id" , "api" , "hpc" }
2743
@@ -56,8 +72,9 @@ def _pascal(qrmi_type: str, acronyms=True) -> str:
5672
5773def _resource_type_enum (qrmi_type : str ):
5874 """The QRMI ResourceType for a qrmi_type string. An alias first, then the
59- name derived from the string. Falls back to the first member with a warning
60- rather than failing on a QRMI version we have not seen."""
75+ name derived from the string. Raises when nothing matches. A guessed type
76+ reads the wrong credentials and asks the wrong service about the task, so
77+ it is never better than stopping."""
6178 from qrmi import ResourceType
6279
6380 q = (qrmi_type or "" ).lower ()
@@ -70,12 +87,29 @@ def _resource_type_enum(qrmi_type: str):
7087 rt = getattr (ResourceType , name , None )
7188 if rt is not None :
7289 return rt
73- log (
74- f"[qrmi] WARNING: no ResourceType matched { qrmi_type !r} "
75- f"(tried { candidates } ); falling back to a default member"
90+ members = [k for k in vars (ResourceType ) if not k .startswith ("_" )]
91+ raise ValueError (
92+ f"no QRMI ResourceType matches { qrmi_type !r} , tried { candidates } , "
93+ f"QRMI has { members } "
7694 )
77- members = [v for k , v in vars (ResourceType ).items () if not k .startswith ("_" )]
78- return members [0 ] if members else None
95+
96+
97+ def _derive_ibm_env (backend : str , rtype ) -> None :
98+ """Fill in <backend><infix>_* from IBM_CLOUD_TOKEN and IBM_CLOUD_CRN,
99+ never overwriting a variable that is already set. The webhook copies the
100+ workload's Secret env onto the sidecar, and that Secret holds the plain
101+ token and CRN, not QRMI's names."""
102+ name = rtype if isinstance (rtype , str ) else getattr (rtype , "name" , str (rtype ))
103+ infix = _ENV_INFIX .get (name .rsplit ("." , 1 )[- 1 ])
104+ if not infix :
105+ return
106+ values = dict (_IBM_DEFAULTS )
107+ if os .environ .get ("IBM_CLOUD_TOKEN" ):
108+ values ["_IAM_APIKEY" ] = os .environ ["IBM_CLOUD_TOKEN" ]
109+ if os .environ .get ("IBM_CLOUD_CRN" ):
110+ values ["_SERVICE_CRN" ] = os .environ ["IBM_CLOUD_CRN" ]
111+ for suffix , value in values .items ():
112+ os .environ .setdefault (backend + infix + suffix , value )
79113
80114
81115def _status_name (status ) -> str :
@@ -235,35 +269,37 @@ def _qrmi_resource(self, task: QRMITask):
235269 import qrmi
236270
237271 rt = _resource_type_enum (task .resource_type )
272+ _derive_ibm_env (
273+ task .backend ,
274+ _RESOURCE_TYPE_ALIASES .get ((task .resource_type or "" ).lower (), rt ),
275+ )
238276 task ._resource = qrmi .QuantumResource (task .backend , rt )
239277 return task ._resource
240278
241- def _status (self , task : QRMITask ) -> "str | None" :
242- try :
243- status = self ._qrmi_resource (task ).task_status (task .job_id )
244- return _status_name (status )
245- except Exception as e :
246- log (f"[qrmi] task_status error (will retry): { e } " )
247- return None
279+ def _status (self , task : QRMITask ) -> str :
280+ # Errors are raised, not swallowed. The sidecar's poll loop counts
281+ # them and gives up after FLUENCE_POLL_MAX_ERRORS, which tells the
282+ # scout to release its session. Swallowed, a status call that can
283+ # never work polled forever while the scout held the session.
284+ return _status_name (self ._qrmi_resource (task ).task_status (task .job_id ))
248285
249286 def is_ready_to_ungate (self , task : QRMITask , position = None ) -> bool :
250287 # QRMI reports no queue position, so position is accepted to match the
251288 # interface and the sidecar's call, and ignored. Ready means the task
252289 # is running on the device or has completed. Failed and cancelled are
253290 # not ready, task_failed reports them.
254- name = self ._status (task )
255- return name in _READY_STATES if name else False
291+ return self ._status (task ) in _READY_STATES
256292
257293 def task_failed (self , task : QRMITask ) -> bool :
258- name = self ._status (task )
259- return name in _FAILED_STATES if name else False
294+ return self ._status (task ) in _FAILED_STATES
260295
261296 def queue_position (self , task : QRMITask ):
262297 # QRMI has no queue position, so report 1 once the task runs and None before
263- name = self ._status (task )
264- if name and name in _READY_STATES :
265- return 1
266- return None
298+ try :
299+ name = self ._status (task )
300+ except Exception :
301+ return None
302+ return 1 if name in _READY_STATES else None
267303
268304 def job_id (self , task : QRMITask ) -> str :
269305 return task .job_id
0 commit comments