refactor(sandbox): streamline connector configuration injection and cleanup
- Removed the RegisterProxyConfigs function and its related logic from the sandbox initialization, simplifying the setup process. - Updated the GetComputer function to handle connector configuration injection directly within the ClaudeRunner.Stream method, ensuring a more cohesive approach to configuration management. - Introduced a new injectA2OConfig function to push connector configurations to the a2o proxy, enhancing flexibility and error handling. - Improved logging for connector configuration injection to provide better traceability during execution.
This commit is contained in:
parent
5465fb2718
commit
ff5a3e4220
4 changed files with 106 additions and 134 deletions
|
|
@ -88,11 +88,6 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options)
|
|||
}
|
||||
_ = identifier
|
||||
|
||||
// 3.5. Inject connector configs into a2o proxy via HTTP API.
|
||||
if regErr := sandboxv2.RegisterProxyConfigs(stdCtx, computer); regErr != nil {
|
||||
log.Printf("[sandbox/v2] RegisterProxyConfigs: %v (non-fatal)", regErr)
|
||||
}
|
||||
|
||||
// 4. Get Runner.
|
||||
runner, err := sandboxv2.Get(cfg.Runner.Name)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,14 @@ func buildEnv(req *types.StreamRequest, p platform) map[string]string {
|
|||
connectorID := req.Connector.ID()
|
||||
env["ANTHROPIC_BASE_URL"] = fmt.Sprintf("http://127.0.0.1:%d/c/%s", defaultA2OPort, connectorID)
|
||||
env["ANTHROPIC_API_KEY"] = "dummy"
|
||||
// Use a valid Anthropic model name to pass Claude CLI's local
|
||||
// validation. The a2o proxy ignores this and substitutes the
|
||||
// real backend model from its connector config.
|
||||
env["ANTHROPIC_MODEL"] = "claude-sonnet-4-6"
|
||||
env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = "claude-sonnet-4-6"
|
||||
env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = "claude-sonnet-4-6"
|
||||
env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = "claude-sonnet-4-6"
|
||||
env["CLAUDE_CODE_SUBAGENT_MODEL"] = "claude-sonnet-4-6"
|
||||
}
|
||||
|
||||
if thinking, ok := setting["thinking"].(map[string]interface{}); ok {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@ package claude
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
"github.com/yaoapp/kun/log"
|
||||
agentContext "github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
||||
|
|
@ -82,6 +86,11 @@ func (r *ClaudeRunner) Stream(ctx context.Context, req *types.StreamRequest, han
|
|||
|
||||
p := resolvePlatform(computer)
|
||||
|
||||
// Inject connector config into a2o proxy (best-effort, errors ignored).
|
||||
if req.Connector != nil && req.Connector.Is(connector.OPENAI) {
|
||||
injectA2OConfig(ctx, computer, req.Connector)
|
||||
}
|
||||
|
||||
if req.ChatID != "" {
|
||||
if ws := computer.Workplace(); ws != nil {
|
||||
processed, err := prepareAttachments(ctx, req.Messages, req.ChatID, ws)
|
||||
|
|
@ -130,3 +139,90 @@ func (r *ClaudeRunner) Cleanup(ctx context.Context, computer infra.Computer) err
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
type a2oConnectorConfig struct {
|
||||
Backend string `json:"backend"`
|
||||
Model string `json:"model"`
|
||||
APIKey string `json:"api_key"`
|
||||
Options map[string]interface{} `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
func buildSingleA2OConfig(conn connector.Connector) *a2oConnectorConfig {
|
||||
settings := conn.Setting()
|
||||
if settings == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := &a2oConnectorConfig{}
|
||||
|
||||
if host, ok := settings["host"].(string); ok && host != "" {
|
||||
cfg.Backend = connector.BuildAPIURL(host, "/chat/completions")
|
||||
} else if proxy, ok := settings["proxy"].(string); ok && proxy != "" {
|
||||
cfg.Backend = connector.BuildAPIURL(proxy, "/chat/completions")
|
||||
}
|
||||
if model, ok := settings["model"].(string); ok && model != "" {
|
||||
cfg.Model = model
|
||||
}
|
||||
if key, ok := settings["key"].(string); ok && key != "" {
|
||||
cfg.APIKey = key
|
||||
}
|
||||
|
||||
extra := make(map[string]interface{})
|
||||
for k, v := range settings {
|
||||
switch k {
|
||||
case "host", "model", "key", "proxy", "type":
|
||||
continue
|
||||
default:
|
||||
extra[k] = v
|
||||
}
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
cfg.Options = extra
|
||||
}
|
||||
|
||||
if cfg.Backend == "" {
|
||||
return nil
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// injectA2OConfig pushes the connector config to the a2o proxy.
|
||||
// For box (Linux container): uses sh pipe since Docker exec stdin may not work.
|
||||
// For host: uses WithStdin which works reliably on all platforms.
|
||||
// Best-effort: errors are logged and ignored.
|
||||
func injectA2OConfig(ctx context.Context, computer infra.Computer, conn connector.Connector) {
|
||||
cfg := buildSingleA2OConfig(conn)
|
||||
if cfg == nil {
|
||||
log.Trace("[claude] injectA2OConfig: no valid config for connector %s", conn.ID())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
log.Trace("[claude] injectA2OConfig: marshal error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
connID := conn.ID()
|
||||
var result *infra.ExecResult
|
||||
|
||||
info := computer.ComputerInfo()
|
||||
if info.Kind == "host" {
|
||||
result, err = computer.Exec(ctx, []string{"tai", "a2o", "config", "put", connID}, infra.WithStdin(data))
|
||||
} else {
|
||||
escaped := strings.ReplaceAll(string(data), "'", "'\\''")
|
||||
script := fmt.Sprintf("echo '%s' | tai a2o config put %s", escaped, connID)
|
||||
result, err = computer.Exec(ctx, []string{"sh", "-c", script})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Trace("[claude] injectA2OConfig: exec error (ignored): %v", err)
|
||||
return
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
log.Trace("[claude] injectA2OConfig: exit %d stderr=%s (ignored)", result.ExitCode, result.Stderr)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("[claude] injectA2OConfig: connector=%s injected ok", connID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
package sandboxv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
mathrand "math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
"github.com/yaoapp/kun/log"
|
||||
agentContext "github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
||||
|
|
@ -100,8 +95,8 @@ func ResolveNodeID(ctx *agentContext.Context, cfg *types.SandboxConfig, manager
|
|||
}
|
||||
|
||||
// GetComputer obtains or creates a Computer for the current request.
|
||||
// Connector config injection is handled via RegisterProxyConfigs after
|
||||
// the Computer is obtained.
|
||||
// Connector config is injected per-execution inside ClaudeRunner.Stream
|
||||
// via "tai a2o config put".
|
||||
// Returns the Computer, the resolved identifier, and any error.
|
||||
func GetComputer(ctx *agentContext.Context, cfg *types.SandboxConfig, manager *infra.Manager) (infra.Computer, string, error) {
|
||||
ownerID := resolveOwnerID(ctx)
|
||||
|
|
@ -388,128 +383,6 @@ func pickNodeByFilter(filter *types.ComputerFilter, image string) (string, error
|
|||
return candidates[mathrand.Intn(len(candidates))], nil
|
||||
}
|
||||
|
||||
const defaultA2OPort = 3099
|
||||
|
||||
// a2oConnectorConfig mirrors the tai/a2o ConnectorConfig struct for JSON serialization.
|
||||
type a2oConnectorConfig struct {
|
||||
Backend string `json:"backend"`
|
||||
Model string `json:"model"`
|
||||
APIKey string `json:"api_key"`
|
||||
Options map[string]interface{} `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterProxyConfigs injects all OpenAI-compatible connector configs into
|
||||
// the a2o proxy running inside the container via POST /config.
|
||||
// For host mode it uses Go's net/http directly; for box mode it uses computer.Exec.
|
||||
func RegisterProxyConfigs(ctx context.Context, computer infra.Computer) error {
|
||||
configs := buildA2OConfigs()
|
||||
if len(configs) == 0 {
|
||||
log.Trace("[sandbox/v2] RegisterProxyConfigs: no OpenAI connectors to register")
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := json.Marshal(configs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal connector configs: %w", err)
|
||||
}
|
||||
|
||||
info := computer.ComputerInfo()
|
||||
a2oURL := fmt.Sprintf("http://127.0.0.1:%d", defaultA2OPort)
|
||||
|
||||
if info.Kind == "host" {
|
||||
return registerProxyConfigsHTTP(a2oURL, data)
|
||||
}
|
||||
return registerProxyConfigsExec(ctx, computer, data)
|
||||
}
|
||||
|
||||
func buildA2OConfigs() map[string]*a2oConnectorConfig {
|
||||
configs := make(map[string]*a2oConnectorConfig)
|
||||
for id, conn := range connector.Connectors {
|
||||
if !conn.Is(connector.OPENAI) {
|
||||
continue
|
||||
}
|
||||
settings := conn.Setting()
|
||||
if settings == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg := &a2oConnectorConfig{}
|
||||
if host, ok := settings["host"].(string); ok && host != "" {
|
||||
cfg.Backend = connector.BuildAPIURL(host, "/chat/completions")
|
||||
}
|
||||
if model, ok := settings["model"].(string); ok && model != "" {
|
||||
cfg.Model = model
|
||||
}
|
||||
if key, ok := settings["key"].(string); ok && key != "" {
|
||||
cfg.APIKey = key
|
||||
}
|
||||
|
||||
extra := make(map[string]interface{})
|
||||
for k, v := range settings {
|
||||
switch k {
|
||||
case "host", "model", "key", "proxy", "type":
|
||||
continue
|
||||
default:
|
||||
extra[k] = v
|
||||
}
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
cfg.Options = extra
|
||||
}
|
||||
|
||||
if cfg.Backend != "" {
|
||||
configs[id] = cfg
|
||||
}
|
||||
}
|
||||
return configs
|
||||
}
|
||||
|
||||
func registerProxyConfigsHTTP(a2oURL string, data []byte) error {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
configURL := a2oURL + "/config"
|
||||
|
||||
for attempt := 0; attempt < 10; attempt++ {
|
||||
resp, err := client.Get(a2oURL + "/health")
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
resp, err := client.Post(configURL, "application/json", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("POST %s: %w", configURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("POST %s: status %d", configURL, resp.StatusCode)
|
||||
}
|
||||
|
||||
log.Trace("[sandbox/v2] RegisterProxyConfigs(host): injected %d bytes", len(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerProxyConfigsExec(ctx context.Context, computer infra.Computer, data []byte) error {
|
||||
escaped := strings.ReplaceAll(string(data), "'", "'\\''")
|
||||
healthCmd := fmt.Sprintf("for i in $(seq 1 20); do wget -qO- http://127.0.0.1:%d/health >/dev/null 2>&1 && break; sleep 0.5; done", defaultA2OPort)
|
||||
postCmd := fmt.Sprintf("echo '%s' | wget -qO- --post-data=@- --header='Content-Type: application/json' http://127.0.0.1:%d/config", escaped, defaultA2OPort)
|
||||
script := healthCmd + " && " + postCmd
|
||||
|
||||
result, err := computer.Exec(ctx, []string{"sh", "-c", script})
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec register proxy configs: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("register proxy configs: exit %d, stderr=%s", result.ExitCode, result.Stderr)
|
||||
}
|
||||
|
||||
log.Trace("[sandbox/v2] RegisterProxyConfigs(box): injected %d bytes, stdout=%s", len(data), result.Stdout)
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomID() string {
|
||||
b := make([]byte, 8)
|
||||
_, _ = rand.Read(b)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue