Story-1318 · pramaan-docs ADR-116 · Bug-1310

Atlas runs OpenRouter + DeepSeek only — and the model lives in model.json

Branch issue-1318-atlas-openrouter-deepseek-modeljson · Story: pramaan-io/pramaan-spec#1318 · Provenance: pramaan-docs ADR-116 (model egress is a router), founder inference route-policy 2026-06-15 (firm-settings "AI inference policy"), fixes Bug-1310 (kimi-k2.5 structured-output instability). Amends ADR-116's "no in-region tier" only as a UI-stub direction — see the doctrine note below.

🛰️ 1 · Satellite — why this exists

Atlas was wired so its default brain was a Bedrock model (kimi-k2.5), with the model slugs buried in atlas.yaml alongside behaviour. Two things were wrong. (1) kimi-k2.5 is unstable for our use: it intermittently omits AtlasTurn fields → the brain retries (30–40s each, several times) → the run looks like a hang (Bug-1310). DeepSeek v3.2 via OpenRouter is clean — verified live: forum grounding works (proceeding_type "MCIA arbitration · sole arbitrator"), usage 3426/728, no retries. (2) Bedrock is being dropped entirely (India frontier gap; ADR-116 multi-provider). No Bedrock model should remain as a default or tier anywhere.

The naïve fix — just swap the kimi slug for a DeepSeek slug in atlas.yaml — misses the point twice: it leaves model config tangled with behaviour in one YAML, and it leaves the provider defaulting to bedrock with a Bedrock fallback literal (apac.amazon.nova-lite-v1:0) one env-var away. The decision: extract the model into a dedicated per-agent model.json, make OpenRouter the only provider path, and purge every Bedrock model slug from config, defaults, and scripts.

✈️ 2 · Airplane — the shape of the change

BEFORE                                   AFTER
atlas.yaml  models: {bedrock slugs}      atlas.yaml  (behaviour only — NO model block)
            provider: bedrock            model.json  provider=openrouter, base_url, route,
config.py   default literal nova-lite                tiers{quick/advisory/fallback}=DeepSeek,
provider.py default branch = bedrock                 default=quick, ZDR, coming_soon_routes
                                         config.py   reads model.json; default literal = DeepSeek
                                         provider.py default branch = openrouter; ZDR on every call

resolution order (unchanged contract):  ENV  >  model.json  >  safe DeepSeek literal
   ATLAS_INFERENCE_PROVIDER / ATLAS_MODEL_ID / ATLAS_OPENROUTER_BASE_URL / ATLAS_TEMPERATURE /
   ATLAS_MAX_TOKENS / ATLAS_REGION  — env always wins (broker/deploy override path)

Route policy (founder, 2026-06-15). Inference is a firm setting. TODAY exactly one live route: Global → OpenRouter zero-data-retention, no region restriction — the default and only resolvable tier. US / EU / India region tiers are coming-soon stubs (no live route; recorded in coming_soon_routes). The broker (pramaan-functions) is governance — it reads firm policy, resolves Global→OpenRouter-ZDR, and passes provider/model in agent_invoke. Atlas is execution — one OpenRouter path.

🔬 3 · Microscope — what changed, and WHY each piece

PieceWhatWHY
agents/atlas/model.json (NEW)The model-config surface: provider=openrouter, base_url, route=global, zero_data_retention=true, region=null, tiers (quick/advisory/fallback, all DeepSeek), default=quick, coming_soon_routes, temperature, max_tokens.One file a dev opens to change Atlas's brain — split from behaviour. All slugs are OpenRouter DeepSeek; zero Bedrock.
atlas.yamlThe entire models: block removed; a pointer comment left in its place. Keeps caps, identity, reflection, truth boundary, tools, transport.atlas.yaml is now behaviour only; the model moved to model.json.
config.pyNew _load_model_config() reads model.json; provider default → openrouter; default tier quick; fallback literal → deepseek/deepseek-v3.2 (was apac.amazon.nova-lite-v1:0). Pricing table re-keyed to DeepSeek slugs.Removes the last Bedrock literal; cost stays accurate for the real model (Bug-1307 lineage).
provider.pyDefault branch flipped to openrouter (bedrock branch kept dormant as a mechanism, never default). model_settings() now pins ZDR on every OpenRouter call via extra_body={"provider":{"data_collection":"deny"}}.Founder Global-ZDR policy enforced on the wire, per call — OpenRouter may then route only to providers that do not store content.
scripts/deploy-atlas-staging.sh · scripts/dev-atlas-openrouter.shATLAS_MODEL_ID default → deepseek/deepseek-v3.2; kimi/claude-haiku comment examples → DeepSeek.The deploy/dev defaults must not reintroduce a dropped model.
Doctrine: AGENTS.md · agents/atlas/AGENTS.mdModel row → DeepSeek v3.2 (OpenRouter) + a model.json row; egress prose made provider-accurate ("inference provider", OpenRouter, ADR-116) instead of "Bedrock-only".The brain now calls OpenRouter, not Bedrock — the single-egress principle is preserved, the provider corrected. No silent drift.
Tests: test_atlas_config.py, test_atlas_provider.py (NEW); test_atlas_usage.py, test_atlas_runtime.py updatedAssert the no-env default resolves provider=openrouter + model=deepseek/deepseek-v3.2 driven BY model.json; no Bedrock slug in config values; ZDR present on OpenRouter settings; DeepSeek pricing.Locks the founder decision in CI — any reintroduction of a Bedrock slug or a dropped ZDR fails the suite.
ZDR is enforced, not labelled The "zero data retention" tier is not just a UI word: every OpenRouter request carries provider.data_collection="deny" in the body. A request can only land on a provider that does not store content. Verified by test_openrouter_settings_pin_zero_data_retention.
Doctrine paper trail The founder route-policy reconciles the ADR-116-vs-ADR-120 tension: tiers exist as a firm setting, but TODAY only the Global route is live — which matches ADR-116's "quality-first, no in-region tier live today". US/EU/India are UI stubs with no route. This needs an ADR amendment (amend pramaan-docs ADR-116, or a new ADR recording the settings-driven tier direction). Flagged here per "do not silently drift"; not blocking this PR.

✅ 4 · Verification

CheckResult
(cd agents/atlas && pytest tests -q)90 passed (incl. new config + provider tests for the OpenRouter/DeepSeek default and ZDR).
ruff check agents/ · ruff check tests/ scripts/All checks passed.
python scripts/check_brain_egress.pybrain-egress check: clean (ADR-110 §6 / ADR-121 D5).
Default path (no env)AtlasConfig.load() → provider openrouter, model deepseek/deepseek-v3.2, base_url https://openrouter.ai/api/v1 — driven by model.json (asserted).
Zero BedrockNo Bedrock model slug in model.json values, config, atlas.yaml, or the deploy/dev scripts. Remaining "kimi" strings are history-noting comments only.
Scope honoured No Atlas prompt/behaviour-semantics change (prompts.py/models.py untouched). The model-config surface moved and the provider flipped; the resolution contract (ENV > file > literal) is unchanged. Live staging runtime untouched — the runtime recreate / env wiring is the Bug-1310 / pramaan-infra cutover, separate.