Skip to content

Commit 5f61bdc

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into develop
2 parents b9433de + 16aea12 commit 5f61bdc

12 files changed

Lines changed: 655 additions & 65 deletions

File tree

cmd/aima/infra.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ func buildAgentStatusPayload(ctx context.Context, llmClient *agent.OpenAIClient,
105105
return json.Marshal(payload)
106106
}
107107

108+
func buildFreshAgentStatusPayload(ctx context.Context, db *state.DB, llmClient *agent.OpenAIClient, localAPIKey, toolMode string, activeRuns int) (json.RawMessage, error) {
109+
if db != nil && llmClient != nil {
110+
reloadLLMSettings(ctx, db, llmClient, localAPIKey)
111+
}
112+
return buildAgentStatusPayload(ctx, llmClient, toolMode, activeRuns)
113+
}
114+
108115
func loadLLMSettings(ctx context.Context, db *state.DB) llmSettings {
109116
settings := llmSettings{
110117
Endpoint: defaultLLMEndpoint(),

cmd/aima/main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ func run() error {
183183
if skipPerms {
184184
ctx = context.WithValue(ctx, ctxKeySkipPerms, true)
185185
}
186+
reloadLLMSettings(ctx, db, llmClient, proxyServer.APIKey())
186187
result, sid, toolCalls, err := dispatcher.Ask(ctx, query, agent.DispatchOption{SessionID: sessionID})
187188
if err != nil {
188189
return nil, "", err
@@ -198,7 +199,7 @@ func run() error {
198199
if explorationMgr != nil {
199200
activeRuns = explorationMgr.ActiveCount()
200201
}
201-
return buildAgentStatusPayload(ctx, llmClient, goAgent.ToolMode(), activeRuns)
202+
return buildFreshAgentStatusPayload(ctx, db, llmClient, proxyServer.APIKey(), goAgent.ToolMode(), activeRuns)
202203
}
203204
// 9c. Wire rollback tools
204205
deps.RollbackList = func(ctx context.Context) (json.RawMessage, error) {
@@ -270,6 +271,7 @@ func run() error {
270271
return json.Marshal(map[string]string{"status": "ok"})
271272
},
272273
DispatchAskStream: func(ctx context.Context, query, sessionID string, cb func(string, []byte)) (json.RawMessage, error) {
274+
reloadLLMSettings(ctx, db, llmClient, proxyServer.APIKey())
273275
var streamCB agent.StreamCallback
274276
if cb != nil {
275277
streamCB = func(ev agent.StreamEvent) {
@@ -299,6 +301,9 @@ func run() error {
299301
}
300302
return raw, nil
301303
},
304+
APIKey: func(context.Context) string {
305+
return proxyServer.APIKey()
306+
},
302307
})
303308

304309
// AIMA inference HTTP routes and OpenClaw config sync share backend/catalog readers,

cmd/aima/main_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1972,6 +1972,66 @@ func TestReloadLLMSettings_FallsBackToServeAPIKeyForLocalProxy(t *testing.T) {
19721972
}
19731973
}
19741974

1975+
func TestBuildFreshAgentStatusPayloadReloadsModelFromDB(t *testing.T) {
1976+
ctx := context.Background()
1977+
db, err := state.Open(ctx, ":memory:")
1978+
if err != nil {
1979+
t.Fatalf("Open: %v", err)
1980+
}
1981+
defer db.Close()
1982+
1983+
t.Setenv("AIMA_LLM_ENDPOINT", "")
1984+
t.Setenv("AIMA_LLM_MODEL", "")
1985+
t.Setenv("AIMA_API_KEY", "")
1986+
t.Setenv("AIMA_LLM_USER_AGENT", "")
1987+
t.Setenv("AIMA_LLM_EXTRA_PARAMS", "")
1988+
1989+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1990+
switch r.URL.Path {
1991+
case "/status":
1992+
w.Header().Set("Content-Type", "application/json")
1993+
_, _ = w.Write([]byte(`{"status":"ok","models":[{"model_name":"new-model","ready":true}]}`))
1994+
case "/v1/models":
1995+
w.Header().Set("Content-Type", "application/json")
1996+
_, _ = w.Write([]byte(`{"data":[{"id":"new-model"}]}`))
1997+
default:
1998+
http.NotFound(w, r)
1999+
}
2000+
}))
2001+
defer server.Close()
2002+
2003+
if err := db.SetConfig(ctx, "llm.endpoint", server.URL+"/v1"); err != nil {
2004+
t.Fatalf("SetConfig endpoint: %v", err)
2005+
}
2006+
if err := db.SetConfig(ctx, "llm.model", "new-model"); err != nil {
2007+
t.Fatalf("SetConfig model: %v", err)
2008+
}
2009+
2010+
client := agent.NewOpenAIClient(server.URL+"/v1", agent.WithModel("old-model"))
2011+
data, err := buildFreshAgentStatusPayload(ctx, db, client, "", "enabled", 0)
2012+
if err != nil {
2013+
t.Fatalf("buildFreshAgentStatusPayload: %v", err)
2014+
}
2015+
2016+
var payload struct {
2017+
LLMRoute struct {
2018+
ConfiguredModel string `json:"configured_model"`
2019+
Selected struct {
2020+
Model string `json:"model"`
2021+
} `json:"selected"`
2022+
} `json:"llm_route"`
2023+
}
2024+
if err := json.Unmarshal(data, &payload); err != nil {
2025+
t.Fatalf("unmarshal payload: %v", err)
2026+
}
2027+
if payload.LLMRoute.ConfiguredModel != "new-model" {
2028+
t.Fatalf("configured model = %q, want new-model", payload.LLMRoute.ConfiguredModel)
2029+
}
2030+
if payload.LLMRoute.Selected.Model != "new-model" {
2031+
t.Fatalf("selected model = %q, want new-model", payload.LLMRoute.Selected.Model)
2032+
}
2033+
}
2034+
19752035
func TestMCPToolAdapter_SystemConfigReadAllowedWriteBlocked(t *testing.T) {
19762036
s := mcp.NewServer()
19772037
called := 0

cmd/aima/runtime_select.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,41 @@ func findMatchingDeployments(ctx context.Context, query string, suppress func(*r
245245
return matches
246246
}
247247

248+
func findExactDeploymentNameMatches(ctx context.Context, query string, suppress func(*runtime.DeploymentStatus) bool, rts ...runtime.Runtime) []matchedDeployment {
249+
matches := make([]matchedDeployment, 0)
250+
seen := make(map[string]struct{})
251+
for _, rt := range uniqueRuntimes(rts...) {
252+
if status, err := rt.Status(ctx, query); err == nil && status != nil && strings.EqualFold(status.Name, query) {
253+
if suppress == nil || !suppress(status) {
254+
key := fmt.Sprintf("%p|%s", rt, status.Name)
255+
if _, ok := seen[key]; !ok {
256+
seen[key] = struct{}{}
257+
matches = append(matches, matchedDeployment{Runtime: rt, Status: status})
258+
}
259+
}
260+
}
261+
statuses, err := rt.List(ctx)
262+
if err != nil {
263+
continue
264+
}
265+
for _, status := range statuses {
266+
if status == nil || !strings.EqualFold(status.Name, query) {
267+
continue
268+
}
269+
if suppress != nil && suppress(status) {
270+
continue
271+
}
272+
key := fmt.Sprintf("%p|%s", rt, status.Name)
273+
if _, ok := seen[key]; ok {
274+
continue
275+
}
276+
seen[key] = struct{}{}
277+
matches = append(matches, matchedDeployment{Runtime: rt, Status: status})
278+
}
279+
}
280+
return matches
281+
}
282+
248283
func summarizeMatchedDeployments(matches []matchedDeployment) string {
249284
if len(matches) == 0 {
250285
return ""
@@ -327,3 +362,25 @@ func shouldReuseExistingDeployment(existing *runtime.DeploymentStatus, engineTyp
327362
}
328363
return len(configOverrides) == 0
329364
}
365+
366+
func findReusableDeployment(ctx context.Context, deployName, modelName, engineType, slot string, configOverrides map[string]any, suppress func(*runtime.DeploymentStatus) bool, rts ...runtime.Runtime) (*runtime.DeploymentStatus, error) {
367+
matches := findMatchingDeployments(ctx, modelName, suppress, rts...)
368+
reusable := make([]matchedDeployment, 0, len(matches))
369+
for _, match := range matches {
370+
if shouldReuseExistingDeployment(match.Status, engineType, slot, configOverrides) {
371+
reusable = append(reusable, match)
372+
}
373+
}
374+
if len(reusable) > 1 {
375+
return nil, fmt.Errorf("model %q already has multiple active deployments (%s); use an exact deployment name to undeploy the extra instance", modelName, summarizeMatchedDeployments(reusable))
376+
}
377+
if len(reusable) == 1 {
378+
return reusable[0].Status, nil
379+
}
380+
381+
existing, err := findDeploymentStatus(ctx, deployName, suppress, rts...)
382+
if err != nil || !shouldReuseExistingDeployment(existing, engineType, slot, configOverrides) {
383+
return nil, nil
384+
}
385+
return existing, nil
386+
}

cmd/aima/tooldeps_deploy.go

Lines changed: 63 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -153,40 +153,46 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
153153
}
154154
deployName := knowledge.SanitizePodName(modelName + "-" + resolved.Engine)
155155
suppressRecentlyDeleted := loadDeletedDeploymentSuppressor(ctx, db)
156-
if existing, _ := findDeploymentStatus(ctx, deployName, suppressRecentlyDeleted, activeRt, rt, nativeRt, dockerRt); existing != nil {
157-
if shouldReuseExistingDeployment(existing, engineType, slot, configOverrides) {
158-
proxyServer.RegisterBackend(modelName, &proxy.Backend{
159-
ModelName: modelName,
160-
UpstreamModel: deploymentUpstreamModel(existing, upstreamModel),
161-
EngineType: resolved.Engine,
162-
Address: existing.Address,
163-
Ready: existing.Ready,
164-
ParameterCount: firstNonEmpty(existing.Labels[proxy.LabelParameterCount], catalogModelParameterCount(cat, modelName)),
165-
ContextWindowTokens: firstPositiveInt(contextWindowFromStatus(existing), contextWindowFromResolvedConfig(resolved.Config)),
166-
})
167-
runtimeName := activeRt.Name()
168-
if existing.Runtime != "" {
169-
runtimeName = existing.Runtime
170-
}
171-
status := "deploying"
172-
if existing.Ready {
173-
status = "ready"
174-
}
175-
result := map[string]any{
176-
"name": deployName,
177-
"model": modelName,
178-
"engine": resolved.Engine,
179-
"slot": resolved.Slot,
180-
"status": status,
181-
"phase": existing.Phase,
182-
"runtime": runtimeName,
183-
"config": resolved.Config,
184-
}
185-
if existing.Address != "" {
186-
result["address"] = existing.Address
187-
}
188-
return json.Marshal(result)
189-
}
156+
if existing, err := findReusableDeployment(ctx, deployName, modelName, engineType, slot, configOverrides, suppressRecentlyDeleted, activeRt, rt, nativeRt, dockerRt); err != nil {
157+
return nil, err
158+
} else if existing != nil {
159+
proxyServer.RegisterBackend(modelName, &proxy.Backend{
160+
ModelName: modelName,
161+
UpstreamModel: deploymentUpstreamModel(existing, upstreamModel),
162+
EngineType: resolved.Engine,
163+
Address: existing.Address,
164+
Ready: existing.Ready,
165+
ParameterCount: firstNonEmpty(existing.Labels[proxy.LabelParameterCount], catalogModelParameterCount(cat, modelName)),
166+
ContextWindowTokens: firstPositiveInt(contextWindowFromStatus(existing), contextWindowFromResolvedConfig(resolved.Config)),
167+
})
168+
runtimeName := activeRt.Name()
169+
if existing.Runtime != "" {
170+
runtimeName = existing.Runtime
171+
}
172+
status := "deploying"
173+
if existing.Ready {
174+
status = "ready"
175+
}
176+
existingName := firstNonEmpty(existing.Name, deployName)
177+
result := map[string]any{
178+
"name": existingName,
179+
"model": modelName,
180+
"engine": resolved.Engine,
181+
"slot": resolved.Slot,
182+
"status": status,
183+
"phase": existing.Phase,
184+
"runtime": runtimeName,
185+
"config": resolved.Config,
186+
"reused": true,
187+
"message": fmt.Sprintf("deployment %s already exists; returning current deployment", existingName),
188+
}
189+
if existing.Address != "" {
190+
result["address"] = existing.Address
191+
}
192+
if err := setActiveLLMModelConfig(ctx, db, modelName); err != nil {
193+
return nil, err
194+
}
195+
return json.Marshal(result)
190196
}
191197
// Pre-flight: ensure image is available in containerd for K3S deployments.
192198
// Auto-import from Docker or pre-pull from registries if needed.
@@ -296,6 +302,9 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
296302
ParameterCount: catalogModelParameterCount(cat, modelName),
297303
ContextWindowTokens: contextWindowFromResolvedConfig(resolved.Config),
298304
})
305+
if err := setActiveLLMModelConfig(ctx, db, modelName); err != nil {
306+
return nil, err
307+
}
299308
result := map[string]any{
300309
"name": deployName,
301310
"model": modelName, "engine": resolved.Engine,
@@ -450,7 +459,10 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
450459
}
451460

452461
deps.DeployDelete = func(ctx context.Context, name string) error {
453-
matches := findMatchingDeployments(ctx, name, nil, rt, nativeRt, dockerRt)
462+
matches := findExactDeploymentNameMatches(ctx, name, nil, rt, nativeRt, dockerRt)
463+
if len(matches) == 0 {
464+
matches = findMatchingDeployments(ctx, name, nil, rt, nativeRt, dockerRt)
465+
}
454466
if len(matches) == 0 {
455467
// Backward-compatible fallback: some UI paths pass the model name
456468
// instead of the concrete deployment name.
@@ -463,6 +475,9 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
463475
if len(matches) == 0 {
464476
return fmt.Errorf("deployment %q not found", name)
465477
}
478+
if len(matches) > 1 {
479+
return fmt.Errorf("deployment %q is ambiguous; matches: %s; use an exact deployment name", name, summarizeMatchedDeployments(matches))
480+
}
466481

467482
for _, match := range matches {
468483
if match.Status == nil {
@@ -481,7 +496,6 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
481496
deletedAt := time.Now()
482497
tombstoneKeys := []string{name}
483498
seenKeys := map[string]struct{}{normalizeDeletedDeploymentKey(name): {}}
484-
verificationQueries := []string{name}
485499
rememberKey := func(key string) {
486500
norm := normalizeDeletedDeploymentKey(key)
487501
if norm == "" {
@@ -493,18 +507,6 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
493507
seenKeys[norm] = struct{}{}
494508
tombstoneKeys = append(tombstoneKeys, key)
495509
}
496-
rememberVerificationQuery := func(key string) {
497-
norm := normalizeDeletedDeploymentKey(key)
498-
if norm == "" {
499-
return
500-
}
501-
for _, existing := range verificationQueries {
502-
if normalizeDeletedDeploymentKey(existing) == norm {
503-
return
504-
}
505-
}
506-
verificationQueries = append(verificationQueries, key)
507-
}
508510

509511
for _, match := range matches {
510512
if match.Runtime == nil || match.Status == nil {
@@ -516,14 +518,10 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
516518
rememberKey(match.Status.Name)
517519
modelKey := deploymentModelKey(match.Status)
518520
rememberKey(modelKey)
519-
rememberVerificationQuery(match.Status.Name)
520-
rememberVerificationQuery(modelKey)
521521
}
522522

523-
for _, query := range verificationQueries {
524-
if remaining := findMatchingDeployments(ctx, query, nil, rt, nativeRt, dockerRt); len(remaining) > 0 {
525-
return fmt.Errorf("delete deployment %q: deployment still active after delete (%s)", name, summarizeMatchedDeployments(remaining))
526-
}
523+
if remaining := findExactDeploymentNameMatches(ctx, matches[0].Status.Name, nil, rt, nativeRt, dockerRt); len(remaining) > 0 {
524+
return fmt.Errorf("delete deployment %q: deployment still active after delete (%s)", name, summarizeMatchedDeployments(remaining))
527525
}
528526

529527
for _, key := range tombstoneKeys {
@@ -605,6 +603,17 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
605603
}
606604
}
607605

606+
func setActiveLLMModelConfig(ctx context.Context, db *state.DB, modelName string) error {
607+
modelName = strings.TrimSpace(modelName)
608+
if db == nil || modelName == "" {
609+
return nil
610+
}
611+
if err := db.SetConfig(ctx, "llm.model", modelName); err != nil {
612+
return fmt.Errorf("update llm.model after deploy: %w", err)
613+
}
614+
return nil
615+
}
616+
608617
func catalogModelParameterCount(cat *knowledge.Catalog, name string) string {
609618
if cat == nil {
610619
return ""

0 commit comments

Comments
 (0)