Skip to main content

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:

ProviderAdapterModelsAuth
AWS BedrockBedrockAdapterSonnet 4.6, Opus 4.7 (cross-region inference profiles)IAM role on AKS pod (Workload Identity → AWS STS)
Anthropic DirectAnthropicDirectAdapterSonnet 4.6, Opus 4.7anthropic-direct-api-key in agent's token vault
Azure AI FoundryFoundryOpenAIAdapterKimi 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.

AliasModel ID on the wireProviderNotes
opusus.anthropic.claude-opus-4-7BedrockDefault for high-reasoning tasks. Currently quota-gated — see ADR-008 amendment 2026-04-20.
sonnetus.anthropic.claude-sonnet-4-6BedrockDefault for most spec-agent runs.
opus-directclaude-opus-4-7Anthropic DirectBypasses Bedrock quota. Requires anthropic-direct-api-key in agent token vault.
sonnet-directclaude-sonnet-4-6Anthropic DirectSame as opus-direct, for Sonnet.
kimiKimi-K2.6FoundryMoonshot 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
  1. Primary Sonnet workflow starts (per dispatcher default).
  2. Sonnet posts an ack reply: [S46] spawned the [kimi] workflow for your re-request (workflow_id: …).
  3. Sonnet calls the request_model_response tool with model="kimi".
  4. A secondary workflow is spawned with workflow ID spec-agent-drive-<file_id>-kimi-secondary and modelOverride="kimi".
  5. 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:

LabelModel
[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:

  1. System prompt instruction — the personality prompt tells the model to start every response with its label.
  2. Deterministic fallback in doneOutputPayload — if the model forgets, code prepends the label before persisting.
  3. 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.

  1. ModelRegistry.kt — alias entry (one line in the ALIASES map): "deepseek" to ModelEntry("DeepSeek-V3.2", ModelProvider.FOUNDRY_OPENAI),
  2. ModelLabels.kt — label branch (one branch in the when expression): modelId.contains("DeepSeek-V3.2", ignoreCase = true) -> "[DSV3]"
  3. 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 fourth ModelProvider enum value, write a new adapter in agents/shared/model/adapters/, and register it in DefaultModelClientFactory.

After steps 1+2, operators can immediately invoke the new model via [deepseek] in a comment.

  • 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