fix(opencode): x-opencode-directory, Content-Length, warmup /api/model; serve без явного Env
Some checks failed
CI / test (push) Failing after 2m13s
CI / build-and-package (amd64, linux) (push) Failing after 1m3s
CI / build-and-package (amd64, windows) (push) Successful in 29s

This commit is contained in:
ki.sagidullin
2026-08-24 08:38:59 +05:00
parent 88b455203f
commit c9ab753c1e
6 changed files with 32 additions and 55 deletions

View File

@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
)
@@ -24,10 +23,11 @@ import (
// Prompt не блокирует: вердикт собирается поллингом из content[].type=="text"
// новых assistant-сообщений (см. Runner.awaitVerdict).
type Client struct {
BaseURL string // http://host:port (без завершающего слеша)
Password string // basic auth (username "opencode")
Debug bool // включать отладочные логи API-вызовов (log.level=debug)
http *http.Client // единый клиент: все операции быстрые (нет блокирующего Send)
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)
}
// ClientErr — классы ошибок клиента.
@@ -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)
req.Header.Set("Content-Type", "application/json")
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
}

View File

@@ -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 сам
// берёт её по умолчанию из своего глобального конфига.

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -200,29 +200,19 @@ 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 := ""
for _, kv := range cmd.Env {
if strings.HasPrefix(kv, prefix) {
path = strings.TrimPrefix(kv, prefix)
break
if cmd.Env != nil {
for _, kv := range cmd.Env {
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)
}
}
func TestServerCmd_CustomOpenCodeConfig(t *testing.T) {