Setting the file. One moment.
Test Preflight Bedrock · LLM To Bedrock · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page 81
Creating Amazon Aurora Db Cluster With Instances
104
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
77
def test_probe_routes_embedding_models_to_invoke_model_not_converse
— line 77
This file
Number 30.29
Position 29 of 35
Type Python
Size 11 KB
Lines 259 scripts/ test_preflight_bedrock.py
Python · 259 lines · 11 KB
10 assert "bedrock:InvokeModel" in v[ "detail" ]
11
12
13 def test_classify_model_not_available_suggests_cross_region_profile ():
14 v = p.classify_invoke_error( "ValidationException" , "model identifier is invalid" )
15 assert v[ "ok" ] is False
16 assert v[ "reason" ] == "model_unavailable"
17
18
19 def test_classify_throttle_is_ok_for_preflight ():
20 # A throttle on the 1-token probe means we ARE authorized — treat as pass.
21 v = p.classify_invoke_error( "ThrottlingException" , "rate exceeded" )
22 assert v[ "ok" ] is True
23
24
25 def test_classify_expired_token_maps_to_credentials ():
26 v = p.classify_invoke_error( "ExpiredTokenException" , "The security token included in the request is expired" )
27 assert v[ "ok" ] is False
28 assert v[ "reason" ] == "credentials"
29
30
31 def test_probe_model_botocore_error_returns_json_verdict_not_traceback ():
32 # Regression: NoCredentialsError used to escape as an unhandled traceback,
33 # so the orchestrator's JSON parse failed exactly when guidance was needed.
34 from botocore.exceptions import NoCredentialsError
35
36 class NoCredsClient :
37 def converse (self, ** kwargs):
38 raise NoCredentialsError()
39
40 v = p.probe_model(NoCredsClient(), "any.model-v1:0" )
41 assert v[ "ok" ] is False
42 assert v[ "reason" ] == "credentials"
43 assert "NoCredentialsError" in v[ "detail" ]
44
45
46 def test_quota_rpm_matches_model_name_token ():
47 quotas = [
48 { "QuotaName" : "On-demand model inference requests per minute for Anthropic Claude Haiku 4.5" , "Value" : 50.0 },
49 { "QuotaName" : "On-demand model inference requests per minute for Amazon Nova Lite" , "Value" : 1000.0 },
50 { "QuotaName" : "Cross-region model inference tokens per day for Anthropic Claude" , "Value" : 9.9e9 },
51 ]
52 rpm = p.quota_rpm(quotas, "us.anthropic.claude-haiku-4-5-20251001-v1:0" )
53 assert rpm == 50 # the Nova quota and the per-day quota must not match
54
55
56 def test_quota_rpm_no_match_returns_none ():
57 quotas = [{ "QuotaName" : "On-demand model inference requests per minute for Amazon Nova Lite" , "Value" : 1000.0 }]
58 assert p.quota_rpm(quotas, "us.anthropic.claude-haiku-4-5-20251001-v1:0" ) is None
59
60
61 def test_main_empty_models_is_a_failure_not_a_vacuous_pass (capsys):
62 # Regression: `--models ""` used to exit 0 with all_ok=True.
63 import json
64 rc = p.main([ "--region" , "us-east-1" , "--models" , " , " ])
65 assert rc == 1
66 out = json.loads(capsys.readouterr().out)
67 assert out[ "ok" ] is False
68 assert out[ "reason" ] == "no_models"
69
70
71 def test_embedding_model_detection ():
72 assert p.is_embedding_model( "amazon.titan-embed-text-v2:0" ) is True
73 assert p.is_embedding_model( "cohere.embed-english-v3" ) is True
74 assert p.is_embedding_model( "us.anthropic.claude-haiku-4-5-20251001-v1:0" ) is False
75
76
77 def test_probe_routes_embedding_models_to_invoke_model_not_converse ():
78 # Regression: titan-embed probed via converse() got ValidationException and
79 # was misreported as model_unavailable, blocking valid embeddings migrations.
80 class Recorder :
81 called = None
82 def converse (self, ** kwargs):
83 Recorder.called = "converse"
84 return {}
85 def invoke_model (self, ** kwargs):
86 Recorder.called = "invoke_model"
87 assert kwargs[ "modelId" ] == "amazon.titan-embed-text-v2:0"
88 import json as j
89 assert "inputText" in j.loads(kwargs[ "body" ])
90 return {}
91
92 v = p.probe_model(Recorder(), "amazon.titan-embed-text-v2:0" )
93 assert Recorder.called == "invoke_model"
94 assert v[ "ok" ] is True
95
96
97 def test_probe_unknown_embedding_family_passes_with_caveat ():
98 class Boom :
99 def converse (self, ** kwargs):
100 raise AssertionError ( "must not call converse for embeddings" )
101 def invoke_model (self, ** kwargs):
102 raise AssertionError ( "must not probe an unknown embedding family" )
103
104 v = p.probe_model(Boom(), "somevendor.embed-x-v1:0" )
105 assert v[ "ok" ] is True
106 assert v[ "reason" ] == "embedding_unprobed"
107
108
109 def test_aggregate_failure_lifts_first_failing_reason_to_top_level ():
110 # Regression: per-model failures left no top-level reason, so the
111 # orchestrator's documented `ok==false + reason` branches never matched.
112 results = [
113 { "ok" : True , "reason" : "ok" , "detail" : "fine" , "model_id" : "m1" },
114 { "ok" : False , "reason" : "model_unavailable" , "detail" : "nope" , "model_id" : "m2" },
115 { "ok" : False , "reason" : "authz" , "detail" : "denied" , "model_id" : "m3" },
116 ]
117 agg = p.aggregate_failure(results)
118 assert agg[ "reason" ] == "model_unavailable"
119 assert agg[ "failing_models" ] == [ "m2" , "m3" ]
120
121
122 def test_aggregate_failure_empty_when_all_ok ():
123 assert p.aggregate_failure([{ "ok" : True , "reason" : "ok" , "detail" : "" , "model_id" : "m" }]) == {}
124
125
126 def test_access_denied_model_access_variant_routes_to_console_fix ():
127 # Bedrock's "model access not enabled" also surfaces as AccessDeniedException;
128 # sending the user to IAM for it is the wrong fix.
129 v = p.classify_invoke_error(
130 "AccessDeniedException" ,
131 "You don't have access to the model with the specified model ID. "
132 "Enable model access in the Amazon Bedrock console." )
133 assert v[ "ok" ] is False
134 assert v[ "reason" ] == "model_access"
135 assert "console" in v[ "detail" ].lower()
136
137
138 def test_access_denied_iam_variant_still_routes_to_authz ():
139 v = p.classify_invoke_error(
140 "AccessDeniedException" ,
141 "User: arn:aws:iam::123:user/x is not authorized to perform: "
142 "bedrock:InvokeModel on resource ..." )
143 assert v[ "reason" ] == "authz"
144 assert "bedrock:InvokeModel" in v[ "detail" ]
145
146
147 def test_mantle_model_detection_excludes_gpt_oss_and_cris_forms ():
148 # Bare proprietary GPT ids are mantle-served; gpt-oss speaks Converse; and
149 # GPT-5.6 CRIS profile ids (us./in./global.) are bedrock-runtime targets that
150 # MUST take the Converse probe — Converse is supported there (2026-08-21).
151 assert p.is_mantle_model( "openai.gpt-5.6-terra" ) is True
152 assert p.is_mantle_model( "openai.gpt-5.6-sol" ) is True
153 assert p.is_mantle_model( "openai.gpt-5.5" ) is True
154 assert p.is_mantle_model( "openai.gpt-5.4" ) is True
155 assert p.is_mantle_model( "openai.gpt-oss-120b-1:0" ) is False
156 assert p.is_mantle_model( "us.openai.gpt-5.6-sol" ) is False
157 assert p.is_mantle_model( "in.openai.gpt-5.6-luna" ) is False
158 assert p.is_mantle_model( "global.openai.gpt-5.6-terra" ) is False
159 assert p.is_mantle_model( "anthropic.claude-sonnet-4-6" ) is False
160 assert p.is_mantle_model( "amazon.nova-lite-v1:0" ) is False
161
162
163 def test_mantle_authz_error_points_at_mantle_actions_not_invoke_model ():
164 # Regression: sending the user to bedrock:InvokeModel is a dead end for these
165 # models — mantle inference needs bedrock-mantle:* actions.
166 v = p.classify_mantle_error( 403 , "User is not authorized to perform bedrock-mantle:CreateInference" )
167 assert v[ "ok" ] is False
168 assert v[ "reason" ] == "authz"
169 assert "bedrock-mantle" in v[ "detail" ]
170 assert "does NOT authorize" in v[ "detail" ]
171
172
173 def test_mantle_model_unavailable_remedy_is_family_split ():
174 # Verified 2026-08-21: mantle itself is in-region only, but GPT-5.6 now has a
175 # bedrock-runtime CRIS path — so the 404 remedy must offer the CRIS form for
176 # 5.6 while making clear 5.5/5.4 have no prefixed form. An earlier version of
177 # this test asserted the opposite (never suggest a prefix), which matched the
178 # pre-2026-08-21 docs.
179 v = p.classify_mantle_error( 404 , "model not found" )
180 assert v[ "ok" ] is False
181 assert v[ "reason" ] == "model_unavailable"
182 assert "in-region only" in v[ "detail" ]
183 assert "CRIS" in v[ "detail" ] and "us./in./global." in v[ "detail" ]
184 assert "GPT-5.5/5.4" in v[ "detail" ]
185
186 def test_mantle_throttle_is_ok_for_preflight ():
187 v = p.classify_mantle_error( 429 , "too many tokens per minute" )
188 assert v[ "ok" ] is True
189 assert v[ "reason" ] == "throttled_ok"
190
191
192 def test_mantle_model_access_variant_routes_to_console_fix ():
193 v = p.classify_mantle_error( 403 , "You do not have access to the model with the specified model ID" )
194 assert v[ "ok" ] is False
195 assert v[ "reason" ] == "model_access"
196 assert "console" in v[ "detail" ].lower()
197
198
199 def test_main_probes_mantle_models_without_bedrock_runtime (monkeypatch, capsys):
200 # Regression: mantle-only models were probed with bedrock-runtime Converse,
201 # which always fails, so preflight blocked every GPT-5.x migration.
202 import json
203 calls = {}
204
205 def fake_probe_mantle (model_id, region):
206 calls[ "mantle" ] = (model_id, region)
207 return { "ok" : True , "reason" : "ok" , "detail" : "Mantle Responses API authorized." }
208
209 def fake_probe_model (client, model_id):
210 raise AssertionError ( "must not probe a mantle-only model via bedrock-runtime" )
211
212 monkeypatch.setattr(p, "probe_mantle_model" , fake_probe_mantle)
213 monkeypatch.setattr(p, "probe_model" , fake_probe_model)
214 monkeypatch.setattr(p, "fetch_bedrock_quotas" , lambda region: [])
215
216 import boto3
217 monkeypatch.setattr(boto3, "client" , lambda * a, ** k: object ())
218
219 rc = p.main([ "--region" , "us-east-1" , "--models" , "openai.gpt-5.6-terra" , "--dataset-size" , "9999" ])
220 out = json.loads(capsys.readouterr().out)
221
222 assert rc == 0
223 assert calls[ "mantle" ] == ( "openai.gpt-5.6-terra" , "us-east-1" )
224 entry = out[ "models" ][ 0 ]
225 assert entry[ "ok" ] is True
226 assert entry[ "rpm_quota" ] is None
227 # A dataset larger than any RPM number must not produce an RPM pacing warning
228 # for a model that has no RPM quota.
229 assert "quota_warning" not in entry
230 assert "no RPM quota" in entry[ "quota_note" ]
231
232
233 def test_mantle_missing_deps_fails_closed (monkeypatch):
234 # Regression: this returned ok=True with a caveat, mirroring the rare
235 # `embedding_unprobed` case. But a missing SDK is the NORMAL path if the
236 # dependency is absent, so passing turned a fail-fast preflight into an
237 # unconditional green light — endpoint, model and IAM access never checked.
238 import builtins
239 real_import = builtins.__import__
240
241 def no_openai (name, * a, ** k):
242 if name in ( "openai" , "aws_bedrock_token_generator" ):
243 raise ImportError ( f "No module named { name !r} " )
244 return real_import(name, * a, ** k)
245
246 monkeypatch.setattr(builtins, "__import__" , no_openai)
247 v = p.probe_mantle_model( "openai.gpt-5.6-terra" , "us-east-1" )
248 assert v[ "ok" ] is False
249 assert v[ "reason" ] == "mantle_deps_missing"
250 assert "NOT verified" in v[ "detail" ]
251
252
253 def test_mantle_deps_are_declared_in_pinned_env ():
254 # The fail-closed path above must never trigger in a correctly synced env,
255 # so the pinned toolchain has to declare both packages.
256 import pathlib
257 toml = (pathlib.Path( __file__ ).resolve().parent / "pyproject.toml" ).read_text()
258 assert "openai>=2.45.0" in toml
259 assert "aws-bedrock-token-generator" in toml