Skip to content

Commit e0949a3

Browse files
committed
feat(orchestrator): provider-pluggable agents + edit/show/set/reset/doctor CLI
Per-agent provider/model/base_url configuration via TOML — no code changes required. Six providers built in: NIM, OpenAI, Anthropic, Ollama, Groq, custom (any OpenAI-compatible endpoint). New subcommands (under sin-code orchestrator-run): agent-edit <name> Open in $EDITOR (auto-seeded with defaults) agent-set <name> k=v Programmatic edits (e.g. model=haiku provider=openai) agent-reset <name> Remove user override, fall back to defaults agent-show <name> Display effective merged config agent-doctor [name] Validate all agents (model exists on provider, API key present, base URL reachable) Config layer: - AgentConfig gained: Provider, BaseURL - New env vars: SIN_CODE_CONFIG_DIR, SIN_LLM_BASE_URL, SIN_LLM_API_KEY - Per-agent TOML at ~/.config/sin-code/agents/<name>/agent.toml (overrides default; XDG_CONFIG_HOME and SIN_CODE_CONFIG_DIR respected) LLM layer (cmd/sin-code/internal/llm/): - providers.go: registry of 6 providers with env-var-aware key resolution - NIMAgent renamed to LLMAgent (provider-agnostic); backwards-compat alias kept - NIM model aliases updated to currently-available NIM catalog (haiku, kimi, qwen, nemotron, gpt-oss, llama-70b, llama-3.3-70b, ...) Tests added: 30+ unit tests (providers_test.go, agent_helpers_test.go), 2 E2E testscripts (agent_set_show.txt, agent_doctor.txt). Coverage: 86.9% on internal package, 92.6% on llm package.
1 parent d9d23b8 commit e0949a3

15 files changed

Lines changed: 1450 additions & 60 deletions
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// SPDX-License-Identifier: MIT
2+
// Purpose: agent-show and agent-doctor CLI commands.
3+
package internal
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"os"
11+
"strings"
12+
"time"
13+
14+
"github.com/spf13/cobra"
15+
16+
"github.com/OpenSIN-Code/SIN-Code-Bundle/cmd/sin-code/internal/llm"
17+
"github.com/OpenSIN-Code/SIN-Code-Bundle/cmd/sin-code/internal/orchestrator"
18+
)
19+
20+
var (
21+
agDoctorOffline bool
22+
)
23+
24+
var OrchestratorAgentShowCmd = &cobra.Command{
25+
Use: "agent-show <name>",
26+
Short: "Show effective config for an agent (merged defaults + user overrides)",
27+
Args: cobra.ExactArgs(1),
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
name := strings.ToLower(args[0])
30+
merged, source, err := loadEffectiveAgent(name)
31+
if err != nil {
32+
return err
33+
}
34+
if orch2Format == "json" {
35+
out := map[string]interface{}{
36+
"agent": merged,
37+
"source": source,
38+
}
39+
return json.NewEncoder(os.Stdout).Encode(out)
40+
}
41+
fmt.Printf("Agent %s (source: %s):\n", name, source)
42+
fmt.Printf(" description: %s\n", merged.Description)
43+
fmt.Printf(" type: %s\n", merged.Type)
44+
fmt.Printf(" provider: %s\n", orDash(merged.Provider))
45+
fmt.Printf(" base_url: %s\n", orDash(merged.BaseURL))
46+
fmt.Printf(" model: %s\n", orDash(merged.Model))
47+
fmt.Printf(" max_tokens: %d\n", merged.MaxTokens)
48+
fmt.Printf(" temperature: %g\n", merged.Temperature)
49+
fmt.Printf(" system_file: %s\n", orDash(merged.SystemFile))
50+
fmt.Printf(" max_context: %d\n", merged.MaxContext)
51+
fmt.Printf(" memory_ns: %s\n", orDash(merged.MemoryNS))
52+
fmt.Printf(" retention: %d days\n", merged.RetentionDays)
53+
if len(merged.ToolsAllow) > 0 {
54+
fmt.Printf(" tools_allow: %s\n", strings.Join(merged.ToolsAllow, ", "))
55+
}
56+
if len(merged.ToolsDeny) > 0 {
57+
fmt.Printf(" tools_deny: %s\n", strings.Join(merged.ToolsDeny, ", "))
58+
}
59+
return nil
60+
},
61+
}
62+
63+
type DoctorReport struct {
64+
Agent string `json:"agent"`
65+
OK bool `json:"ok"`
66+
Issues []string `json:"issues,omitempty"`
67+
Info map[string]interface{} `json:"info,omitempty"`
68+
}
69+
70+
var OrchestratorAgentDoctorCmd = &cobra.Command{
71+
Use: "agent-doctor [name]",
72+
Short: "Validate agents: model IDs exist, API keys present, base URLs reachable",
73+
Args: cobra.MaximumNArgs(1),
74+
RunE: func(cmd *cobra.Command, args []string) error {
75+
offline := agDoctorOffline || os.Getenv("SIN_LLM_OFFLINE") == "1"
76+
var filterName string
77+
if len(args) == 1 {
78+
filterName = strings.ToLower(args[0])
79+
}
80+
agents, err := loadAllEffectiveAgents()
81+
if err != nil {
82+
return err
83+
}
84+
if filterName != "" {
85+
filtered := []orchestrator.AgentConfig{}
86+
for _, a := range agents {
87+
if a.Name == filterName {
88+
filtered = append(filtered, a)
89+
}
90+
}
91+
agents = filtered
92+
}
93+
report := runDoctor(agents, offline)
94+
if orch2Format == "json" {
95+
return json.NewEncoder(os.Stdout).Encode(report)
96+
}
97+
printDoctor(report)
98+
failed := 0
99+
for _, r := range report {
100+
if !r.OK {
101+
failed++
102+
}
103+
}
104+
if failed > 0 {
105+
return fmt.Errorf("%d/%d agents have issues", failed, len(report))
106+
}
107+
return nil
108+
},
109+
}
110+
111+
func init() {
112+
OrchestratorAgentDoctorCmd.Flags().BoolVar(&agDoctorOffline, "offline", false, "Skip /v1/models network check")
113+
OrchestratorRunCmd.AddCommand(OrchestratorAgentShowCmd)
114+
OrchestratorRunCmd.AddCommand(OrchestratorAgentDoctorCmd)
115+
}
116+
117+
func runDoctor(agents []orchestrator.AgentConfig, offline bool) []DoctorReport {
118+
out := make([]DoctorReport, 0, len(agents))
119+
for _, a := range agents {
120+
rep := DoctorReport{Agent: a.Name, OK: true, Info: map[string]interface{}{}}
121+
providerName := a.Provider
122+
if providerName == "" {
123+
providerName = "nim"
124+
}
125+
prov, perr := llm.LookupProvider(providerName)
126+
if perr != nil {
127+
rep.OK = false
128+
rep.Issues = append(rep.Issues, "unknown provider: "+providerName)
129+
out = append(out, rep)
130+
continue
131+
}
132+
rep.Info["provider"] = providerName
133+
rep.Info["provider_description"] = prov.Description
134+
135+
baseURL := a.BaseURL
136+
if baseURL == "" {
137+
baseURL = prov.BaseURL
138+
}
139+
if baseURL == "" {
140+
rep.OK = false
141+
rep.Issues = append(rep.Issues, "no base_url configured")
142+
} else {
143+
rep.Info["base_url"] = baseURL
144+
}
145+
146+
if prov.APIKeyEnv != "" {
147+
if os.Getenv(prov.APIKeyEnv) == "" && os.Getenv("SIN_LLM_API_KEY") == "" {
148+
rep.OK = false
149+
rep.Issues = append(rep.Issues, fmt.Sprintf("missing API key: set %s or SIN_LLM_API_KEY", prov.APIKeyEnv))
150+
} else {
151+
rep.Info["api_key_env"] = prov.APIKeyEnv
152+
}
153+
}
154+
155+
model := a.Model
156+
if model == "" {
157+
model = prov.DefaultModel
158+
}
159+
rep.Info["model"] = model
160+
161+
if !offline && baseURL != "" {
162+
models, merr := fetchModels(baseURL, prov.APIKeyEnv)
163+
if merr != nil {
164+
rep.Issues = append(rep.Issues, "could not fetch /v1/models: "+merr.Error())
165+
} else {
166+
if !stringInList(models, model) {
167+
rep.OK = false
168+
rep.Issues = append(rep.Issues, fmt.Sprintf("model %q not in provider's model list", model))
169+
}
170+
rep.Info["models_available"] = len(models)
171+
}
172+
}
173+
out = append(out, rep)
174+
}
175+
return out
176+
}
177+
178+
func fetchModels(baseURL, apiKeyEnv string) ([]string, error) {
179+
apiKey := ""
180+
if apiKeyEnv != "" {
181+
apiKey = os.Getenv(apiKeyEnv)
182+
}
183+
if apiKey == "" {
184+
apiKey = os.Getenv("SIN_LLM_API_KEY")
185+
}
186+
req, err := http.NewRequestWithContext(context.Background(), "GET", strings.TrimRight(baseURL, "/")+"/models", nil)
187+
if err != nil {
188+
return nil, err
189+
}
190+
if apiKey != "" {
191+
req.Header.Set("Authorization", "Bearer "+apiKey)
192+
}
193+
client := &http.Client{Timeout: 5 * time.Second}
194+
resp, err := client.Do(req)
195+
if err != nil {
196+
return nil, err
197+
}
198+
defer resp.Body.Close()
199+
if resp.StatusCode != 200 {
200+
return nil, fmt.Errorf("status %d", resp.StatusCode)
201+
}
202+
var out struct {
203+
Data []struct {
204+
ID string `json:"id"`
205+
} `json:"data"`
206+
}
207+
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
208+
return nil, err
209+
}
210+
ids := make([]string, 0, len(out.Data))
211+
for _, d := range out.Data {
212+
ids = append(ids, d.ID)
213+
}
214+
return ids, nil
215+
}
216+
217+
func stringInList(list []string, s string) bool {
218+
for _, v := range list {
219+
if v == s {
220+
return true
221+
}
222+
}
223+
return false
224+
}
225+
226+
func printDoctor(reports []DoctorReport) {
227+
fmt.Printf("Doctor — %d agent(s):\n\n", len(reports))
228+
for _, r := range reports {
229+
icon := "✓"
230+
if !r.OK {
231+
icon = "✗"
232+
}
233+
fmt.Printf("%s %s\n", icon, r.Agent)
234+
if prov, ok := r.Info["provider"].(string); ok {
235+
fmt.Printf(" provider: %s\n", prov)
236+
}
237+
if base, ok := r.Info["base_url"].(string); ok {
238+
fmt.Printf(" base_url: %s\n", base)
239+
}
240+
if model, ok := r.Info["model"].(string); ok {
241+
fmt.Printf(" model: %s\n", model)
242+
}
243+
if cnt, ok := r.Info["models_available"].(int); ok {
244+
fmt.Printf(" available: %d models on provider\n", cnt)
245+
}
246+
for _, issue := range r.Issues {
247+
fmt.Printf(" ! %s\n", issue)
248+
}
249+
fmt.Println()
250+
}
251+
}

0 commit comments

Comments
 (0)