@@ -107,6 +107,44 @@ def _dict_modules_body() -> dict:
107107 }
108108 }
109109
110+ @staticmethod
111+ def _realistic_list_modules_body () -> dict :
112+ """Body shape litellm actually produces — primary has a real template,
113+ fallback entries have ``template: []`` because litellm only converts
114+ the top-level ``messages`` for the primary module. The SDK is
115+ responsible for broadcasting the primary's template to every
116+ fallback entry; without that the orchestration server rejects with
117+ ``config.modules[N].prompt_templating.prompt.template should be
118+ non-empty`` (the exact failure that reached integration tests).
119+ """
120+ primary_template = [{"role" : "user" , "content" : "Reply with 'ok'." }]
121+ return {
122+ "config" : {
123+ "modules" : [
124+ {
125+ "prompt_templating" : {
126+ "prompt" : {"template" : primary_template },
127+ "model" : {
128+ "name" : "anthropic--claude-4.5-sonnet" ,
129+ "params" : {},
130+ "version" : "latest" ,
131+ },
132+ }
133+ },
134+ {
135+ "prompt_templating" : {
136+ "prompt" : {"template" : []},
137+ "model" : {
138+ "name" : "mistral-small" ,
139+ "params" : {},
140+ "version" : "latest" ,
141+ },
142+ }
143+ },
144+ ]
145+ }
146+ }
147+
110148 def test_fallback_injected_into_optional_params_before_super (self ):
111149 _install_fallback (FallbackConfig ([FallbackModel (model = "sap/mistral-small" )]))
112150 optional_params : dict = {}
@@ -217,6 +255,75 @@ def test_fallback_only_no_filtering_keys(self):
217255 for entry in body ["config" ]["modules" ]:
218256 assert "filtering" not in entry
219257
258+ def test_primary_template_broadcasts_to_fallback_entries (self ):
259+ # Regression: previously fallback entries went out with
260+ # ``prompt.template == []`` and the orchestration server rejected with
261+ # ``config.modules[1].prompt_templating.prompt.template should be
262+ # non-empty``. The patch now copies the primary's template across.
263+ _install_fallback (FallbackConfig ([FallbackModel (model = "sap/mistral-small" )]))
264+
265+ with patch (
266+ "sap_cloud_sdk.aicore.filtering.filters."
267+ "GenAIHubOrchestrationConfig.transform_request" ,
268+ return_value = self ._realistic_list_modules_body (),
269+ ):
270+ body = OrchestrationPatchConfig ().transform_request (
271+ model = "sap/anthropic--claude-4.5-sonnet" ,
272+ messages = [{"role" : "user" , "content" : "Reply with 'ok'." }],
273+ optional_params = {},
274+ litellm_params = {},
275+ headers = {},
276+ )
277+
278+ modules = body ["config" ]["modules" ]
279+ primary_template = modules [0 ]["prompt_templating" ]["prompt" ]["template" ]
280+ assert primary_template , "primary template should be non-empty"
281+ for entry in modules [1 :]:
282+ assert entry ["prompt_templating" ]["prompt" ]["template" ] == primary_template
283+
284+ def test_template_broadcast_noop_for_single_module_body (self ):
285+ # No fallback installed → litellm emits a single dict (not a list);
286+ # the broadcast must not touch it. (Also: nothing to broadcast to.)
287+ with patch (
288+ "sap_cloud_sdk.aicore.filtering.filters."
289+ "GenAIHubOrchestrationConfig.transform_request" ,
290+ return_value = self ._dict_modules_body (),
291+ ):
292+ body = OrchestrationPatchConfig ().transform_request (
293+ model = "sap/anthropic--claude-4.5-sonnet" ,
294+ messages = [],
295+ optional_params = {},
296+ litellm_params = {},
297+ headers = {},
298+ )
299+
300+ modules = body ["config" ]["modules" ]
301+ assert isinstance (modules , dict )
302+ # Untouched — same empty template the fixture started with.
303+ assert modules ["prompt_templating" ]["prompt" ]["template" ] == []
304+
305+ def test_template_broadcast_skipped_when_primary_template_empty (self ):
306+ # Defensive: if the primary itself somehow has no template, do not
307+ # propagate the empty value (no point) — leave fallback entries alone.
308+ _install_fallback (FallbackConfig ([FallbackModel (model = "sap/mistral-small" )]))
309+
310+ with patch (
311+ "sap_cloud_sdk.aicore.filtering.filters."
312+ "GenAIHubOrchestrationConfig.transform_request" ,
313+ return_value = self ._list_modules_body (),
314+ ):
315+ body = OrchestrationPatchConfig ().transform_request (
316+ model = "sap/anthropic--claude-4.5-sonnet" ,
317+ messages = [],
318+ optional_params = {},
319+ litellm_params = {},
320+ headers = {},
321+ )
322+
323+ modules = body ["config" ]["modules" ]
324+ for entry in modules :
325+ assert entry ["prompt_templating" ]["prompt" ]["template" ] == []
326+
220327
221328# ---------------------------------------------------------------------------
222329# transform_response — intermediate_failures attachment
0 commit comments