openai: align models list with tags (#16556)

This commit is contained in:
Parth Sareen
2026-06-05 17:59:05 -07:00
committed by GitHub
parent a0099da2d1
commit e09b3f9fb5
3 changed files with 157 additions and 2 deletions
+7 -2
View File
@@ -408,11 +408,16 @@ func ToCompleteChunk(id string, r api.GenerateResponse) CompletionChunk {
func ToListCompletion(r api.ListResponse) ListCompletion { func ToListCompletion(r api.ListResponse) ListCompletion {
var data []Model var data []Model
for _, m := range r.Models { for _, m := range r.Models {
id := m.Model
if id == "" {
id = m.Name
}
data = append(data, Model{ data = append(data, Model{
Id: m.Name, Id: id,
Object: "model", Object: "model",
Created: m.ModifiedAt.Unix(), Created: m.ModifiedAt.Unix(),
OwnedBy: model.ParseName(m.Name).Namespace, OwnedBy: model.ParseName(id).Namespace,
}) })
} }
+42
View File
@@ -356,6 +356,48 @@ func TestFromCompleteRequest_WithLogprobs(t *testing.T) {
} }
} }
func TestToListCompletionUsesModelIdentity(t *testing.T) {
modified := time.Unix(1234567890, 0).UTC()
result := ToListCompletion(api.ListResponse{
Models: []api.ListModelResponse{
{
Name: "legacy-name:latest",
Model: "namespace/exposed-model:latest",
ModifiedAt: modified,
},
{
Name: "fallback-name:latest",
ModifiedAt: modified.Add(time.Second),
},
},
})
if result.Object != "list" {
t.Fatalf("object = %q, want list", result.Object)
}
if len(result.Data) != 2 {
t.Fatalf("models = %d, want 2", len(result.Data))
}
if result.Data[0].Id != "namespace/exposed-model:latest" {
t.Fatalf("id = %q, want model field", result.Data[0].Id)
}
if result.Data[0].OwnedBy != "namespace" {
t.Fatalf("owned_by = %q, want namespace", result.Data[0].OwnedBy)
}
if result.Data[0].Created != modified.Unix() {
t.Fatalf("created = %d, want %d", result.Data[0].Created, modified.Unix())
}
if result.Data[1].Id != "fallback-name:latest" {
t.Fatalf("fallback id = %q, want name field", result.Data[1].Id)
}
if result.Data[1].OwnedBy != "library" {
t.Fatalf("fallback owned_by = %q, want library", result.Data[1].OwnedBy)
}
}
func TestToChatCompletion_WithLogprobs(t *testing.T) { func TestToChatCompletion_WithLogprobs(t *testing.T) {
createdAt := time.Unix(1234567890, 0) createdAt := time.Unix(1234567890, 0)
resp := api.ChatResponse{ resp := api.ChatResponse{
+108
View File
@@ -3,13 +3,20 @@ package server
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"net/http/httptest"
"os"
"slices" "slices"
"testing" "testing"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/ollama/ollama/api" "github.com/ollama/ollama/api"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/openai"
"github.com/ollama/ollama/types/model"
) )
func TestList(t *testing.T) { func TestList(t *testing.T) {
@@ -76,3 +83,104 @@ func TestList(t *testing.T) {
} }
} }
} }
func TestOpenAIListMatchesTagsModels(t *testing.T) {
gin.SetMode(gin.TestMode)
setTestHome(t, t.TempDir())
cache := newModelListCache()
s := Server{modelCaches: &modelCaches{modelList: cache}}
cache.Start(context.Background())
if err := cache.Wait(context.Background()); err != nil {
t.Fatal(err)
}
createModel := func(name string) {
t.Helper()
_, digest := createBinFile(t, nil, nil)
w := createRequest(t, s.CreateHandler, api.CreateRequest{
Model: name,
Files: map[string]string{"model.gguf": digest},
Stream: &stream,
})
if w.Code != http.StatusOK {
t.Fatalf("create %s status = %d, want 200: %s", name, w.Code, w.Body.String())
}
}
createModel("older-model")
createModel("newer-model")
setManifestTime := func(name string, modified time.Time) {
t.Helper()
parsed := model.ParseName(name)
path, err := manifest.PathForName(parsed)
if err != nil {
t.Fatalf("manifest path for %s: %v", name, err)
}
if err := os.Chtimes(path, modified, modified); err != nil {
t.Fatalf("set manifest time for %s: %v", name, err)
}
if err := cache.RefreshModel(parsed); err != nil {
t.Fatalf("refresh %s: %v", name, err)
}
}
older := time.Unix(1000, 0).UTC()
newer := time.Unix(2000, 0).UTC()
setManifestTime("older-model:latest", older)
setManifestTime("newer-model:latest", newer)
router, err := s.GenerateRoutes(nil)
if err != nil {
t.Fatal(err)
}
doGet := func(path string, dest any) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, path, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
body, err := io.ReadAll(w.Body)
if err != nil {
t.Fatalf("read %s response: %v", path, err)
}
if w.Code != http.StatusOK {
t.Fatalf("GET %s status = %d, want 200: %s", path, w.Code, string(body))
}
if err := json.Unmarshal(body, dest); err != nil {
t.Fatalf("decode %s response: %v", path, err)
}
}
var tags api.ListResponse
doGet("/api/tags", &tags)
var models openai.ListCompletion
doGet("/v1/models", &models)
if len(tags.Models) != 2 {
t.Fatalf("/api/tags models = %d, want 2: %+v", len(tags.Models), tags.Models)
}
if len(models.Data) != len(tags.Models) {
t.Fatalf("/v1/models data = %d, want %d", len(models.Data), len(tags.Models))
}
for i, tagModel := range tags.Models {
v1Model := models.Data[i]
if v1Model.Id != tagModel.Model {
t.Fatalf("model %d id = %q, want /api/tags model %q", i, v1Model.Id, tagModel.Model)
}
if v1Model.Created != tagModel.ModifiedAt.Unix() {
t.Fatalf("model %d created = %d, want modified_at %d", i, v1Model.Created, tagModel.ModifiedAt.Unix())
}
}
if got, want := models.Data[0].Id, "newer-model:latest"; got != want {
t.Fatalf("first /v1/models id = %q, want %q", got, want)
}
}