16739dee60
* server: align generate with native chat templates /api/generate rebuilt chat-like prompts through the Go template path even when the model selected its native GGUF Jinja chat template, so the same model rendered differently between generate and chat. Route chat-like generate requests through the shared native chat preparation path, keep deprecated context and image handling working there, and keep explicit OLLAMA_GO_TEMPLATE overrides intact. Fixes #16792 * review comments Fall back to "{{ .Prompt }}" when lacking templates
261 lines
7.6 KiB
Go
261 lines
7.6 KiB
Go
//go:build integration && generate
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ollama/ollama/api"
|
|
)
|
|
|
|
func TestGenerateNativeJinjaDebugRender(t *testing.T) {
|
|
if os.Getenv("OLLAMA_TEST_EXISTING") != "" {
|
|
t.Skip("requires a test-managed server started with OLLAMA_GO_TEMPLATE=0")
|
|
}
|
|
t.Setenv("OLLAMA_GO_TEMPLATE", "0")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
client, _, cleanup := InitServerConnection(ctx, t)
|
|
defer cleanup()
|
|
pullOrSkip(ctx, t, client, smol)
|
|
|
|
modelName := createGenerateJinjaModel(ctx, t, client, smol)
|
|
defer func() {
|
|
if err := client.Delete(context.Background(), &api.DeleteRequest{Model: modelName}); err != nil {
|
|
t.Logf("failed to delete %s: %v", modelName, err)
|
|
}
|
|
}()
|
|
|
|
t.Run("generate matches chat", func(t *testing.T) {
|
|
prompt := "Reply with one short sentence about blue skies."
|
|
generateRender := debugGenerateRender(ctx, t, client, api.GenerateRequest{
|
|
Model: modelName,
|
|
Prompt: prompt,
|
|
})
|
|
chatRender := debugChatRender(ctx, t, client, api.ChatRequest{
|
|
Model: modelName,
|
|
Messages: []api.Message{
|
|
{Role: "user", Content: prompt},
|
|
},
|
|
})
|
|
|
|
if generateRender == "" {
|
|
t.Fatal("generate debug render was empty")
|
|
}
|
|
if strings.Contains(generateRender, "GO_TEMPLATE_SENTINEL") {
|
|
t.Fatalf("generate used Go TEMPLATE instead of native chat template: %q", generateRender)
|
|
}
|
|
if generateRender != chatRender {
|
|
t.Fatalf("generate render does not match chat render\ngenerate: %q\nchat: %q", generateRender, chatRender)
|
|
}
|
|
})
|
|
|
|
t.Run("context prepends prior tokens", func(t *testing.T) {
|
|
first := generateOnce(ctx, t, client, api.GenerateRequest{
|
|
Model: modelName,
|
|
Prompt: "Say hello.",
|
|
Options: map[string]any{
|
|
"num_predict": 1,
|
|
"temperature": 0,
|
|
},
|
|
})
|
|
if len(first.Context) == 0 {
|
|
t.Fatal("generate response did not include context")
|
|
}
|
|
|
|
next := api.GenerateRequest{
|
|
Model: modelName,
|
|
Prompt: "Say goodbye.",
|
|
}
|
|
withoutContext := debugGenerateRender(ctx, t, client, next)
|
|
next.Context = first.Context
|
|
withContext := debugGenerateRender(ctx, t, client, next)
|
|
|
|
if withContext == withoutContext {
|
|
t.Fatal("context debug render did not change the prompt")
|
|
}
|
|
if !strings.HasSuffix(withContext, withoutContext) {
|
|
t.Fatalf("context render should preserve current native template as suffix\nwith context: %q\nwithout: %q", withContext, withoutContext)
|
|
}
|
|
})
|
|
|
|
t.Run("images use completion media markers", func(t *testing.T) {
|
|
resp := debugGenerate(ctx, t, client, api.GenerateRequest{
|
|
Model: modelName,
|
|
Prompt: "compare [img] and [img]",
|
|
Images: []api.ImageData{
|
|
[]byte("first-test-image"),
|
|
[]byte("second-test-image"),
|
|
},
|
|
})
|
|
|
|
if resp.DebugInfo == nil {
|
|
t.Fatal("missing debug info")
|
|
}
|
|
if resp.DebugInfo.ImageCount != 2 {
|
|
t.Fatalf("image count = %d, want 2", resp.DebugInfo.ImageCount)
|
|
}
|
|
rendered := resp.DebugInfo.RenderedTemplate
|
|
if !strings.Contains(rendered, "[img-0]") || !strings.Contains(rendered, "[img-1]") {
|
|
t.Fatalf("rendered template missing image markers: %q", rendered)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGenerateNativeJinjaImages(t *testing.T) {
|
|
if os.Getenv("OLLAMA_TEST_EXISTING") != "" {
|
|
t.Skip("requires a test-managed server started with OLLAMA_GO_TEMPLATE=0")
|
|
}
|
|
t.Setenv("OLLAMA_GO_TEMPLATE", "0")
|
|
skipUnderMinVRAM(t, 6)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
client, _, cleanup := InitServerConnection(ctx, t)
|
|
defer cleanup()
|
|
_, _, ollamaHome := decodeTestImages(t)
|
|
|
|
t.Run("actual image processing", func(t *testing.T) {
|
|
for _, baseModel := range testModels([]string{"gemma3:4b"}) {
|
|
t.Run(baseModel, func(t *testing.T) {
|
|
setupVisionModel(ctx, t, client, baseModel)
|
|
|
|
modelName := createGenerateJinjaModel(ctx, t, client, baseModel)
|
|
defer func() {
|
|
if err := client.Delete(context.Background(), &api.DeleteRequest{Model: modelName}); err != nil {
|
|
t.Logf("failed to delete %s: %v", modelName, err)
|
|
}
|
|
}()
|
|
|
|
req := api.GenerateRequest{
|
|
Model: modelName,
|
|
Prompt: "Describe what you see in this image briefly.",
|
|
Images: []api.ImageData{ollamaHome},
|
|
Options: map[string]any{
|
|
"seed": 42,
|
|
"temperature": 0.0,
|
|
},
|
|
KeepAlive: &api.Duration{Duration: 10 * time.Second},
|
|
}
|
|
|
|
resp := debugGenerate(ctx, t, client, req)
|
|
if resp.DebugInfo == nil {
|
|
t.Fatal("missing debug info")
|
|
}
|
|
if resp.DebugInfo.ImageCount != 1 {
|
|
t.Fatalf("image count = %d, want 1", resp.DebugInfo.ImageCount)
|
|
}
|
|
rendered := resp.DebugInfo.RenderedTemplate
|
|
if strings.Contains(rendered, "GO_TEMPLATE_SENTINEL") {
|
|
t.Fatalf("generate with images used Go TEMPLATE instead of native chat template: %q", rendered)
|
|
}
|
|
if !strings.Contains(rendered, "[img-0]") {
|
|
t.Fatalf("rendered template missing image marker: %q", rendered)
|
|
}
|
|
|
|
DoGenerate(ctx, t, client, req, []string{
|
|
"llama", "animal", "build", "model", "open", "cartoon", "character",
|
|
}, 120*time.Second, 30*time.Second)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func createGenerateJinjaModel(ctx context.Context, t *testing.T, client *api.Client, baseModel string) string {
|
|
t.Helper()
|
|
|
|
modelName := fmt.Sprintf("test-generate-jinja-%d", time.Now().UnixNano())
|
|
stream := false
|
|
err := client.Create(ctx, &api.CreateRequest{
|
|
Model: modelName,
|
|
From: baseModel,
|
|
Stream: &stream,
|
|
Template: `GO_TEMPLATE_SENTINEL {{ if .System }}{{ .System }} {{ end }}{{ if .Prompt }}{{ .Prompt }}{{ end }}{{ range .Messages }}{{ .Content }}{{ end }}`,
|
|
}, func(api.ProgressResponse) error {
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create %s from %s: %v", modelName, baseModel, err)
|
|
}
|
|
return modelName
|
|
}
|
|
|
|
func debugGenerateRender(ctx context.Context, t *testing.T, client *api.Client, req api.GenerateRequest) string {
|
|
t.Helper()
|
|
resp := debugGenerate(ctx, t, client, req)
|
|
if resp.DebugInfo == nil {
|
|
t.Fatal("missing debug info")
|
|
}
|
|
return resp.DebugInfo.RenderedTemplate
|
|
}
|
|
|
|
func debugGenerate(ctx context.Context, t *testing.T, client *api.Client, req api.GenerateRequest) api.GenerateResponse {
|
|
t.Helper()
|
|
|
|
stream := false
|
|
req.Stream = &stream
|
|
req.DebugRenderOnly = true
|
|
|
|
var final api.GenerateResponse
|
|
if err := client.Generate(ctx, &req, func(resp api.GenerateResponse) error {
|
|
final = resp
|
|
return nil
|
|
}); err != nil {
|
|
if strings.Contains(err.Error(), "requires more system memory") {
|
|
t.Skipf("model is too large for this test system: %v", err)
|
|
}
|
|
t.Fatalf("generate debug render failed: %v", err)
|
|
}
|
|
return final
|
|
}
|
|
|
|
func debugChatRender(ctx context.Context, t *testing.T, client *api.Client, req api.ChatRequest) string {
|
|
t.Helper()
|
|
|
|
stream := false
|
|
req.Stream = &stream
|
|
req.DebugRenderOnly = true
|
|
|
|
var final api.ChatResponse
|
|
if err := client.Chat(ctx, &req, func(resp api.ChatResponse) error {
|
|
final = resp
|
|
return nil
|
|
}); err != nil {
|
|
if strings.Contains(err.Error(), "requires more system memory") {
|
|
t.Skipf("model is too large for this test system: %v", err)
|
|
}
|
|
t.Fatalf("chat debug render failed: %v", err)
|
|
}
|
|
if final.DebugInfo == nil {
|
|
t.Fatal("missing debug info")
|
|
}
|
|
return final.DebugInfo.RenderedTemplate
|
|
}
|
|
|
|
func generateOnce(ctx context.Context, t *testing.T, client *api.Client, req api.GenerateRequest) api.GenerateResponse {
|
|
t.Helper()
|
|
|
|
stream := false
|
|
req.Stream = &stream
|
|
|
|
var final api.GenerateResponse
|
|
if err := client.Generate(ctx, &req, func(resp api.GenerateResponse) error {
|
|
final = resp
|
|
return nil
|
|
}); err != nil {
|
|
if strings.Contains(err.Error(), "requires more system memory") {
|
|
t.Skipf("model is too large for this test system: %v", err)
|
|
}
|
|
t.Fatalf("generate failed: %v", err)
|
|
}
|
|
return final
|
|
}
|