mirror of
https://github.com/ollama/ollama.git
synced 2026-04-22 00:36:11 +02:00
Compare commits
12 Commits
v0.13.0-rc
...
jmorganca/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d96fb7deb3 | ||
|
|
412954c452 | ||
|
|
793248c280 | ||
|
|
8b1b89a984 | ||
|
|
47e272c35a | ||
|
|
417a81fda3 | ||
|
|
dba62ff3a5 | ||
|
|
d70e935526 | ||
|
|
5c1063df7f | ||
|
|
cb485b2019 | ||
|
|
b2af50960f | ||
|
|
eac5b8bfbd |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -15,6 +15,8 @@ ml/backend/**/*.cu linguist-vendored
|
|||||||
ml/backend/**/*.cuh linguist-vendored
|
ml/backend/**/*.cuh linguist-vendored
|
||||||
ml/backend/**/*.m linguist-vendored
|
ml/backend/**/*.m linguist-vendored
|
||||||
ml/backend/**/*.metal linguist-vendored
|
ml/backend/**/*.metal linguist-vendored
|
||||||
|
ml/backend/**/*.comp linguist-vendored
|
||||||
|
ml/backend/**/*.glsl linguist-vendored
|
||||||
ml/backend/**/CMakeLists.txt linguist-vendored
|
ml/backend/**/CMakeLists.txt linguist-vendored
|
||||||
|
|
||||||
llama/build-info.cpp linguist-generated
|
llama/build-info.cpp linguist-generated
|
||||||
|
|||||||
@@ -397,8 +397,8 @@ func checkUserLoggedIn(uiServerPort int) bool {
|
|||||||
// handleConnectURLScheme fetches the connect URL and opens it in the browser
|
// handleConnectURLScheme fetches the connect URL and opens it in the browser
|
||||||
func handleConnectURLScheme() {
|
func handleConnectURLScheme() {
|
||||||
if checkUserLoggedIn(uiServerPort) {
|
if checkUserLoggedIn(uiServerPort) {
|
||||||
slog.Info("user is already logged in, opening settings instead")
|
slog.Info("user is already logged in, opening app instead")
|
||||||
sendUIRequestMessage("/")
|
showWindow(wv.webview.Window())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,6 +466,8 @@ func handleURLSchemeInCurrentInstance(urlSchemeRequest string) {
|
|||||||
if isConnect {
|
if isConnect {
|
||||||
handleConnectURLScheme()
|
handleConnectURLScheme()
|
||||||
} else {
|
} else {
|
||||||
sendUIRequestMessage("/")
|
if wv.webview != nil {
|
||||||
|
showWindow(wv.webview.Window())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,27 +24,14 @@ bool firstTimeRun,startHidden; // Set in run before initialization
|
|||||||
for (NSURL *url in urls) {
|
for (NSURL *url in urls) {
|
||||||
if ([url.scheme isEqualToString:@"ollama"]) {
|
if ([url.scheme isEqualToString:@"ollama"]) {
|
||||||
NSString *path = url.path;
|
NSString *path = url.path;
|
||||||
if (!path || [path isEqualToString:@""]) {
|
|
||||||
// For URLs like ollama://settings (without triple slash),
|
|
||||||
// the "settings" part is parsed as the host, not the path.
|
|
||||||
// We need to convert it to a path by prepending "/"
|
|
||||||
if (url.host && ![url.host isEqualToString:@""]) {
|
|
||||||
path = [@"/" stringByAppendingString:url.host];
|
|
||||||
} else {
|
|
||||||
path = @"/";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([path isEqualToString:@"/connect"] || [url.host isEqualToString:@"connect"]) {
|
if (path && ([path isEqualToString:@"/connect"] || [url.host isEqualToString:@"connect"])) {
|
||||||
// Special case: handle connect by opening browser instead of app
|
// Special case: handle connect by opening browser instead of app
|
||||||
handleConnectURL();
|
handleConnectURL();
|
||||||
} else {
|
} else {
|
||||||
// Set app to be active and visible
|
// Set app to be active and visible
|
||||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||||
[NSApp activateIgnoringOtherApps:YES];
|
[NSApp activateIgnoringOtherApps:YES];
|
||||||
|
|
||||||
// Open the path with the UI
|
|
||||||
[self uiRequest:path];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -260,7 +247,7 @@ bool firstTimeRun,startHidden; // Set in run before initialization
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)openHelp:(id)sender {
|
- (void)openHelp:(id)sender {
|
||||||
NSURL *url = [NSURL URLWithString:@"https://github.com/ollama/ollama/tree/main/docs"];
|
NSURL *url = [NSURL URLWithString:@"https://docs.ollama.com/"];
|
||||||
[[NSWorkspace sharedWorkspace] openURL:url];
|
[[NSWorkspace sharedWorkspace] openURL:url];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ func handleURLSchemeRequest(urlScheme string) {
|
|||||||
if isConnect {
|
if isConnect {
|
||||||
handleConnectURLScheme()
|
handleConnectURLScheme()
|
||||||
} else {
|
} else {
|
||||||
sendUIRequestMessage("/")
|
if wv.webview != nil {
|
||||||
|
showWindow(wv.webview.Window())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
50
cmd/eval/README.md
Normal file
50
cmd/eval/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# eval
|
||||||
|
|
||||||
|
Evaluation tool for testing Ollama models.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Run all tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -model llama3.2:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Run specific suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -model llama3.2:latest -suite tool-calling-basic -v
|
||||||
|
```
|
||||||
|
|
||||||
|
List available suites:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Tests
|
||||||
|
|
||||||
|
Edit `suites.go` to add new test suites. Each test needs:
|
||||||
|
|
||||||
|
- `Name`: test identifier
|
||||||
|
- `Prompt`: what to send to the model
|
||||||
|
- `Check`: function to validate the response
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{
|
||||||
|
Name: "my-test",
|
||||||
|
Prompt: "What is 2+2?",
|
||||||
|
Check: Contains("4"),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Available check functions:
|
||||||
|
|
||||||
|
- `HasResponse()` - response is non-empty
|
||||||
|
- `Contains(s)` - response contains substring
|
||||||
|
- `CallsTool(name)` - model called specific tool
|
||||||
|
- `NoTools()` - model called no tools
|
||||||
|
- `MinTools(n)` - model called at least n tools
|
||||||
|
- `All(checks...)` - all checks pass
|
||||||
151
cmd/eval/eval.go
Normal file
151
cmd/eval/eval.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test is a single evaluation test
|
||||||
|
type Test struct {
|
||||||
|
Name string
|
||||||
|
Prompt string
|
||||||
|
System string
|
||||||
|
Tools []api.Tool
|
||||||
|
Think bool
|
||||||
|
Options map[string]any
|
||||||
|
Check func(response string, tools []api.ToolCall) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suite is a collection of tests
|
||||||
|
type Suite struct {
|
||||||
|
Name string
|
||||||
|
Tests []Test
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result holds test execution results
|
||||||
|
type Result struct {
|
||||||
|
Name string
|
||||||
|
Passed bool
|
||||||
|
Error error
|
||||||
|
Duration time.Duration
|
||||||
|
Response string
|
||||||
|
Tools []string
|
||||||
|
ToolCalls []api.ToolCall
|
||||||
|
Thinking bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes a test against a model
|
||||||
|
func Run(ctx context.Context, client *api.Client, model string, test Test) Result {
|
||||||
|
result := Result{Name: test.Name}
|
||||||
|
|
||||||
|
req := &api.ChatRequest{
|
||||||
|
Model: model,
|
||||||
|
Messages: []api.Message{
|
||||||
|
{Role: "user", Content: test.Prompt},
|
||||||
|
},
|
||||||
|
Options: test.Options,
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.System != "" {
|
||||||
|
req.Messages = append([]api.Message{
|
||||||
|
{Role: "system", Content: test.System},
|
||||||
|
}, req.Messages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.Tools) > 0 {
|
||||||
|
req.Tools = test.Tools
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.Think {
|
||||||
|
req.Think = &api.ThinkValue{Value: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp strings.Builder
|
||||||
|
var toolCalls []api.ToolCall
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
err := client.Chat(ctx, req, func(r api.ChatResponse) error {
|
||||||
|
resp.WriteString(r.Message.Content)
|
||||||
|
if r.Message.Thinking != "" {
|
||||||
|
result.Thinking = true
|
||||||
|
}
|
||||||
|
toolCalls = append(toolCalls, r.Message.ToolCalls...)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
result.Duration = time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Response = resp.String()
|
||||||
|
result.Tools = uniqueToolNames(toolCalls)
|
||||||
|
result.ToolCalls = toolCalls
|
||||||
|
result.Passed = test.Check(result.Response, toolCalls)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueToolNames(calls []api.ToolCall) []string {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var names []string
|
||||||
|
for _, c := range calls {
|
||||||
|
if !seen[c.Function.Name] {
|
||||||
|
seen[c.Function.Name] = true
|
||||||
|
names = append(names, c.Function.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check functions for common test patterns
|
||||||
|
|
||||||
|
func HasResponse() func(string, []api.ToolCall) bool {
|
||||||
|
return func(resp string, _ []api.ToolCall) bool {
|
||||||
|
return strings.TrimSpace(resp) != ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Contains(s string) func(string, []api.ToolCall) bool {
|
||||||
|
return func(resp string, _ []api.ToolCall) bool {
|
||||||
|
return strings.Contains(strings.ToLower(resp), strings.ToLower(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallsTool(name string) func(string, []api.ToolCall) bool {
|
||||||
|
return func(_ string, tools []api.ToolCall) bool {
|
||||||
|
for _, t := range tools {
|
||||||
|
if t.Function.Name == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NoTools() func(string, []api.ToolCall) bool {
|
||||||
|
return func(_ string, tools []api.ToolCall) bool {
|
||||||
|
return len(tools) == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinTools(n int) func(string, []api.ToolCall) bool {
|
||||||
|
return func(_ string, tools []api.ToolCall) bool {
|
||||||
|
return len(tools) >= n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func All(checks ...func(string, []api.ToolCall) bool) func(string, []api.ToolCall) bool {
|
||||||
|
return func(resp string, tools []api.ToolCall) bool {
|
||||||
|
for _, check := range checks {
|
||||||
|
if !check(resp, tools) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
217
cmd/eval/main.go
Normal file
217
cmd/eval/main.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
model := flag.String("model", "", "model to evaluate")
|
||||||
|
suite := flag.String("suite", "", "comma-separated list of suites to run (empty runs all)")
|
||||||
|
list := flag.Bool("list", false, "list available suites")
|
||||||
|
verbose := flag.Bool("v", false, "verbose output")
|
||||||
|
timeout := flag.Int("timeout", 60, "timeout per test in seconds")
|
||||||
|
export := flag.String("export", "eval-results.json", "export results to file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *list {
|
||||||
|
for _, s := range suites {
|
||||||
|
fmt.Printf("%s (%d tests)\n", s.Name, len(s.Tests))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *model == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: -model parameter is required\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := api.ClientFromEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
if err := client.Heartbeat(ctx); err != nil {
|
||||||
|
cancel()
|
||||||
|
fmt.Fprintf(os.Stderr, "error: cannot connect to ollama\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
selected := suites
|
||||||
|
if *suite != "" {
|
||||||
|
suiteNames := strings.Split(*suite, ",")
|
||||||
|
selected = []Suite{}
|
||||||
|
var notFound []string
|
||||||
|
|
||||||
|
for _, name := range suiteNames {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, s := range suites {
|
||||||
|
if s.Name == name {
|
||||||
|
selected = append(selected, s)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
notFound = append(notFound, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(notFound) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: suite(s) not found: %s\n", strings.Join(notFound, ", "))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []Result
|
||||||
|
for _, s := range selected {
|
||||||
|
if *verbose {
|
||||||
|
fmt.Printf("\n%s (%d tests)\n", s.Name, len(s.Tests))
|
||||||
|
}
|
||||||
|
for i, test := range s.Tests {
|
||||||
|
if test.Options == nil {
|
||||||
|
test.Options = map[string]any{"temperature": 0.1}
|
||||||
|
}
|
||||||
|
if test.Check == nil {
|
||||||
|
test.Check = HasResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
if *verbose {
|
||||||
|
fmt.Printf(" [%d/%d] %s... ", i+1, len(s.Tests), test.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*timeout)*time.Second)
|
||||||
|
result := Run(ctx, client, *model, test)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
results = append(results, result)
|
||||||
|
|
||||||
|
if *verbose {
|
||||||
|
if result.Error != nil {
|
||||||
|
fmt.Printf("ERROR: %v\n", result.Error)
|
||||||
|
} else if result.Passed {
|
||||||
|
fmt.Printf("PASS (%.2fs)", result.Duration.Seconds())
|
||||||
|
if len(result.Tools) > 0 || result.Thinking {
|
||||||
|
fmt.Printf(" [")
|
||||||
|
if len(result.Tools) > 0 {
|
||||||
|
fmt.Printf("tools: %s", strings.Join(result.Tools, ","))
|
||||||
|
}
|
||||||
|
if result.Thinking {
|
||||||
|
if len(result.Tools) > 0 {
|
||||||
|
fmt.Printf(", ")
|
||||||
|
}
|
||||||
|
fmt.Printf("thinking")
|
||||||
|
}
|
||||||
|
fmt.Printf("]")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Print tool calls with details
|
||||||
|
if len(result.ToolCalls) > 0 {
|
||||||
|
fmt.Printf(" Tool Calls:\n")
|
||||||
|
for _, tc := range result.ToolCalls {
|
||||||
|
argsJSON, _ := json.Marshal(tc.Function.Arguments)
|
||||||
|
fmt.Printf(" - %s: %s\n", tc.Function.Name, string(argsJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print response if there is one
|
||||||
|
if result.Response != "" {
|
||||||
|
fmt.Printf(" Response: %s\n", result.Response)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("FAIL (%.2fs)\n", result.Duration.Seconds())
|
||||||
|
|
||||||
|
// Print tool calls with details even on failure
|
||||||
|
if len(result.ToolCalls) > 0 {
|
||||||
|
fmt.Printf(" Tool Calls:\n")
|
||||||
|
for _, tc := range result.ToolCalls {
|
||||||
|
argsJSON, _ := json.Marshal(tc.Function.Arguments)
|
||||||
|
fmt.Printf(" - %s: %s\n", tc.Function.Name, string(argsJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print response even on failure
|
||||||
|
if result.Response != "" {
|
||||||
|
fmt.Printf(" Response: %s\n", result.Response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printSummary(results)
|
||||||
|
|
||||||
|
if *export != "" {
|
||||||
|
if err := writeJSON(*export, results); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: export failed: %v\n", err)
|
||||||
|
} else if *verbose {
|
||||||
|
fmt.Printf("\nResults: %s\n", *export)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if anyFailed(results) {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSummary(results []Result) {
|
||||||
|
var passed, failed, errors int
|
||||||
|
for _, r := range results {
|
||||||
|
if r.Error != nil {
|
||||||
|
errors++
|
||||||
|
} else if r.Passed {
|
||||||
|
passed++
|
||||||
|
} else {
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total := len(results)
|
||||||
|
rate := 0.0
|
||||||
|
if total > 0 {
|
||||||
|
rate = float64(passed) / float64(total) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%d/%d passed (%.1f%%)", passed, total, rate)
|
||||||
|
if errors > 0 {
|
||||||
|
fmt.Printf(", %d errors", errors)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func anyFailed(results []Result) bool {
|
||||||
|
for _, r := range results {
|
||||||
|
if !r.Passed || r.Error != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(path string, results []Result) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(results)
|
||||||
|
}
|
||||||
178
cmd/eval/suites.go
Normal file
178
cmd/eval/suites.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/ollama/ollama/api"
|
||||||
|
|
||||||
|
var suites = []Suite{
|
||||||
|
{
|
||||||
|
Name: "basic-qa",
|
||||||
|
Tests: []Test{
|
||||||
|
{
|
||||||
|
Name: "simple-math",
|
||||||
|
Prompt: "What is 2+2? Reply with just the number.",
|
||||||
|
Check: Contains("4"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "capital-city",
|
||||||
|
Prompt: "What is the capital of France? Reply with just the city name.",
|
||||||
|
Check: Contains("Paris"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "greeting",
|
||||||
|
Prompt: "Say hello",
|
||||||
|
Check: HasResponse(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "reasoning",
|
||||||
|
Tests: []Test{
|
||||||
|
{
|
||||||
|
Name: "logic-puzzle",
|
||||||
|
Prompt: "If all roses are flowers and some flowers fade quickly, can we conclude that some roses fade quickly? Answer yes or no.",
|
||||||
|
Check: Contains("no"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "counting",
|
||||||
|
Prompt: "How many letters are in the word 'HELLO'?",
|
||||||
|
Check: Contains("5"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "instruction-following",
|
||||||
|
Tests: []Test{
|
||||||
|
{
|
||||||
|
Name: "json-output",
|
||||||
|
Prompt: "Reply with a JSON object containing a 'status' field set to 'ok'.",
|
||||||
|
Check: All(Contains("status"), Contains("ok")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "system-prompt",
|
||||||
|
Prompt: "What is your name?",
|
||||||
|
System: "You are a helpful assistant named TestBot. When asked your name, always respond with 'TestBot'.",
|
||||||
|
Check: Contains("TestBot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tool-calling-basic",
|
||||||
|
Tests: []Test{
|
||||||
|
{
|
||||||
|
Name: "single-tool",
|
||||||
|
Prompt: "What's the weather like in San Francisco?",
|
||||||
|
Tools: []api.Tool{weatherTool},
|
||||||
|
Check: CallsTool("get_weather"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tool-selection",
|
||||||
|
Prompt: "What time is it in Tokyo?",
|
||||||
|
Tools: []api.Tool{weatherTool, timeTool},
|
||||||
|
Check: CallsTool("get_time"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "no-tool-needed",
|
||||||
|
Prompt: "What is 2+2?",
|
||||||
|
Tools: []api.Tool{weatherTool, timeTool},
|
||||||
|
Check: NoTools(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tool-calling-advanced",
|
||||||
|
Tests: []Test{
|
||||||
|
{
|
||||||
|
Name: "parallel-calls",
|
||||||
|
Prompt: "Get the weather in both New York and Los Angeles.",
|
||||||
|
Tools: []api.Tool{weatherTool},
|
||||||
|
Check: All(CallsTool("get_weather"), MinTools(2)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multi-param",
|
||||||
|
Prompt: "Search for Italian restaurants with prices between $20 and $40.",
|
||||||
|
Tools: []api.Tool{restaurantTool},
|
||||||
|
Check: CallsTool("search_restaurants"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tool-calling-thinking",
|
||||||
|
Tests: []Test{
|
||||||
|
{
|
||||||
|
Name: "thinking-before-tool",
|
||||||
|
Prompt: "I need to know the weather in Paris before I decide what to pack.",
|
||||||
|
Tools: []api.Tool{weatherTool},
|
||||||
|
Think: true,
|
||||||
|
Check: CallsTool("get_weather"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "thinking-multi-tool",
|
||||||
|
Prompt: "I'm planning a trip to London. I need to know what time it is there and what the weather is like.",
|
||||||
|
Tools: []api.Tool{weatherTool, timeTool},
|
||||||
|
Think: true,
|
||||||
|
Check: MinTools(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var weatherTool = api.Tool{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Description: "Get the current weather in a given location",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"location"},
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"location": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The city and state",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeTool = api.Tool{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get_time",
|
||||||
|
Description: "Get the current time in a timezone",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"timezone"},
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"timezone": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The timezone name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var restaurantTool = api.Tool{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "search_restaurants",
|
||||||
|
Description: "Search for restaurants",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"cuisine"},
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"cuisine": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "Type of cuisine",
|
||||||
|
},
|
||||||
|
"min_price": {
|
||||||
|
Type: api.PropertyType{"number"},
|
||||||
|
Description: "Minimum price",
|
||||||
|
},
|
||||||
|
"max_price": {
|
||||||
|
Type: api.PropertyType{"number"},
|
||||||
|
Description: "Maximum price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -125,10 +125,20 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml.
|
|||||||
supportedMu := sync.Mutex{}
|
supportedMu := sync.Mutex{}
|
||||||
supported := make(map[string]map[string]map[string]int) // [Library][libDir][ID] = pre-deletion devices index
|
supported := make(map[string]map[string]map[string]int) // [Library][libDir][ID] = pre-deletion devices index
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
|
libDir := devices[i].LibraryPath[len(devices[i].LibraryPath)-1]
|
||||||
if !devices[i].NeedsInitValidation() {
|
if !devices[i].NeedsInitValidation() {
|
||||||
|
// No need to validate, add to the supported map
|
||||||
|
supportedMu.Lock()
|
||||||
|
if _, ok := supported[devices[i].Library]; !ok {
|
||||||
|
supported[devices[i].Library] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if _, ok := supported[devices[i].Library][libDir]; !ok {
|
||||||
|
supported[devices[i].Library][libDir] = make(map[string]int)
|
||||||
|
}
|
||||||
|
supported[devices[i].Library][libDir][devices[i].ID] = i
|
||||||
|
supportedMu.Unlock()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
libDir := devices[i].LibraryPath[len(devices[i].LibraryPath)-1]
|
|
||||||
slog.Debug("verifying if device is supported", "library", libDir, "description", devices[i].Description, "compute", devices[i].Compute(), "id", devices[i].ID, "pci_id", devices[i].PCIID)
|
slog.Debug("verifying if device is supported", "library", libDir, "description", devices[i].Description, "compute", devices[i].Compute(), "id", devices[i].ID, "pci_id", devices[i].PCIID)
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(i int) {
|
go func(i int) {
|
||||||
|
|||||||
@@ -149,9 +149,6 @@ PARAMETER <parameter> <parametervalue>
|
|||||||
|
|
||||||
| Parameter | Description | Value Type | Example Usage |
|
| Parameter | Description | Value Type | Example Usage |
|
||||||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------- |
|
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------- |
|
||||||
| mirostat | Enable Mirostat sampling for controlling perplexity. (default: 0, 0 = disabled, 1 = Mirostat, 2 = Mirostat 2.0) | int | mirostat 0 |
|
|
||||||
| mirostat_eta | Influences how quickly the algorithm responds to feedback from the generated text. A lower learning rate will result in slower adjustments, while a higher learning rate will make the algorithm more responsive. (Default: 0.1) | float | mirostat_eta 0.1 |
|
|
||||||
| mirostat_tau | Controls the balance between coherence and diversity of the output. A lower value will result in more focused and coherent text. (Default: 5.0) | float | mirostat_tau 5.0 |
|
|
||||||
| num_ctx | Sets the size of the context window used to generate the next token. (Default: 2048) | int | num_ctx 4096 |
|
| num_ctx | Sets the size of the context window used to generate the next token. (Default: 2048) | int | num_ctx 4096 |
|
||||||
| repeat_last_n | Sets how far back for the model to look back to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx) | int | repeat_last_n 64 |
|
| repeat_last_n | Sets how far back for the model to look back to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx) | int | repeat_last_n 64 |
|
||||||
| repeat_penalty | Sets how strongly to penalize repetitions. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. (Default: 1.1) | float | repeat_penalty 1.1 |
|
| repeat_penalty | Sets how strongly to penalize repetitions. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. (Default: 1.1) | float | repeat_penalty 1.1 |
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ func (kv KV) OllamaEngineRequired() bool {
|
|||||||
"qwen3vl", "qwen3vlmoe",
|
"qwen3vl", "qwen3vlmoe",
|
||||||
"deepseekocr",
|
"deepseekocr",
|
||||||
"deepseek2",
|
"deepseek2",
|
||||||
|
"nomic-bert",
|
||||||
}, kv.Architecture())
|
}, kv.Architecture())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -388,9 +388,9 @@ func NewFunctionNameMap() *FunctionNameMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the handler with tools and optional last message
|
// Init initializes the handler with tools, optional last message, and think value
|
||||||
// Implements the Parser interface
|
// Implements the Parser interface
|
||||||
func (h *HarmonyMessageHandler) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
|
func (h *HarmonyMessageHandler) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
// Initialize the harmony parser
|
// Initialize the harmony parser
|
||||||
if h.HarmonyParser == nil {
|
if h.HarmonyParser == nil {
|
||||||
h.HarmonyParser = &HarmonyParser{
|
h.HarmonyParser = &HarmonyParser{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package kvcache
|
package kvcache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -20,8 +21,17 @@ type testCase struct {
|
|||||||
expectedMask []float32
|
expectedMask []float32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runPermutedVariants(t *testing.T, fn func(t *testing.T, backend *testBackend)) {
|
||||||
|
t.Helper()
|
||||||
|
for _, permuted := range []bool{false, true} {
|
||||||
|
t.Run(fmt.Sprintf("PermutedV=%t", permuted), func(t *testing.T) {
|
||||||
|
fn(t, &testBackend{permutedV: permuted})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestStore(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
cache := NewCausalCache(nil)
|
cache := NewCausalCache(nil)
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
|
|
||||||
@@ -51,10 +61,11 @@ func TestStore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCache(t, backend, cache, tests)
|
testCache(t, backend, cache, tests)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSWA(t *testing.T) {
|
func TestSWA(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
cache := NewSWACache(1, nil)
|
cache := NewSWACache(1, nil)
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
|
|
||||||
@@ -94,10 +105,11 @@ func TestSWA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCache(t, backend, cache, tests)
|
testCache(t, backend, cache, tests)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSWASeparateBatches(t *testing.T) {
|
func TestSWASeparateBatches(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
cache := NewSWACache(1, nil)
|
cache := NewSWACache(1, nil)
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
|
|
||||||
@@ -174,10 +186,11 @@ func TestSWASeparateBatches(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCache(t, backend, cache, tests)
|
testCache(t, backend, cache, tests)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSWAMem(t *testing.T) {
|
func TestSWAMem(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
cache := NewSWAMemCache(1, 3, nil)
|
cache := NewSWAMemCache(1, 3, nil)
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
|
|
||||||
@@ -217,19 +230,20 @@ func TestSWAMem(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCache(t, backend, cache, tests)
|
testCache(t, backend, cache, tests)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChunkedAttention(t *testing.T) {
|
func TestChunkedAttention(t *testing.T) {
|
||||||
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
cache := NewChunkedAttentionCache(2, nil)
|
cache := NewChunkedAttentionCache(2, nil)
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
|
|
||||||
var b testBackend
|
cache.Init(backend, ml.DTypeF16, 1, 16, 16)
|
||||||
cache.Init(&b, ml.DTypeF16, 1, 16, 16)
|
|
||||||
|
|
||||||
x := float32(math.Inf(-1))
|
x := float32(math.Inf(-1))
|
||||||
|
|
||||||
testCache(
|
testCache(
|
||||||
t, &b, cache,
|
t, backend, cache,
|
||||||
[]testCase{
|
[]testCase{
|
||||||
{
|
{
|
||||||
name: "FirstBatch",
|
name: "FirstBatch",
|
||||||
@@ -275,10 +289,11 @@ func TestChunkedAttention(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSequences(t *testing.T) {
|
func TestSequences(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
cache := NewCausalCache(nil)
|
cache := NewCausalCache(nil)
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
|
|
||||||
@@ -308,10 +323,11 @@ func TestSequences(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCache(t, backend, cache, tests)
|
testCache(t, backend, cache, tests)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemove(t *testing.T) {
|
func TestRemove(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) {
|
cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) {
|
||||||
return key.Add(ctx, shift), nil
|
return key.Add(ctx, shift), nil
|
||||||
})
|
})
|
||||||
@@ -386,10 +402,11 @@ func TestRemove(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCache(t, backend, cache, tests)
|
testCache(t, backend, cache, tests)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCopy(t *testing.T) {
|
func TestCopy(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) { return key, nil })
|
cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) { return key, nil })
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
|
|
||||||
@@ -426,6 +443,7 @@ func TestCopy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCache(t, backend, cache, tests)
|
testCache(t, backend, cache, tests)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCache(t *testing.T, backend ml.Backend, cache Cache, tests []testCase) {
|
func testCache(t *testing.T, backend ml.Backend, cache Cache, tests []testCase) {
|
||||||
@@ -463,7 +481,7 @@ func testCache(t *testing.T, backend ml.Backend, cache Cache, tests []testCase)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCanResume(t *testing.T) {
|
func TestCanResume(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
windowSize := int32(4)
|
windowSize := int32(4)
|
||||||
cache := NewSWACache(windowSize, nil)
|
cache := NewSWACache(windowSize, nil)
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
@@ -534,10 +552,11 @@ func TestCanResume(t *testing.T) {
|
|||||||
if !cache.CanResume(0, 5) {
|
if !cache.CanResume(0, 5) {
|
||||||
t.Errorf("after shift: CanResume(0, 5) = false, want true (latest position)")
|
t.Errorf("after shift: CanResume(0, 5) = false, want true (latest position)")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCanResumeSWAMem(t *testing.T) {
|
func TestCanResumeSWAMem(t *testing.T) {
|
||||||
backend := &testBackend{}
|
runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
|
||||||
windowSize := int32(4)
|
windowSize := int32(4)
|
||||||
memSize := int32(5)
|
memSize := int32(5)
|
||||||
cache := NewSWAMemCache(windowSize, memSize, nil)
|
cache := NewSWAMemCache(windowSize, memSize, nil)
|
||||||
@@ -598,10 +617,12 @@ func TestCanResumeSWAMem(t *testing.T) {
|
|||||||
if !cache.CanResume(0, 7) {
|
if !cache.CanResume(0, 7) {
|
||||||
t.Errorf("after shift: CanResume(0, 7) = false, want true (latest position)")
|
t.Errorf("after shift: CanResume(0, 7) = false, want true (latest position)")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type testBackend struct {
|
type testBackend struct {
|
||||||
ml.Backend
|
ml.Backend
|
||||||
|
permutedV bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *testBackend) NewContext() ml.Context {
|
func (b *testBackend) NewContext() ml.Context {
|
||||||
@@ -612,6 +633,10 @@ func (b *testBackend) NewContextSize(int) ml.Context {
|
|||||||
return &testContext{}
|
return &testContext{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *testBackend) CacheConfig() ml.CacheConfig {
|
||||||
|
return ml.CacheConfig{PermutedV: b.permutedV}
|
||||||
|
}
|
||||||
|
|
||||||
type testContext struct {
|
type testContext struct {
|
||||||
ml.Context
|
ml.Context
|
||||||
}
|
}
|
||||||
@@ -766,6 +791,102 @@ func (t *testTensor) View(ctx ml.Context, offset int, shape ...int) ml.Tensor {
|
|||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *testTensor) Permute(ctx ml.Context, order ...int) ml.Tensor {
|
||||||
|
if len(t.shape) > 4 || len(order) > 4 {
|
||||||
|
panic("permute only supports up to 4 dimensions")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(order) != len(t.shape) && len(order) != 4 {
|
||||||
|
panic("invalid number of dimensions for permute")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ggml_permute expects 4 axes, so fill in any missing dimensions.
|
||||||
|
orderFull := append(make([]int, 0, 4), order...)
|
||||||
|
for len(orderFull) < 4 {
|
||||||
|
orderFull = append(orderFull, len(orderFull))
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := [4]bool{}
|
||||||
|
|
||||||
|
shape4 := [4]int{1, 1, 1, 1}
|
||||||
|
for i := 0; i < len(t.shape) && i < 4; i++ {
|
||||||
|
shape4[i] = t.shape[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
newShape4 := [4]int{1, 1, 1, 1}
|
||||||
|
for axis := range 4 {
|
||||||
|
dst := orderFull[axis]
|
||||||
|
if dst < 0 || dst >= 4 {
|
||||||
|
panic("invalid axis for permute")
|
||||||
|
}
|
||||||
|
if seen[dst] {
|
||||||
|
panic("duplicate axis for permute")
|
||||||
|
}
|
||||||
|
seen[dst] = true
|
||||||
|
newShape4[dst] = shape4[axis]
|
||||||
|
}
|
||||||
|
|
||||||
|
total := len(t.data)
|
||||||
|
newData := make([]float32, total)
|
||||||
|
|
||||||
|
if total > 0 {
|
||||||
|
oldDims := shape4
|
||||||
|
newDims := newShape4
|
||||||
|
|
||||||
|
oldStride := [4]int{1, 1, 1, 1}
|
||||||
|
newStride := [4]int{1, 1, 1, 1}
|
||||||
|
for i := 1; i < 4; i++ {
|
||||||
|
oldStride[i] = oldStride[i-1] * oldDims[i-1]
|
||||||
|
newStride[i] = newStride[i-1] * newDims[i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var coords [4]int
|
||||||
|
var newCoords [4]int
|
||||||
|
|
||||||
|
for idx := range total {
|
||||||
|
remainder := idx
|
||||||
|
for axis := range 4 {
|
||||||
|
dim := oldDims[axis]
|
||||||
|
if dim == 0 {
|
||||||
|
coords[axis] = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
coords[axis] = remainder % dim
|
||||||
|
remainder /= dim
|
||||||
|
}
|
||||||
|
|
||||||
|
for axis := range 4 {
|
||||||
|
newCoords[orderFull[axis]] = coords[axis]
|
||||||
|
}
|
||||||
|
|
||||||
|
newIndex := 0
|
||||||
|
for axis := range 4 {
|
||||||
|
if newDims[axis] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newIndex += newCoords[axis] * newStride[axis]
|
||||||
|
}
|
||||||
|
|
||||||
|
newData[newIndex] = t.data[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
numDims := 4
|
||||||
|
for numDims > 1 && newShape4[numDims-1] <= 1 {
|
||||||
|
numDims--
|
||||||
|
}
|
||||||
|
|
||||||
|
newShape := make([]int, numDims)
|
||||||
|
copy(newShape, newShape4[:numDims])
|
||||||
|
|
||||||
|
return &testTensor{
|
||||||
|
dtype: t.dtype,
|
||||||
|
elementSize: t.elementSize,
|
||||||
|
data: newData,
|
||||||
|
shape: newShape,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *testTensor) SetRows(ctx ml.Context, src ml.Tensor, idxs ml.Tensor) ml.Tensor {
|
func (t *testTensor) SetRows(ctx ml.Context, src ml.Tensor, idxs ml.Tensor) ml.Tensor {
|
||||||
dst := t
|
dst := t
|
||||||
srcTensor := src.(*testTensor)
|
srcTensor := src.(*testTensor)
|
||||||
|
|||||||
@@ -236,11 +236,6 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(c fs.Config) (model.Model, error) {
|
func New(c fs.Config) (model.Model, error) {
|
||||||
if c.Uint("attention.key_length_mla") == 0 {
|
|
||||||
// non-MLA models aren't yet supported
|
|
||||||
return nil, model.ErrUnsupportedModel
|
|
||||||
}
|
|
||||||
|
|
||||||
layers := make([]Layer, c.Uint("block_count"))
|
layers := make([]Layer, c.Uint("block_count"))
|
||||||
|
|
||||||
firstDenseLayerIndex := int(c.Uint("leading_dense_block_count"))
|
firstDenseLayerIndex := int(c.Uint("leading_dense_block_count"))
|
||||||
@@ -259,6 +254,30 @@ func New(c fs.Config) (model.Model, error) {
|
|||||||
keyLength := int(cmp.Or(c.Uint("attention.key_length_mla"), c.Uint("attention.key_length")))
|
keyLength := int(cmp.Or(c.Uint("attention.key_length_mla"), c.Uint("attention.key_length")))
|
||||||
valueLength := int(cmp.Or(c.Uint("attention.value_length_mla"), c.Uint("attention.value_length")))
|
valueLength := int(cmp.Or(c.Uint("attention.value_length_mla"), c.Uint("attention.value_length")))
|
||||||
|
|
||||||
|
var pre []string
|
||||||
|
switch c.String("tokenizer.ggml.pre") {
|
||||||
|
case "deepseek-v3":
|
||||||
|
pre = []string{
|
||||||
|
// Split regex into multiple parts (according to DeepSeek3's regex)
|
||||||
|
"\\p{N}{1,3}",
|
||||||
|
`[一-龥-ゟ゠-ヿ]+`,
|
||||||
|
"[!\"#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~][A-Za-z]+|[^\r\n\\p{L}\\p{P}\\p{S}]?[\\p{L}\\p{M}]+| ?[\\p{P}\\p{S}]+[\r\n]*|\\s*[\r\n]+|\\s+(?!\\S)|\\s+",
|
||||||
|
}
|
||||||
|
case "deepseek-llm":
|
||||||
|
// TODO: these models haven't been vetted so skip for now
|
||||||
|
// pre = []string{
|
||||||
|
// "[\r\n]",
|
||||||
|
// "\\s?[A-Za-zµÀ-ÖØ-öø-ƺƼ-ƿDŽ-ʓʕ-ʯͰ-ͳͶͷͻ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-ՖႠ-ჅᎠ-Ᏽᏸ-ᏽᲐ-ᲺᲽ-Ჿᴀ-ᴫᵫ-ᵷᵹ-ᶚḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℴℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-ⱻⱾ-ⳤⳫ-ⳮⳲⳳꙀ-ꙭꚀ-ꚛꜢ-ꝯꝱ-ꞇꞋ-ꞎꭰ-ꮿff-stﬓ-ﬗA-Za-z𐐀-𐑏𐒰-𐓓𐓘-𐓻𐲀-𐲲𐳀-𐳲𑢠-𑣟𞤀-𞥃]+",
|
||||||
|
// "\\s?[!-/:-~!-/:-~‘-‟ -。]+",
|
||||||
|
// "\\s+$",
|
||||||
|
// "[一-龥ࠀ-一가-]+",
|
||||||
|
// "[0-9]",
|
||||||
|
// }
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return nil, model.ErrUnsupportedTokenizer
|
||||||
|
}
|
||||||
|
|
||||||
m := Model{
|
m := Model{
|
||||||
BytePairEncoding: model.NewBytePairEncoding(
|
BytePairEncoding: model.NewBytePairEncoding(
|
||||||
&model.Vocabulary{
|
&model.Vocabulary{
|
||||||
@@ -273,10 +292,7 @@ func New(c fs.Config) (model.Model, error) {
|
|||||||
c.Ints("tokenizer.ggml.eos_token_ids")...,
|
c.Ints("tokenizer.ggml.eos_token_ids")...,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
// Split regex into multiple parts (according to DeepSeek3's regex)
|
pre...,
|
||||||
"\\p{N}{1,3}",
|
|
||||||
`[一-龥-ゟ゠-ヿ]+`,
|
|
||||||
"[!\"#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~][A-Za-z]+|[^\r\n\\p{L}\\p{P}\\p{S}]?[\\p{L}\\p{M}]+| ?[\\p{P}\\p{S}]+[\r\n]*|\\s*[\r\n]+|\\s+(?!\\S)|\\s+",
|
|
||||||
),
|
),
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
Options: &Options{
|
Options: &Options{
|
||||||
|
|||||||
319
model/parsers/cogito.go
Normal file
319
model/parsers/cogito.go
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CogitoParserState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CogitoCollectingThinking CogitoParserState = iota
|
||||||
|
CogitoCollectingContent
|
||||||
|
CogitoCollectingToolCalls
|
||||||
|
CogitoCollectingToolOutput
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cogitoThinkingCloseTag = "</think>"
|
||||||
|
cogitoToolCallsBeginTag = "<|tool▁calls▁begin|>"
|
||||||
|
cogitoToolCallsEndTag = "<|tool▁calls▁end|>"
|
||||||
|
cogitoToolCallBeginTag = "<|tool▁call▁begin|>"
|
||||||
|
cogitoToolCallEndTag = "<|tool▁call▁end|>"
|
||||||
|
cogitoToolSepTag = "<|tool▁sep|>"
|
||||||
|
cogitoToolOutputBeginTag = "<|tool▁output▁begin|>"
|
||||||
|
cogitoToolOutputEndTag = "<|tool▁output▁end|>"
|
||||||
|
cogitoToolOutputsBeginTag = "<|tool▁outputs▁begin|>"
|
||||||
|
cogitoToolOutputsEndTag = "<|tool▁outputs▁end|>"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CogitoParser struct {
|
||||||
|
state CogitoParserState
|
||||||
|
buffer strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CogitoParser) HasToolSupport() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CogitoParser) HasThinkingSupport() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CogitoParser) setInitialState(lastMessage *api.Message, tools []api.Tool, thinkValue *api.ThinkValue) {
|
||||||
|
prefill := lastMessage != nil && lastMessage.Role == "assistant"
|
||||||
|
|
||||||
|
// Check both model capability AND request preference
|
||||||
|
thinkingEnabled := thinkValue != nil && thinkValue.Bool()
|
||||||
|
// thinkingEnabled should be set to false for tools
|
||||||
|
|
||||||
|
if !thinkingEnabled {
|
||||||
|
p.state = CogitoCollectingContent
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefill && lastMessage.Content != "" {
|
||||||
|
p.state = CogitoCollectingContent
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: for cogito, if there are tools, then we don't want to be thinking
|
||||||
|
if len(tools) > 0 {
|
||||||
|
p.state = CogitoCollectingContent
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.state = CogitoCollectingThinking
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CogitoParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
|
p.setInitialState(lastMessage, tools, thinkValue)
|
||||||
|
return tools
|
||||||
|
}
|
||||||
|
|
||||||
|
type cogitoEvent interface {
|
||||||
|
isCogitoEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
type cogitoEventThinkingContent struct {
|
||||||
|
content string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cogitoEventContent struct {
|
||||||
|
content string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cogitoEventToolCall struct {
|
||||||
|
toolCall api.ToolCall
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cogitoEventThinkingContent) isCogitoEvent() {}
|
||||||
|
func (cogitoEventContent) isCogitoEvent() {}
|
||||||
|
func (cogitoEventToolCall) isCogitoEvent() {}
|
||||||
|
|
||||||
|
func (p *CogitoParser) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) {
|
||||||
|
p.buffer.WriteString(s)
|
||||||
|
events := p.parseEvents()
|
||||||
|
|
||||||
|
var toolCalls []api.ToolCall
|
||||||
|
var contentSb strings.Builder
|
||||||
|
var thinkingSb strings.Builder
|
||||||
|
for _, event := range events {
|
||||||
|
switch event := event.(type) {
|
||||||
|
case cogitoEventToolCall:
|
||||||
|
toolCalls = append(toolCalls, event.toolCall)
|
||||||
|
case cogitoEventThinkingContent:
|
||||||
|
thinkingSb.WriteString(event.content)
|
||||||
|
case cogitoEventContent:
|
||||||
|
contentSb.WriteString(event.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CogitoParser) parseEvents() []cogitoEvent {
|
||||||
|
var all []cogitoEvent
|
||||||
|
|
||||||
|
keepLooping := true
|
||||||
|
for keepLooping {
|
||||||
|
var events []cogitoEvent
|
||||||
|
events, keepLooping = p.eat()
|
||||||
|
if len(events) > 0 {
|
||||||
|
all = append(all, events...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CogitoParser) eat() ([]cogitoEvent, bool) {
|
||||||
|
var events []cogitoEvent
|
||||||
|
bufStr := p.buffer.String()
|
||||||
|
if bufStr == "" {
|
||||||
|
return events, false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.state {
|
||||||
|
case CogitoCollectingThinking:
|
||||||
|
if strings.Contains(bufStr, cogitoThinkingCloseTag) { // thinking[</think>] -> content
|
||||||
|
split := strings.SplitN(bufStr, cogitoThinkingCloseTag, 2)
|
||||||
|
thinking := split[0]
|
||||||
|
thinking = strings.TrimRightFunc(thinking, unicode.IsSpace)
|
||||||
|
|
||||||
|
remaining := split[1]
|
||||||
|
remaining = strings.TrimLeftFunc(remaining, unicode.IsSpace)
|
||||||
|
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(remaining)
|
||||||
|
p.state = CogitoCollectingContent
|
||||||
|
|
||||||
|
if len(thinking) > 0 {
|
||||||
|
events = append(events, cogitoEventThinkingContent{content: thinking})
|
||||||
|
}
|
||||||
|
return events, true
|
||||||
|
} else if overlapLen := overlap(bufStr, cogitoThinkingCloseTag); overlapLen > 0 { // partial </think>
|
||||||
|
beforePartialTag := bufStr[:len(bufStr)-overlapLen]
|
||||||
|
trailingLen := trailingWhitespaceLen(beforePartialTag)
|
||||||
|
ambiguousStart := len(beforePartialTag) - trailingLen
|
||||||
|
|
||||||
|
unambiguous := bufStr[:ambiguousStart]
|
||||||
|
ambiguous := bufStr[ambiguousStart:]
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(ambiguous)
|
||||||
|
if len(unambiguous) > 0 {
|
||||||
|
events = append(events, cogitoEventThinkingContent{content: unambiguous})
|
||||||
|
}
|
||||||
|
return events, false
|
||||||
|
} else { // otherwise its thinking content
|
||||||
|
whitespaceLen := trailingWhitespaceLen(bufStr)
|
||||||
|
ambiguousStart := len(bufStr) - whitespaceLen
|
||||||
|
|
||||||
|
unambiguous := bufStr[:ambiguousStart]
|
||||||
|
ambiguous := bufStr[ambiguousStart:]
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(ambiguous)
|
||||||
|
if len(unambiguous) > 0 {
|
||||||
|
events = append(events, cogitoEventThinkingContent{content: unambiguous})
|
||||||
|
}
|
||||||
|
return events, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case CogitoCollectingContent:
|
||||||
|
switch {
|
||||||
|
case strings.Contains(bufStr, cogitoToolCallsBeginTag): // content[<|tool▁calls▁begin|>] -> tool calls
|
||||||
|
split := strings.SplitN(bufStr, cogitoToolCallsBeginTag, 2)
|
||||||
|
contentBefore := strings.TrimRightFunc(split[0], unicode.IsSpace)
|
||||||
|
remaining := split[1]
|
||||||
|
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(remaining)
|
||||||
|
p.state = CogitoCollectingToolCalls
|
||||||
|
|
||||||
|
if len(contentBefore) > 0 {
|
||||||
|
events = append(events, cogitoEventContent{content: contentBefore})
|
||||||
|
}
|
||||||
|
return events, true
|
||||||
|
case strings.Contains(bufStr, cogitoToolOutputsBeginTag): // content[<|tool▁outputs▁begin|>] -> tool outputs
|
||||||
|
split := strings.SplitN(bufStr, cogitoToolOutputsBeginTag, 2)
|
||||||
|
contentBefore := strings.TrimRightFunc(split[0], unicode.IsSpace)
|
||||||
|
remaining := split[1]
|
||||||
|
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(remaining)
|
||||||
|
p.state = CogitoCollectingToolOutput
|
||||||
|
|
||||||
|
if len(contentBefore) > 0 {
|
||||||
|
events = append(events, cogitoEventContent{content: contentBefore})
|
||||||
|
}
|
||||||
|
return events, true
|
||||||
|
default: // otherwise its content
|
||||||
|
p.buffer.Reset()
|
||||||
|
if len(bufStr) > 0 {
|
||||||
|
events = append(events, cogitoEventContent{content: bufStr})
|
||||||
|
}
|
||||||
|
return events, false
|
||||||
|
}
|
||||||
|
case CogitoCollectingToolCalls:
|
||||||
|
if idx := strings.Index(bufStr, cogitoToolCallBeginTag); idx != -1 {
|
||||||
|
startIdx := idx + len(cogitoToolCallBeginTag)
|
||||||
|
if endIdx := strings.Index(bufStr[startIdx:], cogitoToolCallEndTag); endIdx != -1 {
|
||||||
|
toolCallContent := bufStr[startIdx : startIdx+endIdx]
|
||||||
|
|
||||||
|
if toolCall, err := p.parseToolCallContent(toolCallContent); err == nil {
|
||||||
|
remaining := bufStr[startIdx+endIdx+len(cogitoToolCallEndTag):]
|
||||||
|
remaining = strings.TrimLeftFunc(remaining, unicode.IsSpace)
|
||||||
|
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(remaining)
|
||||||
|
|
||||||
|
events = append(events, cogitoEventToolCall{toolCall: toolCall})
|
||||||
|
return events, true
|
||||||
|
} else {
|
||||||
|
slog.Warn("cogito tool call parsing failed", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx := strings.Index(bufStr, cogitoToolCallsEndTag); idx != -1 {
|
||||||
|
remaining := bufStr[idx+len(cogitoToolCallsEndTag):]
|
||||||
|
remaining = strings.TrimLeftFunc(remaining, unicode.IsSpace)
|
||||||
|
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(remaining)
|
||||||
|
p.state = CogitoCollectingContent
|
||||||
|
|
||||||
|
return events, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, false
|
||||||
|
|
||||||
|
case CogitoCollectingToolOutput:
|
||||||
|
if idx := strings.Index(bufStr, cogitoToolOutputBeginTag); idx != -1 {
|
||||||
|
startIdx := idx + len(cogitoToolOutputBeginTag)
|
||||||
|
if endIdx := strings.Index(bufStr[startIdx:], cogitoToolOutputEndTag); endIdx != -1 {
|
||||||
|
remaining := bufStr[startIdx+endIdx+len(cogitoToolOutputEndTag):]
|
||||||
|
remaining = strings.TrimLeftFunc(remaining, unicode.IsSpace)
|
||||||
|
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(remaining)
|
||||||
|
|
||||||
|
return events, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx := strings.Index(bufStr, cogitoToolOutputsEndTag); idx != -1 {
|
||||||
|
remaining := bufStr[idx+len(cogitoToolOutputsEndTag):]
|
||||||
|
remaining = strings.TrimLeftFunc(remaining, unicode.IsSpace)
|
||||||
|
|
||||||
|
p.buffer.Reset()
|
||||||
|
p.buffer.WriteString(remaining)
|
||||||
|
p.state = CogitoCollectingContent
|
||||||
|
|
||||||
|
return events, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CogitoParser) parseToolCallContent(content string) (api.ToolCall, error) {
|
||||||
|
// Expected format: function<|tool▁sep|>tool_name\n```json\n{args}\n```
|
||||||
|
parts := strings.SplitN(content, cogitoToolSepTag, 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return api.ToolCall{}, errors.New("invalid format")
|
||||||
|
}
|
||||||
|
nameAndArgs := parts[1]
|
||||||
|
|
||||||
|
jsonStart := strings.Index(nameAndArgs, "\n```json\n")
|
||||||
|
if jsonStart == -1 {
|
||||||
|
return api.ToolCall{}, errors.New("invalid format")
|
||||||
|
}
|
||||||
|
toolName := strings.TrimSpace(nameAndArgs[:jsonStart])
|
||||||
|
jsonContent := nameAndArgs[jsonStart+len("\n```json\n"):]
|
||||||
|
|
||||||
|
jsonEnd := strings.Index(jsonContent, "\n```")
|
||||||
|
if jsonEnd == -1 {
|
||||||
|
return api.ToolCall{}, errors.New("invalid format")
|
||||||
|
}
|
||||||
|
argsJSON := jsonContent[:jsonEnd]
|
||||||
|
|
||||||
|
var args api.ToolCallFunctionArguments
|
||||||
|
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
||||||
|
return api.ToolCall{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.ToolCall{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: toolName,
|
||||||
|
Arguments: args,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
565
model/parsers/cogito_test.go
Normal file
565
model/parsers/cogito_test.go
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCogitoParser(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expectedContent string
|
||||||
|
expectedThinking string
|
||||||
|
expectedToolCalls []api.ToolCall
|
||||||
|
tools []api.Tool
|
||||||
|
lastMessage *api.Message
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple_content",
|
||||||
|
input: "This is a simple response.",
|
||||||
|
expectedContent: "This is a simple response.",
|
||||||
|
expectedThinking: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "thinking_only",
|
||||||
|
input: "This is thinking content.</think>This is response content.",
|
||||||
|
expectedContent: "This is response content.",
|
||||||
|
expectedThinking: "This is thinking content.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool_call_simple",
|
||||||
|
input: `<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_weather
|
||||||
|
` + "```json\n" + `{"location":"Paris"}
|
||||||
|
` + "```" + `<|tool▁call▁end|><|tool▁calls▁end|>`,
|
||||||
|
expectedToolCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "Paris",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: []api.Tool{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"location": {Type: api.PropertyType{"string"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "thinking_with_tool_call",
|
||||||
|
input: `I need to check the weather.</think><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_weather
|
||||||
|
` + "```json\n" + `{"location":"Paris"}
|
||||||
|
` + "```" + `<|tool▁call▁end|><|tool▁calls▁end|>`,
|
||||||
|
expectedContent: "I need to check the weather.</think>",
|
||||||
|
expectedThinking: "", // No thinking when tools are present (Cogito-specific behavior)
|
||||||
|
expectedToolCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "Paris",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: []api.Tool{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"location": {Type: api.PropertyType{"string"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple_tool_calls",
|
||||||
|
input: `<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_weather
|
||||||
|
` + "```json\n" + `{"location":"Paris"}
|
||||||
|
` + "```" + `<|tool▁call▁end|>
|
||||||
|
<|tool▁call▁begin|>function<|tool▁sep|>get_weather
|
||||||
|
` + "```json\n" + `{"location":"London"}
|
||||||
|
` + "```" + `<|tool▁call▁end|><|tool▁calls▁end|>`,
|
||||||
|
expectedToolCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "Paris",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "London",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: []api.Tool{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"location": {Type: api.PropertyType{"string"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex_tool_arguments",
|
||||||
|
input: `<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>process_data
|
||||||
|
` + "```json\n" + `{"items":["item1","item2"],"config":{"enabled":true,"threshold":0.95},"count":42}
|
||||||
|
` + "```" + `<|tool▁call▁end|><|tool▁calls▁end|>`,
|
||||||
|
expectedToolCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "process_data",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"items": []any{"item1", "item2"},
|
||||||
|
"config": map[string]any{"enabled": true, "threshold": 0.95},
|
||||||
|
"count": 42.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool_output_parsing",
|
||||||
|
input: `<|tool▁outputs▁begin|><|tool▁output▁begin|>{"temperature": 22, "condition": "sunny"}<|tool▁output▁end|><|tool▁outputs▁end|>`,
|
||||||
|
expectedContent: "",
|
||||||
|
expectedThinking: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "thinking_with_multiline_content",
|
||||||
|
input: `This is line 1
|
||||||
|
This is line 2
|
||||||
|
This is line 3</think>Final response here.`,
|
||||||
|
expectedContent: "Final response here.",
|
||||||
|
expectedThinking: "This is line 1\nThis is line 2\nThis is line 3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no_thinking_simple",
|
||||||
|
input: "This is content.",
|
||||||
|
expectedContent: "This is content.",
|
||||||
|
expectedThinking: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefill_content_only",
|
||||||
|
input: "Continuing from previous content.",
|
||||||
|
expectedContent: "Continuing from previous content.",
|
||||||
|
lastMessage: &api.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "Previous content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefill_with_thinking",
|
||||||
|
input: "Continuing thinking</think>Continuing content.",
|
||||||
|
expectedContent: "Continuing content.",
|
||||||
|
expectedThinking: "Continuing thinking",
|
||||||
|
lastMessage: &api.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Edge cases
|
||||||
|
{
|
||||||
|
name: "nested_think_tags_in_thinking",
|
||||||
|
input: "I'm thinking <think>nested</think> more thinking</think>Final content.",
|
||||||
|
expectedContent: "more thinking</think>Final content.",
|
||||||
|
expectedThinking: "I'm thinking <think>nested",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple_think_close_tags",
|
||||||
|
input: "First thinking</think>Content</think>More content.",
|
||||||
|
expectedContent: "Content</think>More content.",
|
||||||
|
expectedThinking: "First thinking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty_thinking_content",
|
||||||
|
input: "</think>Just content here.",
|
||||||
|
expectedContent: "</think>Just content here.",
|
||||||
|
expectedThinking: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "thinking_disabled_with_think_tags",
|
||||||
|
input: "Content with </think> tags should be treated as content.",
|
||||||
|
expectedContent: "Content with </think> tags should be treated as content.",
|
||||||
|
expectedThinking: "",
|
||||||
|
lastMessage: &api.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "existing", // Forces non-thinking mode
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Use thinking-enabled parser for tests that expect thinking
|
||||||
|
hasThinking := tt.expectedThinking != ""
|
||||||
|
parser := &CogitoParser{} // it has thinking support
|
||||||
|
parser.Init(tt.tools, tt.lastMessage, &api.ThinkValue{Value: hasThinking}) // but we should set it with the request that the user wants
|
||||||
|
|
||||||
|
content, thinking, toolCalls, err := parser.Add(tt.input, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.expectedContent, content); diff != "" {
|
||||||
|
t.Errorf("content mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.expectedThinking, thinking); diff != "" {
|
||||||
|
t.Errorf("thinking mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.expectedToolCalls, toolCalls); diff != "" {
|
||||||
|
t.Errorf("tool calls mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCogitoParser_Streaming(t *testing.T) {
|
||||||
|
parser := &CogitoParser{}
|
||||||
|
parser.Init(nil, nil, &api.ThinkValue{Value: true})
|
||||||
|
|
||||||
|
chunks := []string{
|
||||||
|
"This is ",
|
||||||
|
"thinking content",
|
||||||
|
".</think>This is ",
|
||||||
|
"content.<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_tool\n```json\n{\"arg\":\"value\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>",
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalContent, finalThinking strings.Builder
|
||||||
|
var finalToolCalls []api.ToolCall
|
||||||
|
|
||||||
|
for i, chunk := range chunks {
|
||||||
|
done := i == len(chunks)-1
|
||||||
|
content, thinking, toolCalls, err := parser.Add(chunk, done)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add() error on chunk %d: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalContent.WriteString(content)
|
||||||
|
finalThinking.WriteString(thinking)
|
||||||
|
finalToolCalls = append(finalToolCalls, toolCalls...)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedContent := "This is content."
|
||||||
|
expectedThinking := "This is thinking content."
|
||||||
|
expectedToolCalls := []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "test_tool",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"arg": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalContent.String() != expectedContent {
|
||||||
|
t.Errorf("expected content %q, got %q", expectedContent, finalContent.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalThinking.String() != expectedThinking {
|
||||||
|
t.Errorf("expected thinking %q, got %q", expectedThinking, finalThinking.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(expectedToolCalls, finalToolCalls); diff != "" {
|
||||||
|
t.Errorf("tool calls mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCogitoParser_StreamingEdgeCases(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
chunks []string
|
||||||
|
expectedContent string
|
||||||
|
expectedThinking string
|
||||||
|
expectedToolCalls []api.ToolCall
|
||||||
|
hasThinkingSupport bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "split_thinking_tag",
|
||||||
|
chunks: []string{
|
||||||
|
"This is thinking content</thi",
|
||||||
|
"nk>This is content.",
|
||||||
|
},
|
||||||
|
expectedContent: "This is content.",
|
||||||
|
expectedThinking: "This is thinking content",
|
||||||
|
hasThinkingSupport: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "split_tool_calls_begin_tag_conservative_parsing",
|
||||||
|
chunks: []string{
|
||||||
|
"Content before<|tool▁calls▁beg",
|
||||||
|
"in|><|tool▁call▁begin|>function<|tool▁sep|>test\n```json\n{}\n```<|tool▁call▁end|><|tool▁calls▁end|>",
|
||||||
|
},
|
||||||
|
// Parser is conservative - treats incomplete tags as content
|
||||||
|
expectedContent: "Content before<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test\n```json\n{}\n```<|tool▁call▁end|><|tool▁calls▁end|>",
|
||||||
|
expectedToolCalls: nil,
|
||||||
|
hasThinkingSupport: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "thinking_disabled_with_split_tags",
|
||||||
|
chunks: []string{
|
||||||
|
"Content with </thi",
|
||||||
|
"nk> should be treated as content.",
|
||||||
|
},
|
||||||
|
expectedContent: "Content with </think> should be treated as content.",
|
||||||
|
expectedThinking: "",
|
||||||
|
hasThinkingSupport: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
parser := &CogitoParser{}
|
||||||
|
parser.Init(nil, nil, &api.ThinkValue{Value: tt.hasThinkingSupport})
|
||||||
|
|
||||||
|
var finalContent, finalThinking strings.Builder
|
||||||
|
var finalToolCalls []api.ToolCall
|
||||||
|
|
||||||
|
for i, chunk := range tt.chunks {
|
||||||
|
done := i == len(tt.chunks)-1
|
||||||
|
content, thinking, toolCalls, err := parser.Add(chunk, done)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add() error on chunk %d: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalContent.WriteString(content)
|
||||||
|
finalThinking.WriteString(thinking)
|
||||||
|
finalToolCalls = append(finalToolCalls, toolCalls...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalContent.String() != tt.expectedContent {
|
||||||
|
t.Errorf("expected content %q, got %q", tt.expectedContent, finalContent.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalThinking.String() != tt.expectedThinking {
|
||||||
|
t.Errorf("expected thinking %q, got %q", tt.expectedThinking, finalThinking.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.expectedToolCalls, finalToolCalls); diff != "" {
|
||||||
|
t.Errorf("tool calls mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCogitoParser_HasToolSupport(t *testing.T) {
|
||||||
|
parser := &CogitoParser{}
|
||||||
|
if !parser.HasToolSupport() {
|
||||||
|
t.Error("CogitoParser should support tools")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCogitoParser_Init(t *testing.T) {
|
||||||
|
parser := &CogitoParser{}
|
||||||
|
|
||||||
|
tools := []api.Tool{
|
||||||
|
{Function: api.ToolFunction{Name: "test_tool"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMessage := &api.Message{Role: "assistant", Content: "previous"}
|
||||||
|
|
||||||
|
returnedTools := parser.Init(tools, lastMessage, nil)
|
||||||
|
|
||||||
|
if len(returnedTools) != len(tools) {
|
||||||
|
t.Errorf("expected %d tools returned, got %d", len(tools), len(returnedTools))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCogitoParser_parseToolCallContent(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
expected api.ToolCall
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid_tool_call_standard_format",
|
||||||
|
content: `function<|tool▁sep|>get_weather
|
||||||
|
` + "```json\n" + `{"location":"Paris"}
|
||||||
|
` + "```",
|
||||||
|
expected: api.ToolCall{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "Paris",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid_tool_call_complex_args",
|
||||||
|
content: `function<|tool▁sep|>process_data
|
||||||
|
` + "```json\n" + `{"items":["item1","item2"],"config":{"enabled":true},"count":42}
|
||||||
|
` + "```",
|
||||||
|
expected: api.ToolCall{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "process_data",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"items": []any{"item1", "item2"},
|
||||||
|
"config": map[string]any{"enabled": true},
|
||||||
|
"count": 42.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid_tool_call_empty_args",
|
||||||
|
content: `function<|tool▁sep|>no_args_tool
|
||||||
|
` + "```json\n" + `{}
|
||||||
|
` + "```",
|
||||||
|
expected: api.ToolCall{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "no_args_tool",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing_separator",
|
||||||
|
content: `functionget_weather` + "```json\n" + `{"location":"Paris"}` + "\n```",
|
||||||
|
expected: api.ToolCall{},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_function_type",
|
||||||
|
content: `not_function<|tool▁sep|>get_weather` + "```json\n" + `{"location":"Paris"}` + "\n```",
|
||||||
|
expected: api.ToolCall{},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing_json_block_start",
|
||||||
|
content: `function<|tool▁sep|>get_weather{"location":"Paris"}` + "```",
|
||||||
|
expected: api.ToolCall{},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing_json_block_end",
|
||||||
|
content: `function<|tool▁sep|>get_weather` + "```json\n" + `{"location":"Paris"}`,
|
||||||
|
expected: api.ToolCall{},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_json",
|
||||||
|
content: `function<|tool▁sep|>get_weather` + "```json\n" + `{location:Paris}` + "\n```",
|
||||||
|
expected: api.ToolCall{},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty_function_type",
|
||||||
|
content: `<|tool▁sep|>get_weather` + "```json\n" + `{"location":"Paris"}` + "\n```",
|
||||||
|
expected: api.ToolCall{},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool_with_spaces_in_name",
|
||||||
|
content: `function<|tool▁sep|> get_weather
|
||||||
|
` + "```json\n" + `{"location":"Paris"}
|
||||||
|
` + "```",
|
||||||
|
expected: api.ToolCall{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "Paris",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool_with_multiline_json",
|
||||||
|
content: `function<|tool▁sep|>get_weather
|
||||||
|
` + "```json\n" + `{
|
||||||
|
"location": "Paris",
|
||||||
|
"units": "metric"
|
||||||
|
}
|
||||||
|
` + "```",
|
||||||
|
expected: api.ToolCall{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "Paris",
|
||||||
|
"units": "metric",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool_with_nested_objects",
|
||||||
|
content: `function<|tool▁sep|>complex_tool
|
||||||
|
` + "```json\n" + `{"nested":{"deep":{"value":123}}}
|
||||||
|
` + "```",
|
||||||
|
expected: api.ToolCall{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "complex_tool",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"nested": map[string]any{
|
||||||
|
"deep": map[string]any{
|
||||||
|
"value": 123.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
parser := &CogitoParser{}
|
||||||
|
|
||||||
|
result, err := parser.parseToolCallContent(tt.content)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error but got none")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||||
|
t.Errorf("tool call mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
44
model/parsers/intellect3.go
Normal file
44
model/parsers/intellect3.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
"github.com/ollama/ollama/thinking"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Intellect3Parser combines thinking support using
|
||||||
|
// the built-in thinking parser, with tool call support
|
||||||
|
// via qwen3-coder's parser.
|
||||||
|
type Intellect3Parser struct {
|
||||||
|
thinkingParser thinking.Parser
|
||||||
|
toolParser Qwen3CoderParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Intellect3Parser) HasToolSupport() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Intellect3Parser) HasThinkingSupport() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Intellect3Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
|
p.thinkingParser = thinking.Parser{
|
||||||
|
OpeningTag: "<think>",
|
||||||
|
ClosingTag: "</think>",
|
||||||
|
}
|
||||||
|
p.toolParser = Qwen3CoderParser{}
|
||||||
|
return p.toolParser.Init(tools, lastMessage, thinkValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Intellect3Parser) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) {
|
||||||
|
// First extract thinking content
|
||||||
|
thinkingContent, remainingContent := p.thinkingParser.AddContent(s)
|
||||||
|
|
||||||
|
// Then process the remaining content for tool calls
|
||||||
|
toolContent, _, toolCalls, err := p.toolParser.Add(remainingContent, done)
|
||||||
|
if err != nil {
|
||||||
|
return "", thinkingContent, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolContent, thinkingContent, toolCalls, nil
|
||||||
|
}
|
||||||
542
model/parsers/intellect3_test.go
Normal file
542
model/parsers/intellect3_test.go
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntellect3ParserThinkingOnly(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
desc string
|
||||||
|
chunks []string
|
||||||
|
wantText string
|
||||||
|
wantThink string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple thinking content",
|
||||||
|
chunks: []string{"<think>I need to analyze this</think>Here is my response"},
|
||||||
|
wantText: "Here is my response",
|
||||||
|
wantThink: "I need to analyze this",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "thinking with whitespace",
|
||||||
|
chunks: []string{"<think>\n Some thoughts \n</think>\n\nContent"},
|
||||||
|
wantText: "Content",
|
||||||
|
wantThink: "Some thoughts \n", // Thinking parser preserves internal whitespace
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "thinking only",
|
||||||
|
chunks: []string{"<think>Just thinking</think>"},
|
||||||
|
wantText: "",
|
||||||
|
wantThink: "Just thinking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "no thinking tags",
|
||||||
|
chunks: []string{"Just regular content"},
|
||||||
|
wantText: "Just regular content",
|
||||||
|
wantThink: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "streaming thinking content",
|
||||||
|
chunks: []string{"<think>Fir", "st part", " second part</think>Content"},
|
||||||
|
wantText: "Content",
|
||||||
|
wantThink: "First part second part",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "partial opening tag",
|
||||||
|
chunks: []string{"<thi", "nk>Thinking</think>Content"},
|
||||||
|
wantText: "Content",
|
||||||
|
wantThink: "Thinking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "partial closing tag",
|
||||||
|
chunks: []string{"<think>Thinking</thi", "nk>Content"},
|
||||||
|
wantText: "Content",
|
||||||
|
wantThink: "Thinking",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
parser := Intellect3Parser{}
|
||||||
|
parser.Init(nil, nil, nil)
|
||||||
|
|
||||||
|
var gotText, gotThink string
|
||||||
|
for i, chunk := range tc.chunks {
|
||||||
|
isLast := i == len(tc.chunks)-1
|
||||||
|
text, think, calls, err := parser.Add(chunk, isLast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
gotText += text
|
||||||
|
gotThink += think
|
||||||
|
if len(calls) > 0 {
|
||||||
|
t.Fatalf("expected no tool calls, got %v", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotText != tc.wantText {
|
||||||
|
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||||
|
}
|
||||||
|
if gotThink != tc.wantThink {
|
||||||
|
t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntellect3ParserToolCallsOnly(t *testing.T) {
|
||||||
|
tools := []api.Tool{
|
||||||
|
tool("get_weather", map[string]api.ToolProperty{
|
||||||
|
"location": {Type: api.PropertyType{"string"}},
|
||||||
|
"unit": {Type: api.PropertyType{"string"}},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
desc string
|
||||||
|
chunks []string
|
||||||
|
wantText string
|
||||||
|
wantCalls []api.ToolCall
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple tool call",
|
||||||
|
chunks: []string{
|
||||||
|
"Let me check the weather<tool_call><function=get_weather>\n<parameter=location>\nSan Francisco\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||||
|
},
|
||||||
|
wantText: "Let me check the weather",
|
||||||
|
wantCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "San Francisco",
|
||||||
|
"unit": "celsius",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "tool call streaming",
|
||||||
|
chunks: []string{
|
||||||
|
"Checking<tool_call><function=get_wea",
|
||||||
|
"ther>\n<parameter=location>\nNew York\n</param", // nolint:all
|
||||||
|
"eter>\n<parameter=unit>\nfahrenheit\n</parameter>\n</function></tool_call>Done",
|
||||||
|
},
|
||||||
|
wantText: "CheckingDone",
|
||||||
|
wantCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "New York",
|
||||||
|
"unit": "fahrenheit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multiple tool calls",
|
||||||
|
chunks: []string{
|
||||||
|
"<tool_call><function=get_weather>\n<parameter=location>\nBoston\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||||
|
"<tool_call><function=get_weather>\n<parameter=location>\nSeattle\n</parameter>\n<parameter=unit>\nfahrenheit\n</parameter>\n</function></tool_call>",
|
||||||
|
},
|
||||||
|
wantText: "",
|
||||||
|
wantCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "Boston",
|
||||||
|
"unit": "celsius",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "Seattle",
|
||||||
|
"unit": "fahrenheit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "no tool calls",
|
||||||
|
chunks: []string{"Just regular content"},
|
||||||
|
wantText: "Just regular content",
|
||||||
|
wantCalls: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
parser := Intellect3Parser{}
|
||||||
|
parser.Init(tools, nil, nil)
|
||||||
|
|
||||||
|
var gotText string
|
||||||
|
var gotCalls []api.ToolCall
|
||||||
|
for i, chunk := range tc.chunks {
|
||||||
|
isLast := i == len(tc.chunks)-1
|
||||||
|
text, think, calls, err := parser.Add(chunk, isLast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
gotText += text
|
||||||
|
gotCalls = append(gotCalls, calls...)
|
||||||
|
if think != "" {
|
||||||
|
t.Fatalf("expected no thinking, got %q", think)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotText != tc.wantText {
|
||||||
|
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotCalls, tc.wantCalls) {
|
||||||
|
t.Errorf("tool calls: got %#v, want %#v", gotCalls, tc.wantCalls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntellect3ParserCombined(t *testing.T) {
|
||||||
|
tools := []api.Tool{
|
||||||
|
tool("get_weather", map[string]api.ToolProperty{
|
||||||
|
"location": {Type: api.PropertyType{"string"}},
|
||||||
|
"unit": {Type: api.PropertyType{"string"}},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
desc string
|
||||||
|
chunks []string
|
||||||
|
wantText string
|
||||||
|
wantThink string
|
||||||
|
wantCalls []api.ToolCall
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "thinking then tool call",
|
||||||
|
chunks: []string{
|
||||||
|
"<think>Need to get weather data</think>Let me check<tool_call><function=get_weather>\n<parameter=location>\nParis\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||||
|
},
|
||||||
|
wantText: "Let me check",
|
||||||
|
wantThink: "Need to get weather data",
|
||||||
|
wantCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "Paris",
|
||||||
|
"unit": "celsius",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "thinking, tool call, and final content",
|
||||||
|
chunks: []string{
|
||||||
|
"<think>User wants weather info</think>Checking weather<tool_call><function=get_weather>\n<parameter=location>\nTokyo\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>Done!",
|
||||||
|
},
|
||||||
|
wantText: "Checking weatherDone!",
|
||||||
|
wantThink: "User wants weather info",
|
||||||
|
wantCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "Tokyo",
|
||||||
|
"unit": "celsius",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "streaming combined content",
|
||||||
|
chunks: []string{
|
||||||
|
"<think>Analyzing",
|
||||||
|
" the request</think>",
|
||||||
|
"Let me help<tool_call>",
|
||||||
|
"<function=get_weather>\n<parameter=location>\nLondon",
|
||||||
|
"\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function>",
|
||||||
|
"</tool_call>There you go!",
|
||||||
|
},
|
||||||
|
wantText: "Let me helpThere you go!",
|
||||||
|
wantThink: "Analyzing the request",
|
||||||
|
wantCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "London",
|
||||||
|
"unit": "celsius",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multiple tool calls with thinking",
|
||||||
|
chunks: []string{
|
||||||
|
"<think>Need multiple locations</think>",
|
||||||
|
"<tool_call><function=get_weather>\n<parameter=location>\nBoston\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||||
|
"and<tool_call><function=get_weather>\n<parameter=location>\nBerlin\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||||
|
},
|
||||||
|
wantText: "and",
|
||||||
|
wantThink: "Need multiple locations",
|
||||||
|
wantCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "Boston",
|
||||||
|
"unit": "celsius",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "Berlin",
|
||||||
|
"unit": "celsius",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
parser := Intellect3Parser{}
|
||||||
|
parser.Init(tools, nil, nil)
|
||||||
|
|
||||||
|
var gotText, gotThink string
|
||||||
|
var gotCalls []api.ToolCall
|
||||||
|
for i, chunk := range tc.chunks {
|
||||||
|
isLast := i == len(tc.chunks)-1
|
||||||
|
text, think, calls, err := parser.Add(chunk, isLast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
gotText += text
|
||||||
|
gotThink += think
|
||||||
|
gotCalls = append(gotCalls, calls...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotText != tc.wantText {
|
||||||
|
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||||
|
}
|
||||||
|
if gotThink != tc.wantThink {
|
||||||
|
t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotCalls, tc.wantCalls) {
|
||||||
|
t.Errorf("tool calls: got %#v, want %#v", gotCalls, tc.wantCalls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntellect3ParserEdgeCases(t *testing.T) {
|
||||||
|
tools := []api.Tool{
|
||||||
|
tool("test_func", map[string]api.ToolProperty{
|
||||||
|
"param": {Type: api.PropertyType{"string"}},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
desc string
|
||||||
|
chunks []string
|
||||||
|
wantText string
|
||||||
|
wantThink string
|
||||||
|
wantCalls int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "empty input",
|
||||||
|
chunks: []string{""},
|
||||||
|
wantText: "",
|
||||||
|
wantThink: "",
|
||||||
|
wantCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "only whitespace",
|
||||||
|
chunks: []string{" \n \t "},
|
||||||
|
wantText: "",
|
||||||
|
wantThink: "",
|
||||||
|
wantCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "unclosed thinking tag",
|
||||||
|
chunks: []string{"<think>Never closes"},
|
||||||
|
wantText: "",
|
||||||
|
wantThink: "Never closes",
|
||||||
|
wantCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "unclosed tool call tag",
|
||||||
|
chunks: []string{"<tool_call><function=test_func>\n<parameter=param>\nvalue\n</parameter>\n</function>"},
|
||||||
|
wantText: "", // Qwen3CoderParser waits for closing tag, doesn't emit partial tool calls
|
||||||
|
wantThink: "",
|
||||||
|
wantCalls: 0, // Won't be parsed until </tool_call> is seen
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "unicode in thinking",
|
||||||
|
chunks: []string{"<think>思考中 🤔</think>答案是 42"},
|
||||||
|
wantText: "答案是 42",
|
||||||
|
wantThink: "思考中 🤔",
|
||||||
|
wantCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fake thinking tag",
|
||||||
|
chunks: []string{"<thinking>This is not the right tag</thinking>Content"},
|
||||||
|
wantText: "<thinking>This is not the right tag</thinking>Content",
|
||||||
|
wantThink: "",
|
||||||
|
wantCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fake tool call tag",
|
||||||
|
chunks: []string{"<tool>Not a tool call</tool>"},
|
||||||
|
wantText: "<tool>Not a tool call</tool>",
|
||||||
|
wantThink: "",
|
||||||
|
wantCalls: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
parser := Intellect3Parser{}
|
||||||
|
parser.Init(tools, nil, nil)
|
||||||
|
|
||||||
|
var gotText, gotThink string
|
||||||
|
var gotCalls []api.ToolCall
|
||||||
|
for i, chunk := range tc.chunks {
|
||||||
|
isLast := i == len(tc.chunks)-1
|
||||||
|
text, think, calls, err := parser.Add(chunk, isLast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
gotText += text
|
||||||
|
gotThink += think
|
||||||
|
gotCalls = append(gotCalls, calls...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotText != tc.wantText {
|
||||||
|
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||||
|
}
|
||||||
|
if gotThink != tc.wantThink {
|
||||||
|
t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink)
|
||||||
|
}
|
||||||
|
if len(gotCalls) != tc.wantCalls {
|
||||||
|
t.Errorf("tool calls count: got %d, want %d", len(gotCalls), tc.wantCalls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntellect3ParserCapabilities(t *testing.T) {
|
||||||
|
parser := Intellect3Parser{}
|
||||||
|
|
||||||
|
if !parser.HasToolSupport() {
|
||||||
|
t.Error("Intellect3Parser should have tool support")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parser.HasThinkingSupport() {
|
||||||
|
t.Error("Intellect3Parser should have thinking support")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntellect3ParserInit(t *testing.T) {
|
||||||
|
parser := Intellect3Parser{}
|
||||||
|
|
||||||
|
tools := []api.Tool{
|
||||||
|
tool("test", map[string]api.ToolProperty{
|
||||||
|
"param": {Type: api.PropertyType{"string"}},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
returnedTools := parser.Init(tools, nil, nil)
|
||||||
|
|
||||||
|
// Should return tools unchanged (delegated to Qwen3CoderParser)
|
||||||
|
if !reflect.DeepEqual(returnedTools, tools) {
|
||||||
|
t.Errorf("Init should return tools unchanged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntellect3ParserWhitespaceHandling(t *testing.T) {
|
||||||
|
tools := []api.Tool{
|
||||||
|
tool("test", map[string]api.ToolProperty{
|
||||||
|
"param": {Type: api.PropertyType{"string"}},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
desc string
|
||||||
|
chunks []string
|
||||||
|
wantText string
|
||||||
|
wantThink string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "whitespace between thinking and content",
|
||||||
|
chunks: []string{"<think>Thinking</think>\n\n\nContent"},
|
||||||
|
wantText: "Content",
|
||||||
|
wantThink: "Thinking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "whitespace inside thinking tags",
|
||||||
|
chunks: []string{"<think> \n Thinking \n </think>Content"},
|
||||||
|
wantText: "Content",
|
||||||
|
wantThink: "Thinking \n ", // Thinking parser preserves internal whitespace
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "leading whitespace before thinking",
|
||||||
|
chunks: []string{" <think>Thinking</think>Content"},
|
||||||
|
wantText: "Content",
|
||||||
|
wantThink: "Thinking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "whitespace before tool call",
|
||||||
|
chunks: []string{"Text <tool_call><function=test>\n<parameter=param>\nvalue\n</parameter>\n</function></tool_call>"},
|
||||||
|
wantText: "Text",
|
||||||
|
wantThink: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "whitespace after tool call",
|
||||||
|
chunks: []string{"<tool_call><function=test>\n<parameter=param>\nvalue\n</parameter>\n</function></tool_call> Text"},
|
||||||
|
wantText: "Text",
|
||||||
|
wantThink: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
parser := Intellect3Parser{}
|
||||||
|
parser.Init(tools, nil, nil)
|
||||||
|
|
||||||
|
var gotText, gotThink string
|
||||||
|
for i, chunk := range tc.chunks {
|
||||||
|
isLast := i == len(tc.chunks)-1
|
||||||
|
text, think, _, err := parser.Add(chunk, isLast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
gotText += text
|
||||||
|
gotThink += think
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotText != tc.wantText {
|
||||||
|
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||||
|
}
|
||||||
|
if gotThink != tc.wantThink {
|
||||||
|
t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Parser interface {
|
type Parser interface {
|
||||||
// Init initializes the parser with tools and optional last message for chat prefill
|
// Init initializes the parser with tools, optional last message for chat prefill, and think value
|
||||||
// Returns processed tools if the parser needs to modify them (e.g., harmony renames them)
|
// Returns processed tools if the parser needs to modify them (e.g., harmony renames them)
|
||||||
Init(tools []api.Tool, lastMessage *api.Message) []api.Tool
|
Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool
|
||||||
// Add processes streamed content and returns parsed content, thinking, and tool calls
|
// Add processes streamed content and returns parsed content, thinking, and tool calls
|
||||||
// The done flag indicates if this is the last chunk (used for draining accumulators)
|
// The done flag indicates if this is the last chunk (used for draining accumulators)
|
||||||
Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error)
|
Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error)
|
||||||
@@ -52,6 +52,10 @@ func ParserForName(name string) Parser {
|
|||||||
return &PassthroughParser{}
|
return &PassthroughParser{}
|
||||||
case "harmony":
|
case "harmony":
|
||||||
return harmony.NewHarmonyMessageHandler()
|
return harmony.NewHarmonyMessageHandler()
|
||||||
|
case "cogito":
|
||||||
|
return &CogitoParser{}
|
||||||
|
case "intellect-3":
|
||||||
|
return &Intellect3Parser{}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -59,7 +63,7 @@ func ParserForName(name string) Parser {
|
|||||||
|
|
||||||
type PassthroughParser struct{}
|
type PassthroughParser struct{}
|
||||||
|
|
||||||
func (p *PassthroughParser) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
|
func (p *PassthroughParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
return tools // passthrough doesn't modify tools
|
return tools // passthrough doesn't modify tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type mockParser struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockParser) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
|
func (m *mockParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (p *Qwen3CoderParser) HasThinkingSupport() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Qwen3CoderParser) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
|
func (p *Qwen3CoderParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
p.tools = tools
|
p.tools = tools
|
||||||
return tools // Qwen doesn't modify tools
|
return tools // Qwen doesn't modify tools
|
||||||
}
|
}
|
||||||
@@ -432,7 +432,7 @@ func transformToXML(raw string) string {
|
|||||||
groups := qwenTagRegex.FindStringSubmatch(match)
|
groups := qwenTagRegex.FindStringSubmatch(match)
|
||||||
tag := groups[1]
|
tag := groups[1]
|
||||||
var escapedValue strings.Builder
|
var escapedValue strings.Builder
|
||||||
xml.EscapeText(&escapedValue, []byte(groups[2]))
|
_ = xml.EscapeText(&escapedValue, []byte(groups[2])) // error is always nil for strings.Builder
|
||||||
return fmt.Sprintf(`<%s name="%s">`, tag, escapedValue.String())
|
return fmt.Sprintf(`<%s name="%s">`, tag, escapedValue.String())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (p *Qwen3VLParser) setInitialState(lastMessage *api.Message) {
|
|||||||
p.state = CollectingThinkingContent
|
p.state = CollectingThinkingContent
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Qwen3VLParser) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
|
func (p *Qwen3VLParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
p.tools = tools
|
p.tools = tools
|
||||||
p.setInitialState(lastMessage)
|
p.setInitialState(lastMessage)
|
||||||
return tools
|
return tools
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ func TestQwen3VLNonThinkingParserStreaming(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: false}
|
parser := Qwen3VLParser{hasThinkingSupport: false}
|
||||||
parser.Init([]api.Tool{}, nil)
|
parser.Init([]api.Tool{}, nil, nil)
|
||||||
|
|
||||||
for i, step := range tc.steps {
|
for i, step := range tc.steps {
|
||||||
parser.buffer.WriteString(step.input)
|
parser.buffer.WriteString(step.input)
|
||||||
@@ -515,7 +515,7 @@ func TestQwenOldParserStreaming(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: false}
|
parser := Qwen3VLParser{hasThinkingSupport: false}
|
||||||
parser.Init([]api.Tool{}, nil)
|
parser.Init([]api.Tool{}, nil, nil)
|
||||||
|
|
||||||
for i, step := range tc.steps {
|
for i, step := range tc.steps {
|
||||||
parser.buffer.WriteString(step.input)
|
parser.buffer.WriteString(step.input)
|
||||||
@@ -822,7 +822,7 @@ func TestQwen3VLNonThinkingToolCallWhitespaceHandling(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: false}
|
parser := Qwen3VLParser{hasThinkingSupport: false}
|
||||||
parser.Init([]api.Tool{}, nil)
|
parser.Init([]api.Tool{}, nil, nil)
|
||||||
|
|
||||||
for i, step := range tc.steps {
|
for i, step := range tc.steps {
|
||||||
parser.buffer.WriteString(step.input)
|
parser.buffer.WriteString(step.input)
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ func TestQwen3VLThinkingParserStreaming(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: true}
|
parser := Qwen3VLParser{hasThinkingSupport: true}
|
||||||
parser.Init([]api.Tool{}, nil)
|
parser.Init([]api.Tool{}, nil, nil)
|
||||||
// parser.state = CollectingThinkingContent
|
// parser.state = CollectingThinkingContent
|
||||||
|
|
||||||
for i, step := range tc.steps {
|
for i, step := range tc.steps {
|
||||||
@@ -386,7 +386,7 @@ func TestQwen3VLParserState(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: tc.hasThinking}
|
parser := Qwen3VLParser{hasThinkingSupport: tc.hasThinking}
|
||||||
parser.Init(nil, tc.last)
|
parser.Init(nil, tc.last, nil)
|
||||||
if parser.state != tc.wantState {
|
if parser.state != tc.wantState {
|
||||||
t.Errorf("%s: got state %v, want %v", tc.desc, parser.state, tc.wantState)
|
t.Errorf("%s: got state %v, want %v", tc.desc, parser.state, tc.wantState)
|
||||||
}
|
}
|
||||||
@@ -437,7 +437,7 @@ func TestQwen3VLThinkingParserWithThinkingPrefill(t *testing.T) {
|
|||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: true}
|
parser := Qwen3VLParser{hasThinkingSupport: true}
|
||||||
parser.Init([]api.Tool{}, last)
|
parser.Init([]api.Tool{}, last, nil)
|
||||||
|
|
||||||
for i, step := range tc.steps {
|
for i, step := range tc.steps {
|
||||||
parser.buffer.WriteString(step.input)
|
parser.buffer.WriteString(step.input)
|
||||||
@@ -500,7 +500,7 @@ func TestQwen3VLThinkingParserWithNonThinkingPrefill(t *testing.T) {
|
|||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: true}
|
parser := Qwen3VLParser{hasThinkingSupport: true}
|
||||||
parser.Init([]api.Tool{}, last)
|
parser.Init([]api.Tool{}, last, nil)
|
||||||
|
|
||||||
for i, step := range tc.steps {
|
for i, step := range tc.steps {
|
||||||
parser.buffer.WriteString(step.input)
|
parser.buffer.WriteString(step.input)
|
||||||
@@ -523,7 +523,7 @@ func TestQwen3VLThinkingParserStreamingAssistantPrefillContent(t *testing.T) {
|
|||||||
// last message is assistant with content ⇒ start in CollectingContent
|
// last message is assistant with content ⇒ start in CollectingContent
|
||||||
last := &api.Message{Role: "assistant", Content: "has content"}
|
last := &api.Message{Role: "assistant", Content: "has content"}
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: true}
|
parser := Qwen3VLParser{hasThinkingSupport: true}
|
||||||
parser.Init([]api.Tool{}, last)
|
parser.Init([]api.Tool{}, last, nil)
|
||||||
|
|
||||||
type step struct {
|
type step struct {
|
||||||
input string
|
input string
|
||||||
@@ -750,7 +750,7 @@ func TestQwen3VLThinkingWhitespaceHandling(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: true}
|
parser := Qwen3VLParser{hasThinkingSupport: true}
|
||||||
parser.Init([]api.Tool{}, nil)
|
parser.Init([]api.Tool{}, nil, nil)
|
||||||
|
|
||||||
for i, step := range tc.steps {
|
for i, step := range tc.steps {
|
||||||
parser.buffer.WriteString(step.input)
|
parser.buffer.WriteString(step.input)
|
||||||
@@ -859,7 +859,7 @@ func TestQwen3VLToolCallWhitespaceHandling(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
parser := Qwen3VLParser{hasThinkingSupport: true}
|
parser := Qwen3VLParser{hasThinkingSupport: true}
|
||||||
parser.Init([]api.Tool{}, tc.prefillMsg)
|
parser.Init([]api.Tool{}, tc.prefillMsg, nil)
|
||||||
|
|
||||||
for i, step := range tc.steps {
|
for i, step := range tc.steps {
|
||||||
parser.buffer.WriteString(step.input)
|
parser.buffer.WriteString(step.input)
|
||||||
|
|||||||
160
model/renderers/intellect3.go
Normal file
160
model/renderers/intellect3.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package renderers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Intellect3Renderer struct{}
|
||||||
|
|
||||||
|
func (r *Intellect3Renderer) Render(messages []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
// filter out system messages and choose the first (if any) to win
|
||||||
|
var systemMessage string
|
||||||
|
var filteredMessages []api.Message
|
||||||
|
for _, message := range messages {
|
||||||
|
if message.Role != "system" {
|
||||||
|
filteredMessages = append(filteredMessages, message)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if systemMessage == "" {
|
||||||
|
systemMessage = message.Content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if systemMessage != "" || len(tools) > 0 {
|
||||||
|
sb.WriteString(imStartTag + "system\n")
|
||||||
|
|
||||||
|
sb.WriteString(systemMessage)
|
||||||
|
|
||||||
|
if len(tools) > 0 {
|
||||||
|
sb.WriteString("\n\n# Tools\n\nYou have access to the following functions:\n\n")
|
||||||
|
sb.WriteString("<tools>")
|
||||||
|
for _, tool := range tools {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
sb.WriteString("<function>\n")
|
||||||
|
sb.WriteString("<name>" + tool.Function.Name + "</name>")
|
||||||
|
if tool.Function.Description != "" {
|
||||||
|
sb.WriteString("\n<description>" + tool.Function.Description + "</description>")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n<parameters>")
|
||||||
|
|
||||||
|
for name, prop := range tool.Function.Parameters.Properties {
|
||||||
|
sb.WriteString("\n<parameter>")
|
||||||
|
sb.WriteString("\n<name>" + name + "</name>")
|
||||||
|
|
||||||
|
if len(prop.Type) > 0 {
|
||||||
|
sb.WriteString("\n<type>" + formatToolDefinitionType(prop.Type) + "</type>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if prop.Description != "" {
|
||||||
|
sb.WriteString("\n<description>" + prop.Description + "</description>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render any additional keys not already handled
|
||||||
|
handledKeys := map[string]bool{
|
||||||
|
"type": true,
|
||||||
|
"description": true,
|
||||||
|
}
|
||||||
|
sb.WriteString(renderAdditionalKeys(prop, handledKeys))
|
||||||
|
|
||||||
|
sb.WriteString("\n</parameter>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render extra keys for parameters (everything except 'type' and 'properties')
|
||||||
|
paramHandledKeys := map[string]bool{
|
||||||
|
"type": true,
|
||||||
|
"properties": true,
|
||||||
|
}
|
||||||
|
sb.WriteString(renderAdditionalKeys(tool.Function.Parameters, paramHandledKeys))
|
||||||
|
|
||||||
|
sb.WriteString("\n</parameters>")
|
||||||
|
sb.WriteString("\n</function>")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n</tools>")
|
||||||
|
sb.WriteString("\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>")
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(imEndTag + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, message := range filteredMessages {
|
||||||
|
lastMessage := i == len(filteredMessages)-1
|
||||||
|
prefill := lastMessage && message.Role == "assistant"
|
||||||
|
switch message.Role {
|
||||||
|
case "assistant":
|
||||||
|
if len(message.ToolCalls) > 0 {
|
||||||
|
sb.WriteString(imStartTag + "assistant")
|
||||||
|
|
||||||
|
// Add thinking tags if present
|
||||||
|
if message.Thinking != "" {
|
||||||
|
sb.WriteString("\n<think>" + strings.TrimSpace(message.Thinking) + "</think>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.Content != "" {
|
||||||
|
sb.WriteString("\n" + strings.TrimSpace(message.Content) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, toolCall := range message.ToolCalls {
|
||||||
|
sb.WriteString("\n<tool_call>\n<function=" + toolCall.Function.Name + ">")
|
||||||
|
for name, value := range toolCall.Function.Arguments {
|
||||||
|
valueStr := formatToolCallArgument(value)
|
||||||
|
sb.WriteString("\n<parameter=" + name + ">\n" + valueStr + "\n</parameter>")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n</function>\n</tool_call>")
|
||||||
|
}
|
||||||
|
sb.WriteString("<|im_end|>\n")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(imStartTag + "assistant")
|
||||||
|
|
||||||
|
// Add thinking tags if present
|
||||||
|
if message.Thinking != "" {
|
||||||
|
sb.WriteString("\n<think>" + strings.TrimSpace(message.Thinking) + "</think>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add content if present
|
||||||
|
if message.Content != "" {
|
||||||
|
if message.Thinking != "" {
|
||||||
|
sb.WriteString("\n" + strings.TrimSpace(message.Content))
|
||||||
|
} else {
|
||||||
|
sb.WriteString("\n" + message.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !prefill {
|
||||||
|
sb.WriteString(imEndTag + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "tool":
|
||||||
|
// consecutive tool responses should share a single `<im_start>user`, but
|
||||||
|
// have their own <tool_response> tags
|
||||||
|
|
||||||
|
// only start a new user block if this is the first tool response
|
||||||
|
if i == 0 || filteredMessages[i-1].Role != "tool" {
|
||||||
|
sb.WriteString(imStartTag + "user\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString("<tool_response>\n")
|
||||||
|
sb.WriteString(message.Content)
|
||||||
|
sb.WriteString("\n</tool_response>\n")
|
||||||
|
|
||||||
|
// close the user block only if this is the last tool response
|
||||||
|
if i == len(filteredMessages)-1 || filteredMessages[i+1].Role != "tool" {
|
||||||
|
sb.WriteString(imEndTag + "\n")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
sb.WriteString(imStartTag + message.Role + "\n")
|
||||||
|
sb.WriteString(message.Content)
|
||||||
|
sb.WriteString(imEndTag + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastMessage && !prefill {
|
||||||
|
sb.WriteString(imStartTag + "assistant\n<think>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
@@ -59,6 +59,9 @@ func rendererForName(name string) Renderer {
|
|||||||
case "cogito":
|
case "cogito":
|
||||||
renderer := &CogitoRenderer{isThinking: true}
|
renderer := &CogitoRenderer{isThinking: true}
|
||||||
return renderer
|
return renderer
|
||||||
|
case "intellect-3":
|
||||||
|
renderer := &Intellect3Renderer{}
|
||||||
|
return renderer
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ func (s *Server) GenerateHandler(c *gin.Context) {
|
|||||||
builtinParser = parsers.ParserForName(m.Config.Parser)
|
builtinParser = parsers.ParserForName(m.Config.Parser)
|
||||||
if builtinParser != nil {
|
if builtinParser != nil {
|
||||||
// no tools or last message for generate endpoint
|
// no tools or last message for generate endpoint
|
||||||
builtinParser.Init(nil, nil)
|
builtinParser.Init(nil, nil, req.Think)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2051,7 +2051,7 @@ func (s *Server) ChatHandler(c *gin.Context) {
|
|||||||
lastMessage = &msgs[len(msgs)-1]
|
lastMessage = &msgs[len(msgs)-1]
|
||||||
}
|
}
|
||||||
// Initialize parser and get processed tools
|
// Initialize parser and get processed tools
|
||||||
processedTools = builtinParser.Init(req.Tools, lastMessage)
|
processedTools = builtinParser.Init(req.Tools, lastMessage, req.Think)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user