-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: add Ollama API endpoints /api/version, /api/tags, /api/show for Copilot integration #442
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
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 |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package ollama | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "net/http" | ||
| "github.com/go-chi/chi/v5" | ||
| "ds2api/internal/config" | ||
| "ds2api/internal/util" | ||
| ) | ||
|
|
||
| var WriteJSON = util.WriteJSON | ||
|
|
||
| type ConfigReader interface { | ||
| ModelAliases() map[string]string | ||
| } | ||
|
|
||
| type Handler struct { | ||
| Store ConfigReader | ||
| } | ||
|
|
||
| type OllamaModelRequest struct { | ||
| Model string `json:"model"` | ||
| } | ||
|
|
||
| func RegisterRoutes(r chi.Router, h *Handler) { | ||
| r.Get("/api/version", h.GetVersion) | ||
| r.Get("/api/tags", h.ListOllamaModels) | ||
| r.Post("/api/show", h.GetOllamaModel) | ||
| } | ||
|
|
||
| func (h *Handler) GetVersion(w http.ResponseWriter, r *http.Request) { | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write([]byte(`{"version":"0.23.1"}`)) | ||
| } | ||
| func (h *Handler) ListOllamaModels(w http.ResponseWriter, r *http.Request) { | ||
| WriteJSON(w, http.StatusOK, config.OllamaModelsResponse()) | ||
| } | ||
| func (h *Handler) GetOllamaModel(w http.ResponseWriter, r *http.Request) { | ||
| var payload OllamaModelRequest | ||
| if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { | ||
| http.Error(w, "Invalid JSON body: "+err.Error(), http.StatusBadRequest) | ||
| return | ||
| } | ||
| defer r.Body.Close() | ||
| modelID := payload.Model | ||
| model, ok := config.OllamaModelByID(h.Store, modelID) | ||
| if !ok { | ||
| http.Error(w, "Model not found.", http.StatusNotFound) | ||
| return | ||
| } | ||
| WriteJSON(w, http.StatusOK, model) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| package ollama | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
|
|
||
| "github.com/go-chi/chi/v5" | ||
| ) | ||
|
|
||
| type ollamaTestSurface struct { | ||
| Store shared.ConfigReader | ||
| handler *Handler | ||
| } | ||
|
|
||
| GetVersion(w http.ResponseWriter, r *http.Request) { | ||
|
Check failure on line 16 in internal/httpapi/ollama/handler_routes_test.go
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write([]byte(`{"version":"0.23.1"}`)) | ||
| } | ||
| func (h *Handler) ListOllamaModels(w http.ResponseWriter, r *http.Request) { | ||
| WriteJSON(w, http.StatusOK, config.OllamaModelsResponse()) | ||
| } | ||
| func (h *Handler) GetOllamaModel | ||
|
|
||
| func registerOllamaTestRoutes(r chi.Router, h *ollamaTestSurface) { | ||
| r.Get("/api/version", h.handler().GetVersion) | ||
| r.Get("/api/tags", h.modelsHandler().ListOllamaModels) | ||
| r.Post("/api/show", h.chatHandler().GetOllamaModel) | ||
| } | ||
|
|
||
|
|
||
| func TestGetOllamaVersionRoute(t *testing.T) { | ||
| h := &ollamaTestSurface{} | ||
| r := chi.NewRouter() | ||
| registerOllamaTestRoutes(r, h) | ||
| req := httptest.NewRequest(http.MethodGet, "/api/version", nil) | ||
| rec := httptest.NewRecorder() | ||
| r.ServeHTTP(rec, req) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| func TestGetOllamaModelsRoute(t *testing.T) { | ||
| h := &ollamaTestSurface{} | ||
| r := chi.NewRouter() | ||
| registerOllamaTestRoutes(r, h) | ||
| req := httptest.NewRequest(http.MethodGet, "/api/tags", nil) | ||
| rec := httptest.NewRecorder() | ||
| r.ServeHTTP(rec, req) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| func TestGetOllamaModelRoute(t *testing.T) { | ||
| h := &ollamaTestSurface{} | ||
| r := chi.NewRouter() | ||
| registerOllamaTestRoutes(r, h) | ||
|
|
||
| t.Run("direct", func(t *testing.T) { | ||
| body := `{"model":"deepseek-v4-flash"}` | ||
| req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(body)) | ||
| req.Header.Set("Content-Type", "application/json") | ||
| rec := httptest.NewRecorder() | ||
| r.ServeHTTP(rec, req) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("direct_nothinking", func(t *testing.T) { | ||
| body := `{"model":"deepseek-v4-flash-nothinking"}` | ||
| req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(body)) | ||
| req.Header.Set("Content-Type", "application/json") | ||
| rec := httptest.NewRecorder() | ||
| r.ServeHTTP(rec, req) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("direct_expert", func(t *testing.T) { | ||
| body := `{"model":"models/deepseek-v4-pro"}` | ||
| req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(body)) | ||
| req.Header.Set("Content-Type", "application/json") | ||
| rec := httptest.NewRecorder() | ||
| r.ServeHTTP(rec, req) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("direct_vision", func(t *testing.T) { | ||
| body := `{"model":"deepseek-v4-vision"}` | ||
| req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(body)) | ||
| req.Header.Set("Content-Type", "application/json") | ||
| rec := httptest.NewRecorder() | ||
| r.ServeHTTP(rec, req) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| func TestGetOllamaModelRouteNotFound(t *testing.T) { | ||
| h := &ollamaTestSurface{} | ||
| r := chi.NewRouter() | ||
| registerOllamaTestRoutes(r, h) | ||
|
|
||
| body := `{"model":"not-exists"}` | ||
| req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(body)) | ||
| req.Header.Set("Content-Type", "application/json") | ||
| rec := httptest.NewRecorder() | ||
| r.ServeHTTP(rec, req) | ||
| if rec.Code != http.StatusNotFound { | ||
| t.Fatalf("expected 404, got %d body=%s", rec.Code, rec.Body.String()) | ||
| } | ||
| } | ||
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.
models/-prefixed IDs before resolving/api/showforwardspayload.modeldirectly intoResolveModel, which only accepts canonical model names or configured aliases. Inputs like"models/deepseek-v4-pro"therefore resolve to not found and return 404, even though they refer to supported models (this exact form is exercised in the added route tests). Any Copilot/Ollama client that sends prefixed model IDs will fail model capability discovery.Useful? React with 👍 / 👎.