AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Agent Advisor provides a structured, multi-phase workflow to help users determine the optimal AWS runtime for their AI agents. It uses a deterministic scoring engine to evaluate requirements and guides users through a process that includes discovery, architectural design, cost estimation, and the creation of an executable proof-of-concept. The skill supports various AWS compute options, including AgentCore, ECS, EKS, and Lambda, and integrates with migration engines for existing workloads.
What it does
- Recommends AWS runtimes for AI agents based on technical requirements.
- Generates migration plans for existing AI workloads moving to AWS.
- Provides architectural guidance for agentic systems.
- Assembles executable proof-of-concept scaffolding.
- Estimates coarse cost magnitudes for proposed agent deployments.
- Integrates with external migration engines for workload transitions.
When to use it
- When you need to decide which AWS runtime is best for a new AI agent.
- When migrating an existing AI agent workload to AWS.
- When you need to add services like memory, identity, or observability to an existing AWS agent.
- When you require a structured plan and POC for deploying agents on AWS.
Advantages
- Provides a deterministic, repeatable recommendation process.
- Supports a wide range of AWS compute environments.
- Offers a unified flow from initial idea to executable POC.
- Adapts to both technical and business-focused user backgrounds.
Usage examples
- “I have an agent idea, what do I build?”
- The agent initiates the intake process to clarify your requirements and recommend an appropriate AWS architecture.
- “Move my agents to AWS.”
- The agent triggers the migration workflow to assess your existing workload and plan the transition to an AWS runtime.
- “Which runtime for my agent, AgentCore vs ECS vs EKS vs Lambda?”
- The agent runs a scoring analysis to compare these runtimes against your specific agentic needs.
- “Add AgentCore services to an agent already on AWS.”
- The agent enters the add-capabilities branch to guide you through integrating memory, gateway, and observability services.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/aws/agent-advisor/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip agent-advisor.zip -d .agents/skills
Documentation by AWS
AWS Agent Advisor
Helps startups decide how and where to run AI agents on AWS. Deterministic scoring recommends a runtime; the conversation adapts to the user's technical background.
Definitions
"Load" = Read the file with the Read tool and follow it. Do not summarize or skip.
$RUN_DIR= the run directory under.agent-advisor/(e.g..agent-advisor/0630-1430/), created in Intake.$PLUGIN=${CLAUDE_PLUGIN_ROOT}(the installed plugin root). On Claude Code this token substitutes inline. If${CLAUDE_PLUGIN_ROOT}does not resolve (some Cursor/Codex builds, or a literal${CLAUDE_PLUGIN_ROOT}string showing up in a path error), fall back to the skill's own directory: this SKILL.md lives at<plugin>/skills/agent-advisor/SKILL.md, so the engine and its data are all inside this skill — scripts at./scripts/..., runtime profiles at./references/runtimes/..., and decision refs at./references/decision-refs/...relative to it. Prefer${CLAUDE_PLUGIN_ROOT}/skills/agent-advisor/...; use the relative fallback only when it fails to resolve.
Prerequisites
uvavailable (for scoring). Check:uv --version. If missing, tell the user to install it from the official install guide (https://docs.astral.sh/uv/getting-started/installation/ — e.g.brew install uvorpipx install uv) and stop.
Phase Structure (frontmatter)
Phase, fragment, and assembler files carry a YAML frontmatter block that declares how each
phase is composed — its inputs, triggers, fragments, assembler, artifacts, gates, and
ordering. The execution contract is the vendored references/vendored/dsl/INTERPRETER.md:
it defines every frontmatter key, the fragment/assembler model, the gate protocol
(HANDOFF_OK / GATE_FAIL), and the interpreter loop. Load it first (once, at the
start of a run), then execute each phase file's prose body. Elsewhere in this skill,
INTERPRETER.md (without a path) refers to this loaded contract.
Execution
This skill is driven by the interpreter loop in INTERPRETER.md (§ The interpreter loop):
it reads .phase-status.json, determines the current phase, runs each phase's
_preconditions / fragments / _assemble / _postconditions, advances on HANDOFF_OK
via _advances_to, and validates state. The backbone (intake → discover → clarify →
confirm → design → estimate → generate → migration-plan → poc → complete) and the
one sidebar branch (add-capabilities) are derived
from the phase files' frontmatter — they are not restated here.
Cold start (entry phase). With no run under .agent-advisor/ carrying a
.phase-status.json, begin at references/phases/intake/intake.md — this skill's entry
phase (the one carrying _init: true). On a warm start, current_phase in
.phase-status.json is authoritative (INTERPRETER.md § The interpreter loop).
Skill bindings (INTERPRETER.md § Skill bindings). This skill declares:
Run root:
.agent-advisor/—$RUN_DIRis this skill's name for the run directory (.agent-advisor/[MMDD-HHMM]/). Intake's own prose performs the_initbootstrap.State shape: § State file below (advisor-specific keys such as
entry_point,audience,recommendation_reviewed,migration_plan_ctx,migration_plan_unavailable); the shared state schema is not vendored.Run seed (optional):
$RUN_DIR/seed.json, else.agent-advisor/seed.jsonat the run root (schemascripts/schemas/seed.json) supplies machine-readable answers for a non-interactive run — the Clarify dimensions, the two gate answers, the POC mode, the live-probe answer, and aco_recommendtie-break. It is the HIGHEST-precedence source for every value it carries (clarify.md Step 2.5), which is what makes a repeated run's score comparable: the deterministic engine gets byte-identical input. A gate the seed omits is declined; a dimension the seed omits falls through to detection, then prose, then anassumedvalue that MUST be recorded in$RUN_DIR/UNANSWERED.md. With no seed, the interactive flow is unchanged.Resolved statuses:
skipped(routing resolved the phase without running it), plusnot_applicableformigration_planonly.Conditional backbone routing: the entry-point routing below. When a routing rule marks a phase not-applicable, set it
skippedand advance through its_advances_toin the same state write.
Routing & gates (orchestration)
Sidebar placement and conditional backbone routing are orchestration prose owned by
this file (INTERPRETER.md § Skill bindings, § Backbone vs sidebar).
Entry-point routing:
build_scratch→ skip Discover; Clarify → Confirm → Design → Estimate → Generate → Gate 2 → POC (any winning runtime). No migration plan (nothing existing to migrate).build_deploy→ Discover (if code) → Clarify → Confirm → Design → Estimate → Generate → Gate 1 → Migration Plan (if existing non-AWS AI workload detected and user confirms) → Gate 2 → POC (any winning runtime).migrate→ Discover (if code) → Clarify → Confirm → Design → Estimate (target-state run cost; migration TCO comparison stays with the Migration Plan engine) → Generate → Gate 1 → Migration Plan (in-skill, reusing the siblinggcp-to-awsskill) → Gate 2 → POC (any winning runtime, when the plan was produced). Declining Gate 1 keeps the classic handoff: pointer to/aws-startup-advisor:llm-to-bedrockwithhandoff-summary.md.add_capabilities→ loadreferences/phases/add-capabilities/add-capabilities.mdand follow it (no runtime scoring; writescapabilities-recommendation.md). This is a self-contained branch — it does NOT pass through Clarify / Confirm / Design / Estimate / Generate, so the phase gate below never applies to it.Temporal detection routes into
migratewith temporal units pre-seeded (see discover).
Gate semantics (backbone tail):
Gate 1 →
migration_planruns only whengenerateis done ANDrecommendation_reviewed == true(generate.md Step 5.5) AND entry point ∈ {migrate, build_deploy} AND the run is migration-eligible (generate.md Step 6) AND the user confirmed Gate 1. Otherwise resolve it:not_applicable(build_scratch / no migratable workload) orskipped(declined) — and advance.Gate 2 →
pocruns only whenphases.poc == "in_progress"(set when the user answers Gate 2 "yes" — asked in generate.md Step 7 or migration-plan.md Step 6) ANDrecommendation_reviewed == true. Any winning runtime (agentcore / ecs / eks / lambda / lambda_microvms) — the POC shape follows the verdict (poc.md Step 3 dispatch onreferences/decision-refs/poc-shapes.md). Gate 2 is only offered whenmigration_plan∈ {completed, skipped, not_applicable} — orin_progresson build_deploy only (Stage 2 failed/aborted; fallback POC from design.json per migration-plan.md failure handling); for entry pointmigrate, only whenmigration_plan == "completed"(the POC implements the plan) OR when the stage resolvednot_applicablewithmigration_plan_unavailable == "engine_absent"— a standalone deployment that does not bundle the migration engine, where Gate 2 is offered by migration-plan.md Step -1 and the POC is design-backed. A migrate-POC with no plan for any OTHER reason (the user declined) has nothing to implement.Persisting Gate 2 as
phases.poc = "in_progress"BEFORE poc.md loads makes the confirmation resumable: if the session breaks between the "yes" and the load, the interpreter re-enterspocwithout re-asking. (A declared deviation fromINTERPRETER.md§ The interpreter loop step 5's gate-then-in_progressordering — the user's confirmation is the entry event worth persisting.)
Phase gate: Do NOT load design.md / estimate.md / generate.md unless
$RUN_DIR/.phase-status.json exists and BOTH phases.clarify == "completed" AND
phases.confirm == "completed". Confirm confirms the deployment model, the service
set, and (for a co_recommend tie) the user's chosen_runtime — Design and the diagram depend on
its confirm.json output, so it must not be skipped. If the user asks to skip Clarify or Pass 2,
refuse briefly and run it.
State file (.phase-status.json)
{
"run_id": "0630-1430",
"entry_point": "build_scratch",
"audience": "technical",
"current_phase": "clarify",
"phases": {
"intake": "completed",
"discover": "skipped",
"clarify": "in_progress",
"confirm": "pending",
"design": "pending",
"estimate": "pending",
"generate": "pending",
"migration_plan": "pending",
"poc": "pending"
}
}Status values: pending → in_progress → completed, plus skipped. Use read-merge-write:
read before each update, change only the advancing keys, keep prior phases.
recommendation_reviewed (top level, boolean) is set to true by generate.md Step 5.5 when
the user explicitly confirms they have seen the recommendation. Gate 1, Gate 2, and the
migration_plan / poc states all require it — no gate may be asked while it is absent.
migration_plan additionally uses not_applicable (build_scratch, or no migratable workload
detected). When Stage 2 runs, migration_plan_ctx is added at the top level:
{"repo": "<abs path to target repo>", "migration_dir": "<abs path to .migration/<id>/>"} —
Stage 3 reads gcp-to-aws artifacts ONLY via this recorded path, never by re-globbing.
Files
| File | Purpose |
|---|---|
references/vendored/dsl/INTERPRETER.md | Vendored DSL execution contract (interpreter loop + gate protocol) |
references/phases/intake/intake.md | Entry point + technical background + open context |
references/phases/discover/discover.md | Lightweight code detection |
references/phases/clarify/clarify.md | Clarify orchestrator + answer mapping to scoring keys |
references/phases/clarify/clarify-technical.md | Technical-background question wording |
references/phases/clarify/clarify-business.md | Business-background question wording |
references/phases/confirm/confirm.md | Winner-specific follow-ups |
references/phases/design/design.md | Assemble recommendation; Migrate handoff branch |
references/phases/estimate/estimate.md | Coarse cost magnitude |
references/phases/generate/generate.md | Layered recommendation doc + scaffolding |
references/phases/migration-plan/migration-plan.md | Stage 2: full migration plan via the sibling gcp-to-aws engine |
references/decision-refs/temporal.md | Temporal rules: Tier 1/2 tables, adapter, runbooks, commercials (consumed by discover/clarify/design/generate) |
references/decision-refs/poc-shapes.md | Per-runtime POC deploy shapes (ECS/EKS/Lambda/MicroVMs/Temporal) |
references/decision-refs/*.md | Runtime service cards, model defaults, freshness |
references/decision-refs/workload-classes.md | Deterministic verdicts for non-agent workload units (batch/service/io) |
references/runtimes/*.json | Runtime registry (read by scoring.py) |
scripts/scoring.py | Deterministic scoring engine |
scripts/test_temporal_decision_refs.py | Content lock for the Temporal decision reference |
scripts/test_poc_shapes.py | Content lock for the POC deploy shapes |
scripts/test_workload_classes.py | Content lock for workload-classes.md (verdicts table) |
scripts/test_unit_grouping.py | Unit grouping + pattern matching (workload-class assignment) |
scripts/test_collapse_invariant.py | Collapse-invariant ordering enforcement (A→B implies [B] ⊆ [A] outputs) |
Maturity and readiness contract
Intake persists target_maturity (prototype, private_beta, or production) in run state. Clarify carries it and the readable readiness record into answers.json. Current-run verification evidence remains exclusively in the sibling $RUN_DIR/current-run-verifications.json artifact: never copy it into seed.json or answers.json. Design and Generate may consume that artifact only after validating its schema and matching run_id, and may carry forward verified outcomes but not the raw evidence records. Load references/decision-refs/maturity-readiness.md whenever target maturity is selected. A cached volatile fact may inform discovery, but only a record verified in this run can make a verification-required constraint final; otherwise the score remains provisional with deferred verification requirements.
Contextual offers (final step)
Before you finish: your answer is not complete until you have run the Contextual offers (final step) below.
This is a mandatory closing step of every final recommendation — complete it before you consider your answer done. Run this offer check inline yourself; do not depend on loading another skill. The recommendation is decided on merit first; this step never changes, re-ranks, or re-opens it, and the offer is looked up only after the advice is final.
Read
../knowledge-base-for-startups/references/offers.md— the offer index (Offer · Summary · Keywords), one row per offer.Judge relevance against what the founder actually surfaced this session. Surface an offer only if one holds: a named-product match (you recommended a specific partner product by name and it has a row); a direct intent/component match (an offer's Keywords directly serve a concrete workload, a component in the final recommendation, or a need the founder raised). Keep a high bar — if the match is weak, or you would be inferring an unstated need, append nothing. Never surface an offer that competes with a product or provider the founder already uses or decided to keep (e.g. existing auth provider → no Auth0 unless they are already on Auth0, and then only as a credit on it; existing vector DB such as Pinecone → no Qdrant).
If (and only if) one offer clears the bar, open only its detail file
../knowledge-base-for-startups/references/offers/<slug>.mdand append exactly one quiet, dismissible italic line — stating the value and condensing eligibility faithfully from that file (drop no qualifying criterion; never fabricate), with the redeem link built from the canonicalsource_urlplus asourceattribution param — append?source=ide-startupAdvisor-<host>(or&source=...if the url already has a query string), filling<host>from your execution environment (Claude Code →claude; AWS Startup Advisor IDE extension / VS Code →vscode; Cursor →cursor; Kiro →kiro; Codex →codex), or the genericide-startupAdvisorwhen you cannot tell. Only append the query param; never change the destination. When an offer clears the bar you MUST append that single offer line as the last step; do not omit it. The offer is never the pick — only a line under the AWS pick — and stage gates in the references apply (e.g. no Datadog line for an early-stage founder; see../architect-for-startups/references/observability.md). For a single-service pick (auth, payments, vector DB, observability, etc.) where missing budget/team/traffic inputs would not change the pick, assume an early-stage (pre-PMF) startup unless the founder says otherwise and answer in one turn without a stage-only question; this only removes a stage-only question and never skips the Intake/Clarify/Confirm gates before final advice or the POC. Show exactly one, never a list. If none clears the bar, append nothing and do not mention offers, partner alternatives, or why none appeared.
Standard line:
***<Offer>** has an exclusive offer via AWS Activate — <value>, <eligibility>. [Redeem →](<source_url>?source=ide-startupAdvisor-<host>)*Caps and control: at most one offer per response and often none; no more than one per five messages and two per session; show a given offer at most once per session and never one already shown, claimed, or dismissed; if the founder has muted offers, skip this step entirely. These per-five-messages, per-session, and already-shown caps are session-state limits; in a fresh session with no prior offers they are non-binding, so do not withhold an otherwise-qualifying offer merely because you cannot verify session history. See ../contextual-offers-for-startups/SKILL.md for the full rules — but perform the check inline; it must not depend on that skill being loaded.
Files
- ATTRIBUTION.mdAdded by AutoGineer907 B
- LICENSELicence file added by AutoGineer9.9 KiB
- NOTICELicence file added by AutoGineer67 B
- SKILL.md19.2 KiB
- references/decision-refs/agentcore.md5.4 KiB
- references/decision-refs/batch.md2.3 KiB
- references/decision-refs/cost-levers.md1.8 KiB
- references/decision-refs/ecs.md1.4 KiB
- references/decision-refs/eks.md1.1 KiB
- references/decision-refs/freshness.md10.7 KiB
- references/decision-refs/lambda-microvms.md1.9 KiB
- references/decision-refs/lambda.md1.1 KiB
- references/decision-refs/managed-alternatives.md1.9 KiB
- references/decision-refs/maturity-readiness.md2.8 KiB
- references/decision-refs/model-selection.md4.2 KiB
- references/decision-refs/poc-shapes.md15.5 KiB
- references/decision-refs/temporal.md17.5 KiB
- references/decision-refs/workload-classes.md2.2 KiB
- references/diagram/build-diagram.md5.1 KiB
- references/handoff/handoff-migration.md2.1 KiB
- references/output-templates/recommendation-doc.md5.1 KiB
- references/phases/add-capabilities/add-capabilities-assemble.md951 B
- references/phases/add-capabilities/add-capabilities.md4.6 KiB
- references/phases/clarify/clarify-assemble.md1.4 KiB
- references/phases/clarify/clarify-business.md3.3 KiB
- references/phases/clarify/clarify-technical.md2.7 KiB
- references/phases/clarify/clarify.md26.5 KiB
- references/phases/confirm/confirm-assemble.md1019 B
- references/phases/confirm/confirm.md15.1 KiB
- references/phases/design/design-assemble.md1.7 KiB
- references/phases/design/design.md26.9 KiB
- references/phases/discover/discover-assemble.md1.2 KiB
- references/phases/discover/discover.md10.0 KiB
- references/phases/estimate/estimate-assemble.md780 B
- references/phases/estimate/estimate.md19.0 KiB
- references/phases/generate/generate-assemble.md1.0 KiB
- references/phases/generate/generate-report.md45.6 KiB
- references/phases/generate/generate.md15.7 KiB
- references/phases/intake/intake-assemble.md991 B
- references/phases/intake/intake.md5.6 KiB
- references/phases/migration-plan/migration-plan-assemble.md1.2 KiB
- references/phases/migration-plan/migration-plan-gcp-constraints.md3.9 KiB
- references/phases/migration-plan/migration-plan.md32.1 KiB
- references/phases/model-recommend/model-recommend-assemble.md937 B
- references/phases/model-recommend/model-recommend.md17.2 KiB
- references/phases/poc/poc-assemble.md1.0 KiB
- references/phases/poc/poc-report.md12.6 KiB
- references/phases/poc/poc.md44.8 KiB
- references/report-help-banner.md6.0 KiB
- references/report-shell.md12.3 KiB
- references/runtimes/agentcore.json3.1 KiB
- references/runtimes/ecs.json1.5 KiB
- references/runtimes/eks.json1.4 KiB
- references/runtimes/lambda.json2.4 KiB
- references/runtimes/lambda_microvms.json3.3 KiB
- references/vendored/README.md2.0 KiB
- references/vendored/ai/bedrock-pricing-cache.md57.2 KiB
- references/vendored/ai/models/anthropic-bedrock.json8.6 KiB
- references/vendored/ai/models/openai-bedrock.json28.0 KiB
- references/vendored/dsl/INTERPRETER.md51.8 KiB
- scripts/anthropic_model_recommendation.pyScript35.0 KiB
- scripts/build_diagram.pyScript21.2 KiB
- scripts/model_recommendation.pyScript8.8 KiB
- scripts/openai_model_recommendation.pyScript58.1 KiB
- scripts/pyproject.toml170 B
- scripts/schemas/current-run-verifications.json1.1 KiB
- scripts/schemas/model-recommendation-input.json5.4 KiB
- scripts/schemas/model-recommendation.json13.1 KiB
- scripts/schemas/model-verification.json2.5 KiB
- scripts/schemas/scoring-result.json3.5 KiB
- scripts/schemas/seed.json5.6 KiB
- scripts/score_units.pyScript2.9 KiB
- scripts/scoring.pyScript16.5 KiB
- scripts/test_build_diagram.pyScript21.8 KiB
- scripts/test_collapse_invariant.pyScript2.0 KiB
- scripts/test_cost_levers.pyScript1.7 KiB
- scripts/test_model_recommendation.pyScript13.9 KiB
- scripts/test_openai_model_recommendation.pyScript46.5 KiB
- scripts/test_opus55_generation_contracts.pyScript20.8 KiB
- scripts/test_opus55_integration.pyScript21.5 KiB
- scripts/test_poc_shapes.pyScript14.2 KiB
- scripts/test_scoring.pyScript35.3 KiB
- scripts/test_temporal_decision_refs.pyScript7.4 KiB
- scripts/test_unit_grouping.pyScript71.7 KiB
- scripts/test_verify_model_path.pyScript25.8 KiB
- scripts/test_workload_classes.pyScript5.2 KiB
- scripts/uv.lock97.8 KiB
- scripts/verify_model_path.pyScript10.9 KiB
scripts/openai_model_recommendation.py
"""Deterministic OpenAI-to-Bedrock model/path recommendation for agent-advisor.
Factual baseline: an internal OpenAI-to-Bedrock migration reference (2026-06 live
probes and its recorded feature-status notes). The engine is offline:
no openai/boto3/token-generator imports at module load. Rankings are only produced
where the reference provides evidence; otherwise the engine returns an explicit
decision or a provisional limitation rather than inventing a comparative order.
"""
import re
import anthropic_model_recommendation
# --- Source model-family detection (ported from the reference compatibility matrix) ---
# Reasoning generation: gpt-5 / gpt-6 variants and o1/o3/o4.
_REASONING_PATTERNS = (
re.compile(r"(?i)(^|[^a-z0-9])gpt[-_ ]?[56](\.\d+)?([^0-9]|$)"),
re.compile(r"(?i)(^|[^a-z0-9])o[134](-mini|-pro|-preview)?([^a-z0-9]|$)"),
)
# Legacy generation: gpt-4*, gpt-3.5 / gpt-35.
_LEGACY_PATTERNS = (
re.compile(r"(?i)gpt[-_ ]?4o"),
re.compile(r"(?i)gpt[-_ ]?4(?!\d)"),
re.compile(r"(?i)gpt[-_ ]?4[-_ ]?32k"),
re.compile(r"(?i)gpt[-_ ]?3\.?5"),
re.compile(r"(?i)gpt[-_ ]?35"),
)
# GPT-5.2+ re-accepts sampling on Responses; earlier reasoning models differ.
_GPT5_MINOR = re.compile(r"(?i)gpt[-_ ]?5\.(\d+)")
def detect_family(model_id):
"""Return 'reasoning', 'legacy', or 'unknown'.
Fails to 'unknown' for opaque deployment names (e.g. 'prod-default') so the
engine never infers a model family from a deployment alias.
"""
name = (model_id or "").strip()
if not name:
return "unknown"
for pat in _REASONING_PATTERNS:
if pat.search(name):
return "reasoning"
for pat in _LEGACY_PATTERNS:
if pat.search(name):
return "legacy"
return "unknown"
def _gpt5_minor(model_id):
match = _GPT5_MINOR.search(model_id or "")
return int(match.group(1)) if match else None
def _primary_source_id(source):
ids = source.get("model_ids") or []
return ids[0] if ids else ""
def _source_family(source):
"""Family for the workload: reasoning if ANY id is reasoning, else legacy if
any is legacy, else unknown (opaque deployment names stay unknown)."""
families = [detect_family(mid) for mid in source.get("model_ids") or [""]]
if "reasoning" in families:
return "reasoning"
if "legacy" in families:
return "legacy"
return "unknown"
def _finding(code, tag, message, remediation):
return {"code": code, "tag": tag, "message": message, "remediation": remediation}
def _delta(code, category, description):
return {"code": code, "category": category, "description": description}
# --- Feature vocabulary (provider-scoped; codes match scanner feature-catalog) ---
_HOSTED_TOOL_IMPACTS = {
"web_search": (
"Hosted web search does not execute live on Bedrock Mantle (verified passthrough: "
"no query issued, no citations).",
"Implement a client-side tool loop, or expose a server-side tool (MCP/Lambda) through "
"Bedrock AgentCore Gateway; use Knowledge Bases for document retrieval.",
),
"file_search_retrieval": (
"Hosted file search / retrieval is not a drop-in on Bedrock.",
"Re-platform retrieval onto Bedrock Knowledge Bases or application-owned retrieval.",
),
"files_api": (
"The OpenAI Files API and vector stores do not port directly.",
"Redesign data ingestion and retrieval; use Knowledge Bases or application storage.",
),
"vector_stores": (
"OpenAI vector stores are not a Bedrock primitive.",
"Move vectors to a Bedrock-supported store and retrieve from the application.",
),
"assistants_threads": (
"Assistants/Threads hosted state is not available on Bedrock.",
"Redesign agent state, tools, and memory (e.g. Bedrock AgentCore).",
),
"audio_modality": (
"Audio is not a single-call text-model capability on Bedrock.",
"Split into a verified STT/TTS service path (Amazon Transcribe / Polly).",
),
"embeddings": (
"Embeddings require a separate Bedrock embedding model, not the text model.",
"Select a sourced Bedrock embedding model (e.g. Titan/Cohere) as a separate workload.",
),
"images": (
"Image generation/editing is a separate capability, not a text-model swap.",
"Select a sourced Bedrock image model/service as a separate workload.",
),
}
# Converse tier map: a governance workload keeps its capability tier when moving
# from the (Mantle-only) GPT-5.x family to Claude on runtime Converse.
# GPT-5.6 Sol (frontier) -> Claude Opus 5.5
# GPT-5.6 Terra / 5.5 / 5.4 -> Claude Sonnet 5 (also the default tier)
# GPT-5.6 Luna (fast/low-cost)-> Claude Haiku 4.5
_CONVERSE_TIER_DEFAULT = "anthropic_claude_sonnet_5"
_CONVERSE_TIER_ORDER = (
"anthropic_claude_sonnet_5",
"anthropic_claude_opus_5_5",
"anthropic_claude_haiku_4_5",
)
def _converse_tier_for_source(source):
sid = _primary_source_id(source).lower()
if "5.6-sol" in sid or "6-astra" in sid:
return "anthropic_claude_opus_5_5"
if "5.6-luna" in sid:
return "anthropic_claude_haiku_4_5"
# 5.6-terra, 5.5, 5.4, legacy, and unknown sources all map to the balanced tier.
return _CONVERSE_TIER_DEFAULT
_SOURCE_MODEL_KEYS = {
"gpt-5.6-sol": "openai_gpt_5_6_sol",
"gpt-5.6-terra": "openai_gpt_5_6_terra",
"gpt-5.6-luna": "openai_gpt_5_6_luna",
"gpt-6-astra": "openai_gpt_6_astra",
"gpt-6-sol": "openai_gpt_6_sol",
"gpt-6-luna": "openai_gpt_6_luna",
"gpt-6.1-sol": "openai_gpt_6_1_sol",
}
def _same_model_catalog_key(source):
"""Return the exact catalog key for a recognized source model identity."""
sid = re.sub(r"[_ ]", "-", _primary_source_id(source).lower())
for identity in sorted(_SOURCE_MODEL_KEYS, key=len, reverse=True):
if re.search(rf"(?<![a-z0-9]){re.escape(identity)}(?![a-z0-9])", sid):
return _SOURCE_MODEL_KEYS[identity]
return None
def _same_model_runtime_key(source):
"""Return the exact source model when it has a runtime_converse path."""
return _same_model_catalog_key(source)
def _converse_candidate_order(source, requirements=None):
tier = (
"anthropic_claude_opus_5_5"
if (requirements or {}).get("priority") == "quality"
else _converse_tier_for_source(source)
)
order = [tier] + [k for k in _CONVERSE_TIER_ORDER if k != tier]
same = _same_model_runtime_key(source)
if same:
# Same model outranks any cross-family tier: keeping the model removes
# prompt-adaptation and behavior-delta risk entirely.
order.insert(0, same)
return order
def _mantle_path(source, requirements):
same = _same_model_catalog_key(source)
if (
source.get("api_surface") == "chat_completions"
and _api_continuity(requirements) in {"required", "preferred"}
and same is not None
and same.startswith("openai_gpt_6")
):
return "mantle_openai_chat"
return "mantle_openai_responses"
def _mantle_candidate_order(catalog, source):
same = _same_model_catalog_key(source)
keys = list(catalog["models"])
return ([same] if same else []) + [key for key in keys if key != same]
def _hard_profile_requirement(requirements):
"""Whether runtime selection must prove a specific CRIS profile is callable."""
residency = requirements.get("data_residency")
return bool(
requirements.get("inference_profile_id")
or residency == "global_allowed"
or (residency == "geo_required" and requirements.get("cris_geography"))
)
def _catalog_model_for_path(catalog, path, detected_features=None, requirements=None,
candidate_order=None, region=None):
"""Pick the first available catalog model for `path` whose capability evidence
covers the required/detected text features (evidence-driven fallback: a newer
model without feature-level evidence is skipped in favor of an older probed
one). Mantle candidates with cataloged region availability are skipped outside
that footprint; runtime Converse availability remains governed by CRIS resolution.
When no candidate covers the requirements, return the first available model together
with its unmet-capability list so the caller can fail closed. Deterministic:
iterates `candidate_order` when given (e.g. the Converse tier map), else
catalog key order."""
first = None
first_unmet = None
keys = candidate_order if candidate_order is not None else list(catalog["models"])
for model_key in keys:
model = catalog["models"].get(model_key)
if model is None:
continue
path_config = model["paths"].get(path, {})
if path_config.get("available") is not True:
continue
if "capabilities" in path_config:
model = {**model, "capabilities": path_config["capabilities"]}
availability = model.get("region_availability")
if path.startswith("mantle_") and region and region != "unknown" and availability:
supported_regions = set(availability.get("commercial", []))
supported_regions.update(availability.get("govcloud", []))
if region not in supported_regions:
continue
if (
path == "runtime_converse"
and not path_config.get("inference_profile_regions")
and path_config.get("requires_cris")
and _hard_profile_requirement(requirements or {})
and _resolve_invocation_model_id(
path_config["model_id"], True, requirements or {}, model, region
)
is None
):
continue
unmet = _unsupported_required_capabilities(
model, detected_features, requirements or {}
)
if first is None:
first = (model_key, model, path_config)
first_unmet = unmet
if not unmet:
return (model_key, model, path_config), []
return first, first_unmet
def _resolve_invocation_model_id(model_id, requires_cris, requirements, model=None, region=None):
if (model or {}).get("inference_profiles"):
return anthropic_model_recommendation._resolve_invocation_model_id(
model_id, requires_cris, requirements, model, region
)
if not requires_cris:
return model_id
explicit = requirements.get("inference_profile_id")
if explicit:
return explicit
residency = requirements.get("data_residency", "unknown")
if residency == "global_allowed":
return f"global.{model_id}"
if residency == "geo_required" and requirements.get("cris_geography"):
return f"{requirements['cris_geography']}.{model_id}"
return None
def _api_continuity(requirements):
"""Provider-neutral continuity: required | preferred | not_required | unknown.
Accepts the explicit `api_continuity` field; falls back to the OpenAI-specific
`preserve_openai_api` boolean. Never reuses Anthropic's preserve_messages_api.
"""
explicit = requirements.get("api_continuity")
if explicit:
return explicit
if requirements.get("preserve_openai_api") is True:
return "required"
if requirements.get("preserve_openai_api") is False:
return "not_required"
return "unknown"
def _runtime_required(requirements):
return bool(requirements.get("governance")) or requirements.get(
"multi_model_converse", False
)
def _model_identity(model_key, model, path_config, invocation_model_id=None):
return {
"model_key": model_key,
"display_name": model["display_name"],
"family": model["family"],
"version": model["version"],
"context_window": model["context_window"],
"output_token_ceiling": model["output_token_ceiling"],
"path_model_id": path_config["model_id"],
"requires_cris": anthropic_model_recommendation._invocation_requires_cris(
path_config, invocation_model_id
),
}
def _numeric_requirement_conflict(model, requirements):
"""Fail closed: an unknown catalog limit cannot satisfy a hard numeric need."""
conflicts = []
for req_key, cat_key, label in (
("min_context_tokens", "context_window", "context window"),
("expected_output_tokens", "output_token_ceiling", "output ceiling"),
):
need = requirements.get(req_key)
if not need:
continue
have = model.get(cat_key)
if have == "unknown" or not isinstance(have, int):
conflicts.append(
f"{label} for {model['display_name']} is unknown in the catalog; "
f"a required {need}-token need cannot be confirmed"
)
elif have < need:
conflicts.append(
f"{label} for {model['display_name']} ({have}) is below the required {need}"
)
return conflicts
# Extracts a numeric OpenAI version (major or major.minor) from an id/version.
_OPENAI_VERSION = re.compile(r"(?i)gpt[-_ ]?(\d+(?:\.\d+)?)")
def _openai_version(text):
match = _OPENAI_VERSION.search(text or "")
return match.group(1) if match else None
def _version_tuple(value):
if not value:
return None
parts = value.split(".")
try:
return (int(parts[0]), int(parts[1]) if len(parts) > 1 else 0)
except (ValueError, IndexError):
return None
def _target_accepts_sampling(target_model):
"""GPT-5.2+ re-accepts sampling params on Responses (verified for GPT-5.4).
Returns True/False for an OpenAI target of known version, None when the
target is not an OpenAI model or its version can't be parsed.
"""
if target_model is None or target_model.get("generation") == "bedrock_native":
return None
if target_model.get("display_name") == "OpenAI GPT-6 Astra (on Bedrock)":
return None
version = _version_tuple(target_model.get("version"))
if version is None:
return None
return version >= (5, 2)
def _source_analysis(source, target_model=None):
"""Source-side facts plus, when a target has been selected, target-derived
version/generation change flags. Target fields stay null before selection."""
source_id = _primary_source_id(source)
family = _source_family(source)
surface = source.get("api_surface")
if target_model is None:
target_version = None
version_changed = None
generation_changes = None
else:
target_version = target_model.get("version")
target_generation = target_model.get("generation")
if target_generation == "bedrock_native":
# OpenAI source -> a Bedrock-native model is always a model change.
version_changed = True
generation_changes = True
else:
source_version = _openai_version(source_id)
if source_version and target_version:
version_changed = source_version != target_version
else:
# o-series / opaque source vs a specific GPT-5.x target: a move,
# but we cannot pin the exact source version.
version_changed = family in {"legacy", "reasoning"} or None
generation_changes = family == "legacy" and target_generation == "reasoning"
return {
"detected_version": source_id or None,
"target_version": target_version,
"version_changed": version_changed,
"source_family": family,
"source_api_surface": surface,
"model_generation_changes": generation_changes,
}
def _reasoning_findings(source, requirements, target_model, path, detected_features=()):
"""Version- and surface-specific parameter findings, derived from the SELECTED
target model and path — never from the source id, and never Anthropic's blanket
sampling-removal rule."""
blocks, tuning, deltas = [], [], []
src_family = _source_family(source)
src_id = _primary_source_id(source)
target_generation = target_model.get("generation") if target_model else None
target_name = target_model.get("display_name") if target_model else "the target model"
if target_generation == "reasoning":
tuning.append(
_finding(
"reasoning_token_headroom",
"[TUNE]",
"Reasoning models spend hidden thinking tokens against the output budget; "
"too-small caps return status:incomplete.",
"Size the output cap for reasoning output. The reference uses ~2.5x the legacy "
"budget plus a 4096 floor as a STARTING heuristic, not a guaranteed value; "
"tune from measured output distributions.",
)
)
if src_family == "legacy" and target_generation == "reasoning":
deltas.append(
_delta(
"model_generation_hop",
"version",
f"Source '{src_id}' is a GPT-4.x/legacy model migrating to a reasoning target "
f"({target_name}): reasoning controls, token budget, and prompt behavior change.",
)
)
# Sampling acceptance is a property of the TARGET model and the selected path.
if target_model and target_model.get("sampling_parameters_supported") is False:
remediation = (
"Remove temperature/top_p/top_k from requests to this target; do not rescale them "
"or disable thinking. Route user-visible controls through the existing "
"parameter_removed confirmation before changing them."
)
finding = _finding(
"sampling_parameters_removed",
"[BLOCKS]" if "sampling_params" in detected_features else "[TUNE]",
f"{target_name} does not support sampling controls with its required thinking mode.",
remediation,
)
(blocks if finding["tag"] == "[BLOCKS]" else tuning).append(finding)
if "sampling_params" in detected_features:
deltas.append(_delta("sampling_parameters_removed", "feature", remediation))
elif path == "runtime_converse":
tuning.append(
_finding(
"sampling_via_converse",
"[TUNE]",
f"On runtime Converse, sampling is set through inferenceConfig "
f"(temperature/topP) on {target_name}, not OpenAI SDK kwargs.",
"Move temperature/top_p into Converse inferenceConfig; penalties/logprobs/stop "
"are not universally supported and must be verified.",
)
)
else:
accepts = _target_accepts_sampling(target_model)
if accepts is True:
tuning.append(
_finding(
"sampling_params_accepted",
"[TUNE]",
f"{target_name} (GPT-5.2+) accepts temperature and top_p on the Responses "
"surface; do not strip them.",
"Keep temperature/top_p and calibrate against a golden set.",
)
)
tuning.append(
_finding(
"sampling_penalties_rejected",
"[TUNE]",
"frequency_penalty and presence_penalty were rejected as SDK kwargs on the "
"verified GPT-5.4 Responses probe.",
"Remove frequency_penalty/presence_penalty. logprobs, logit_bias, and stop "
"were NOT part of that probe and are version/endpoint-specific — verify each "
"for the exact target model rather than assuming acceptance or rejection.",
)
)
elif accepts is False:
tuning.append(
_finding(
"sampling_params_version_specific",
"[TUNE]",
f"{target_name} predates GPT-5.2 and may reject sampling parameters on "
"Responses; behavior is version-specific.",
"Verify sampling acceptance for the exact target model and surface.",
)
)
elif target_model and target_model.get("display_name") == "OpenAI GPT-6 Astra (on Bedrock)":
tuning.append(
_finding(
"sampling_params_unverified", "[TUNE]",
"Astra's model card does not specify accepted sampling parameters.",
"Probe sampling and reasoning settings on the selected API; do not "
"inherit GPT-5.x parameter acceptance or rejection rules.",
)
)
return blocks, tuning, deltas
def _chat_to_responses_deltas():
return [
_delta(
"chat_to_responses_request",
"path",
"Chat Completions -> Responses: messages->input, system message->instructions, "
"max_tokens->max_output_tokens.",
),
_delta(
"chat_to_responses_response",
"path",
"Read model output from output_text instead of the chat message content.",
),
_delta(
"chat_to_responses_tools",
"feature",
"Tool results move to function_call_output on the Responses surface.",
),
_delta(
"chat_to_responses_state",
"path",
"Replayed multi-turn context uses previous_response_id where appropriate, or "
"application-owned state.",
),
]
def _feature_findings(detected_features, requirements, path=None, target_model=None):
"""Structured output, n, tools, state, and hosted-tool/modality impacts.
Findings are path-aware: Mantle Responses keeps verified capabilities
(responses.parse typed output, hosted store=True continuation) that a
Bedrock-native Converse target does not."""
detected = set(detected_features or [])
detected.update(requirements.get("critical_features") or [])
is_responses = path == "mantle_openai_responses"
is_chat = path == "mantle_openai_chat"
blocks, tuning, deltas, impacts = [], [], [], []
if "structured_output_json" in detected:
if path == "runtime_converse" and (target_model or {}).get("display_name") == "OpenAI GPT-6 Astra (on Bedrock)":
deltas.append(
_delta(
"structured_output_text_format",
"feature",
"Astra runtime JSON Schema output is documented for non-streaming calls. "
"Set outputConfig.textFormat.type=json_schema and put name plus schema "
"(encoded as a JSON string) in outputConfig.textFormat.structure.jsonSchema. "
"Also set additionalModelRequestFields.text.format.strict=true. Use an "
"object schema with all fields required and additionalProperties=false; "
"validate the schema and check refusal/incomplete responses before parsing.",
)
)
elif is_responses:
# Verified on Mantle: responses.parse(text_format=Model) returns a typed object.
deltas.append(
_delta(
"structured_output_text_format",
"feature",
"On Mantle Responses, keep typed structured output: "
"client.responses.parse(..., text_format=Model) is verified, or use raw "
"text.format json_schema.",
)
)
else:
deltas.append(
_delta(
"structured_output_text_format",
"feature",
"For the selected non-Responses path, enforce the schema through that "
"path's supported request shape plus application validation; the OpenAI "
"Responses parse() helper is not available.",
)
)
if requirements.get("uses_n") or "multiple_candidates_n" in detected:
if path == "mantle_openai_chat":
if target_model and target_model.get("display_name") == "OpenAI GPT-6 Astra (on Bedrock)":
tuning.append(
_finding(
"multiple_candidates_n_unverified", "[TUNE]",
"Astra Chat Completions support for n has not been verified.",
"Probe n on the selected model and endpoint before preserving it; "
"if unsupported, request multiple candidates with repeated calls.",
)
)
else:
deltas.append(
_delta(
"responses_no_n",
"feature",
"The Responses API does not support n; request multiple candidates with "
"repeated calls.",
)
)
if "tool_or_function_calling" in detected:
if is_chat:
tool_description = (
"On Mantle Chat Completions, preserve assistant tool_calls and continue each "
"result as a role: tool message with the matching tool_call_id; the "
"application executes the tool."
)
else:
tool_description = (
"Tool calls continue via function_call_output on Responses; the application "
"executes the tool."
)
deltas.append(_delta("tool_result_shape", "feature", tool_description))
if "conversation_state" in detected or requirements.get("uses_hosted_state"):
if target_model and target_model.get("display_name") == "OpenAI GPT-6 Astra (on Bedrock)":
deltas.append(
_delta(
"conversation_state_ownership",
"path",
"Astra's hosted state behavior has not been probed on this API. Verify "
"retention and continuation semantics; use application-owned history "
"until those requirements are confirmed.",
)
)
elif is_responses:
deltas.append(
_delta(
"conversation_state_ownership",
"path",
"Two verified modes on Mantle Responses: server-hosted continuation "
"(store=True + previous_response_id) or manual replay (store=False with "
"application-owned history). Retention/compliance/availability are "
"verification questions, not a lost capability.",
)
)
elif is_chat:
deltas.append(
_delta(
"conversation_state_ownership",
"path",
"Mantle Chat Completions uses application-owned message history; replay "
"the prior user, assistant, tool-call, and tool-result messages on each "
"request.",
)
)
else:
deltas.append(
_delta(
"conversation_state_ownership",
"path",
"This path does not use hosted Responses state; carry "
"conversation history in an application-owned store.",
)
)
impacted = sorted(detected.intersection(_HOSTED_TOOL_IMPACTS))
for feature in impacted:
impact, remediation = _HOSTED_TOOL_IMPACTS[feature]
impacts.append(
{"feature": feature, "impact": impact, "recommendation": remediation}
)
return blocks, tuning, deltas, impacts
# Text-model features that map to portable request-shape changes on any path.
_PORTABLE_FEATURES = {"max_tokens", "sampling_params", "sampling_params_accepted"}
# Separate-capability modalities that need their OWN target, not the text model.
# service = the Bedrock service family the reference names; candidate stays null
# (unresolved) because the reference does not pin a specific verified model ID.
_SEPARATE_MODALITY_TARGETS = {
"embeddings": (
"Amazon Bedrock embedding model (e.g. Titan/Cohere families)",
"05_migrating_for_real.ipynb maps embeddings to a separate Bedrock embedding model",
),
"images": (
"Amazon Bedrock image model/service (e.g. Titan Image / Nova / Stability)",
"03_reasoning_api_migration.ipynb maps images to a separate Bedrock image model",
),
"audio_modality": (
"Amazon Transcribe / Polly (STT/TTS)",
"feature-catalog + notebooks map audio to Transcribe/Polly, not a text model",
),
}
def _additional_targets(detected_features, requirements):
"""Emit an explicit per-modality target contract; candidate stays null
(unresolved) since the reference names a service family, not a verified ID (G06)."""
detected = set(detected_features or [])
detected.update(requirements.get("critical_features") or [])
targets = []
for capability in sorted(detected.intersection(_SEPARATE_MODALITY_TARGETS)):
service, evidence = _SEPARATE_MODALITY_TARGETS[capability]
targets.append(
{
"capability": capability,
"status": "unresolved",
"candidate": None,
"service": service,
"evidence": evidence
+ "; select and verify a specific model/service in the target account",
}
)
return targets
def _feature_assessment(workload):
"""Merge detected_features and feature_status; feature_status is authoritative
for detected/absent/unknown, so an explicit 'unknown' is preserved (G08)."""
assessment = {}
for feature in workload.get("detected_features") or []:
assessment[feature] = "detected"
for feature, status in (workload.get("feature_status") or {}).items():
assessment[feature] = status
return dict(sorted(assessment.items()))
def _unknown_required_features(feature_assessment, requirements):
"""Required features whose status is explicitly unknown (G08)."""
required = set(requirements.get("critical_features") or [])
return sorted(f for f in required if feature_assessment.get(f) == "unknown")
def _unsupported_required_capabilities(target_model, detected_features, requirements):
"""Required/detected text-model features NOT evidenced by the target's catalog
capabilities. Hosted tools/modalities are handled as architecture impacts, not here."""
if target_model is None:
return []
catalog_caps = set(target_model.get("capabilities") or [])
needed = set(requirements.get("critical_features") or [])
needed.update(detected_features or [])
checkable = needed.intersection(
{
"tool_or_function_calling",
"structured_output_json",
"streaming",
"image_input_vision",
"reasoning",
}
)
if target_model.get("display_name") == "OpenAI GPT-6 Astra (on Bedrock)" and "prompt_caching" in needed:
checkable.add("prompt_caching")
return sorted(checkable - catalog_caps)
def _compatibility(detected_features, requirements, path, target_model=None):
"""native = only features the SELECTED target's catalog evidences; features
needed but not evidenced go to unsupported (never derive native from the name)."""
detected = set(detected_features or [])
detected.update(requirements.get("critical_features") or [])
text_features = detected.intersection(
{"tool_or_function_calling", "structured_output_json", "streaming", "image_input_vision", "reasoning"}
)
if (target_model or {}).get("display_name") == "OpenAI GPT-6 Astra (on Bedrock)" and "prompt_caching" in detected:
text_features.add("prompt_caching")
catalog_caps = set((target_model or {}).get("capabilities") or [])
native = sorted(text_features.intersection(catalog_caps)) if target_model else []
unsupported = sorted(text_features - catalog_caps) if target_model else []
rearchitecture = sorted(detected.intersection(_HOSTED_TOOL_IMPACTS))
portable = sorted(detected.intersection(_PORTABLE_FEATURES))
return {
"native": native,
"portable": portable,
"rearchitecture": rearchitecture,
"unsupported": unsupported,
}
def _evaluation(detected_features, requirements):
detected = set(detected_features or [])
detected.update(requirements.get("critical_features") or [])
trajectory = bool(
detected.intersection(
{"tool_or_function_calling", "assistants_threads", "web_search", "file_search_retrieval"}
)
or "agentic" in (requirements.get("critical_features") or [])
)
gates = [
"Build a deterministic discovery inventory of clients, APIs, model IDs, and features.",
"Compare viable path/model candidates on a representative golden set.",
"Size quota for peak traffic and reasoning-output tokens.",
"Fail on refusal mishandling, truncation, or invalid structured output.",
]
if trajectory:
gates.extend(
[
"Verify correct tool selection, valid tool arguments, and tool-result continuation.",
"Verify loop termination and structured-output validity.",
]
)
return {"mode": "trajectory" if trajectory else "prompt", "gates": gates}
def _verification(region, catalog, path, requires_cris, invocation_model_id, selected,
model=None, requirements=None):
if not selected:
return {
"region": region,
"catalog_verified_at": catalog["verified_at"],
"verified_at": None,
"probe_status": "not_applicable",
"availability_claim": "not_selected",
"invocation_model_id": None,
"required_checks": [
"Resolve the model/path decision before running an availability probe."
],
}
requires_cris = requires_cris and invocation_model_id != (model or {}).get(
"paths", {}
).get(path, {}).get("model_id")
checks = [
"Probe the selected model through the selected API path in the target account and region.",
"Verify path-specific IAM, model access, and quota before code rewrite or POC generation.",
]
if requires_cris:
checks.insert(
1, "Resolve and probe a Global or geography-scoped CRIS inference profile."
)
return {
"region": region,
"catalog_verified_at": catalog["verified_at"],
"verified_at": None,
"probe_status": "not_run",
"availability_claim": "provisional",
"invocation_model_id": invocation_model_id,
"required_checks": checks,
**anthropic_model_recommendation._profile_verification(
(model or {}).get("paths", {}).get(path, {}).get("model_id"),
requires_cris, model, region, requirements,
),
}
def _decision_options(catalog, workload, region):
"""Two-sided option set: the selected Mantle API vs runtime Converse governance."""
options = []
detected = [feature for feature, status in _feature_assessment(workload).items()
if status == "detected"]
astra = _same_model_runtime_key(workload["source"]) == "openai_gpt_6_astra"
mantle_path = _mantle_path(workload["source"], workload["requirements"])
if astra:
preferred = workload["requirements"].get("preferred_api_path")
if preferred in {"mantle_openai_chat", "mantle_openai_responses"}:
mantle_path = preferred
elif workload["source"].get("api_surface") == "chat_completions":
mantle_path = "mantle_openai_chat"
# A decision may offer changing an incompatible Chat preference to the
# documented Responses caching path; it must not silently select it.
needed = set(detected) | set(workload["requirements"].get("critical_features") or [])
if "prompt_caching" in needed:
mantle_path = _mantle_path(workload["source"], workload["requirements"])
mantle, _ = _catalog_model_for_path(
catalog, mantle_path, detected, workload["requirements"],
candidate_order=["openai_gpt_6_astra"] if astra else _mantle_candidate_order(catalog, workload["source"]),
region=region,
)
mantle_requires_region_change = False
if mantle is None and region != "unknown":
mantle, _ = _catalog_model_for_path(
catalog, mantle_path, detected, workload["requirements"],
candidate_order=_mantle_candidate_order(catalog, workload["source"]),
region="unknown",
)
mantle_requires_region_change = mantle is not None
if mantle:
model_key, model, path_config = mantle
reason = (
"Preserves the OpenAI SDK and Chat Completions surface; gives up "
"runtime-only Bedrock governance."
if mantle_path == "mantle_openai_chat"
else "Preserves the OpenAI SDK and Responses surface; gives up "
"runtime-only Bedrock governance."
)
if mantle_requires_region_change:
availability = model.get("region_availability") or {}
supported_regions = sorted(
set(availability.get("commercial", []))
| set(availability.get("govcloud", []))
)
region_label = ", ".join(supported_regions) or "a catalog-supported region"
reason += (
f" Requires changing from {region} to a catalog-supported Mantle region "
f"({region_label})."
)
options.append(
{
"model_key": model_key,
"model": path_config["model_id"],
"api_path": mantle_path,
"invocation_model_id": _resolve_invocation_model_id(
path_config["model_id"], path_config["requires_cris"], workload["requirements"],
model, region,
),
"requires_cris": path_config["requires_cris"],
"reason": reason,
}
)
runtime, _ = _catalog_model_for_path(
catalog, "runtime_converse", detected, workload["requirements"],
candidate_order=_converse_candidate_order(workload["source"], workload["requirements"]),
region=region,
)
if runtime:
model_key, model, path_config = runtime
invocation_model_id = _resolve_invocation_model_id(
path_config["model_id"], path_config["requires_cris"], workload["requirements"],
model, region,
)
options.append(
{
"model_key": model_key,
"model": path_config["model_id"],
"api_path": "runtime_converse",
"invocation_model_id": invocation_model_id,
"requires_cris": anthropic_model_recommendation._invocation_requires_cris(
path_config, invocation_model_id
),
"reason": (
"SAME-MODEL governance path: this OpenAI target runs on bedrock-runtime via a "
"CRIS id — Guardrails (Converse API only) and invocation logging without a "
"model change. Check the model's region/profile matrix and dated prices."
if model_key == _same_model_runtime_key(workload["source"])
else "Uses Bedrock-native Converse request/response shapes and a "
"cross-family model; requires rewriting the OpenAI integration."
),
}
)
return options
def _base(workload, decision_status):
return {
"workload_id": workload["workload_id"],
"provider_module": "openai",
"decision_status": decision_status,
"source": workload["source"],
}
def recommend_openai_workload(workload, region, catalog):
"""Return a recommendation dict in the shared downstream contract shape."""
source = workload["source"]
requirements = workload["requirements"]
surface = source.get("api_surface")
continuity = _api_continuity(requirements)
runtime_required = _runtime_required(requirements)
continuity_required = continuity == "required"
feature_assessment = _feature_assessment(workload)
# feature_status is authoritative: a feature is "effectively detected" only if
# the merged assessment marks it detected. Explicit absent/unknown are excluded,
# so downstream compatibility/deltas/targets never contradict the assessment.
detected = [f for f, status in feature_assessment.items() if status == "detected"]
unknown_required = _unknown_required_features(feature_assessment, requirements)
def _unresolved(
reason_head, reasons, block, path_for_compat, decision_options=None
):
# Shared decision_required assembly (no target selected).
rec = _base(workload, "decision_required")
rec.update(
{
"source_analysis": _source_analysis(source, None),
"feature_assessment": feature_assessment,
"primary_model": None,
"model_identity": None,
"api_path": None,
"invocation_model_id": None,
"decision_options": (
_decision_options(catalog, workload, region)
if decision_options is None
else decision_options
),
"alternatives": [],
"rationale": [reason_head] + reasons,
"blocks": [block],
"tuning": [],
"compatibility": _compatibility(detected, requirements, path_for_compat),
"architecture_impacts": _feature_findings(detected, requirements, path_for_compat)[3],
"additional_targets": _additional_targets(detected, requirements),
"migration_deltas": [],
"evaluation": _evaluation(detected, requirements),
"rollout": {
"strategy": "decision_required",
"gate": "Resolve the open decision before implementation.",
},
"verification": _verification(region, catalog, None, False, None, selected=False),
}
)
return rec
# --- Fail closed: a required feature with unknown status is not ready (G08) ---
if unknown_required:
return _unresolved(
"Required features are unresolved (status unknown): "
+ ", ".join(unknown_required),
[
"The scan did not confirm these required features as detected or absent, so "
"readiness cannot be asserted."
],
_finding(
"feature_scan_incomplete",
"[BLOCKS]",
"Required features are unresolved (status unknown): "
+ ", ".join(unknown_required),
"Scan the recorded source paths and mark each required feature detected or "
"absent, then rerun Model Recommend.",
),
"",
)
# --- Hard conflict: OpenAI continuity required AND runtime-only governance ---
if continuity_required and runtime_required:
options = _decision_options(catalog, workload, region)
if len(options) < 2:
raise ValueError(
f"catalog cannot supply both decision options for workload "
f"{workload['workload_id']}"
)
return _unresolved(
"Required OpenAI API continuity conflicts with a runtime-only Bedrock "
"governance requirement.",
[
"Mantle preserves the OpenAI SDK and the supported source API surface; runtime "
"Converse provides Bedrock-native governance and keeps an exact same-model "
"runtime candidate when the catalog provides one."
],
_finding(
"model_path_decision_required",
"[BLOCKS]",
"OpenAI API continuity and runtime-only governance cannot both be satisfied "
"by one path.",
"Choose the Mantle continuity option or the runtime governance option, "
"update requirements, and rerun Model Recommend.",
),
"",
)
# --- Select a path ---
if runtime_required:
path = "runtime_converse"
candidate_order = _converse_candidate_order(source, requirements)
same = _same_model_runtime_key(source)
if same:
rationale_head = (
"Bedrock governance or multi-model requirements select runtime Converse; "
f"the source model itself ({catalog['models'][same]['display_name']}) runs "
"there via a CRIS id, so the same-model candidate leads, with the Claude "
"tier as the cross-family fallback."
)
else:
tier = _converse_tier_for_source(source)
rationale_head = (
"Bedrock governance or multi-model requirements select runtime Converse; "
f"the source tier maps to {catalog['models'][tier]['display_name']} "
"(capability-evidence fallback across Claude tiers). Sources without an "
"exact runtime catalog identity require this family switch."
)
else:
path = _mantle_path(source, requirements)
candidate_order = _mantle_candidate_order(catalog, source)
if path == "mantle_openai_chat":
rationale_head = (
"GPT-6 Chat Completions API continuity requires an in-region Mantle Chat "
"Completions model."
)
else:
rationale_head = (
"OpenAI source lands on Mantle Responses, with the exact source model "
"evaluated before capability and numeric fallbacks."
)
# Astra's new paths must not inherit the older GPT-5.x defaults. Keep this
# source on Astra and fail closed when the required path lacks evidence.
if _same_model_runtime_key(source) == "openai_gpt_6_astra":
candidate_order = ["openai_gpt_6_astra"]
preferred = requirements.get("preferred_api_path")
if preferred:
available = catalog["models"]["openai_gpt_6_astra"]["paths"].get(preferred, {})
if (available.get("available") is not True
or runtime_required and preferred != "runtime_converse"
or continuity_required and preferred == "runtime_converse"):
return _unresolved(
"The explicit Astra API preference conflicts with the available paths "
"or required API/governance constraints.", [],
_finding(
"preferred_api_path_conflict", "[BLOCKS]",
f"The requested {preferred} path cannot satisfy this workload.",
"Choose a supported API or revise the conflicting requirements, "
"then rerun Model Recommend.",
), preferred,
)
path = preferred
elif not runtime_required and surface == "chat_completions":
path = "mantle_openai_chat"
rationale_head = (
"Keep GPT-6 Astra on Bedrock using the compatible explicit API preference, "
"otherwise the source-compatible API or required runtime Converse path; "
"apply Astra's own region and feature evidence."
)
# Evidence-driven selection: pick the first candidate on the path whose catalog
# capabilities cover the required/detected text features (a newer model without
# feature-level evidence falls back to an older probed one). G02: fail closed
# only when NO candidate on the path has the evidence.
catalog_hit, unsupported = _catalog_model_for_path(
catalog, path, detected, requirements, candidate_order=candidate_order,
region=region,
)
if not catalog_hit:
if path.startswith("mantle_"):
return _unresolved(
rationale_head,
[
f"The catalog has no {path} candidate available in {region}; "
"select another region or migration path."
],
_finding(
"mantle_region_unavailable",
"[BLOCKS]",
f"No in-region {path} model is available in {region}.",
"Choose a region in the selected model's Mantle footprint, choose "
"runtime Converse, or update the continuity requirement, then rerun "
"Model Recommend.",
),
path,
)
raise ValueError(f"catalog has no available model for path {path}")
model_key, model, path_config = catalog_hit
if path == "mantle_openai_chat":
if model_key == _same_model_catalog_key(source):
rationale_head = (
"GPT-6 Chat Completions source with API continuity lands on the exact "
"same model through Mantle Chat Completions."
)
else:
rationale_head = (
f"The exact source model is unavailable in {region}; "
f"{model['display_name']} is the in-region Mantle Chat Completions fallback."
)
if unsupported:
return _unresolved(
rationale_head,
[
f"No {path} candidate has catalog capability evidence for: "
+ ", ".join(unsupported)
],
_finding(
"unverified_capability",
"[BLOCKS]",
f"No {path} candidate is evidenced to support required features: "
+ ", ".join(unsupported),
"Add dated capability evidence for a candidate, choose a different path/model, "
"or drop the requirement, then rerun Model Recommend.",
),
path,
)
needed = set(detected) | set(requirements.get("critical_features") or [])
if (model_key == "openai_gpt_6_astra" and path == "runtime_converse"
and {"structured_output_json", "streaming"} <= needed):
return _unresolved(
rationale_head, [],
_finding(
"structured_output_streaming_unverified", "[BLOCKS]",
"Astra runtime JSON Schema output is documented only for non-streaming calls.",
"Use non-streaming structured output or separate the streaming workload, "
"then update requirements and rerun Model Recommend.",
), path,
)
numeric_conflicts = _numeric_requirement_conflict(model, requirements)
if numeric_conflicts:
return _unresolved(
rationale_head,
numeric_conflicts,
_finding(
"unverified_capacity",
"[BLOCKS]",
"; ".join(numeric_conflicts),
"Source the missing capability data from a dated reference, or reduce the "
"hard numeric requirement, then rerun Model Recommend.",
),
path,
)
invocation_model_id = _resolve_invocation_model_id(
path_config["model_id"], path_config["requires_cris"], requirements, model, region
)
regions = path_config.get("supported_regions")
profile_regions = path_config.get("inference_profile_regions")
if regions is not None and region not in regions:
return _unresolved(
rationale_head, [],
_finding(
"model_region_unavailable", "[BLOCKS]",
f"{model['display_name']} is not listed for {path} in {region}.",
"Choose a supported region or a supported CRIS path with explicit "
"data-residency consent, or evaluate a different model.",
), path,
)
if profile_regions is not None and (invocation_model_id is not None or _hard_profile_requirement(requirements)):
profile_key = invocation_model_id
if invocation_model_id and invocation_model_id.startswith("arn:"):
match = re.fullmatch(
r"arn:aws:bedrock:([^:]+):[0-9]{12}:inference-profile/([^/]+)",
invocation_model_id,
)
profile_key = match.group(2) if match and match.group(1) == region else None
if region not in profile_regions.get(profile_key, []):
return _unresolved(
rationale_head, [],
_finding(
"inference_profile_unverified", "[BLOCKS]",
f"{invocation_model_id} has no catalog evidence for calls from {region}.",
"Select a cataloged profile available from this region, or verify an "
"application inference profile in the target account before proceeding.",
), path,
)
source_analysis = _source_analysis(source, model)
# --- Findings (target- and path-derived) ---
r_blocks, r_tuning, r_deltas = _reasoning_findings(source, requirements, model, path, detected)
f_blocks, f_tuning, f_deltas, impacts = _feature_findings(detected, requirements, path, model)
blocks = r_blocks + f_blocks
tuning = r_tuning + f_tuning
deltas = list(r_deltas)
if model.get("adaptive_thinking_only"):
finding = _finding(
"adaptive_thinking_required",
"[BLOCKS]" if requirements.get("thinking_enabled") is False else "[TUNE]",
"Opus 5.5 always uses adaptive thinking and does not support disabled thinking, manual budgets, or forced any/tool choice.",
"Set output_config.effort as needed (default medium), and size max_tokens "
"for thinking plus response text. Use auto/none tool choice; a hard forced-tool "
"requirement needs a verified compatible target. Verify billed usage on the workload.",
)
(blocks if finding["tag"] == "[BLOCKS]" else tuning).append(finding)
if path == "runtime_converse" and path_config["requires_cris"] and invocation_model_id is None:
# GPT-5.6 on bedrock-runtime is CRIS-only — there is no in-region invocation
# form. A residency posture that forbids both Global and Geo CRIS makes the
# same-model governance path unusable; say so explicitly instead of shipping
# a recommendation with an unresolvable invocation id.
blocks.append(
_finding(
"cris_residency_unresolved",
"[BLOCKS]",
"This runtime path is CRIS-only, and the stated data-residency posture "
"permits neither Global nor Geo cross-region inference, so no invocation "
"id can be resolved.",
"Relax residency to Global or a Geo CRIS geography, supply an explicit "
"inference_profile_id, or take the Claude cross-family Converse path.",
)
)
if path == "mantle_openai_responses" and surface == "chat_completions":
deltas.extend(_chat_to_responses_deltas())
blocks.append(
_finding(
"chat_completions_to_responses_required",
"[BLOCKS]",
(
"The selected Astra Responses path requires reshaping the Chat source."
if model_key == "openai_gpt_6_astra"
else "GPT-5.x on Mantle rejects Chat Completions; the source must reshape "
"to the Responses API."
),
"Apply the request/response/tool/state reshape deltas before cutover.",
)
)
if path == "mantle_openai_chat" and surface == "responses":
deltas.extend([
_delta(
"responses_to_chat_request", "path",
"Map Responses input/instructions to Chat messages/system content and "
"verify the selected model's output-budget parameters.",
),
_delta(
"responses_to_chat_response", "path",
"Read choices[0].message.content instead of output_text; preserve Chat "
"messages and tool_call_id rather than Responses output items.",
),
])
blocks.append(_finding(
"responses_to_chat_completions_required", "[BLOCKS]",
"The selected Astra Chat path requires reshaping the Responses source.",
"Apply the request/response/tool/state mapping before cutover.",
))
deltas.extend(f_deltas)
if path == "runtime_converse":
same_model = model_key == _same_model_runtime_key(source)
deltas.append(
_delta(
"openai_sdk_to_converse",
"path",
"The OpenAI SDK integration is rewritten to boto3 bedrock-runtime Converse; "
+ (
"the selected OpenAI model identity is preserved, while streaming moves "
"to ConverseStream event shapes."
if same_model
else "the selected model also changes families, and streaming moves to "
"ConverseStream event shapes."
),
)
)
rationale = [rationale_head]
if model["context_window"] == "unknown" or model["output_token_ceiling"] == "unknown":
rationale.append(
f"{model['display_name']} path is evidenced by the reference, but its context/output "
"limits are unknown in the catalog and must be probed."
)
rec = _base(workload, "recommended")
rec.update(
{
"source_analysis": source_analysis,
"feature_assessment": feature_assessment,
"primary_model": path_config["model_id"],
"model_identity": _model_identity(model_key, model, path_config, invocation_model_id),
"api_path": path,
"invocation_model_id": invocation_model_id,
"decision_options": [],
"alternatives": [],
"rationale": rationale,
"blocks": blocks,
"tuning": tuning,
"compatibility": _compatibility(detected, requirements, path, model),
"architecture_impacts": impacts,
"additional_targets": _additional_targets(detected, requirements),
"migration_deltas": deltas,
"evaluation": _evaluation(detected, requirements),
"rollout": {
"strategy": "canary",
"gate": "Compare source and target on the golden set before percentage rollout.",
},
"verification": _verification(
region, catalog, path, path_config["requires_cris"], invocation_model_id,
selected=True, model=model, requirements=requirements,
),
}
)
return rec
Requirements and compatibility
Written for: Claude Code
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Names Claude Code tools, Needs Python
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- AWS
- Publisher
- AWS (Official)
- Source
- aws/agent-toolkit-for-aws · plugins/aws-startup-advisor/skills/agent-advisor · commit 0a218f8
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- NOTICE (added by AutoGineer from NOTICE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by AWS.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download agent-advisor.zip