fix(opencode): x-opencode-directory, Content-Length, warmup /api/model; serve без явного Env
This commit is contained in:
@@ -24,7 +24,7 @@ var Names = []string{"analyst", "dev", "reviewer", "chat", "postmortem"}
|
||||
// WriteTo распаковывает всех встроенных агентов в каталог dir/agent
|
||||
// (создаёт его). Файлы перезаписываются — встроенная копия всегда актуальна.
|
||||
func WriteTo(dir string) error {
|
||||
agentsDir := filepath.Join(dir, "agent")
|
||||
agentsDir := filepath.Join(dir, "agents")
|
||||
for _, name := range Names {
|
||||
data, err := FS.ReadFile(name + ".md")
|
||||
if err != nil {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
@@ -26,6 +25,7 @@ import (
|
||||
type Client struct {
|
||||
BaseURL string // http://host:port (без завершающего слеша)
|
||||
Password string // basic auth (username "opencode")
|
||||
Directory string // каталог сервера — идёт заголовком x-opencode-directory
|
||||
Debug bool // включать отладочные логи API-вызовов (log.level=debug)
|
||||
http *http.Client // единый клиент: все операции быстрые (нет блокирующего Send)
|
||||
}
|
||||
@@ -56,14 +56,15 @@ func (c *Client) do(ctx context.Context, method, path, op string, body []byte) (
|
||||
if err != nil {
|
||||
return nil, &ClientErr{Op: "connect", Err: err}
|
||||
}
|
||||
if body != nil {
|
||||
req.ContentLength = int64(len(body))
|
||||
}
|
||||
if c.Password != "" {
|
||||
req.SetBasicAuth("opencode", c.Password)
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if c.Debug {
|
||||
log.Printf("opencode api debug: %s -> %s %s%s", op, method, c.BaseURL, path)
|
||||
if c.Directory != "" {
|
||||
req.Header.Set("x-opencode-directory", c.Directory)
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -75,14 +76,8 @@ func (c *Client) do(ctx context.Context, method, path, op string, body []byte) (
|
||||
return nil, &ClientErr{Op: "connect", Err: err}
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
if c.Debug {
|
||||
log.Printf("opencode api debug: %s response: status %d", op, resp.StatusCode)
|
||||
}
|
||||
return nil, &ClientErr{Op: op, Err: fmt.Errorf("status %d: %s", resp.StatusCode, truncateStr(string(b), 300))}
|
||||
}
|
||||
if c.Debug {
|
||||
log.Printf("opencode api debug: %s response (%d bytes)", op, len(b))
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -130,6 +125,12 @@ func (c *Client) CreateSession(ctx context.Context, agent string) (string, error
|
||||
if out.Data.ID == "" {
|
||||
return "", &ClientErr{Op: "create", Err: fmt.Errorf("пустой id сессии")}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
// Холостой вызов /api/model: прогревает сервер (модель/провайдеры),
|
||||
// чтобы первый промпт не спотыкался о «тёплый» старт.
|
||||
_, _ = c.do(ctx, http.MethodGet, "/api/model", "models-warmup", nil)
|
||||
time.Sleep(2 * time.Second)
|
||||
_, _ = c.do(ctx, http.MethodGet, "/api/model", "models-warmup", nil)
|
||||
return out.Data.ID, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ func (r *Runner) Run(ctx context.Context, prompt, cwd, agent, sessionID string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{BaseURL: srv.Addr(), Password: srv.Password, Debug: r.Debug}
|
||||
c := &Client{BaseURL: srv.Addr(), Password: srv.Password, Directory: srv.Dir, Debug: r.Debug}
|
||||
|
||||
// Сессия: заданная (resume) или новая. Модель НЕ выбираем — opencode сам
|
||||
// берёт её по умолчанию из своего глобального конфига.
|
||||
|
||||
@@ -146,7 +146,7 @@ func TestRun_ContextCancel(t *testing.T) {
|
||||
done <- res
|
||||
errCh <- err
|
||||
}()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
time.Sleep(6000 * time.Millisecond)
|
||||
cancel()
|
||||
res := <-done
|
||||
if err := <-errCh; err != nil {
|
||||
|
||||
@@ -120,6 +120,8 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
s.done = done
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Printf("opencode serve: spawn dir=%s HOME=%q USERPROFILE=%q OPENCODE_CONFIG=%q (serve наследует окружение родителя)", cmd.Dir, os.Getenv("HOME"), os.Getenv("USERPROFILE"), s.ConfigPath)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("opencode serve: start %v: %w", s.Bin, err)
|
||||
}
|
||||
@@ -143,24 +145,8 @@ func (s *Server) serveCmd(ctx context.Context) *exec.Cmd {
|
||||
// Своя process-group: чтобы killGroup (по pgid) убивал только сервер и его
|
||||
// дочерние процессы, а не чужой процесс (например, тест-реннер).
|
||||
setpgid(cmd)
|
||||
env := append(os.Environ(),
|
||||
"OPENCODE_DISABLE_AUTOUPDATE=1",
|
||||
"OPENCODE_DISABLE_MODELS_FETCH=1")
|
||||
// Гарантированно указываем глобальный конфиг opencode: serve обязан
|
||||
// подхватить модель/провайдеров из него (иначе возможен фоллбэк на
|
||||
// случайную модель из каталога).
|
||||
if s.ConfigPath != "" {
|
||||
env = append(env, "OPENCODE_CONFIG="+s.ConfigPath)
|
||||
}
|
||||
// Агенты (analyst/dev/...) opencode находит сам через project-каталог
|
||||
// .opencode (см. internal/agents).
|
||||
if s.DBPath != "" {
|
||||
env = append(env, "OPENCODE_DB="+s.DBPath)
|
||||
}
|
||||
if s.Password != "" {
|
||||
env = append(env, "OPENCODE_SERVER_PASSWORD="+s.Password)
|
||||
}
|
||||
cmd.Env = env
|
||||
// Env не задаём: serve наследует окружение родителя и сам резолвит свой
|
||||
// глобальный/локальный конфиг (модель/провайдеры, агентов, БД) как обычно.
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -200,28 +200,18 @@ func atoiOrZero(s string) int {
|
||||
return n
|
||||
}
|
||||
|
||||
func TestServeCmd_SetsOpenCodeConfig(t *testing.T) {
|
||||
// serveCmd должен всегда передавать OPENCODE_CONFIG: без него serve может
|
||||
// не подхватить модель/провайдеров из глобального конфига.
|
||||
func TestServeCmd_NoCustomEnv(t *testing.T) {
|
||||
// serveCmd не должен задавать свой Env: serve наследует окружение родителя
|
||||
// и сам резолвит конфиг/БД/пароль как обычно.
|
||||
s := &Server{}
|
||||
s.defaults()
|
||||
cmd := s.serveCmd(context.Background())
|
||||
if cmd.Env == nil {
|
||||
t.Fatal("serveCmd: Env не задан")
|
||||
}
|
||||
prefix := "OPENCODE_CONFIG="
|
||||
path := ""
|
||||
if cmd.Env != nil {
|
||||
for _, kv := range cmd.Env {
|
||||
if strings.HasPrefix(kv, prefix) {
|
||||
path = strings.TrimPrefix(kv, prefix)
|
||||
break
|
||||
if strings.HasPrefix(kv, "OPENCODE_") {
|
||||
t.Errorf("serveCmd: не ожидал env %q", kv)
|
||||
}
|
||||
}
|
||||
if path == "" {
|
||||
t.Fatal("serveCmd: OPENCODE_CONFIG не выставлен")
|
||||
}
|
||||
if want := s.ConfigPath; path != want {
|
||||
t.Errorf("OPENCODE_CONFIG = %q, want %q", path, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user