fix(opencode): reasoning-fallback в вердикте + API-логи в debug-уровень
Some checks failed
CI / test (pull_request) Failing after 1m21s
CI / build-and-package (amd64, linux) (pull_request) Failing after 1m1s
CI / build-and-package (amd64, windows) (pull_request) Successful in 23s

- verdict(): если в завершённом assistant-сообщении нет text-парта, но есть
  reasoning — вердикт собирается из reasoning (fallback), а не падает с
  'нет text-части в ответе'. Решает сбой dev/reviewer/postmortem на моделях,
  отвечающих только thinking (например, через прокси tokentool).
- Отладочные логи API-вызовов (запрос/ответ) помечены маркером 'debug',
  чтобы в панели «Логи» они классифицировались как debug, а не info.
This commit is contained in:
ki.sagidullin
2026-08-23 14:43:05 +05:00
parent 963e7b478e
commit 8cf4fc9f7c
5 changed files with 118 additions and 12 deletions

View File

@@ -24,9 +24,9 @@ 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)
BaseURL string // http://host:port (без завершающего слеша)
Password string // basic auth (username "opencode")
Debug bool // включать отладочные логи API-вызовов (log.level=debug)
http *http.Client // единый клиент: все операции быстрые (нет блокирующего Send)
}
@@ -63,7 +63,7 @@ func (c *Client) do(ctx context.Context, method, path, op string, body []byte) (
req.Header.Set("Content-Type", "application/json")
}
if c.Debug {
log.Printf("opencode api %s -> %s %s%s", op, method, c.BaseURL, path)
log.Printf("opencode api debug: %s -> %s %s%s", op, method, c.BaseURL, path)
}
resp, err := c.http.Do(req)
if err != nil {
@@ -76,12 +76,12 @@ func (c *Client) do(ctx context.Context, method, path, op string, body []byte) (
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
if c.Debug {
log.Printf("opencode api %s response: status %d", op, resp.StatusCode)
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 %s response (%d bytes)", op, len(b))
log.Printf("opencode api debug: %s response (%d bytes)", op, len(b))
}
return b, nil
}
@@ -320,6 +320,26 @@ func assistantText(msgs []v2Message, since int64) []string {
return texts
}
// assistantVerdict собирает финальный текст ответа: сначала text-парты, а если
// их нет — только reasoning-парты (fallback для моделей, которые на некоторые
// запросы отвечают лишь reasoning без text). usedReasoning=true означает, что
// text-партов не было вовсе и вердикт собран из reasoning.
func assistantVerdict(msgs []v2Message, since int64) (texts []string, usedReasoning bool) {
if texts := assistantText(msgs, since); len(texts) > 0 {
return texts, false
}
ass := assistantSince(msgs, since)
reasoning := make([]string, 0, len(ass))
for i := len(ass) - 1; i >= 0; i-- {
for _, p := range ass[i].Content {
if p.Type == "reasoning" && p.Text != "" {
reasoning = append(reasoning, p.Text)
}
}
}
return reasoning, len(reasoning) > 0
}
func truncateStr(s string, n int) string {
if len(s) <= n {
return s