diff --git a/docs/resources/agents_model.md b/docs/resources/agents_model.md index 602ea06..8500c6a 100644 --- a/docs/resources/agents_model.md +++ b/docs/resources/agents_model.md @@ -44,9 +44,12 @@ resource "coderd_agents_model" "sonnet" { input_price_per_million_tokens = "3" output_price_per_million_tokens = "15" } + reasoning_effort = { + default = "high" + max = "high" + } provider_options = { anthropic = { - effort = "high" thinking = { budget_tokens = 4096 } } } diff --git a/examples/resources/coderd_agents_model/resource.tf b/examples/resources/coderd_agents_model/resource.tf index 62eed4c..41a710b 100644 --- a/examples/resources/coderd_agents_model/resource.tf +++ b/examples/resources/coderd_agents_model/resource.tf @@ -26,9 +26,12 @@ resource "coderd_agents_model" "sonnet" { input_price_per_million_tokens = "3" output_price_per_million_tokens = "15" } + reasoning_effort = { + default = "high" + max = "high" + } provider_options = { anthropic = { - effort = "high" thinking = { budget_tokens = 4096 } } } diff --git a/go.mod b/go.mod index 02e2c66..91a2b79 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.4 require ( cdr.dev/slog/v3 v3.1.0 - github.com/coder/coder/v2 v2.34.0-rc.0.0.20260701204415-db7f4438b4ad + github.com/coder/coder/v2 v2.34.0-rc.0.0.20260713111656-06107768a8d6 github.com/coder/retry v1.5.1 github.com/coder/serpent v0.15.0 github.com/coder/websocket v1.8.15 @@ -53,8 +53,9 @@ require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-textseg/v17 v17.0.1 // indirect github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect - github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -86,7 +87,7 @@ require ( github.com/ebitengine/purego v0.10.1 // indirect github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect - github.com/go-chi/chi/v5 v5.2.4 // indirect + github.com/go-chi/chi/v5 v5.3.1 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -121,7 +122,7 @@ require ( github.com/imdario/mergo v0.3.16 // indirect github.com/invopop/jsonschema v0.14.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/compress v1.19.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect github.com/magiconair/properties v1.8.10 // indirect @@ -188,7 +189,7 @@ require ( github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark-meta v1.1.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zclconf/go-cty v1.18.1 // indirect + github.com/zclconf/go-cty v1.19.0 // indirect github.com/zeebo/errs v1.4.0 // indirect go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect go.nhat.io/otelsql v0.16.0 // indirect @@ -217,14 +218,14 @@ require ( golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.47.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect - google.golang.org/grpc v1.81.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect + google.golang.org/grpc v1.82.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.74.0 // indirect gopkg.in/ini.v1 v1.67.3 // indirect diff --git a/go.sum b/go.sum index 4ebef93..63a2074 100644 --- a/go.sum +++ b/go.sum @@ -76,10 +76,12 @@ github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUS github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-textseg/v17 v17.0.1 h1:bpMXRgQ5cEoRNuQke1a80/Nl6w3G5eoIbWo9f3gXkAs= +github.com/apparentlymart/go-textseg/v17 v17.0.1/go.mod h1:fa8X4jgGeevslICIY6LcdjkSecWnXmYd9Lk34z/VxZs= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= -github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= +github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -125,8 +127,8 @@ github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoK github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/coder/coder/v2 v2.34.0-rc.0.0.20260701204415-db7f4438b4ad h1:dVCql8eMM+GBtPmIvdY1hPvONY234fmeZjNLYp5+IVQ= -github.com/coder/coder/v2 v2.34.0-rc.0.0.20260701204415-db7f4438b4ad/go.mod h1:sJc0Y7Rd2kzqjdCMwfrebST+qPJprLub3+TVS7jXHQo= +github.com/coder/coder/v2 v2.34.0-rc.0.0.20260713111656-06107768a8d6 h1:zEKALHu+grAlyUJejp6XZoRGkvOQyWYIfY0WEWVXNy8= +github.com/coder/coder/v2 v2.34.0-rc.0.0.20260713111656-06107768a8d6/go.mod h1:imH39Jw6JBYWzFZtLx3juwWPhx+KxDqL3oPALrAh/U0= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= @@ -182,8 +184,8 @@ github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeO github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= -github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= @@ -313,8 +315,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -532,8 +534,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM= -github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= +github.com/zclconf/go-cty v1.19.0 h1:IV8WdqYZc2c5rLX9bEoLNXKojBAp0MZPBHMIrCoa/s4= +github.com/zclconf/go-cty v1.19.0/go.mod h1:12W89jGn3JCOIQi7infWr9m80rOkb5RNYJqXMZcN4c8= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -695,8 +697,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -723,10 +725,10 @@ google.golang.org/genproto v0.0.0-20260610212136-7ab31c22f7ad h1:cYL1DPJAQr4JMvh google.golang.org/genproto v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:cVHIikDNAdx8ISZeW+2rYkEMf3xn0GSaBYmVnWXQBUo= google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw= google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d h1:mpAgMyM9vQHxycBlDq50y1VHpfSfVwzXvrQKtYbXuUY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/integration/agents-model-test/main.tf b/integration/agents-model-test/main.tf index c725126..810ed5a 100644 --- a/integration/agents-model-test/main.tf +++ b/integration/agents-model-test/main.tf @@ -42,10 +42,13 @@ resource "coderd_agents_model" "claude_opus" { cache_read_price_per_million_tokens = "0.5" cache_write_price_per_million_tokens = "6.25" } + reasoning_effort = { + default = "high" + max = "high" + } provider_options = { anthropic = { send_reasoning = true - effort = "high" } } }) @@ -63,10 +66,13 @@ resource "coderd_agents_model" "claude_sonnet" { input_price_per_million_tokens = "3" output_price_per_million_tokens = "15" } + reasoning_effort = { + default = "max" + max = "max" + } provider_options = { anthropic = { send_reasoning = true - effort = "max" web_search_enabled = true thinking = { budget_tokens = 16000 @@ -89,10 +95,13 @@ resource "coderd_agents_model" "gpt_xhigh" { output_price_per_million_tokens = "15" cache_read_price_per_million_tokens = "0.25" } + reasoning_effort = { + default = "xhigh" + max = "xhigh" + } provider_options = { openai = { parallel_tool_calls = false - reasoning_effort = "xhigh" reasoning_summary = "detailed" text_verbosity = "high" web_search_enabled = true @@ -110,10 +119,9 @@ resource "coderd_agents_model" "gpt_mini" { compression_threshold = 70 model_config = jsonencode({ - provider_options = { - openai = { - reasoning_effort = "medium" - } + reasoning_effort = { + default = "medium" + max = "medium" } }) } diff --git a/integration/integration_test.go b/integration/integration_test.go index adf2d45..f65d728 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -224,51 +224,59 @@ func TestIntegration(t *testing.T) { "cache_read_price_per_million_tokens": "0.5", "cache_write_price_per_million_tokens": "6.25" }, - "provider_options": { - "anthropic": { - "send_reasoning": true, - "effort": "high" - } + "reasoning_effort": { + "default": "high", + "max": "high" + }, + "provider_options": { + "anthropic": { + "send_reasoning": true + } } - }`}, + }`}, "claude-sonnet-4-6": {"anthropic", `{ "cost": { - "input_price_per_million_tokens": "3", + "input_price_per_million_tokens": "3", "output_price_per_million_tokens": "15" - }, - "provider_options": { + }, + "reasoning_effort": { + "default": "max", + "max": "max" + }, + "provider_options": { "anthropic": { - "send_reasoning": true, - "effort": "max", - "web_search_enabled": true, - "thinking": { - "budget_tokens": 16000 - } + "send_reasoning": true, + "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" + "cost": { + "input_price_per_million_tokens": "2.5", + "output_price_per_million_tokens": "15", + "cache_read_price_per_million_tokens": "0.25" + }, + "reasoning_effort": { + "default": "xhigh", + "max": "xhigh" }, - "provider_options": { - "openai": { - "parallel_tool_calls": false, - "reasoning_effort": "xhigh", - "reasoning_summary": "detailed", + "provider_options": { + "openai": { + "parallel_tool_calls": false, + "reasoning_summary": "detailed", "text_verbosity": "high", - "web_search_enabled": true, + "web_search_enabled": true, "search_context_size": "medium" } } }`}, "gpt-5.4-mini": {"openai", `{ - "provider_options": { - "openai": { - "reasoning_effort": "medium" - } + "reasoning_effort": { + "default": "medium", + "max": "medium" } }`}, } diff --git a/internal/provider/agents_model_config.go b/internal/provider/agents_model_config.go index b3f479f..f42bd99 100644 --- a/internal/provider/agents_model_config.go +++ b/internal/provider/agents_model_config.go @@ -135,11 +135,12 @@ func agentsModelConfigCanonicalJSON(raw string) (string, error) { // agentsModelConfigSortedJSON re-encodes a JSON document with object keys sorted // alphabetically (recursively) and compact spacing, matching Terraform's // jsonencode output. Numbers are preserved verbatim via json.Number, so the only -// change is key order. Coder stores model_config in the SDK struct's field order, -// which is not alphabetical; without this the byte string in state never matches -// the user's jsonencode config, and the framework's raw-byte plan guard -// (server_planresourcechange.go: PlannedState.Raw.Equal(PriorState.Raw)) then -// marks the computed updated_at attribute unknown on every plan after import. +// change is key order. The model_config read back from Coder is re-encoded from +// codersdk.ChatModelCallConfig in struct field order, which is not alphabetical; +// without this the byte string in state never matches the user's jsonencode +// config, and the framework's raw-byte plan guard (server_planresourcechange.go: +// PlannedState.Raw.Equal(PriorState.Raw)) then marks the computed updated_at +// attribute unknown on every plan after import. func agentsModelConfigSortedJSON(raw []byte) (string, error) { dec := json.NewDecoder(bytes.NewReader(raw)) dec.UseNumber() @@ -232,3 +233,46 @@ func (v agentsModelConfigNotEmptyValidator) ValidateString(_ context.Context, re ) } } + +// agentsModelConfigNoDroppedKeysValidator rejects a model_config containing +// settings the provider would silently drop when decoding into +// codersdk.ChatModelCallConfig. Without it a removed or renamed field in the +// SDK type would drop the user's setting with no plan-time error, and semantic +// equality would hide the loss. +type agentsModelConfigNoDroppedKeysValidator struct{} + +var _ validator.String = agentsModelConfigNoDroppedKeysValidator{} + +func (v agentsModelConfigNoDroppedKeysValidator) Description(_ context.Context) string { + return "model_config must only contain settings from the Coder chat model config schema this provider was built against; unrecognized settings would be silently dropped." +} + +func (v agentsModelConfigNoDroppedKeysValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v agentsModelConfigNoDroppedKeysValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + // Invalid or non-object JSON is left for the custom type and the not-empty + // validator to report. + if _, err := agentsModelConfigCanonicalJSON(req.ConfigValue.ValueString()); err != nil { + return + } + // The lenient decode above succeeded, so a strict failure can only be an + // unknown field. + var config codersdk.ChatModelCallConfig + if err := config.UnmarshalStrict([]byte(req.ConfigValue.ValueString())); err != nil { + resp.Diagnostics.AddAttributeError( + req.Path, + "Unrecognized model_config settings", + fmt.Sprintf( + "model_config contains a setting that is not part of the Coder chat model config schema this provider was built against, so it would be silently dropped before reaching Coder: %s. "+ + "If the setting is valid for your Coder deployment, upgrade the provider to a version built against your Coder release; otherwise remove it or fix the field name. "+ + "See https://pkg.go.dev/github.com/coder/coder/v2/codersdk#ChatModelCallConfig.", + err, + ), + ) + } +} diff --git a/internal/provider/agents_model_resource.go b/internal/provider/agents_model_resource.go index 30a6259..df626e3 100644 --- a/internal/provider/agents_model_resource.go +++ b/internal/provider/agents_model_resource.go @@ -146,6 +146,7 @@ func (r *AgentsModelResource) Schema(ctx context.Context, req resource.SchemaReq Optional: true, Validators: []validator.String{ agentsModelConfigNotEmptyValidator{}, + agentsModelConfigNoDroppedKeysValidator{}, }, PlanModifiers: []planmodifier.String{ agentsModelConfigUseStateIfSemanticallyEqual{}, diff --git a/internal/provider/agents_model_resource_test.go b/internal/provider/agents_model_resource_test.go index 21fea26..8574caf 100644 --- a/internal/provider/agents_model_resource_test.go +++ b/internal/provider/agents_model_resource_test.go @@ -391,6 +391,40 @@ func TestAgentsModelConfigNotEmptyValidator(t *testing.T) { }) } +func TestAgentsModelConfigNoDroppedKeysValidator(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config types.String + wantErr string + }{ + {name: "recognized config", config: types.StringValue(`{"max_output_tokens":8192,"temperature":0.7}`)}, + {name: "unknown nested field", config: types.StringValue(`{"provider_options":{"anthropic":{"bogus_setting":"x"}}}`), wantErr: "bogus_setting"}, + {name: "null", config: types.StringNull()}, + {name: "unknown", config: types.StringUnknown()}, + // Invalid and non-object JSON are other validators' problem. + {name: "invalid json", config: types.StringValue(`{`)}, + {name: "non-object json", config: types.StringValue(`[1,2]`)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + resp := &validator.StringResponse{} + agentsModelConfigNoDroppedKeysValidator{}.ValidateString(t.Context(), validator.StringRequest{ + Path: path.Root("model_config"), + ConfigValue: tt.config, + }, resp) + if tt.wantErr == "" { + require.False(t, resp.Diagnostics.HasError()) + return + } + require.True(t, resp.Diagnostics.HasError()) + require.Contains(t, resp.Diagnostics[0].Detail(), tt.wantErr) + }) + } +} + func TestAgentsModelResourceValidationDefersUnknownConfig(t *testing.T) { t.Parallel()