fix(opencode): serve передаёт OPENCODE_CONFIG — детерминированный выбор модели
Some checks failed
CI / test (push) Failing after 1m36s
CI / build-and-package (amd64, linux) (push) Failing after 1m16s
CI / build-and-package (amd64, windows) (push) Successful in 34s

This commit is contained in:
ki.sagidullin
2026-08-23 20:27:24 +05:00
parent 3c51ffee4e
commit 88b455203f
2 changed files with 61 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
)
@@ -31,6 +32,12 @@ type Server struct {
Password string // basic auth (если непустой — сервер защищён)
Dir string // каталог, в котором запускается serve (project сервера)
// ConfigPath — путь к глобальному конфигу opencode (opencode.jsonc).
// Передаётся serve через env OPENCODE_CONFIG, чтобы гарантированно
// подхватить модель/провайдеров независимо от резолва глобального пути.
// Пусто — serve резолвит конфиг сам (см. defaults).
ConfigPath string
// URL задаёт внешний сервер. Пусто — супервайзер владеет процессом.
URL string
PollInterval time.Duration // как часто проверять /global/health
@@ -60,6 +67,20 @@ func (s *Server) defaults() {
if s.Stdout == nil {
s.Stdout = os.Stderr
}
if s.ConfigPath == "" {
s.ConfigPath = defaultOpenCodeConfigPath()
}
}
// defaultOpenCodeConfigPath возвращает путь к глобальному конфигу opencode,
// совпадающий с тем, что opencode загружает по умолчанию: ~/.config/opencode/
// (в т.ч. на Windows — см. Global.Path.config в исходниках opencode).
func defaultOpenCodeConfigPath() string {
home, _ := os.UserHomeDir()
if home == "" {
home = "."
}
return filepath.Join(home, ".config", "opencode", "opencode.jsonc")
}
// baseURL собирает полный адрес сервера (http://host:port).
@@ -125,6 +146,12 @@ func (s *Server) serveCmd(ctx context.Context) *exec.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 != "" {

View File

@@ -198,4 +198,38 @@ func atoiOrZero(s string) int {
n = n*10 + int(c-'0')
}
return n
}
func TestServeCmd_SetsOpenCodeConfig(t *testing.T) {
// serveCmd должен всегда передавать OPENCODE_CONFIG: без него 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 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) {
// явно заданный путь переопределяет дефолтный
s := &Server{ConfigPath: `C:\custom\opencode.jsonc`}
s.defaults()
if s.ConfigPath != `C:\custom\opencode.jsonc` {
t.Errorf("ConfigPath = %q, want explicit", s.ConfigPath)
}
}