fix(opencode): reasoning-fallback в вердикте + API-логи в debug-уровень
- verdict(): если в завершённом assistant-сообщении нет text-парта, но есть reasoning — вердикт собирается из reasoning (fallback), а не падает с 'нет text-части в ответе'. Решает сбой dev/reviewer/postmortem на моделях, отвечающих только thinking (например, через прокси tokentool). - Отладочные логи API-вызовов (запрос/ответ) помечены маркером 'debug', чтобы в панели «Логи» они классифицировались как debug, а не info.
This commit is contained in:
@@ -25,10 +25,11 @@ type fakeAPIServer struct {
|
||||
sessionID string
|
||||
created bool
|
||||
active bool
|
||||
blockPrompt bool
|
||||
messages []v2Message
|
||||
verdictText string
|
||||
failCreate bool
|
||||
blockPrompt bool
|
||||
messages []v2Message
|
||||
verdictText string
|
||||
verdictReasoning string // завершённый ответ только с reasoning-партом (без text)
|
||||
failCreate bool
|
||||
failMessages bool
|
||||
createdModel *ModelRef // модель, полученная на POST /api/session
|
||||
promptCalls int
|
||||
@@ -133,6 +134,9 @@ func (f *fakeAPIServer) handler() http.Handler {
|
||||
if msgs == nil && f.verdictText != "" && !f.blockPrompt {
|
||||
msgs = []v2Message{f.assistantMsg(f.verdictText)}
|
||||
}
|
||||
if msgs == nil && f.verdictReasoning != "" && !f.blockPrompt {
|
||||
msgs = []v2Message{f.assistantReasoningMsg(f.verdictReasoning)}
|
||||
}
|
||||
if msgs == nil {
|
||||
msgs = []v2Message{}
|
||||
}
|
||||
@@ -153,6 +157,19 @@ func (f *fakeAPIServer) assistantMsg(text string) v2Message {
|
||||
}
|
||||
}
|
||||
|
||||
// assistantReasoningMsg строит завершённое assistant-сообщение только с
|
||||
// reasoning-партом (без text) — для проверки fallback-сценария.
|
||||
func (f *fakeAPIServer) assistantReasoningMsg(text string) v2Message {
|
||||
now := time.Now().UnixMilli()
|
||||
return v2Message{
|
||||
ID: "msg_r",
|
||||
Type: "assistant",
|
||||
Content: []v2Part{{Type: "reasoning", Text: text}},
|
||||
Finish: "end_turn",
|
||||
Time: v2Time{Created: &now, Completed: &now},
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
@@ -279,6 +296,48 @@ func Test_newestAssistant(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_assistantVerdict(t *testing.T) {
|
||||
older := time.Now().Add(-time.Minute).UnixMilli()
|
||||
newer := time.Now().UnixMilli()
|
||||
reasoningOf := func(text string, at *int64) v2Message {
|
||||
return v2Message{ID: "r", Type: "assistant", Content: []v2Part{{Type: "reasoning", Text: text}}, Time: v2Time{Created: at}}
|
||||
}
|
||||
|
||||
// reasoning-only: text-партов нет → fallback на reasoning, usedReasoning=true.
|
||||
// Сообщения приходят новейшими первыми (как из API) → размышление 2 новее.
|
||||
reasoningOnlyMsgs := []v2Message{
|
||||
reasoningOf("размышление 2", &newer),
|
||||
reasoningOf("размышление 1", &older),
|
||||
}
|
||||
texts, used := assistantVerdict(reasoningOnlyMsgs, older)
|
||||
if !used {
|
||||
t.Error("usedReasoning = false, want true для reasoning-only")
|
||||
}
|
||||
if len(texts) != 2 || texts[0] != "размышление 1" || texts[1] != "размышление 2" {
|
||||
t.Errorf("verdict = %v, want [размышление 1 размышление 2] (хронологически)", texts)
|
||||
}
|
||||
|
||||
// text + reasoning → берётся text, reasoning игнорируется.
|
||||
mixed := []v2Message{
|
||||
{ID: "a", Type: "assistant",
|
||||
Content: []v2Part{{Type: "reasoning", Text: "thinking"}, {Type: "text", Text: "ответ"}},
|
||||
Time: v2Time{Created: &newer}},
|
||||
}
|
||||
texts, used = assistantVerdict(mixed, older)
|
||||
if used {
|
||||
t.Error("usedReasoning = true, want false (есть text)")
|
||||
}
|
||||
if len(texts) != 1 || texts[0] != "ответ" {
|
||||
t.Errorf("verdict = %v, want [ответ]", texts)
|
||||
}
|
||||
|
||||
// пусто → пусто и usedReasoning=false.
|
||||
empty := []v2Message{{ID: "u", Type: "user", Time: v2Time{Created: &newer}}}
|
||||
if texts, used := assistantVerdict(empty, older); used || len(texts) != 0 {
|
||||
t.Errorf("пусто: texts=%v usedReasoning=%v, want пусто/false", texts, used)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseModelString(t *testing.T) {
|
||||
m := parseModelString("tokentool/deepseek/deepseek-v4-flash-0731")
|
||||
if m == nil || m.ProviderID != "tokentool" || m.ID != "deepseek/deepseek-v4-flash-0731" {
|
||||
|
||||
Reference in New Issue
Block a user