Skip to content

feat: add experimental coderd_agents_model resource#371

Open
ethanndickson wants to merge 7 commits into
docs/add-agents-mdfrom
ethan/agents-model-resource
Open

feat: add experimental coderd_agents_model resource#371
ethanndickson wants to merge 7 commits into
docs/add-agents-mdfrom
ethan/agents-model-resource

Conversation

@ethanndickson

@ethanndickson ethanndickson commented Jun 24, 2026

Copy link
Copy Markdown
Member

Stack — builds on #369 (AGENTS.md contributor guide) and the coderd_ai_provider resource in #368; review/merge those first.

Relates to CODAGT-607

Summary

Adds coderd_agents_model for managing Coder Agents admin-managed chat model configurations from Terraform — binding a model identifier to a configured AI provider — with full CRUD + import, generated docs/examples, acceptance/unit tests and an integration test with the provider resource. The resource is marked experimental via a warning callout in its docs.

Approach

  • model_config as normalized JSON. The open-ended per-call tuning blob (token limits, temperature, cost/pricing, provider options) is a single JSON string backed by a custom type that compares by JSON semantic equality, so whitespace, key ordering, and equivalent number spellings (a 3.00 cost returned as 3) don't produce perpetual diffs. An empty jsonencode({}) is rejected at plan time because Coder discards it.
  • Provider-specific controls live under provider_options. Common per-call knobs — reasoning effort, reasoning summary, parallel tool calls, and web search — are nested per provider rather than top-level, since each provider names them differently. Anthropic uses effort (and/or thinking.budget_tokens), send_reasoning, web_search_enabled, and disable_parallel_tool_use; OpenAI uses reasoning_effort, reasoning_summary, parallel_tool_calls, and web_search_enabled. The examples show both.
  • provider_type derived from ai_provider_id. A model takes its provider via a required ai_provider_id, and Coder derives the runtime provider_type from it. A plan modifier keeps the prior value while ai_provider_id is unchanged and lets it recompute when the binding changes, so it never shows stale plan noise or trips the post-apply consistency check.
  • Server-owned default election. Coder elects exactly one default model server-side, so is_default is optional/computed: set it to true on one model and omit it on others. Setting false is rejected by config validation, since the server manages promotion/demotion itself and would otherwise produce a post-apply consistency error.

Why model_config is JSON, not typed attributes

provider_options is a tagged union (exactly one of openai/anthropic/google/… with non-obvious aliases like Bedrock→anthropic), and the config carries decimal pricing and free-form map[string]any fields — none of which Terraform's type system can express without re-implementing the server's own validation. A single normalized JSON string sidesteps all of that and lets new upstream tuning fields land with a codersdk bump instead of schema churn. This mirrors the closest prior art: AWS's bedrockagent additional_model_request_fields, which models per-provider model overrides the same way.

Schema

resource "coderd_ai_provider" "anthropic" {
  type     = "anthropic"
  name     = "anthropic"
  base_url = "https://api.anthropic.com"

  api_key_wo         = var.anthropic_api_key
  api_key_wo_version = 1
}

resource "coderd_agents_model" "sonnet" {
  ai_provider_id = coderd_ai_provider.anthropic.id
  model          = "claude-3-5-sonnet-20241022"
  display_name   = "Claude 3.5 Sonnet"
  enabled        = true
  is_default     = true
  context_limit  = 200000

  model_config = jsonencode({
    max_output_tokens = 8192
    temperature       = 0.7
    cost = {
      input_price_per_million_tokens  = "3"
      output_price_per_million_tokens = "15"
    }
    # Provider-specific controls. Anthropic uses an effort level (low, medium,
    # high, xhigh, max) and/or an extended-thinking token budget, plus common
    # knobs like reasoning visibility, web search, and parallel tool use.
    provider_options = {
      anthropic = {
        effort                    = "high"
        thinking                  = { budget_tokens = 4096 }
        send_reasoning            = true
        web_search_enabled        = true
        disable_parallel_tool_use = false
      }
    }
  })
}

resource "coderd_agents_model" "gpt" {
  ai_provider_id = coderd_ai_provider.openai.id
  model          = "gpt-5"
  display_name   = "GPT-5"
  context_limit  = 400000

  model_config = jsonencode({
    max_output_tokens = 8192
    # OpenAI uses reasoning_effort (none, minimal, low, medium, high, xhigh),
    # plus common knobs like reasoning summary, parallel tool calls, and web search.
    provider_options = {
      openai = {
        reasoning_effort    = "high"
        reasoning_summary   = "detailed"
        parallel_tool_calls = true
        web_search_enabled  = true
      }
    }
  })
}

Computed: id, provider_type (derived from ai_provider_id), display_name, enabled, is_default, compression_threshold, created_at, and updated_at.

Notes

No write-only arguments, so there's no Terraform 1.11+ requirement — the API key lives on the referenced coderd_ai_provider. The chat-model endpoints are experimental (/api/experimental); since there's no get-by-id route, Read lists model configs and filters by ID.

Follow-up to #368 (coderd_ai_provider).

ethanndickson commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

@ethanndickson ethanndickson changed the title feat: add coderd_agents_model resource feat: add experimental coderd_agents_model resource Jun 24, 2026
@ethanndickson ethanndickson changed the base branch from docs/add-agents-md to graphite-base/371 June 24, 2026 14:41
@ethanndickson ethanndickson force-pushed the ethan/agents-model-resource branch from 1d93af4 to a40ebf9 Compare June 24, 2026 14:47
@ethanndickson ethanndickson changed the base branch from graphite-base/371 to docs/add-agents-md June 24, 2026 14:47
@ethanndickson ethanndickson force-pushed the ethan/agents-model-resource branch 2 times, most recently from 3eba546 to 44955ac Compare June 24, 2026 15:08
@ethanndickson ethanndickson force-pushed the docs/add-agents-md branch 2 times, most recently from ce7bd51 to ac6b0b1 Compare June 24, 2026 15:15
@ethanndickson ethanndickson force-pushed the ethan/agents-model-resource branch 2 times, most recently from 342a9f1 to b5bcd68 Compare June 25, 2026 04:21
@ethanndickson ethanndickson force-pushed the ethan/agents-model-resource branch from b5bcd68 to 043dfff Compare June 25, 2026 05:26
@ethanndickson ethanndickson marked this pull request as ready for review June 25, 2026 05:28
@ethanndickson

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 25, 2026

Copy link
Copy Markdown

Chat: Review posted | View chat
Requested: 2026-06-25 08:11 UTC by @ethanndickson
Spend: $67.82 / $100.00

Review history
  • R1 (2026-06-25): 14 reviewers, 4 Nit, 6 P2, 6 P3, COMMENT. Review
  • R2 (2026-06-25): 4 reviewers, 4 Nit, 6 P2, 7 P3, 1 P4, COMMENT. Review
  • R3 (2026-06-25): 3 reviewers, 4 Nit, 6 P2, 7 P3, 1 P4, APPROVE. Review

deep-review v0.9.0 | Round 3 | 44cd01d..4353f7a

Last posted: Round 3, 18 findings (6 P2, 7 P3, 1 P4, 4 Nit), APPROVE. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Author fixed (9a02e24) agents_model_resource.go:275 Update overrides server-returned DisplayName with plan value; Create does not R1 Netero Yes
CRF-2 P3 Author fixed (9a02e24) agents_model_resource_test.go:1 No deferral/unknown-variable test for plan-time unknown values R1 Netero Yes
CRF-3 P2 Author fixed (9a02e24) agents_model_resource.go:304 createRequest sends explicit is_default: false when user omits attribute, suppressing server-side default election R1 Hisoka P2, Meruem P2, Knov P2 Yes
CRF-4 P2 Author fixed (9a02e24) agents_model_resource.go:228 Read treats list endpoint 404 as model-not-found, silently removes all agents_model resources from state R1 Hisoka P2, Mafuuu P2 Yes
CRF-5 P2 Author fixed (9a02e24) agents_model_resource_test.go:1 No test for ValidateConfig rejecting is_default = false R1 Bisky P2, Chopper P3 Yes
CRF-6 P2 Author fixed (9a02e24) agents_model_resource_test.go:1 No deferral/unknown-variable test matching documented AGENTS.md pattern R1 Mafu-san P2, Netero P3 Yes
CRF-7 P2 Author fixed (9a02e24) agents_model_resource.go:302 display_name = "" causes consistency error on Create and perpetual drift on Update due to omitempty + DisplayName override R1 Kite P2, Mafuuu P3, Netero P3 Yes
CRF-8 P2 Author fixed (9a02e24) agents_model_resource.go:148 UseStateForUnknown on is_default causes cross-resource inconsistency when another model takes the default R1 Pariston P2 Yes
CRF-9 P3 Author fixed (9a02e24) agents_model_config.go:176 Non-object JSON (arrays, primitives) passes validator silently, defers error to apply time R1 Knov Yes
CRF-10 P3 Author fixed (9a02e24) agents_model_resource_test.go:67 updateRequest test proves edge case but not core contract (unchanged fields excluded) R1 Bisky Yes
CRF-11 P3 Author fixed (9a02e24) agents_model_resource.go:40 Resource stores ExperimentalClient instead of CoderdProviderData, breaking codebase convention R1 Zoro Yes
CRF-12 P3 Author fixed (9a02e24) agents_model_resource.go:233 Read error message says "list" instead of "read", exposing implementation detail R1 Leorio Yes
CRF-13 P3 Dropped by orchestrator (low consequence, substance is inline) agents_model_config.go:145 Comment references function in a different repository R1 Gon No
CRF-14 Nit Author contested; panel closed R2 (4/4 accept) agents_model_resource.go:150 Magic number 70 for compression threshold default R1 Gon Yes
CRF-15 Nit Author fixed (9a02e24) agents_model_resource_test.go:332 ImportStateVerifyIgnore for model_config has no comment R1 Bisky Yes
CRF-16 Nit Dropped by orchestrator (error path already carries attribute context) agents_model_resource.go:77 Error summary "Invalid is_default" restates attribute name R1 Leorio No
CRF-17 Nit Author contested; panel closed R2 (4/4 accept) agents_model_resource_test.go:624 testCheckAgentsModelConfig checks only 2 of 4 model_config fields R1 Chopper Yes
CRF-18 Nit Author fixed (9a02e24) agents_model_resource_test.go:546 createAccAgentsModelAIProvider duplicates createAccAgentsModelAIProviderOfType R1 Zoro Yes
CRF-19 Nit Dropped by orchestrator (scope creep, convention change across all resources) agents_model_resource.go:29 ResourceWithConfigure compile-time assertion only on this resource R1 Zoro No
CRF-20 Nit Dropped by orchestrator (style preference, substance present) agents_model_config.go:142 Comment bloat on validator and modifier docs R1 Gon No
CRF-21 P3 Author fixed (4353f7a) agents_model_resource.go:330 No test exercises is_default unknown-value guard in updateRequest or createRequest R2 Bisky Yes
CRF-22 P4 Author fixed (4353f7a) agents_model_resource_test.go:67 updateRequest unit test covers only ModelConfig-clearing; other field-diff branches unit-uncovered R2 Bisky Yes

Contested and acknowledged

(none)

CRF-14 (Nit, agents_model_resource.go:150) - Magic number 70

  • Finding: Reviewer proposed extracting 70 to a named constant.
  • Author defense: Repo consistently inlines schema-default literals; no resource extracts them to named constants. Introducing one diverges from house style.
  • Panel closure (R2, 4/4): All 4 reviewers verified 20+ inline literals across 6 resource files with zero named constants. Defense is factual; the finding asks for a convention change across all resources, not a fix for this resource.

CRF-17 (Nit, agents_model_resource_test.go:624) - testCheckAgentsModelConfig checks only 2 of 4 fields

  • Finding: Reviewer proposed adding cost assertions to close the gap.
  • Author defense: Cost round-tripping is proven end-to-end by TestAccAgentsModelResourceModelConfigNoDrift.
  • Panel closure (R2, 4/4): All 4 reviewers verified the no-drift test writes cost values with trailing zeros and asserts an empty replan. If cost round-tripping failed, step 2 would error. Stronger proof than field-level assertions.

Law analysis

Effective LOC: 1435 additions. Head SHA: 043dfff. Verdict: Don't split. Enforcement: Advisory (not applicable).

Round log

Round 1

Panel. Netero: 2 P3. Law: Don't split. Panel: 14 reviewers (Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, Ging-Go, Meruem, Chopper, Zoro, Knov, Komugi, Kite). 6 P2, 4 P3, 4 Nit posted. 4 dropped. Reviewed against 44cd01d..043dfff.

Round 2

Panel. Netero: 0 new findings. Panel: 4 reviewers (Bisky, Mafuuu, Mafu-san, Kite). CRF-14, CRF-17 contested and panel-closed (4/4 accept). 1 P3, 1 P4 new. 14 prior findings verified fixed. Reviewed against 44cd01d..9a02e24.

Round 3

Netero: 0 findings. CRF-21, CRF-22 addressed in 4353f7a. All 22 findings resolved. Approved. Reviewed against 44cd01d..4353f7a.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 043dfffd18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/provider/agents_model_resource.go Outdated
@ethanndickson ethanndickson force-pushed the ethan/agents-model-resource branch from 043dfff to 0179060 Compare June 25, 2026 05:42
@ethanndickson

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0179060d00

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/provider/agents_model_resource.go Outdated
Comment thread internal/provider/agents_model_resource.go Outdated
@ethanndickson ethanndickson force-pushed the ethan/agents-model-resource branch from 0179060 to 9db9cd2 Compare June 25, 2026 06:03
@ethanndickson

Copy link
Copy Markdown
Member Author

@codex review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-structured resource with some genuinely good engineering. The custom agentsModelConfigType that canonicalizes through the SDK struct to prevent perpetual diffs from decimal normalization is the right approach, and the agentsModelConfigNotEmptyValidator turns a confusing Terraform core error into an actionable plan-time message. The useStateForUnknownUnlessChanged plan modifier is clean generic infrastructure. Test coverage is thorough, especially the semantic equality suite and the no-drift acceptance test.

Severity counts: 6 P2, 6 P3, 4 Nit.

The P2s cluster around two themes: (1) Optional+Computed fields with no Default sending explicit zero values to the server when the user omits them (is_default, display_name), and (2) missing test coverage for validation paths that carry real user-facing value. Both are fixable without structural changes.

"If someone deleted every if !m.X.Equal(state.X) guard and unconditionally set all fields, this test would still pass." (Bisky, on the updateRequest test)

This is a first-round review. The full panel has reviewed.

🤖 This review was automatically generated with Coder Agents.

Comment thread internal/provider/agents_model_resource.go Outdated
Comment thread internal/provider/agents_model_resource_test.go
Comment thread internal/provider/agents_model_resource.go Outdated
Comment thread internal/provider/agents_model_resource.go Outdated
Comment thread internal/provider/agents_model_resource.go
Comment thread internal/provider/agents_model_resource.go Outdated
Comment thread internal/provider/agents_model_resource.go
Comment thread internal/provider/agents_model_resource_test.go
Comment thread internal/provider/agents_model_resource_test.go
Comment thread internal/provider/agents_model_resource_test.go
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 9db9cd27ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ethanndickson

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 14 prior findings addressed. The fixes are thorough: CRF-3 (is_default omission) guards with IsUnknown(), CRF-4 (list 404) removes the isNotFound guard, CRF-7 (display_name) adds LengthAtLeast(1) + DisplayName validator, CRF-8 drops UseStateForUnknown from is_default AND adds null/unknown guards in updateRequest. CRF-15 went beyond the ask, replacing ImportStateVerifyIgnore with a semantic comparison.

Contested findings CRF-14 (magic number 70) and CRF-17 (testCheckAgentsModelConfig field coverage) both closed by the panel (4/4 accept). The author's defenses are factual: inline literals are house style, and cost round-tripping is proven end-to-end by the no-drift test.

Two new findings, both test coverage gaps for the is_default unknown-value guards introduced by the CRF-3/CRF-8 fixes.

"types.BoolUnknown().ValueBoolPointer() returns &false (unknown has zero-value internals). Dropping the IsUnknown() predicate would silently send is_default: false to the API on every unrelated update, demoting the model from default." (Bisky)

🤖 This review was automatically generated with Coder Agents.

Comment thread internal/provider/agents_model_resource.go
Comment thread internal/provider/agents_model_resource_test.go
@ethanndickson

Copy link
Copy Markdown
Member Author

@codex review

@ethanndickson

Copy link
Copy Markdown
Member Author

/coder-agents-review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4353f7a08e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/provider/agents_model_resource.go

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All findings resolved across three rounds (6 P2, 6 P3, 1 P4, 4 Nit raised; 16 fixed, 2 panel-closed, 4 dropped). The fixes were thorough and several went beyond the minimum: CRF-15 replaced ImportStateVerifyIgnore with semantic comparison, CRF-8 addressed both plan and apply sides, CRF-21/CRF-22 added targeted unit tests for the unknown-value guards.

The resource is well-engineered. The custom agentsModelConfigType that canonicalizes through the SDK struct, the agentsModelConfigNotEmptyValidator that turns a confusing core error into an actionable message, and the useStateForUnknownUnlessChanged plan modifier are all clean, tested, and correctly motivated.

"The model_config custom type shows genuine problem understanding." (Mafu-san, R1)

🤖 This review was automatically generated with Coder Agents.

@ethanndickson ethanndickson self-assigned this Jun 25, 2026

@matifali matifali left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work Ethan. I guess the next step now is to start dogfooding it.

@linear-code

linear-code Bot commented Jun 25, 2026

Copy link
Copy Markdown

CODAGT-607

Comment on lines +230 to +231
require.NoError(t, err)
assert.JSONEq(t, w.config, string(got), "model_config for %s", m.Model)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest using cmp.Diff instead for nicer diffs

Comment on lines +707 to +711
{
// Create bound to the anthropic provider.
Config: cfg(anthropic.ID.String(), 200000),
Check: resource.TestCheckResourceAttr("coderd_agents_model.sonnet", "provider_type", "anthropic"),
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worthwhile adding an idempotence test?

Comment on lines +212 to +215
"claude-opus-4-8": {"anthropic", `{"max_output_tokens":128000,"cost":{"input_price_per_million_tokens":"5","output_price_per_million_tokens":"25","cache_read_price_per_million_tokens":"0.5","cache_write_price_per_million_tokens":"6.25"},"provider_options":{"anthropic":{"send_reasoning":true,"effort":"high"}}}`},
"claude-sonnet-4-6": {"anthropic", `{"cost":{"input_price_per_million_tokens":"3","output_price_per_million_tokens":"15"},"provider_options":{"anthropic":{"send_reasoning":true,"effort":"max","web_search_enabled":true,"thinking":{"budget_tokens":16000}}}}`},
"gpt-5.5": {"openai", `{"cost":{"input_price_per_million_tokens":"2.5","output_price_per_million_tokens":"15","cache_read_price_per_million_tokens":"0.25"},"provider_options":{"openai":{"parallel_tool_calls":false,"reasoning_effort":"xhigh","reasoning_summary":"detailed","text_verbosity":"high","web_search_enabled":true,"search_context_size":"medium"}}}`},
"gpt-5.4-mini": {"openai", `{"provider_options":{"openai":{"reasoning_effort":"medium"}}}`},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: pretty-format for readability

Add a Terraform resource for managing Coder Agents admin-managed chat
model configurations. A model config binds a model identifier to a
configured AI provider via a required ai_provider_id and carries
context/compression settings plus an open-ended per-call tuning blob.

The model_config attribute is a custom normalized JSON string type so it
is passed through to Coder verbatim and compared with JSON semantic
equality, avoiding schema churn as Coder adds tuning fields. An empty
config (jsonencode({})) is rejected at plan time because Coder discards
it, which would otherwise leave Terraform's state inconsistent. Includes
full CRUD, import, examples, generated docs, and focused
unit/acceptance tests.
@ethanndickson ethanndickson force-pushed the ethan/agents-model-resource branch from 86ac6f2 to 877f8dc Compare June 27, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants