Setting the file. One moment.
OpenAI Model Recommendation · Agent Advisor · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page Add Capabilities
81
Creating Amazon Aurora Db Cluster With Instances
104
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
def _api_continuity
— line 210
This file
Number 23.59
Position 59 of 81
Type Python
Size 41 KB
Lines 994 scripts/ openai_model_recommendation.py
Python · 994 lines · 41 KB
=
(
16 re.compile( r " (?i) (^ | [ ^a-z0-9 ]) gpt [ -_ ] ? 5 ( \. \d + ) ? ([ ^0-9 ] | $) " ),
17 re.compile( r " (?i) (^ | [ ^a-z0-9 ]) o [ 134 ]( -mini | -pro | -preview ) ? ([ ^a-z0-9 ] | $) " ),
18 )
19 # Legacy generation: gpt-4*, gpt-3.5 / gpt-35.
20 _LEGACY_PATTERNS = (
21 re.compile( r " (?i) gpt [ -_ ] ? 4o" ),
22 re.compile( r " (?i) gpt [ -_ ] ? 4 (?! \d ) " ),
23 re.compile( r " (?i) gpt [ -_ ] ? 4 [ -_ ] ? 32k" ),
24 re.compile( r " (?i) gpt [ -_ ] ? 3 \. ? 5" ),
25 re.compile( r " (?i) gpt [ -_ ] ? 35" ),
26 )
27 # GPT-5.2+ re-accepts sampling on Responses; earlier reasoning models differ.
28 _GPT5_MINOR = re.compile( r " (?i) gpt [ -_ ] ? 5 \. (\d + ) " )
29
30
31 def detect_family (model_id):
32 """Return 'reasoning', 'legacy', or 'unknown'.
33
34 Fails to 'unknown' for opaque deployment names (e.g. 'prod-default') so the
35 engine never infers a model family from a deployment alias.
36 """
37 name = (model_id or "" ).strip()
38 if not name:
39 return "unknown"
40 for pat in _REASONING_PATTERNS :
41 if pat.search(name):
42 return "reasoning"
43 for pat in _LEGACY_PATTERNS :
44 if pat.search(name):
45 return "legacy"
46 return "unknown"
47
48
49 def _gpt5_minor (model_id):
50 match = _GPT5_MINOR .search(model_id or "" )
51 return int (match.group( 1 )) if match else None
52
53
54 def _primary_source_id (source):
55 ids = source.get( "model_ids" ) or []
56 return ids[ 0 ] if ids else ""
57
58
59 def _source_family (source):
60 """Family for the workload: reasoning if ANY id is reasoning, else legacy if
61 any is legacy, else unknown (opaque deployment names stay unknown)."""
62 families = [detect_family(mid) for mid in source.get( "model_ids" ) or [ "" ]]
63 if "reasoning" in families:
64 return "reasoning"
65 if "legacy" in families:
66 return "legacy"
67 return "unknown"
68
69
70 def _finding (code, tag, message, remediation):
71 return { "code" : code, "tag" : tag, "message" : message, "remediation" : remediation}
72
73
74 def _delta (code, category, description):
75 return { "code" : code, "category" : category, "description" : description}
76
77
78 # --- Feature vocabulary (provider-scoped; codes match scanner feature-catalog) ---
79 _HOSTED_TOOL_IMPACTS = {
80 "web_search" : (
81 "Hosted web search does not execute live on Bedrock Mantle (verified passthrough: "
82 "no query issued, no citations)." ,
83 "Implement a client-side tool loop, or expose a server-side tool (MCP/Lambda) through "
84 "Bedrock AgentCore Gateway; use Knowledge Bases for document retrieval." ,
85 ),
86 "file_search_retrieval" : (
87 "Hosted file search / retrieval is not a drop-in on Bedrock." ,
88 "Re-platform retrieval onto Bedrock Knowledge Bases or application-owned retrieval." ,
89 ),
90 "files_api" : (
91 "The OpenAI Files API and vector stores do not port directly." ,
92 "Redesign data ingestion and retrieval; use Knowledge Bases or application storage." ,
93 ),
94 "vector_stores" : (
95 "OpenAI vector stores are not a Bedrock primitive." ,
96 "Move vectors to a Bedrock-supported store and retrieve from the application." ,
97 ),
98 "assistants_threads" : (
99 "Assistants/Threads hosted state is not available on Bedrock." ,
100 "Redesign agent state, tools, and memory (e.g. Bedrock AgentCore)." ,
101 ),
102 "audio_modality" : (
103 "Audio is not a single-call text-model capability on Bedrock." ,
104 "Split into a verified STT/TTS service path (Amazon Transcribe / Polly)." ,
105 ),
106 "embeddings" : (
107 "Embeddings require a separate Bedrock embedding model, not the text model." ,
108 "Select a sourced Bedrock embedding model (e.g. Titan/Cohere) as a separate workload." ,
109 ),
110 "images" : (
111 "Image generation/editing is a separate capability, not a text-model swap." ,
112 "Select a sourced Bedrock image model/service as a separate workload." ,
113 ),
114 }
115
116
117 # Converse tier map: a governance workload keeps its capability tier when moving
118 # from the (Mantle-only) GPT-5.x family to Claude on runtime Converse.
119 # GPT-5.6 Sol (frontier) -> Claude Opus 4.8
120 # GPT-5.6 Terra / 5.5 / 5.4 -> Claude Sonnet 5 (also the default tier)
121 # GPT-5.6 Luna (fast/low-cost)-> Claude Haiku 4.5
122 _CONVERSE_TIER_DEFAULT = "anthropic_claude_sonnet_5"
123 _CONVERSE_TIER_ORDER = (
124 "anthropic_claude_sonnet_5" ,
125 "anthropic_claude_opus_4_8" ,
126 "anthropic_claude_haiku_4_5" ,
127 )
128
129
130 def _converse_tier_for_source (source):
131 sid = _primary_source_id(source).lower()
132 if "5.6-sol" in sid:
133 return "anthropic_claude_opus_4_8"
134 if "5.6-luna" in sid:
135 return "anthropic_claude_haiku_4_5"
136 # 5.6-terra, 5.5, 5.4, legacy, and unknown sources all map to the balanced tier.
137 return _CONVERSE_TIER_DEFAULT
138
139
140 def _same_model_runtime_key (source):
141 """GPT-5.6 sources have a SAME-MODEL runtime_converse path via CRIS (verified
142 2026-08-21) — governance requirements no longer force a family switch for
143 them. Returns the catalog key, or None for every other source (5.5/5.4 and
144 legacy have no runtime path, so their Converse candidates stay Claude)."""
145 sid = _primary_source_id(source).lower()
146 for marker, key in (( "5.6-sol" , "openai_gpt_5_6_sol" ),
147 ( "5.6-terra" , "openai_gpt_5_6_terra" ),
148 ( "5.6-luna" , "openai_gpt_5_6_luna" )):
149 if marker in sid:
150 return key
151 return None
152
153
154 def _converse_candidate_order (source):
155 tier = _converse_tier_for_source(source)
156 order = [tier] + [k for k in _CONVERSE_TIER_ORDER if k != tier]
157 same = _same_model_runtime_key(source)
158 if same:
159 # Same model outranks any cross-family tier: keeping the model removes
160 # prompt-adaptation and behavior-delta risk entirely, and on Global CRIS
161 # it is also the cost-parity option.
162 order.insert( 0 , same)
163 return order
164
165
166 def _catalog_model_for_path (catalog, path, detected_features = None , requirements = None ,
167 candidate_order = None ):
168 """Pick the first available catalog model for `path` whose capability evidence
169 covers the required/detected text features (evidence-driven fallback: a newer
170 model without feature-level evidence is skipped in favor of an older probed
171 one). When no candidate covers them, return the first available model together
172 with its unmet-capability list so the caller can fail closed. Deterministic:
173 iterates `candidate_order` when given (e.g. the Converse tier map), else
174 catalog key order."""
175 first = None
176 first_unmet = None
177 keys = candidate_order if candidate_order is not None else list (catalog[ "models" ])
178 for model_key in keys:
179 model = catalog[ "models" ].get(model_key)
180 if model is None :
181 continue
182 path_config = model[ "paths" ].get(path, {})
183 if path_config.get( "available" ) is not True :
184 continue
185 unmet = _unsupported_required_capabilities(
186 model, detected_features, requirements or {}
187 )
188 if first is None :
189 first = (model_key, model, path_config)
190 first_unmet = unmet
191 if not unmet:
192 return (model_key, model, path_config), []
193 return first, first_unmet
194
195
196 def _resolve_invocation_model_id (model_id, requires_cris, requirements):
197 if not requires_cris:
198 return model_id
199 explicit = requirements.get( "inference_profile_id" )
200 if explicit:
201 return explicit
202 residency = requirements.get( "data_residency" , "unknown" )
203 if residency == "global_allowed" :
204 return f "global. { model_id } "
205 if residency == "geo_required" and requirements.get( "cris_geography" ):
206 return f " { requirements[ 'cris_geography' ] } . { model_id } "
207 return None
208
209
210 def _api_continuity (requirements):
211 """Provider-neutral continuity: required | preferred | not_required | unknown.
212
213 Accepts the explicit `api_continuity` field; falls back to the OpenAI-specific
214 `preserve_openai_api` boolean. Never reuses Anthropic's preserve_messages_api.
215 """
216 explicit = requirements.get( "api_continuity" )
217 if explicit:
218 return explicit
219 if requirements.get( "preserve_openai_api" ) is True :
220 return "required"
221 if requirements.get( "preserve_openai_api" ) is False :
222 return "not_required"
223 return "unknown"
224
225
226 def _runtime_required (requirements):
227 return bool (requirements.get( "governance" )) or requirements.get(
228 "multi_model_converse" , False
229 )
230
231
232 def _model_identity (model_key, model, path_config):
233 return {
234 "model_key" : model_key,
235 "display_name" : model[ "display_name" ],
236 "family" : model[ "family" ],
237 "version" : model[ "version" ],
238 "context_window" : model[ "context_window" ],
239 "output_token_ceiling" : model[ "output_token_ceiling" ],
240 "path_model_id" : path_config[ "model_id" ],
241 "requires_cris" : path_config[ "requires_cris" ],
242 }
243
244
245 def _numeric_requirement_conflict (model, requirements):
246 """Fail closed: an unknown catalog limit cannot satisfy a hard numeric need."""
247 conflicts = []
248 for req_key, cat_key, label in (
249 ( "min_context_tokens" , "context_window" , "context window" ),
250 ( "expected_output_tokens" , "output_token_ceiling" , "output ceiling" ),
251 ):
252 need = requirements.get(req_key)
253 if not need:
254 continue
255 have = model.get(cat_key)
256 if have == "unknown" or not isinstance (have, int ):
257 conflicts.append(
258 f " { label } for { model[ 'display_name' ] } is unknown in the catalog; "
259 f "a required { need } -token need cannot be confirmed"
260 )
261 elif have < need:
262 conflicts.append(
263 f " { label } for { model[ 'display_name' ] } ( { have } ) is below the required { need } "
264 )
265 return conflicts
266
267
268 # Extracts a numeric OpenAI version (major or major.minor) from an id/version.
269 _OPENAI_VERSION = re.compile( r " (?i) gpt [ -_ ] ? (\d + (?: \. \d + ) ? ) " )
270
271
272 def _openai_version (text):
273 match = _OPENAI_VERSION .search(text or "" )
274 return match.group( 1 ) if match else None
275
276
277 def _version_tuple (value):
278 if not value:
279 return None
280 parts = value.split( "." )
281 try :
282 return ( int (parts[ 0 ]), int (parts[ 1 ]) if len (parts) > 1 else 0 )
283 except ( ValueError , IndexError ):
284 return None
285
286
287 def _target_accepts_sampling (target_model):
288 """GPT-5.2+ re-accepts sampling params on Responses (verified for GPT-5.4).
289
290 Returns True/False for an OpenAI target of known version, None when the
291 target is not an OpenAI model or its version can't be parsed.
292 """
293 if target_model is None or target_model.get( "generation" ) == "bedrock_native" :
294 return None
295 version = _version_tuple(target_model.get( "version" ))
296 if version is None :
297 return None
298 return version >= ( 5 , 2 )
299
300
301 def _source_analysis (source, target_model = None ):
302 """Source-side facts plus, when a target has been selected, target-derived
303 version/generation change flags. Target fields stay null before selection."""
304 source_id = _primary_source_id(source)
305 family = _source_family(source)
306 surface = source.get( "api_surface" )
307 if target_model is None :
308 target_version = None
309 version_changed = None
310 generation_changes = None
311 else :
312 target_version = target_model.get( "version" )
313 target_generation = target_model.get( "generation" )
314 if target_generation == "bedrock_native" :
315 # OpenAI source -> a Bedrock-native model is always a model change.
316 version_changed = True
317 generation_changes = True
318 else :
319 source_version = _openai_version(source_id)
320 if source_version and target_version:
321 version_changed = source_version != target_version
322 else :
323 # o-series / opaque source vs a specific GPT-5.x target: a move,
324 # but we cannot pin the exact source version.
325 version_changed = family in { "legacy" , "reasoning" } or None
326 generation_changes = family == "legacy" and target_generation == "reasoning"
327 return {
328 "detected_version" : source_id or None ,
329 "target_version" : target_version,
330 "version_changed" : version_changed,
331 "source_family" : family,
332 "source_api_surface" : surface,
333 "model_generation_changes" : generation_changes,
334 }
335
336
337 def _reasoning_findings (source, requirements, target_model, path):
338 """Version- and surface-specific parameter findings, derived from the SELECTED
339 target model and path — never from the source id, and never Anthropic's blanket
340 sampling-removal rule."""
341 blocks, tuning, deltas = [], [], []
342 src_family = _source_family(source)
343 src_id = _primary_source_id(source)
344 target_generation = target_model.get( "generation" ) if target_model else None
345 target_name = target_model.get( "display_name" ) if target_model else "the target model"
346
347 if target_generation == "reasoning" :
348 tuning.append(
349 _finding(
350 "reasoning_token_headroom" ,
351 "[TUNE]" ,
352 "Reasoning models spend hidden thinking tokens against the output budget; "
353 "too-small caps return status:incomplete." ,
354 "Size the output cap for reasoning output. The reference uses ~2.5x the legacy "
355 "budget plus a 4096 floor as a STARTING heuristic, not a guaranteed value; "
356 "tune from measured output distributions." ,
357 )
358 )
359 if src_family == "legacy" and target_generation == "reasoning" :
360 deltas.append(
361 _delta(
362 "model_generation_hop" ,
363 "version" ,
364 f "Source ' { src_id } ' is a GPT-4.x/legacy model migrating to a reasoning target "
365 f "( { target_name } ): reasoning controls, token budget, and prompt behavior change." ,
366 )
367 )
368
369 # Sampling acceptance is a property of the TARGET model and the selected path.
370 if path == "runtime_converse" :
371 tuning.append(
372 _finding(
373 "sampling_via_converse" ,
374 "[TUNE]" ,
375 f "On runtime Converse, sampling is set through inferenceConfig "
376 f "(temperature/topP) on { target_name } , not OpenAI SDK kwargs." ,
377 "Move temperature/top_p into Converse inferenceConfig; penalties/logprobs/stop "
378 "are not universally supported and must be verified." ,
379 )
380 )
381 else :
382 accepts = _target_accepts_sampling(target_model)
383 if accepts is True :
384 tuning.append(
385 _finding(
386 "sampling_params_accepted" ,
387 "[TUNE]" ,
388 f " { target_name } (GPT-5.2+) accepts temperature and top_p on the Responses "
389 "surface; do not strip them." ,
390 "Keep temperature/top_p and calibrate against a golden set." ,
391 )
392 )
393 tuning.append(
394 _finding(
395 "sampling_penalties_rejected" ,
396 "[TUNE]" ,
397 "frequency_penalty and presence_penalty were rejected as SDK kwargs on the "
398 "verified GPT-5.4 Responses probe." ,
399 "Remove frequency_penalty/presence_penalty. logprobs, logit_bias, and stop "
400 "were NOT part of that probe and are version/endpoint-specific — verify each "
401 "for the exact target model rather than assuming acceptance or rejection." ,
402 )
403 )
404 elif accepts is False :
405 tuning.append(
406 _finding(
407 "sampling_params_version_specific" ,
408 "[TUNE]" ,
409 f " { target_name } predates GPT-5.2 and may reject sampling parameters on "
410 "Responses; behavior is version-specific." ,
411 "Verify sampling acceptance for the exact target model and surface." ,
412 )
413 )
414 return blocks, tuning, deltas
415
416
417 def _chat_to_responses_deltas ():
418 return [
419 _delta(
420 "chat_to_responses_request" ,
421 "path" ,
422 "Chat Completions -> Responses: messages->input, system message->instructions, "
423 "max_tokens->max_output_tokens." ,
424 ),
425 _delta(
426 "chat_to_responses_response" ,
427 "path" ,
428 "Read model output from output_text instead of the chat message content." ,
429 ),
430 _delta(
431 "chat_to_responses_tools" ,
432 "feature" ,
433 "Tool results move to function_call_output on the Responses surface." ,
434 ),
435 _delta(
436 "chat_to_responses_state" ,
437 "path" ,
438 "Replayed multi-turn context uses previous_response_id where appropriate, or "
439 "application-owned state." ,
440 ),
441 ]
442
443
444 def _feature_findings (detected_features, requirements, path = None ):
445 """Structured output, n, tools, state, and hosted-tool/modality impacts.
446
447 Findings are path-aware: Mantle Responses keeps verified capabilities
448 (responses.parse typed output, hosted store=True continuation) that a
449 Bedrock-native Converse target does not."""
450 detected = set (detected_features or [])
451 detected.update(requirements.get( "critical_features" ) or [])
452 is_mantle = path == "mantle_openai_responses"
453 blocks, tuning, deltas, impacts = [], [], [], []
454
455 if "structured_output_json" in detected:
456 if is_mantle:
457 # Verified on Mantle: responses.parse(text_format=Model) returns a typed object.
458 deltas.append(
459 _delta(
460 "structured_output_text_format" ,
461 "feature" ,
462 "On Mantle Responses, keep typed structured output: "
463 "client.responses.parse(..., text_format=Model) is verified, or use raw "
464 "text.format json_schema." ,
465 )
466 )
467 else :
468 deltas.append(
469 _delta(
470 "structured_output_text_format" ,
471 "feature" ,
472 "For a Bedrock-native Converse target, enforce the schema via tool-use or "
473 "prompt + validation; the OpenAI parse() helper is not the selected path." ,
474 )
475 )
476 if requirements.get( "uses_n" ) or "multiple_candidates_n" in detected:
477 deltas.append(
478 _delta(
479 "responses_no_n" ,
480 "feature" ,
481 "The Responses API does not support n; request multiple candidates with "
482 "repeated calls." ,
483 )
484 )
485 if "tool_or_function_calling" in detected:
486 deltas.append(
487 _delta(
488 "tool_result_shape" ,
489 "feature" ,
490 "Tool calls continue via function_call_output on Responses; the application "
491 "executes the tool." ,
492 )
493 )
494 if "conversation_state" in detected or requirements.get( "uses_hosted_state" ):
495 if is_mantle:
496 deltas.append(
497 _delta(
498 "conversation_state_ownership" ,
499 "path" ,
500 "Two verified modes on Mantle Responses: server-hosted continuation "
501 "(store=True + previous_response_id) or manual replay (store=False with "
502 "application-owned history). Retention/compliance/availability are "
503 "verification questions, not a lost capability." ,
504 )
505 )
506 else :
507 deltas.append(
508 _delta(
509 "conversation_state_ownership" ,
510 "path" ,
511 "A Bedrock-native Converse target has no hosted Responses state; carry "
512 "conversation history in an application-owned store." ,
513 )
514 )
515
516 impacted = sorted (detected.intersection( _HOSTED_TOOL_IMPACTS ))
517 for feature in impacted:
518 impact, remediation = _HOSTED_TOOL_IMPACTS [feature]
519 impacts.append(
520 { "feature" : feature, "impact" : impact, "recommendation" : remediation}
521 )
522 return blocks, tuning, deltas, impacts
523
524
525 # Text-model features that map to portable request-shape changes on any path.
526 _PORTABLE_FEATURES = { "max_tokens" , "sampling_params" , "sampling_params_accepted" }
527
528
529 # Separate-capability modalities that need their OWN target, not the text model.
530 # service = the Bedrock service family the reference names; candidate stays null
531 # (unresolved) because the reference does not pin a specific verified model ID.
532 _SEPARATE_MODALITY_TARGETS = {
533 "embeddings" : (
534 "Amazon Bedrock embedding model (e.g. Titan/Cohere families)" ,
535 "05_migrating_for_real.ipynb maps embeddings to a separate Bedrock embedding model" ,
536 ),
537 "images" : (
538 "Amazon Bedrock image model/service (e.g. Titan Image / Nova / Stability)" ,
539 "03_reasoning_api_migration.ipynb maps images to a separate Bedrock image model" ,
540 ),
541 "audio_modality" : (
542 "Amazon Transcribe / Polly (STT/TTS)" ,
543 "feature-catalog + notebooks map audio to Transcribe/Polly, not a text model" ,
544 ),
545 }
546
547
548 def _additional_targets (detected_features, requirements):
549 """Emit an explicit per-modality target contract; candidate stays null
550 (unresolved) since the reference names a service family, not a verified ID (G06)."""
551 detected = set (detected_features or [])
552 detected.update(requirements.get( "critical_features" ) or [])
553 targets = []
554 for capability in sorted (detected.intersection( _SEPARATE_MODALITY_TARGETS )):
555 service, evidence = _SEPARATE_MODALITY_TARGETS [capability]
556 targets.append(
557 {
558 "capability" : capability,
559 "status" : "unresolved" ,
560 "candidate" : None ,
561 "service" : service,
562 "evidence" : evidence
563 + "; select and verify a specific model/service in the target account" ,
564 }
565 )
566 return targets
567
568
569 def _feature_assessment (workload):
570 """Merge detected_features and feature_status; feature_status is authoritative
571 for detected/absent/unknown, so an explicit 'unknown' is preserved (G08)."""
572 assessment = {}
573 for feature in workload.get( "detected_features" ) or []:
574 assessment[feature] = "detected"
575 for feature, status in (workload.get( "feature_status" ) or {}).items():
576 assessment[feature] = status
577 return dict ( sorted (assessment.items()))
578
579
580 def _unknown_required_features (feature_assessment, requirements):
581 """Required features whose status is explicitly unknown (G08)."""
582 required = set (requirements.get( "critical_features" ) or [])
583 return sorted (f for f in required if feature_assessment.get(f) == "unknown" )
584
585
586 def _unsupported_required_capabilities (target_model, detected_features, requirements):
587 """Required/detected text-model features NOT evidenced by the target's catalog
588 capabilities. Hosted tools/modalities are handled as architecture impacts, not here."""
589 if target_model is None :
590 return []
591 catalog_caps = set (target_model.get( "capabilities" ) or [])
592 needed = set (requirements.get( "critical_features" ) or [])
593 needed.update(detected_features or [])
594 checkable = needed.intersection(
595 {
596 "tool_or_function_calling" ,
597 "structured_output_json" ,
598 "streaming" ,
599 "image_input_vision" ,
600 "reasoning" ,
601 }
602 )
603 return sorted (checkable - catalog_caps)
604
605
606 def _compatibility (detected_features, requirements, path, target_model = None ):
607 """native = only features the SELECTED target's catalog evidences; features
608 needed but not evidenced go to unsupported (never derive native from the name)."""
609 detected = set (detected_features or [])
610 detected.update(requirements.get( "critical_features" ) or [])
611 text_features = detected.intersection(
612 { "tool_or_function_calling" , "structured_output_json" , "streaming" , "image_input_vision" , "reasoning" }
613 )
614 catalog_caps = set ((target_model or {}).get( "capabilities" ) or [])
615 native = sorted (text_features.intersection(catalog_caps)) if target_model else []
616 unsupported = sorted (text_features - catalog_caps) if target_model else []
617 rearchitecture = sorted (detected.intersection( _HOSTED_TOOL_IMPACTS ))
618 portable = sorted (detected.intersection( _PORTABLE_FEATURES ))
619 return {
620 "native" : native,
621 "portable" : portable,
622 "rearchitecture" : rearchitecture,
623 "unsupported" : unsupported,
624 }
625
626
627 def _evaluation (detected_features, requirements):
628 detected = set (detected_features or [])
629 detected.update(requirements.get( "critical_features" ) or [])
630 trajectory = bool (
631 detected.intersection(
632 { "tool_or_function_calling" , "assistants_threads" , "web_search" , "file_search_retrieval" }
633 )
634 or "agentic" in (requirements.get( "critical_features" ) or [])
635 )
636 gates = [
637 "Build a deterministic discovery inventory of clients, APIs, model IDs, and features." ,
638 "Compare viable path/model candidates on a representative golden set." ,
639 "Size quota for peak traffic and reasoning-output tokens." ,
640 "Fail on refusal mishandling, truncation, or invalid structured output." ,
641 ]
642 if trajectory:
643 gates.extend(
644 [
645 "Verify correct tool selection, valid tool arguments, and tool-result continuation." ,
646 "Verify loop termination and structured-output validity." ,
647 ]
648 )
649 return { "mode" : "trajectory" if trajectory else "prompt" , "gates" : gates}
650
651
652 def _verification (region, catalog, path, requires_cris, invocation_model_id, selected):
653 if not selected:
654 return {
655 "region" : region,
656 "catalog_verified_at" : catalog[ "verified_at" ],
657 "verified_at" : None ,
658 "probe_status" : "not_applicable" ,
659 "availability_claim" : "not_selected" ,
660 "invocation_model_id" : None ,
661 "required_checks" : [
662 "Resolve the model/path decision before running an availability probe."
663 ],
664 }
665 checks = [
666 "Probe the selected model through the selected API path in the target account and region." ,
667 "Verify path-specific IAM, model access, and quota before code rewrite or POC generation." ,
668 ]
669 if requires_cris:
670 checks.insert(
671 1 , "Resolve and probe a Global or geography-scoped CRIS inference profile."
672 )
673 return {
674 "region" : region,
675 "catalog_verified_at" : catalog[ "verified_at" ],
676 "verified_at" : None ,
677 "probe_status" : "not_run" ,
678 "availability_claim" : "provisional" ,
679 "invocation_model_id" : invocation_model_id,
680 "required_checks" : checks,
681 }
682
683
684 def _decision_options (catalog, workload, region):
685 """Two-sided option set: Mantle Responses continuity vs runtime Converse governance."""
686 options = []
687 detected = workload.get( "detected_features" ) or []
688 mantle, _ = _catalog_model_for_path(
689 catalog, "mantle_openai_responses" , detected, workload[ "requirements" ]
690 )
691 if mantle:
692 model_key, model, path_config = mantle
693 options.append(
694 {
695 "model_key" : model_key,
696 "model" : path_config[ "model_id" ],
697 "api_path" : "mantle_openai_responses" ,
698 "invocation_model_id" : _resolve_invocation_model_id(
699 path_config[ "model_id" ], path_config[ "requires_cris" ], workload[ "requirements" ]
700 ),
701 "requires_cris" : path_config[ "requires_cris" ],
702 "reason" : "Preserves the OpenAI SDK and Responses surface; gives up runtime-only "
703 "Bedrock governance." ,
704 }
705 )
706 runtime, _ = _catalog_model_for_path(
707 catalog, "runtime_converse" , detected, workload[ "requirements" ],
708 candidate_order = _converse_candidate_order(workload[ "source" ]),
709 )
710 if runtime:
711 model_key, model, path_config = runtime
712 options.append(
713 {
714 "model_key" : model_key,
715 "model" : path_config[ "model_id" ],
716 "api_path" : "runtime_converse" ,
717 "invocation_model_id" : _resolve_invocation_model_id(
718 path_config[ "model_id" ], path_config[ "requires_cris" ], workload[ "requirements" ]
719 ),
720 "requires_cris" : path_config[ "requires_cris" ],
721 "reason" : (
722 "SAME-MODEL governance path: this GPT-5.6 target runs on bedrock-runtime via a "
723 "CRIS id — Guardrails (Converse API only), invocation logging, and cost parity "
724 "on Global CRIS (1.10x on Geo/In-Region pricing), without a model change."
725 if model_key == _same_model_runtime_key(workload[ "source" ])
726 else "Uses Bedrock-native Converse request/response shapes and a Bedrock-native "
727 "model; requires rewriting the OpenAI integration."
728 ),
729 }
730 )
731 return options
732
733
734 def _base (workload, decision_status):
735 return {
736 "workload_id" : workload[ "workload_id" ],
737 "provider_module" : "openai" ,
738 "decision_status" : decision_status,
739 "source" : workload[ "source" ],
740 }
741
742
743 def recommend_openai_workload (workload, region, catalog):
744 """Return a recommendation dict in the shared downstream contract shape."""
745 source = workload[ "source" ]
746 requirements = workload[ "requirements" ]
747 surface = source.get( "api_surface" )
748 continuity = _api_continuity(requirements)
749 runtime_required = _runtime_required(requirements)
750 continuity_required = continuity == "required"
751
752 feature_assessment = _feature_assessment(workload)
753 # feature_status is authoritative: a feature is "effectively detected" only if
754 # the merged assessment marks it detected. Explicit absent/unknown are excluded,
755 # so downstream compatibility/deltas/targets never contradict the assessment.
756 detected = [f for f, status in feature_assessment.items() if status == "detected" ]
757
758 unknown_required = _unknown_required_features(feature_assessment, requirements)
759
760 def _unresolved (reason_head, reasons, block, path_for_compat):
761 # Shared decision_required assembly (no target selected).
762 rec = _base(workload, "decision_required" )
763 rec.update(
764 {
765 "source_analysis" : _source_analysis(source, None ),
766 "feature_assessment" : feature_assessment,
767 "primary_model" : None ,
768 "model_identity" : None ,
769 "api_path" : None ,
770 "invocation_model_id" : None ,
771 "decision_options" : _decision_options(catalog, workload, region),
772 "alternatives" : [],
773 "rationale" : [reason_head] + reasons,
774 "blocks" : [block],
775 "tuning" : [],
776 "compatibility" : _compatibility(detected, requirements, path_for_compat),
777 "architecture_impacts" : _feature_findings(detected, requirements, path_for_compat)[ 3 ],
778 "additional_targets" : _additional_targets(detected, requirements),
779 "migration_deltas" : [],
780 "evaluation" : _evaluation(detected, requirements),
781 "rollout" : {
782 "strategy" : "decision_required" ,
783 "gate" : "Resolve the open decision before implementation." ,
784 },
785 "verification" : _verification(region, catalog, None , False , None , selected = False ),
786 }
787 )
788 return rec
789
790 # --- Fail closed: a required feature with unknown status is not ready (G08) ---
791 if unknown_required:
792 return _unresolved(
793 "Required features are unresolved (status unknown): "
794 + ", " .join(unknown_required),
795 [
796 "The scan did not confirm these required features as detected or absent, so "
797 "readiness cannot be asserted."
798 ],
799 _finding(
800 "feature_scan_incomplete" ,
801 "[BLOCKS]" ,
802 "Required features are unresolved (status unknown): "
803 + ", " .join(unknown_required),
804 "Scan the recorded source paths and mark each required feature detected or "
805 "absent, then rerun Model Recommend." ,
806 ),
807 "" ,
808 )
809
810 # --- Hard conflict: OpenAI continuity required AND runtime-only governance ---
811 if continuity_required and runtime_required:
812 options = _decision_options(catalog, workload, region)
813 if len (options) < 2 :
814 raise ValueError (
815 f "catalog cannot supply both decision options for workload "
816 f " { workload[ 'workload_id' ] } "
817 )
818 return _unresolved(
819 "Required OpenAI API continuity conflicts with a runtime-only Bedrock "
820 "governance requirement." ,
821 [
822 "Mantle preserves the OpenAI SDK/Responses surface; runtime Converse provides "
823 "Bedrock-native governance with a Bedrock-native model."
824 ],
825 _finding(
826 "model_path_decision_required" ,
827 "[BLOCKS]" ,
828 "OpenAI API continuity and runtime-only governance cannot both be satisfied "
829 "by one path." ,
830 "Choose the Mantle continuity option or the runtime governance option, "
831 "update requirements, and rerun Model Recommend." ,
832 ),
833 "" ,
834 )
835
836 # --- Select a path ---
837 candidate_order = None
838 if runtime_required:
839 path = "runtime_converse"
840 candidate_order = _converse_candidate_order(source)
841 same = _same_model_runtime_key(source)
842 if same:
843 rationale_head = (
844 "Bedrock governance or multi-model requirements select runtime Converse; "
845 f "the source model itself ( { catalog[ 'models' ][same][ 'display_name' ] } ) runs "
846 "there via a CRIS id (verified 2026-08-21), so the same-model candidate "
847 "leads, with the Claude tier as the cross-family fallback."
848 )
849 else :
850 tier = _converse_tier_for_source(source)
851 rationale_head = (
852 "Bedrock governance or multi-model requirements select runtime Converse; "
853 f "the source tier maps to { catalog[ 'models' ][tier][ 'display_name' ] } "
854 "(capability-evidence fallback across Claude tiers). GPT-5.5/5.4 sources "
855 "have no runtime path, so governance implies this family switch."
856 )
857 else :
858 path = "mantle_openai_responses"
859 rationale_head = (
860 "OpenAI source with API continuity lands on Mantle Responses (GPT-5.x is "
861 "Responses-only on Mantle)."
862 )
863
864 # Evidence-driven selection: pick the first candidate on the path whose catalog
865 # capabilities cover the required/detected text features (a newer model without
866 # feature-level evidence falls back to an older probed one). G02: fail closed
867 # only when NO candidate on the path has the evidence.
868 catalog_hit, unsupported = _catalog_model_for_path(
869 catalog, path, detected, requirements, candidate_order = candidate_order
870 )
871 if not catalog_hit:
872 raise ValueError ( f "catalog has no available model for path { path } " )
873 model_key, model, path_config = catalog_hit
874 if unsupported:
875 return _unresolved(
876 rationale_head,
877 [
878 f "No { path } candidate has catalog capability evidence for: "
879 + ", " .join(unsupported)
880 ],
881 _finding(
882 "unverified_capability" ,
883 "[BLOCKS]" ,
884 f "No { path } candidate is evidenced to support required features: "
885 + ", " .join(unsupported),
886 "Add dated capability evidence for a candidate, choose a different path/model, "
887 "or drop the requirement, then rerun Model Recommend." ,
888 ),
889 path,
890 )
891
892 numeric_conflicts = _numeric_requirement_conflict(model, requirements)
893 if numeric_conflicts:
894 return _unresolved(
895 rationale_head,
896 numeric_conflicts,
897 _finding(
898 "unverified_capacity" ,
899 "[BLOCKS]" ,
900 "; " .join(numeric_conflicts),
901 "Source the missing capability data from a dated reference, or reduce the "
902 "hard numeric requirement, then rerun Model Recommend." ,
903 ),
904 path,
905 )
906
907 invocation_model_id = _resolve_invocation_model_id(
908 path_config[ "model_id" ], path_config[ "requires_cris" ], requirements
909 )
910 source_analysis = _source_analysis(source, model)
911
912 # --- Findings (target- and path-derived) ---
913 r_blocks, r_tuning, r_deltas = _reasoning_findings(source, requirements, model, path)
914 f_blocks, f_tuning, f_deltas, impacts = _feature_findings(detected, requirements, path)
915 blocks = r_blocks + f_blocks
916 tuning = r_tuning + f_tuning
917 deltas = list (r_deltas)
918
919 if path == "runtime_converse" and path_config[ "requires_cris" ] and invocation_model_id is None :
920 # GPT-5.6 on bedrock-runtime is CRIS-only — there is no in-region invocation
921 # form. A residency posture that forbids both Global and Geo CRIS makes the
922 # same-model governance path unusable; say so explicitly instead of shipping
923 # a recommendation with an unresolvable invocation id.
924 blocks.append(
925 _finding(
926 "cris_residency_unresolved" ,
927 "[BLOCKS]" ,
928 "This runtime path is CRIS-only, and the stated data-residency posture "
929 "permits neither Global nor Geo cross-region inference, so no invocation "
930 "id can be resolved." ,
931 "Relax residency to Global or a Geo CRIS geography, supply an explicit "
932 "inference_profile_id, or take the Claude cross-family Converse path." ,
933 )
934 )
935
936 if path == "mantle_openai_responses" and surface == "chat_completions" :
937 deltas.extend(_chat_to_responses_deltas())
938 blocks.append(
939 _finding(
940 "chat_completions_to_responses_required" ,
941 "[BLOCKS]" ,
942 "GPT-5.x on Mantle rejects Chat Completions; the source must reshape to the "
943 "Responses API." ,
944 "Apply the request/response/tool/state reshape deltas before cutover; do not "
945 "target mantle_openai_chat for GPT-5.x." ,
946 )
947 )
948 deltas.extend(f_deltas)
949
950 if path == "runtime_converse" :
951 deltas.append(
952 _delta(
953 "openai_sdk_to_converse" ,
954 "path" ,
955 "The OpenAI SDK integration is rewritten to boto3 bedrock-runtime Converse with a "
956 "Bedrock-native model; streaming moves to ConverseStream event shapes." ,
957 )
958 )
959
960 rationale = [rationale_head]
961 if model[ "context_window" ] == "unknown" or model[ "output_token_ceiling" ] == "unknown" :
962 rationale.append(
963 f " { model[ 'display_name' ] } path is evidenced by the reference, but its context/output "
964 "limits are unknown in the catalog and must be probed."
965 )
966 rec = _base(workload, "recommended" )
967 rec.update(
968 {
969 "source_analysis" : source_analysis,
970 "feature_assessment" : feature_assessment,
971 "primary_model" : path_config[ "model_id" ],
972 "model_identity" : _model_identity(model_key, model, path_config),
973 "api_path" : path,
974 "invocation_model_id" : invocation_model_id,
975 "decision_options" : [],
976 "alternatives" : [],
977 "rationale" : rationale,
978 "blocks" : blocks,
979 "tuning" : tuning,
980 "compatibility" : _compatibility(detected, requirements, path, model),
981 "architecture_impacts" : impacts,
982 "additional_targets" : _additional_targets(detected, requirements),
983 "migration_deltas" : deltas,
984 "evaluation" : _evaluation(detected, requirements),
985 "rollout" : {
986 "strategy" : "canary" ,
987 "gate" : "Compare source and target on the golden set before percentage rollout." ,
988 },
989 "verification" : _verification(
990 region, catalog, path, path_config[ "requires_cris" ], invocation_model_id, selected = True
991 ),
992 }
993 )
994 return rec