[object Object]

← back to Exo

fix ollama API compatibility for VS Code Copilot (#2091)

e8ec8d501004b5152d098109bd3b3f3529a73f34 · 2026-05-14 17:12:58 +0100 · Heidar

Ollama adapter fixes for VS Code Copilot (#2042):

  - /api/version: bare semver "1.0.0" - Copilot parseInts each segment.
- /api/show: populate model_info + capabilities - Copilot crashes on
null model_info and filters by `tools`.
- Add POST /ollama/v1/chat/completions - ollama serves the OpenAI-compat
route here, BYOK clients 405 without it.


Before:
<img width="1380" height="144" alt="image"
src="https://github.com/user-attachments/assets/99d5464f-187d-4432-9a31-8229c55aa209"
/>

After:
<img width="1362" height="181" alt="image"
src="https://github.com/user-attachments/assets/361dc006-d8df-435f-8d8b-4fa4f44a8c23"
/>
<img width="279" height="909" alt="image"
src="https://github.com/user-attachments/assets/4621aba7-bd57-4762-8568-34a3383a6025"
/>

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

Files touched

Diff

commit e8ec8d501004b5152d098109bd3b3f3529a73f34
Author: Heidar <74025356+Heidar-An@users.noreply.github.com>
Date:   Thu May 14 17:12:58 2026 +0100

    fix ollama API compatibility for VS Code Copilot (#2091)
    
    Ollama adapter fixes for VS Code Copilot (#2042):
    
      - /api/version: bare semver "1.0.0" - Copilot parseInts each segment.
    - /api/show: populate model_info + capabilities - Copilot crashes on
    null model_info and filters by `tools`.
    - Add POST /ollama/v1/chat/completions - ollama serves the OpenAI-compat
    route here, BYOK clients 405 without it.
    
    
    Before:
    <img width="1380" height="144" alt="image"
    src="https://github.com/user-attachments/assets/99d5464f-187d-4432-9a31-8229c55aa209"
    />
    
    After:
    <img width="1362" height="181" alt="image"
    src="https://github.com/user-attachments/assets/361dc006-d8df-435f-8d8b-4fa4f44a8c23"
    />
    <img width="279" height="909" alt="image"
    src="https://github.com/user-attachments/assets/4621aba7-bd57-4762-8568-34a3383a6025"
    />
    
    Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
---
 src/exo/api/main.py             | 23 ++++++++++++++++++++++-
 src/exo/api/types/ollama_api.py | 11 +++++++++++
 2 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/src/exo/api/main.py b/src/exo/api/main.py
index e346a4f9..d3b03e46 100644
--- a/src/exo/api/main.py
+++ b/src/exo/api/main.py
@@ -104,6 +104,7 @@ from exo.api.types.claude_api import (
     ClaudeMessagesResponse,
 )
 from exo.api.types.ollama_api import (
+    OllamaCapability,
     OllamaChatRequest,
     OllamaChatResponse,
     OllamaGenerateRequest,
@@ -138,6 +139,7 @@ from exo.shared.models import model_cards
 from exo.shared.models.model_cards import (
     ModelCard,
     ModelId,
+    ModelTask,
 )
 from exo.shared.tracing import TraceEvent, compute_stats, export_trace, load_trace_file
 from exo.shared.types.chunks import (
@@ -375,6 +377,9 @@ class API:
         # Ollama API
         self.app.head("/ollama/")(self.ollama_version)
         self.app.head("/ollama/api/version")(self.ollama_version)
+        self.app.post("/ollama/v1/chat/completions", response_model=None)(
+            self.chat_completions
+        )
         self.app.post("/ollama/api/chat", response_model=None)(self.ollama_chat)
         self.app.post("/ollama/api/api/chat", response_model=None)(self.ollama_chat)
         self.app.post("/ollama/api/v1/chat", response_model=None)(self.ollama_chat)
@@ -1678,6 +1683,20 @@ class API:
                 status_code=404, detail=f"Model not found: {model_name}"
             ) from exc
 
+        capabilities: list[OllamaCapability] = []
+        if ModelTask.TextGeneration in card.tasks:
+            capabilities.extend(("completion", "tools"))
+        if card.vision is not None:
+            capabilities.append("vision")
+
+        architecture = card.family or "unknown"
+        model_info: dict[str, Any] = {
+            "general.architecture": architecture,
+            "general.basename": card.base_model or str(card.model_id),
+        }
+        if card.context_length > 0:
+            model_info[f"{architecture}.context_length"] = card.context_length
+
         return OllamaShowResponse(
             modelfile=f"FROM {card.model_id}",
             template="{{ .Prompt }}",
@@ -1685,6 +1704,8 @@ class API:
                 family=card.family or None,
                 quantization_level=card.quantization or None,
             ),
+            model_info=model_info,
+            capabilities=capabilities,
         )
 
     async def ollama_ps(self) -> OllamaPsResponse:
@@ -1707,7 +1728,7 @@ class API:
 
     async def ollama_version(self) -> dict[str, str]:
         """Returns version information for Ollama API compatibility."""
-        return {"version": "exo v1.0"}
+        return {"version": "1.0.0"}
 
     def _calculate_total_available_memory(self) -> Memory:
         """Calculate total available memory across all nodes in bytes."""
diff --git a/src/exo/api/types/ollama_api.py b/src/exo/api/types/ollama_api.py
index 58a54ac0..c122a5dc 100644
--- a/src/exo/api/types/ollama_api.py
+++ b/src/exo/api/types/ollama_api.py
@@ -11,6 +11,16 @@ from exo.shared.models.model_cards import ModelId
 
 OllamaRole = Literal["system", "user", "assistant", "tool"]
 OllamaDoneReason = Literal["stop", "length", "tool_call", "error"]
+OllamaCapability = Literal[
+    "completion",
+    "tools",
+    "insert",
+    "vision",
+    "embedding",
+    "thinking",
+    "image",
+    "audio",
+]
 
 
 class OllamaToolFunction(BaseModel, frozen=True):
@@ -133,6 +143,7 @@ class OllamaShowResponse(BaseModel, frozen=True, strict=True):
     template: str | None = None
     details: OllamaModelDetails | None = None
     model_info: dict[str, Any] | None = None
+    capabilities: list[OllamaCapability] = []
 
 
 class OllamaPsModel(BaseModel, frozen=True, strict=True):

← 1fd15d59 create directory on startup (#2089)  ·  back to Exo  ·  fix: omit null delta fields in streaming chat completions (i 88d46d46 →