-
Notifications
You must be signed in to change notification settings - Fork 115
fix: update Ollama API compatibility version and enhance ShowResponse structure for VSCode Copilot #862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: update Ollama API compatibility version and enhance ShowResponse structure for VSCode Copilot #862
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,7 @@ import ( | |||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| "github.com/docker/model-runner/pkg/distribution/oci" | ||||||||||||||||||||||||||||||||||
| "github.com/docker/model-runner/pkg/distribution/types" | ||||||||||||||||||||||||||||||||||
| "github.com/docker/model-runner/pkg/inference" | ||||||||||||||||||||||||||||||||||
| "github.com/docker/model-runner/pkg/inference/models" | ||||||||||||||||||||||||||||||||||
| "github.com/docker/model-runner/pkg/inference/scheduling" | ||||||||||||||||||||||||||||||||||
|
|
@@ -179,10 +180,15 @@ func (w *ollamaProgressWriter) Flush() { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ollamaCompatVersion is the Ollama API compatibility version reported by Docker Model Runner. | ||||||||||||||||||||||||||||||||||
| // This must be >= the minimum version required by clients (e.g., VSCode Copilot Chat requires >= 0.6.4). | ||||||||||||||||||||||||||||||||||
| // Bump this when adding support for features introduced in newer Ollama versions. | ||||||||||||||||||||||||||||||||||
| const ollamaCompatVersion = "0.6.4" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // handleVersion handles GET /api/version | ||||||||||||||||||||||||||||||||||
| func (h *HTTPHandler) handleVersion(w http.ResponseWriter, r *http.Request) { | ||||||||||||||||||||||||||||||||||
| response := map[string]string{ | ||||||||||||||||||||||||||||||||||
| "version": "0.1.0", | ||||||||||||||||||||||||||||||||||
| "version": ollamaCompatVersion, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| w.Header().Set("Content-Type", "application/json") | ||||||||||||||||||||||||||||||||||
|
|
@@ -337,23 +343,92 @@ func (h *HTTPHandler) handleShowModel(w http.ResponseWriter, r *http.Request) { | |||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| architecture := config.GetArchitecture() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Build response | ||||||||||||||||||||||||||||||||||
| response := ShowResponse{ | ||||||||||||||||||||||||||||||||||
| Details: ModelDetails{ | ||||||||||||||||||||||||||||||||||
| Format: "gguf", | ||||||||||||||||||||||||||||||||||
| Family: config.GetArchitecture(), | ||||||||||||||||||||||||||||||||||
| Families: []string{config.GetArchitecture()}, | ||||||||||||||||||||||||||||||||||
| Family: architecture, | ||||||||||||||||||||||||||||||||||
| Families: []string{architecture}, | ||||||||||||||||||||||||||||||||||
| ParameterSize: config.GetParameters(), | ||||||||||||||||||||||||||||||||||
| QuantizationLevel: config.GetQuantization(), | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Build capabilities list and model_info for Ollama API compatibility | ||||||||||||||||||||||||||||||||||
| // (required by clients like VSCode Copilot Chat) | ||||||||||||||||||||||||||||||||||
| response.Capabilities, response.ModelInfo = h.buildShowModelMetadata(model, config, architecture) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Try to read the chat template | ||||||||||||||||||||||||||||||||||
| if templatePath, err := model.ChatTemplatePath(); err == nil && templatePath != "" { | ||||||||||||||||||||||||||||||||||
| response.Template = templatePath | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+364
to
+366
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Ollama API expects the
Suggested change
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| w.Header().Set("Content-Type", "application/json") | ||||||||||||||||||||||||||||||||||
| if err := json.NewEncoder(w).Encode(response); err != nil { | ||||||||||||||||||||||||||||||||||
| h.log.Error("Failed to encode response", "error", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // buildShowModelMetadata extracts capabilities and model_info from model config. | ||||||||||||||||||||||||||||||||||
| // This provides the metadata that Ollama clients (e.g., VSCode Copilot Chat) need | ||||||||||||||||||||||||||||||||||
| // to discover model features like vision support, tool calling, and context window. | ||||||||||||||||||||||||||||||||||
| func (h *HTTPHandler) buildShowModelMetadata(model types.Model, config types.ModelConfig, architecture string) ([]string, map[string]interface{}) { | ||||||||||||||||||||||||||||||||||
| capabilities := []string{"completion"} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Check for vision support via multimodal projector | ||||||||||||||||||||||||||||||||||
| if mmprojPath, err := model.MMPROJPath(); err == nil && mmprojPath != "" { | ||||||||||||||||||||||||||||||||||
| capabilities = append(capabilities, "vision") | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Check for tool support - we advertise tool calling for all chat models | ||||||||||||||||||||||||||||||||||
| // since llama.cpp supports tool calling through chat templates | ||||||||||||||||||||||||||||||||||
| capabilities = append(capabilities, "tools") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Build model_info map (Ollama format used by VSCode Copilot Chat) | ||||||||||||||||||||||||||||||||||
| modelInfo := map[string]interface{}{ | ||||||||||||||||||||||||||||||||||
| "general.architecture": architecture, | ||||||||||||||||||||||||||||||||||
| "general.basename": modelName(model), | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Add context length from GGUF metadata or config | ||||||||||||||||||||||||||||||||||
| if ctxSize := config.GetContextSize(); ctxSize != nil { | ||||||||||||||||||||||||||||||||||
| modelInfo[architecture+".context_length"] = *ctxSize | ||||||||||||||||||||||||||||||||||
| } else if typedConfig, ok := config.(*types.Config); ok && typedConfig.GGUF != nil { | ||||||||||||||||||||||||||||||||||
| if ctxLen, found := typedConfig.GGUF[architecture+".context_length"]; found { | ||||||||||||||||||||||||||||||||||
| modelInfo[architecture+".context_length"] = ctxLen | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if embLen, found := typedConfig.GGUF[architecture+".embedding_length"]; found { | ||||||||||||||||||||||||||||||||||
| modelInfo[architecture+".embedding_length"] = embLen | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+398
to
+405
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Values extracted from the
Suggested change
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return capabilities, modelInfo | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // modelName extracts a human-readable name from a model's tags. | ||||||||||||||||||||||||||||||||||
| func modelName(model types.Model) string { | ||||||||||||||||||||||||||||||||||
| tags := model.Tags() | ||||||||||||||||||||||||||||||||||
| if len(tags) > 0 { | ||||||||||||||||||||||||||||||||||
| return tags[0] | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if id, err := model.ID(); err == nil { | ||||||||||||||||||||||||||||||||||
| return id | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // containsString checks if a slice contains a given string. | ||||||||||||||||||||||||||||||||||
| func containsString(slice []string, s string) bool { | ||||||||||||||||||||||||||||||||||
| for _, item := range slice { | ||||||||||||||||||||||||||||||||||
| if item == s { | ||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+422
to
+430
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The References
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // handleChat handles POST /api/chat | ||||||||||||||||||||||||||||||||||
| func (h *HTTPHandler) handleChat(w http.ResponseWriter, r *http.Request) { | ||||||||||||||||||||||||||||||||||
| ctx := r.Context() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ package ollama | |
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
|
|
@@ -200,6 +202,122 @@ func TestEnsureDataURIPrefix(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestHandleVersion(t *testing.T) { | ||
| // Verify version is >= 0.6.4 (minimum required by VSCode Copilot Chat) | ||
| version := ollamaCompatVersion | ||
| parts := strings.Split(version, ".") | ||
| if len(parts) != 3 { | ||
| t.Fatalf("Expected semver format, got %s", version) | ||
| } | ||
|
|
||
| major := 0 | ||
| minor := 0 | ||
| patch := 0 | ||
| fmt.Sscanf(parts[0], "%d", &major) | ||
| fmt.Sscanf(parts[1], "%d", &minor) | ||
| fmt.Sscanf(parts[2], "%d", &patch) | ||
|
|
||
| // Must be >= 0.6.4 | ||
| versionNum := major*10000 + minor*100 + patch | ||
| minimumNum := 0*10000 + 6*100 + 4 | ||
| if versionNum < minimumNum { | ||
| t.Errorf("ollamaCompatVersion %s is below minimum 0.6.4 required by VSCode Copilot Chat", version) | ||
| } | ||
| } | ||
|
|
||
| func TestShowResponseHasRequiredFields(t *testing.T) { | ||
| // Verify ShowResponse struct can marshal the fields required by VSCode Copilot Chat | ||
| response := ShowResponse{ | ||
| Details: ModelDetails{ | ||
| Format: "gguf", | ||
| Family: "llama", | ||
| Families: []string{"llama"}, | ||
| ParameterSize: "8B", | ||
| QuantizationLevel: "Q4_K_M", | ||
| }, | ||
| Template: "{{ .System }}\n{{ .Prompt }}", | ||
| Capabilities: []string{"completion", "tools"}, | ||
| ModelInfo: map[string]interface{}{ | ||
| "general.architecture": "llama", | ||
| "general.basename": "ai/test-model:latest", | ||
| "llama.context_length": 4096, | ||
| "llama.embedding_length": 2048, | ||
| }, | ||
| } | ||
|
|
||
| data, err := json.Marshal(response) | ||
| if err != nil { | ||
| t.Fatalf("Failed to marshal ShowResponse: %v", err) | ||
| } | ||
|
|
||
| // Verify all expected fields are present in JSON | ||
| var parsed map[string]interface{} | ||
| if err := json.Unmarshal(data, &parsed); err != nil { | ||
| t.Fatalf("Failed to unmarshal JSON: %v", err) | ||
| } | ||
|
|
||
| // Check capabilities field exists and is an array | ||
| caps, ok := parsed["capabilities"] | ||
| if !ok { | ||
| t.Error("capabilities field missing from ShowResponse JSON") | ||
| } | ||
| capsArr, ok := caps.([]interface{}) | ||
| if !ok { | ||
| t.Error("capabilities should be an array") | ||
| } | ||
| if len(capsArr) != 2 { | ||
| t.Errorf("Expected 2 capabilities, got %d", len(capsArr)) | ||
| } | ||
|
|
||
| // Check model_info field exists and has required keys | ||
| modelInfo, ok := parsed["model_info"] | ||
| if !ok { | ||
| t.Error("model_info field missing from ShowResponse JSON") | ||
| } | ||
| infoMap, ok := modelInfo.(map[string]interface{}) | ||
| if !ok { | ||
| t.Error("model_info should be a map") | ||
| } | ||
| if _, ok := infoMap["general.architecture"]; !ok { | ||
| t.Error("model_info missing general.architecture") | ||
| } | ||
| if _, ok := infoMap["general.basename"]; !ok { | ||
| t.Error("model_info missing general.basename") | ||
| } | ||
|
|
||
| // Check template field exists | ||
| if _, ok := parsed["template"]; !ok { | ||
| t.Error("template field missing from ShowResponse JSON") | ||
| } | ||
|
|
||
| // Check details.family field exists (required by VSCode Copilot) | ||
| details, ok := parsed["details"].(map[string]interface{}) | ||
| if !ok { | ||
| t.Error("details should be a map") | ||
| } | ||
| if _, ok := details["family"]; !ok { | ||
| t.Error("details.family missing from ShowResponse JSON") | ||
| } | ||
| } | ||
|
|
||
| func TestContainsString(t *testing.T) { | ||
| tests := []struct { | ||
| slice []string | ||
| s string | ||
| expected bool | ||
| }{ | ||
| {[]string{"a", "b", "c"}, "b", true}, | ||
| {[]string{"a", "b", "c"}, "d", false}, | ||
| {[]string{}, "a", false}, | ||
| {[]string{"completion", "tools", "vision"}, "vision", true}, | ||
| } | ||
| for _, tt := range tests { | ||
| if got := containsString(tt.slice, tt.s); got != tt.expected { | ||
| t.Errorf("containsString(%v, %q) = %v, want %v", tt.slice, tt.s, got, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add the }
// containsString checks if a slice contains a given string.
func containsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
} |
||
|
|
||
| func TestConvertMessages_PreservesOrder(t *testing.T) { | ||
| messages := []Message{ | ||
| {Role: "system", Content: "You are a helpful assistant"}, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
ospackage is required to read the chat template file from disk inhandleShowModel. Please add it to the imports.