From 1e3fdf131d7e90afbfe206bfc902f97eb4dd48ce Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 24 Jun 2026 17:14:19 +0200 Subject: [PATCH 1/2] direct: fix persistent drift on model_serving_endpoints The backend always returns a defaulted config.traffic_config (100% to the served entity) even when the user does not specify one. #5693 dropped the suppression for it (and for budget_policy_id) assuming the new missing-in-remote auto-skip covered them, but both are present in RemoteType so the auto-skip never fires, and the direct engine reported a permanent update after every clean deploy. Suppress config.traffic_config as a backend_default (skip only when the user leaves it empty; real drift is still detected if it is set) and keep budget_policy_id under ignore_remote_changes (GET never echoes it). Make Changes.HasChange skip-aware so a suppressed-only change under a section no longer triggers a spurious section update (e.g. PUT /config on an unrelated email/tags update). Teach the testserver to default traffic_config like the backend so the running-endpoint acceptance test reproduces the drift locally. Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + .../catalog-name/out.second-plan.direct.json | 35 +++++++++++++++- .../name-change/out.second-plan.direct.json | 35 +++++++++++++++- .../out.second-plan.direct.json | 35 +++++++++++++++- .../recreate/route-optimized/output.txt | 11 ++++- .../schema-name/out.second-plan.direct.json | 35 +++++++++++++++- .../table-prefix/out.second-plan.direct.json | 35 +++++++++++++++- .../update/ai-gateway/out.plan.direct.json | 35 +++++++++++++++- .../out.plan.direct.json | 35 +++++++++++++++- .../update/both_gateway_and_tags/output.txt | 22 +++++++++- .../update/config/databricks.yml.tmpl | 7 ++++ .../update/config/out.plan.direct.json | 30 ++++++++++++-- .../update/config/output.txt | 40 +++++++++++++++++-- .../email-notifications/out.plan.direct.json | 35 +++++++++++++++- .../update/tags/out.plan.direct.json | 35 +++++++++++++++- bundle/deployplan/plan.go | 8 +++- bundle/direct/dresources/resources.yml | 13 ++++++ libs/testserver/serving_endpoints.go | 34 ++++++++++++++++ 18 files changed, 452 insertions(+), 29 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 1459df29473..1c6f1e32e62 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -7,6 +7,7 @@ ### CLI ### Bundles +* Fixed persistent drift on `model_serving_endpoints` in the direct engine, where a clean deploy was always followed by a spurious update because the backend-defaulted `traffic_config` was treated as a change. A suppressed field also no longer triggers an unnecessary config update on otherwise-unrelated changes. ### Dependency updates diff --git a/acceptance/bundle/resources/model_serving_endpoints/recreate/catalog-name/out.second-plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/recreate/catalog-name/out.second-plan.direct.json index 30f587571b4..a8328a003fa 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/recreate/catalog-name/out.second-plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/recreate/catalog-name/out.second-plan.direct.json @@ -51,7 +51,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -85,7 +94,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ORIGINAL_ENDPOINT_ID]" }, @@ -97,6 +115,19 @@ "new": "other_catalog", "remote": "main" }, + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/acceptance/bundle/resources/model_serving_endpoints/recreate/name-change/out.second-plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/recreate/name-change/out.second-plan.direct.json index 64624d0afc3..de30a0060bf 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/recreate/name-change/out.second-plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/recreate/name-change/out.second-plan.direct.json @@ -41,7 +41,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -70,11 +79,33 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ORIGINAL_ENDPOINT_ID]" }, "changes": { + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/acceptance/bundle/resources/model_serving_endpoints/recreate/route-optimized/out.second-plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/recreate/route-optimized/out.second-plan.direct.json index d448e4bbaf7..64db7fc0d81 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/recreate/route-optimized/out.second-plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/recreate/route-optimized/out.second-plan.direct.json @@ -34,7 +34,16 @@ "scale_to_zero_enabled": true, "workload_size": "Small" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "llama", + "served_model_name": "llama", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -59,11 +68,33 @@ "scale_to_zero_enabled": true, "workload_size": "Small" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "llama", + "served_model_name": "llama", + "traffic_percentage": 100 + } + ] + } }, "name": "[ORIGINAL_ENDPOINT_ID]" }, "changes": { + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "llama", + "served_model_name": "llama", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/acceptance/bundle/resources/model_serving_endpoints/recreate/route-optimized/output.txt b/acceptance/bundle/resources/model_serving_endpoints/recreate/route-optimized/output.txt index d9ede211b2b..68e28ba5053 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/recreate/route-optimized/output.txt +++ b/acceptance/bundle/resources/model_serving_endpoints/recreate/route-optimized/output.txt @@ -55,7 +55,16 @@ Deployment complete! "scale_to_zero_enabled": true, "workload_size": "Small" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "llama", + "served_model_name": "llama", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", diff --git a/acceptance/bundle/resources/model_serving_endpoints/recreate/schema-name/out.second-plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/recreate/schema-name/out.second-plan.direct.json index e08b0601c0e..8d8abea3b9c 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/recreate/schema-name/out.second-plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/recreate/schema-name/out.second-plan.direct.json @@ -51,7 +51,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -85,7 +94,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ORIGINAL_ENDPOINT_ID]" }, @@ -97,6 +115,19 @@ "new": "other_schema", "remote": "default" }, + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/acceptance/bundle/resources/model_serving_endpoints/recreate/table-prefix/out.second-plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/recreate/table-prefix/out.second-plan.direct.json index 8c85604c02f..117c13dde9c 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/recreate/table-prefix/out.second-plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/recreate/table-prefix/out.second-plan.direct.json @@ -51,7 +51,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -85,7 +94,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ORIGINAL_ENDPOINT_ID]" }, @@ -97,6 +115,19 @@ "new": "other_table", "remote": "my_table" }, + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/acceptance/bundle/resources/model_serving_endpoints/update/ai-gateway/out.plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/update/ai-gateway/out.plan.direct.json index f12f34c0fb5..dfc95436488 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/update/ai-gateway/out.plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/update/ai-gateway/out.plan.direct.json @@ -51,7 +51,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -85,7 +94,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ENDPOINT_ID]" }, @@ -96,6 +114,19 @@ "new": "second-inference-catalog", "remote": "first-inference-catalog" }, + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/acceptance/bundle/resources/model_serving_endpoints/update/both_gateway_and_tags/out.plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/update/both_gateway_and_tags/out.plan.direct.json index 243ff2f567f..6aab5909600 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/update/both_gateway_and_tags/out.plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/update/both_gateway_and_tags/out.plan.direct.json @@ -57,7 +57,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -97,7 +106,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ENDPOINT_ID]", "tags": [ @@ -114,6 +132,19 @@ "new": "second-inference-catalog", "remote": "first-inference-catalog" }, + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/acceptance/bundle/resources/model_serving_endpoints/update/both_gateway_and_tags/output.txt b/acceptance/bundle/resources/model_serving_endpoints/update/both_gateway_and_tags/output.txt index c4a7725fe9f..94c2b5ef2e1 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/update/both_gateway_and_tags/output.txt +++ b/acceptance/bundle/resources/model_serving_endpoints/update/both_gateway_and_tags/output.txt @@ -19,7 +19,16 @@ Deployment complete! }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } } >>> update_file.py databricks.yml value: my-team-one value: my-team-two @@ -104,7 +113,16 @@ Deployment complete! }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } } >>> [CLI] bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/model_serving_endpoints/update/config/databricks.yml.tmpl b/acceptance/bundle/resources/model_serving_endpoints/update/config/databricks.yml.tmpl index 726f5dce1f0..006e4221236 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/update/config/databricks.yml.tmpl +++ b/acceptance/bundle/resources/model_serving_endpoints/update/config/databricks.yml.tmpl @@ -17,3 +17,10 @@ resources: task: llm/v1/chat openai_config: openai_api_key: "{{secrets/test-scope/openai-key}}" + # Set explicitly so terraform and direct produce identical update requests: + # the backend defaults traffic_config, and terraform round-trips that default + # back into its config update while the direct engine sends only the user config. + traffic_config: + routes: + - served_model_name: prod + traffic_percentage: 100 diff --git a/acceptance/bundle/resources/model_serving_endpoints/update/config/out.plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/update/config/out.plan.direct.json index a6f72a06cd0..8b65f260ab1 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/update/config/out.plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/update/config/out.plan.direct.json @@ -21,7 +21,15 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ENDPOINT_ID]" } @@ -41,7 +49,15 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -70,7 +86,15 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ENDPOINT_ID]" }, diff --git a/acceptance/bundle/resources/model_serving_endpoints/update/config/output.txt b/acceptance/bundle/resources/model_serving_endpoints/update/config/output.txt index 156936aa5fb..701acd1de73 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/update/config/output.txt +++ b/acceptance/bundle/resources/model_serving_endpoints/update/config/output.txt @@ -19,7 +19,15 @@ Deployment complete! }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } } >>> update_file.py databricks.yml name: gpt-4o-mini name: gpt-5o-mini @@ -50,7 +58,15 @@ Deployment complete! }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ENDPOINT_ID]" } @@ -71,7 +87,15 @@ Deployment complete! }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } } } @@ -89,7 +113,15 @@ Deployment complete! }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } } >>> [CLI] bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/model_serving_endpoints/update/email-notifications/out.plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/update/email-notifications/out.plan.direct.json index e017672bbcf..1d4165ddaae 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/update/email-notifications/out.plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/update/email-notifications/out.plan.direct.json @@ -46,7 +46,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -80,7 +89,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "email_notifications": { "on_update_success": [ @@ -90,6 +108,19 @@ "name": "[ENDPOINT_ID]" }, "changes": { + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/acceptance/bundle/resources/model_serving_endpoints/update/tags/out.plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/update/tags/out.plan.direct.json index ab92eedef09..c158c16afec 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/update/tags/out.plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/update/tags/out.plan.direct.json @@ -47,7 +47,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "creation_timestamp": [UNIX_TIME_MILLIS][0], "creator": "[USERNAME]", @@ -82,7 +91,16 @@ }, "name": "prod" } - ] + ], + "traffic_config": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } }, "name": "[ENDPOINT_ID]", "tags": [ @@ -93,6 +111,19 @@ ] }, "changes": { + "config.traffic_config": { + "action": "skip", + "reason": "backend_default", + "remote": { + "routes": [ + { + "served_entity_name": "prod", + "served_model_name": "prod", + "traffic_percentage": 100 + } + ] + } + }, "description": { "action": "skip", "reason": "empty", diff --git a/bundle/deployplan/plan.go b/bundle/deployplan/plan.go index c7fe8a3c989..4bd5cdc5339 100644 --- a/bundle/deployplan/plan.go +++ b/bundle/deployplan/plan.go @@ -111,7 +111,10 @@ const ( ReasonDrop = "!drop" ) -// HasChange checks if there are any changes for fields with the given prefix. +// HasChange checks if there are any actionable changes for fields with the given prefix. +// Skipped changes (e.g. backend defaults or ignored remote changes) do not count, so a +// section composed only of suppressed changes is treated as unchanged. Callers use this to +// decide whether to issue a section update, and updating for a skip-only change is a no-op. // This function is path-aware and correctly handles path component boundaries. // For example: // - HasChange for path "a" matches "a" and "a.b" but not "aa" @@ -122,6 +125,9 @@ func (c *Changes) HasChange(fieldPath *structpath.PathNode) bool { } for field := range *c { + if (*c)[field].Action == Skip { + continue + } fieldNode, err := structpath.ParsePath(field) if err != nil { continue diff --git a/bundle/direct/dresources/resources.yml b/bundle/direct/dresources/resources.yml index 2001d47dc4f..38d79357df1 100644 --- a/bundle/direct/dresources/resources.yml +++ b/bundle/direct/dresources/resources.yml @@ -235,6 +235,12 @@ resources: reason: immutable - field: route_optimized reason: immutable + ignore_remote_changes: + # budget_policy_id is in ServingEndpointDetailed but GET never populates it + # (the API returns effective_budget_policy_id instead), so the remote always + # reports empty. This is not a backend default, so suppress remote changes. + - field: budget_policy_id + reason: no_update_api ignore_local_changes: - field: budget_policy_id reason: no_update_api @@ -258,6 +264,13 @@ resources: - field: route_optimized values: [false] + # https://github.com/databricks/terraform-provider-databricks/blob/4eba541abe1a9f50993ea7b9dd83874207e224a1/serving/resource_model_serving.go#L370 + # common.CustomizeSchemaPath(m, "config", "traffic_config").SetComputed() + # The backend defaults traffic_config (100% to the served entity) when the user + # does not specify one. Suppress only that default: if the user sets traffic_config + # and the remote diverges, normal drift detection still applies. + - field: config.traffic_config + registered_models: ignore_remote_changes: # Output-only timestamp/user fields populated by the backend on read. diff --git a/libs/testserver/serving_endpoints.go b/libs/testserver/serving_endpoints.go index 7706b376815..87cd2356e93 100644 --- a/libs/testserver/serving_endpoints.go +++ b/libs/testserver/serving_endpoints.go @@ -53,6 +53,36 @@ func servedModelsInputToOutput(input []serving.ServedModelInput) []serving.Serve return models } +// defaultTrafficConfig mirrors the backend: when the user does not specify a +// traffic_config, the endpoint defaults to routing 100% of traffic to its single +// served entity, and this default is echoed back on GET. This is what makes +// traffic_config a backend-managed field the bundle must not treat as drift; the +// fake has to reproduce it or the persistent-drift regression is invisible locally. +func defaultTrafficConfig(config *serving.EndpointCoreConfigOutput) { + if config == nil || config.TrafficConfig != nil { + return + } + var names []string + for _, e := range config.ServedEntities { + names = append(names, e.Name) + } + for _, m := range config.ServedModels { + names = append(names, m.Name) + } + // The backend requires an explicit traffic_config when there is more than one + // served entity, so only the single-entity default is well-defined here. + if len(names) != 1 { + return + } + config.TrafficConfig = &serving.TrafficConfig{ + Routes: []serving.Route{{ + ServedEntityName: names[0], + ServedModelName: names[0], + TrafficPercentage: 100, + }}, + } +} + // AutoCaptureConfig is the legacy inference-table API; testserver mirrors // the production conversion until callers migrate to AI Gateway inference tables. // @@ -108,6 +138,8 @@ func (s *FakeWorkspace) ServingEndpointCreate(req Request) Response { if createReq.Config.AutoCaptureConfig != nil { config.AutoCaptureConfig = autoCaptureConfigInputToOutput(createReq.Config.AutoCaptureConfig) } + + defaultTrafficConfig(config) } now := nowMilli() @@ -182,6 +214,8 @@ func (s *FakeWorkspace) ServingEndpointUpdate(req Request, name string) Response if updateReq.AutoCaptureConfig != nil { config.AutoCaptureConfig = autoCaptureConfigInputToOutput(updateReq.AutoCaptureConfig) } + + defaultTrafficConfig(config) } endpoint.Config = config From e52b56889551599c0906def8b905903fe22cbbfd Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 24 Jun 2026 17:15:08 +0200 Subject: [PATCH 2/2] Add PR number to changelog entry Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 1c6f1e32e62..38249430e34 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -7,7 +7,7 @@ ### CLI ### Bundles -* Fixed persistent drift on `model_serving_endpoints` in the direct engine, where a clean deploy was always followed by a spurious update because the backend-defaulted `traffic_config` was treated as a change. A suppressed field also no longer triggers an unnecessary config update on otherwise-unrelated changes. +* Fixed persistent drift on `model_serving_endpoints` in the direct engine, where a clean deploy was always followed by a spurious update because the backend-defaulted `traffic_config` was treated as a change. A suppressed field also no longer triggers an unnecessary config update on otherwise-unrelated changes. ([#5708](https://github.com/databricks/cli/pull/5708)) ### Dependency updates