Compare commits

..

7 Commits

Author SHA1 Message Date
nicole pardal
03abdb4969 fixed pretokenizer 2025-12-09 10:02:17 -08:00
nicole pardal
57c1d7db9a fixed generation issue 2025-12-08 00:35:49 -08:00
nicole pardal
91d6370a62 removed original olmo support 2025-12-01 14:17:46 -08:00
nicole pardal
38a2a6468f removed olmo1 support 2025-12-01 14:14:31 -08:00
nicole pardal
064ec63ddf lint 2025-11-26 20:05:25 -08:00
nicole pardal
fd959fbf7a updated converter 2025-11-26 19:42:34 -08:00
nicole pardal
cfc9729edf olmo model initial 2025-11-25 15:49:09 -08:00
14 changed files with 333 additions and 1347 deletions

View File

@@ -1,50 +0,0 @@
# 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

View File

@@ -1,151 +0,0 @@
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
}
}

View File

@@ -1,217 +0,0 @@
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)
}

View File

@@ -1,178 +0,0 @@
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",
},
},
},
},
}

View File

@@ -200,6 +200,8 @@ func ConvertModel(fsys fs.FS, f *os.File) error {
conv = &qwen25VLModel{}
case "Qwen3VLForConditionalGeneration", "Qwen3VLMoeForConditionalGeneration":
conv = &qwen3VLModel{}
case "OLMo2ForCausalLM", "Olmo2ForCausalLM", "OLMo3ForCausalLM", "Olmo3ForCausalLM":
conv = &olmoModel{}
case "BertModel":
conv = &bertModel{}
case "CohereForCausalLM":

94
convert/convert_olmo.go Normal file
View File

@@ -0,0 +1,94 @@
package convert
import (
"cmp"
"github.com/ollama/ollama/fs/ggml"
)
type olmoModel struct {
ModelParameters
HiddenSize uint32 `json:"hidden_size"`
NumHiddenLayers uint32 `json:"num_hidden_layers"`
IntermediateSize uint32 `json:"intermediate_size"`
NumAttentionHeads uint32 `json:"num_attention_heads"`
NumKeyValueHeads uint32 `json:"num_key_value_heads"`
MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
RMSNormEPS float32 `json:"rms_norm_eps"`
RopeTheta float32 `json:"rope_theta"`
ClampKQV float32 `json:"f_clamp_kqv"`
SlidingWindow uint32 `json:"sliding_window"`
LayerTypes []string `json:"layer_types"`
}
var _ ModelConverter = (*olmoModel)(nil)
func (p *olmoModel) KV(t *Tokenizer) ggml.KV {
kv := p.ModelParameters.KV(t)
kv["general.architecture"] = "olmo"
kv["olmo.block_count"] = p.NumHiddenLayers
kv["olmo.context_length"] = p.MaxPositionEmbeddings
kv["olmo.embedding_length"] = p.HiddenSize
kv["olmo.feed_forward_length"] = p.IntermediateSize
kv["olmo.attention.head_count"] = p.NumAttentionHeads
kv["olmo.attention.head_count_kv"] = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
if p.RopeTheta > 0 {
kv["olmo.rope.freq_base"] = p.RopeTheta
} else {
kv["olmo.rope.freq_base"] = float32(10000.0)
}
if p.RMSNormEPS > 0 {
kv["olmo.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
}
if p.ClampKQV > 0 {
kv["olmo.attention.clamp_kqv"] = p.ClampKQV
}
if p.SlidingWindow > 0 {
kv["olmo.attention.sliding_window"] = p.SlidingWindow
}
if len(p.LayerTypes) > 0 {
kv["olmo.attention.layer_types"] = p.LayerTypes
}
return kv
}
func (p *olmoModel) Tensors(ts []Tensor) []*ggml.Tensor {
out := make([]*ggml.Tensor, 0, len(ts))
for _, t := range ts {
out = append(out, &ggml.Tensor{
Name: t.Name(),
Kind: t.Kind(),
Shape: t.Shape(),
WriterTo: t,
})
}
return out
}
func (p *olmoModel) Replacements() []string {
return []string{
"lm_head", "output",
"model.embed_tokens", "token_embd",
"model.layers", "blk",
"model.norm", "output_norm",
"self_attn.q_proj", "attn_q",
"self_attn.k_proj", "attn_k",
"self_attn.v_proj", "attn_v",
"self_attn.o_proj", "attn_output",
"self_attn.q_norm", "attn_q_norm",
"self_attn.k_norm", "attn_k_norm",
"post_attention_layernorm", "post_attention_norm",
"post_feedforward_layernorm", "post_ffw_norm",
"mlp.gate_proj", "ffn_gate",
"mlp.down_proj", "ffn_down",
"mlp.up_proj", "ffn_up",
}
}

View File

@@ -149,6 +149,9 @@ PARAMETER <parameter> <parametervalue>
| 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 |
| 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 |

View File

@@ -13,6 +13,7 @@ import (
_ "github.com/ollama/ollama/model/models/mistral3"
_ "github.com/ollama/ollama/model/models/mllama"
_ "github.com/ollama/ollama/model/models/nomicbert"
_ "github.com/ollama/ollama/model/models/olmo"
_ "github.com/ollama/ollama/model/models/qwen2"
_ "github.com/ollama/ollama/model/models/qwen25vl"
_ "github.com/ollama/ollama/model/models/qwen3"

233
model/models/olmo/model.go Normal file
View File

@@ -0,0 +1,233 @@
package olmo
import (
"cmp"
"math"
"github.com/ollama/ollama/fs"
"github.com/ollama/ollama/kvcache"
"github.com/ollama/ollama/ml"
"github.com/ollama/ollama/ml/nn"
"github.com/ollama/ollama/ml/nn/fast"
"github.com/ollama/ollama/ml/nn/rope"
"github.com/ollama/ollama/model"
"github.com/ollama/ollama/model/input"
)
type Options struct {
hiddenSize, numHeads, numKVHeads int
headDim, ropeDim int
eps, ropeBase, ropeScale float32
clampKQV float32
originalContextLength int
attnFactor float32
}
type Model struct {
model.Base
model.TextProcessor
TokenEmbedding *nn.Embedding `gguf:"token_embd"`
Layers []Layer `gguf:"blk"`
OutputNorm *nn.RMSNorm `gguf:"output_norm"`
Output *nn.Linear `gguf:"output,alt:token_embd"`
Options
}
func New(c fs.Config) (model.Model, error) {
vocabulary := model.Vocabulary{
Values: c.Strings("tokenizer.ggml.tokens"),
Scores: c.Floats("tokenizer.ggml.scores"),
Types: c.Ints("tokenizer.ggml.token_type"),
Merges: c.Strings("tokenizer.ggml.merges"),
AddBOS: c.Bool("tokenizer.ggml.add_bos_token", true),
BOS: []int32{int32(c.Uint("tokenizer.ggml.bos_token_id"))},
AddEOS: c.Bool("tokenizer.ggml.add_eos_token", false),
EOS: append(
[]int32{int32(c.Uint("tokenizer.ggml.eos_token_id"))},
c.Ints("tokenizer.ggml.eos_token_ids")...,
),
}
if c.String("tokenizer.ggml.model") != "gpt2" {
return nil, model.ErrUnsupportedTokenizer
}
var pretokenizers []string
if c.String("tokenizer.ggml.pre") != "default" {
pretokenizers = []string{
`(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+`,
}
}
processor := model.NewBytePairEncoding(&vocabulary, pretokenizers...)
m := Model{
TextProcessor: processor,
Layers: make([]Layer, c.Uint("block_count")),
Options: Options{
hiddenSize: int(c.Uint("embedding_length")),
numHeads: int(c.Uint("attention.head_count")),
numKVHeads: int(c.Uint("attention.head_count_kv")),
headDim: int(c.Uint("attention.key_length")),
ropeDim: int(c.Uint("rope.dimension_count")),
eps: c.Float("attention.layer_norm_rms_epsilon"),
ropeBase: c.Float("rope.freq_base", 1e4),
ropeScale: c.Float("rope.scaling.factor", 1),
clampKQV: c.Float("attention.clamp_kqv", 0),
originalContextLength: int(c.Uint("rope.scaling.original_context_length")),
attnFactor: c.Float("rope.scaling.attn_factor", 1),
},
}
m.Cache = kvcache.NewCausalCache(m.Shift)
return &m, nil
}
type SelfAttention struct {
Query *nn.Linear `gguf:"attn_q"`
Key *nn.Linear `gguf:"attn_k"`
Value *nn.Linear `gguf:"attn_v"`
Output *nn.Linear `gguf:"attn_output"`
QNorm *nn.RMSNorm `gguf:"attn_q_norm"`
KNorm *nn.RMSNorm `gguf:"attn_k_norm"`
RopeFactors ml.Tensor `gguf:"rope_freqs.weight"`
}
func (o *Options) ropeOptions(factors ml.Tensor, isSWA bool) []func(*rope.Options) {
opts := []func(*rope.Options){
rope.WithFactors(factors),
}
if !isSWA && o.originalContextLength > 0 {
opts = append(opts,
rope.WithOriginalContextLength(o.originalContextLength),
rope.WithExtrapolationFactor(1.),
rope.WithAttentionFactor(o.attnFactor),
)
}
return opts
}
func (sa *SelfAttention) Forward(ctx ml.Context, hiddenState, positions ml.Tensor, cache kvcache.Cache, opts *Options, isSWA bool) ml.Tensor {
batchSize := hiddenState.Dim(1)
headDim := cmp.Or(opts.headDim, opts.hiddenSize/opts.numHeads)
ropeDim := cmp.Or(opts.ropeDim, headDim)
query := sa.Query.Forward(ctx, hiddenState)
if sa.QNorm != nil {
query = sa.QNorm.Forward(ctx, query, opts.eps)
}
query = query.Reshape(ctx, headDim, opts.numHeads, batchSize)
key := sa.Key.Forward(ctx, hiddenState)
if sa.KNorm != nil {
key = sa.KNorm.Forward(ctx, key, opts.eps)
}
key = key.Reshape(ctx, headDim, opts.numKVHeads, batchSize)
value := sa.Value.Forward(ctx, hiddenState)
value = value.Reshape(ctx, headDim, opts.numKVHeads, batchSize)
freqScale := float32(1.0)
if !isSWA {
freqScale = 1. / opts.ropeScale
}
ropeOpts := opts.ropeOptions(sa.RopeFactors, isSWA)
query = fast.RoPE(ctx, query, positions, ropeDim, opts.ropeBase, freqScale, ropeOpts...)
key = fast.RoPE(ctx, key, positions, ropeDim, opts.ropeBase, freqScale, ropeOpts...)
attention := nn.Attention(ctx, query, key, value, 1.0/math.Sqrt(float64(headDim)), cache)
attention = attention.Reshape(ctx, headDim*opts.numHeads, batchSize)
return sa.Output.Forward(ctx, attention)
}
func (m *Model) Shift(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) {
ropeDim := cmp.Or(m.ropeDim, m.hiddenSize/m.numHeads)
isSWA := isSWALayer(layer)
freqScale := float32(1.0)
if !isSWA {
freqScale = 1. / m.ropeScale
}
ropeOpts := m.Options.ropeOptions(m.Layers[layer].SelfAttention.RopeFactors, isSWA)
return fast.RoPE(ctx, key, shift, ropeDim, m.ropeBase, freqScale, ropeOpts...), nil
}
type MLP struct {
Up *nn.Linear `gguf:"ffn_up"`
Down *nn.Linear `gguf:"ffn_down"`
Gate *nn.Linear `gguf:"ffn_gate"`
}
func (mlp *MLP) Forward(ctx ml.Context, hiddenState ml.Tensor, opts *Options) ml.Tensor {
hiddenState = mlp.Gate.Forward(ctx, hiddenState).SILU(ctx, mlp.Up.Forward(ctx, hiddenState))
return mlp.Down.Forward(ctx, hiddenState)
}
type Layer struct {
SelfAttention *SelfAttention
PostAttentionNorm *nn.RMSNorm `gguf:"post_attention_norm"`
MLP *MLP
PostFFWNorm *nn.RMSNorm `gguf:"post_ffw_norm"`
}
func (l *Layer) Forward(ctx ml.Context, hiddenState, positions, outputs ml.Tensor, cache kvcache.Cache, opts *Options, isSWA bool) ml.Tensor {
residual := hiddenState
hiddenState = l.SelfAttention.Forward(ctx, hiddenState, positions, cache, opts, isSWA)
if outputs != nil {
hiddenState = hiddenState.Rows(ctx, outputs)
residual = residual.Rows(ctx, outputs)
}
if l.PostAttentionNorm != nil {
hiddenState = l.PostAttentionNorm.Forward(ctx, hiddenState, opts.eps)
}
ffnInput := hiddenState.Add(ctx, residual)
hiddenState = l.MLP.Forward(ctx, ffnInput, opts)
if l.PostFFWNorm != nil {
hiddenState = l.PostFFWNorm.Forward(ctx, hiddenState, opts.eps)
}
return hiddenState.Add(ctx, ffnInput)
}
func isSWALayer(layerIdx int) bool {
return (layerIdx+1)%4 != 0
}
func (m *Model) Forward(ctx ml.Context, batch input.Batch) (ml.Tensor, error) {
positions := ctx.Input().FromInts(batch.Positions, len(batch.Positions))
hiddenState := m.TokenEmbedding.Forward(ctx, batch.Inputs)
for i, layer := range m.Layers {
m.Cache.SetLayer(i)
isSWA := isSWALayer(i)
var outputs ml.Tensor
if i == len(m.Layers)-1 {
outputs = batch.Outputs
}
hiddenState = layer.Forward(ctx, hiddenState, positions, outputs, m.Cache, &m.Options, isSWA)
}
hiddenState = m.OutputNorm.Forward(ctx, hiddenState, m.eps)
return m.Output.Forward(ctx, hiddenState), nil
}
func init() {
model.Register("olmo2", New)
}

View File

@@ -1,44 +0,0 @@
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
}

View File

@@ -1,542 +0,0 @@
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)
}
})
}
}

View File

@@ -54,8 +54,6 @@ func ParserForName(name string) Parser {
return harmony.NewHarmonyMessageHandler()
case "cogito":
return &CogitoParser{}
case "intellect-3":
return &Intellect3Parser{}
default:
return nil
}

View File

@@ -1,160 +0,0 @@
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
}

View File

@@ -59,9 +59,6 @@ func rendererForName(name string) Renderer {
case "cogito":
renderer := &CogitoRenderer{isThinking: true}
return renderer
case "intellect-3":
renderer := &Intellect3Renderer{}
return renderer
default:
return nil
}