mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-17 13:19:18 +02:00
Ollama 0.33.3 gives two answers for gemma3: /api/tags lists it as ['completion'], while /api/show for the same model reports ['completion', 'vision']. getModelCapabilities trusted the list outright and cached it for the life of the process, so image upload stayed disabled for gemma3, one of the most common small vision models, even though the model downloader correctly shows it as text + image. Re-pulling does not help: the local digest matches the registry. A list that reports vision is still trusted without probing. A list that omits it is no longer conclusive: the model is probed through /api/show (then llama.cpp /props), and the list's answer stands only when the probes have nothing better, which keeps hybrid routers without /api/show working. The service's own copy of the short-circuit is removed so the rule lives in the one tested function. Verified on NOMAD3 (v1.35.0 + this patch): installed-models moves gemma3:4b from unsupported to supported, every other model unchanged, and a chat with an attached image is answered correctly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
import type { ModelVisionCapability } from '../../types/ollama.js'
|
|
|
|
export type ModelCapabilities = {
|
|
thinking: boolean
|
|
vision: ModelVisionCapability
|
|
}
|
|
|
|
export type ModelCapabilityProbes = {
|
|
ollamaShow: () => Promise<unknown>
|
|
llamaProps: () => Promise<unknown>
|
|
}
|
|
|
|
export type InstalledModelMetadata = {
|
|
name: string
|
|
size: number
|
|
capabilities?: string[]
|
|
}
|
|
|
|
export function capabilitiesFromOllamaShow(value: unknown): ModelCapabilities | null {
|
|
if (!value || typeof value !== 'object') return null
|
|
const capabilities = (value as { capabilities?: unknown }).capabilities
|
|
if (!Array.isArray(capabilities) || !capabilities.every((item) => typeof item === 'string')) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
thinking: capabilities.includes('thinking'),
|
|
vision: capabilities.includes('vision') ? 'supported' : 'unsupported',
|
|
}
|
|
}
|
|
|
|
export function visionCapabilityFromLlamaProps(value: unknown): ModelVisionCapability | null {
|
|
if (!value || typeof value !== 'object') return null
|
|
const modalities = (value as { modalities?: unknown }).modalities
|
|
if (modalities && typeof modalities === 'object' && !Array.isArray(modalities)) {
|
|
const vision = (modalities as { vision?: unknown }).vision
|
|
return typeof vision === 'boolean' ? (vision ? 'supported' : 'unsupported') : null
|
|
}
|
|
if (Array.isArray(modalities) && modalities.every((item) => typeof item === 'string')) {
|
|
return modalities.includes('image') || modalities.includes('vision')
|
|
? 'supported'
|
|
: 'unsupported'
|
|
}
|
|
return null
|
|
}
|
|
|
|
export async function resolveModelCapabilities(
|
|
advertisedMetadata: unknown,
|
|
probes: ModelCapabilityProbes
|
|
): Promise<ModelCapabilities | null> {
|
|
const advertised = capabilitiesFromOllamaShow(advertisedMetadata)
|
|
// A model list that reports vision is trusted outright. One that does not is
|
|
// not conclusive: Ollama's /api/tags lists gemma3 as ['completion'] while its
|
|
// /api/show for the same model reports ['completion', 'vision'], which left
|
|
// image upload disabled for one of the most common small vision models.
|
|
if (advertised?.vision === 'supported') return advertised
|
|
|
|
try {
|
|
const ollama = capabilitiesFromOllamaShow(await probes.ollamaShow())
|
|
if (ollama) return ollama
|
|
} catch {}
|
|
|
|
try {
|
|
const vision = visionCapabilityFromLlamaProps(await probes.llamaProps())
|
|
if (vision) return { thinking: advertised?.thinking ?? false, vision }
|
|
} catch {}
|
|
|
|
return advertised
|
|
}
|
|
|
|
export function installedModelsFromOpenAIResponse(value: unknown): InstalledModelMetadata[] {
|
|
if (!value || typeof value !== 'object') return []
|
|
const data = (value as { data?: unknown }).data
|
|
if (!Array.isArray(data)) return []
|
|
|
|
return data.flatMap((entry) => {
|
|
if (!entry || typeof entry !== 'object') return []
|
|
const { id, capabilities } = entry as { id?: unknown; capabilities?: unknown }
|
|
if (typeof id !== 'string') return []
|
|
|
|
const model: InstalledModelMetadata = { name: id, size: 0 }
|
|
if (
|
|
Array.isArray(capabilities) &&
|
|
capabilities.every((capability) => typeof capability === 'string')
|
|
) {
|
|
model.capabilities = capabilities
|
|
}
|
|
return [model]
|
|
})
|
|
}
|