fix(opencode): выбор модели — только глобальный конфиг opencode; агент передаётся в сессию
Some checks failed
CI / test (push) Failing after 1m16s
CI / build-and-package (amd64, linux) (push) Failing after 1m4s
CI / build-and-package (amd64, windows) (push) Successful in 29s

- CreateSession(ctx, agent) шлёт {agent} в POST /api/session, модель не выбираем
- удалён internal/opencode/config.go (ReadModelRef/JSONC-стрип) и его тесты
- runner.go больше не читает конфиг opencode и не хардпинит модель
- тесты и README обновлены
This commit is contained in:
ki.sagidullin
2026-08-23 17:22:36 +05:00
parent 0ad53e338d
commit 4c0dc80aa8
8 changed files with 46 additions and 332 deletions

View File

@@ -75,29 +75,18 @@ func (r *Runner) Run(ctx context.Context, prompt, cwd, agent, sessionID string)
}
c := &Client{BaseURL: srv.Addr(), Password: srv.Password, Debug: r.Debug}
// Модель по умолчанию из конфига opencode — хардпиним её в сессии, чтобы
// не зависеть от fallback-логики opencode (класс O5 WARN: если модель не
// считывается/не задана — предупреждаем и работаем без явного указания).
model, mErr := ReadModelRef(srv.Config, srv.ConfigDir)
if mErr != nil {
r.logf("WARN opencode: не удалось прочитать model из конфига: %v", mErr)
} else if model == nil {
r.logf("WARN opencode: в конфиге opencode не задан top-level model — модель не хардпинится (риск fallback)")
} else {
r.logf("opencode(%s) model=%s", agent, model)
}
// Сессия: заданная (resume) или новая.
// Сессия: заданная (resume) или новая. Модель НЕ выбираем — opencode сам
// берёт её по умолчанию из своего глобального конфига.
sid := sessionID
if sid == "" {
sid, err = c.CreateSession(ctx, model)
sid, err = c.CreateSession(ctx, agent)
if err != nil {
return nil, fmt.Errorf("opencode: create session: %w", err)
}
r.logf("opencode(%s) session=%s на %s", agent, sid, srv.Addr())
}
return r.awaitVerdict(ctx, c, model, sid, agent, prompt)
return r.awaitVerdict(ctx, c, sid, agent, prompt)
}
// settlePolls — сколько подряд опросов должно подтвердить завершение ответа,
@@ -109,7 +98,7 @@ const settlePolls = 2
// контролируя idle/hard таймауты. Завершение: сессия ушла из активных дренажей
// И есть новое завершённое assistant-сообщение, стабильное в течение settlePolls
// опросов. Возвращает вердикт (текст text-партов), либо rc=-1 при таймауте.
func (r *Runner) awaitVerdict(ctx context.Context, c *Client, model *ModelRef, sid, agent, prompt string) (*Result, error) {
func (r *Runner) awaitVerdict(ctx context.Context, c *Client, sid, agent, prompt string) (*Result, error) {
// admit промпта; граница «новых» сообщений — время создания user-сообщения.
admittedAt := time.Now().UnixMilli()
adm, err := c.Prompt(ctx, sid, prompt)
@@ -187,7 +176,7 @@ func (r *Runner) awaitVerdict(ctx context.Context, c *Client, model *ModelRef, s
doneSeen++
emptySeen = 0
if doneSeen >= settlePolls {
return r.verdict(model, cur, msgs, admittedAt, sid)
return r.verdict(cur, msgs, admittedAt, sid)
}
case !active && cur == nil:
// сессия завершилась, но нового assistant-сообщения так и нет
@@ -207,12 +196,7 @@ func (r *Runner) awaitVerdict(ctx context.Context, c *Client, model *ModelRef, s
}
// verdict собирает финальный результат из новых assistant-сообщений.
// Проверяет фактическую модель ответа и логирует warning при расхождении
// с ожидаемой (устойчивость к «не той» модели — класс O5 WARN).
func (r *Runner) verdict(model *ModelRef, cur *v2Message, msgs []v2Message, since int64, sid string) (*Result, error) {
if model != nil && cur.Model != nil && (model.ProviderID != cur.Model.ProviderID || model.ID != cur.Model.ID) {
r.logf("WARN opencode: сессия %s отвечала моделью %s, а не ожидаемой %s — проверь providers в конфиге (v2-схема: provider.api / request, а не npm/options)", sid, cur.Model, model)
}
func (r *Runner) verdict(cur *v2Message, msgs []v2Message, since int64, sid string) (*Result, error) {
if cur.Error != nil && cur.Error.Message != "" {
return nil, &ClientErr{Op: "prompt", Err: errors.New(cur.Error.Message)}
}