Skip to content

Commit 660346c

Browse files
authored
Merge pull request #721 from VedantMadane/feat/rich-model-metadata
feat: expose richer model metadata in v1/models
2 parents 53ca4b7 + 024ffea commit 660346c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

pkg/inference/models/api.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,38 @@ func ToOpenAI(m types.Model) (*OpenAIModel, error) {
8282
id = tags[0]
8383
}
8484

85-
return &OpenAIModel{
85+
model := &OpenAIModel{
8686
ID: id,
8787
Object: "model",
8888
Created: created,
8989
OwnedBy: "docker",
90-
}, nil
90+
}
91+
92+
config, err := m.Config()
93+
if err != nil {
94+
return nil, fmt.Errorf("get config: %w", err)
95+
}
96+
97+
if config != nil {
98+
model.DMR = &DMRMetadata{
99+
ContextWindow: config.GetContextSize(),
100+
Architecture: config.GetArchitecture(),
101+
Parameters: config.GetParameters(),
102+
Quantization: config.GetQuantization(),
103+
Size: config.GetSize(),
104+
}
105+
}
106+
107+
return model, nil
108+
}
109+
110+
// DMRMetadata contains Docker Model Runner-specific metadata about a model.
111+
type DMRMetadata struct {
112+
ContextWindow *int32 `json:"context_window,omitempty"`
113+
Architecture string `json:"architecture,omitempty"`
114+
Parameters string `json:"parameters,omitempty"`
115+
Quantization string `json:"quantization,omitempty"`
116+
Size string `json:"size,omitempty"`
91117
}
92118

93119
// OpenAIModel represents a locally stored model using OpenAI conventions.
@@ -100,6 +126,8 @@ type OpenAIModel struct {
100126
Created int64 `json:"created"`
101127
// OwnedBy is the model owner. At the moment, it is always "docker".
102128
OwnedBy string `json:"owned_by"`
129+
// DMR contains Docker Model Runner-specific metadata.
130+
DMR *DMRMetadata `json:"dmr,omitempty"`
103131
}
104132

105133
// OpenAIModelList represents a list of models using OpenAI conventions.

pkg/inference/models/api_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,172 @@ func TestModelUnmarshalJSONInvalidData(t *testing.T) {
312312
}
313313
}
314314

315+
// mockModel implements types.Model for testing ToOpenAI.
316+
type mockModel struct {
317+
id string
318+
tags []string
319+
config types.ModelConfig
320+
desc types.Descriptor
321+
}
322+
323+
func (m *mockModel) ID() (string, error) { return m.id, nil }
324+
func (m *mockModel) Tags() []string { return m.tags }
325+
func (m *mockModel) Config() (types.ModelConfig, error) { return m.config, nil }
326+
func (m *mockModel) Descriptor() (types.Descriptor, error) { return m.desc, nil }
327+
func (m *mockModel) GGUFPaths() ([]string, error) { return nil, nil }
328+
func (m *mockModel) SafetensorsPaths() ([]string, error) { return nil, nil }
329+
func (m *mockModel) DDUFPaths() ([]string, error) { return nil, nil }
330+
func (m *mockModel) ConfigArchivePath() (string, error) { return "", nil }
331+
func (m *mockModel) MMPROJPath() (string, error) { return "", nil }
332+
func (m *mockModel) ChatTemplatePath() (string, error) { return "", nil }
333+
334+
func TestToOpenAIWithFullConfig(t *testing.T) {
335+
m := &mockModel{
336+
id: "sha256:abc123",
337+
tags: []string{"ai/smollm2:latest"},
338+
config: &types.Config{
339+
Format: "gguf",
340+
Quantization: "Q4_K_M",
341+
Parameters: "1.7B",
342+
Architecture: "llama",
343+
Size: "1.7B",
344+
ContextSize: int32Ptr(8192),
345+
},
346+
desc: types.Descriptor{},
347+
}
348+
349+
result, err := ToOpenAI(m)
350+
require.NoError(t, err)
351+
352+
assert.Equal(t, "ai/smollm2:latest", result.ID)
353+
assert.Equal(t, "model", result.Object)
354+
assert.Equal(t, "docker", result.OwnedBy)
355+
356+
require.NotNil(t, result.DMR)
357+
require.NotNil(t, result.DMR.ContextWindow)
358+
assert.Equal(t, int32(8192), *result.DMR.ContextWindow)
359+
assert.Equal(t, "llama", result.DMR.Architecture)
360+
assert.Equal(t, "1.7B", result.DMR.Parameters)
361+
assert.Equal(t, "Q4_K_M", result.DMR.Quantization)
362+
assert.Equal(t, "1.7B", result.DMR.Size)
363+
}
364+
365+
func TestToOpenAIWithNilConfig(t *testing.T) {
366+
m := &mockModel{
367+
id: "sha256:abc123",
368+
tags: []string{"ai/model:latest"},
369+
desc: types.Descriptor{},
370+
}
371+
372+
result, err := ToOpenAI(m)
373+
require.NoError(t, err)
374+
375+
assert.Equal(t, "ai/model:latest", result.ID)
376+
assert.Equal(t, "model", result.Object)
377+
assert.Equal(t, "docker", result.OwnedBy)
378+
assert.Nil(t, result.DMR)
379+
}
380+
381+
func TestToOpenAIWithoutTags(t *testing.T) {
382+
m := &mockModel{
383+
id: "sha256:abc123",
384+
desc: types.Descriptor{},
385+
config: &types.Config{
386+
Architecture: "mistral",
387+
},
388+
}
389+
390+
result, err := ToOpenAI(m)
391+
require.NoError(t, err)
392+
393+
assert.Equal(t, "sha256:abc123", result.ID)
394+
require.NotNil(t, result.DMR)
395+
assert.Equal(t, "mistral", result.DMR.Architecture)
396+
}
397+
398+
func TestToOpenAIDMROmittedWhenNilConfig(t *testing.T) {
399+
m := &mockModel{
400+
id: "sha256:abc123",
401+
tags: []string{"ai/model:latest"},
402+
desc: types.Descriptor{},
403+
}
404+
405+
result, err := ToOpenAI(m)
406+
require.NoError(t, err)
407+
408+
data, err := json.Marshal(result)
409+
require.NoError(t, err)
410+
411+
var raw map[string]interface{}
412+
err = json.Unmarshal(data, &raw)
413+
require.NoError(t, err)
414+
415+
_, hasDMR := raw["dmr"]
416+
assert.False(t, hasDMR, "dmr field should be omitted when config is nil")
417+
}
418+
419+
func TestToOpenAIContextWindowOmittedWhenNil(t *testing.T) {
420+
m := &mockModel{
421+
id: "sha256:abc123",
422+
tags: []string{"ai/model:latest"},
423+
desc: types.Descriptor{},
424+
config: &types.Config{
425+
Architecture: "llama",
426+
},
427+
}
428+
429+
result, err := ToOpenAI(m)
430+
require.NoError(t, err)
431+
432+
require.NotNil(t, result.DMR)
433+
assert.Nil(t, result.DMR.ContextWindow)
434+
435+
data, err := json.Marshal(result)
436+
require.NoError(t, err)
437+
438+
var raw map[string]interface{}
439+
err = json.Unmarshal(data, &raw)
440+
require.NoError(t, err)
441+
442+
dmr, ok := raw["dmr"].(map[string]interface{})
443+
require.True(t, ok)
444+
_, hasCtxWindow := dmr["context_window"]
445+
assert.False(t, hasCtxWindow, "context_window should be omitted when nil")
446+
}
447+
448+
func TestToOpenAIList(t *testing.T) {
449+
models := []types.Model{
450+
&mockModel{
451+
id: "sha256:aaa",
452+
tags: []string{"ai/model1:latest"},
453+
desc: types.Descriptor{},
454+
config: &types.Config{
455+
Architecture: "llama",
456+
Parameters: "7B",
457+
},
458+
},
459+
&mockModel{
460+
id: "sha256:bbb",
461+
tags: []string{"ai/model2:latest"},
462+
desc: types.Descriptor{},
463+
},
464+
}
465+
466+
result, err := ToOpenAIList(models)
467+
require.NoError(t, err)
468+
469+
assert.Equal(t, "list", result.Object)
470+
require.Len(t, result.Data, 2)
471+
472+
assert.Equal(t, "ai/model1:latest", result.Data[0].ID)
473+
require.NotNil(t, result.Data[0].DMR)
474+
assert.Equal(t, "llama", result.Data[0].DMR.Architecture)
475+
assert.Equal(t, "7B", result.Data[0].DMR.Parameters)
476+
477+
assert.Equal(t, "ai/model2:latest", result.Data[1].ID)
478+
assert.Nil(t, result.Data[1].DMR)
479+
}
480+
315481
// Helper function to create int32 pointers
316482
func int32Ptr(i int32) *int32 {
317483
return &i

0 commit comments

Comments
 (0)