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://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/test_openai_model_recommendation.py
"""Tests for the OpenAI-to-Bedrock provider module (handoff Section 11 matrix)."""
import json
import pathlib
import jsonschema
import pytest
import model_recommendation
import openai_model_recommendation as oai
SCRIPTS = pathlib.Path(model_recommendation.__file__).parent
OPENAI_CATALOG = model_recommendation.load_openai_catalog()
def _workload(**overrides):
workload = {
"workload_id": "chat-svc",
"source": {
"provider": "openai",
"model_ids": ["gpt-5.4"],
"sdk": "openai",
"api_surface": "responses",
"source_paths": ["src/app.py"],
},
"requirements": {
"priority": "balanced",
"critical_features": [],
},
"detected_features": [],
}
for key, value in overrides.items():
if key in {"source", "requirements"}:
workload[key].update(value)
else:
workload[key] = value
return workload
def _recommend(workload=None, region="us-east-2"):
return oai.recommend_openai_workload(
workload or _workload(), region, OPENAI_CATALOG
)
def _codes(items):
return {item["code"] for item in items}
def _delta_codes(rec):
return {d["code"] for d in rec["migration_deltas"]}
# --- 11.1 Provider and path selection -------------------------------------
def test_responses_source_with_continuity_recommends_mantle_responses():
rec = _recommend(
_workload(requirements={"api_continuity": "required"})
)
assert rec["provider_module"] == "openai"
assert rec["decision_status"] == "recommended"
assert rec["api_path"] == "mantle_openai_responses"
assert rec["primary_model"] == "openai.gpt-5.6-sol"
def test_chat_completions_gpt5_target_selects_responses_not_chat():
rec = _recommend(
_workload(
source={"api_surface": "chat_completions"},
requirements={"api_continuity": "required"},
)
)
assert rec["api_path"] == "mantle_openai_responses"
assert rec["api_path"] != "mantle_openai_chat"
assert "chat_completions_to_responses_required" in _codes(rec["blocks"])
def test_chat_completions_case_includes_full_reshape_deltas():
rec = _recommend(
_workload(
source={"api_surface": "chat_completions"},
requirements={"api_continuity": "required"},
detected_features=["tool_or_function_calling", "conversation_state"],
)
)
codes = _delta_codes(rec)
assert "chat_to_responses_request" in codes
assert "chat_to_responses_response" in codes
assert "chat_to_responses_tools" in codes
assert "chat_to_responses_state" in codes
def test_governance_selects_runtime_converse():
rec = _recommend(
_workload(requirements={"governance": ["guardrails", "invocation_logging"]})
)
assert rec["decision_status"] == "recommended"
assert rec["api_path"] == "runtime_converse"
assert rec["primary_model"] == "anthropic.claude-sonnet-5"
assert rec["model_identity"]["display_name"] == (
"Claude Sonnet 5 (Converse tier match for GPT-5.6 Terra / 5.5 / 5.4)"
)
assert rec["model_identity"]["version"] == "5"
def test_continuity_plus_runtime_only_returns_decision_required():
rec = _recommend(
_workload(
requirements={
"api_continuity": "required",
"governance": ["guardrails"],
}
)
)
assert rec["decision_status"] == "decision_required"
assert rec["primary_model"] is None
assert rec["api_path"] is None
assert {o["api_path"] for o in rec["decision_options"]} == {
"mantle_openai_responses",
"runtime_converse",
}
assert "model_path_decision_required" in _codes(rec["blocks"])
def test_decision_options_explain_tradeoffs():
rec = _recommend(
_workload(
requirements={"api_continuity": "required", "governance": ["guardrails"]}
)
)
reasons = {o["api_path"]: o["reason"] for o in rec["decision_options"]}
assert "OpenAI SDK" in reasons["mantle_openai_responses"]
assert "Bedrock-native" in reasons["runtime_converse"]
# --- GPT-6 exact-model routing --------------------------------------------
_GPT6_MODELS = (
("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"),
)
@pytest.mark.parametrize(("source_id", "target_id"), _GPT6_MODELS)
def test_gpt6_supported_region_selects_exact_model_and_validates_schema(
source_id, target_id
):
critical = ["image_input_vision"] if source_id in {"gpt-6-sol", "gpt-6-luna"} else []
workload = _workload(
source={"model_ids": [source_id]},
requirements={"api_continuity": "required", "critical_features": critical},
)
out = _full_recommend(workload, region="us-east-1")
jsonschema.validate(out, _OUT_SCHEMA)
rec = out["workloads"]["chat-svc"]
assert oai.detect_family(source_id) == "reasoning"
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] == target_id
assert rec["api_path"] == "mantle_openai_responses"
if critical:
assert "image_input_vision" in rec["compatibility"]["native"]
@pytest.mark.parametrize(("source_id", "target_id"), _GPT6_MODELS)
@pytest.mark.parametrize(
("residency", "extra_requirements", "prefix"),
(
("global_allowed", {}, "global."),
("geo_required", {"cris_geography": "us"}, "us."),
),
)
def test_gpt6_governance_selects_exact_runtime_model_and_cris_id(
source_id, target_id, residency, extra_requirements, prefix
):
requirements = {
"governance": ["guardrails"],
"data_residency": residency,
**extra_requirements,
}
rec = _recommend(
_workload(source={"model_ids": [source_id]}, requirements=requirements)
)
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] == target_id
assert rec["api_path"] == "runtime_converse"
assert rec["invocation_model_id"] == prefix + target_id
delta = _delta_by_code(rec, "openai_sdk_to_converse")
assert "identity is preserved" in delta["description"]
assert "Bedrock-native model" not in delta["description"]
@pytest.mark.parametrize(
("source_id", "region", "geography", "unsupported_id"),
(
("gpt-6-sol", "eu-west-1", "eu", "eu.openai.gpt-6-sol"),
("gpt-6.1-sol", "ap-southeast-2", "au", "au.openai.gpt-6.1-sol"),
),
)
def test_gpt6_unsupported_geo_profile_falls_back_without_fabricated_id(
source_id, region, geography, unsupported_id
):
requirements = {
"governance": ["guardrails"],
"data_residency": "geo_required",
"cris_geography": geography,
}
rec = _recommend(
_workload(source={"model_ids": [source_id]}, requirements=requirements),
region=region,
)
assert rec["decision_status"] == "recommended"
assert rec["invocation_model_id"] != unsupported_id
assert not rec["invocation_model_id"].endswith(
f"openai.{source_id}"
)
conflict = _recommend(
_workload(
source={"model_ids": [source_id], "api_surface": "chat_completions"},
requirements={"api_continuity": "required", **requirements},
),
region=region,
)
assert conflict["decision_status"] == "decision_required"
assert unsupported_id not in {
option["invocation_model_id"] for option in conflict["decision_options"]
}
def test_active_catalog_contains_gpt6_profile_metadata_and_current_provenance():
assert OPENAI_CATALOG["verified_at"] == "2026-09-24"
for model_key, _ in _GPT6_MODELS:
catalog_key = oai._SOURCE_MODEL_KEYS[model_key]
assert set(OPENAI_CATALOG["models"][catalog_key]["inference_profiles"]) == {
"us",
"global",
}
@pytest.mark.parametrize(("source_id", "target_id"), _GPT6_MODELS)
def test_gpt6_chat_completions_preserves_chat_surface(source_id, target_id):
rec = _recommend(
_workload(
source={"model_ids": [source_id], "api_surface": "chat_completions"},
requirements={"api_continuity": "required"},
),
region="us-east-1",
)
assert rec["primary_model"] == target_id
assert rec["api_path"] == "mantle_openai_chat"
assert "chat_completions_to_responses_required" not in _codes(rec["blocks"])
def test_gpt6_mantle_skips_exact_model_outside_cataloged_region():
rec = _recommend(
_workload(
source={"model_ids": ["gpt-6-sol"]},
requirements={"api_continuity": "required"},
),
region="us-west-2",
)
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] != "openai.gpt-6-sol"
assert "fallback" in rec["rationale"][0]
assert "exact same model" not in rec["rationale"][0]
def test_gpt6_chat_outside_every_mantle_footprint_returns_blocking_decision():
workload = _workload(
source={"model_ids": ["gpt-6-sol"], "api_surface": "chat_completions"},
requirements={"api_continuity": "required"},
)
out = _full_recommend(workload, region="eu-west-1")
jsonschema.validate(out, _OUT_SCHEMA)
rec = out["workloads"]["chat-svc"]
assert rec["decision_status"] == "decision_required"
assert rec["primary_model"] is None
assert "mantle_region_unavailable" in _codes(rec["blocks"])
assert "eu-west-1" in rec["blocks"][0]["message"]
assert {option["api_path"] for option in rec["decision_options"]} == {
"mantle_openai_chat",
"runtime_converse",
}
def test_gpt6_chat_conflict_option_names_chat_completions_surface():
rec = _recommend(
_workload(
source={"model_ids": ["gpt-6-sol"], "api_surface": "chat_completions"},
requirements={
"api_continuity": "required",
"governance": ["guardrails"],
},
),
region="us-east-1",
)
option = next(
option
for option in rec["decision_options"]
if option["api_path"] == "mantle_openai_chat"
)
assert "Chat Completions surface" in option["reason"]
assert "Responses surface" not in option["reason"]
def test_gpt6_chat_findings_preserve_chat_request_shapes():
rec = _recommend(
_workload(
source={"model_ids": ["gpt-6-sol"], "api_surface": "chat_completions"},
requirements={
"api_continuity": "required",
"uses_n": True,
"uses_hosted_state": True,
},
detected_features=["tool_or_function_calling", "conversation_state"],
),
region="us-east-1",
)
assert rec["api_path"] == "mantle_openai_chat"
assert "responses_no_n" not in _delta_codes(rec)
tool_delta = _delta_by_code(rec, "tool_result_shape")
assert "role: tool" in tool_delta["description"]
assert "tool_call_id" in tool_delta["description"]
assert "function_call_output" not in tool_delta["description"]
state_delta = _delta_by_code(rec, "conversation_state_ownership")
assert "application-owned message history" in state_delta["description"]
assert "previous_response_id" not in state_delta["description"]
def test_gpt6_chat_conflict_outside_mantle_region_is_schema_valid():
workload = _workload(
source={"model_ids": ["gpt-6-sol"], "api_surface": "chat_completions"},
requirements={
"api_continuity": "required",
"governance": ["guardrails"],
},
)
out = _full_recommend(workload, region="eu-west-1")
jsonschema.validate(out, _OUT_SCHEMA)
rec = out["workloads"]["chat-svc"]
assert rec["decision_status"] == "decision_required"
assert {option["api_path"] for option in rec["decision_options"]} == {
"mantle_openai_chat",
"runtime_converse",
}
mantle = next(
option for option in rec["decision_options"]
if option["api_path"] == "mantle_openai_chat"
)
assert "Requires changing from eu-west-1" in mantle["reason"]
def test_gpt6_chat_unknown_required_feature_outside_region_has_two_options():
workload = _workload(
source={"model_ids": ["gpt-6-sol"], "api_surface": "chat_completions"},
requirements={
"api_continuity": "required",
"critical_features": ["streaming"],
},
feature_status={"streaming": "unknown"},
)
out = _full_recommend(workload, region="eu-west-1")
jsonschema.validate(out, _OUT_SCHEMA)
rec = out["workloads"]["chat-svc"]
assert rec["decision_status"] == "decision_required"
assert len(rec["decision_options"]) >= 2
@pytest.mark.parametrize(
"requirements",
(
{
"api_continuity": "preferred",
"governance": ["guardrails"],
"critical_features": ["streaming"],
},
{
"api_continuity": "preferred",
"governance": ["guardrails"],
"expected_output_tokens": 10000000,
},
),
)
def test_runtime_unresolved_branches_outside_mantle_region_are_schema_valid(
requirements,
):
workload = _workload(
source={"model_ids": ["gpt-6-sol"], "api_surface": "chat_completions"},
requirements=requirements,
)
out = _full_recommend(workload, region="eu-west-1")
jsonschema.validate(out, _OUT_SCHEMA)
rec = out["workloads"]["chat-svc"]
assert rec["decision_status"] == "decision_required"
assert {option["api_path"] for option in rec["decision_options"]} == {
"mantle_openai_chat",
"runtime_converse",
}
mantle = next(
option for option in rec["decision_options"]
if option["api_path"] == "mantle_openai_chat"
)
assert "Requires changing from eu-west-1" in mantle["reason"]
# --- 11.2 Model-generation analysis ---------------------------------------
def test_gpt4_to_gpt5_emits_model_generation_finding():
rec = _recommend(
_workload(
source={"model_ids": ["gpt-4o"], "api_surface": "chat_completions"},
requirements={"api_continuity": "required"},
)
)
assert rec["source_analysis"]["source_family"] == "legacy"
assert rec["source_analysis"]["model_generation_changes"] is True
assert "model_generation_hop" in _delta_codes(rec)
def test_oseries_source_is_reasoning_not_legacy():
rec = _recommend(_workload(source={"model_ids": ["o3-mini"]}))
assert rec["source_analysis"]["source_family"] == "reasoning"
assert rec["source_analysis"]["model_generation_changes"] is False
def test_opaque_deployment_name_stays_unknown():
assert oai.detect_family("prod-default") == "unknown"
rec = _recommend(_workload(source={"model_ids": ["prod-default"]}))
assert rec["source_analysis"]["source_family"] == "unknown"
def test_reasoning_workload_requires_output_headroom():
rec = _recommend() # gpt-5.4 reasoning
assert "reasoning_token_headroom" in _codes(rec["tuning"])
def test_reasoning_headroom_is_a_starting_heuristic_not_a_guarantee():
rec = _recommend()
msg = next(
f["remediation"] for f in rec["tuning"] if f["code"] == "reasoning_token_headroom"
)
assert "2.5x" in msg
assert "STARTING heuristic" in msg or "not a guaranteed" in msg
# --- 11.3 Parameter and API behavior --------------------------------------
def test_sampling_is_target_derived_not_source_derived():
# G01: the Mantle target is openai.gpt-5.4 (accepts sampling), so BOTH an
# early-GPT-5 source and a GPT-5.4 source get the same target-derived finding.
early = _recommend(_workload(source={"model_ids": ["gpt-5.1"]}))
later = _recommend(_workload(source={"model_ids": ["gpt-5.4"]}))
assert "sampling_params_accepted" in _codes(early["tuning"])
assert "sampling_params_accepted" in _codes(later["tuning"])
# G10: penalties/logprobs/stop are called out as rejected separately.
assert "sampling_penalties_rejected" in _codes(early["tuning"])
def test_gpt54_does_not_get_anthropic_sampling_removal():
rec = _recommend(_workload(source={"model_ids": ["gpt-5.4"]}))
assert "sampling_parameters_removed" not in _codes(rec["blocks"])
assert "sampling_params_accepted" in _codes(rec["tuning"])
def test_n_usage_emits_repeated_call_requirement():
rec = _recommend(_workload(requirements={"uses_n": True}))
assert "responses_no_n" in _delta_codes(rec)
def test_tool_results_emit_function_call_output_delta():
rec = _recommend(_workload(detected_features=["tool_or_function_calling"]))
assert "tool_result_shape" in _delta_codes(rec)
def test_structured_output_emits_text_format_mapping():
rec = _recommend(_workload(detected_features=["structured_output_json"]))
assert "structured_output_text_format" in _delta_codes(rec)
def test_multiturn_state_emits_previous_response_or_app_state():
rec = _recommend(_workload(requirements={"uses_hosted_state": True}))
assert "conversation_state_ownership" in _delta_codes(rec)
# --- 11.4 Architecture impacts --------------------------------------------
def _impact_features(rec):
return {a["feature"] for a in rec["architecture_impacts"]}
def test_hosted_web_search_not_reported_as_live_native_search():
rec = _recommend(_workload(detected_features=["web_search"]))
impacts = {a["feature"]: a["impact"] for a in rec["architecture_impacts"]}
assert "web_search" in impacts
assert "passthrough" in impacts["web_search"].lower()
def test_file_search_and_vector_stores_produce_retrieval_impacts():
rec = _recommend(
_workload(detected_features=["file_search_retrieval", "files_api", "vector_stores"])
)
assert {"file_search_retrieval", "files_api", "vector_stores"} <= _impact_features(rec)
def test_assistants_threads_produce_state_impacts():
rec = _recommend(_workload(detected_features=["assistants_threads"]))
assert "assistants_threads" in _impact_features(rec)
def test_modalities_are_separate_capability_paths():
rec = _recommend(
_workload(detected_features=["audio_modality", "embeddings", "images"])
)
assert {"audio_modality", "embeddings", "images"} <= _impact_features(rec)
def test_agentic_workload_uses_trajectory_evaluation():
rec = _recommend(_workload(detected_features=["tool_or_function_calling"]))
assert rec["evaluation"]["mode"] == "trajectory"
# --- 11.5 Catalog and verification ----------------------------------------
def test_context_requirement_beyond_every_catalog_window_requires_decision():
# 2M exceeds every cataloged window (GPT-5.6 is 1M as of 2026-08-21 — a 400K
# requirement now legitimately passes, which an earlier version of this test
# used as its probe value). This is the KNOWN-limit-too-small case; the
# unknown-limit invariant has its own probe below.
rec = _recommend(_workload(requirements={"min_context_tokens": 2000000}))
assert rec["decision_status"] == "decision_required"
def test_unknown_limits_do_not_pass_hard_numeric_requirement():
# Fail-closed invariant: an unknown catalog limit cannot satisfy a hard numeric
# need. output_token_ceiling is "unknown" on every entry as of 2026-08-21, so it
# probes the invariant the way min_context_tokens no longer can.
rec = _recommend(_workload(requirements={"expected_output_tokens": 100000}))
assert rec["decision_status"] == "decision_required"
assert "unverified_capacity" in _codes(rec["blocks"])
def test_cris_only_path_with_unresolvable_residency_blocks_explicitly():
# GPT-5.6 on bedrock-runtime is CRIS-only. A residency posture permitting
# neither Global nor Geo CRIS must surface an explicit block, not a silent
# invocation_model_id: None.
rec = _recommend(_workload(
source={"model_ids": ["gpt-5.6-terra"], "api_surface": "responses"},
requirements={"governance": ["guardrails"], "data_residency": "in_region_required"},
))
assert "cris_residency_unresolved" in _codes(rec["blocks"])
def test_no_aws_account_leaves_probe_not_run_and_provisional():
rec = _recommend()
assert rec["verification"]["probe_status"] == "not_run"
assert rec["verification"]["availability_claim"] == "provisional"
def test_decision_required_verification_is_not_applicable():
rec = _recommend(
_workload(requirements={"api_continuity": "required", "governance": ["guardrails"]})
)
assert rec["verification"]["probe_status"] == "not_applicable"
assert rec["verification"]["availability_claim"] == "not_selected"
def test_catalog_limits_are_sourced_or_unknown():
# Limits must be positive sourced integers or the explicit string "unknown" —
# never a fabricated placeholder. Every model must cite its capability source.
for model in OPENAI_CATALOG["models"].values():
for limit in (model["context_window"], model["output_token_ceiling"]):
assert limit == "unknown" or (isinstance(limit, int) and limit > 0)
assert model.get("capability_source")
# --- Mixed provider provenance (via orchestrator) -------------------------
def test_mixed_provider_run_keeps_catalog_provenance():
data = {
"schema_version": 2,
"region": "us-east-2",
"primary_unit": "claude-agent",
"workloads": [
{
"workload_id": "claude-agent",
"source": {
"provider": "anthropic",
"model_ids": ["claude-3-7-sonnet-latest"],
"sdk": "anthropic",
"api_surface": "messages",
"source_paths": ["a.py"],
},
"requirements": {"priority": "balanced", "critical_features": ["tool_use"]},
"detected_features": [],
},
{
"workload_id": "openai-svc",
"source": {
"provider": "openai",
"model_ids": ["gpt-5.4"],
"sdk": "openai",
"api_surface": "responses",
"source_paths": ["b.py"],
},
"requirements": {"priority": "balanced", "critical_features": [], "api_continuity": "required"},
"detected_features": [],
},
],
}
out = model_recommendation.recommend(data)
assert out["workloads"]["claude-agent"]["provider_module"] == "anthropic"
assert out["workloads"]["openai-svc"]["provider_module"] == "openai"
assert out["catalog_provenance"]["claude-agent"]["provider"] == "anthropic"
assert out["catalog_provenance"]["openai-svc"]["provider"] == "openai"
# Output validates against the schema.
schema = json.loads((SCRIPTS / "schemas" / "model-recommendation.json").read_text())
jsonschema.validate(out, schema)
def test_openai_output_validates_against_schema():
data = {
"schema_version": 2,
"region": "us-east-2",
"primary_unit": "chat-svc",
"workloads": [_workload(requirements={"api_continuity": "required"})],
}
schemas = SCRIPTS / "schemas"
jsonschema.validate(
data, json.loads((schemas / "model-recommendation-input.json").read_text())
)
out = model_recommendation.recommend(data)
jsonschema.validate(
out, json.loads((schemas / "model-recommendation.json").read_text())
)
# --- Regression tests for the Codex gap review (G01-G10) -------------------
def _delta_by_code(rec, code):
return next((d for d in rec["migration_deltas"] if d["code"] == code), None)
def test_g01_runtime_target_is_not_reported_as_gpt5():
# A Bedrock-native (Nova) target must not carry target_version gpt-5.x.
rec = _recommend(_workload(requirements={"governance": ["guardrails"]}))
assert rec["api_path"] == "runtime_converse"
assert rec["source_analysis"]["target_version"] == "5" # Claude Sonnet 5, not gpt-5.x
assert rec["source_analysis"]["target_version"] != "gpt-5.x"
def test_g01_same_version_source_and_target_not_marked_changed():
# gpt-5.6 source -> openai.gpt-5.6-sol target: version did not change.
rec = _recommend(_workload(source={"model_ids": ["gpt-5.6"]}))
assert rec["source_analysis"]["target_version"] == "5.6"
assert rec["source_analysis"]["version_changed"] is False
def test_g01_legacy_source_to_reasoning_target_marks_generation_change():
rec = _recommend(
_workload(source={"model_ids": ["gpt-4o"], "api_surface": "chat_completions"},
requirements={"api_continuity": "required"})
)
assert rec["source_analysis"]["model_generation_changes"] is True
assert rec["source_analysis"]["version_changed"] is True
def test_g02_runtime_selection_fails_closed_on_unproven_capability():
# No runtime_converse candidate has streaming evidence in the catalog, so a
# workload requiring it must NOT get a silent native claim — it fails closed.
rec = _recommend(
_workload(
requirements={"governance": ["guardrails"], "critical_features": ["streaming"]}
)
)
assert rec["decision_status"] == "decision_required"
assert "unverified_capability" in _codes(rec["blocks"])
def test_g02_native_only_lists_catalog_evidenced_features():
# Detected structured output is only feature-probed on gpt-5.4, so the engine
# falls back past gpt-5.6-sol (no structured-output evidence) to gpt-5.4 and
# native lists exactly what that catalog entry supports.
rec = _recommend(
_workload(detected_features=["tool_or_function_calling", "structured_output_json"])
)
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] == "openai.gpt-5.4" # evidence-driven fallback
assert set(rec["compatibility"]["native"]) == {
"tool_or_function_calling",
"structured_output_json",
}
def test_g02_vision_requirement_selects_the_evidenced_candidate():
# image_input_vision is evidenced on gpt-5.6-sol (model card) but not on
# gpt-5.5/terra/luna/5.4 — the engine must select the evidenced candidate,
# never claim native on an unevidenced one.
rec = _recommend(
_workload(
requirements={"critical_features": ["image_input_vision"]},
)
)
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] == "openai.gpt-5.6-sol"
assert "image_input_vision" in rec["compatibility"]["native"]
def test_g03_mantle_keeps_typed_responses_parse():
rec = _recommend(_workload(detected_features=["structured_output_json"]))
delta = _delta_by_code(rec, "structured_output_text_format")
assert delta is not None
assert "responses.parse" in delta["description"]
assert "no direct" not in delta["description"].lower()
def test_g04_mantle_state_offers_hosted_and_manual_modes():
rec = _recommend(_workload(requirements={"uses_hosted_state": True}))
delta = _delta_by_code(rec, "conversation_state_ownership")
assert delta is not None
assert "store=True" in delta["description"]
assert "store=False" in delta["description"]
def test_g08_feature_status_unknown_blocks_readiness():
rec = _recommend(
_workload(
requirements={"critical_features": ["structured_output_json"]},
feature_status={"structured_output_json": "unknown"},
)
)
assert "feature_scan_incomplete" in _codes(rec["blocks"])
def test_g08_feature_assessment_is_populated():
rec = _recommend(
_workload(
detected_features=["tool_or_function_calling"],
feature_status={"web_search": "absent"},
)
)
assert rec["feature_assessment"]["tool_or_function_calling"] == "detected"
assert rec["feature_assessment"]["web_search"] == "absent"
def test_g06_separate_modalities_emit_additional_targets():
rec = _recommend(
_workload(detected_features=["embeddings", "audio_modality", "images"])
)
targets = {t["capability"]: t for t in rec["additional_targets"]}
assert set(targets) == {"embeddings", "audio_modality", "images"}
# Unknown target model stays unresolved with a named service, never a fake ID.
for cap, t in targets.items():
assert t["status"] == "unresolved"
assert t["candidate"] is None
assert t["service"]
def test_g06_no_modalities_means_empty_additional_targets():
rec = _recommend(_workload(detected_features=["tool_or_function_calling"]))
assert rec["additional_targets"] == []
# --- Regression tests for the consistency re-review (N01-N04, G08 enforcement) ---
import json as _json # noqa: E402
import pathlib as _pathlib # noqa: E402
_OUT_SCHEMA = _json.loads(
(SCRIPTS / "schemas" / "model-recommendation.json").read_text()
)
def _full_recommend(workload, region="us-east-2"):
data = {
"schema_version": 2,
"region": region,
"primary_unit": workload["workload_id"],
"workloads": [workload],
}
return model_recommendation.recommend(data)
def test_n01_decision_required_openai_output_is_schema_valid():
# A continuity/governance conflict is decision_required; the full artifact must
# validate (model_generation_changes null before selection is allowed).
out = _full_recommend(
_workload(requirements={"api_continuity": "required", "governance": ["guardrails"]})
)
jsonschema.validate(out, _OUT_SCHEMA)
rec = out["workloads"]["chat-svc"]
assert rec["decision_status"] == "decision_required"
assert rec["source_analysis"]["model_generation_changes"] is None
def test_n02_absent_feature_is_not_reported_native_or_delta():
rec = _recommend(
_workload(feature_status={"structured_output_json": "absent"})
)
assert "structured_output_json" not in rec["compatibility"]["native"]
assert "structured_output_text_format" not in _delta_codes(rec)
def test_n02_status_only_detected_feature_flows_to_behavior():
# Detected via feature_status (not the array) must still drive native/delta.
rec = _recommend(
_workload(detected_features=[], feature_status={"tool_or_function_calling": "detected"})
)
assert "tool_or_function_calling" in rec["compatibility"]["native"]
assert "tool_result_shape" in _delta_codes(rec)
def test_g08_unknown_required_feature_forces_decision_required():
rec = _recommend(
_workload(
requirements={"critical_features": ["structured_output_json"]},
feature_status={"structured_output_json": "unknown"},
)
)
assert rec["decision_status"] == "decision_required"
assert "feature_scan_incomplete" in _codes(rec["blocks"])
assert rec["primary_model"] is None
def test_n03_tool_requirement_never_lands_on_unevidenced_nova():
# Nova has no asserted capabilities; a governance workload requiring tools must
# land on the evidenced Converse candidate (Claude), never claim Nova native.
rec = _recommend(
_workload(
requirements={
"governance": ["guardrails"],
"critical_features": ["tool_or_function_calling"],
}
)
)
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] == "anthropic.claude-sonnet-5"
assert "tool_or_function_calling" in rec["compatibility"]["native"]
def test_n04_logprobs_and_stop_not_declared_rejected():
rec = _recommend(_workload(source={"model_ids": ["gpt-5.4"]}))
finding = next(
f for f in rec["tuning"] if f["code"] == "sampling_penalties_rejected"
)
# Only the two probed penalties are declared rejected in the message.
assert "frequency_penalty" in finding["message"]
assert "presence_penalty" in finding["message"]
assert "logprobs" not in finding["message"]
assert "stop" not in finding["message"]
# logprobs/stop are routed to verification, not rejection.
assert "verify" in finding["remediation"].lower()
# --- Converse tier mapping (source OpenAI tier -> matching Claude tier) ----
def test_tier_map_sol_source_maps_to_opus_on_converse():
# Verified 2026-08-21: GPT-5.6 runs on bedrock-runtime via CRIS, so a 5.6
# source with governance requirements keeps ITS OWN model — the same-model
# candidate outranks the Claude tier. (Before 2026-08-21 the docs said
# mantle-only, and this test asserted the Opus fallback.)
rec = _recommend(
_workload(source={"model_ids": ["gpt-5.6-sol"]},
requirements={"governance": ["guardrails"]})
)
assert rec["primary_model"] == "openai.gpt-5.6-sol"
assert rec["api_path"] == "runtime_converse"
def test_tier_map_sol_cross_family_fallback_is_opus():
# The Claude tier map is preserved as the cross-family option behind the
# same-model candidate.
from openai_model_recommendation import _converse_candidate_order
order = _converse_candidate_order({"model_ids": ["gpt-5.6-sol"]})
assert order[0] == "openai_gpt_5_6_sol"
assert order[1] == "anthropic_claude_opus_5_5"
def test_tier_map_luna_source_maps_to_haiku_on_converse():
# Same-model-first (2026-08-21): Luna keeps Luna; Haiku is the cross-family
# fallback in the candidate order.
rec = _recommend(
_workload(source={"model_ids": ["gpt-5.6-luna"]},
requirements={"governance": ["guardrails"]})
)
assert rec["primary_model"] == "openai.gpt-5.6-luna"
from openai_model_recommendation import _converse_candidate_order
order = _converse_candidate_order({"model_ids": ["gpt-5.6-luna"]})
assert order[0] == "openai_gpt_5_6_luna"
assert order[1] == "anthropic_claude_haiku_4_5"
def test_tier_map_terra_and_55_sources_map_to_sonnet_on_converse():
# Terra (5.6) keeps itself — same-model runtime path exists. GPT-5.5 has NO
# runtime path (mantle-only, re-verified 2026-08-21), so its governance
# Converse target remains the Claude balanced tier.
rec_terra = _recommend(
_workload(source={"model_ids": ["gpt-5.6-terra"]},
requirements={"governance": ["guardrails"]})
)
assert rec_terra["primary_model"] == "openai.gpt-5.6-terra"
rec_55 = _recommend(
_workload(source={"model_ids": ["gpt-5.5"]},
requirements={"governance": ["guardrails"]})
)
assert rec_55["primary_model"] == "anthropic.claude-sonnet-5"
def test_tier_map_falls_back_across_tiers_on_capability_evidence():
# The capability-evidence fallback mechanism: when the first candidate in the
# order lacks evidence for a critical feature, the engine advances to the next
# tier that covers it. Exercised with an explicit order because the Luna
# source no longer produces a Haiku-first order — its same-model candidate
# carries reasoning evidence and satisfies the requirement directly
# (2026-08-21 same-model-first change).
from model_recommendation import load_openai_catalog
from openai_model_recommendation import _catalog_model_for_path
catalog = load_openai_catalog()
hit, unmet = _catalog_model_for_path(
catalog, "runtime_converse", detected_features=[],
requirements={"critical_features": ["reasoning"]},
candidate_order=["anthropic_claude_haiku_4_5", "anthropic_claude_sonnet_5"],
)
assert hit is not None
assert hit[0] == "anthropic_claude_sonnet_5"
def test_same_model_governance_still_recommended_end_to_end():
# A 5.6 source with governance + reasoning requirement lands on itself.
rec = _recommend(
_workload(source={"model_ids": ["gpt-5.6-luna"]},
requirements={"governance": ["guardrails"],
"critical_features": ["reasoning"]})
)
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] == "openai.gpt-5.6-luna"
assert rec["api_path"] == "runtime_converse"
@pytest.mark.parametrize("surface,path", [
("responses", "mantle_openai_responses"),
("chat_completions", "mantle_openai_chat"),
])
@pytest.mark.parametrize("region", ["us-east-1", "us-west-2"])
def test_astra_keeps_source_model_and_api_on_mantle(surface, path, region):
rec = oai.recommend_openai_workload(
_workload(source={"model_ids": ["gpt-6-astra"], "api_surface": surface}),
region, OPENAI_CATALOG,
)
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] == "openai.gpt-6-astra"
assert rec["api_path"] == path
assert rec["source_analysis"]["source_family"] == "reasoning"
assert rec["source_analysis"]["version_changed"] is False
assert "chat_completions_to_responses_required" not in _codes(rec["blocks"])
assert "sampling_params_accepted" not in _codes(rec["tuning"])
assert "sampling_params_unverified" in _codes(rec["tuning"])
@pytest.mark.parametrize("region", ["us-east-2", "eu-west-1"])
@pytest.mark.parametrize("surface", ["responses", "chat_completions"])
def test_astra_mantle_outside_supported_regions_requires_decision(region, surface):
rec = oai.recommend_openai_workload(
_workload(source={"model_ids": ["gpt-6-astra"], "api_surface": surface}),
region, OPENAI_CATALOG,
)
assert rec["decision_status"] == "decision_required"
assert rec["primary_model"] is None
assert "mantle_region_unavailable" in _codes(rec["blocks"])
@pytest.mark.parametrize("surface,preferred,expected,delta", [
("chat_completions", "mantle_openai_responses", "mantle_openai_responses",
"chat_to_responses_request"),
("chat_completions", "mantle_openai_chat", "mantle_openai_chat", None),
("responses", "mantle_openai_chat", "mantle_openai_chat", "responses_to_chat_request"),
("responses", "mantle_openai_responses", "mantle_openai_responses", None),
("chat_completions", "runtime_converse", "runtime_converse", "openai_sdk_to_converse"),
])
def test_astra_explicit_api_preference_reaches_validated_output(surface, preferred, expected, delta):
data = {
"schema_version": 2, "region": "us-west-2", "primary_unit": "chat-svc",
"workloads": [_workload(
source={"model_ids": ["gpt-6-astra"], "api_surface": surface},
requirements={"preferred_api_path": preferred, "data_residency": "global_allowed"},
)],
}
jsonschema.validate(data, json.loads((SCRIPTS / "schemas/model-recommendation-input.json").read_text()))
result = model_recommendation.recommend(data)
jsonschema.validate(result, json.loads((SCRIPTS / "schemas/model-recommendation.json").read_text()))
rec = result["workloads"]["chat-svc"]
assert rec["decision_status"] == "recommended"
assert rec["api_path"] == expected
assert rec["primary_model"] == "openai.gpt-6-astra"
if delta:
assert delta in _delta_codes(rec)
assert not any("GPT-5.x" in block["message"] for block in rec["blocks"])
@pytest.mark.parametrize("preferred,constraints", [
("mantle_openai_chat", {"governance": ["guardrails"]}),
("mantle_openai_responses", {"governance": ["guardrails"]}),
("runtime_converse", {"api_continuity": "required"}),
("runtime_invoke", {}),
("mantle_messages", {}),
])
def test_astra_api_preference_conflicts_require_an_explicit_decision(preferred, constraints):
rec = oai.recommend_openai_workload(
_workload(
source={"model_ids": ["gpt-6-astra"]},
requirements={"preferred_api_path": preferred, **constraints},
), "us-west-2", OPENAI_CATALOG,
)
assert rec["decision_status"] == "decision_required"
assert rec["api_path"] is None
assert "preferred_api_path_conflict" in _codes(rec["blocks"])
@pytest.mark.parametrize("region", ["us-east-1", "us-west-2"])
@pytest.mark.parametrize("surface,preferred,expected", [
("chat_completions", None, None),
("responses", "mantle_openai_chat", None),
("responses", None, "mantle_openai_responses"),
("chat_completions", "mantle_openai_responses", "mantle_openai_responses"),
])
def test_astra_required_caching_is_checked_on_the_selected_api(region, surface, preferred, expected):
requirements = {"critical_features": ["prompt_caching"]}
if preferred:
requirements["preferred_api_path"] = preferred
rec = oai.recommend_openai_workload(
_workload(source={"model_ids": ["gpt-6-astra"], "api_surface": surface},
requirements=requirements),
region, OPENAI_CATALOG,
)
assert rec["api_path"] == expected
if expected is None:
assert rec["decision_status"] == "decision_required"
assert "unverified_capability" in _codes(rec["blocks"])
assert "prompt_caching" in rec["blocks"][0]["message"]
assert rec["decision_options"][0]["api_path"] == "mantle_openai_responses"
else:
assert rec["decision_status"] == "recommended"
assert "prompt_caching" in rec["compatibility"]["native"]
def test_astra_decision_options_ignore_an_explicitly_absent_cache_signal():
workload = _workload(
source={"model_ids": ["gpt-6-astra"], "api_surface": "chat_completions"},
requirements={"preferred_api_path": "mantle_openai_chat"},
detected_features=["prompt_caching"],
feature_status={"prompt_caching": "absent"},
)
options = oai._decision_options(OPENAI_CATALOG, workload, "us-west-2")
assert options[0]["api_path"] == "mantle_openai_chat"
@pytest.mark.parametrize("signals", [
{"requirements": {"uses_n": True}},
{"detected_features": ["multiple_candidates_n"]},
])
def test_astra_chat_n_requires_verification_without_responses_reshape(signals):
rec = oai.recommend_openai_workload(
_workload(source={"model_ids": ["gpt-6-astra"], "api_surface": "chat_completions"},
**signals),
"us-west-2", OPENAI_CATALOG,
)
assert rec["api_path"] == "mantle_openai_chat"
assert "multiple_candidates_n_unverified" in _codes(rec["tuning"])
assert "responses_no_n" not in _delta_codes(rec)
assert "chat_completions_to_responses_required" not in _codes(rec["blocks"])
def test_astra_runtime_preserves_model_with_sourced_streaming_and_limits():
rec = _recommend(_workload(
source={"model_ids": ["gpt-6-astra"]},
requirements={
"governance": ["guardrails"], "critical_features": ["streaming"],
"data_residency": "global_allowed", "min_context_tokens": 1050000,
"expected_output_tokens": 128000,
},
))
assert rec["decision_status"] == "recommended"
assert rec["primary_model"] == "openai.gpt-6-astra"
assert rec["api_path"] == "runtime_converse"
assert rec["invocation_model_id"] == "global.openai.gpt-6-astra"
assert "streaming" in rec["compatibility"]["native"]
@pytest.mark.parametrize("region,geography", [
("eu-west-1", "eu"),
("eu-west-1", "us"),
("ap-south-1", "in"),
])
def test_astra_rejects_unsupported_geo_profile_or_caller_region(region, geography):
rec = oai.recommend_openai_workload(
_workload(
source={"model_ids": ["gpt-6-astra"]},
requirements={
"governance": ["guardrails"], "data_residency": "geo_required",
"cris_geography": geography,
},
), region, OPENAI_CATALOG,
)
assert rec["decision_status"] == "decision_required"
assert "inference_profile_unverified" in _codes(rec["blocks"])
def test_astra_runtime_does_not_inherit_mantle_tools():
rec = _recommend(_workload(
source={"model_ids": ["gpt-6-astra"]},
requirements={"governance": ["guardrails"],
"critical_features": ["tool_or_function_calling"]},
))
assert rec["decision_status"] == "decision_required"
assert rec["primary_model"] is None
assert "unverified_capability" in _codes(rec["blocks"])
def test_astra_runtime_json_schema_retains_model_and_documented_configuration():
data = {
"schema_version": 2, "region": "us-west-2", "primary_unit": "chat-svc",
"workloads": [_workload(
source={"model_ids": ["gpt-6-astra"]},
requirements={"governance": ["guardrails"], "data_residency": "global_allowed",
"critical_features": ["structured_output_json"]},
)],
}
jsonschema.validate(data, json.loads((SCRIPTS / "schemas/model-recommendation-input.json").read_text()))
result = model_recommendation.recommend(data)
jsonschema.validate(result, json.loads((SCRIPTS / "schemas/model-recommendation.json").read_text()))
rec = result["workloads"]["chat-svc"]
assert rec["decision_status"] == "recommended"
assert rec["api_path"] == "runtime_converse"
assert rec["invocation_model_id"] == "global.openai.gpt-6-astra"
assert rec["verification"]["availability_claim"] == "provisional"
assert "structured_output_json" in rec["compatibility"]["native"]
guidance = next(d["description"] for d in rec["migration_deltas"]
if d["code"] == "structured_output_text_format")
assert "outputConfig.textFormat.structure.jsonSchema" in guidance
assert "additionalModelRequestFields.text.format.strict=true" in guidance
assert "JSON string" in guidance and "non-streaming" in guidance
@pytest.mark.parametrize("status,expected", [
("detected", "decision_required"), ("absent", "recommended"),
])
def test_astra_json_schema_does_not_claim_streaming_support(status, expected):
rec = _recommend(_workload(
source={"model_ids": ["gpt-6-astra"]},
requirements={"governance": ["guardrails"], "data_residency": "global_allowed",
"critical_features": ["structured_output_json"]},
detected_features=["streaming"], feature_status={"streaming": status},
))
assert rec["decision_status"] == expected
if expected == "decision_required":
assert "structured_output_streaming_unverified" in _codes(rec["blocks"])
@pytest.mark.parametrize("profile", [
"arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.openai.gpt-6-astra",
"arn:aws-cn:bedrock:us-west-2:111122223333:inference-profile/us.openai.gpt-6-astra",
"arn:aws:bedrock:us-west-2:bad-account:inference-profile/us.openai.gpt-6-astra",
"arn:aws:bedrock:us-west-2:111122223333:application-inference-profile/example",
"arn:aws:bedrock:us-west-2:111122223333:inference-profile/eu.openai.gpt-6-astra",
"arn:aws:bedrock:us-west-2:111122223333:inference-profile/us.openai.gpt-5.6-sol",
])
def test_astra_rejects_unverified_profile_arn_or_region(profile):
rec = oai.recommend_openai_workload(
_workload(source={"model_ids": ["gpt-6-astra"]},
requirements={"governance": ["guardrails"], "inference_profile_id": profile}),
"us-west-2", OPENAI_CATALOG,
)
assert rec["decision_status"] == "decision_required"
assert "inference_profile_unverified" in _codes(rec["blocks"])
@pytest.mark.parametrize("requirement,limit", [
("min_context_tokens", 1050001),
("expected_output_tokens", 128001),
])
def test_astra_rejects_capacity_above_documented_limit(requirement, limit):
rec = oai.recommend_openai_workload(
_workload(source={"model_ids": ["gpt-6-astra"]},
requirements={requirement: limit}),
"us-west-2", OPENAI_CATALOG,
)
assert rec["decision_status"] == "decision_required"
assert "unverified_capacity" in _codes(rec["blocks"])
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 or AWS.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download agent-advisor.zip