mirror of
https://github.com/coder/coder.git
synced 2026-09-01 14:53:15 +08:00
feat(codersdk): add strict unmarshalling for ChatModelCallConfig (#27187)
Adds `ChatModelCallConfig.UnmarshalStrict`: `UnmarshalJSON` except unknown fields and trailing data are errors instead of being silently dropped. `model_config` is free-form JSON at its edges (Terraform config, API bodies), so a typo'd setting is dropped with no signal; coder/terraform-provider-coderd#388 uses this for plan-time validation. It has to live in codersdk because the custom `UnmarshalJSON` and its unexported aux struct (which defines the accepted key set, including legacy pricing aliases) make strict decoding impossible from outside the package. `UnmarshalJSON` stays lenient since it is on the read path for stored configs and older clients, where unknown keys mean version skew rather than user error. _Opened by Coder Agents on behalf of @ethanndickson._ Relates to CODAGT-797
This commit is contained in:
+24
-1
@@ -1484,6 +1484,29 @@ type ChatModelCallConfig struct {
|
||||
// UnmarshalJSON accepts both the current nested cost object and the previous
|
||||
// top-level pricing keys so legacy stored model_config JSON continues to load.
|
||||
func (c *ChatModelCallConfig) UnmarshalJSON(data []byte) error {
|
||||
return c.unmarshal(data, json.Unmarshal)
|
||||
}
|
||||
|
||||
// UnmarshalStrict is UnmarshalJSON except unknown fields are an error instead
|
||||
// of being silently dropped. Clients that accept free-form model config JSON
|
||||
// (e.g. the Terraform provider) use it to reject settings this SDK version
|
||||
// does not recognize before they are lost.
|
||||
func (c *ChatModelCallConfig) UnmarshalStrict(data []byte) error {
|
||||
return c.unmarshal(data, func(data []byte, v any) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
// Match json.Unmarshal: reject any trailing data after the value.
|
||||
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
|
||||
return xerrors.New("unexpected trailing data after JSON value")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ChatModelCallConfig) unmarshal(data []byte, decode func(data []byte, v any) error) error {
|
||||
type chatModelCallConfigAlias ChatModelCallConfig
|
||||
aux := struct {
|
||||
*chatModelCallConfigAlias
|
||||
@@ -1494,7 +1517,7 @@ func (c *ChatModelCallConfig) UnmarshalJSON(data []byte) error {
|
||||
}{
|
||||
chatModelCallConfigAlias: (*chatModelCallConfigAlias)(c),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
if err := decode(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -548,6 +548,31 @@ func TestChatModelCallConfig_UnmarshalLegacyPricing(t *testing.T) {
|
||||
require.True(t, decoded.Cost.InputPricePerMillionTokens.Equal(decimal.RequireFromString("1.5")))
|
||||
}
|
||||
|
||||
func TestChatModelCallConfig_UnmarshalStrict(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var decoded codersdk.ChatModelCallConfig
|
||||
err := decoded.UnmarshalStrict([]byte(`{
|
||||
"temperature": 0.5,
|
||||
"cost": {"input_price_per_million_tokens": "5"},
|
||||
"input_price_per_million_tokens": 1.5,
|
||||
"provider_options": {"anthropic": {"thinking": {"budget_tokens": 1024}}}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decoded.Temperature)
|
||||
require.True(t, decoded.Cost.InputPricePerMillionTokens.Equal(decimal.RequireFromString("5")))
|
||||
|
||||
err = decoded.UnmarshalStrict([]byte(`{"provider_options": {"anthropic": {"bogus_setting": true}}}`))
|
||||
require.ErrorContains(t, err, `unknown field "bogus_setting"`)
|
||||
|
||||
// Trailing data after the first value is rejected, matching json.Unmarshal.
|
||||
err = decoded.UnmarshalStrict([]byte(`{"temperature": 0.5} {"bogus_setting": true}`))
|
||||
require.ErrorContains(t, err, "trailing data")
|
||||
|
||||
// UnmarshalJSON stays lenient.
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"bogus_setting": true}`), &decoded))
|
||||
}
|
||||
|
||||
func TestChatCostSummary_JSONRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user