diff --git a/migrate/plugins/migration-to-aws/agents/llm2bedrock-code-analyzer.md b/migrate/plugins/migration-to-aws/agents/llm2bedrock-code-analyzer.md index 9fd865a4..25c4051f 100644 --- a/migrate/plugins/migration-to-aws/agents/llm2bedrock-code-analyzer.md +++ b/migrate/plugins/migration-to-aws/agents/llm2bedrock-code-analyzer.md @@ -124,7 +124,11 @@ Determine: 1. **Source provider** (the value emitted in `source_provider`): one of `openai` / `anthropic` (1P) / `google` (Gemini, including Vertex AI) / `cohere` / `custom` (OpenAI-compatible). §7.1.2 below distinguishes Vertex AI internally for §12 only — the public enum stays at these 5 values so downstream agents don't need to learn a new branch. 2. **AI framework**: raw SDK / LangChain / LlamaIndex / Vercel AI SDK / custom 3. **SDK version**: read from lockfile or manifest -4. **Same model family**: defaults to `false`. Set `same_model_family: true` ONLY when ALL plan model mappings go from Anthropic 1P (direct `anthropic` SDK) to Bedrock Claude — in that case the prompt-adaptation step is skipped downstream. Mixed projects (e.g. chat=Anthropic→Claude AND embeddings=OpenAI→Cohere) → `false`. +4. **Same model family**: defaults to `false`. Set `same_model_family: true` when ALL plan model mappings keep the model itself, which is now true in two cases: + - Anthropic 1P (direct `anthropic` SDK) → Bedrock Claude, and + - OpenAI → **the same OpenAI model on Bedrock**, i.e. every `aws_model_id` is a proprietary GPT model (`openai.gpt-5*`) matching its `source_model`. + + In both cases the prompt-adaptation step is skipped downstream, because the model is unchanged. Mixed projects (e.g. chat=Anthropic→Claude AND embeddings=OpenAI→Cohere) → `false`. A GPT source mapped to Claude/Nova/`gpt-oss` is a model change → `false`. ## 7.1.1 Disambiguate `openai` vs OpenAI-compatible @@ -240,6 +244,8 @@ If `source_provider ∈ {openai, google}` AND `same_model_family == false`, scan For any other source_provider (`anthropic`, `cohere`, `custom`) OR `same_model_family == true`, set `behavior_deltas: []` and skip the rest of this section. +**Exception — OpenAI same-model on mantle.** When `source_provider == openai` AND `same_model_family == true` (every target is a proprietary `openai.gpt-5*` model), do NOT emit `behavior_deltas: []` wholesale. The model is unchanged, so the parameter-surface deltas (temperature range, penalties, stop sequences) genuinely do not apply — but the **API surface** can still change. Read only the "Same-model (mantle) deltas" section of the reference's `openai-to-bedrock.md` and emit those: Chat Completions → Responses, reasoning items round-tripping, endpoint path and credential, and prompt-caching availability. Skipping these would leave a `chat.completions.create` call pointed at a model that does not verifiably accept it. + 1. Read the `behavior-delta-detection` reference at the absolute path given in your context block's `behavior-delta-detection reference:` line. Call that file's directory `` (strip the filename from that path). @@ -308,7 +314,7 @@ find -type f \( \( -name "*.json" -path "*langsmith*" \) -o \( -name "*.j - `cohere` / `custom` / `unknown` / empty — no stable HTTP contract callable with stdlib alone. - `errors` contains the EXACT substring `vertex AI auth detected (ADC, not API key)` (per §7.1.2) — Vertex AI uses ADC, not API keys; pasting a Gemini API key against Vertex would 401. Match the full phrase to avoid false hits from other `errors` entries that happen to contain "vertex". -- `same_model_family == true` (Anthropic 1P → Bedrock Claude) — the evaluator skips quality scoring entirely, so a live baseline adds no value. +- `same_model_family == true` (Anthropic 1P → Bedrock Claude, or OpenAI → the same GPT model on Bedrock) — the evaluator skips quality scoring entirely, so a live baseline adds no value. The source and target are the same model, so a baseline would be comparing it against itself. Set `source_baseline_available` from the `Source baseline available:` line in your context — the orchestration skill sets it to `true` when the user already supplied a key in Phase B3, `false` otherwise. Echo that value; do not hardcode either way (hardcoding `false` would clobber an already-collected key's signal for the evaluator downstream). diff --git a/migrate/plugins/migration-to-aws/agents/llm2bedrock-code-rewriter.md b/migrate/plugins/migration-to-aws/agents/llm2bedrock-code-rewriter.md index 64b0f6d9..1093b001 100644 --- a/migrate/plugins/migration-to-aws/agents/llm2bedrock-code-rewriter.md +++ b/migrate/plugins/migration-to-aws/agents/llm2bedrock-code-rewriter.md @@ -37,7 +37,7 @@ Read from prompt context (forwarded from llm2bedrock-code-analyzer, llm2bedrock- - `files_to_modify` — list of `": "`. §10 iterates over this exact list. - `dependencies_to_replace` — list of `" -> "`. §12 applies these to the manifest. - `behavior_deltas` — list of parameter-surface differences. The user ALREADY confirmed each one at the orchestration checkpoint; §9 applies the confirmed decisions. - - `same_model_family` — `true` for Anthropic 1P → Bedrock Claude. Skip prompt adaptation in §10. + - `same_model_family` — `true` when the model itself is unchanged: Anthropic 1P → Bedrock Claude, or OpenAI → the same GPT model on Bedrock (`openai.gpt-5*`). Skip prompt adaptation in §10, and leave model parameters (`temperature`, penalties, stop sequences) untouched — they did not change. - `special_patterns` — `{streaming, function_calling, embeddings, vision}` booleans. Drives §8 examples to apply. - **From `llm2bedrock-prompt-evaluator`** (T2-4) — adapted prompts (if any) at `/.saws-migrate/eval-results/adapted_prompts.jsonl`. §10 step 2 injects these where applicable. - **`Confirmed behavior-delta decisions file (Read it):`** — a context line naming `/delta-decisions.json`. `Read` that file: a JSON array where each entry carries a behavior delta and the user's chosen resolution/option (`[]` = none). §9 applies these EXACTLY as decided. @@ -152,27 +152,65 @@ If your context has a `Rewrite strategy: mantle` line, use the **Mantle express The source SDK stays. Per client, change only three things: -- **base_url** → `https://bedrock-mantle..api.aws/v1` (OpenAI-compatible SDKs) or `https://bedrock-mantle..api.aws/anthropic/v1` (Anthropic SDK). +- **base_url** → depends on the target model family. Pick from this table; getting it wrong returns 404, not a helpful error: + + | Target model | base_url | + | ---------------------------------------- | ------------------------------------------------------ | + | Proprietary OpenAI GPT (`openai.gpt-5*`) | `https://bedrock-mantle..api.aws/openai/v1` | + | Other OpenAI-compatible targets | `https://bedrock-mantle..api.aws/v1` | + | Anthropic SDK | `https://bedrock-mantle..api.aws/anthropic/v1` | + + If your context has a `Mantle base path` line, use it verbatim — it is authoritative over this table. - **Credential** → a Bedrock bearer token, NOT the original provider key, read from the `AWS_BEARER_TOKEN_BEDROCK` env var. Do not leave the old `api_key=os.environ["OPENAI_API_KEY"]` line in place. - **Model ID** → the Bedrock model id from the `Mantle model map` context line (the `aws_model_id` from the migration plan). -OpenAI SDK example: +**When your context has `Same model: true`**, the target is the same model the app already used. Do NOT change model parameters (`temperature`, penalties, stop sequences) — they are unchanged, and §9 will not ask about them. Limit edits to base_url, credential, and model id, plus the Chat Completions → Responses reshape below if the source used Chat Completions. + +OpenAI SDK example — proprietary GPT target (note the `openai/v1` path): ```python # Before from openai import OpenAI client = OpenAI() # api_key from OPENAI_API_KEY -# After (Mantle — same SDK) +# After (Mantle — same SDK, same model) import os from openai import OpenAI client = OpenAI( - base_url="https://bedrock-mantle.us-east-1.api.aws/v1", + base_url="https://bedrock-mantle.us-east-1.api.aws/openai/v1", api_key=os.environ["AWS_BEARER_TOKEN_BEDROCK"], ) -# model="gpt-4o" -> model="anthropic.claude-haiku-4-5" +# model="gpt-5.5" -> model="openai.gpt-5.5" +``` + +A bearer token read from the environment expires within 12 hours. When the target repo has a long-running process (a server, worker, or scheduled job rather than a short CLI run), prefer the auto-refreshing client and note the added dependency in `dependency_changes`: + +```python +from aws_bedrock_token_generator import provide_token # aws-bedrock-token-generator +from openai import BedrockOpenAI # openai>=2.45.0 + +region = "us-east-1" +client = BedrockOpenAI( + aws_region=region, + bedrock_token_provider=lambda: provide_token(region=region), + max_retries=6, +) ``` +**Chat Completions → Responses (proprietary GPT targets only).** Chat Completions is unverified for these models; every AWS sample uses Responses. If the source calls `chat.completions.create`, reshape it — this is the one part of the Mantle lane that is not config-only: + +```python +# Before +r = client.chat.completions.create(model="gpt-5.5", messages=msgs, max_tokens=512) +text = r.choices[0].message.content + +# After +r = client.responses.create(model="openai.gpt-5.5", input=msgs, max_output_tokens=512, store=False) +text = r.output_text +``` + +For multi-turn or tool-calling flows, append `r.output` to the next request's `input` — these models emit reasoning items that must round-trip, and dropping them degrades quality silently rather than raising an error. + Anthropic SDK example: ```python @@ -189,7 +227,9 @@ client = anthropic.Anthropic( ) ``` -Do NOT rewrite request/response parsing — the whole point of Mantle is that the source SDK's call and response shapes are preserved. After applying the three changes above, skip the Converse-specific guidance in the rest of §8 and the §9 behavior-delta application still applies normally. +Do NOT rewrite request/response parsing — the whole point of Mantle is that the source SDK's call and response shapes are preserved. The single exception is the Chat Completions → Responses reshape above, which applies only to proprietary GPT targets. After applying the changes above, skip the Converse-specific guidance in the rest of §8; the §9 behavior-delta application still applies normally. + +**Never rewrite a proprietary GPT target to Converse.** `openai.gpt-5*` models have no `bedrock-runtime` surface, so a boto3 `converse()` call against one fails at runtime. If your context pairs a `openai.gpt-5*` model id with the Converse path, stop and report the contradiction rather than generating code that cannot work. ### Converse rewrite (default) diff --git a/migrate/plugins/migration-to-aws/agents/llm2bedrock-prompt-evaluator.md b/migrate/plugins/migration-to-aws/agents/llm2bedrock-prompt-evaluator.md index 30da391e..f5a364d0 100644 --- a/migrate/plugins/migration-to-aws/agents/llm2bedrock-prompt-evaluator.md +++ b/migrate/plugins/migration-to-aws/agents/llm2bedrock-prompt-evaluator.md @@ -39,12 +39,12 @@ Read from prompt context (forwarded from llm2bedrock-code-analyzer, llm2bedrock- - **``** — `/.saws-migrate/golden-dataset/prompts.jsonl` (from T2-2). May be empty if T2-2 took the abort / paste / vision-no-images / embeddings path. - **``** — `/.saws-migrate/golden-dataset/templates/prompt_template.txt` (from T2-2). -- **``** — Bedrock target model ID from the migration plan, validated by llm2bedrock-code-analyzer §10. Substitute in every `boto3.converse` call below. +- **``** — Bedrock target model ID from the migration plan, validated by llm2bedrock-code-analyzer §10. Substitute it in every Bedrock call below. **It also selects the API path:** an `openai.gpt-5*` id (excluding `-oss`) is mantle-only and must use the Responses client in §6a; everything else uses `boto3.converse`. See the table at the top of §6. - **``** — AWS region for Bedrock (the `AWS region:` line in your context). - **From `llm2bedrock-code-analyzer` (`AiAnalysisData`)** — key fields: - `source_provider` — `openai` / `anthropic` / `google` / `cohere` / `custom`. Drives §9 baseline gating. (Vertex AI customers are emitted as `google` here; the analyzer's `errors` field carries the `vertex AI auth detected` signal that gates baseline collection upstream — by the time you reach §9, `source_baseline_available` already reflects that.) - `source_models` — list of source-model IDs. Pass `` to the §9 baseline skill verbatim. - - `same_model_family` — `true` only for Anthropic 1P → Bedrock Claude; triggers §8 short-circuit. + - `same_model_family` — `true` when the model is unchanged: Anthropic 1P → Bedrock Claude, or OpenAI → the same GPT model on Bedrock (`openai.gpt-5*`). Triggers the §8 short-circuit. - `source_baseline_available` — `true` iff the user supplied a source-provider API key (orchestration skill Phase B3) and it was written to `/.saws-migrate/.source-provider-env`. When `false`, §9 skips and the report banner will note the gap. - `special_patterns` — `{streaming, function_calling, embeddings, vision}` booleans. Drives §5 layer selection. - `bedrock_provider_available` — informational ONLY. This is a rewrite-strategy flag for T2-5, NOT an account-capability flag. Do NOT use it to decide whether your Bedrock calls will work — Step §6 verifies that directly. @@ -125,7 +125,25 @@ When reporting results, clearly separate which layers passed / failed / skipped. # 6. Setup + Bedrock connectivity check Create the eval results directory and verify Bedrock connectivity against the -target model using the SAME API path Step 4 will use (`boto3.converse`). +target model using the SAME API path the evaluation will use. + +**First, pick the API path from `` — this decides every Bedrock +call in §6, §9.5 and §10:** + +| `` matches | API path | Connectivity | Vision smoke | Golden eval | +| ------------------------------------------------ | ---------------- | ------------ | ------------ | ------------------------ | +| `openai.gpt-5*` (not `-oss`) | Mantle Responses | §6a | §9.5a | §8 loop (same-model) | +| anything else (Claude, Nova, `openai.gpt-oss-*`) | `boto3.converse` | §6 | §9.5 | §10, or §8 if same-model | + +The proprietary OpenAI GPT models are served ONLY on the `bedrock-mantle` +endpoint. Calling `boto3.converse` against one fails, so running the Converse +check below for a mantle target would abort this agent before it ever reached +§8's same-model short-circuit. Route those to §6a instead. + +This choice is made once and holds for every Bedrock call in the run. It is +independent of `same_model_family`: the short-circuit decides whether to _score_, +while this table decides _how to call_. A same-model Anthropic run still uses +Converse, and a same-model OpenAI run still needs its vision smoke test. ```bash mkdir -p /.saws-migrate/eval-results @@ -166,6 +184,57 @@ Interpret the result: `bedrock_provider_available` from the orchestrator context (it is a rewrite-strategy flag, not an account-capability flag). +# 6a. Mantle connectivity check (proprietary OpenAI GPT targets) + +Use this INSTEAD of §6 when `` matches `openai.gpt-5*` (excluding +`-oss`). Same purpose, different endpoint and SDK. + +```bash +mkdir -p /.saws-migrate/eval-results + +AWS_REGION= when your context has an `AWS profile` line> uv run --project python - <<'PY' +import os, sys +from aws_bedrock_token_generator import provide_token +from openai import BedrockOpenAI +region = os.environ.get('AWS_REGION', 'us-east-1') +try: + client = BedrockOpenAI(aws_region=region, + bedrock_token_provider=lambda: provide_token(region=region), + max_retries=2) + r = client.responses.create(model='', input='ping', + max_output_tokens=16, store=False) + print('OK:', r.output_text) +except Exception as e: + print(f'FAIL [{type(e).__name__}]: {e}', file=sys.stderr) + sys.exit(1) +PY +``` + +Interpret the result: + +- **Exit 0 + "OK:" line** — proceed to §7. +- **HTTP 404 / model-not-found** — the model is not available at this endpoint or + region. These models are **in-region only**, so there is no cross-region + inference profile to fall back to and `resolve-bedrock-model-id`'s + inference-profile ranking does not apply. Return + `{ blocked: { reason: 'model_unresolvable', detail: ' is not available on the bedrock-mantle endpoint in . These models are in-region only, so there is no cross-region inference profile to fall back to: a supported region or a different model id is required.' } }`. + Use `model_unresolvable` rather than a new reason — its recovery path (user + picks or pastes an ID, recorded in `resolved_model_overrides`) is exactly right + here. Do not offer a `us.`-prefixed candidate; those do not exist for these models. +- **HTTP 401 / 403** — distinguish the two causes from the message. If it names + model access, return `{ blocked: { reason: 'model_access', ... } }` as in §6. + Otherwise it is IAM: the principal needs the `bedrock-mantle:*` actions + (`CreateInference`, `CallWithBearerToken`), **not** `bedrock:InvokeModel`. + Return `{ blocked: { reason: 'authz', detail: ' — grant bedrock-mantle actions (e.g. the AmazonBedrockMantleInferenceAccess managed policy); bedrock:InvokeModel does not authorize these models.' } }`. +- **`ModuleNotFoundError`** — the pinned scripts environment is missing `openai` or + `aws-bedrock-token-generator` (both are declared in `scripts/pyproject.toml`). + Return `{ blocked: { reason: 'mantle_deps_missing', detail: ' — re-sync the pinned environment.' } }`. Do NOT treat this as a passing check. +- **Any other failure** — surface the exact error type and message and STOP, as in §6. + +Because these targets are same-model migrations, §8's short-circuit applies right +after §7: run each golden prompt through this same Responses client for +connectivity verification rather than the Converse script in §10. + # 7. Load golden dataset ```bash @@ -177,14 +246,61 @@ If `total_golden_cases == 0` (T2-2 abort / paste / vision-no-images / embeddings # 8. Same-model-family short-circuit -If `same_model_family: true` (Anthropic 1P → Bedrock Claude): +If `same_model_family: true` — either Anthropic 1P → Bedrock Claude, or OpenAI → the same GPT model on Bedrock (`openai.gpt-5*`): - Skip rubric generation and scoring (no parameter-surface drift to score against). - Just verify each prompt works on Bedrock (connectivity + response format): run each prompt, check for errors, verify response is non-empty. +- **Use the API path §6 selected, not §10's script.** For an Anthropic 1P → Claude run that is `boto3.converse`. For an `openai.gpt-5*` run it is the Mantle Responses client from §6a — `client.responses.create(model='', input=, max_output_tokens=, store=False)`, reading `r.output_text`. §10's script is Converse-only and will fail against a mantle target; do not call it here. - Output pass / fail per prompt; count successes as `success_count`. - Compute `pass_rate = success_count / total_cases` (connectivity-only ratio) and write `failures = total_cases - success_count`. - In §14, set `live_source_baseline: false` (no live comparison ran) and add `notes` prefix `same_model_family: true — connectivity-only verification, no rubric scoring`. T2-6 reads that prefix to render the report banner with "connectivity verified" instead of "judge scored X/Y prompts". Set `source_baseline_quality: 'unknown'` (no live baseline ran). -- Skip to §14 (no §9 baseline, no §11 scoring, no §12–§13 adaptation). +- **§9.5 still runs when `special_patterns.vision == true`** — do NOT skip it. It is the Layer 2 gate that proves image input works before any per-case image call, and skipping it would let a vision migration report `pass_rate: 1.0` without a single image request. §9.5 selects its own API path (Converse or Responses) the same way §6 does, so it is valid for both same-model variants. +- **Send image input for every golden case that has one.** If a case's `image_path` is non-null, the verification call MUST include the image, not just the prompt text — see the per-path shapes below. A vision run whose cases are sent as text-only is not a verification, and the resulting pass rate is meaningless. +- Skip to §14 (no §9 baseline, no §10 Converse eval, no §11 scoring, no §12–§13 adaptation). §10 is Converse-only, so for a mantle target it is replaced by the loop described here, not merely skipped. + +Build the request with the shared helper — do NOT assemble these dicts inline: + +```python +import sys; sys.path.insert(0, "") +from image_input import converse_message, responses_message + +# Golden-case fields are `user_prompt`, `system_prompt`, `image_path` — NOT `prompt`. +# (Canonical record shape: llm2bedrock-log-ingestor §9.) +raw = open(case["image_path"], "rb").read() if case.get("image_path") else None + +# Converse target: +msgs = [converse_message(case["user_prompt"], case.get("image_path"), raw)] +kwargs = {"modelId": "", "messages": msgs, + "inferenceConfig": {"maxTokens": 512}} +if case.get("system_prompt"): + kwargs["system"] = [{"text": case["system_prompt"]}] +r = c.converse(**kwargs) + +# Mantle target: a system prompt becomes a developer-role item ahead of the user item. +items = [] +if case.get("system_prompt"): + items.append({"role": "developer", + "content": [{"type": "input_text", "text": case["system_prompt"]}]}) +items.append(responses_message(case["user_prompt"], case.get("image_path"), raw)) +r = client.responses.create(model="", input=items, + max_output_tokens=512, store=False) +``` + +Two things the helper exists to get right, both of which fail _only_ on golden cases +while the §9.5 / §9.5a smoke tests still pass (those hardcode a known-good jpeg): + +- **The file extension is not the wire format.** A `.jpg` case is `format: "jpeg"` + for Converse and `image/jpeg` for Responses; `jpg` and `image/jpg` are rejected. +- **Responses content must be wrapped in a message item.** `input` takes + `[{"role": "user", "content": [...]}]`, not a bare `[{"type": "input_text", ...}]` + list. `responses_message` returns the wrapper for exactly this reason. + +An unsupported image type raises rather than being passed through, so record that +case as a failure with the raised message rather than retrying it as text. + +Count a case as failed if the call errors or returns empty output. If a case with an +`image_path` cannot be sent as an image at all, record it as a failure and say so in +`notes` — never silently downgrade it to a text-only call. # 9. Live source-model baseline (PM trust-gap fix) @@ -199,8 +315,9 @@ If `same_model_family: true` (Anthropic 1P → Bedrock Claude): for the final report. The report will surface a banner explaining the pass rate is not a side-by-side comparison. -- `same_model_family == true` (Anthropic 1P → Bedrock Claude) → SKIP. - §8 already short-circuits scoring entirely; live baseline adds nothing. +- `same_model_family == true` (Anthropic 1P → Bedrock Claude, or OpenAI → + the same GPT model on Bedrock) → SKIP. §8 already short-circuits scoring + entirely; a live baseline would compare the model against itself. **Procedure:** @@ -242,6 +359,10 @@ If `same_model_family: true` (Anthropic 1P → Bedrock Claude): If `special_patterns.vision == false`, SKIP this section. +**This section runs for same-model migrations too** (both Anthropic 1P → Claude and OpenAI → the same GPT model). §8 short-circuits scoring, not image verification: it is the only gate that proves image input works before per-case image calls, so skipping it would let a vision migration pass without ever sending an image. + +**Pick the API path exactly as §6 did.** The script below uses `boto3.converse` and is valid for Converse targets. For a mantle-only `openai.gpt-5*` target use §9.5a instead — the GPT-5.x model cards list image input as supported, but it goes through the Responses API, not Converse. + Otherwise, run a one-shot Bedrock call against a public Wikipedia image to prove the SDK accepts image input before §10 attempts it on every golden prompt. If the public CDN isn't reachable, the smoke is INCONCLUSIVE — do NOT attempt an inline-fixture fallback (tiny synthetic JPEGs trip Claude's minimum-dimension validators and produce false `VISION_FAIL` even when the SDK is fine): ```bash @@ -280,8 +401,60 @@ Outcomes: - **`VISION_INFRA_SKIPPED`** — image download failed (DNS / proxy / air-gapped machine). Bedrock vision was NOT exercised; the test is inconclusive at this layer. Add to `notes`: `vision_smoke_skipped: CDN unreachable — Bedrock vision SDK path not exercised at smoke layer`. Proceed to §10 — golden cases carry their own images from T2-2, which will exercise the SDK directly. - **`VISION_FAIL`** — Bedrock rejected the image (`ValidationException`, `AccessDeniedException`, etc.). Surface the exact error in your result file's `notes`, STOP — golden vision eval will fail the same way. (If the failure is an `AccessDeniedException` on model access, route it through `{ blocked: { reason: 'model_access', detail: ... } }` per §6.) +# 9.5a Mantle vision smoke test (proprietary OpenAI GPT targets) + +Use this INSTEAD of §9.5 when `` matches `openai.gpt-5*` (excluding `-oss`) AND `special_patterns.vision == true`. Same purpose and same three outcomes; the image travels as a base64 data URL on the Responses API rather than as Converse image bytes. + +```bash +AWS_REGION= when your context has an `AWS profile` line> uv run --project python - <<'PY' +import base64, os, sys +try: + import urllib.request + img = urllib.request.urlopen( + "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/320px-Cat_November_2010-1a.jpg", + timeout=15, + ).read() +except Exception as e: + print(f"VISION_INFRA_SKIPPED [{type(e).__name__}]: {e}", file=sys.stderr) + sys.exit(0) + +from aws_bedrock_token_generator import provide_token +from openai import BedrockOpenAI +region = os.environ.get("AWS_REGION", "us-east-1") +data_url = "data:image/jpeg;base64," + base64.b64encode(img).decode() +try: + client = BedrockOpenAI(aws_region=region, + bedrock_token_provider=lambda: provide_token(region=region), + max_retries=2) + r = client.responses.create( + model="", + input=[{"role": "user", "content": [ + {"type": "input_text", "text": "Describe this image briefly."}, + # `detail` is Required on ResponseInputImageParam in the pinned SDK. + {"type": "input_image", "image_url": data_url, "detail": "auto"}, + ]}], + max_output_tokens=32, + store=False, + ) + print("VISION_OK:", r.output_text) +except Exception as e: + print(f"VISION_FAIL [{type(e).__name__}]: {e}", file=sys.stderr) + sys.exit(1) +PY +``` + +Outcomes are the same as §9.5: `VISION_OK` → proceed; `VISION_INFRA_SKIPPED` → record `vision_smoke_skipped` in `notes` and proceed (the golden cases carry their own images and will exercise the path directly); `VISION_FAIL` → surface the exact error and STOP, routing a model-access denial through `{ blocked: { reason: 'model_access', ... } }` and an IAM denial through `{ blocked: { reason: 'authz', ... } }` per §6a. + +**On `VISION_FAIL` specifically, do not fall back to text-only prompts.** The migration's vision capability is unverified at that point, and a text-only pass rate would misrepresent it as working. The content-block shape above is the documented OpenAI Responses form; if it is rejected for a reason other than access, report the exact error rather than guessing at an alternative shape. + # 10. Run golden prompt evaluation +> **Converse-only.** The script below calls `boto3.converse` and is valid only for +> the API path §6 selected. A `openai.gpt-5*` target never reaches this section — +> §8's same-model short-circuit fires first and verifies prompts through the Mantle +> Responses client from §6a. If you somehow arrive here with a mantle target, +> stop and report the contradiction rather than running a call that cannot work. + For each prompt in the golden dataset, run the evaluation via `python` stdin (avoids the brittle nested-heredoc + escaped-quote pattern that breaks on any literal `'` inside the script): ```bash @@ -668,7 +841,9 @@ These mutually-exclusive control states replace the normal eval object when they - **`blocked`** — a genuine hard stop that needs user resolution. Return `{ blocked: { reason, detail } }` where `reason` is one of: - `model_access` — Bedrock model access not enabled for the account (§6 `AccessDeniedException`, or a §9.5 vision access denial). Put the console URL + exact error in `detail`. - `source_key_auth` — the source-provider API key returned 401/403 (§9 all-401/403). Put the provider name + that a new key (or skip) is needed in `detail`. NEVER put the key value in `detail`. - - `model_unresolvable` — the target model id cannot be resolved even after `resolve-bedrock-model-id` (§6). Put the exact `ValidationException` message + the model id you tried in `detail`. + - `model_unresolvable` — the target model id cannot be resolved even after `resolve-bedrock-model-id` (§6), **or** a mantle target returned 404 in this region (§6a). Put the exact error message + the model id you tried in `detail`; for the mantle case also state that these models are in-region only. + - `authz` — IAM denies inference (§6 `AccessDeniedException` on `bedrock:InvokeModel*`, or §6a 401/403 that does not name model access). Name the action set to grant in `detail`: `bedrock:InvokeModel*` for Converse targets, the `bedrock-mantle:*` actions for mantle targets. Keep this distinct from `model_access` — they have different fixes. + - `mantle_deps_missing` — the pinned scripts environment lacks `openai` / `aws-bedrock-token-generator`, so a mantle target could not be probed (§6a `ModuleNotFoundError`). Say access was NOT verified and that the env needs re-syncing. - **`partial`** — the run was throttle-truncated. Return `{ partial: { completed: , total: , reason: 'throttled' } }` when the 429 retry budget (§10) is exhausted with cases still unscored. ## What goes in the eval object's fields diff --git a/migrate/plugins/migration-to-aws/agents/llm2bedrock-report-generator.md b/migrate/plugins/migration-to-aws/agents/llm2bedrock-report-generator.md index b25988ae..3d179b6c 100644 --- a/migrate/plugins/migration-to-aws/agents/llm2bedrock-report-generator.md +++ b/migrate/plugins/migration-to-aws/agents/llm2bedrock-report-generator.md @@ -113,6 +113,9 @@ The pricing script only covers Bedrock models. For the source provider (OpenAI, | Model | Input (USD/1M) | Output (USD/1M) | | -------------------------- | -------------- | --------------- | +| gpt-5.6-luna | 0.20 | 1.20 | +| gpt-5.5 | 5.00 | 30.00 | +| gpt-5.4 | 2.50 | 15.00 | | gpt-4o | 2.50 | 10.00 | | gpt-4o-mini | 0.15 | 0.60 | | gpt-4-turbo | 10.00 | 30.00 | @@ -124,6 +127,8 @@ The pricing script only covers Bedrock models. For the source provider (OpenAI, | claude-3-5-sonnet (1P API) | 3.00 | 15.00 | | claude-3-haiku (1P API) | 0.25 | 1.25 | +**Same-model pairs are cost-neutral — do not report a delta as a finding.** When a source GPT model maps to itself on Bedrock (`openai.gpt-5*`), Bedrock charges OpenAI's first-party rate, so both sides of the pair use the same per-token numbers and the computed delta is zero by construction. Report it as "cost unchanged (Bedrock charges provider list price)" rather than as "no savings identified", and do not flag the zero delta as a risk. `gpt-5.6-terra` and `gpt-5.6-sol` rates are not published in AWS sources — if a pair involves them, treat the cost line as "(pricing unavailable)" per §6.1 rather than substituting another tier's rate. + If the source model is not in this table (likely for any model released after the table's last update — check the provider's public pricing page if you know current rates), note it as a gap in the Risk Assessment section, label the figure "(estimated from ``)", and estimate using the closest listed model's pricing. ## 6.3 Sum token usage + compute costs (per model pair) @@ -211,6 +216,11 @@ uv run --project python /iam_policy.py \ The script handles the dual-ARN pattern: foundation-model ARNs for plain model IDs and inference-profile ARNs for geo-prefixed IDs (e.g. `us.anthropic.claude-sonnet-4-20250514-v1:0`). The output is a ready-to-use IAM policy JSON file. +**Mantle targets get different actions.** When any target is a proprietary OpenAI GPT model (`openai.gpt-5*`), the script also emits `bedrock-mantle:CreateInference` / `Get*` / `List*` scoped to `arn:aws:bedrock-mantle:::project/*`, plus `bedrock-mantle:CallWithBearerToken` on `*` (AWS does not permit narrowing that one). `bedrock:InvokeModel` alone does **not** authorize these models. Two consequences to carry into the report: + +- For an all-mantle run there is no `BedrockInvokeModelScoped` statement, because those models have no foundation-model or inference-profile ARN to scope to. That absence is correct, not a generation failure. +- Mantle authorizes at project granularity, so the policy cannot be scoped to specific model IDs. If the reader needs to restrict the model set, say so explicitly and point them at a service control policy. + If the account ID is unavailable (run-context `aws_account` is empty), skip this step and note it in the Risk Assessment section as "IAM policy not generated — AWS account ID unavailable". # 7. Generate report @@ -254,11 +264,12 @@ Case 1 — `no_golden_cases: true` is in the evaluator's `notes`: Case 2 — `same_model_family: true — connectivity-only verification` is in the evaluator's `notes`: -> ℹ️ **Connectivity-only verification (same-family migration).** This -> is an Anthropic 1P → Bedrock Claude run; rubric scoring was skipped -> because there is no parameter-surface drift to score. The pass rate -> reflects whether each prompt returned a non-empty response on -> Bedrock, not judge-rated quality. +> ℹ️ **Connectivity-only verification (same-model migration).** This run +> keeps the same model on Bedrock — either Anthropic 1P → Bedrock Claude, +> or OpenAI → the same GPT model on Bedrock. Rubric scoring was skipped +> because the model is unchanged, so there is no parameter-surface drift +> to score. The pass rate reflects whether each prompt returned a +> non-empty response on Bedrock, not judge-rated quality. Case 3 — `live_source_baseline == false`: diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/design-refs/ai-openai-to-bedrock.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/design-refs/ai-openai-to-bedrock.md index 700f38a8..3859fec2 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/design-refs/ai-openai-to-bedrock.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/design-refs/ai-openai-to-bedrock.md @@ -2,251 +2,301 @@ **Applies to:** OpenAI SDK usage detected in GCP-hosted applications → Amazon Bedrock -This file is loaded by `design-ai.md` when `ai-workload-profile.json` has `summary.ai_source` = `"openai"` or `"both"`. It provides model mapping tables with pricing and honest competitive analysis for OpenAI → Bedrock migration decisions. +This file is loaded by `design-ai.md` when `ai-workload-profile.json` has `summary.ai_source` = `"openai"` or +`"both"`. It provides the selection policy and mapping tables for OpenAI → Bedrock migration decisions. -Many GCP-hosted applications use OpenAI's API rather than Vertex AI. This guide covers that migration path. +**Facts live in `references/shared/openai-on-bedrock.md`** — model IDs, endpoint paths, region matrix, quotas, prompt +caching rules, and pricing provenance. Read it before applying this file. Do not restate those facts here. -Verify all pricing via AWS Pricing MCP or `references/shared/pricing-cache.md`. Uses OpenAI Standard tier pricing. - -**Model lifecycle:** Before recommending any Bedrock model, check `references/shared/ai-model-lifecycle.md`. Do not recommend Legacy models as primary selections for new migrations. Legacy models are annotated below where they appear. +**Model lifecycle:** before recommending any Bedrock model, check `references/shared/ai-model-lifecycle.md`. Do not +recommend Legacy models as primary selections for new migrations. --- -## Key Insight: The Landscape Has Changed (April 2026) - -**It is no longer "Bedrock is always cheaper."** It depends on the model. - -- **Bedrock cheaper:** GPT-5.5 flagship (17% cheaper output via Opus 4.6), Nova Lite vs Mini models (85-94%), Nova Micro vs Nano (65-87%), Nova 2 Pro vs Pro models (90-95%), DeepSeek-R1 vs o3 (32%) -- **OpenAI cheaper:** GPT-5.4 (5%), GPT-5.2 (50%), GPT-5.1/5 (40%), GPT-4.1 (43%), GPT-4o (29%), o4-mini/o3-mini/o1-mini (69%) +## Key Insight: OpenAI's Own Models Run on Bedrock -> **GPT-5.5 note (April 23, 2026):** GPT-5.5 doubled pricing to $5/$30 per MTok vs GPT-5.4's $2.50/$15. Claude Opus 4.6 at $5/$25 now matches on input and is **17% cheaper on output**. This reverses the GPT-5.4 dynamic where OpenAI was cheaper — at the GPT-5.5 tier, Bedrock wins on cost. GPT-5.5 uses 40% fewer output tokens on coding tasks (per OpenAI), partially offsetting the price hike for Codex-style workloads. +**The old premise of this guide — "OpenAI models are unavailable on AWS, so migrating means switching model families" +— is obsolete.** GPT-5.6 Sol / Terra / Luna, GPT-5.5, and GPT-5.4 are generally available on Bedrock, at OpenAI's +first-party rates, counting toward existing AWS commitments. ---- - -## Model Mapping Tables +Two consequences that invert the previous decision logic: -### GPT-5.5 Series (Latest — April 23, 2026) +1. **When the source model is on Bedrock, keep it.** A same-model move has no behavior delta to validate, no prompt + re-engineering, and no eval regression risk — the migration is an endpoint and credential change, not a model + change. This is the default recommendation. +2. **Cost is neutral on a same-model move, so it is no longer the argument.** Bedrock charges first-party rates, so + there is no saving _and_ no penalty. The case rests on AWS commitments, IAM/VPC/CloudTrail governance, + data residency, prompt caching, and consolidating one vendor relationship. Never present a same-model migration as + a cost-reduction exercise. -GPT-5.5 is the first fully retrained base model since GPT-4.5. Natively omnimodal (text + image + audio + video), 88.7% SWE-Bench Verified, 256K context in ChatGPT / 1M in API. Two variants: standard and Pro. No Mini/Nano variants at launch (expected Q3 2026). Percentages below are blended savings using a 2:1 input-to-output token ratio. +Cross-family mapping (to Claude / Nova / DeepSeek) is still the right answer in two situations: the source model has +no Bedrock equivalent, or the user's priority is cost and is willing to accept a model change to get it. Both are +covered below — but neither is the default. -| OpenAI Model | Price (in/out per 1M) | Best Bedrock Match | Bedrock Price | Winner | -| ------------ | --------------------- | -------------------- | -------------- | ------------------- | -| GPT-5.5 | $5.00 / $30.00 | Claude Opus 4.6 | $5.00 / $25.00 | Bedrock 17% cheaper | -| GPT-5.5 | $5.00 / $30.00 | Claude Sonnet 4.6 | $3.00 / $15.00 | Bedrock 53% cheaper | -| GPT-5.5 Pro | $30.00 / $180.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 95% cheaper | +--- -> **Token efficiency caveat:** OpenAI reports GPT-5.5 uses ~40% fewer output tokens on Codex-style tasks vs GPT-5.4. Effective cost increase over GPT-5.4 is ~50% (not 100%) for coding workloads. For non-coding workloads, the full 2× price applies. +## Selection Policy -### GPT-5.4 Series +Apply in order. Stop at the first tier that resolves. -Percentages below are blended savings using a 2:1 input-to-output token ratio. GPT-5.4 uses breakpoint pricing at 272K input tokens; rates below assume <272K context. +### Tier 0 — Source model is on Bedrock (default path) -| OpenAI Model | Price (in/out per 1M) | Best Bedrock Match | Bedrock Price | Winner | -| ------------ | --------------------- | -------------------- | -------------- | ------------------- | -| GPT-5.4 | $2.50 / $15.00 | Claude Sonnet 4.6 | $3.00 / $15.00 | OpenAI 5% cheaper | -| GPT-5.4 Mini | $0.75 / $4.50 | Nova Lite | $0.06 / $0.24 | Bedrock 94% cheaper | -| GPT-5.4 Nano | $0.20 / $1.25 | Nova Micro | $0.035 / $0.14 | Bedrock 87% cheaper | -| GPT-5.4 Pro | $30.00 / $180.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 94% cheaper | +If the detected model is GPT-5.6 Sol / Terra / Luna, GPT-5.5, or GPT-5.4, the target is **the same model on +Bedrock**. -### Flagship (GPT-5/5.2 Series) +| Source model | Bedrock target | Model ID | Assessment | +| ------------- | -------------- | ---------------------- | ---------------------------------------- | +| GPT-5.6 Sol | GPT-5.6 Sol | `openai.gpt-5.6-sol` | `strong_migrate` — same model, same rate | +| GPT-5.6 Terra | GPT-5.6 Terra | `openai.gpt-5.6-terra` | `strong_migrate` — same model, same rate | +| GPT-5.6 Luna | GPT-5.6 Luna | `openai.gpt-5.6-luna` | `strong_migrate` — same model, same rate | +| GPT-5.5 | GPT-5.5 | `openai.gpt-5.5` | `strong_migrate` — same model, same rate | +| GPT-5.4 | GPT-5.4 | `openai.gpt-5.4` | `strong_migrate` — same model, same rate | -Percentages below are blended savings using a 2:1 input-to-output token ratio. +Then apply the **region gate** below. Record `migration_path: "mantle_openai_responses"` and +`model_change: false` in `aws-design-ai.json` → `ai_architecture.code_migration`. -| OpenAI Model | Price (in/out per 1M) | Best Bedrock Match | Bedrock Price | Winner | -| --------------- | --------------------- | --------------------- | -------------- | ------------------- | -| GPT-5.2 | $1.75 / $14.00 | Claude Opus 4.7 / 4.6 | $5.00 / $25.00 | OpenAI 50% cheaper | -| GPT-5.1 / GPT-5 | $1.25 / $10.00 | Claude Sonnet 4.6 | $3.00 / $15.00 | OpenAI 40% cheaper | -| GPT-5 Mini | $0.25 / $2.00 | Nova Lite | $0.06 / $0.24 | Bedrock 86% cheaper | -| GPT-5 Nano | $0.05 / $0.40 | Nova Micro | $0.035 / $0.14 | Bedrock 58% cheaper | +Report the assessment honestly: `strong_migrate` here means "low-risk, well-supported move", not "cheaper". -### Pro Models (Extended Reasoning) +### Region gate (applies to every Tier 0 selection) -> **Lifecycle note:** Nova Premier v1 is **Legacy** (EOL Sep 14, 2026). Nova 2 Pro (Preview) is the Active successor for reasoning-heavy workloads. Pricing differs — see `pricing-cache.md`. +These models are **in-region only** — no Geo or Global cross-region inference, so there is no fallback region. +Check `preferences.json` → `design_constraints.target_region` against the region matrix in +`shared/openai-on-bedrock.md`. -| OpenAI Model | Price (in/out per 1M) | Best Bedrock Match | Bedrock Price | Winner | -| ------------ | --------------------- | -------------------- | -------------- | ------------------- | -| GPT-5.5 Pro | $30.00 / $180.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 95% cheaper | -| GPT-5.4 Pro | $30.00 / $180.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 94% cheaper | -| GPT-5.2 Pro | $21.00 / $168.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 93% cheaper | -| GPT-5 Pro | $15.00 / $120.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 90% cheaper | +If the target region does not carry the selected model: -### GPT-4.1 Series +1. Prefer offering a **region change** to a supported region, if the migration has no data-residency constraint + pinning it elsewhere. This preserves the zero-behavior-delta benefit, which is the whole point of Tier 0. +2. If the region is fixed, Tier 0 is unavailable. Fall through to Tier 1 and say why explicitly — the user needs to + know they are taking a model change because of a region constraint, not because of a model judgment. -| OpenAI Model | Price (in/out per 1M) | Best Bedrock Match | Bedrock Price | Winner | -| ------------ | --------------------- | ------------------ | -------------- | ------------------- | -| GPT-4.1 | $2.00 / $8.00 | Claude Sonnet 4.6 | $3.00 / $15.00 | OpenAI 43% cheaper | -| GPT-4.1 Mini | $0.40 / $1.60 | Nova Lite | $0.06 / $0.24 | Bedrock 85% cheaper | -| GPT-4.1 Nano | $0.10 / $0.40 | Nova Micro | $0.035 / $0.14 | Bedrock 65% cheaper | +Record the outcome in `regional_warnings[]`. -### GPT-4o Series +### Tier 1 — Source model is not on Bedrock (present two options, do not pre-pick) -| OpenAI Model | Price (in/out per 1M) | Best Bedrock Match | Bedrock Price | Winner | -| ------------ | --------------------- | ------------------ | -------------- | ------------------- | -| GPT-4o | $2.50 / $10.00 | Claude Sonnet 4.6 | $3.00 / $15.00 | OpenAI 29% cheaper | -| GPT-4o Mini | $0.15 / $0.60 | Nova Lite | $0.06 / $0.24 | Bedrock 60% cheaper | +Applies to GPT-4o, GPT-4.1, GPT-4 / GPT-4 Turbo, GPT-3.5 Turbo, the o-series, GPT-5 / 5.1 / 5.2, and all `*-Pro` +and `*-Mini` / `*-Nano` variants — none of which are on Bedrock. -### Reasoning Models (o-series) +There is no same-model target, so a model change is unavoidable. **Present both options with their trade-offs and +let the user choose.** Do not silently default to either. -> **Lifecycle note:** Nova Premier v1 is **Legacy** (EOL Sep 14, 2026). Table below uses Nova 2 Pro (Preview) as the Active replacement. +| Option | What it means | Trade-off | +| ----------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| **A) Stay with OpenAI, newer tier** | Move to the nearest GPT tier on Bedrock (table below) | Same vendor, same prompt idioms and tool-calling semantics; still a generation change needing eval | +| **B) Cross-family** | Move to Claude / Nova / DeepSeek (tables further below) | Often materially cheaper; different prompt behavior, so budget for prompt work and eval | -| OpenAI Model | Price (in/out per 1M) | Best Bedrock Match | Bedrock Price | Winner | -| --------------------------- | --------------------- | -------------------- | -------------- | ------------------- | -| o1-pro | $150.00 / $600.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 98% cheaper | -| o3-pro | $20.00 / $80.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 87% cheaper | -| o1 | $15.00 / $60.00 | Nova 2 Pro (Preview) | $1.38 / $11.00 | Bedrock 83% cheaper | -| o3 | $2.00 / $8.00 | DeepSeek-R1 | $1.35 / $5.40 | Bedrock 32% cheaper | -| o4-mini / o3-mini / o1-mini | $1.10 / $4.40 | Claude Sonnet 4.6 | $3.00 / $15.00 | OpenAI 69% cheaper | +Option A tier mapping: -### Legacy Models +| Source tier | Bedrock GPT target | +| -------------------------------------------------------- | ------------------ | +| Flagship reasoning (o1-pro, o3-pro, GPT-5 Pro, `*-Pro`) | GPT-5.6 Sol | +| Flagship general (GPT-4o, GPT-4.1, GPT-5/5.1/5.2, GPT-4) | GPT-5.6 Terra | +| Mid / reasoning (o1, o3, o4-mini) | GPT-5.6 Terra | +| Fast / cheap (`*-mini`, `*-nano`, GPT-3.5 Turbo) | GPT-5.6 Luna | -| OpenAI Model | Price (in/out per 1M) | Best Bedrock Match | Bedrock Price | Winner | -| ------------- | --------------------- | ------------------ | -------------- | ----------------------------------------- | -| GPT-4 Turbo | $10.00 / $30.00 | Claude Sonnet 4.6 | $3.00 / $15.00 | Bedrock 58% cheaper | -| GPT-4 | $30.00 / $60.00 | Claude Sonnet 4.6 | $3.00 / $15.00 | Bedrock 82% cheaper | -| GPT-3.5 Turbo | $0.50 / $1.50 | Llama 4 Maverick | $0.24 / $0.97 | Bedrock 42% cheaper + much better quality | +Option A is subject to the same region gate as Tier 0. -### OpenAI Models on Bedrock (gpt-oss) +### Tier 2 — Cost priority override -OpenAI's open-source models are available directly on Bedrock, enabling migration without switching model families: +If `ai_constraints.ai_priority == "cost"`, present the cheapest adequate Bedrock target **as an explicit +alternative alongside** the Tier 0 / Tier 1 recommendation — not as a replacement for it. The user asked for a +migration, and swapping model families is a bigger change than they may have signed up for; make the trade visible +rather than deciding for them. -| OpenAI Model | Price (in/out per 1M) | Bedrock gpt-oss | Bedrock Price | Notes | -| ------------ | --------------------- | --------------- | ------------- | ------------------------------------- | -| GPT-4o Mini | $0.15 / $0.60 | gpt-oss-120b | $0.15 / $0.60 | Same cost, runs on AWS infrastructure | -| GPT-5 Nano | $0.05 / $0.40 | gpt-oss-20b | $0.07 / $0.30 | Similar budget tier on AWS | +See "Cost-optimized cross-family alternatives" below for the numbers. -This path avoids model-family risk: the application stays on OpenAI-architecture models while consolidating on AWS infrastructure. +--- -_Percentages are blended savings using a 2:1 input-to-output token ratio. Actual savings depend on your input/output ratio._ +## Cost-Optimized Cross-Family Alternatives + +All figures per 1M tokens. Percentages are blended savings at a 2:1 input-to-output ratio. **Both sides of these +comparisons are now Bedrock prices** where the OpenAI model is on Bedrock — this is a Bedrock-vs-Bedrock model +choice, not a provider comparison. Verify rates via `shared/pricing-cache.md` or the AWS Pricing MCP, noting that +the Pricing MCP does not carry GPT-5.x (see `shared/openai-on-bedrock.md`). + +### Against Tier 0 models (same-model baseline vs cross-family) + +| Bedrock GPT baseline | Price | Cross-family alternative | Price | Delta | +| -------------------- | ------------ | ------------------------ | ------------ | -------------------------------- | +| GPT-5.5 | 5.00 / 30.00 | Claude Sonnet 4.6 | 3.00 / 15.00 | Sonnet 48% cheaper | +| GPT-5.5 | 5.00 / 30.00 | Claude Opus 4.6 | 5.00 / 25.00 | Opus 13% cheaper | +| GPT-5.4 | 2.50 / 15.00 | Claude Sonnet 4.6 | 3.00 / 15.00 | GPT-5.4 5% cheaper — near parity | +| GPT-5.6 Luna | 0.20 / 1.20 | Claude Haiku 4.5 | 1.00 / 5.00 | **Luna 77% cheaper** | +| GPT-5.6 Luna | 0.20 / 1.20 | Nova Lite | 0.06 / 0.24 | Nova Lite 77% cheaper | +| GPT-5.6 Luna | 0.20 / 1.20 | Nova Micro | 0.035 / 0.14 | Nova Micro 87% cheaper | + +Two findings worth surfacing to users: + +- **GPT-5.6 Luna undercuts Claude Haiku 4.5 by ~77%.** For the fast/cheap tier, the OpenAI model is now the cheaper + Bedrock option. Do not reflexively map a cheap OpenAI model to Haiku on cost grounds. +- **GPT-5.4 and Sonnet 4.6 are within 5%.** At that spread, cost is noise; choose on capability and on whether a + model change is acceptable. + +GPT-5.6 Terra and Sol rates are unverified (see `shared/openai-on-bedrock.md`) — do not publish a comparison row +for them until the rates are read off the pricing page. + +### Sources with no Bedrock equivalent (cross-family reference) + +Used for Tier 1 Option B. Source prices are OpenAI's own; Bedrock prices are the target. + +| OpenAI Model | Source price | Cross-family target | Bedrock price | Delta | +| --------------------------- | --------------- | -------------------- | ------------- | ---------------------------------------- | +| GPT-5.5 Pro | 30.00 / 180.00 | Nova 2 Pro (Preview) | 1.375 / 11.00 | Bedrock 94% cheaper | +| GPT-5.4 Pro | 30.00 / 180.00 | Nova 2 Pro (Preview) | 1.375 / 11.00 | Bedrock 94% cheaper | +| GPT-5.2 Pro | 21.00 / 168.00 | Nova 2 Pro (Preview) | 1.375 / 11.00 | Bedrock 93% cheaper | +| GPT-5 Pro | 15.00 / 120.00 | Nova 2 Pro (Preview) | 1.375 / 11.00 | Bedrock 91% cheaper | +| o1-pro | 150.00 / 600.00 | Nova 2 Pro (Preview) | 1.375 / 11.00 | Bedrock 98% cheaper | +| o3-pro | 20.00 / 80.00 | Nova 2 Pro (Preview) | 1.375 / 11.00 | Bedrock 89% cheaper | +| o1 | 15.00 / 60.00 | Nova 2 Pro (Preview) | 1.375 / 11.00 | Bedrock 85% cheaper | +| o3 | 2.00 / 8.00 | DeepSeek-R1 | 1.35 / 5.40 | Bedrock 32% cheaper | +| o4-mini / o3-mini / o1-mini | 1.10 / 4.40 | GPT-5.6 Luna | 0.20 / 1.20 | Bedrock 76% cheaper | +| GPT-5.2 | 1.75 / 14.00 | Claude Sonnet 4.6 | 3.00 / 15.00 | Source 17% cheaper | +| GPT-5.1 / GPT-5 | 1.25 / 10.00 | Claude Sonnet 4.6 | 3.00 / 15.00 | Source 40% cheaper | +| GPT-5 Mini | 0.25 / 2.00 | Nova Lite | 0.06 / 0.24 | Bedrock 86% cheaper | +| GPT-5 Nano | 0.05 / 0.40 | Nova Micro | 0.035 / 0.14 | Bedrock 58% cheaper | +| GPT-4.1 | 2.00 / 8.00 | Claude Sonnet 4.6 | 3.00 / 15.00 | Source 43% cheaper | +| GPT-4.1 Mini | 0.40 / 1.60 | Nova Lite | 0.06 / 0.24 | Bedrock 85% cheaper | +| GPT-4.1 Nano | 0.10 / 0.40 | Nova Micro | 0.035 / 0.14 | Bedrock 65% cheaper | +| GPT-4o | 2.50 / 10.00 | Claude Sonnet 4.6 | 3.00 / 15.00 | Source 29% cheaper | +| GPT-4o Mini | 0.15 / 0.60 | Nova Lite | 0.06 / 0.24 | Bedrock 60% cheaper | +| GPT-4 Turbo | 10.00 / 30.00 | Claude Sonnet 4.6 | 3.00 / 15.00 | Bedrock 58% cheaper | +| GPT-4 | 30.00 / 60.00 | Claude Sonnet 4.6 | 3.00 / 15.00 | Bedrock 82% cheaper | +| GPT-3.5 Turbo | 0.50 / 1.50 | GPT-5.6 Luna | 0.20 / 1.20 | Bedrock 36% cheaper + far better quality | + +For the rows where the source is cheaper (GPT-5.2, GPT-5.1/5, GPT-4.1, GPT-4o), note that these models are on a +vendor deprecation path anyway; Option A (GPT-5.6 on Bedrock) is usually the better framing than defending a stale +model on price. + +### Open-weight gpt-oss + +`openai.gpt-oss-120b` (0.15 / 0.60) and `openai.gpt-oss-20b` (0.07 / 0.30) remain available and, unlike the +proprietary GPT models, **do** support `bedrock-runtime` / Converse. They sit a capability class below the GPT-5.x +frontier tier. Recommend them when the user wants OpenAI-architecture models on the Bedrock-native runtime surface +(for Guardrails, invocation logging, or Converse-based tooling) rather than through mantle. --- ## Migration Decision Framework -**Migrate to Bedrock if:** +**Migrate to Bedrock, same model (Tier 0), if:** the source is GPT-5.6 / 5.5 / 5.4 and the target region carries it. +This is the common case and needs no cost justification. + +**Migrate to Bedrock, model change (Tier 1/2), if:** -- Using GPT-5.5 flagship → Bedrock 17% cheaper on output via Opus 4.6 ($5/$25 vs $5/$30); Sonnet 4.6 is 53% cheaper -- Using Pro/expensive models (GPT-5.5 Pro, GPT-5.4 Pro, o1-pro) → 87-98% savings via Nova 2 Pro -- Using Mini/Nano models at high volume → 87-94% savings via Nova Lite/Micro -- Using legacy GPT-4/3.5 → 42-82% savings -- Need AWS infrastructure integration -- Need prompt caching (Claude only, 90% savings on cached content) -- Using o3 for reasoning → DeepSeek-R1 on Bedrock is 32% cheaper -- Want to stay on OpenAI models → gpt-oss on Bedrock (same models, AWS infrastructure) +- The source model is not on Bedrock (see the region and catalog lists in `shared/openai-on-bedrock.md`) +- The user's priority is cost and they accept a model change — note that the cheapest option is often Nova, and that + GPT-5.6 Luna already beats Claude Haiku 4.5 on price +- Bedrock-native features are required that mantle does not expose (Guardrails, Knowledge Bases, invocation logging, + Converse-based tooling) — these need a Bedrock-native model or gpt-oss -**Consider staying on OpenAI if:** +**Consider staying on OpenAI's own API only if:** -- Using GPT-5.5 for omnimodal (audio/video) → Claude is text+image only; GPT-5.5 has native audio/video -- Using GPT-5.4 flagship → only 5% cheaper than Sonnet 4.6; marginal either way -- Using mid-tier flagships (GPT-5, GPT-4.1, o3, o4-mini) → OpenAI 29-69% cheaper -- Low volume (<$500/mo) where absolute savings are small -- Heavily integrated with OpenAI ecosystem (Assistants API, gpt-image, Whisper, Realtime) -- Need Realtime API (no Bedrock equivalent) +- The target region is fixed and carries no suitable model, and a model change is unacceptable +- The workload needs the **Realtime API** — no Bedrock equivalent +- The workload depends on **gpt-image**, **Whisper**, **TTS**, or **OpenAI embeddings** — these map to other AWS + services (Stability AI, Transcribe, Polly, Titan Embeddings), not to an OpenAI model on Bedrock +- The workload uses **Assistants API with file search, vector stores, or code interpreter** — see the decision tree + below +- Chat Completions cannot be reshaped to Responses and probing confirms Chat Completions is unsupported -**Analyze carefully:** Calculate actual token usage x model-specific pricing. Small % differences matter at scale. +**Cost is not on that list.** Since Bedrock charges first-party rates, "staying on OpenAI to save money" is not a +valid conclusion for any model in the Bedrock catalog. --- ## Feature Migration -| OpenAI Feature | Bedrock Equivalent | Notes | -| -------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| OpenAI SDK (direct) | Mantle OpenAI-compat endpoints | Zero code changes — set `OPENAI_BASE_URL` + API key + model ID | -| Function calling | Claude tools (excellent, similar format) | Minimal changes (works via Mantle or Converse API) | -| Streaming | All major models | Verify gateway format | -| Vision (GPT-4V) | Claude Sonnet/Haiku, Llama 4 Maverick | 70-95% cheaper | -| Embeddings (ada-002) | Titan Embeddings ($0.02/1M, 1536 dims) | Must re-embed all docs | -| DALL-E / gpt-image | Nova Canvas ($0.04-$0.08/img) | DALL-E EOL May 12, 2026; OpenAI replacement is gpt-image-1.5; Titan Image Gen v2 is Legacy (EOL Jun 30, 2026); use Nova Canvas | -| Whisper (STT) | Amazon Transcribe ($0.024/min) | 4x more expensive but more features | -| TTS | Amazon Polly | Different pricing model | -| Assistants API | See Assistants API decision tree below | Path depends on which Assistants features are used — see decision tree | -| JSON mode | Claude (excellent), Nova Pro (good) | Most models via prompt | -| Realtime API | No equivalent | Stay on OpenAI for this | +| OpenAI Feature | Bedrock Equivalent | Notes | +| ------------------------------------------ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| OpenAI SDK (direct) | Same model on mantle Responses API | Base URL + credential + model ID change; see `shared/openai-on-bedrock.md` for the `/openai/v1` path | +| Responses API | Mantle Responses API | Closest to a drop-in; still verify the path and model ID | +| Chat Completions | Reshape to Responses | Unverified for GPT-5.x — probe before committing | +| Function calling | Supported on GPT-5.x via mantle; Claude tools via Converse | Same-model keeps tool semantics identical | +| Reasoning effort | `reasoning={"effort": ...}` on mantle | `none` / `low` / `medium` / `high` / `xhigh` / `max` | +| Prompt caching | GPT-5.6 only (90% off cached input) | Not listed for GPT-5.5 / 5.4; Claude has its own caching | +| Streaming | Supported | Verify per surface | +| Vision | GPT-5.x (image input) or Claude / Llama 4 | Same-model preserves behavior | +| Embeddings (ada-002, `text-embedding-3-*`) | Titan Embeddings v2 | No OpenAI embedding model on Bedrock; must re-embed all documents | +| DALL-E / gpt-image | Stability AI | Nova Canvas v1 is Legacy; see `ai-model-lifecycle.md` | +| Whisper (STT) | Amazon Transcribe | Different service, API, and pricing model | +| TTS | Amazon Polly / Nova 2 Sonic | Different pricing model | +| Assistants API | See decision tree below | Path depends on which features are used | +| Realtime API | No equivalent | Stay on OpenAI for this | +| Codex | Codex on Bedrock (GA) | Pay-per-token, inference through Bedrock, counts toward AWS commitments | +| Guardrails / KB / invocation logging | Bedrock-native model or gpt-oss | Not available through the mantle GPT path | --- ## Common Migration Paths -### OpenAI SDK → Mantle (minimal code changes) - -If the application uses the OpenAI Python/JS SDK directly (`from openai import OpenAI` / `new OpenAI()`), Bedrock's [Mantle OpenAI-compatible endpoints](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html) allow migration with minimal code changes — primarily environment variables plus a model string swap: - -1. Set `OPENAI_BASE_URL=https://bedrock-mantle.{region}.api.aws/v1` -2. Set `OPENAI_API_KEY=` — use a Bedrock API key, **not** your existing OpenAI API key -3. Change model string (e.g., `gpt-5.4` → `anthropic.claude-sonnet-4-6` or `openai.gpt-oss-120b`) - -**Hard gates before recommending Mantle:** +### Same model on Bedrock (Tier 0) — smallest possible change -- **Model compatibility:** Verify the selected Bedrock model supports the Responses API — [check API compatibility](https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html). Not all models do. Do not recommend Mantle Responses API unless the target model is confirmed compatible. -- **Region availability:** Mantle is available in 13 regions (us-east-1, us-east-2, us-west-2, ap-northeast-1, ap-south-1, ap-southeast-2, ap-southeast-3, eu-central-1, eu-west-1, eu-west-2, eu-south-1, eu-north-1, sa-east-1). If the target region is outside this list, do not recommend Mantle — use the boto3 Converse API path instead. +The application keeps the OpenAI SDK and the same model. Changes are limited to: -Supports Chat Completions API, Responses API, streaming, and stateful conversations. +1. Base URL → `https://bedrock-mantle.{region}.api.aws/openai/v1` (note the `openai/v1` segment) +2. Credentials → Bedrock API key or the auto-refreshing `BedrockOpenAI` token provider, **not** an OpenAI key +3. Model ID → the `openai.gpt-*` form +4. IAM → `bedrock-mantle:*` actions, e.g. via `AmazonBedrockMantleInferenceAccess` -**Responses API capabilities (when stateful conversations matter):** +Full snippet and prerequisites in `shared/openai-on-bedrock.md`. No prompt changes, no eval regression expected — +but still run the eval harness to confirm, because infrastructure and tokenizer-adjacent behavior can differ. -- **Stateful conversation management** — Bedrock rebuilds context automatically; no need to pass full conversation history on each request -- **Async / long-running inference** — background processing for workloads that exceed typical request timeouts (useful for complex agentic tasks) -- **Streaming + non-streaming** — both modes supported via the same endpoint +### Assistants API → migration decision tree -### Assistants API → Migration Decision Tree +Assistants API and Responses API are different surfaces. Do not treat all Assistants usage as a config-only change. -Assistants API and Responses API are different surfaces. Do not treat all Assistants API usage as an env-var-only migration. Apply this decision tree: +**1. App already uses the OpenAI Responses API** (`responses.create`) +→ Cleanest path. Base URL + credential + model ID change. -**1. App already uses OpenAI Responses API** (`responses.create`) -→ Mantle is the cleanest path. Env var swap + model string change. Minimal code changes. +**2. App uses Assistants API only for stateful multi-turn conversation** (no hosted tools, no file search, no code +interpreter, no persistent Assistant objects, no complex run lifecycle) +→ Mantle Responses is viable. Requires migrating `threads`/`runs` to `responses.create` — a small API migration +(days), not a redesign. Not a zero-code change. -**2. App uses Assistants API only for stateful multi-turn conversation** (no hosted tools, no file search, no code interpreter, no persistent Assistant objects, no complex run lifecycle) -→ Mantle Responses API is viable. Requires migrating from `threads`/`runs` calls to `responses.create` — this is a small API migration (days), not a full redesign. Not a zero-code-change swap. +**3. App uses Assistants API with simple hosted tools** (function calling only) +→ Mantle Responses with tool use is viable. Moderate migration (1–2 weeks) to adapt tool definitions and the run +lifecycle. -**3. App uses Assistants API with simple hosted tools** (function calling only, no file search or code interpreter) -→ Mantle Responses API with tool use is viable. Moderate code migration (1-2 weeks) to adapt tool definitions and run lifecycle. +**4. App uses Assistants API with file search, vector stores, code interpreter, persistent Assistant objects, or +complex run lifecycle management** +→ Do not recommend mantle. Evaluate Bedrock AgentCore (Harness sessions/memory, action groups as MCP tools via +Gateway, gateway-fronted knowledge bases) for a full agentic replacement (2–4 weeks), or app-managed orchestration +if the team prefers to own state. Never target classic Bedrock Agents (`bedrock-agent`) — it is in maintenance mode +and closed to new customers as of July 30, 2026. -**4. App uses Assistants API with file search, vector stores, code interpreter, persistent Assistant objects, or complex run lifecycle management** -→ Do not recommend Mantle. Evaluate: Bedrock AgentCore (Harness sessions/memory, action groups as MCP tools via Gateway, gateway-fronted knowledge bases) for full agentic replacement (2-4 week migration), or app-managed orchestration if the team prefers to own state. Never target classic Bedrock Agents (`bedrock-agent`): it is in maintenance mode and closed to new customers as of July 30, 2026. +### When to prefer a Bedrock-native model over the mantle GPT path -**When to prefer Converse API over Mantle:** If you need Bedrock-specific features (Guardrails, Knowledge Bases, prompt caching, AgentCore integration) or your target region doesn't have Mantle. Mantle is the fastest path; Converse API is the most feature-complete path. +Choose Claude / Nova via Converse when the workload needs Bedrock Guardrails, Knowledge Bases, invocation logging, +cross-region inference, or a region the GPT models do not serve. The mantle GPT path is the smallest change; the +Bedrock-native path is the most feature-complete. -### GPT-5.4 → Claude Sonnet 4.6 +### High-spend tiering -Near price parity (~5% difference). Migration case is driven by AWS consolidation, agentic reliability, or prompt caching — not cost. Both have ~200K+ context. Low risk. - -### GPT-5.4 Mini/Nano → Nova Lite/Micro - -87-94% savings. Strong cost case at any volume. Nova Lite (300K context) covers most GPT-5.4 Mini use cases. - -### GPT-4/4 Turbo → Claude Sonnet 4.6 - -70-90% savings, similar or better quality, longer context (200K vs 128K). Low risk. - -### GPT-3.5 Turbo → Llama 4 Maverick - -Similar cost, dramatically better quality, 1M context (vs 16K). - -### GPT-4 → Multi-Model (high spend) - -Tier by complexity: simple → Nova Micro/Llama 4 Scout (60%), moderate → Llama 4 Maverick/Nova Pro (30%), complex → Claude Sonnet (10%). 85-95% savings. - -### Pro models → Nova 2 Pro - -83-98% savings. Strong migration case at any volume. (Nova Premier v1 is Legacy — use Nova 2 Pro instead.) +Tier by task complexity: simple (60%) → Nova Micro / GPT-5.6 Luna, moderate (30%) → Nova Pro / GPT-5.6 Terra, +complex (10%) → Claude Sonnet 4.6 / GPT-5.6 Sol. Mixing families across tiers is fine, but note that mantle and +`bedrock-runtime` are different surfaces — a tiered router spanning both needs two client paths. --- ## Volume-Based Recommendations -**Low (<1M tokens/day):** Use best model for quality. Cost difference minimal. - -**Medium (1-10M tokens/day):** Present cost comparison at volume. At 5M input + 2.5M output/day, evaluate per-model economics carefully. +**Low (<1M tokens/day):** use the best model for quality. Cost difference is immaterial. -**High (10-100M tokens/day):** Multi-model tiered approach recommended. Route by task complexity. +**Medium (1–10M tokens/day):** present the cost comparison. For Tier 0 the comparison is against the cross-family +alternative, not against the source provider. -**Very high (>100M tokens/day):** Mandatory tiering: +**High (10–100M tokens/day):** consider tiering. Enable GPT-5.6 prompt caching first — cached input is both 90% +cheaper and exempt from the input-TPM quota, which relieves the binding constraint on mantle. -- Simple tasks (60%) → Nova Micro or Llama 4 Scout -- Moderate tasks (30%) → Llama 4 Maverick or Nova Pro -- Complex tasks (10%) → Claude Sonnet 4.6 +**Very high (>100M tokens/day):** tiering plus caching. Check TPM headroom per model per region early; there is no +RPM quota and no `bedrock-runtime` fallback for GPT models, so quota increases are the scaling lever. See +`shared/ai-migration-guardrails.md`. --- ## OpenAI Pricing Tiers -OpenAI offers 4 tiers: Batch (50% off, 24hr), Flex (30-50% off, higher latency), Standard (baseline), Priority (2x, lowest latency). This guide uses Standard tier for comparison. +OpenAI's own API offers Batch (50% off, 24hr), Flex (30–50% off, higher latency), Standard (baseline), and Priority +(2x, lowest latency). Source-side figures in this file use Standard. Bedrock exposes Standard / Priority / Flex / +Reserved service tiers, but per-model tier support renders as empty cells in the AWS docs — verify before +recommending Flex or Reserved as a cost lever. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/clarify/clarify-ai-only.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/clarify/clarify-ai-only.md index 681cceb5..84b83716 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/clarify/clarify-ai-only.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/clarify/clarify-ai-only.md @@ -159,23 +159,26 @@ Establishes baseline Bedrock recommendation. Override hierarchy: Q10 special fea _Skip when:_ `models[].model_id` is populated in `ai-workload-profile.json` — auto-detect from detected model IDs with `chosen_by: "extracted"` and do not present this question. The detected models are already shown in the Step 1 summary. -> A) Gemini Flash | B) Gemini Pro | C) GPT-3.5 Turbo | D) GPT-4/4 Turbo | E) GPT-4o | F) GPT-5.4/Mini/Nano | G) GPT-5/5.x (older) | H) GPT-5.5/Pro | I) o-series | J) Claude (Anthropic SDK) | K) Other/Multiple | L) Don't know - -| Source | Baseline Recommendation | Pricing Context | -| -------------- | --------------------------------- | ---------------------------------- | -| Gemini Flash | Claude Haiku 4.5 ($1/$5) | Strong savings | -| Gemini Pro | Claude Sonnet 4.6 ($3/$15) | Comparable tier | -| GPT-3.5 Turbo | Claude Haiku 4.5 ($1/$5) | Faster and cheaper | -| GPT-4/4 Turbo | Claude Sonnet 4.6 ($3/$15) | Major savings (GPT-4T: $10/$30) | -| GPT-4o | Claude Sonnet 4.6 ($3/$15) | Modest savings on output | -| GPT-5.4 | Claude Sonnet 4.6 ($3/$15) | ~5% cheaper on OpenAI; near parity | -| GPT-5.4 Mini | Nova Lite ($0.06/$0.24) | 94% cheaper on Bedrock | -| GPT-5.4 Nano | Nova Micro ($0.035/$0.14) | 87% cheaper on Bedrock | -| GPT-5.4 Pro | Nova 2 Pro ($1.38/$11) | 94% cheaper on Bedrock | -| GPT-5/5.x | Claude Sonnet 4.6 ($3/$15) | Savings story is quality, not cost | -| GPT-5 flagship | Claude Opus 4.7 / 4.6 ($5/$25) | Cheaper than GPT-5 Pro ($15/$120) | -| o-series | Sonnet 4.6 with extended thinking | o1 $15/$60 → significant savings | -| Claude (any) | Same model on Bedrock | Client swap only — no model change | +> A) Gemini Flash | B) Gemini Pro | C) GPT-3.5 Turbo | D) GPT-4/4 Turbo | E) GPT-4o | F) GPT-5.4/Mini/Nano | F2) GPT-5.6 Sol/Terra/Luna | G) GPT-5/5.x (older) | H) GPT-5.5/Pro | I) o-series | J) Claude (Anthropic SDK) | K) Other/Multiple | L) Don't know + +| Source | Baseline Recommendation | Pricing Context | +| ------------- | -------------------------------- | ---------------------------------- | +| Gemini Flash | Claude Haiku 4.5 ($1/$5) | Strong savings | +| Gemini Pro | Claude Sonnet 4.6 ($3/$15) | Comparable tier | +| GPT-5.6 (any) | **Same model on Bedrock** | Provider list price — cost neutral | +| GPT-5.5 | **Same model on Bedrock** | Provider list price — cost neutral | +| GPT-5.4 | **Same model on Bedrock** | Provider list price — cost neutral | +| GPT-3.5 Turbo | GPT-5.6 Luna; or Haiku 4.5 | Luna 36% cheaper, 77% under Haiku | +| GPT-4/4 Turbo | GPT-5.6 Terra; or Sonnet 4.6 | Not on Bedrock — offer both | +| GPT-4o | GPT-5.6 Terra; or Sonnet 4.6 | Not on Bedrock — offer both | +| GPT-5.4 Mini | GPT-5.6 Luna; or Nova Lite | Mini not on Bedrock — offer both | +| GPT-5.4 Nano | GPT-5.6 Luna; or Nova Micro | Nano not on Bedrock — offer both | +| GPT-5.x Pro | GPT-5.6 Sol; or Nova 2 Pro | Pro not on Bedrock — offer both | +| GPT-5/5.1/5.2 | GPT-5.6 Terra; or Sonnet 4.6 | Not on Bedrock — offer both | +| o-series | GPT-5.6 Sol/Terra; or Sonnet 4.6 | Not on Bedrock — offer both | +| Claude (any) | Same model on Bedrock | Client swap only — no model change | + +**Same-model rows first.** GPT-5.6 / 5.5 / 5.4 run on Bedrock at OpenAI's first-party rates, so those sources map to themselves and the cost delta is zero — the case is AWS commitments, governance, and residency, not savings. They are `bedrock-mantle` / Responses-only and in-region only; see `references/shared/openai-on-bedrock.md`. For sources with no Bedrock equivalent, present both a same-vendor upgrade and a cross-family option rather than pre-picking. Override examples: GPT-4 + Q2=cost → Haiku; Flash + Q10=extended thinking → Sonnet; GPT-4o + Q10=speech → Nova 2 Sonic; GPT-5.5 + Q2=cost → Sonnet 4.6. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/clarify/clarify-ai.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/clarify/clarify-ai.md index 5ddc42cf..67364b17 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/clarify/clarify-ai.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/clarify/clarify-ai.md @@ -283,7 +283,9 @@ _Skip when:_ Primary model fully resolved from discovery. Use detected value wit **Cold path — family first, then branch:** When nothing was detected, do NOT present the full option list below in one message. Ask a short family question first — "Which provider family is your primary model? 1. Gemini 2. OpenAI GPT 3. OpenAI o-series (reasoning) 4. Other / not sure" — then present only the matching subset of the options below. The full list remains the reference catalog for interpretation. -Establishes baseline Bedrock recommendation. **Override hierarchy:** Q17 special features (hard override) > Q16 priority > Q18/Q21 volume and latency > Q19 source model (baseline only). +Establishes baseline Bedrock recommendation. **Override hierarchy:** Q17 special features (hard override) > same-model availability on Bedrock > Q16 priority > Q18/Q21 volume and latency > Q19 source model (baseline only). + +**Same-model availability outranks Q16.** When Q19 identifies a model that is available on Bedrock (GPT-5.6 Sol / Terra / Luna, GPT-5.5, GPT-5.4 — see `references/shared/openai-on-bedrock.md`), the recommendation is that same model, and a `balanced`/unset Q16 must not move it to another family. `Q16 = cost` adds a cheaper alternative alongside it rather than replacing it. Only a Q17 hard feature override the source model cannot serve displaces it. > A) Gemini 3.5 Flash (GA — current flagship Flash model) > B) Gemini 3.5 Flash Thinking (thinking budget enabled) @@ -302,8 +304,9 @@ Establishes baseline Bedrock recommendation. **Override hierarchy:** Q17 special > O) GPT-5 / GPT-5.x (older) > P) GPT-5.5 / GPT-5.5 Pro > Q) o-series (o1, o3) -> R) Other / Multiple models -> S) I don't know +> R) GPT-5.6 Sol / Terra / Luna +> S) Other / Multiple models +> T) I don't know | Source Model | Baseline Bedrock Recommendation | Pricing Context | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -316,21 +319,21 @@ Establishes baseline Bedrock recommendation. **Override hierarchy:** Q17 special | Gemini 3 Pro / 3.1 Pro | Claude Sonnet 4.6 ($3/$15) — agentic reliability; or Nova 2 Pro ($1.38/$11) — cost | Gemini 3.1 Pro is $2/$12 — cheaper than Sonnet 4.6; migration case is agentic reliability and AWS ecosystem, NOT cost. Be honest: Gemini 3.1 Pro leads on general benchmarks. | | Gemini Flash 1.5 / 2.0 (older) | Nova Lite ($0.06/$0.24) or Nova Micro ($0.035/$0.14) — **flag Gemini 1.5 Flash as EOL (Sep 2025); recommend upgrading source model to 3.5 Flash before or alongside migration** | Strong Bedrock cost savings; 1.5 Flash is past EOL so migration is doubly urgent | | Gemini Pro 1.5 (older) | Claude Sonnet 4.6 ($3/$15) — **flag Gemini 1.5 Pro as EOL (Sep 2025); recommend upgrading source model to 3.1 Pro before or alongside migration** | 1.5 Pro is past EOL; migration to Bedrock and source model upgrade should be planned together | -| GPT-3.5 Turbo | Claude Haiku 4.5 ($1/$5) — cost-equivalent | Haiku is faster and cheaper | -| GPT-4 / GPT-4 Turbo | Claude Sonnet 4.6 ($3/$15) — quality equivalent | Major savings: GPT-4 Turbo is $10/$30 vs Sonnet $3/$15 | -| GPT-4o | Claude Sonnet 4.6 ($3/$15) — performance equivalent | Modest savings on output; input slightly higher on Bedrock | -| GPT-5.4 | Claude Sonnet 4.6 ($3/$15) — near price parity | GPT-5.4 is $2.50/$15 — ~5% cheaper; migration case is AWS consolidation, not cost | -| GPT-5.4 Mini | Nova Lite ($0.06/$0.24) — massive cost savings | 94% cheaper on Bedrock; strong migration case | -| GPT-5.4 Nano | Nova Micro ($0.035/$0.14) — massive cost savings | 87% cheaper on Bedrock; strong migration case | -| GPT-5.4 Pro | Nova 2 Pro ($1.38/$11) — flagship reasoning on AWS | 94% cheaper on Bedrock; strongest migration case | -| GPT-5 / GPT-5.x (older) | Claude Sonnet 4.6 ($3/$15) — performance equivalent | GPT-5 is $1.25/$10 — savings story is quality/features, not cost | -| GPT-5 (flagship use case) | Claude Opus 4.6 ($5/$25) — flagship-to-flagship | Opus still cheaper than GPT-5 Pro ($15/$120) | -| GPT-5.5 | Claude Opus 4.6 ($5/$25) — flagship-to-flagship | Bedrock 17% cheaper on output ($25 vs $30); same input price | -| GPT-5.5 (cost-sensitive) | Claude Sonnet 4.6 ($3/$15) — 53% cheaper | Strong cost case; Sonnet leads on agentic reliability | -| GPT-5.5 Pro | Nova 2 Pro ($1.38/$11) — flagship reasoning on AWS | 95% cheaper on Bedrock; strongest migration case | -| o-series (o1, o3) | Claude Sonnet 4.6 with extended thinking; Opus 4.6 for most demanding | o1 is $15/$60 — significant savings with Sonnet 4.6 at $3/$15 | - -**Override examples:** GPT-4 + Q16=cost → Haiku; Flash + Q17=extended thinking → Sonnet; GPT-4o + Q17=speech → Nova 2 Sonic; GPT-3.5 + Q22=complex → Sonnet; GPT-5 + Q16=balanced → Sonnet; GPT-5.5 + Q16=cost → Sonnet 4.6; Gemini 2.5 Flash Thinking + Q16=cost → Nova Lite (if thinking budget is low) or Sonnet 4.6 (if full thinking mode). +| GPT-5.6 Sol / Terra / Luna | **The same model on Bedrock** (`openai.gpt-5.6-sol` / `-terra` / `-luna`) | Bedrock charges OpenAI's first-party rate — cost is neutral, not a saving. Default recommendation; no behavior delta. Mantle/Responses only, in-region only. | +| GPT-5.5 | **GPT-5.5 on Bedrock** (`openai.gpt-5.5`) | Same model, same rate ($5/$30). If the user's priority is cost and a model change is acceptable, offer Sonnet 4.6 ($3/$15, 48% cheaper) or Opus 4.6 ($5/$25, 13% cheaper) as alternatives — not as the default. | +| GPT-5.4 | **GPT-5.4 on Bedrock** (`openai.gpt-5.4`) | Same model, same rate ($2.50/$15). Sonnet 4.6 is within 5% — at that spread cost is noise, so there is no cost reason to change models. | +| GPT-3.5 Turbo | GPT-5.6 Luna ($0.20/$1.20) — same vendor, far better quality; or Claude Haiku 4.5 ($1/$5) | Luna is 36% cheaper than GPT-3.5 **and** 77% cheaper than Haiku 4.5. Present both; Luna is usually the better framing. | +| GPT-4 / GPT-4 Turbo | GPT-5.6 Terra (same vendor, newer tier); or Claude Sonnet 4.6 ($3/$15) | Not on Bedrock — a model change is unavoidable. GPT-4 Turbo is $10/$30, so Sonnet is 58% cheaper; Terra keeps prompt idioms. Offer both. | +| GPT-4o | GPT-5.6 Terra (same vendor, newer tier); or Claude Sonnet 4.6 ($3/$15) | Not on Bedrock. GPT-4o is $2.50/$10 — Sonnet is 29% **more** expensive, so cost does not favor the cross-family move. Offer both. | +| GPT-5.4 Mini | GPT-5.6 Luna ($0.20/$1.20); or Nova Lite ($0.06/$0.24) | Mini is not on Bedrock. Nova Lite is cheapest; Luna keeps the vendor. Offer both. | +| GPT-5.4 Nano | GPT-5.6 Luna ($0.20/$1.20); or Nova Micro ($0.035/$0.14) | Nano is not on Bedrock. Nova Micro is cheapest; Luna keeps the vendor. Offer both. | +| GPT-5.4 Pro / GPT-5.5 Pro | GPT-5.6 Sol (same vendor, flagship reasoning); or Nova 2 Pro ($1.375/$11) | Pro variants are not on Bedrock. Nova 2 Pro is 94% cheaper than the $30/$180 Pro rate; Sol keeps the vendor. Offer both. | +| GPT-5 / GPT-5.1 / GPT-5.2 | GPT-5.6 Terra (same vendor, newer tier); or Claude Sonnet 4.6 ($3/$15) | Not on Bedrock. These are cheaper than Sonnet at source (GPT-5.1 by 40%, GPT-5.2 by 17%), so the cross-family case is capability/consolidation, not cost — and they are on a vendor deprecation path. | +| o-series (o1, o3) | GPT-5.6 Sol or Terra (same vendor); or Claude Sonnet 4.6 with extended thinking / DeepSeek-R1 | Not on Bedrock. o1 at $15/$60 → Nova 2 Pro 85% cheaper; o3 at $2/$8 → DeepSeek-R1 32% cheaper. Offer a same-vendor and a cross-family option. | + +**Override examples:** GPT-4 + Q16=cost → Haiku; Flash + Q17=extended thinking → Sonnet; GPT-4o + Q17=speech → Nova 2 Sonic; GPT-3.5 + Q22=complex → Sonnet; GPT-5 + Q16=balanced → Sonnet; Gemini 2.5 Flash Thinking + Q16=cost → Nova Lite (if thinking budget is low) or Sonnet 4.6 (if full thinking mode). + +**Same-model examples (Q16 does not displace these):** GPT-5.5 + Q16=balanced → **GPT-5.5 on Bedrock**; GPT-5.5 + Q16=cost → **GPT-5.5 on Bedrock**, with Sonnet 4.6 offered as a 48%-cheaper alternative the user can accept or decline; GPT-5.6 Luna + Q16=cost → **Luna on Bedrock** (already cheaper than Haiku 4.5; only Nova undercuts it); GPT-5.4 + Q17=speech → Nova 2 Sonic (Q17 hard override wins — the source model cannot serve speech-to-speech). Interpret → `ai_model_baseline`. Default: auto-detect from code, fallback Q16 priority-based. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/design/design-ai.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/design/design-ai.md index 207ec341..86a2f73f 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/design/design-ai.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/design/design-ai.md @@ -23,7 +23,10 @@ Read `$MIGRATION_DIR/preferences.json` → `ai_constraints` (if present). If abs **Load source-specific design reference based on `ai_source`:** - `"gemini"` → load `references/design-refs/ai-gemini-to-bedrock.md` -- `"openai"` → load `references/design-refs/ai-openai-to-bedrock.md` +- `"openai"` → load `references/design-refs/ai-openai-to-bedrock.md` **and** + `references/shared/openai-on-bedrock.md` (the fact base: model IDs, endpoint paths, region matrix, quotas, + caching). OpenAI's proprietary GPT models run on Bedrock, so the source model is often itself the target — the + mapping guide's Tier 0 path is the default, not a cross-family swap. - `"anthropic"` → load `references/design-refs/ai-anthropic-to-bedrock.md` (Anthropic SDK → Bedrock Converse API client swap; do NOT use ai-openai-to-bedrock.md for Anthropic SDK users) - `"both"` → load both `ai-gemini-to-bedrock.md` and `ai-openai-to-bedrock.md` - `"other"` or absent → load `references/design-refs/ai.md` (traditional ML rubric — Vision API, Speech API, Document AI, custom models only; do NOT use for Anthropic SDK users) @@ -93,8 +96,14 @@ For each `workloads[]` entry: | `text_to_speech` | Text-to-speech | Amazon Polly | | `unknown` | Text/reasoning (default) | Apply override hierarchy below | -2. **For text/reasoning capabilities:** Apply the existing override hierarchy from `ai_constraints`: - - Q17 special features (hard override) > Q16 priority > Q18/Q21 volume and latency > source model baseline +2. **For text/reasoning capabilities:** Apply the override hierarchy from `ai_constraints`: + - Q17 special features (hard override) > **same-model availability** > Q16 priority > Q18/Q21 volume and latency > + source model baseline + - **Same-model availability outranks Q16 priority.** If the source model is available on Bedrock and the target + region carries it, keep it — a `balanced` or unset priority must not silently move the workload to a different + model family. Only a Q17 hard feature override (a capability the source model cannot serve) beats it. An + explicit `ai_priority = "cost"` does not override it either; it adds a cost alternative alongside the same-model + recommendation (see the mapping guide's Tier 2). - This ensures single-workload sophistication is preserved per workload 3. **Emit one `design_block` per workload** in `aws-design-ai.json`: @@ -141,12 +150,26 @@ Treat model mapping as compatibility-guided, not 1:1 parity. Before cutover, req **Stay-or-migrate assessment per model:** +Check the same-model case FIRST — it short-circuits the cost comparison: + +- **Source model is available on Bedrock** (per `shared/openai-on-bedrock.md`) AND the target region carries it → + `"strong_migrate"`. Bedrock charges the model provider's first-party rate, so cost is neutral by construction; do + not run a savings comparison and do not let a neutral cost result downgrade the assessment. Set + `model_change: false`. The rationale is risk (no behavior delta) plus AWS commitments, governance, and residency. +- Source model on Bedrock but the target region does not carry it → treat as no-same-model below, and record the + region as the cause in `regional_warnings[]`. Do not report this as a model-quality judgment. - Bedrock cheaper → `"strong_migrate"` - Bedrock within 25% of source AND priority != cost → `"moderate_migrate"` - Source > 25% cheaper AND priority = cost → `"weak_migrate"` or `"recommend_stay"` Overall assessment = weakest across all models. If any `"recommend_stay"`, flag prominently. +**`recommend_stay` requires a non-cost reason when the source provider's models are on Bedrock.** For an OpenAI +source, cost can no longer justify staying (first-party rate parity). Only these justify `recommend_stay`: a fixed +region with no suitable model, Realtime API dependence, image/speech/embedding capabilities that map to other AWS +services rather than to an OpenAI model, Assistants API with file search / vector stores / code interpreter, or a +confirmed unsupported API surface. Record which one in `honest_assessment_reason`. + **Model comparison table** (include in output and user summary): Model, Provider, Max Context, Input/Output Price per 1M, Price Comparison, Streaming, Function Calling, Assessment. **Quota risk assessment** (per `references/shared/bedrock-quotas.md`): @@ -183,8 +206,10 @@ Prompt Routing as an option. It automatically routes each request to the cheapes that can handle it at adequate quality — the AWS-native automation of the tiered strategy above. > Intelligent Prompt Routing only routes within a single model family. It does NOT replace -> cross-provider routing (e.g., Claude ↔ GPT-4o). If the startup was using OpenRouter or -> LiteLLM to route across providers, they still need app-level routing for cross-family calls. +> cross-family routing (e.g., Claude ↔ GPT-5.6). Note that even though both families are now on +> Bedrock, Claude is reached via `bedrock-runtime`/Converse and the proprietary GPT models via +> `bedrock-mantle`/Responses — two different client surfaces. A router spanning them still needs +> app-level or LiteLLM routing with two client paths; being "all on Bedrock" does not unify them. > One-line caveat: adds a routing-prediction latency hop; verify model support at > docs.aws.amazon.com/bedrock/latest/userguide/prompt-routing.html before recommending. @@ -243,13 +268,13 @@ For each model in `models[]`, record: - **Workload type**: text generation, embeddings, vision, code generation, custom model - **Integration pattern mapping**: -| GCP Pattern | AWS Pattern | Effort | -| ------------ | ------------------------------------------------ | ------- | -| `direct_sdk` | Mantle OpenAI-compat (if OpenAI source + region) | Minimal | -| `direct_sdk` | Bedrock SDK (boto3 / AWS SDK) | Medium | -| `framework` | LangChain/LlamaIndex + Bedrock | Low | -| `rest_api` | Bedrock REST API | Medium | -| `mixed` | Match per-model | Varies | +| GCP Pattern | AWS Pattern | Effort | +| ------------ | ------------------------------------------------------------------------- | ------- | +| `direct_sdk` | Mantle Responses API (OpenAI source, model on Bedrock, region carries it) | Minimal | +| `direct_sdk` | Bedrock SDK (boto3 / AWS SDK) | Medium | +| `framework` | LangChain/LlamaIndex + Bedrock | Low | +| `rest_api` | Bedrock REST API | Medium | +| `mixed` | Match per-model | Varies | - **Migration complexity**: Low / Medium / High @@ -277,23 +302,27 @@ For each detected `integration.pattern` and `ai_source`, generate before/after m **Patterns to include (matched to detected language and source):** -| Pattern | Source | Target | Key Change | -| -------------------- | ------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------- | -| Direct SDK (OpenAI) | OpenAI | Mantle (OpenAI-compat) | Change `OPENAI_BASE_URL` + `OPENAI_API_KEY` + model string (zero code changes) | -| Direct SDK | Vertex AI | boto3 Converse API | `generate_content()` → `converse()` | -| Direct SDK | OpenAI | boto3 Converse API | `completions.create()` → `converse()` (use if Mantle region unavailable or Converse features needed) | -| Direct SDK | Anthropic | boto3 Converse API | `messages.create()` → `converse()` with Claude model ID on Bedrock | -| LangChain | ChatVertexAI / ChatOpenAI | ChatBedrock | Swap import and model_id | -| LlamaIndex | Vertex / OpenAI LLM | BedrockConverse | Swap import | -| LLM Router (LiteLLM) | Any | Config change | `model="bedrock/"` (1 line) | -| Embeddings | TextEmbeddingModel | Titan Embeddings v2 | `invoke_model` with JSON body | -| Streaming | `stream=True` | `converse_stream` | Event loop over `contentBlockDelta` | +| Pattern | Source | Target | Key Change | +| ------------------------------- | ------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------- | +| Direct SDK (OpenAI), same model | OpenAI | Mantle Responses API | Base URL → `.../openai/v1`, Bedrock credential, `openai.gpt-*` model ID, `bedrock-mantle:*` IAM | +| Direct SDK | Vertex AI | boto3 Converse API | `generate_content()` → `converse()` | +| Direct SDK | OpenAI | boto3 Converse API | `completions.create()` → `converse()` (use if Mantle region unavailable or Converse features needed) | +| Direct SDK | Anthropic | boto3 Converse API | `messages.create()` → `converse()` with Claude model ID on Bedrock | +| LangChain | ChatVertexAI / ChatOpenAI | ChatBedrock | Swap import and model_id | +| LlamaIndex | Vertex / OpenAI LLM | BedrockConverse | Swap import | +| LLM Router (LiteLLM) | Any | Config change | `model="bedrock/"` (1 line) | +| Embeddings | TextEmbeddingModel | Titan Embeddings v2 | `invoke_model` with JSON body | +| Streaming | `stream=True` | `converse_stream` | Event loop over `contentBlockDelta` | + +**Mantle Responses API (primary path for OpenAI sources):** If `ai_source = "openai"` and the source model is on Bedrock, this is the primary option. The application keeps the OpenAI SDK and the same model; only the base URL, credential, model ID, and IAM actions change. Read `references/shared/openai-on-bedrock.md` for the exact values — three details break naive implementations: the path is `/openai/v1/responses` (not `/v1/responses`), the credential must be a Bedrock API key or token provider (not an OpenAI key), and IAM needs `bedrock-mantle:*` actions (not `bedrock:InvokeModel`). Record `migration_path: "mantle_openai_responses"` and `model_change: false`. + +**This is not a zero-code change if the source uses Chat Completions.** Chat Completions support is unverified for the proprietary GPT models — every AWS sample uses Responses. If `integration` evidence shows `chat.completions.create`, plan a reshape to `responses.create` and probe the target account before committing. Only a source already on `responses.create` is close to a drop-in. -**Mantle (OpenAI-compatible endpoints):** If `ai_source = "openai"` and `integration.pattern = "direct_sdk"`, prefer the Mantle path as the primary migration option. Mantle provides OpenAI-compatible Chat Completions and Responses APIs on Bedrock — the existing OpenAI SDK code works with zero changes, only environment variable updates. Check [Mantle regional availability](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html) — if the target region does not have Mantle, fall back to the boto3 Converse API path. Record `migration_path: "mantle"` or `migration_path: "converse"` in `aws-design-ai.json` → `ai_architecture.code_migration`. +**No Converse fallback for proprietary GPT models.** The GPT-5.x models are `bedrock-mantle` only and in-region only — there is no `bedrock-runtime` path and no cross-region inference profile. If the workload needs Bedrock Guardrails, Knowledge Bases, invocation logging, or a region these models do not serve, that requires a **model change** to a Bedrock-native model (or gpt-oss), not an endpoint change. Record `migration_path: "converse"` with `model_change: true` in that case. -**Mantle throughput caveat (medium/high volume):** Mantle runs on a shared 10,000 RPM account limit. For workloads with `ai_token_volume = "medium"` or `"high"`, add a note in the design summary: "Mantle is subject to a shared 10K RPM account limit. At medium/high volume, monitor for 429s and consider migrating to `bedrock-runtime` (Converse API) for dedicated throughput." See `references/shared/ai-migration-guardrails.md` for the full risk table. +**Mantle throughput (medium/high volume):** quotas on `bedrock-mantle` are **per-model, per-region input TPM and output TPM — there is no RPM quota**. For `ai_token_volume = "medium"` or `"high"`, note: "Mantle enforces per-model input/output TPM quotas per region; 429s indicate a TPM ceiling, not a request-rate cap. Mitigate with exponential backoff, spreading load across minutes, and prompt caching (cached input is exempt from the input-TPM quota). There is no `bedrock-runtime` fallback for these models, so sustained growth needs a quota increase." See `references/shared/ai-migration-guardrails.md`. -**gpt-oss migration path:** If `ai_source = "openai"` and the user wants to preserve OpenAI model architecture while consolidating on AWS, offer `gpt-oss` on Bedrock as a fourth migration path alongside Mantle, Converse API, and framework swap. Record `migration_path: "gpt-oss"` in `aws-design-ai.json` → `ai_architecture.code_migration`. The gpt-oss path uses the Converse API with the gpt-oss Bedrock model ID — it is not an OpenAI-compatible endpoint. Note the Claude 4.7+ output TPM cap (2M) if the user is migrating from a high-output OpenAI workload. +**gpt-oss migration path:** If `ai_source = "openai"` and the user wants OpenAI-architecture models on the Bedrock-native runtime surface, offer `gpt-oss` as an additional path. Unlike the proprietary GPT models, gpt-oss **does** support `bedrock-runtime` (Converse / InvokeModel), so it is the option when Guardrails or invocation logging are required and an OpenAI-lineage model is preferred. It sits a capability class below the GPT-5.x frontier tier. Record `migration_path: "gpt-oss"`. Generate concrete code examples using actual model IDs from the selected Bedrock models. Only include patterns matching the detected integration. @@ -323,19 +352,20 @@ Write `aws-design-ai.json` to `$MIGRATION_DIR/`. **Schema — top-level fields:** -| Field | Type | Description | -| ------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `metadata` | object | `phase`, `focus`, `ai_source`, `bedrock_models_selected`, `timestamp` | -| `ai_architecture.honest_assessment` | string | `"strong_migrate"`, `"moderate_migrate"`, `"weak_migrate"`, `"recommend_stay"` | -| `ai_architecture.tiered_strategy` | object/null | Tiered model routing (null for low/medium volume) | -| `ai_architecture.bedrock_models` | array | Per-model: `gcp_model_id`, `aws_model_id`, `capabilities_matched[]`, `capability_gaps[]`, `honest_assessment`, `source_provider_price`, `bedrock_price`, `price_comparison`, `migration_complexity` | -| `ai_architecture.capability_mapping` | object | Per-capability: `parity` (full/partial/none), `notes` | -| `ai_architecture.code_migration` | object | `primary_pattern`, `framework`, `files_to_modify[]`, `dependency_changes` | -| `ai_architecture.infrastructure` | array | GCP resource → AWS equivalent mappings with confidence | -| `ai_architecture.services_to_migrate` | array | GCP service → AWS service with effort and notes | -| `regional_warnings` | array | Per-service: `service`, `target_region`, `nearest_available`, `impact` (empty array if all services available) | -| `multi_model_warnings` | array | Per-warning: `type`, `message` (empty array if single model or no coordination issues) | -| `agentic_design` | object/null | Present only when `agentic_profile.is_agentic == true`. Contains `migration_approach`, path-specific config (e.g., `harness_config`). Null or absent for non-agentic workloads. | +| Field | Type | Description | +| ------------------------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `metadata` | object | `phase`, `focus`, `ai_source`, `bedrock_models_selected`, `timestamp` | +| `ai_architecture.honest_assessment` | string | `"strong_migrate"`, `"moderate_migrate"`, `"weak_migrate"`, `"recommend_stay"` | +| `ai_architecture.honest_assessment_reason` | string/null | Required when `honest_assessment == "recommend_stay"`. The specific non-cost blocker (region, Realtime API, separate-service capability, Assistants API feature, unsupported API surface). Null otherwise. | +| `ai_architecture.tiered_strategy` | object/null | Tiered model routing (null for low/medium volume) | +| `ai_architecture.bedrock_models` | array | Per-model: `gcp_model_id`, `aws_model_id`, `capabilities_matched[]`, `capability_gaps[]`, `honest_assessment`, `source_provider_price`, `bedrock_price`, `price_comparison`, `migration_complexity` | +| `ai_architecture.capability_mapping` | object | Per-capability: `parity` (full/partial/none), `notes` | +| `ai_architecture.code_migration` | object | `primary_pattern`, `framework`, `files_to_modify[]`, `dependency_changes` | +| `ai_architecture.infrastructure` | array | GCP resource → AWS equivalent mappings with confidence | +| `ai_architecture.services_to_migrate` | array | GCP service → AWS service with effort and notes | +| `regional_warnings` | array | Per-service: `service`, `target_region`, `nearest_available`, `impact` (empty array if all services available) | +| `multi_model_warnings` | array | Per-warning: `type`, `message` (empty array if single model or no coordination issues) | +| `agentic_design` | object/null | Present only when `agentic_profile.is_agentic == true`. Contains `migration_approach`, path-specific config (e.g., `harness_config`). Null or absent for non-agentic workloads. | ## Validation Checklist @@ -347,6 +377,9 @@ Write `aws-design-ai.json` to `$MIGRATION_DIR/`. - [ ] All model IDs use current Bedrock identifiers (Active status per `shared/ai-model-lifecycle.md`) - [ ] No Legacy model is used as `bedrock_models[].aws_model_id` unless no Active alternative exists (with EOL date noted) - [ ] `honest_assessment` logic is consistent (weakest model drives overall) +- [ ] If `ai_source` includes `"openai"`: every model that `shared/openai-on-bedrock.md` lists as available on Bedrock, and whose target region carries it, is mapped to **itself** with `model_change: false` — not to a Claude/Nova substitute +- [ ] If `honest_assessment == "recommend_stay"`: `honest_assessment_reason` names a non-cost blocker (cost parity alone is never sufficient for an OpenAI source) +- [ ] No `bedrock_models[]` entry pairs a proprietary `openai.gpt-*` model ID with a Converse/`bedrock-runtime` migration path (those models are mantle-only) - [ ] `regional_warnings` is present (empty array `[]` if no issues; populated if any service unavailable in target region) - [ ] `multi_model_warnings` is present (empty array `[]` if single model or no coordination issues) - [ ] If `agentic_profile.is_agentic == true`: `agentic_design` object is present with `migration_approach` matching `preferences.json` diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/discover/discover-preview.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/discover/discover-preview.md index 202e6aa0..635a3d1d 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/discover/discover-preview.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/discover/discover-preview.md @@ -83,23 +83,41 @@ from `references/shared/pricing-cache.md` (Source Provider Pricing + Bedrock Mod **Source model → Bedrock equivalent mapping:** -| Source model pattern | Bedrock equivalent | Bedrock model ID | -| ----------------------------------------------- | -------------------------- | ------------------------------------------ | -| `gpt-4o`, `gpt-4.1`, `gpt-5.*` flagship | Claude Sonnet 4.6 | `anthropic.claude-sonnet-4-6` | -| `gpt-4o-mini`, `gpt-4.1-mini`, `gpt-5.*-mini` | Claude Haiku 4.5 | `anthropic.claude-haiku-4-5-20251001-v1:0` | -| `gpt-3.5-turbo`, `gpt-4.1-nano`, `gpt-5.*-nano` | Amazon Nova Micro | `amazon.nova-micro-v1:0` | -| `o3`, `o4-mini`, reasoning models | Claude Sonnet 4.6 | `anthropic.claude-sonnet-4-6` | -| `gemini-2.5-pro`, `gemini-3.*-pro` | Claude Sonnet 4.6 | `anthropic.claude-sonnet-4-6` | -| `gemini-2.5-flash`, `gemini-2.0-flash` | Claude Haiku 4.5 | `anthropic.claude-haiku-4-5-20251001-v1:0` | -| `gemini-2.0-flash-lite` | Amazon Nova Lite | `amazon.nova-lite-v1:0` | -| `claude-3-5-sonnet`, `claude-sonnet-*` | Claude Sonnet 4.6 | `anthropic.claude-sonnet-4-6` | -| `claude-3-5-haiku`, `claude-haiku-*` | Claude Haiku 4.5 | `anthropic.claude-haiku-4-5-20251001-v1:0` | -| `claude-3-opus`, `claude-opus-*` | Claude Opus 4.6 | `anthropic.claude-opus-4-6-v1` | -| `text-embedding-*`, `*-embedding-*` | Amazon Titan Embeddings v2 | `amazon.titan-embed-text-v2:0` | -| `dall-e-*`, `imagen-*`, image generation | Amazon Nova Canvas | `amazon.nova-canvas-v1:0` | -| `whisper-*`, speech-to-text | Amazon Transcribe | (non-token service — note separately) | -| `tts-*`, text-to-speech | Amazon Polly | (non-token service — note separately) | -| Unknown / other | Amazon Nova Pro | `amazon.nova-pro-v1:0` | +**Same-model rows first.** OpenAI's proprietary GPT models run on Bedrock, so these sources map to themselves and +the price comparison is parity (no saving, no penalty). Match these before falling through to the cross-family rows. + +| Source model pattern | Bedrock equivalent | Bedrock model ID | +| -------------------------------------- | ------------------ | ---------------------- | +| `gpt-5.6-sol`, `gpt-5.6` flagship | GPT-5.6 Sol | `openai.gpt-5.6-sol` | +| `gpt-5.6-terra` | GPT-5.6 Terra | `openai.gpt-5.6-terra` | +| `gpt-5.6-luna` | GPT-5.6 Luna | `openai.gpt-5.6-luna` | +| `gpt-5.5` (not `-pro`) | GPT-5.5 | `openai.gpt-5.5` | +| `gpt-5.4` (not `-pro`/`-mini`/`-nano`) | GPT-5.4 | `openai.gpt-5.4` | + +These are `bedrock-mantle` / Responses-API only and in-region only (us-east-1, us-east-2, plus us-west-2 for Terra, +Luna, and GPT-5.4; us-gov-west-1 for GPT-5.4). At Discover time the target region may not be known — record the +same-model mapping and let Design apply the region gate. See `references/shared/openai-on-bedrock.md`. + +**Cross-family rows** — for sources with no Bedrock equivalent: + +| Source model pattern | Bedrock equivalent | Bedrock model ID | +| ------------------------------------------------------- | -------------------------------- | ------------------------------------------ | +| `gpt-4o`, `gpt-4.1`, `gpt-5`/`5.1`/`5.2` | Claude Sonnet 4.6 | `anthropic.claude-sonnet-4-6` | +| `gpt-4o-mini`, `gpt-4.1-mini`, `gpt-5.*-mini` | Claude Haiku 4.5 | `anthropic.claude-haiku-4-5-20251001-v1:0` | +| `gpt-3.5-turbo`, `gpt-4.1-nano`, `gpt-5.*-nano` | Amazon Nova Micro | `amazon.nova-micro-v1:0` | +| `gpt-*-pro` (GPT-5.x Pro), `o1-pro`, `o3-pro` | Amazon Nova 2 Pro | `amazon.nova-2-pro-v1:0` | +| `o3`, `o4-mini`, reasoning models | Claude Sonnet 4.6 | `anthropic.claude-sonnet-4-6` | +| `gemini-2.5-pro`, `gemini-3.*-pro` | Claude Sonnet 4.6 | `anthropic.claude-sonnet-4-6` | +| `gemini-2.5-flash`, `gemini-2.0-flash` | Claude Haiku 4.5 | `anthropic.claude-haiku-4-5-20251001-v1:0` | +| `gemini-2.0-flash-lite` | Amazon Nova Lite | `amazon.nova-lite-v1:0` | +| `claude-3-5-sonnet`, `claude-sonnet-*` | Claude Sonnet 4.6 | `anthropic.claude-sonnet-4-6` | +| `claude-3-5-haiku`, `claude-haiku-*` | Claude Haiku 4.5 | `anthropic.claude-haiku-4-5-20251001-v1:0` | +| `claude-3-opus`, `claude-opus-*` | Claude Opus 4.6 | `anthropic.claude-opus-4-6-v1` | +| `text-embedding-*`, `*-embedding-*` | Amazon Titan Embeddings v2 | `amazon.titan-embed-text-v2:0` | +| `dall-e-*`, `gpt-image-*`, `imagen-*`, image generation | Stability AI — Stable Image Core | `stability.stable-image-core-v1:0` | +| `whisper-*`, speech-to-text | Amazon Transcribe | (non-token service — note separately) | +| `tts-*`, text-to-speech | Amazon Polly | (non-token service — note separately) | +| Unknown / other | Amazon Nova Pro | `amazon.nova-pro-v1:0` | For each mapped model pair, record `source_model` and `bedrock_equivalent` (model name only). Do NOT compute or display per-token pricing comparisons at this stage — cost analysis diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/estimate/estimate-ai.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/estimate/estimate-ai.md index 0017830f..0bdf2318 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/estimate/estimate-ai.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/estimate/estimate-ai.md @@ -109,12 +109,15 @@ Do **not** repeat these as "costs" in the user-facing summary. Present the monthly and annual cost difference between current GCP AI spend and projected Bedrock cost: +- **If the model is unchanged** (`model_change: false`): state that Bedrock charges the model provider's first-party rate, so projected cost is **the same** — then make the case on non-cost grounds. Do not present a savings figure of $0 as a negative finding. - **If Bedrock is cheaper**: present monthly and annual savings clearly - **If Bedrock is more expensive**: state clearly, justify with non-cost benefits or note "not justified if cost is the only priority" -Reference `aws-design-ai.json` → `honest_assessment`. If `"recommend_stay"`, present prominently. +Reference `aws-design-ai.json` → `honest_assessment`. If `"recommend_stay"`, present prominently along with `honest_assessment_reason`. -**Non-cost benefits to present:** model flexibility (30+ models), prompt caching (Claude, 90% savings), AWS ecosystem (Guardrails, Knowledge Bases, Agents), vendor diversification, multi-model strategy. +**Non-cost benefits to present:** usage counting toward existing AWS commitments, IAM/VPC/PrivateLink/KMS/CloudTrail governance, in-region processing for data residency, prompt caching (Claude, and GPT-5.6 at 90% off cached input with cached tokens exempt from the input-TPM quota), model flexibility (100+ models), AWS ecosystem (Guardrails, Knowledge Bases, AgentCore), and — for a same-model move — the elimination of behavior-delta and prompt-regression risk. + +**Pricing source caveat for OpenAI models:** the AWS Price List API does not carry the proprietary GPT-5.x models, so the `awspricing` MCP returns no rows for them. An empty result is **not** evidence the model is unavailable or free. Use `shared/pricing-cache.md`, and treat rows marked `unverified` there as blocking for any quoted figure — resolve them from the Bedrock pricing page first. See `shared/openai-on-bedrock.md`. **Note:** Human/professional-services one-time migration costs are intentionally out of scope for this advisor and excluded from ROI calculations. @@ -147,6 +150,7 @@ Produce a clear migrate/stay/optimize verdict for the AI workload migration. Thi | Condition | Verdict | `recommendation.path` | | --------------------------------------------------------------------------------------------------------------------------------- | ------------------- | --------------------- | +| **Same model on Bedrock at provider parity** (`model_change: false`) — cost delta is ~zero by construction | Migrate | `migrate_optimized` | | Bedrock cheaper AND capabilities match | Migrate | `migrate_optimized` | | Bedrock more expensive BUT non-cost benefits justify (vendor diversification, Guardrails, multi-model) AND user priority ≠ `cost` | Migrate with caveat | `migrate_optimized` | | Bedrock more expensive AND user priority = `cost` AND no compelling non-cost reason | Stay | `stay` | @@ -170,6 +174,7 @@ Produce a clear migrate/stay/optimize verdict for the AI workload migration. Thi - MUST emit `recommendation` — never omit. If data is insufficient, set `confidence: "low"` and state why in `rationale`. - If `honest_assessment` from `aws-design-ai.json` says `recommend_stay`, `recommendation.path` MUST be `stay` regardless of cost numbers. +- **A zero or near-zero cost delta is not a reason to stay.** When `bedrock_models[].model_change` is `false`, the source and Bedrock rates are the same model at the same price, so the delta carries no signal. Do not route these to `stay` via the "more expensive AND priority = cost" row, and do not report them as "no savings identified" — report cost as neutral and argue the case on commitments, governance, residency, prompt caching, and eliminated behavior-delta risk. - For multi-workload runs: if some workloads favor migration and others don't, use `migrate_phased` and list which workloads to migrate vs. keep in `rationale`. --- diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/generate/generate-ai.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/generate/generate-ai.md index c42f8300..d36fdc22 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/generate/generate-ai.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/generate/generate-ai.md @@ -51,17 +51,17 @@ Based on `ai-workload-profile.json` → `integration.pattern` and `integration.l **Migration patterns to include (matched to detected language and source):** -| Source SDK | Target | Key Change | -| ------------------ | --------------------------------- | --------------------------------------------------------------------------------- | -| OpenAI SDK | Mantle OpenAI-compat | Set `OPENAI_BASE_URL` + `OPENAI_API_KEY` + model string (zero code changes) | -| Vertex AI (Python) | boto3 Bedrock Converse API | `GenerativeModel.generate_content()` → `bedrock.converse()` | -| Vertex AI (JS) | @aws-sdk/client-bedrock-runtime | `model.generateContent()` → `client.send(new ConverseCommand())` | -| Vertex AI (Go) | aws-sdk-go-v2 bedrockruntime | `aiplatform` → `bedrockruntime.Converse()` | -| Vertex AI (Java) | AWS SDK BedrockRuntimeClient | `GenerativeModel` → `BedrockRuntimeClient.converse()` | -| OpenAI SDK | boto3 Bedrock Converse API | `client.chat.completions.create()` → `bedrock.converse()` (if Mantle unavailable) | -| LiteLLM | LiteLLM config change | `model="gpt-4o"` → `model="bedrock/anthropic.claude-sonnet-4-6"` | -| LangChain | langchain_aws | `ChatOpenAI`/`ChatVertexAI` → `ChatBedrock` | -| LlamaIndex | llama_index.llms.bedrock_converse | `Vertex` → `BedrockConverse` | +| Source SDK | Target | Key Change | +| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| OpenAI SDK (same model) | Mantle Responses API | Base URL → `.../openai/v1`, Bedrock token/API key (not an OpenAI key), `openai.gpt-*` model ID. Config-only **if** already on `responses.create`; a Chat Completions source must reshape to Responses. | +| Vertex AI (Python) | boto3 Bedrock Converse API | `GenerativeModel.generate_content()` → `bedrock.converse()` | +| Vertex AI (JS) | @aws-sdk/client-bedrock-runtime | `model.generateContent()` → `client.send(new ConverseCommand())` | +| Vertex AI (Go) | aws-sdk-go-v2 bedrockruntime | `aiplatform` → `bedrockruntime.Converse()` | +| Vertex AI (Java) | AWS SDK BedrockRuntimeClient | `GenerativeModel` → `BedrockRuntimeClient.converse()` | +| OpenAI SDK | boto3 Bedrock Converse API | `client.chat.completions.create()` → `bedrock.converse()` (if Mantle unavailable) | +| LiteLLM | LiteLLM config change | `model="gpt-4o"` → `model="bedrock/anthropic.claude-sonnet-4-6"`, or keep the GPT family via the mantle endpoint when the source model is on Bedrock | +| LangChain | langchain_aws | `ChatOpenAI`/`ChatVertexAI` → `ChatBedrock` | +| LlamaIndex | llama_index.llms.bedrock_converse | `Vertex` → `BedrockConverse` | For each detected language and pattern, generate before/after code examples using actual model IDs from `aws-design-ai.json`. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/ai-migration-guardrails.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/ai-migration-guardrails.md index 4579d706..84bf2939 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/ai-migration-guardrails.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/ai-migration-guardrails.md @@ -28,25 +28,35 @@ AgentCore services have different regional footprints. Always validate via `get_ --- -## Bedrock Mantle Throughput Limits (Shared Account) +## Bedrock Mantle Throughput Limits -Bedrock Mantle provides OpenAI-compatible endpoints on Bedrock. It runs on a **shared account limit of 10,000 RPM** across all Mantle users in a region — this is not a per-customer quota. +Bedrock Mantle serves OpenAI-compatible and Anthropic-compatible APIs on Bedrock. -**Risk table:** +### OpenAI proprietary models (GPT-5.6 / 5.5 / 5.4) — TPM only, no RPM -| Workload Volume | Risk Level | Guidance | -| ---------------------- | ---------- | ---------------------------------------------------------------------------------------- | -| Low (< 100 RPM) | Low | Mantle is a good fit; shared limit is not a concern | -| Medium (100–1,000 RPM) | Medium | Monitor for 429s at peak; have a fallback ready | -| High (> 1,000 RPM) | High | Use `bedrock-runtime` (Converse API) directly — not subject to the shared Mantle RPM cap | +Inference on `bedrock-mantle` for these models is governed by **two per-model, per-region quotas: input tokens per minute and output tokens per minute. There is no requests-per-minute quota.** Exceeding a TPM quota returns HTTP 429. Cached input tokens read through prompt caching **do not count** against the input-TPM quota. -**When to use `bedrock-runtime` instead of Mantle:** +| Workload Volume | Risk Level | Guidance | +| --------------- | ---------- | -------------------------------------------------------------------------------------------------- | +| Low | Low | Default TPM quotas are ample | +| Medium | Medium | Monitor 429s against **token** throughput, not request rate; enable prompt caching | +| High | High | Enable prompt caching first (cached input is exempt from input TPM), then request a quota increase | -- Production workloads with sustained high request rates -- Latency-sensitive workloads where shared-limit throttling is unacceptable -- Workloads that need per-customer quota increases via Service Quotas +**There is no `bedrock-runtime` fallback for these models.** They are `bedrock-mantle` only and in-region only — no Converse path, no Geo/Global cross-region inference profile. Do **not** advise "switch to `bedrock-runtime` for dedicated throughput": that requires moving to a different model (Bedrock-native or `gpt-oss`), which is a model change with its own eval cost, not an endpoint change. Scaling levers, in order: -**Source:** [AWS Bedrock Mantle scaling throughput best practices](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html) +1. Prompt caching (GPT-5.6 only) — 90% off cached input and exempt from the input-TPM quota +2. Exponential backoff with a bounded retry count (`max_retries` on the OpenAI SDK) +3. Spreading load across minutes and ramping request rate gradually rather than bursting +4. A Service Quotas increase for that model's input/output TPM in that region +5. Only if the above are insufficient: change models, accepting the eval cost + +See `references/shared/openai-on-bedrock.md` for the endpoint, region matrix, and caching parameters. + +**Source:** [Get started with GPT-5.6 on Amazon Bedrock — Quotas and scaling](https://aws.amazon.com/blogs/machine-learning/get-started-with-openai-gpt-5-6-sol-terra-and-luna-on-amazon-bedrock/) + +### Other models on Mantle + +For non-OpenAI models served through Mantle, verify current quota dimensions and any shared-account limits in the [Mantle documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html) before advising on throughput. Do not carry the OpenAI TPM-only model over to other providers without checking, and do not assume previously documented shared-RPM behavior still applies. --- @@ -61,9 +71,10 @@ Claude models on Mantle have an additional **output TPM cap** that differs by mo **Impact for migration decisions:** -- For Claude migrations at medium/high volume: the 2M output TPM cap on Claude 4.7+ is the binding constraint, not the 10K RPM limit -- For gpt-oss migrations (OpenAI model architecture on Bedrock): check whether the target model is Claude 4.7+ and flag the output TPM cap in the design -- When output-heavy workloads (long JSON, tool outputs, multi-step reasoning) are detected, flag this cap prominently and recommend `bedrock-runtime` for production +- For Claude migrations at medium/high volume: the 2M output TPM cap on Claude 4.7+ is the binding constraint +- For OpenAI proprietary GPT targets: this Claude cap does not apply. Their constraint is the per-model input/output TPM quota described above, and `bedrock-runtime` is not an available mitigation +- For `gpt-oss` targets: these do run on `bedrock-runtime`, so standard account TPM limits and the Converse-path mitigations apply +- When output-heavy workloads (long JSON, tool outputs, multi-step reasoning) are detected, flag the relevant cap prominently; recommend `bedrock-runtime` for production **only** when the target model actually has a `bedrock-runtime` path --- diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/openai-on-bedrock.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/openai-on-bedrock.md new file mode 100644 index 00000000..01d79363 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/openai-on-bedrock.md @@ -0,0 +1,226 @@ +# OpenAI Models on Amazon Bedrock + +**Last verified:** 2026-08-10 +**Sources:** [OpenAI model cards](https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards-openai.html) (per-model +cards linked below), [GPT-5.6 launch post](https://aws.amazon.com/blogs/machine-learning/get-started-with-openai-gpt-5-6-sol-terra-and-luna-on-amazon-bedrock/), +[GPT-5.6 GA announcement](https://aws.amazon.com/about-aws/whats-new/2026/07/openai-gpt-sol-terra/), +[GPT-5.6 pricing update](https://aws.amazon.com/about-aws/whats-new/2026/07/openai-gpt-terra-luna-pricing-bedrock/) + +OpenAI's **proprietary** models are available on Bedrock, not just the open-weight `gpt-oss` family. This changes the +default shape of every OpenAI → AWS migration: the source model itself is frequently a Bedrock target, so a +cross-family swap to Claude/Nova is no longer the only option — and is no longer the default. + +**This file is the single source of truth for OpenAI-on-Bedrock facts in this plugin.** `ai-openai-to-bedrock.md` +(mapping policy), `design-ai.md` (selection), `estimate-ai.md` (costing), and `ai-migration-guardrails.md` (quota +risk) all defer to it. Do not restate model IDs, regions, or endpoint paths elsewhere — link here. + +--- + +## Model Catalog + +| Model | Model ID (mantle) | Launched | Context | Lifecycle | Model card | +| ----------------- | ---------------------------------------- | ------------ | ------- | --------- | ------------------------------------------------------------------------------------------------ | +| GPT-5.6 Sol | `openai.gpt-5.6-sol` | Jul 13, 2026 | 1M | Active | [card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-sol.html) | +| GPT-5.6 Terra | `openai.gpt-5.6-terra` | Jul 13, 2026 | 1M | Active | [card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-terra.html) | +| GPT-5.6 Luna | `openai.gpt-5.6-luna` | Jul 13, 2026 | 1M | Active | [card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-luna.html) | +| GPT-5.5 | `openai.gpt-5.5` | Jun 1, 2026 | 272K | Active | [card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-55.html) | +| GPT-5.4 | `openai.gpt-5.4` | Jun 1, 2026 | 272K | Active | [card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-54.html) | +| gpt-oss-120b | `openai.gpt-oss-120b` | Aug 5, 2025 | 128K | Active | open-weight; also on `bedrock-runtime` as `openai.gpt-oss-120b-1:0` | +| gpt-oss-20b | `openai.gpt-oss-20b` | Aug 5, 2025 | 128K | Active | open-weight; also on `bedrock-runtime` as `openai.gpt-oss-20b-1:0` | +| GPT OSS Safeguard | `openai.gpt-oss-safeguard-120b` / `-20b` | — | — | Active | content-moderation / guardrail enforcement, not general chat | + +**Naming:** GPT-5.6 uses generation number + capability tier. `Sol` = flagship reasoning, `Terra` = balanced +production, `Luna` = high-volume / low-latency. Tiers advance on independent cadences, so a future `Terra` may not +share a generation with a future `Sol`. + +> **Context-window conflict (resolved):** the GPT-5.6 launch blog states 272K for all three variants; all three +> model cards state 1M. **The model cards are authoritative** — use 1M for GPT-5.6. GPT-5.5 and GPT-5.4 are 272K on +> both sources. Re-check on refresh; if AWS corrects the blog, the cards still win. + +**Not on Bedrock (as of this refresh):** GPT-4o, GPT-4.1, GPT-4 / GPT-4 Turbo, GPT-3.5 Turbo, the o-series +(o1/o3/o4-mini), GPT-5 / GPT-5.1 / GPT-5.2, and the `*-Pro` variants (GPT-5.5 Pro, GPT-5.4 Pro). Sources whose model +is on this list have no same-model landing target — see `ai-openai-to-bedrock.md` for the two-option path. + +--- + +## Access Path — `bedrock-mantle` Only + +All five proprietary GPT models are reached through the **OpenAI Responses API on the `bedrock-mantle` endpoint**: + +``` +https://bedrock-mantle.{region}.api.aws/openai/v1 +``` + +Three constraints that break naive assumptions: + +1. **The path is `/openai/v1/responses`, not `/v1/responses`.** Every GPT model card carries this note explicitly: + the OpenAI models sit on a different path from other models served on the mantle responses endpoint. Code that + hardcodes `/v1` against a GPT model ID will 404. +2. **There is no `bedrock-runtime` / Converse path for these models.** Each model card's Programmatic Access table + lists exactly one row, and it is `bedrock-mantle`. Only the open-weight `gpt-oss` models also expose + `bedrock-runtime` (Converse / InvokeModel). This removes the usual "fall back to Converse for dedicated + throughput or Bedrock-native features" escape hatch. +3. **No cross-region inference.** Geo inference ID and Global inference ID are both "Not supported" on every GPT + model card. These are in-region only — there is no CRIS inference profile, and `bedrock:ListInferenceProfiles` + will not return them (see `llm-to-bedrock` → `resolve-bedrock-model-id`). + +**Chat Completions is unverified for these models.** The docs' API-compatibility matrix renders its support marks as +empty cells, so it cannot be read programmatically or by eye. Every AWS code sample and the launch post use +**Responses** exclusively, and no GPT model card lists Chat Completions among supported features. Treat Responses as +the only verified surface; if a source app is built on Chat Completions, plan for a reshape to Responses and probe +the target account before committing. + +### Client setup + +Requires the OpenAI SDK at **>= 2.45.0**. Preferred client auto-refreshes a short-term Bedrock token: + +```python +from aws_bedrock_token_generator import provide_token +from openai import BedrockOpenAI + +region = "us-east-1" +client = BedrockOpenAI( + aws_region=region, + bedrock_token_provider=lambda: provide_token(region=region), + max_retries=6, +) + +response = client.responses.create( + model="openai.gpt-5.6-terra", + input="...", + reasoning={"effort": "medium"}, +) +``` + +The alternative — `OpenAI(base_url=".../openai/v1", api_key=os.environ["AWS_BEARER_TOKEN_BEDROCK"])` — uses a key +that expires within 12 hours and is not refreshed. Do not recommend it for production. + +**IAM:** the managed policy `AmazonBedrockMantleInferenceAccess` grants what inference needs, including +`bedrock-mantle:CreateInference` and `bedrock-mantle:CallWithBearerToken`. Note these are `bedrock-mantle:*` actions +— an IAM policy scoped only to `bedrock:InvokeModel` will not authorize these models. + +**Reasoning effort:** all five accept `none`, `low`, `medium`, `high`, `xhigh`, `max`. Because these models reason +before responding, the model's output items (which may include reasoning items) must be passed back in the next +request for multi-turn and tool-calling flows. + +--- + +## Regional Availability + +In-region only. This is the tightest constraint in the whole path — every other Bedrock target in this plugin is +available in more regions than these. + +| Model | us-east-1 | us-east-2 | us-west-2 | us-gov-west-1 | +| ------------- | --------- | --------- | --------- | ------------- | +| GPT-5.6 Sol | yes | yes | — | — | +| GPT-5.6 Terra | yes | yes | yes | — | +| GPT-5.6 Luna | yes | yes | yes | — | +| GPT-5.5 | yes | yes | — | — | +| GPT-5.4 | yes | yes | yes | yes | + +If the migration's target region is not in this table for the selected model, the same-model path is **unavailable** +— there is no cross-region fallback. Either change the target region or take the cross-family path. + +--- + +## Pricing + +**Bedrock charges OpenAI's first-party rates**, with no AWS markup, and usage counts toward existing AWS +commitments. Both AWS GA announcements and the July 30 pricing update state this explicitly. The practical +consequence for migration analysis: **cost is neutral on a same-model move**, so cost can no longer be the argument +for or against migrating a GPT workload to Bedrock. + +Effective **July 30, 2026**, tracking OpenAI's first-party cut: **Luna −80%**, **Terra −20%**, **Sol unchanged**. + +| Model | Input $/1M | Output $/1M | Provenance | +| ------------- | ------------ | ------------ | -------------------------------------------------------------- | +| GPT-5.6 Luna | 0.20 | 1.20 | AWS News Blog, effective 2026-07-30 — **verified** | +| GPT-5.6 Terra | _unverified_ | _unverified_ | −20% on 2026-07-30; absolute rate not published in AWS sources | +| GPT-5.6 Sol | _unverified_ | _unverified_ | unchanged on 2026-07-30; absolute rate not published | +| GPT-5.5 | 5.00 | 30.00 | OpenAI first-party parity — **confirm before quoting** | +| GPT-5.4 | 2.50 | 15.00 | OpenAI first-party parity — **confirm before quoting** | + +> **The AWS Price List API does not carry these models.** A `get_pricing` query against `AmazonBedrock` returns +> `gpt-oss` and GPT OSS Safeguard only; filtering on `GPT-5`, or on a `gpt-5` usage type, returns zero rows +> (checked against price-list publication 2026-08-04). The `awspricing` MCP is therefore **not** a usable source for +> GPT-5.x rates, and an empty result must not be read as "model unavailable." Rows marked _unverified_ above must be +> read off the [Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/) (OpenAI tab) before they appear in +> any cost estimate. Do not derive them from percentage deltas and present the result as a quoted rate. + +### Prompt caching — GPT-5.6 only + +Listed as a supported feature on the Sol, Terra, and Luna model cards. The GPT-5.5 and GPT-5.4 cards list +client-side tool calling in that slot instead and do **not** list prompt caching. Do not assume caching on 5.5/5.4. + +| Property | Value | +| ----------------- | ------------------------------------------------------------ | +| Cached input read | 90% discount vs uncached input | +| Cache write | 1.25x the uncached input rate | +| Minimum prefix | 1,024 tokens (below this nothing caches, `cached_tokens`= 0) | +| Breakpoints | up to 4 per request | +| Retention | at least 30 minutes | +| Modes | implicit (on by default) and explicit (cache breakpoints) | + +Explicit mode uses `prompt_cache_options={"mode": "explicit"}` plus a `prompt_cache_breakpoint` on the content block +ending the reusable prefix; a stable `prompt_cache_key` improves match reliability. Cached input tokens **do not +count against the input-TPM quota**, which compounds the benefit at scale. + +--- + +## Quotas + +Inference on `bedrock-mantle` is governed by **two per-model, per-region quotas: input tokens per minute and output +tokens per minute. There is no requests-per-minute quota.** Exceeding a TPM quota returns HTTP 429. + +This corrects two claims that were previously applied to all Mantle traffic in this plugin: + +- There is **no shared 10,000 RPM account limit** governing these models — the quota dimension is TPM, per model, + per region. +- "Switch to `bedrock-runtime` for dedicated throughput" is **not an available remedy**, because these models have + no `bedrock-runtime` path at all (see Access Path above). + +The supported mitigations are: exponential backoff with a bounded retry count (`max_retries` on the OpenAI SDK), +spreading load across minutes rather than bursting, ramping request rate gradually, and prompt caching (cached input +is exempt from the input-TPM quota). For sustained volume beyond that, pursue a quota increase. + +Service tiers Standard / Priority / Flex / Reserved are listed on the model cards, but the tier-support marks render +as empty cells; the launch post states GPT-5.6 on-demand runs on **Standard**. Verify tier availability per model +before recommending Flex or Reserved for cost reduction. + +--- + +## Features With No Bedrock Equivalent + +These are the remaining legitimate reasons to keep a workload on OpenAI's own API. Cost is no longer one of them. + +| OpenAI capability | Status on Bedrock | +| --------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| Realtime API | No equivalent | +| Image generation (gpt-image) | Not an OpenAI model on Bedrock; use Stability AI (see lifecycle) | +| Whisper (STT) / TTS | Amazon Transcribe / Polly — different service, API, pricing model | +| Embeddings (`text-embedding-3-*`) | No OpenAI embedding model on Bedrock; use Titan Embeddings v2 | +| Assistants API with file search, vector stores, code interpreter | No direct equivalent — see the decision tree in the mapping guide | +| A model not in the catalog above (GPT-4o, o-series, `*-Pro`, GPT-5/5.1/5.2) | No same-model target; cross-family or upgrade required | + +**Data handling:** these are third-party models under OpenAI terms. Classifier-flagged traffic is retained up to 30 +days for automated abuse detection; retained inputs/outputs are stored and processed by AWS and not shared with +OpenAI unless the customer opts in. Prompts and completions are not used to train models. Calls run under the +customer's IAM policies, inside their VPC, logged to CloudTrail, and in-region inference keeps data in-region. + +**Codex on Bedrock is GA** with pay-per-token pricing, inference through Bedrock, and usage counting toward AWS +commitments — relevant when the source workload is a coding agent. + +--- + +## Refresh Checklist + +This model family is moving fast (two GA waves and a repricing inside 10 weeks). On each refresh: + +1. Re-read the [OpenAI model card index](https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards-openai.html) + for models added or removed, and each per-model card for lifecycle state and EOL date. +2. Recheck the region matrix — every model here is in-region only, so region changes are migration-blocking. +3. Recheck rates on the Bedrock pricing page OpenAI tab, and resolve any row still marked _unverified_. +4. Recheck whether the Price List API has gained GPT-5.x coverage; if it has, drop the caveat above and let + `estimate-ai.md` price these models from the MCP. +5. Recheck whether Chat Completions and `bedrock-runtime` support have been added or clarified. +6. Feed any lifecycle change into `ai-model-lifecycle.md` and any rate change into `pricing-cache.md`. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/pricing-cache.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/pricing-cache.md index 533d91a9..40f11417 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/pricing-cache.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/pricing-cache.md @@ -401,6 +401,11 @@ See `shared/ai-model-lifecycle.md` for lifecycle details. **Do not recommend Leg | Mistral Large 3 | mistral.mistral-large-3-675b-instruct | Mistral | 0.50 | 1.50 | 256K | flagship | active | | DeepSeek-R1 | deepseek.r1-v1:0 | DeepSeek | 1.35 | 5.40 | 128K | reasoning | active | | DeepSeek-V3.1 | — | DeepSeek | 0.58 | 1.68 | — | mid | active (Sydney only) | +| GPT-5.6 Luna | openai.gpt-5.6-luna | OpenAI | 0.20 | 1.20 | 1M | fast | active (mantle only) | +| GPT-5.6 Terra | openai.gpt-5.6-terra | OpenAI | unverified | unverified | 1M | flagship | active (mantle only) | +| GPT-5.6 Sol | openai.gpt-5.6-sol | OpenAI | unverified | unverified | 1M | frontier | active (mantle only) | +| GPT-5.5 | openai.gpt-5.5 | OpenAI | 5.00 | 30.00 | 272K | frontier | active (mantle only) | +| GPT-5.4 | openai.gpt-5.4 | OpenAI | 2.50 | 15.00 | 272K | flagship | active (mantle only) | | gpt-oss-20b | openai.gpt-oss-20b-1:0 | OpenAI | 0.07 | 0.30 | 128K | budget | active | | gpt-oss-120b | openai.gpt-oss-120b-1:0 | OpenAI | 0.15 | 0.60 | 128K | efficient | active | | Gemma 3 4B IT | google.gemma-3-4b-it | Google | 0.04 | 0.08 | 128K | budget | active | @@ -506,9 +511,32 @@ Per [Amazon Bedrock pricing](https://aws.amazon.com/bedrock/pricing/) (DeepSeek) - **Asia Pacific (Mumbai)**, **São Paulo**, **Jakarta**, **Tokyo**, **Stockholm**: DeepSeek v3.2 **$0.74 / $2.22** per 1M. - **Asia Pacific (Sydney)**: DeepSeek v3.1 **$0.5974 / $1.7304**; v3.2 **$0.6386 / $1.9055** (and **Priority** / **Flex** tiers listed separately on the page). -### OpenAI on Bedrock (gpt-oss) +### OpenAI on Bedrock -**Standard tier** per [Amazon Bedrock pricing](https://aws.amazon.com/bedrock/pricing/) (OpenAI). +See `shared/openai-on-bedrock.md` for model IDs, endpoint paths, regions, quotas, and caching rules. **Bedrock +charges OpenAI's first-party rates** for the proprietary GPT models, so a same-model migration is cost-neutral. + +#### Proprietary GPT models (Standard tier, `bedrock-mantle` only) + +| Model | Input $/1M | Output $/1M | Notes | +| ------------- | ------------ | ------------ | ------------------------------------------------------ | +| GPT-5.6 Luna | 0.20 | 1.20 | verified; −80% effective 2026-07-30 | +| GPT-5.6 Terra | _unverified_ | _unverified_ | −20% effective 2026-07-30; absolute rate not published | +| GPT-5.6 Sol | _unverified_ | _unverified_ | unchanged on 2026-07-30; absolute rate not published | +| GPT-5.5 | 5.00 | 30.00 | first-party parity — confirm on the pricing page | +| GPT-5.4 | 2.50 | 15.00 | first-party parity — confirm on the pricing page | + +> **These models are absent from the AWS Price List API.** Querying `AmazonBedrock` returns only `gpt-oss` and GPT +> OSS Safeguard; filtering on `GPT-5` or a `gpt-5` usage type returns zero rows (price-list publication 2026-08-04). +> The `awspricing` MCP cannot price them, and an empty result is **not** evidence the model is unavailable. Resolve +> every _unverified_ row from the [Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/) (OpenAI tab) before +> using it in an estimate; never publish a rate derived from a percentage delta. + +**Prompt caching (GPT-5.6 only):** cached input read at a 90% discount, cache write at 1.25x uncached input, minimum +1,024-token prefix, up to 4 breakpoints, ≥30-minute retention. Cached input is exempt from the input-TPM quota. +GPT-5.5 and GPT-5.4 do not list prompt caching support. + +#### Open-weight gpt-oss | Model | Region | Input $/1M | Output $/1M | | ------------ | --------------------- | ---------- | ----------- | @@ -753,24 +781,33 @@ Prices per 1M tokens. Source: [ai.google.dev/gemini-api/docs/pricing](https://ai Prices per 1M tokens. GPT-5.5 and GPT-5.5 Pro use the same breakpoint pricing structure as GPT-5.4 at 272K input tokens. GPT-5.4 and GPT-5.4 Pro use **breakpoint pricing** at 272K input tokens: rates below are for <272K context; above 272K, input is 2x and output is 1.5x. -| Model | Input $/1M | Output $/1M | Context | Tier | -| ------------ | ---------- | ----------- | ------- | --------- | -| GPT-5.5 | 5.00 | 30.00 | 1M | flagship | -| GPT-5.5 Pro | 30.00 | 180.00 | 1M | premium | -| GPT-5.4 | 2.50 | 15.00 | 1.05M | flagship | -| GPT-5.4 Mini | 0.75 | 4.50 | — | fast | -| GPT-5.4 Nano | 0.20 | 1.25 | — | budget | -| GPT-5.4 Pro | 30.00 | 180.00 | 1.05M | premium | -| GPT-5.2 | 1.75 | 14.00 | 200K | flagship | -| GPT-5.1 | 1.25 | 10.00 | 200K | flagship | -| GPT-5 Mini | 0.25 | 2.00 | 200K | fast | -| GPT-5 Nano | 0.05 | 0.40 | 128K | budget | -| GPT-4.1 | 2.00 | 8.00 | 1M | flagship | -| GPT-4.1 Mini | 0.40 | 1.60 | 1M | fast | -| GPT-4.1 Nano | 0.10 | 0.40 | 1M | budget | -| GPT-4o | 2.50 | 10.00 | 128K | flagship | -| o3 | 2.00 | 8.00 | 200K | reasoning | -| o4-mini | 1.10 | 4.40 | 200K | reasoning | +> **Parity note — do not run a savings calculation for these.** GPT-5.6 Sol / Terra / Luna, GPT-5.5, and GPT-5.4 are +> available on Bedrock at OpenAI's first-party rates (see the **OpenAI on Bedrock** section above). For those five, +> source price and Bedrock price are the same number, so the migration case rests on AWS commitments, governance, +> prompt caching, and data residency — not on a price delta. The rows below still matter for models with **no** +> Bedrock equivalent (GPT-5.x Pro, GPT-5.2/5.1, GPT-4.x, o-series), where a cross-family comparison is real. + +| Model | Input $/1M | Output $/1M | Context | Tier | +| ------------- | ------------ | ------------ | ------- | --------- | +| GPT-5.6 Sol | _unverified_ | _unverified_ | 1M | frontier | +| GPT-5.6 Terra | _unverified_ | _unverified_ | 1M | flagship | +| GPT-5.6 Luna | 0.20 | 1.20 | 1M | fast | +| GPT-5.5 | 5.00 | 30.00 | 1M | flagship | +| GPT-5.5 Pro | 30.00 | 180.00 | 1M | premium | +| GPT-5.4 | 2.50 | 15.00 | 1.05M | flagship | +| GPT-5.4 Mini | 0.75 | 4.50 | — | fast | +| GPT-5.4 Nano | 0.20 | 1.25 | — | budget | +| GPT-5.4 Pro | 30.00 | 180.00 | 1.05M | premium | +| GPT-5.2 | 1.75 | 14.00 | 200K | flagship | +| GPT-5.1 | 1.25 | 10.00 | 200K | flagship | +| GPT-5 Mini | 0.25 | 2.00 | 200K | fast | +| GPT-5 Nano | 0.05 | 0.40 | 128K | budget | +| GPT-4.1 | 2.00 | 8.00 | 1M | flagship | +| GPT-4.1 Mini | 0.40 | 1.60 | 1M | fast | +| GPT-4.1 Nano | 0.10 | 0.40 | 1M | budget | +| GPT-4o | 2.50 | 10.00 | 128K | flagship | +| o3 | 2.00 | 8.00 | 200K | reasoning | +| o4-mini | 1.10 | 4.40 | 200K | reasoning | ## Security Baseline diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/SKILL.md b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/SKILL.md index 583dfdb8..51b6d031 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/SKILL.md +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/SKILL.md @@ -241,7 +241,8 @@ first failing model) plus `failing_models` (all failing ids); per-model verdicts - `ok == false` + `reason: credentials` → show the detail (configure/refresh credentials), stop; user re-runs after fixing. - `ok == false` + `reason: model_access` → model access not enabled in the Bedrock console (NOT an IAM problem): point the user at the console Model access page for the failing models, stop; re-run B4 after they enable it. -- `ok == false` + `reason: authz` → IAM denies `bedrock:InvokeModel`: tell user the IAM action to grant; stop. +- `ok == false` + `reason: authz` → IAM denies inference. For a Converse/InvokeModel target the action to grant is `bedrock:InvokeModel`; for a mantle-only `openai.gpt-5*` target it is the `bedrock-mantle:*` set (see B4a). The `detail` names which. Tell the user the action to grant; stop. +- `ok == false` + `reason: mantle_deps_missing` → the pinned scripts environment lacks `openai` / `aws-bedrock-token-generator`, so a mantle-only target could not be probed at all. This is an environment fault, not a Bedrock verdict: tell the user to re-sync (`uv sync --project $SCRIPTS`) and stop. Do NOT proceed — access was never verified. - `ok == false` + `reason: model_unavailable` → Read the `resolve-bedrock-model-id` reference at `$HELPERS/resolve-bedrock-model-id/resolve-bedrock-model-id.md` and follow its procedure with each ID from `failing_models` + region. AskUserQuestion with the candidates: "Use `` (cross-region inference profile)" / "Paste a different model ID" / "Abort". On a choice, replace the ID in `$TARGET_MODELS` and re-run B4. - `ok == false` + any other `reason` → show `detail` and stop. - `ok == true` → proceed. Surface any `quota_warning`, and any model whose `reason` is @@ -414,6 +415,16 @@ prior-phase file paths), then validate its output file: - `model_unresolvable` → user picks/pastes an ID → record it in `resolved_model_overrides`, fold it into the Target line - `source_key_auth` → user supplies a new key (re-run B3) or sets baseline unavailable +- `authz` → IAM denies inference. The `detail` names the action set to grant: + `bedrock:InvokeModel*` for a Converse target, or the `bedrock-mantle:*` actions + (`CreateInference`, `CallWithBearerToken`) for a mantle-only `openai.gpt-5*` target. + User fixes IAM; nothing fingerprinted changes, so re-dispatch the blocked phase only. + Do NOT route this to `model_access` — the console Model access page is the wrong fix + for an IAM denial and vice versa. +- `mantle_deps_missing` → the pinned scripts environment lacks `openai` / + `aws-bedrock-token-generator`, so a mantle target could not be probed at all. User + re-syncs (`uv sync --project $SCRIPTS`); re-dispatch the blocked phase only. Access was + never verified, so do not treat a previous pass as still valid. - `assess_output_missing` → re-run Phase A, then restart Phase C at C0 After ANY resolution, re-run the C0 recipe (rebuild current-context, apply the invalidation @@ -445,10 +456,16 @@ Below, AskUserQuestion: **Gate (a.5) — Rewrite strategy (from migration plan).** Read `migration_path` from `$MIGRATION_DIR/aws-design-ai.json` → `ai_architecture.code_migration.migration_path`. -If the value is `"mantle"`, set `rewrite_strategy = "mantle"`. Otherwise (value is -`"converse"`, `"gpt-oss"`, or the field is absent), set `rewrite_strategy = "converse"`. +If the value **starts with** `"mantle"` (`"mantle"`, `"mantle_openai_responses"`), set +`rewrite_strategy = "mantle"`. Otherwise (value is `"converse"`, `"gpt-oss"`, or the field is +absent), set `rewrite_strategy = "converse"`. No user question needed — the decision was already made during the Assess/Design phase. +Match on the prefix, not on equality: Design writes the more specific +`"mantle_openai_responses"` for a same-model OpenAI migration, and an equality check against +`"mantle"` would silently route those runs down the Converse path — rewriting working +same-model code into a boto3 Converse client against a model that has no Converse surface. + **Gate (b) — Behavior-delta resolution.** For each `analysis.behavior_deltas[]` with `user_visible == true`, AskUserQuestion with the options from the `behavior-delta-detection` reference (Read `$HELPERS/behavior-delta-detection/behavior-delta-detection.md`, and the @@ -476,6 +493,14 @@ When `rewrite_strategy == "mantle"`, C5's context block ALSO includes: signal for the default Converse path) - `Mantle model map: -> ` — sourced from the plan's `ai_architecture.bedrock_models[]` entries (each `source_model` → `aws_model_id` pair). +- `Mantle surface: responses` and `Mantle base path: /openai/v1` when any mapped + `aws_model_id` is a proprietary GPT model (`openai.gpt-5*`). These are served only on the + `/openai/v1` path via the Responses API — distinct from the `v1` path other mantle models + use — so the rewriter must not emit a `/v1` base URL or a Chat Completions call for them. + See `gcp-to-aws/references/shared/openai-on-bedrock.md`. +- `Same model: true` when `bedrock_models[].model_change` is `false`. Signals the rewriter to + keep model parameters untouched and limit changes to the endpoint, credential, model id, and + (if the source used Chat Completions) the surface reshape. ### C7 — Render summary diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/behavior-delta-detection/behavior-delta-detection.md b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/behavior-delta-detection/behavior-delta-detection.md index 5ca64204..fd401610 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/behavior-delta-detection/behavior-delta-detection.md +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/behavior-delta-detection/behavior-delta-detection.md @@ -12,6 +12,18 @@ The motivation is to prevent silent UX changes during migration. Example: OpenAI For Anthropic 1P → Bedrock (`same_model_family: true`), parameter surfaces are identical — skip this skill entirely. For custom OpenAI-compatible providers (Together, Fireworks, etc.), v1 also skips — emit `behavior_deltas: []`. +**OpenAI → the same GPT model on Bedrock is also `same_model_family: true`.** When the target is a proprietary GPT model on `bedrock-mantle` (`openai.gpt-5.6-sol` / `-terra` / `-luna`, `openai.gpt-5.5`, `openai.gpt-5.4`), the model is unchanged, so the model-parameter deltas in `references/openai-to-bedrock.md` — temperature range, penalty parameters, stop-sequence limits — **do not apply**. Applying them would prompt the user to accept range changes that are not happening. + +That case is not delta-free, though: the **API surface** can change (Chat Completions → Responses) and reasoning models require reasoning items to be echoed back across turns. Read only the "Same-model (mantle) deltas" section of `references/openai-to-bedrock.md` for those, and skip the parameter-surface blocks. + +Decide from the resolved target model id, not from `source_provider`: + +| Source | Target model id | What to load | +| ------ | ------------------------ | --------------------------------------------------------- | +| openai | `openai.gpt-5*` (mantle) | Same-model (mantle) deltas ONLY | +| openai | Claude / Nova / DeepSeek | Full parameter-surface deltas (cross-family) | +| openai | `openai.gpt-oss-*` | Full parameter-surface deltas — different model, Converse | + ## Choose the right reference | source_provider | reference file | diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/behavior-delta-detection/references/openai-to-bedrock.md b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/behavior-delta-detection/references/openai-to-bedrock.md index 7c828007..541be772 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/behavior-delta-detection/references/openai-to-bedrock.md +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/behavior-delta-detection/references/openai-to-bedrock.md @@ -10,6 +10,60 @@ In the `detect_grep` recipes below, `` is the repository path supplied in --- +## Same-model (mantle) deltas + +**Read this section INSTEAD of the parameter-surface sections below when the resolved target is a proprietary GPT model on `bedrock-mantle`** (`openai.gpt-5.6-sol` / `-terra` / `-luna`, `openai.gpt-5.5`, `openai.gpt-5.4`). The model is unchanged, so temperature ranges, penalty parameters, and stop-sequence limits are unchanged — do not raise those as deltas. The deltas here are about the API surface and the endpoint, not about model behavior. + +## chat-completions-to-responses + +- `resolution_kind`: `ux_choice` +- Source (OpenAI): `client.chat.completions.create(...)`, `messages=[...]`, reads `choices[0].message.content` +- Target (mantle GPT): `client.responses.create(...)`, `input=...`, reads `output_text` +- Chat Completions support is **unverified** for these models — every AWS sample uses Responses, and no GPT model card lists Chat Completions as supported. Treat a Chat Completions source as requiring a reshape, and probe the target account before committing to either surface. + +### detect_grep + +```bash +grep -rEn 'chat\.completions\.create' --include="*.py" --include="*.js" --include="*.ts" | grep -v node_modules | grep -v __pycache__ +grep -rEn 'choices\[0\]\.(message|delta)' --include="*.py" --include="*.js" --include="*.ts" | grep -v node_modules | grep -v __pycache__ +``` + +`user_visible` classification: `true` when the response shape is surfaced to users or persisted (chat transcript rendering, stored conversation history, streaming to a UI); `false` for internal one-shot calls whose text is consumed programmatically. + +## reasoning-items-must-round-trip + +- `resolution_kind`: `impl_path` +- These models reason before responding. In multi-turn and tool-calling flows the model's output items — which may include reasoning items — must be appended to the next request's `input`. Dropping them degrades multi-step and tool-use quality without raising an error, so this fails silently. +- Detection: any Responses-API call that builds the next turn's `input` from message text alone rather than appending `response.output`. + +### detect_grep + +```bash +grep -rEn 'responses\.create' --include="*.py" --include="*.js" --include="*.ts" | grep -v node_modules | grep -v __pycache__ +grep -rEn 'function_call_output|tool_call' --include="*.py" --include="*.js" --include="*.ts" | grep -v node_modules | grep -v __pycache__ +``` + +## endpoint-path-and-credential + +- `resolution_kind`: `impl_path` +- Base URL must be `https://bedrock-mantle.{region}.api.aws/openai/v1` — the `openai/v1` segment is required and differs from the `v1` path other mantle models use. A hardcoded `/v1` returns 404. +- The API key must be a Bedrock API key or an auto-refreshing token provider, **not** an existing OpenAI key. A long-lived `OPENAI_API_KEY` read from the environment will fail authentication. +- IAM must grant `bedrock-mantle:*` actions; `bedrock:InvokeModel` does not authorize these models. +- Not user-visible — always `user_visible: false`, `resolution_kind: impl_path`. Apply without prompting. + +## prompt-caching-availability + +- `resolution_kind`: `impl_path` +- Prompt caching is listed as supported on GPT-5.6 only. Do not emit caching configuration for `openai.gpt-5.5` or `openai.gpt-5.4`. + +--- + +## Cross-family parameter-surface deltas + +Everything below applies when the target is **not** the same model — Claude, Nova, DeepSeek, or `gpt-oss`. + +--- + ## temperature-range-mismatch - `resolution_kind`: `ux_choice` diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/resolve-bedrock-model-id/resolve-bedrock-model-id.md b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/resolve-bedrock-model-id/resolve-bedrock-model-id.md index 2e61d479..276319fa 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/resolve-bedrock-model-id/resolve-bedrock-model-id.md +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/references/helpers/resolve-bedrock-model-id/resolve-bedrock-model-id.md @@ -14,6 +14,42 @@ the user to choose when the match is ambiguous. ## Procedure +### Step 0: Route mantle-only models away from inference-profile resolution + +**Check this before Step 1.** If `plan_model_id` matches `^openai\.gpt-5` (the proprietary GPT models — GPT-5.6 +Sol/Terra/Luna, GPT-5.5, GPT-5.4), the inference-profile path below **cannot** resolve it and will always fail: + +- These models are served only on the `bedrock-mantle` endpoint. They have no `bedrock-runtime` model and no + inference profile, so `aws bedrock list-inference-profiles` never returns them. +- They do not support Geo or Global cross-region inference, so there is no `us.` / `global.` / `eu.` prefixed variant + to rank against. The Step 3 token ranking would score every candidate at near-zero overlap and Step 4 would return + `blocked: model_unresolvable` for an ID that is in fact perfectly valid. + +For these IDs, validate against the mantle catalog instead: + +```bash +aws bedrock list-foundation-models \ + --region \ + when your context has an `AWS profile` line> \ + --query "modelSummaries[?starts_with(modelId, 'openai.')].[modelId,modelName]" \ + --output json +``` + +- **Exact match** on `plan_model_id` → return it unchanged. Do not add a regional prefix and do not strip or append a + `-v1:0`-style suffix; the mantle ID form is the literal `openai.gpt-5.6-terra` shape. +- **No match** → the model is not enabled or not available in this region. Return `blocked` with + `reason: model_unavailable_in_region` and put the region plus the `openai.*` IDs that _were_ returned in `detail`, + so the orchestrator can offer them. Because these models are in-region only, the remedy is a **region change or a + different model** — never a cross-region inference profile. +- If the CLI call itself fails or the account lacks `bedrock:ListFoundationModels`, return `blocked` with + `reason: model_unverifiable` rather than guessing. + +Also note for the caller: these models need `bedrock-mantle:*` IAM actions (e.g. via +`AmazonBedrockMantleInferenceAccess`), not `bedrock:InvokeModel`. A resolution success here does not imply the caller +is authorized to invoke. + +Non-`openai.gpt-5*` IDs continue to Step 1 unchanged. + ### Step 1: List live inference profiles ```bash @@ -84,7 +120,12 @@ stops on abort. ## Notes - This skill is idempotent: calling it twice with the same already-validated - ID will hit Step 2 and return immediately. + ID will hit Step 0 (mantle) or Step 2 (inference profile) and return immediately. +- Steps 1–5 assume the target is a `bedrock-runtime` model reachable through an + inference profile. Mantle-only models are handled entirely in Step 0 and never + reach the token-ranking logic. See + `gcp-to-aws/references/shared/openai-on-bedrock.md` for the authoritative list + of mantle-only model IDs and their regions. - Output of this skill should replace the plan's `target_model_id` in the caller's context — downstream phases (evaluator, rewriter) receive the validated ID only. diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/bedrock_pricing.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/bedrock_pricing.py index 10d9b186..cf851e3b 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/bedrock_pricing.py +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/bedrock_pricing.py @@ -51,9 +51,26 @@ def parse_price_dimensions(price_item: dict) -> dict: "amazon.nova-micro-v1:0": {"input_per_1k_usd": 0.000035, "output_per_1k_usd": 0.00014}, "amazon.nova-lite-v1:0": {"input_per_1k_usd": 0.00006, "output_per_1k_usd": 0.00024}, "amazon.nova-pro-v1:0": {"input_per_1k_usd": 0.0008, "output_per_1k_usd": 0.0032}, + # OpenAI proprietary GPT models (bedrock-mantle). Bedrock charges OpenAI's + # first-party rates. The PriceList API does not carry these models at all, so + # this table is the ONLY source — there is no live-lookup fallback for them. + # Luna: verified, reflects the 2026-07-30 reduction. 5.5 / 5.4: first-party parity. + # GPT-5.6 Terra and Sol are deliberately ABSENT — their post-2026-07-30 rates are + # not published in any AWS source, and inventing them would silently corrupt every + # downstream cost estimate. They resolve to `unavailable` with an actionable note. + "openai.gpt-5.6-luna": {"input_per_1k_usd": 0.0002, "output_per_1k_usd": 0.0012}, + "openai.gpt-5.5": {"input_per_1k_usd": 0.005, "output_per_1k_usd": 0.03}, + "openai.gpt-5.4": {"input_per_1k_usd": 0.0025, "output_per_1k_usd": 0.015}, } +def is_mantle_gpt(model_id: str) -> bool: + """Pure: OpenAI's proprietary GPT models, which the AWS PriceList API does not + carry. The open-weight gpt-oss models ARE in the PriceList API and must not match.""" + mid = model_id.lower() + return mid.startswith("openai.gpt-5") and "oss" not in mid + + def unavailable(note: str) -> dict: return {"available": False, "input_per_1k_usd": None, "output_per_1k_usd": None, "note": f"Pricing unavailable: {note}"} @@ -64,6 +81,11 @@ def _static_fallback(model_id: str) -> dict | None: entry = STATIC_FALLBACK.get(model_id) if entry: return {**entry, "available": True, "note": "static fallback (PriceList API had no entry)"} + # Mantle GPT ids require an EXACT match. Their tier names differ only by suffix + # (`openai.gpt-5.6-sol` / `-terra` / `-luna`) at very different price points, so a + # prefix match on e.g. `openai.gpt-5.6` would silently bill Sol's traffic at Luna's rate. + if is_mantle_gpt(model_id): + return None # Try stripping the version suffix for a partial match (e.g. us.anthropic.claude-sonnet-4-6) base = model_id.rsplit("-v", 1)[0] if "-v" in model_id else model_id for key, val in STATIC_FALLBACK.items(): @@ -102,6 +124,16 @@ def lookup(region: str, model_id: str) -> dict: fb["note"] = ("static pricing table (verified 2026-08-04 against " "aws.amazon.com/bedrock/pricing and the vendored pricing cache)") return fb + if is_mantle_gpt(model_id): + # Short-circuit: the PriceList API carries no rows for the proprietary GPT + # models, so a live lookup would burn a round trip and still return nothing — + # and a generic "unavailable" would read as "this model doesn't exist". + return unavailable( + f"the AWS PriceList API does not carry OpenAI's proprietary GPT models, and " + f"{model_id} is not in the static table. This does NOT mean the model is " + f"unavailable. Read the rate from the OpenAI tab of " + f"aws.amazon.com/bedrock/pricing and add it to STATIC_FALLBACK. Do not derive " + f"it from a percentage change to an older rate.") import boto3 from botocore.exceptions import BotoCoreError, ClientError try: diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/iam_policy.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/iam_policy.py index 9b873f41..ce73cc6b 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/iam_policy.py +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/iam_policy.py @@ -16,6 +16,23 @@ def is_inference_profile(model_id: str) -> bool: return bool(_GEO_PREFIX.match(model_id)) +def is_mantle_model(model_id: str) -> bool: + """True for OpenAI's proprietary GPT models, which are served only on the + bedrock-mantle endpoint. They need `bedrock-mantle:*` actions — a policy + granting only `bedrock:InvokeModel` against a foundation-model ARN cannot + authorize them, and they have no inference profile to scope to either. + The open-weight gpt-oss models DO use bedrock-runtime and must not match.""" + mid = model_id.lower() + return mid.startswith("openai.gpt-5") and "oss" not in mid + + +def mantle_project_arn(region: str, account_id: str) -> str: + """ARN scope for mantle inference. Mantle authorizes at project granularity, + not per model, so this cannot be narrowed to specific model IDs — use a + service control policy to restrict the model set.""" + return f"arn:aws:bedrock-mantle:{region}:{account_id}:project/*" + + def foundation_model_arn(model_id: str) -> str: """ARN for a plain foundation-model ID (no geo prefix).""" return f"arn:aws:bedrock:*::foundation-model/{model_id}" @@ -29,12 +46,24 @@ def inference_profile_arn(model_id: str, region: str, account_id: str) -> str: def generate_policy(model_ids: list[str], region: str, account_id: str) -> dict: """Build a scoped IAM policy covering exactly the given model IDs. - Returns a policy dict with one Statement whose Resource list contains: - - foundation-model ARNs for plain model IDs - - inference-profile ARNs for geo-prefixed model IDs + Emits up to three statements, depending on which endpoints the targets use: + - `bedrock:InvokeModel*` scoped to foundation-model ARNs (plain IDs) and + inference-profile ARNs (geo-prefixed IDs), for bedrock-runtime targets + - `bedrock-mantle:CreateInference` / `Get*` / `List*` scoped to the account's + mantle projects, for mantle-only targets (SigV4 auth) + - `bedrock-mantle:CallWithBearerToken` on `*`, for Bedrock API-key auth + + A statement is omitted entirely when no target needs it — notably, an + all-mantle run must not emit an InvokeModel statement with an empty Resource + list, which is an invalid policy. """ + runtime_ids = [m for m in model_ids if not is_mantle_model(m)] + mantle_ids = [m for m in model_ids if is_mantle_model(m)] + + statements = [] + resources = [] - for mid in sorted(set(model_ids)): + for mid in sorted(set(runtime_ids)): if is_inference_profile(mid): resources.append(inference_profile_arn(mid, region, account_id)) base_id = _GEO_PREFIX.sub("", mid) @@ -42,20 +71,43 @@ def generate_policy(model_ids: list[str], region: str, account_id: str) -> dict: else: resources.append(foundation_model_arn(mid)) - return { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "BedrockInvokeModelScoped", - "Effect": "Allow", - "Action": [ - "bedrock:InvokeModel", - "bedrock:InvokeModelWithResponseStream", - ], - "Resource": sorted(set(resources)), - } - ], - } + # Emit unless the ONLY reason there are no resources is that every target is + # mantle-only. A genuinely empty model list keeps the legacy shape (a statement + # with an empty Resource) so existing callers and tests see no behaviour change. + if resources or not mantle_ids: + statements.append({ + "Sid": "BedrockInvokeModelScoped", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + ], + "Resource": sorted(set(resources)), + }) + + if mantle_ids: + statements.append({ + "Sid": "BedrockMantleInference", + "Effect": "Allow", + "Action": [ + "bedrock-mantle:CreateInference", + "bedrock-mantle:Get*", + "bedrock-mantle:List*", + ], + "Resource": mantle_project_arn(region, account_id), + }) + # CallWithBearerToken must be scoped to "*" — AWS does not support + # narrowing it. Required for Bedrock API-key (bearer token) auth, which is + # how the rewriter's generated client authenticates. Omit only if the app + # uses SigV4 exclusively. + statements.append({ + "Sid": "BedrockMantleCallWithBearerToken", + "Effect": "Allow", + "Action": ["bedrock-mantle:CallWithBearerToken"], + "Resource": "*", + }) + + return {"Version": "2012-10-17", "Statement": statements} if __name__ == "__main__": diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/image_input.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/image_input.py new file mode 100644 index 00000000..05fe5bab --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/image_input.py @@ -0,0 +1,90 @@ +"""Build image-bearing request payloads for Converse and the Responses API. + +Two mistakes are easy to make by hand and both produce a confusing failure mode +where the vision smoke test passes (it hardcodes a known-good jpeg) while every +golden case fails: + +1. **Extension is not the wire format.** A `.jpg` file is `jpeg` for Converse and + `image/jpeg` for Responses. Passing `jpg` / `image/jpg` is rejected. +2. **Responses content must be wrapped in a message item.** A bare + `[{"type": "input_text", ...}]` list is not a valid `input`; it has to be + `[{"role": "user", "content": [...]}]`. +3. **`detail` is required on a Responses image block.** The pinned OpenAI SDK + declares `ResponseInputImageParam.detail` as `Required`, so omitting it is a + contract violation even though a TypedDict will not catch it at runtime. + +Callers should use `converse_message` / `responses_message` rather than assembling +these dicts inline, so the shape is decided in one tested place. +""" +from pathlib import Path + +# Accepted by ResponseInputImageParam.detail; "auto" is the SDK's documented default. +DETAIL_LEVELS = ("low", "high", "auto", "original") +DEFAULT_DETAIL = "auto" + +# Formats Bedrock Converse accepts, keyed by the extensions that map onto them. +_CONVERSE_FORMATS = { + "jpg": "jpeg", + "jpeg": "jpeg", + "png": "png", + "gif": "gif", + "webp": "webp", +} + + +def converse_format(image_path: str) -> str: + """Converse `image.format` value for a file path. Raises on unsupported types. + + Note `.jpg` -> `jpeg`: the extension and the wire format differ, and Converse + rejects `jpg`. + """ + ext = Path(image_path).suffix.lstrip(".").lower() + if ext not in _CONVERSE_FORMATS: + raise ValueError( + f"unsupported image type {ext!r} for {image_path}: " + f"Bedrock accepts {sorted(set(_CONVERSE_FORMATS.values()))}") + return _CONVERSE_FORMATS[ext] + + +def mime_type(image_path: str) -> str: + """MIME type for a data URL, e.g. `.jpg` -> `image/jpeg` (never `image/jpg`).""" + return f"image/{converse_format(image_path)}" + + +def data_url(image_path: str, raw: bytes) -> str: + """base64 data URL for the Responses API `input_image.image_url` field.""" + import base64 + return f"data:{mime_type(image_path)};base64,{base64.b64encode(raw).decode()}" + + +def converse_message(prompt: str, image_path: str | None = None, + raw: bytes | None = None) -> dict: + """One Converse `messages[]` item. Image block precedes the text block.""" + content: list[dict] = [] + if image_path is not None: + if raw is None: + raise ValueError("raw image bytes are required when image_path is given") + content.append({"image": {"format": converse_format(image_path), + "source": {"bytes": raw}}}) + content.append({"text": prompt}) + return {"role": "user", "content": content} + + +def responses_message(prompt: str, image_path: str | None = None, + raw: bytes | None = None, detail: str = DEFAULT_DETAIL) -> dict: + """One Responses API `input[]` item, wrapped as a user message. + + Returning the wrapper (rather than a bare content list) is the point: an + unwrapped list is invalid input, and that is the easiest thing to get wrong. + `detail` is always emitted because the SDK declares it Required. + """ + if detail not in DETAIL_LEVELS: + raise ValueError(f"invalid image detail {detail!r}: expected one of {list(DETAIL_LEVELS)}") + content: list[dict] = [{"type": "input_text", "text": prompt}] + if image_path is not None: + if raw is None: + raise ValueError("raw image bytes are required when image_path is given") + content.append({"type": "input_image", + "image_url": data_url(image_path, raw), + "detail": detail}) + return {"role": "user", "content": content} diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/preflight_bedrock.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/preflight_bedrock.py index 0589c9d4..96d266ae 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/preflight_bedrock.py +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/preflight_bedrock.py @@ -7,7 +7,9 @@ caller can parse the verdict instead of a traceback. On failure the top level carries `reason`/`detail`/`failing_models` lifted from the first failing model. Chat models are probed via Converse; embedding models (which don't speak -Converse) via InvokeModel with their family's request body. +Converse) via InvokeModel with their family's request body. OpenAI's proprietary +GPT models are `bedrock-mantle`-only and speak neither Converse nor InvokeModel, +so they are probed via the OpenAI Responses API against the mantle endpoint. The 1-token probe costs a fraction of a cent (noted in output). """ import argparse, json, sys @@ -53,6 +55,87 @@ def is_embedding_model(model_id: str) -> bool: return "embed" in model_id.lower() +def is_mantle_model(model_id: str) -> bool: + """Pure: OpenAI's proprietary GPT models are served only on the bedrock-mantle + endpoint via the OpenAI Responses API. They have no bedrock-runtime model, so + probing them with Converse/InvokeModel always fails with a misleading verdict. + The open-weight gpt-oss models DO speak Converse and must not match here.""" + mid = model_id.lower() + return mid.startswith("openai.gpt-5") and "oss" not in mid + + +def classify_mantle_error(status: int | None, message: str) -> dict: + """Pure: map an OpenAI-SDK HTTP status from the mantle endpoint to a verdict. + + Mirrors classify_invoke_error's contract but for the mantle surface, where the + remedies differ: IAM needs bedrock-mantle:* actions (not bedrock:InvokeModel), + and an unavailable model cannot be fixed with a cross-region inference profile + because these models are in-region only.""" + if status in (401, 403): + lowered = message.lower() + if ("model access" in lowered or "access to the model" in lowered + or "use the model" in lowered or "not authorized to use" in lowered): + return {"ok": False, "reason": "model_access", + "detail": f"Bedrock model access not enabled for this model — {message}. " + f"Enable it in the Bedrock console (Model access page); " + f"this is separate from IAM."} + return {"ok": False, "reason": "authz", + "detail": f"IAM denies mantle inference — {message}. Grant bedrock-mantle " + f"actions (e.g. the AmazonBedrockMantleInferenceAccess managed " + f"policy); bedrock:InvokeModel does NOT authorize these models."} + if status == 404: + return {"ok": False, "reason": "model_unavailable", + "detail": f"Model not available at this mantle endpoint/region — {message}. " + f"These models are in-region only (no cross-region inference " + f"profile); switch to a supported region or a different model."} + if status == 429: + # Reaching a token-per-minute ceiling still proves we are authorized. + return {"ok": True, "reason": "throttled_ok", + "detail": "Authorized (probe throttled on a TPM quota, which still proves access)."} + return {"ok": False, "reason": "unknown", "detail": f"{status}: {message}"} + + +def probe_mantle_model(model_id: str, region: str) -> dict: + """Real minimal probe against the mantle Responses API. + + Fails CLOSED when the OpenAI SDK is missing. This deliberately does NOT follow + the `embedding_unprobed` precedent: that one passes because an unrecognized + embedding family is a rare edge case, whereas a missing SDK here would be the + normal path for every run if the dependency were absent — returning ok=True + would turn a fail-fast preflight into an unconditional green light and let the + migration proceed with endpoint, model, and IAM access never checked. Both + packages are declared in pyproject.toml, so reaching this branch means the + pinned environment is broken and the operator needs to know.""" + try: + from aws_bedrock_token_generator import provide_token + from openai import BedrockOpenAI + except ImportError as e: + return {"ok": False, "reason": "mantle_deps_missing", + "detail": f"Cannot probe mantle-only model {model_id}: {e}. This needs " + f"'openai>=2.45.0' and 'aws-bedrock-token-generator', both declared " + f"in scripts/pyproject.toml — re-sync the pinned environment " + f"(`uv sync --project `). Access was NOT verified; " + f"preflight fails closed rather than assuming it works."} + try: + client = BedrockOpenAI( + aws_region=region, + bedrock_token_provider=lambda: provide_token(region=region), + max_retries=0, + ) + client.responses.create(model=model_id, input="ping", max_output_tokens=16, store=False) + return {"ok": True, "reason": "ok", "detail": "Mantle Responses API authorized."} + except Exception as e: # noqa: BLE001 - SDK raises many types; status is what matters + status = getattr(e, "status_code", None) + if status is None: + resp = getattr(e, "response", None) + status = getattr(resp, "status_code", None) + if status is None: + return {"ok": False, "reason": "credentials", + "detail": f"{type(e).__name__}: {e}. Check AWS credentials, the region " + f"name, and network access to the bedrock-mantle endpoint."} + return classify_mantle_error(int(status), str(e)) + + def _embed_request_body(model_id: str) -> dict | None: """Pure: minimal valid request body per embedding-model family; None if unknown.""" parts = model_id.split(".") @@ -173,14 +256,25 @@ def main(argv=None) -> int: results = [] all_ok = True for model_id in model_ids: - verdict = probe_model(client, model_id) - rpm = quota_rpm(quotas, model_id) - verdict["model_id"] = model_id - verdict["rpm_quota"] = rpm - if rpm is not None and args.dataset_size > rpm: - verdict["quota_warning"] = ( - f"Dataset ({args.dataset_size}) exceeds ~{rpm} RPM quota — " - f"Eval will pace with backoff and may be slow.") + if is_mantle_model(model_id): + verdict = probe_mantle_model(model_id, args.region) + verdict["model_id"] = model_id + # Mantle enforces per-model input/output TPM quotas and has no RPM + # quota, so an RPM-derived pacing warning would be meaningless here. + verdict["rpm_quota"] = None + verdict["quota_note"] = ( + "Mantle quotas are per-model input/output tokens per minute; there is no RPM " + "quota. Pace on token throughput, and note that prompt-cached input tokens are " + "exempt from the input-TPM quota.") + else: + verdict = probe_model(client, model_id) + rpm = quota_rpm(quotas, model_id) + verdict["model_id"] = model_id + verdict["rpm_quota"] = rpm + if rpm is not None and args.dataset_size > rpm: + verdict["quota_warning"] = ( + f"Dataset ({args.dataset_size}) exceeds ~{rpm} RPM quota — " + f"Eval will pace with backoff and may be slow.") all_ok = all_ok and verdict["ok"] results.append(verdict) diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/pyproject.toml b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/pyproject.toml index 6e5bf242..df03ec92 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/pyproject.toml +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/pyproject.toml @@ -6,6 +6,12 @@ dependencies = [ "boto3>=1.35,<2", "botocore>=1.35,<2", "jsonschema>=4,<5", + # Required to preflight OpenAI's proprietary GPT models, which are + # bedrock-mantle-only and speak neither Converse nor InvokeModel. Without + # these, probe_mantle_model cannot run and preflight cannot verify + # endpoint/model/IAM access for any openai.gpt-5* target. + "openai>=2.45.0,<3", + "aws-bedrock-token-generator>=1,<2", ] [dependency-groups] diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/schemas/eval.json b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/schemas/eval.json index 1eea0c5b..eef6efda 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/schemas/eval.json +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/schemas/eval.json @@ -36,7 +36,10 @@ "additionalProperties": false, "required": ["reason", "detail"], "properties": { - "reason": { "type": "string", "enum": ["model_access", "source_key_auth", "model_unresolvable"] }, + "reason": { + "type": "string", + "enum": ["model_access", "source_key_auth", "model_unresolvable", "authz", "mantle_deps_missing"] + }, "detail": { "type": "string" } } } diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_bedrock_pricing.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_bedrock_pricing.py index bbb92210..d7e514e5 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_bedrock_pricing.py +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_bedrock_pricing.py @@ -106,3 +106,41 @@ def deny_boto3(name, *a, **k): out = bp.lookup("us-east-1", "amazon.nova-pro-v1:0") assert out["available"] is True assert out["input_per_1k_usd"] == 0.0008 + + +def test_mantle_gpt_detection_excludes_gpt_oss(): + assert bp.is_mantle_gpt("openai.gpt-5.6-luna") is True + assert bp.is_mantle_gpt("openai.gpt-5.5") is True + assert bp.is_mantle_gpt("openai.gpt-oss-120b-1:0") is False + assert bp.is_mantle_gpt("anthropic.claude-sonnet-4-6") is False + + +def test_mantle_gpt_verified_rates_come_from_static_table(): + v = bp.lookup("us-east-1", "openai.gpt-5.6-luna") + assert v["available"] is True + # $0.20 / $1.20 per 1M == $0.0002 / $0.0012 per 1K + assert v["input_per_1k_usd"] == 0.0002 + assert v["output_per_1k_usd"] == 0.0012 + + +def test_mantle_gpt_never_prefix_matches_a_different_tier(): + # Regression: Sol, Terra and Luna differ only by suffix at very different price + # points, so a prefix match would bill one tier at another tier's rate. + v = bp.lookup("us-east-1", "openai.gpt-5.6") + assert v["available"] is False + assert v["input_per_1k_usd"] is None + + +def test_unpriced_mantle_gpt_says_unavailable_not_nonexistent(monkeypatch): + # Regression: falling through to the PriceList API returned a bare + # "Pricing unavailable", which reads as "no such model" for a GA model. + import boto3 + + def boom(*a, **k): + raise AssertionError("must not call the PriceList API for a mantle GPT model") + + monkeypatch.setattr(boto3, "client", boom) + v = bp.lookup("us-east-1", "openai.gpt-5.6-terra") + assert v["available"] is False + assert "does NOT mean the model is unavailable" in v["note"] + assert "aws.amazon.com/bedrock/pricing" in v["note"] diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_delta_reference_schema.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_delta_reference_schema.py new file mode 100644 index 00000000..1d5b4b58 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_delta_reference_schema.py @@ -0,0 +1,193 @@ +# test_delta_reference_schema.py +"""Guard: every `resolution_kind` the delta references instruct the analyzer to +emit must be accepted by analysis.json. + +The analyzer builds `behavior_deltas` by reading these references, then validates +its own output. A value that appears in a reference but not in the schema makes +T2-3 unable to produce a valid result — and the failure surfaces at runtime in a +subagent, not in CI. This test closes that gap. + +It caught a real case twice over: an invented `mechanical` value, first in the +bullet declarations and then again in a prose sentence inside the same block that +a targeted edit had missed. +""" +import json +import re +from pathlib import Path + +REFS = Path(__file__).resolve().parent.parent / "references/helpers/behavior-delta-detection/references" +SCHEMA = Path(__file__).resolve().parent / "schemas/analysis.json" + +# Matches both the bullet form (`- `resolution_kind`: `impl_path``) and prose +# mentions (`... `resolution_kind: impl_path`. Apply ...`), with the value inside +# or outside the backticks. +PATTERN = re.compile(r"resolution_kind`?\s*:\s*`?([a-z_]+)`?") + + +def allowed_kinds() -> set[str]: + schema = json.loads(SCHEMA.read_text()) + + found: list[list[str]] = [] + + def walk(node): + if isinstance(node, dict): + if node.get("enum") and "resolution_kind" not in found: + pass + for key, value in node.items(): + if key == "resolution_kind" and isinstance(value, dict) and "enum" in value: + found.append(value["enum"]) + walk(value) + elif isinstance(node, list): + for item in node: + walk(item) + + walk(schema) + assert found, "analysis.json no longer declares a resolution_kind enum" + return set(found[0]) + + +def test_every_reference_resolution_kind_is_in_the_schema(): + allowed = allowed_kinds() + offenders = [] + for ref in sorted(REFS.glob("*.md")): + for lineno, line in enumerate(ref.read_text().splitlines(), 1): + for value in PATTERN.findall(line): + if value not in allowed: + offenders.append(f"{ref.name}:{lineno} -> {value!r}") + assert not offenders, ( + "delta references instruct the analyzer to emit resolution_kind values that " + f"analysis.json rejects (allowed: {sorted(allowed)}):\n " + "\n ".join(offenders)) + + +def test_pattern_catches_both_declaration_forms(): + # Guards the guard: if PATTERN stops matching either form, the test above + # silently passes on a broken reference. + assert PATTERN.findall("- `resolution_kind`: `impl_path`") == ["impl_path"] + assert PATTERN.findall("always `user_visible: false`, `resolution_kind: mechanical`.") == ["mechanical"] + + +# --- Same guard, applied to the evaluator's documented control states ---------- +# The evaluator writes `{ blocked: { reason, detail } }` and then validates against +# eval.json. A reason documented in the prompt but absent from the schema means a +# real failure mode (404, IAM denial, missing deps) cannot be represented at all, +# so the phase enters the retry path with no valid outcome file. + +AGENTS = Path(__file__).resolve().parent.parent.parent.parent / "agents" +EVAL_SCHEMA = Path(__file__).resolve().parent / "schemas/eval.json" +REASON_PATTERN = re.compile(r"reason:\s*'([a-z_]+)'") + + +def _eval_enums() -> tuple[set[str], set[str]]: + schema = json.loads(EVAL_SCHEMA.read_text()) + blocked, partial = set(), set() + for branch in schema.get("oneOf", []): + props = branch.get("properties", {}) + if "blocked" in props: + blocked = set(props["blocked"]["properties"]["reason"]["enum"]) + if "partial" in props: + partial = set(props["partial"]["properties"]["reason"]["enum"]) + assert blocked, "eval.json no longer declares a blocked.reason enum" + return blocked, partial + + +def test_evaluator_control_state_reasons_are_representable(): + blocked, partial = _eval_enums() + allowed = blocked | partial + prompt = (AGENTS / "llm2bedrock-prompt-evaluator.md").read_text() + offenders = [] + for lineno, line in enumerate(prompt.splitlines(), 1): + for value in REASON_PATTERN.findall(line): + if value not in allowed: + offenders.append(f"llm2bedrock-prompt-evaluator.md:{lineno} -> {value!r}") + assert not offenders, ( + "the evaluator documents control-state reasons that eval.json rejects " + f"(blocked: {sorted(blocked)}, partial: {sorted(partial)}):\n " + "\n ".join(offenders)) + + +# --- Guard: the evaluator must not hand-assemble Responses image payloads ------- +# Both mistakes below pass the §9.5a smoke test (it hardcodes a known-good jpeg) +# and fail only on golden cases, so a static check is the cheapest place to catch +# a regression in prompt text that no unit test can reach. + +def test_evaluator_does_not_document_a_bare_responses_content_list(): + prompt = (AGENTS / "llm2bedrock-prompt-evaluator.md").read_text() + offenders = [] + for lineno, line in enumerate(prompt.splitlines(), 1): + if "input_image" not in line and "input_text" not in line: + continue + # Any line showing a Responses content block must either be inside a + # role/content wrapper or be delegating to the helper. + if '"role"' in line or "'role'" in line: + continue + if "responses_message" in line: + continue + # The §9.5a snippet wraps across lines; allow the inner block lines there. + offenders.append((lineno, line.strip()[:90])) + # Lines inside the multi-line §9.5a wrapper are legitimate; assert every + # offender sits within 3 lines of a role wrapper. + lines = prompt.splitlines() + real = [] + for lineno, text in offenders: + window = "\n".join(lines[max(0, lineno - 4):lineno]) + # Any message-item role is a valid wrapper: `user` for prompts, `developer` + # for a system prompt on the Responses API. + if not re.search(r'"role":\s*"(user|developer|system|assistant)"', window): + real.append(f"line {lineno}: {text}") + assert not real, ( + "Responses image content documented without a user-message wrapper " + "(input takes [{'role':'user','content':[...]}], not a bare block list):\n " + + "\n ".join(real)) + + +def test_evaluator_never_templates_an_extension_into_a_mime_type(): + # `image/` yields the invalid `image/jpg` for a .jpg case. + prompt = (AGENTS / "llm2bedrock-prompt-evaluator.md").read_text() + bad = [f"line {n}: {l.strip()[:90]}" + for n, l in enumerate(prompt.splitlines(), 1) + if "image/" in l or "'format': " in l or '"format": ' in l] + assert not bad, ( + "extension templated directly into a wire format; use image_input helpers " + "(.jpg -> jpeg / image/jpeg):\n " + "\n ".join(bad)) + + +# --- Guard: golden-case field names, derived from the canonical record ---------- +# The evaluator reads golden cases by key. A key that does not exist in the record +# the log-ingestor writes raises KeyError before any API call, which no unit test +# reaches because the loop lives in prompt text. + +def _canonical_case_fields() -> set[str]: + """Field names from the canonical prompts.jsonl record in the log-ingestor prompt.""" + text = (AGENTS / "llm2bedrock-log-ingestor.md").read_text() + marker = "Write each entry as one JSON object per line" + start = text.index(marker) + block = text[text.index("```json", start) + len("```json"):] + block = block[:block.index("```")] + record = json.loads(block) + assert "user_prompt" in record, "canonical record shape changed — revisit this guard" + return set(record) + + +def test_evaluator_reads_only_canonical_golden_case_fields(): + fields = _canonical_case_fields() + prompt = (AGENTS / "llm2bedrock-prompt-evaluator.md").read_text() + # Subscripts on the per-case variables used in the §8 / §10 loops. + subscript = re.compile(r"\b(?:case|prompt|entry)\[[\"']([a-z_]+)[\"']\]") + getter = re.compile(r"\b(?:case|prompt|entry)\.get\([\"']([a-z_]+)[\"']") + offenders = [] + for lineno, line in enumerate(prompt.splitlines(), 1): + for key in subscript.findall(line) + getter.findall(line): + if key not in fields: + offenders.append(f"llm2bedrock-prompt-evaluator.md:{lineno} -> {key!r}") + assert not offenders, ( + "evaluator reads golden-case fields absent from the canonical record " + f"(fields: {sorted(fields)}):\n " + "\n ".join(offenders)) + + +def test_documented_responses_image_blocks_set_detail(): + # Mirrors the helper-level test, for the image blocks written inline in prompts. + prompt = (AGENTS / "llm2bedrock-prompt-evaluator.md").read_text() + bad = [f"line {n}: {l.strip()[:90]}" + for n, l in enumerate(prompt.splitlines(), 1) + if '"type": "input_image"' in l and '"detail"' not in l] + assert not bad, ( + "inline input_image block without the SDK-required `detail` field:\n " + "\n ".join(bad)) diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_iam_policy.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_iam_policy.py index 14f095cf..850a7f20 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_iam_policy.py +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_iam_policy.py @@ -10,6 +10,8 @@ from iam_policy import ( foundation_model_arn, generate_policy, + is_mantle_model, + mantle_project_arn, inference_profile_arn, is_inference_profile, ) @@ -113,3 +115,56 @@ def test_file_output(self, tmp_path): assert result.returncode == 0 policy = json.loads(out.read_text()) assert len(policy["Statement"][0]["Resource"]) == 3 + + +def test_mantle_model_detection_excludes_gpt_oss(): + assert is_mantle_model("openai.gpt-5.6-terra") is True + assert is_mantle_model("openai.gpt-5.5") is True + assert is_mantle_model("openai.gpt-oss-120b-1:0") is False + assert is_mantle_model("anthropic.claude-sonnet-4-6") is False + + +def test_mantle_target_gets_mantle_actions_not_invoke_model(): + # Regression: the generated policy emitted only bedrock:InvokeModel against a + # foundation-model ARN, which cannot authorize a mantle call — the migrated app + # would deploy and immediately fail authorization. + pol = generate_policy(["openai.gpt-5.6-terra"], "us-east-1", "111122223333") + sids = {s["Sid"] for s in pol["Statement"]} + assert "BedrockMantleInference" in sids + assert "BedrockMantleCallWithBearerToken" in sids + # An all-mantle run must NOT emit an InvokeModel statement with an empty + # Resource list — that is an invalid IAM policy. + assert "BedrockInvokeModelScoped" not in sids + for s in pol["Statement"]: + assert s.get("Resource"), s["Sid"] + + +def test_mantle_bearer_token_is_scoped_to_star(): + # AWS does not support narrowing CallWithBearerToken; it must be "*". + pol = generate_policy(["openai.gpt-5.5"], "us-east-1", "111122223333") + bearer = [s for s in pol["Statement"] if s["Sid"] == "BedrockMantleCallWithBearerToken"][0] + assert bearer["Resource"] == "*" + assert bearer["Action"] == ["bedrock-mantle:CallWithBearerToken"] + + +def test_mantle_inference_scoped_to_account_and_region(): + pol = generate_policy(["openai.gpt-5.5"], "us-west-2", "999988887777") + inf = [s for s in pol["Statement"] if s["Sid"] == "BedrockMantleInference"][0] + assert inf["Resource"] == "arn:aws:bedrock-mantle:us-west-2:999988887777:project/*" + + +def test_mixed_targets_emit_both_runtime_and_mantle_statements(): + pol = generate_policy( + ["openai.gpt-5.6-luna", "us.anthropic.claude-sonnet-4-6"], "us-east-1", "111122223333") + sids = [s["Sid"] for s in pol["Statement"]] + assert "BedrockInvokeModelScoped" in sids + assert "BedrockMantleInference" in sids + invoke = [s for s in pol["Statement"] if s["Sid"] == "BedrockInvokeModelScoped"][0] + # The mantle model must not leak into the InvokeModel resource list. + assert not any("gpt-5" in r for r in invoke["Resource"]) + + +def test_gpt_oss_stays_on_the_runtime_path(): + pol = generate_policy(["openai.gpt-oss-120b-1:0"], "us-east-1", "111122223333") + sids = {s["Sid"] for s in pol["Statement"]} + assert sids == {"BedrockInvokeModelScoped"} diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_image_input.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_image_input.py new file mode 100644 index 00000000..127077ae --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_image_input.py @@ -0,0 +1,143 @@ +# test_image_input.py +"""Regression coverage for the two vision payload mistakes that let a smoke test +pass while every golden case fails.""" +import base64 +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).parent)) +import image_input as ii + +RAW = b"\xff\xd8\xff\xe0fake-jpeg-bytes" + + +def test_jpg_extension_normalizes_to_jpeg(): + # Regression: `.jpg` passed through verbatim produced Converse `format: 'jpg'` + # and MIME `image/jpg`, both rejected. The smoke test hardcodes 'jpeg' and so + # never surfaced it. + assert ii.converse_format("cat.jpg") == "jpeg" + assert ii.converse_format("cat.jpeg") == "jpeg" + assert ii.mime_type("cat.jpg") == "image/jpeg" + assert "image/jpg" not in ii.data_url("cat.jpg", RAW) + + +def test_extension_case_is_ignored(): + assert ii.converse_format("CAT.JPG") == "jpeg" + assert ii.mime_type("shot.PNG") == "image/png" + + +@pytest.mark.parametrize("name,fmt", [ + ("a.png", "png"), ("a.gif", "gif"), ("a.webp", "webp"), +]) +def test_other_supported_formats(name, fmt): + assert ii.converse_format(name) == fmt + assert ii.mime_type(name) == f"image/{fmt}" + + +@pytest.mark.parametrize("name", ["a.bmp", "a.tiff", "a.svg", "a.heic", "noext"]) +def test_unsupported_types_raise_rather_than_guess(name): + # Silently passing an unsupported extension through would produce an opaque + # API rejection at eval time instead of a clear failure here. + with pytest.raises(ValueError, match="unsupported image type"): + ii.converse_format(name) + + +def test_responses_message_is_wrapped_as_a_user_message(): + # Regression: the per-case loop passed a bare [{'type': 'input_text', ...}] + # list as `input`, which is not a valid Responses request. + msg = ii.responses_message("describe", "cat.jpg", RAW) + assert msg["role"] == "user" + assert isinstance(msg["content"], list) + kinds = [b["type"] for b in msg["content"]] + assert kinds == ["input_text", "input_image"] + assert msg["content"][1]["image_url"].startswith("data:image/jpeg;base64,") + assert base64.b64encode(RAW).decode() in msg["content"][1]["image_url"] + + +def test_responses_message_text_only_omits_the_image_block(): + msg = ii.responses_message("describe") + assert msg["role"] == "user" + assert [b["type"] for b in msg["content"]] == ["input_text"] + + +def test_converse_message_puts_image_before_text(): + msg = ii.converse_message("describe", "cat.jpg", RAW) + assert msg["role"] == "user" + assert list(msg["content"][0]) == ["image"] + assert msg["content"][0]["image"]["format"] == "jpeg" + assert msg["content"][0]["image"]["source"]["bytes"] is RAW + assert msg["content"][1] == {"text": "describe"} + + +def test_converse_message_text_only(): + assert ii.converse_message("describe") == { + "role": "user", "content": [{"text": "describe"}]} + + +@pytest.mark.parametrize("fn", [ii.converse_message, ii.responses_message]) +def test_image_path_without_bytes_is_an_error(fn): + with pytest.raises(ValueError, match="raw image bytes"): + fn("describe", "cat.jpg", None) + + +def test_responses_image_block_sets_detail(): + # Regression: `detail` is Required on ResponseInputImageParam in the pinned SDK, + # and omitting it is a contract violation a TypedDict will not catch at runtime. + block = ii.responses_message("describe", "cat.jpg", RAW)["content"][1] + assert block["detail"] == "auto" + + +def _sdk_image_param_keys() -> tuple[set[str], set[str]]: + """(required, all) keys of ResponseInputImageParam, read from the pinned SDK. + + `__required_keys__` is unusable here: the class is declared `total=False` with + per-field `Required[...]` markers, which CPython 3.11 does not fold into + `__required_keys__` — it reports every key as optional. The annotation origin + does carry the marker, so read requiredness from there. + """ + import typing_extensions as te + from openai.types.responses import ResponseInputImageParam + + hints = te.get_type_hints(ResponseInputImageParam, include_extras=True) + required = {k for k, v in hints.items() if te.get_origin(v) is te.Required} + return required, set(hints) + + +def test_responses_image_block_satisfies_the_pinned_sdk_required_fields(): + # Derive the requirement from the installed SDK rather than restating it, so a + # future SDK bump that adds a required field fails here instead of at runtime. + required, known = _sdk_image_param_keys() + assert "detail" in required, "SDK no longer marks detail required — revisit this test" + block = ii.responses_message("describe", "cat.jpg", RAW)["content"][1] + missing = required - set(block) + assert not missing, f"image block missing SDK-required field(s): {sorted(missing)}" + unknown = set(block) - known + assert not unknown, f"image block has field(s) the SDK does not define: {sorted(unknown)}" + + +@pytest.mark.parametrize("detail", ["low", "high", "auto", "original"]) +def test_all_sdk_detail_levels_accepted(detail): + block = ii.responses_message("d", "cat.png", RAW, detail=detail)["content"][1] + assert block["detail"] == detail + + +def test_invalid_detail_rejected(): + with pytest.raises(ValueError, match="invalid image detail"): + ii.responses_message("d", "cat.png", RAW, detail="ultra") + + +def test_detail_levels_match_the_pinned_sdk_literal(): + import typing_extensions as te + from openai.types.responses import ResponseInputImageParam + hints = te.get_type_hints(ResponseInputImageParam, include_extras=True) + literal = te.get_args(te.get_args(hints["detail"])[0]) # unwrap Required[Literal[...]] + assert set(literal) == set(ii.DETAIL_LEVELS) + + +def test_text_only_message_has_no_detail_key(): + # detail belongs to the image block only; a stray key on the text block would + # be an unknown field. + msg = ii.responses_message("describe") + assert "detail" not in msg["content"][0] diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_preflight_bedrock.py b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_preflight_bedrock.py index c726e6b5..cda3a6ea 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_preflight_bedrock.py +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/test_preflight_bedrock.py @@ -142,3 +142,111 @@ def test_access_denied_iam_variant_still_routes_to_authz(): "bedrock:InvokeModel on resource ...") assert v["reason"] == "authz" assert "bedrock:InvokeModel" in v["detail"] + + +def test_mantle_model_detection_excludes_gpt_oss(): + # Proprietary GPT models are mantle-only; gpt-oss speaks Converse and must + # keep the bedrock-runtime path. + assert p.is_mantle_model("openai.gpt-5.6-terra") is True + assert p.is_mantle_model("openai.gpt-5.6-sol") is True + assert p.is_mantle_model("openai.gpt-5.5") is True + assert p.is_mantle_model("openai.gpt-5.4") is True + assert p.is_mantle_model("openai.gpt-oss-120b-1:0") is False + assert p.is_mantle_model("anthropic.claude-sonnet-4-6") is False + assert p.is_mantle_model("amazon.nova-lite-v1:0") is False + + +def test_mantle_authz_error_points_at_mantle_actions_not_invoke_model(): + # Regression: sending the user to bedrock:InvokeModel is a dead end for these + # models — mantle inference needs bedrock-mantle:* actions. + v = p.classify_mantle_error(403, "User is not authorized to perform bedrock-mantle:CreateInference") + assert v["ok"] is False + assert v["reason"] == "authz" + assert "bedrock-mantle" in v["detail"] + assert "does NOT authorize" in v["detail"] + + +def test_mantle_model_unavailable_does_not_suggest_cross_region_profile(): + # These models are in-region only, so the Converse-path remedy ("try us.") + # would be actively misleading. + v = p.classify_mantle_error(404, "model not found") + assert v["ok"] is False + assert v["reason"] == "model_unavailable" + assert "in-region only" in v["detail"] + assert "us." not in v["detail"] + + +def test_mantle_throttle_is_ok_for_preflight(): + v = p.classify_mantle_error(429, "too many tokens per minute") + assert v["ok"] is True + assert v["reason"] == "throttled_ok" + + +def test_mantle_model_access_variant_routes_to_console_fix(): + v = p.classify_mantle_error(403, "You do not have access to the model with the specified model ID") + assert v["ok"] is False + assert v["reason"] == "model_access" + assert "console" in v["detail"].lower() + + +def test_main_probes_mantle_models_without_bedrock_runtime(monkeypatch, capsys): + # Regression: mantle-only models were probed with bedrock-runtime Converse, + # which always fails, so preflight blocked every GPT-5.x migration. + import json + calls = {} + + def fake_probe_mantle(model_id, region): + calls["mantle"] = (model_id, region) + return {"ok": True, "reason": "ok", "detail": "Mantle Responses API authorized."} + + def fake_probe_model(client, model_id): + raise AssertionError("must not probe a mantle-only model via bedrock-runtime") + + monkeypatch.setattr(p, "probe_mantle_model", fake_probe_mantle) + monkeypatch.setattr(p, "probe_model", fake_probe_model) + monkeypatch.setattr(p, "fetch_bedrock_quotas", lambda region: []) + + import boto3 + monkeypatch.setattr(boto3, "client", lambda *a, **k: object()) + + rc = p.main(["--region", "us-east-1", "--models", "openai.gpt-5.6-terra", "--dataset-size", "9999"]) + out = json.loads(capsys.readouterr().out) + + assert rc == 0 + assert calls["mantle"] == ("openai.gpt-5.6-terra", "us-east-1") + entry = out["models"][0] + assert entry["ok"] is True + assert entry["rpm_quota"] is None + # A dataset larger than any RPM number must not produce an RPM pacing warning + # for a model that has no RPM quota. + assert "quota_warning" not in entry + assert "no RPM quota" in entry["quota_note"] + + +def test_mantle_missing_deps_fails_closed(monkeypatch): + # Regression: this returned ok=True with a caveat, mirroring the rare + # `embedding_unprobed` case. But a missing SDK is the NORMAL path if the + # dependency is absent, so passing turned a fail-fast preflight into an + # unconditional green light — endpoint, model and IAM access never checked. + import builtins + real_import = builtins.__import__ + + def no_openai(name, *a, **k): + if name in ("openai", "aws_bedrock_token_generator"): + raise ImportError(f"No module named {name!r}") + return real_import(name, *a, **k) + + monkeypatch.setattr(builtins, "__import__", no_openai) + v = p.probe_mantle_model("openai.gpt-5.6-terra", "us-east-1") + assert v["ok"] is False + assert v["reason"] == "mantle_deps_missing" + assert "NOT verified" in v["detail"] + + +def test_mantle_deps_are_declared_in_pinned_env(): + # The fail-closed path above must never trigger in a correctly synced env, + # so the pinned toolchain has to declare both packages. + import pathlib + toml = (pathlib.Path(__file__).resolve().parent / "pyproject.toml").read_text() + assert "openai>=2.45.0" in toml + assert "aws-bedrock-token-generator" in toml diff --git a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/uv.lock b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/uv.lock index caffcb20..1029159d 100644 --- a/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/uv.lock +++ b/migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.11'", @@ -11,9 +11,11 @@ name = "ai-to-bedrock-scripts" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "aws-bedrock-token-generator" }, { name = "boto3" }, { name = "botocore" }, { name = "jsonschema" }, + { name = "openai" }, ] [package.dev-dependencies] @@ -23,14 +25,39 @@ dev = [ [package.metadata] requires-dist = [ + { name = "aws-bedrock-token-generator", specifier = ">=1,<2" }, { name = "boto3", specifier = ">=1.35,<2" }, { name = "botocore", specifier = ">=1.35,<2" }, { name = "jsonschema", specifier = ">=4,<5" }, + { name = "openai", specifier = ">=2.45.0,<3" }, ] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=8" }] +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -40,6 +67,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "aws-bedrock-token-generator" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/39/cf1c2e12bc5a84af0f96a546481213f81fa6e7927d2bbabd81758c6558ca/aws_bedrock_token_generator-1.1.0.tar.gz", hash = "sha256:95ccb07f63a91ac486561f6df05cc4e04784c8ff5086dc687ed9c5fd3ab1b5ba", size = 19123, upload-time = "2025-07-29T19:53:19.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/fd/745ece98870c3824d294bcdce5dc5e15381188a41bc80832c246b205e40e/aws_bedrock_token_generator-1.1.0-py3-none-any.whl", hash = "sha256:bd12854f7c7e52dde5d980d369379f12d0cc5f0855099d87f38688b0f9de5cd4", size = 10291, upload-time = "2025-07-29T19:53:18.704Z" }, +] + [[package]] name = "boto3" version = "1.43.24" @@ -68,6 +107,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/b7/360b5afe74c4d7cff871ea6e8f335e2e11de2945c9deb1eea6438f49faa2/botocore-1.43.24-py3-none-any.whl", hash = "sha256:42903b4bfafd8f15a735ed940473f28e4ba21b2ea67a9b9aaa11dfa7fcb19fd5", size = 15155182, upload-time = "2026-06-05T19:29:29.457Z" }, ] +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -77,18 +125,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -98,6 +201,105 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, + { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + [[package]] name = "jmespath" version = "1.1.0" @@ -135,6 +337,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "openai" +version = "2.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/cf/36e3e7235fdf6d125c052acc0970924611b17a20a4fe580596faf4566a65/openai-2.53.0.tar.gz", hash = "sha256:baf5802ad08980e1d9d561e1b996e800c8bcd14af5847c6d0e7a5cc59e4d4116", size = 1099435, upload-time = "2026-08-03T21:42:01.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/0f/cc6afea3542a5142c5d8fc8211c5e059a8375105d004a41dfa2c7948dbb0/openai-2.53.0-py3-none-any.whl", hash = "sha256:c694ffc747a3c4d1663ef2b07b811315a476164ee5efa3a993967349ebca7618", size = 1659829, upload-time = "2026-08-03T21:41:59.581Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -153,6 +374,137 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -493,6 +845,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -547,6 +908,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -556,6 +929,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/bc/4eae18cd40c65798a16267572ba346c11f599d44b01603dbd843342042bc/typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d", size = 76711, upload-time = "2026-08-10T09:39:18.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/f7/7a3935abdebd5cf18705a5f0335dd6a3a18bef3baa7cb9edc3b6b9922cc8/typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd", size = 14693, upload-time = "2026-08-10T09:39:16.693Z" }, +] + [[package]] name = "urllib3" version = "2.7.0"