feat: advertise clip family and vision capabilities in Ollama metadata and handle Anthropic image blocks

This commit is contained in:
wmantly
2026-09-04 20:51:11 +00:00
parent 41265cf9db
commit ac66e70c89
+46 -10
View File
@@ -312,12 +312,12 @@ def _model_entry(tag):
"details": { "details": {
"format": "gguf", "format": "gguf",
"family": "qwen35", "family": "qwen35",
"families": ["qwen35", "qwen2"], "families": ["qwen35", "qwen2", "clip"],
"parameter_size": "27.0B", "parameter_size": "27.0B",
"quantization_level": "Q4_K_P", "quantization_level": "Q4_K_P",
"context_length": CTX_SIZE, "context_length": CTX_SIZE,
}, },
"capabilities": ["completion", "chat", "tools", "thinking"], "capabilities": ["completion", "chat", "tools", "thinking", "vision", "multimodal"],
} }
def ollama_tags(): def ollama_tags():
@@ -334,7 +334,7 @@ def ollama_ps():
"details": { "details": {
"format": "gguf", "format": "gguf",
"family": "qwen35", "family": "qwen35",
"families": ["qwen35", "qwen2"], "families": ["qwen35", "qwen2", "clip"],
"parameter_size": "27.0B", "parameter_size": "27.0B",
"quantization_level": "Q4_K_P", "quantization_level": "Q4_K_P",
"context_length": CTX_SIZE, "context_length": CTX_SIZE,
@@ -358,7 +358,7 @@ def ollama_show(payload=None):
"details": { "details": {
"format": "gguf", "format": "gguf",
"family": "qwen35", "family": "qwen35",
"families": ["qwen35", "qwen2"], "families": ["qwen35", "qwen2", "clip"],
"parameter_size": "27.0B", "parameter_size": "27.0B",
"quantization_level": "Q4_K_P", "quantization_level": "Q4_K_P",
"context_length": CTX_SIZE, "context_length": CTX_SIZE,
@@ -372,8 +372,14 @@ def ollama_show(payload=None):
"qwen35.attention.head_count": 32, "qwen35.attention.head_count": 32,
"qwen35.attention.head_count_kv": 4, "qwen35.attention.head_count_kv": 4,
"qwen35.max_output_tokens": MAX_OUTPUT, "qwen35.max_output_tokens": MAX_OUTPUT,
"clip.has_vision_encoder": True,
"clip.has_text_encoder": True,
}, },
"capabilities": ["completion", "chat", "tools", "thinking"], "projector_info": {
"general.architecture": "clip",
"clip.projector_type": "mlp",
},
"capabilities": ["completion", "chat", "tools", "thinking", "vision", "multimodal"],
} }
def ollama_status(): def ollama_status():
@@ -869,27 +875,57 @@ def ollama_pull(payload):
# Anthropic Messages API # Anthropic Messages API
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
def _anthropic_content_to_openai(content, is_last=False): def _anthropic_content_to_openai(content, is_last=False):
"""Convert Anthropic message content (string or block array) to OpenAI string.""" """Convert Anthropic message content (string or block array) to OpenAI content (string or list)."""
if isinstance(content, str): if isinstance(content, str):
return content return content
if not isinstance(content, list):
return str(content)
has_images = any(isinstance(b, dict) and b.get("type") == "image" for b in content)
if not has_images:
parts = [] parts = []
for block in content: for block in content:
if not isinstance(block, dict):
parts.append(str(block))
continue
btype = block.get("type") btype = block.get("type")
if btype == "text": if btype == "text":
parts.append(block.get("text", "")) parts.append(block.get("text", ""))
elif btype == "thinking": elif btype == "thinking":
# keep reasoning in context
if EMIT_THINKING: if EMIT_THINKING:
parts.append(block.get("thinking", "")) parts.append(block.get("thinking", ""))
elif btype == "tool_result": elif btype == "tool_result":
tc = block.get("content", "") tc = block.get("content", "")
if isinstance(tc, list): if isinstance(tc, list):
tc = "".join(b.get("text", "") for b in tc if b.get("type") == "text") tc = "".join(b.get("text", "") for b in tc if isinstance(b, dict) and b.get("type") == "text")
parts.append(f"[tool_result: {tc}]") parts.append(f"[tool_result: {tc}]")
elif btype == "image":
parts.append("[image]")
return "\n".join(parts) return "\n".join(parts)
# Multi-part content with vision images
out_blocks = []
for block in content:
if not isinstance(block, dict):
continue
btype = block.get("type")
if btype == "text":
out_blocks.append({"type": "text", "text": block.get("text", "")})
elif btype == "thinking":
if EMIT_THINKING:
out_blocks.append({"type": "text", "text": block.get("thinking", "")})
elif btype == "tool_result":
tc = block.get("content", "")
if isinstance(tc, list):
tc = "".join(b.get("text", "") for b in tc if isinstance(b, dict) and b.get("type") == "text")
out_blocks.append({"type": "text", "text": f"[tool_result: {tc}]"})
elif btype == "image":
source = block.get("source", {})
if isinstance(source, dict):
data = source.get("data", "")
mtype = source.get("media_type", "image/jpeg")
out_blocks.append({"type": "image_url", "image_url": {"url": f"data:{mtype};base64,{data}"}})
return out_blocks
def _anthropic_tools_to_openai(tools): def _anthropic_tools_to_openai(tools):
"""Anthropic tools ({name,input_schema}) -> OpenAI tools.""" """Anthropic tools ({name,input_schema}) -> OpenAI tools."""
if not tools: if not tools: