mirror of
https://github.com/ollama/ollama.git
synced 2026-04-28 03:39:48 +02:00
Compare commits
5 Commits
brucemacd/
...
v0.16.3-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d02d1d767 | ||
|
|
1a636fb47a | ||
|
|
0759fface9 | ||
|
|
325b72bc31 | ||
|
|
f01a9a7859 |
@@ -6,6 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/envconfig"
|
||||||
"golang.org/x/mod/semver"
|
"golang.org/x/mod/semver"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,6 +33,10 @@ func (c *Codex) Run(model string, args []string) error {
|
|||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"OPENAI_BASE_URL="+envconfig.Host().String()+"/v1/",
|
||||||
|
"OPENAI_API_KEY=ollama",
|
||||||
|
)
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -415,6 +415,12 @@ type multiSelectorModel struct {
|
|||||||
cancelled bool
|
cancelled bool
|
||||||
confirmed bool
|
confirmed bool
|
||||||
width int
|
width int
|
||||||
|
|
||||||
|
// multi enables full multi-select editing mode. The zero value (false)
|
||||||
|
// shows a single-select picker where Enter adds the chosen model to
|
||||||
|
// the existing list. Tab toggles between modes.
|
||||||
|
multi bool
|
||||||
|
singleAdd string // model picked in single mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMultiSelectorModel(title string, items []SelectItem, preChecked []string) multiSelectorModel {
|
func newMultiSelectorModel(title string, items []SelectItem, preChecked []string) multiSelectorModel {
|
||||||
@@ -429,13 +435,23 @@ func newMultiSelectorModel(title string, items []SelectItem, preChecked []string
|
|||||||
m.itemIndex[item.Name] = i
|
m.itemIndex[item.Name] = i
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range preChecked {
|
// Reverse order so preChecked[0] (the current default) ends up last
|
||||||
if idx, ok := m.itemIndex[name]; ok {
|
// in checkOrder, matching the "last checked = default" convention.
|
||||||
|
for i := len(preChecked) - 1; i >= 0; i-- {
|
||||||
|
if idx, ok := m.itemIndex[preChecked[i]]; ok {
|
||||||
m.checked[idx] = true
|
m.checked[idx] = true
|
||||||
m.checkOrder = append(m.checkOrder, idx)
|
m.checkOrder = append(m.checkOrder, idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Position cursor on the current default model
|
||||||
|
if len(preChecked) > 0 {
|
||||||
|
if idx, ok := m.itemIndex[preChecked[0]]; ok {
|
||||||
|
m.cursor = idx
|
||||||
|
m.updateScroll(m.otherStart())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,14 +562,25 @@ func (m multiSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.cancelled = true
|
m.cancelled = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case tea.KeyTab:
|
||||||
|
m.multi = !m.multi
|
||||||
|
|
||||||
case tea.KeyEnter:
|
case tea.KeyEnter:
|
||||||
if len(m.checkOrder) > 0 {
|
if !m.multi {
|
||||||
|
if len(filtered) > 0 && m.cursor < len(filtered) {
|
||||||
|
m.singleAdd = filtered[m.cursor].Name
|
||||||
|
m.confirmed = true
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
} else if len(m.checkOrder) > 0 {
|
||||||
m.confirmed = true
|
m.confirmed = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
case tea.KeySpace:
|
case tea.KeySpace:
|
||||||
m.toggleItem()
|
if m.multi {
|
||||||
|
m.toggleItem()
|
||||||
|
}
|
||||||
|
|
||||||
case tea.KeyUp:
|
case tea.KeyUp:
|
||||||
if m.cursor > 0 {
|
if m.cursor > 0 {
|
||||||
@@ -592,7 +619,9 @@ func (m multiSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
// On some terminals (e.g. Windows PowerShell), space arrives as
|
// On some terminals (e.g. Windows PowerShell), space arrives as
|
||||||
// KeyRunes instead of KeySpace. Intercept it so toggle still works.
|
// KeyRunes instead of KeySpace. Intercept it so toggle still works.
|
||||||
if len(msg.Runes) == 1 && msg.Runes[0] == ' ' {
|
if len(msg.Runes) == 1 && msg.Runes[0] == ' ' {
|
||||||
m.toggleItem()
|
if m.multi {
|
||||||
|
m.toggleItem()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
m.filter += string(msg.Runes)
|
m.filter += string(msg.Runes)
|
||||||
m.cursor = 0
|
m.cursor = 0
|
||||||
@@ -604,6 +633,19 @@ func (m multiSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m multiSelectorModel) renderSingleItem(s *strings.Builder, item SelectItem, idx int) {
|
||||||
|
if idx == m.cursor {
|
||||||
|
s.WriteString(selectorSelectedItemStyle.Render("▸ " + item.Name))
|
||||||
|
} else {
|
||||||
|
s.WriteString(selectorItemStyle.Render(item.Name))
|
||||||
|
}
|
||||||
|
s.WriteString("\n")
|
||||||
|
if item.Description != "" {
|
||||||
|
s.WriteString(selectorDescLineStyle.Render(item.Description))
|
||||||
|
s.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m multiSelectorModel) renderMultiItem(s *strings.Builder, item SelectItem, idx int) {
|
func (m multiSelectorModel) renderMultiItem(s *strings.Builder, item SelectItem, idx int) {
|
||||||
origIdx := m.itemIndex[item.Name]
|
origIdx := m.itemIndex[item.Name]
|
||||||
|
|
||||||
@@ -615,7 +657,7 @@ func (m multiSelectorModel) renderMultiItem(s *strings.Builder, item SelectItem,
|
|||||||
}
|
}
|
||||||
|
|
||||||
suffix := ""
|
suffix := ""
|
||||||
if len(m.checkOrder) > 0 && m.checkOrder[0] == origIdx {
|
if len(m.checkOrder) > 0 && m.checkOrder[len(m.checkOrder)-1] == origIdx {
|
||||||
suffix = " " + selectorDefaultTagStyle.Render("(default)")
|
suffix = " " + selectorDefaultTagStyle.Render("(default)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,6 +679,11 @@ func (m multiSelectorModel) View() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderItem := m.renderSingleItem
|
||||||
|
if m.multi {
|
||||||
|
renderItem = m.renderMultiItem
|
||||||
|
}
|
||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
s.WriteString(selectorTitleStyle.Render(m.title))
|
s.WriteString(selectorTitleStyle.Render(m.title))
|
||||||
@@ -661,7 +708,7 @@ func (m multiSelectorModel) View() string {
|
|||||||
if idx >= len(filtered) {
|
if idx >= len(filtered) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
m.renderMultiItem(&s, filtered[idx], idx)
|
renderItem(&s, filtered[idx], idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if remaining := len(filtered) - m.scrollOffset - displayCount; remaining > 0 {
|
if remaining := len(filtered) - m.scrollOffset - displayCount; remaining > 0 {
|
||||||
@@ -684,7 +731,7 @@ func (m multiSelectorModel) View() string {
|
|||||||
s.WriteString(sectionHeaderStyle.Render("Recommended"))
|
s.WriteString(sectionHeaderStyle.Render("Recommended"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
for _, idx := range recItems {
|
for _, idx := range recItems {
|
||||||
m.renderMultiItem(&s, filtered[idx], idx)
|
renderItem(&s, filtered[idx], idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,7 +751,7 @@ func (m multiSelectorModel) View() string {
|
|||||||
if idx >= len(otherItems) {
|
if idx >= len(otherItems) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
m.renderMultiItem(&s, filtered[otherItems[idx]], otherItems[idx])
|
renderItem(&s, filtered[otherItems[idx]], otherItems[idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
if remaining := len(otherItems) - m.scrollOffset - displayCount; remaining > 0 {
|
if remaining := len(otherItems) - m.scrollOffset - displayCount; remaining > 0 {
|
||||||
@@ -716,15 +763,18 @@ func (m multiSelectorModel) View() string {
|
|||||||
|
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
|
||||||
count := m.selectedCount()
|
if !m.multi {
|
||||||
if count == 0 {
|
s.WriteString(selectorHelpStyle.Render("↑/↓ navigate • enter select • tab add multiple • esc cancel"))
|
||||||
s.WriteString(selectorDescStyle.Render(" Select at least one model."))
|
|
||||||
} else {
|
} else {
|
||||||
s.WriteString(selectorDescStyle.Render(fmt.Sprintf(" %d selected - press enter to continue", count)))
|
count := m.selectedCount()
|
||||||
|
if count == 0 {
|
||||||
|
s.WriteString(selectorDescStyle.Render(" Select at least one model."))
|
||||||
|
} else {
|
||||||
|
s.WriteString(selectorDescStyle.Render(fmt.Sprintf(" %d selected - press enter to continue", count)))
|
||||||
|
}
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString(selectorHelpStyle.Render("↑/↓ navigate • space toggle • tab select single • enter confirm • esc cancel"))
|
||||||
}
|
}
|
||||||
s.WriteString("\n\n")
|
|
||||||
|
|
||||||
s.WriteString(selectorHelpStyle.Render("↑/↓ navigate • space toggle • enter confirm • esc cancel"))
|
|
||||||
|
|
||||||
result := s.String()
|
result := s.String()
|
||||||
if m.width > 0 {
|
if m.width > 0 {
|
||||||
@@ -747,18 +797,28 @@ func SelectMultiple(title string, items []SelectItem, preChecked []string) ([]st
|
|||||||
}
|
}
|
||||||
|
|
||||||
fm := finalModel.(multiSelectorModel)
|
fm := finalModel.(multiSelectorModel)
|
||||||
if fm.cancelled {
|
if fm.cancelled || !fm.confirmed {
|
||||||
return nil, ErrCancelled
|
return nil, ErrCancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fm.confirmed {
|
// Single-add mode: prepend the picked model, keep existing models deduped
|
||||||
return nil, ErrCancelled
|
if fm.singleAdd != "" {
|
||||||
|
result := []string{fm.singleAdd}
|
||||||
|
for _, name := range preChecked {
|
||||||
|
if name != fm.singleAdd {
|
||||||
|
result = append(result, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []string
|
// Multi-edit mode: last checked is default (first in result)
|
||||||
|
last := fm.checkOrder[len(fm.checkOrder)-1]
|
||||||
|
result := []string{fm.items[last].Name}
|
||||||
for _, idx := range fm.checkOrder {
|
for _, idx := range fm.checkOrder {
|
||||||
result = append(result, fm.items[idx].Name)
|
if idx != last {
|
||||||
|
result = append(result, fm.items[idx].Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -539,6 +539,7 @@ func TestMultiView_CursorIndicator(t *testing.T) {
|
|||||||
|
|
||||||
func TestMultiView_CheckedItemShowsX(t *testing.T) {
|
func TestMultiView_CheckedItemShowsX(t *testing.T) {
|
||||||
m := newMultiSelectorModel("Pick:", items("a", "b"), []string{"a"})
|
m := newMultiSelectorModel("Pick:", items("a", "b"), []string{"a"})
|
||||||
|
m.multi = true
|
||||||
content := m.View()
|
content := m.View()
|
||||||
|
|
||||||
if !strings.Contains(content, "[x]") {
|
if !strings.Contains(content, "[x]") {
|
||||||
@@ -550,11 +551,18 @@ func TestMultiView_CheckedItemShowsX(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiView_DefaultTag(t *testing.T) {
|
func TestMultiView_DefaultTag(t *testing.T) {
|
||||||
m := newMultiSelectorModel("Pick:", items("a", "b"), []string{"a"})
|
m := newMultiSelectorModel("Pick:", items("a", "b", "c"), []string{"a", "b"})
|
||||||
|
m.multi = true
|
||||||
content := m.View()
|
content := m.View()
|
||||||
|
|
||||||
if !strings.Contains(content, "(default)") {
|
if !strings.Contains(content, "(default)") {
|
||||||
t.Error("first checked item should have (default) tag")
|
t.Error("should have (default) tag")
|
||||||
|
}
|
||||||
|
// preChecked[0] ("a") should be the default (last in checkOrder)
|
||||||
|
aIdx := strings.Index(content, "a")
|
||||||
|
defaultIdx := strings.Index(content, "(default)")
|
||||||
|
if defaultIdx < aIdx {
|
||||||
|
t.Error("(default) tag should appear after 'a' (the current default)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,6 +593,7 @@ func TestMultiView_OverflowIndicator(t *testing.T) {
|
|||||||
|
|
||||||
func TestMultiUpdate_SpaceTogglesItem(t *testing.T) {
|
func TestMultiUpdate_SpaceTogglesItem(t *testing.T) {
|
||||||
m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil)
|
m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil)
|
||||||
|
m.multi = true
|
||||||
m.cursor = 1
|
m.cursor = 1
|
||||||
|
|
||||||
// Simulate space delivered as tea.KeySpace
|
// Simulate space delivered as tea.KeySpace
|
||||||
@@ -601,6 +610,7 @@ func TestMultiUpdate_SpaceTogglesItem(t *testing.T) {
|
|||||||
|
|
||||||
func TestMultiUpdate_SpaceRuneTogglesItem(t *testing.T) {
|
func TestMultiUpdate_SpaceRuneTogglesItem(t *testing.T) {
|
||||||
m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil)
|
m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil)
|
||||||
|
m.multi = true
|
||||||
m.cursor = 1
|
m.cursor = 1
|
||||||
|
|
||||||
// Simulate space delivered as tea.KeyRunes (Windows PowerShell behavior)
|
// Simulate space delivered as tea.KeyRunes (Windows PowerShell behavior)
|
||||||
@@ -618,6 +628,161 @@ func TestMultiUpdate_SpaceRuneTogglesItem(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Single-add mode ---
|
||||||
|
|
||||||
|
func TestMulti_StartsInSingleMode(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a", "b"), nil)
|
||||||
|
if m.multi {
|
||||||
|
t.Error("should start in single mode (multi=false)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMulti_SingleModeNoCheckboxes(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a", "b"), nil)
|
||||||
|
content := m.View()
|
||||||
|
if strings.Contains(content, "[x]") || strings.Contains(content, "[ ]") {
|
||||||
|
t.Error("single mode should not show checkboxes")
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "▸") {
|
||||||
|
t.Error("single mode should show cursor indicator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMulti_SingleModeEnterPicksItem(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil)
|
||||||
|
m.cursor = 1
|
||||||
|
|
||||||
|
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||||
|
m = updated.(multiSelectorModel)
|
||||||
|
|
||||||
|
if m.singleAdd != "b" {
|
||||||
|
t.Errorf("enter in single mode should pick cursor item, got %q", m.singleAdd)
|
||||||
|
}
|
||||||
|
if !m.confirmed {
|
||||||
|
t.Error("should set confirmed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMulti_SingleModeSpaceIsNoop(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a", "b"), nil)
|
||||||
|
m.cursor = 0
|
||||||
|
|
||||||
|
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||||
|
m = updated.(multiSelectorModel)
|
||||||
|
|
||||||
|
if len(m.checked) != 0 {
|
||||||
|
t.Error("space in single mode should not toggle items")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMulti_SingleModeSpaceRuneIsNoop(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a", "b"), nil)
|
||||||
|
m.cursor = 0
|
||||||
|
|
||||||
|
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}})
|
||||||
|
m = updated.(multiSelectorModel)
|
||||||
|
|
||||||
|
if len(m.checked) != 0 {
|
||||||
|
t.Error("space rune in single mode should not toggle items")
|
||||||
|
}
|
||||||
|
if m.filter != "" {
|
||||||
|
t.Error("space rune in single mode should not add to filter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMulti_TabTogglesMode(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a", "b"), nil)
|
||||||
|
|
||||||
|
if m.multi {
|
||||||
|
t.Fatal("should start in single mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyTab})
|
||||||
|
m = updated.(multiSelectorModel)
|
||||||
|
if !m.multi {
|
||||||
|
t.Error("tab should switch to multi mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyTab})
|
||||||
|
m = updated.(multiSelectorModel)
|
||||||
|
if m.multi {
|
||||||
|
t.Error("tab should switch back to single mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMulti_SingleModeHelpText(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a"), nil)
|
||||||
|
content := m.View()
|
||||||
|
if !strings.Contains(content, "tab add multiple") {
|
||||||
|
t.Error("single mode should show 'tab add multiple' in help")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMulti_MultiModeHelpText(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a"), nil)
|
||||||
|
m.multi = true
|
||||||
|
content := m.View()
|
||||||
|
if !strings.Contains(content, "tab select single") {
|
||||||
|
t.Error("multi mode should show 'tab select single' in help")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- preChecked initialization order ---
|
||||||
|
|
||||||
|
func TestMulti_PreCheckedDefaultIsLast(t *testing.T) {
|
||||||
|
// preChecked[0] ("a") is the current default and should end up
|
||||||
|
// last in checkOrder so it gets the (default) tag.
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a", "b", "c"), []string{"a", "b", "c"})
|
||||||
|
|
||||||
|
if len(m.checkOrder) != 3 {
|
||||||
|
t.Fatalf("expected 3 in checkOrder, got %d", len(m.checkOrder))
|
||||||
|
}
|
||||||
|
lastIdx := m.checkOrder[len(m.checkOrder)-1]
|
||||||
|
if m.items[lastIdx].Name != "a" {
|
||||||
|
t.Errorf("preChecked[0] should be last in checkOrder, got %q", m.items[lastIdx].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMulti_CursorOnDefaultModel(t *testing.T) {
|
||||||
|
// preChecked[0] ("b") is the default; cursor should start on it
|
||||||
|
m := newMultiSelectorModel("Pick:", items("a", "b", "c"), []string{"b", "c"})
|
||||||
|
|
||||||
|
if m.cursor != 1 {
|
||||||
|
t.Errorf("cursor should be on preChecked[0] ('b') at index 1, got %d", m.cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Multi-mode last-checked is default ---
|
||||||
|
|
||||||
|
func TestMulti_LastCheckedIsDefault(t *testing.T) {
|
||||||
|
m := newMultiSelectorModel("Pick:", items("alpha", "beta", "gamma"), nil)
|
||||||
|
m.multi = true
|
||||||
|
|
||||||
|
// Check "alpha" then "gamma"
|
||||||
|
m.cursor = 0
|
||||||
|
m.toggleItem()
|
||||||
|
m.cursor = 2
|
||||||
|
m.toggleItem()
|
||||||
|
|
||||||
|
// Last checked ("gamma") should be at the end of checkOrder
|
||||||
|
lastIdx := m.checkOrder[len(m.checkOrder)-1]
|
||||||
|
if m.items[lastIdx].Name != "gamma" {
|
||||||
|
t.Errorf("last checked should be 'gamma', got %q", m.items[lastIdx].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The (default) tag renders based on checkOrder[len-1]
|
||||||
|
content := m.View()
|
||||||
|
if !strings.Contains(content, "(default)") {
|
||||||
|
t.Fatal("should show (default) tag")
|
||||||
|
}
|
||||||
|
// "alpha" line should NOT have the default tag
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
if strings.Contains(line, "alpha") && strings.Contains(line, "(default)") {
|
||||||
|
t.Error("'alpha' (first checked) should not have (default) tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Key message helpers for testing
|
// Key message helpers for testing
|
||||||
|
|
||||||
type keyType = int
|
type keyType = int
|
||||||
|
|||||||
@@ -429,8 +429,24 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
if m.multiModalSelector.confirmed {
|
if m.multiModalSelector.confirmed {
|
||||||
var selected []string
|
var selected []string
|
||||||
for _, idx := range m.multiModalSelector.checkOrder {
|
if m.multiModalSelector.singleAdd != "" {
|
||||||
selected = append(selected, m.multiModalSelector.items[idx].Name)
|
// Single-add mode: prepend picked model, keep existing deduped
|
||||||
|
selected = []string{m.multiModalSelector.singleAdd}
|
||||||
|
for _, name := range config.IntegrationModels(m.items[m.cursor].integration) {
|
||||||
|
if name != m.multiModalSelector.singleAdd {
|
||||||
|
selected = append(selected, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Last checked is default (first in result)
|
||||||
|
co := m.multiModalSelector.checkOrder
|
||||||
|
last := co[len(co)-1]
|
||||||
|
selected = []string{m.multiModalSelector.items[last].Name}
|
||||||
|
for _, idx := range co {
|
||||||
|
if idx != last {
|
||||||
|
selected = append(selected, m.multiModalSelector.items[idx].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(selected) > 0 {
|
if len(selected) > 0 {
|
||||||
m.changeModels = selected
|
m.changeModels = selected
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -26,6 +26,7 @@ require (
|
|||||||
github.com/d4l3k/go-bfloat16 v0.0.0-20211005043715-690c3bdd05f1
|
github.com/d4l3k/go-bfloat16 v0.0.0-20211005043715-690c3bdd05f1
|
||||||
github.com/dlclark/regexp2 v1.11.4
|
github.com/dlclark/regexp2 v1.11.4
|
||||||
github.com/emirpasic/gods/v2 v2.0.0-alpha
|
github.com/emirpasic/gods/v2 v2.0.0-alpha
|
||||||
|
github.com/klauspost/compress v1.18.3
|
||||||
github.com/mattn/go-runewidth v0.0.16
|
github.com/mattn/go-runewidth v0.0.16
|
||||||
github.com/nlpodyssey/gopickle v0.3.0
|
github.com/nlpodyssey/gopickle v0.3.0
|
||||||
github.com/pdevine/tensor v0.0.0-20240510204454-f88f4562727c
|
github.com/pdevine/tensor v0.0.0-20240510204454-f88f4562727c
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -122,7 +122,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
|||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
|
||||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
|
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
|
||||||
@@ -150,8 +149,9 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+
|
|||||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
|
|
||||||
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/ollama/ollama/openai"
|
"github.com/ollama/ollama/openai"
|
||||||
@@ -496,6 +497,17 @@ func (w *ResponsesWriter) Write(data []byte) (int, error) {
|
|||||||
|
|
||||||
func ResponsesMiddleware() gin.HandlerFunc {
|
func ResponsesMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
if c.GetHeader("Content-Encoding") == "zstd" {
|
||||||
|
reader, err := zstd.NewReader(c.Request.Body, zstd.WithDecoderMaxMemory(8<<20))
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, openai.NewError(http.StatusBadRequest, "failed to decompress zstd body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
c.Request.Body = io.NopCloser(reader)
|
||||||
|
c.Request.Header.Del("Content-Encoding")
|
||||||
|
}
|
||||||
|
|
||||||
var req openai.ResponsesRequest
|
var req openai.ResponsesRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, openai.NewError(http.StatusBadRequest, err.Error()))
|
c.AbortWithStatusJSON(http.StatusBadRequest, openai.NewError(http.StatusBadRequest, err.Error()))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/ollama/ollama/openai"
|
"github.com/ollama/ollama/openai"
|
||||||
@@ -1238,3 +1239,102 @@ func TestImageEditsMiddleware(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func zstdCompress(t *testing.T, data []byte) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
w, err := zstd.NewWriter(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponsesMiddlewareZstd(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
body string
|
||||||
|
useZstd bool
|
||||||
|
oversized bool
|
||||||
|
wantCode int
|
||||||
|
wantModel string
|
||||||
|
wantMessage string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "plain JSON",
|
||||||
|
body: `{"model": "test-model", "input": "Hello"}`,
|
||||||
|
wantCode: http.StatusOK,
|
||||||
|
wantModel: "test-model",
|
||||||
|
wantMessage: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zstd compressed",
|
||||||
|
body: `{"model": "test-model", "input": "Hello"}`,
|
||||||
|
useZstd: true,
|
||||||
|
wantCode: http.StatusOK,
|
||||||
|
wantModel: "test-model",
|
||||||
|
wantMessage: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zstd over max decompressed size",
|
||||||
|
oversized: true,
|
||||||
|
useZstd: true,
|
||||||
|
wantCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var capturedRequest *api.ChatRequest
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(ResponsesMiddleware(), captureRequestMiddleware(&capturedRequest))
|
||||||
|
router.Handle(http.MethodPost, "/v1/responses", func(c *gin.Context) {
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if tt.oversized {
|
||||||
|
bodyReader = bytes.NewReader(zstdCompress(t, bytes.Repeat([]byte("A"), 9<<20)))
|
||||||
|
} else if tt.useZstd {
|
||||||
|
bodyReader = bytes.NewReader(zstdCompress(t, []byte(tt.body)))
|
||||||
|
} else {
|
||||||
|
bodyReader = strings.NewReader(tt.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, "/v1/responses", bodyReader)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if tt.useZstd || tt.oversized {
|
||||||
|
req.Header.Set("Content-Encoding", "zstd")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(resp, req)
|
||||||
|
|
||||||
|
if resp.Code != tt.wantCode {
|
||||||
|
t.Fatalf("expected status %d, got %d: %s", tt.wantCode, resp.Code, resp.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantCode != http.StatusOK {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if capturedRequest == nil {
|
||||||
|
t.Fatal("expected captured request, got nil")
|
||||||
|
}
|
||||||
|
if capturedRequest.Model != tt.wantModel {
|
||||||
|
t.Fatalf("expected model %q, got %q", tt.wantModel, capturedRequest.Model)
|
||||||
|
}
|
||||||
|
if len(capturedRequest.Messages) != 1 || capturedRequest.Messages[0].Content != tt.wantMessage {
|
||||||
|
t.Fatalf("expected single user message %q, got %+v", tt.wantMessage, capturedRequest.Messages)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
# This script installs Ollama on Linux and macOS.
|
# This script installs Ollama on Linux and macOS.
|
||||||
# It detects the current operating system architecture and installs the appropriate version of Ollama.
|
# It detects the current operating system architecture and installs the appropriate version of Ollama.
|
||||||
|
|
||||||
|
# Wrap script in main function so that a truncated partial download doesn't end
|
||||||
|
# up executing half a script.
|
||||||
|
main() {
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
red="$( (/usr/bin/tput bold || :; /usr/bin/tput setaf 1 || :) 2>&-)"
|
red="$( (/usr/bin/tput bold || :; /usr/bin/tput setaf 1 || :) 2>&-)"
|
||||||
@@ -446,3 +450,6 @@ fi
|
|||||||
|
|
||||||
status "NVIDIA GPU ready."
|
status "NVIDIA GPU ready."
|
||||||
install_success
|
install_success
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
|
|||||||
Reference in New Issue
Block a user