mirror of
https://github.com/ollama/ollama.git
synced 2025-11-10 15:07:46 +01:00
streaming and non-streaming
This commit is contained in:
@@ -12,17 +12,24 @@ import (
|
|||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// var MODEL = "qwen3vl-odc-dev"
|
// TestQwen3VLStreaming tests Qwen3-VL with streaming enabled
|
||||||
// var MODEL = "qwen3vl-thinking-odc-dev"
|
func TestQwen3VLStreaming(t *testing.T) {
|
||||||
|
runQwen3VLTests(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestQwen3VLNonStreaming tests Qwen3-VL with streaming disabled
|
||||||
|
func TestQwen3VLNonStreaming(t *testing.T) {
|
||||||
|
runQwen3VLTests(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runQwen3VLTests(t *testing.T, stream bool) {
|
||||||
|
models := []string{"qwen3vl-odc-dev"} // , "qwen3vl-thinking-odc-dev", "qwen3-vl:8b"}
|
||||||
|
|
||||||
// TestQwen3VLScenarios exercises common Qwen3-VL cases using integration helpers
|
|
||||||
func TestQwen3VLScenarios(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
messages []api.Message
|
messages []api.Message
|
||||||
tools []api.Tool
|
tools []api.Tool
|
||||||
images []string
|
images []string
|
||||||
anyResp []string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Text-Only Scenario",
|
name: "Text-Only Scenario",
|
||||||
@@ -30,7 +37,6 @@ func TestQwen3VLScenarios(t *testing.T) {
|
|||||||
{Role: "system", Content: "You are a helpful assistant."},
|
{Role: "system", Content: "You are a helpful assistant."},
|
||||||
{Role: "user", Content: "Write a short haiku about autumn."},
|
{Role: "user", Content: "Write a short haiku about autumn."},
|
||||||
},
|
},
|
||||||
// anyResp: []string{"haiku", "autumn", "fall"},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Single Image Scenario",
|
name: "Single Image Scenario",
|
||||||
@@ -45,7 +51,6 @@ func TestQwen3VLScenarios(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
images: []string{"testdata/question.png"},
|
images: []string{"testdata/question.png"},
|
||||||
// anyResp: []string{"answer", "solution", "explanation"},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Multiple Images Scenario",
|
name: "Multiple Images Scenario",
|
||||||
@@ -56,11 +61,10 @@ func TestQwen3VLScenarios(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "What is the answer to these two questions?",
|
Content: "Use both images to answer the question.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
images: []string{"testdata/satmath1.png", "testdata/satmath2.png"},
|
images: []string{"testdata/question.png", "testdata/menu.png"},
|
||||||
// anyResp: []string{"image", "answer", "analysis"},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Tools Scenario",
|
name: "Tools Scenario",
|
||||||
@@ -90,13 +94,71 @@ func TestQwen3VLScenarios(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// anyResp: []string{"san francisco", "weather", "temperature"},
|
},
|
||||||
|
{
|
||||||
|
name: "Multi-Turn Tools With Image",
|
||||||
|
messages: []api.Message{
|
||||||
|
{Role: "system", Content: "Use tools when actions are required."},
|
||||||
|
{Role: "user", Content: "What's the current temperature in San Francisco?"},
|
||||||
|
{Role: "assistant", Content: "", ToolCalls: []api.ToolCall{
|
||||||
|
{Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"city": "San Francisco",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
{Role: "tool", ToolName: "get_weather", Content: "Sunny"},
|
||||||
|
{Role: "user", Content: "Given that weather, what are the top 10 activities to do in San Francisco? Consider this photo as context."},
|
||||||
|
},
|
||||||
|
tools: []api.Tool{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Description: "Get current weather for a city.",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"city": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The city to get the weather for",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"city"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get_top_10_activities",
|
||||||
|
Description: "Get the top 10 activities for a city given the weather.",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"weather": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The weather in the city",
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The city to get the activities for",
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
Type: api.PropertyType{"base64"},
|
||||||
|
Description: "The image of the city",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"weather", "city", "image"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
images: []string{"testdata/sf-city.jpeg"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// models := []string{"qwen3-vl:8b", "qwen3-vl:30b"}
|
|
||||||
models := []string{"qwen3vl-odc-dev"} // , "qwen3vl-thinking-odc-dev"}
|
|
||||||
|
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -126,6 +188,8 @@ func TestQwen3VLScenarios(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isRemote := os.Getenv("OLLAMA_TEST_EXISTING") != ""
|
||||||
|
|
||||||
// Use integration helpers
|
// Use integration helpers
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -133,7 +197,7 @@ func TestQwen3VLScenarios(t *testing.T) {
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Skip pulling/preloading when using an existing (cloud) server
|
// Skip pulling/preloading when using an existing (cloud) server
|
||||||
if os.Getenv("OLLAMA_TEST_EXISTING") == "" {
|
if !isRemote {
|
||||||
if err := PullIfMissing(ctx, client, req.Model); err != nil {
|
if err := PullIfMissing(ctx, client, req.Model); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -141,29 +205,53 @@ func TestQwen3VLScenarios(t *testing.T) {
|
|||||||
_ = client.Generate(ctx, &api.GenerateRequest{Model: req.Model}, func(r api.GenerateResponse) error { return nil })
|
_ = client.Generate(ctx, &api.GenerateRequest{Model: req.Model}, func(r api.GenerateResponse) error { return nil })
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a tools scenario, validate tool_calls instead of content
|
var contentBuf, thinkingBuf strings.Builder
|
||||||
if len(tt.tools) > 0 {
|
var gotCalls []api.ToolCall
|
||||||
var gotCalls []api.ToolCall
|
|
||||||
err := client.Chat(ctx, &req, func(r api.ChatResponse) error {
|
err := client.Chat(ctx, &req, func(r api.ChatResponse) error {
|
||||||
if len(r.Message.ToolCalls) > 0 {
|
contentBuf.WriteString(r.Message.Content)
|
||||||
gotCalls = append(gotCalls, r.Message.ToolCalls...)
|
thinkingBuf.WriteString(r.Message.Thinking)
|
||||||
}
|
if len(r.Message.ToolCalls) > 0 {
|
||||||
return nil
|
gotCalls = append(gotCalls, r.Message.ToolCalls...)
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("chat error: %v", err)
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("chat error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log responses (truncated)
|
||||||
|
content := contentBuf.String()
|
||||||
|
thinking := thinkingBuf.String()
|
||||||
|
const maxLog = 800
|
||||||
|
if len(thinking) > 0 {
|
||||||
|
if len(thinking) > maxLog {
|
||||||
|
thinking = thinking[:maxLog] + "... [truncated]"
|
||||||
|
}
|
||||||
|
t.Logf("Thinking: %s", thinking)
|
||||||
|
}
|
||||||
|
if len(content) > 0 {
|
||||||
|
if len(content) > maxLog {
|
||||||
|
content = content[:maxLog] + "... [truncated]"
|
||||||
|
}
|
||||||
|
t.Logf("Content: %s", content)
|
||||||
|
}
|
||||||
|
if len(gotCalls) > 0 {
|
||||||
|
t.Logf("Tool calls: %d", len(gotCalls))
|
||||||
|
for i, call := range gotCalls {
|
||||||
|
t.Logf(" [%d] %s(%+v)", i, call.Function.Name, call.Function.Arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a tools scenario, validate tool_calls
|
||||||
|
if len(tt.tools) > 0 {
|
||||||
if len(gotCalls) == 0 {
|
if len(gotCalls) == 0 {
|
||||||
t.Fatalf("expected at least one tool call, got none")
|
t.Fatalf("expected at least one tool call, got none")
|
||||||
}
|
}
|
||||||
if gotCalls[0].Function.Name == "" {
|
if gotCalls[0].Function.Name == "" {
|
||||||
t.Fatalf("tool call missing function name: %#v", gotCalls[0])
|
t.Fatalf("tool call missing function name: %#v", gotCalls[0])
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, validate content contains any of the expected substrings
|
|
||||||
DoChat(ctx, t, client, req, toLowerSlice(tt.anyResp), 240*time.Second, 30*time.Second)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,11 +265,3 @@ func loadImageData(t *testing.T, imagePath string) []byte {
|
|||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func toLowerSlice(in []string) []string {
|
|
||||||
out := make([]string, len(in))
|
|
||||||
for i, s := range in {
|
|
||||||
out[i] = strings.ToLower(s)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user