diff --git a/agentteams-controller/cmd/agt/update.go b/agentteams-controller/cmd/agt/update.go index e8d1685db..5a5c6eb9b 100644 --- a/agentteams-controller/cmd/agt/update.go +++ b/agentteams-controller/cmd/agt/update.go @@ -184,11 +184,12 @@ Create or update each Worker separately to configure its runtime fields.`, func updateManagerCmd() *cobra.Command { var ( - name string - model string - runtime string - image string - soul string + name string + model string + modelProvider string + runtime string + image string + soul string ) cmd := &cobra.Command{ @@ -197,6 +198,7 @@ func updateManagerCmd() *cobra.Command { Long: `Update an existing Manager resource. Only specified fields are changed. agt update manager --name default --model claude-sonnet-4-6 + agt update manager --name default --model qwen3.6-plus --model-provider= agt update manager --name default --image agentteams/agentteams-manager:v1.2.0 To update CPU/memory resources, use a YAML manifest and pass it with 'agt apply -f manager.yaml'.`, RunE: func(cmd *cobra.Command, args []string) error { @@ -206,6 +208,9 @@ func updateManagerCmd() *cobra.Command { req := map[string]interface{}{} setIfNotEmpty(req, "model", model) + if cmd.Flags().Changed("model-provider") { + req["modelProvider"] = modelProvider + } setIfNotEmpty(req, "runtime", runtime) setIfNotEmpty(req, "image", image) setIfNotEmpty(req, "soul", soul) @@ -226,6 +231,7 @@ func updateManagerCmd() *cobra.Command { cmd.Flags().StringVar(&name, "name", "", "Manager name (required)") cmd.Flags().StringVar(&model, "model", "", "LLM model ID") + cmd.Flags().StringVar(&modelProvider, "model-provider", "", "LLM provider name (empty clears the binding)") cmd.Flags().StringVar(&runtime, "runtime", "", "Agent runtime (openclaw|copaw|hermes|openhuman)") cmd.Flags().StringVar(&image, "image", "", "Container image override") cmd.Flags().StringVar(&soul, "soul", "", "Manager SOUL.md content") diff --git a/agentteams-controller/cmd/agt/update_test.go b/agentteams-controller/cmd/agt/update_test.go new file mode 100644 index 000000000..226f9ea71 --- /dev/null +++ b/agentteams-controller/cmd/agt/update_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestUpdateManagerCanClearModelProvider(t *testing.T) { + var body map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut || r.URL.Path != "/api/v1/managers/default" { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + return + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Errorf("decode request body: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + t.Setenv("AGENTTEAMS_CONTROLLER_URL", server.URL) + + cmd := updateManagerCmd() + cmd.SetArgs([]string{"--name", "default", "--model", "known-good-model", "--model-provider="}) + if err := cmd.Execute(); err != nil { + t.Fatalf("execute update manager: %v", err) + } + + if got := body["model"]; got != "known-good-model" { + t.Fatalf("model=%v, want known-good-model", got) + } + provider, ok := body["modelProvider"] + if !ok { + t.Fatal("modelProvider is missing from request") + } + if provider != "" { + t.Fatalf("modelProvider=%v, want empty", provider) + } +} diff --git a/agentteams-controller/internal/server/resource_handler.go b/agentteams-controller/internal/server/resource_handler.go index bfbc010db..bd83a6dfe 100644 --- a/agentteams-controller/internal/server/resource_handler.go +++ b/agentteams-controller/internal/server/resource_handler.go @@ -694,8 +694,8 @@ func (h *ResourceHandler) UpdateManager(w http.ResponseWriter, r *http.Request) if req.Model != "" { mgr.Spec.Model = req.Model } - if req.ModelProvider != "" { - mgr.Spec.ModelProvider = req.ModelProvider + if req.ModelProvider != nil { + mgr.Spec.ModelProvider = *req.ModelProvider } if req.Runtime != "" { mgr.Spec.Runtime = req.Runtime diff --git a/agentteams-controller/internal/server/resource_handler_test.go b/agentteams-controller/internal/server/resource_handler_test.go index d0d6b06d7..c8dc267ff 100644 --- a/agentteams-controller/internal/server/resource_handler_test.go +++ b/agentteams-controller/internal/server/resource_handler_test.go @@ -645,6 +645,41 @@ func TestCreateAndUpdateManagerPersistsModelProvider(t *testing.T) { } } +func TestUpdateManagerClearsModelProviderWhenExplicitlyEmpty(t *testing.T) { + scheme := newServerTestScheme(t) + manager := &v1beta1.Manager{ + ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"}, + Spec: v1beta1.ManagerSpec{ + Model: "custom-model", + ModelProvider: "deleted-provider", + }, + } + k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(manager).Build() + handler := NewResourceHandler(k8sClient, "default", nil, "") + + updateReq := httptest.NewRequest(http.MethodPut, "/api/v1/managers/default", bytes.NewReader([]byte(`{ + "model":"known-good-model", + "modelProvider":"" + }`))) + updateReq.SetPathValue("name", "default") + updateRec := httptest.NewRecorder() + handler.UpdateManager(updateRec, updateReq) + if updateRec.Code != http.StatusOK { + t.Fatalf("expected update status %d, got %d: %s", http.StatusOK, updateRec.Code, updateRec.Body.String()) + } + + var updated v1beta1.Manager + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "default", Namespace: "default"}, &updated); err != nil { + t.Fatalf("get updated manager: %v", err) + } + if updated.Spec.Model != "known-good-model" { + t.Fatalf("updated manager model=%q, want known-good-model", updated.Spec.Model) + } + if updated.Spec.ModelProvider != "" { + t.Fatalf("updated manager modelProvider=%q, want empty", updated.Spec.ModelProvider) + } +} + func TestCreateTeamRequiresExactlyOneLeaderReference(t *testing.T) { scheme := newServerTestScheme(t) leadOne := &v1beta1.Worker{ObjectMeta: metav1.ObjectMeta{Name: "lead-one", Namespace: "default"}} diff --git a/agentteams-controller/internal/server/types.go b/agentteams-controller/internal/server/types.go index 100793389..166580c06 100644 --- a/agentteams-controller/internal/server/types.go +++ b/agentteams-controller/internal/server/types.go @@ -194,7 +194,7 @@ type CreateManagerRequest struct { type UpdateManagerRequest struct { Model string `json:"model,omitempty"` - ModelProvider string `json:"modelProvider,omitempty"` + ModelProvider *string `json:"modelProvider,omitempty"` Runtime string `json:"runtime,omitempty"` Image string `json:"image,omitempty"` Soul string `json:"soul,omitempty"`