Model routing and operator labels
How spec-agent (atlas) selects which model handles a turn, how operators override that selection from a Google Docs comment, and how reply prefixes let operators see which model actually answered.
Summary
- Atlas runs on a multi-provider abstraction. Today it can call Anthropic Sonnet/Opus on AWS Bedrock, Anthropic Sonnet/Opus directly on api.anthropic.com, and Kimi K2.6 on Azure AI Foundry.
- An operator can request a specific model (or several in parallel) by including a tag like
[kimi],[opus],[sonnet],[opus-direct], or[kimi][opus]in a Google Docs comment that mentions the agent. - Every reply atlas posts is prefixed with a model label —
[S46],[O47],[K26]— so operators can tell at a glance which model produced the response. - Adding a new model is one line in
ModelRegistry.kt. No other code changes required.
Provider abstraction
Atlas does not call "Claude" or "Kimi" directly. The executor calls a generic ModelClient interface. Three adapters implement it today:
| Provider | Adapter | Models | Auth |
|---|---|---|---|
| AWS Bedrock | BedrockAdapter | Sonnet 4.6, Opus 4.7 (cross-region inference profiles) | IAM role on AKS pod (Workload Identity → AWS STS) |
| Anthropic Direct | AnthropicDirectAdapter | Sonnet 4.6, Opus 4.7 | anthropic-direct-api-key in agent's token vault |
| Azure AI Foundry | FoundryOpenAIAdapter | Kimi K2.6 (and any other Foundry deployment) | foundry-api-key in Key Vault, fetched via Workload Identity |
Adapters translate atlas's generic ModelRequest/ModelResponse types to each provider's wire format. Tool definitions are written once and work across all three.
The selection happens at the start of each activity execution in DefaultModelClientFactory — atlas resolves the alias (e.g. "kimi") into a ModelEntry(modelId, provider) from ModelRegistry, then instantiates the matching adapter. There is no per-request switching mid-run.
Available aliases
The current registry (case-insensitive). Source of truth: ModelRegistry.kt.
| Alias | Model ID on the wire | Provider | Notes |
|---|---|---|---|
opus | us.anthropic.claude-opus-4-7 | Bedrock | Default for high-reasoning tasks. Currently quota-gated — see ADR-008 amendment 2026-04-20. |
sonnet | us.anthropic.claude-sonnet-4-6 | Bedrock | Default for most spec-agent runs. |
opus-direct | claude-opus-4-7 | Anthropic Direct | Bypasses Bedrock quota. Requires anthropic-direct-api-key in agent token vault. |
sonnet-direct | claude-sonnet-4-6 | Anthropic Direct | Same as opus-direct, for Sonnet. |
kimi | Kimi-K2.6 | Foundry | Moonshot K2.6 deployed in westus. Used for second-opinion / open-source comparison runs. |
Operator override via Google Docs comments
When you mention atlas in a Drive comment (e.g. @atlas-agent@aucert.dev evaluate this approach), the dispatcher starts a primary spec-agent workflow on the default model (Sonnet via Bedrock). If the comment contains a tag like [kimi] or [opus], atlas itself reads the comment, recognises the tag, and routes the work as follows:
Single-tag override ([kimi])
@atlas-agent@aucert.dev [kimi] evaluate if hermes-agent fits our workflow
- Primary Sonnet workflow starts (per dispatcher default).
- Sonnet posts an ack reply:
[S46] spawned the [kimi] workflow for your re-request (workflow_id: …). - Sonnet calls the
request_model_responsetool withmodel="kimi". - A secondary workflow is spawned with workflow ID
spec-agent-drive-<file_id>-kimi-secondaryandmodelOverride="kimi". - The Kimi secondary reads the same comment and posts its own reply, prefixed
[K26].
The primary workflow does not wait for the secondary — fire-and-forget. The secondary runs on the same task queue but in its own workspace clone.
Multi-tag override ([kimi][opus])
Multiple tags spawn one secondary per requested model. The primary still posts the [S46] ack reply; each secondary posts its own answer with the matching prefix.
@atlas-agent@aucert.dev [kimi][opus] which is the right call here?
You will get three replies on the comment thread:
[S46] …— primary's coordination reply[K26] …— Kimi's substantive reply[O47] …— Opus's substantive reply
This is the recommended pattern for getting independent second/third opinions on a non-trivial decision.
Direct-provider override ([opus-direct], [sonnet-direct])
Use when Bedrock is rate-limited, in maintenance, or you want to bypass it for any reason. Same models, different network path. Requires the agent to have an anthropic-direct-api-key token configured in Astra; if missing, the secondary workflow fails fast and posts an error reply.
Tag matching rules
- Case-insensitive:
[Kimi],[KIMI],[kimi]all resolve identically. - Whitespace-tolerant:
[ kimi ]is accepted (leading/trailing spaces ignored). - Order-independent:
[opus][sonnet]and[sonnet][opus]produce the same set of secondaries. - Unknown tags are ignored silently, not errored.
[gpt5]today produces no secondary; the primary continues with default model. Future: warn on unknown tag in primary's reply.
Reply model labels
Every reply atlas posts to a Drive comment is prefixed by a label that identifies the model that produced it:
| Label | Model |
|---|---|
[S46] | Sonnet 4.6 (Bedrock or Direct) |
[O47] | Opus 4.7 (Bedrock or Direct) |
[K26] | Kimi K2.6 (Foundry) |
[??] | Unknown — falls through if a new model is added without a corresponding label branch in ModelLabels.kt |
Labels are applied at three points to ensure consistency:
- System prompt instruction — the personality prompt tells the model to start every response with its label.
- Deterministic fallback in
doneOutputPayload— if the model forgets, code prepends the label before persisting. DocsPostCommentReplyTool— the comment-reply tool prepends the label to every outbound comment.
The prepend function is idempotent: if the content already starts with a label token ([S46] …), it is not double-prefixed.
The [??] fallback is deliberate: losing the label is preferable to crashing the agent. If you see [??] in production, it means a new alias was added to ModelRegistry without a corresponding branch in ModelLabels.label() — fix that file.
Adding a new model
Three files. No other changes required.
ModelRegistry.kt— alias entry (one line in theALIASESmap):"deepseek" to ModelEntry("DeepSeek-V3.2", ModelProvider.FOUNDRY_OPENAI),ModelLabels.kt— label branch (one branch in thewhenexpression):modelId.contains("DeepSeek-V3.2", ignoreCase = true) -> "[DSV3]"- Provider config — only if the model lives on a provider not already wired up. For a new Foundry model, no infra change is needed beyond ensuring the deployment exists in Terraform (
infra/terraform/foundation/foundry.tf). For a new provider (e.g. Google Vertex), you must add a fourthModelProviderenum value, write a new adapter inagents/shared/model/adapters/, and register it inDefaultModelClientFactory.
After steps 1+2, operators can immediately invoke the new model via [deepseek] in a comment.
Related
- ADR-011 — Agent harness strategy (why we built our own provider abstraction instead of adopting a framework)
- ADR-008 — Azure AI Foundry model deployment (Foundry account, deployment naming convention)
- SPEC-005 §4 — Provider-agnostic model abstraction (original design)
- Spec-agent v0.1 handover — operational handover including model rollout history