feat: «Думать» uses a model's own thinking when it has it (OpenAI-compatible reasoning, Anthropic extended thinking) instead of asking it to think in words too; learned per model, the word-thinking prompt kept for models without; without «Думать» such a model is asked not to think
This commit is contained in:
1 parent
37ef886503
commit
03f963340e
2 files changed
+311
-12
No files matched your search
@@ -0,0 +1,144 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// openAIChunk is one streamed delta of an OpenAI-compatible server.
|
||||
func openAIChunk(field, text string) string {
|
||||
b, _ := json.Marshal(map[string]any{"choices": []any{map[string]any{"delta": map[string]string{field: text}}}})
|
||||
return "data: " + string(b) + "\n\n"
|
||||
}
|
||||
|
||||
// A model that thinks by itself (llama.cpp, vLLM: reasoning_content) has its
|
||||
// thinking shown as the chat's own: between ::думаю:: and ::ответ::.
|
||||
func TestAIOpenAINativeReasoning(t *testing.T) {
|
||||
var body map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
fmt.Fprint(w, openAIChunk("reasoning_content", "17*23")+openAIChunk("reasoning_content", " = 391")+
|
||||
openAIChunk("content", "391")+"data: [DONE]\n\n")
|
||||
}))
|
||||
defer srv.Close()
|
||||
got, err := collect(t, aiConfig{Type: "openai", URL: srv.URL, Model: "m", think: "on"})
|
||||
if err != nil || got != "::думаю::\n17*23 = 391\n::ответ::\n391" {
|
||||
t.Fatalf("got %q, %v", got, err)
|
||||
}
|
||||
kw, _ := body["chat_template_kwargs"].(map[string]any)
|
||||
if kw["enable_thinking"] != true {
|
||||
t.Fatalf("thinking not asked for: %v", body)
|
||||
}
|
||||
}
|
||||
|
||||
// A server that does not know the thinking switch (400 on it) is asked again
|
||||
// without it.
|
||||
func TestAIOpenAIRetriesWithoutThinkSwitch(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
var calls []map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var b map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&b)
|
||||
mu.Lock()
|
||||
calls = append(calls, b)
|
||||
mu.Unlock()
|
||||
if _, ok := b["chat_template_kwargs"]; ok {
|
||||
http.Error(w, `{"error":{"message":"Unrecognized request argument supplied: chat_template_kwargs"}}`, 400)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, openAIChunk("content", "ok")+"data: [DONE]\n\n")
|
||||
}))
|
||||
defer srv.Close()
|
||||
got, err := collect(t, aiConfig{Type: "openai", URL: srv.URL, Model: "m", think: "off"})
|
||||
if err != nil || got != "ok" || len(calls) != 2 {
|
||||
t.Fatalf("got %q, %v, %d calls", got, err, len(calls))
|
||||
}
|
||||
}
|
||||
|
||||
// Anthropic: «Думать» turns on extended thinking; its thinking_delta is the
|
||||
// thinking shown.
|
||||
func TestAIAnthropicThinking(t *testing.T) {
|
||||
var body map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
fmt.Fprint(w, "data: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"хм\"}}\n\n"+
|
||||
"data: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"Да\"}}\n\n"+
|
||||
"data: {\"type\":\"message_stop\"}\n\n")
|
||||
}))
|
||||
defer srv.Close()
|
||||
got, err := collect(t, aiConfig{Type: "anthropic", URL: srv.URL, Key: "k", Model: "m", think: "on"})
|
||||
if err != nil || got != "::думаю::\nхм\n::ответ::\nДа" {
|
||||
t.Fatalf("got %q, %v", got, err)
|
||||
}
|
||||
th, _ := body["thinking"].(map[string]any)
|
||||
if th["type"] != "enabled" || body["max_tokens"].(float64) <= th["budget_tokens"].(float64) {
|
||||
t.Fatalf("thinking not asked for: %v", body)
|
||||
}
|
||||
}
|
||||
|
||||
// askThink sends one «Думать» question to the chat and gives the answer and
|
||||
// the system prompts the model got, in order.
|
||||
func askThink(t *testing.T, reply func(system string, body map[string]any) string) (string, []string) {
|
||||
t.Helper()
|
||||
s, c := newTestServer(t)
|
||||
var mu sync.Mutex
|
||||
var systems []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var b map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&b)
|
||||
sys := b["messages"].([]any)[0].(map[string]any)["content"].(string)
|
||||
if strings.Contains(fmt.Sprint(b["messages"]), "название чата") || strings.Contains(sys, "название чата") {
|
||||
fmt.Fprint(w, openAIChunk("content", "Чат")+"data: [DONE]\n\n")
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
systems = append(systems, sys)
|
||||
mu.Unlock()
|
||||
fmt.Fprint(w, reply(sys, b)+"data: [DONE]\n\n")
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
postAIModels(t, s, c, [][7]string{{"", "M", "openai", srv.URL, "m", "", ""}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/panel/ai/chat", strings.NewReader(`{"message":"сколько 17*23","context":"x","think":true}`))
|
||||
req.AddCookie(c)
|
||||
w := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(w, req)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return w.Body.String(), append([]string(nil), systems...)
|
||||
}
|
||||
|
||||
// A model with thinking of its own thinks once, by itself: no "think in
|
||||
// words" prompt on top.
|
||||
func TestAIChatThinkNative(t *testing.T) {
|
||||
got, systems := askThink(t, func(sys string, b map[string]any) string {
|
||||
return openAIChunk("reasoning_content", "считаю") + openAIChunk("content", "391")
|
||||
})
|
||||
if len(systems) != 1 || strings.Contains(systems[0], "Режим размышления") {
|
||||
t.Fatalf("systems: %q", systems)
|
||||
}
|
||||
if !strings.Contains(got, "::думаю::\nсчитаю\n::ответ::\n391") {
|
||||
t.Fatalf("answer: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A model without it is asked again with the prompt, its first answer
|
||||
// dropped unseen.
|
||||
func TestAIChatThinkPromptFallback(t *testing.T) {
|
||||
got, systems := askThink(t, func(sys string, b map[string]any) string {
|
||||
if strings.Contains(sys, "Режим размышления") {
|
||||
return openAIChunk("content", "::думаю::\nсчитаю\n::ответ::\n391")
|
||||
}
|
||||
return openAIChunk("content", "сразу ответ")
|
||||
})
|
||||
if len(systems) != 2 || strings.Contains(systems[0], "Режим размышления") || !strings.Contains(systems[1], "Режим размышления") {
|
||||
t.Fatalf("systems: %q", systems)
|
||||
}
|
||||
if strings.Contains(got, "сразу ответ") || !strings.Contains(got, "::ответ::\n391") {
|
||||
t.Fatalf("answer: %q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user