From 3c1103b2b2c543fae6167b162105a5e24b92f2a8 Mon Sep 17 00:00:00 2001 From: Infersia Date: Tue, 4 Aug 2026 08:34:25 +1000 Subject: [PATCH 1/3] feat(providers): add Infersia provider profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infersia serves open-weight models on dedicated GPUs through an OpenAI-compatible chat-completions endpoint. Two files, no core changes, per plugins/model-providers/README.md. Verified against the live endpoint rather than transcribed from docs: supports_vision=True — an image inside a tool-result message is accepted, tested with a multipart tool message against the served vision model (Step 3.7 Flash). supports_prompt_cache_key=False — prefix caching is automatic and unkeyed. The endpoint tolerates the field rather than honouring it, and the flag is documented as opt-in for endpoints that explicitly accept it, so setting it would advertise behaviour that does not exist. fallback_models=() — following the DeepInfra profile's reasoning: the live catalogue at {base_url}/models is authoritative and returns OpenAI-shaped entries with pricing and context_length. An empty picker on a failed fetch beats routing someone to a retired id, and this catalogue is still changing week to week. default_aux_model is the only hardcoded model id, since aux resolution is synchronous. /models answers unauthenticated, so the doctor probe works before a key is configured. --- plugins/model-providers/infersia/__init__.py | 50 ++++++++++++++++++++ plugins/model-providers/infersia/plugin.yaml | 5 ++ 2 files changed, 55 insertions(+) create mode 100644 plugins/model-providers/infersia/__init__.py create mode 100644 plugins/model-providers/infersia/plugin.yaml diff --git a/plugins/model-providers/infersia/__init__.py b/plugins/model-providers/infersia/__init__.py new file mode 100644 index 0000000000000..71edbfccc3cbb --- /dev/null +++ b/plugins/model-providers/infersia/__init__.py @@ -0,0 +1,50 @@ +"""Infersia provider profile. + +Infersia serves open-weight models on dedicated GPUs through an +OpenAI-compatible chat-completions endpoint, and publishes the exact +quantisation, hardware and measured latency behind every model. + +Address models by their catalogue ID, e.g. ``deepseek/deepseek-v4-flash-0731`` +or ``qwen/qwen3.6-35b-a3b``. Appending ``:free`` selects a rate-limited free +variant where one is published. + +DeepSeek V4 Flash is served at its full 1,048,576-token window rather than a +reduced slice, which is the main reason to reach for this provider on +long-context work. +""" + +from providers import register_provider +from providers.base import ProviderProfile + + +infersia = ProviderProfile( + name="infersia", + aliases=("infersia-ai",), + display_name="Infersia", + description="Infersia — open-weight models with published quantisation", + signup_url="https://infersia.com/dashboard/keys", + env_vars=("INFERSIA_API_KEY", "INFERSIA_BASE_URL"), + base_url="https://api.infersia.com/v1", + auth_type="api_key", + # Images are accepted inside tool-result messages, verified against the + # served vision model (Step 3.7 Flash) with a multipart tool message. + supports_vision=True, + # Prefix caching is automatic and unkeyed here. The endpoint tolerates a + # ``prompt_cache_key`` field rather than honouring it, and this flag is + # documented as opt-in for endpoints that explicitly accept it, so + # claiming it would advertise behaviour that does not exist. + supports_prompt_cache_key=False, + # Auxiliary model for cheap side tasks. Qwen3 8B is the smallest thing in + # the catalogue and the only hardcoded model id here; everything else is + # discovered live. + default_aux_model="qwen/qwen3-8b", + # Deliberately empty, following the DeepInfra profile's reasoning: the + # live catalogue at ``{base_url}/models`` is the source of truth, and it + # returns OpenAI-shaped entries with pricing and context length. When the + # fetch fails the picker shows nothing, which is better than routing + # someone to a model id that has since been retired — this catalogue is + # still changing week to week. + fallback_models=(), +) + +register_provider(infersia) diff --git a/plugins/model-providers/infersia/plugin.yaml b/plugins/model-providers/infersia/plugin.yaml new file mode 100644 index 0000000000000..6ee88824f6841 --- /dev/null +++ b/plugins/model-providers/infersia/plugin.yaml @@ -0,0 +1,5 @@ +name: infersia-provider +kind: model-provider +version: 1.0.0 +description: Infersia — open-weight inference with published quantisation +author: Infersia (@infersia) From 6fc2c6aabcce75b3f32306391a659a0ccefab544 Mon Sep 17 00:00:00 2001 From: Infersia <312459762+infersia@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:12:37 +1000 Subject: [PATCH 2/3] Re-verify the vision flag against a model we still serve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit supports_vision was justified by a check against Step 3.7 Flash, which we retired today. The flag is still correct — qwen/qwen3.6-35b-a3b accepts image input — but the comment pointed at a model that no longer exists, so the claim was unverifiable by anyone reading it. Re-ran the check against the live endpoint: a two-pixel test image sent as a base64 data URI to qwen/qwen3.6-35b-a3b came back correctly described. Comment now names that model and that date. No functional change; fallback_models stays empty so the live catalogue remains the source of truth. Co-Authored-By: Claude Opus 5 --- plugins/model-providers/infersia/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/model-providers/infersia/__init__.py b/plugins/model-providers/infersia/__init__.py index 71edbfccc3cbb..6fb692ee4ecc4 100644 --- a/plugins/model-providers/infersia/__init__.py +++ b/plugins/model-providers/infersia/__init__.py @@ -26,8 +26,10 @@ env_vars=("INFERSIA_API_KEY", "INFERSIA_BASE_URL"), base_url="https://api.infersia.com/v1", auth_type="api_key", - # Images are accepted inside tool-result messages, verified against the - # served vision model (Step 3.7 Flash) with a multipart tool message. + # Images are accepted inside tool-result messages. Re-verified against + # qwen/qwen3.6-35b-a3b on 2026-08-06, after Step 3.7 Flash (the model this + # was originally checked against) was retired: a two-pixel test image sent + # as a base64 data URI came back correctly described. supports_vision=True, # Prefix caching is automatic and unkeyed here. The endpoint tolerates a # ``prompt_cache_key`` field rather than honouring it, and this flag is From c28bab0dd482b4026a4335cf4e5ee6e0dea55c2b Mon Sep 17 00:00:00 2001 From: Infersia <312459762+infersia@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:34:46 +1000 Subject: [PATCH 3/3] Filter live model discovery to the chat models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fallback_models is empty here, so whatever fetch_models returns is exactly what the picker shows. The catalogue at {base_url}/models describes the whole account rather than one endpoint, so it also carries a reranker (/v1/rerank) and two audio models (/v1/audio/transcriptions, /v1/audio/speech). Selecting one of those would send a chat completion to a route that does not answer it. An OpenAI-shaped model object has no capability field, so the entry alone cannot say which route serves it. The architecture block does, and the test is text on BOTH sides: a chat model takes text in and emits text out. Checking the output side alone is not enough — speech-to-text is audio->text, so its output modality is ["text"] and it passes a one-sided test. Written as an allow-list, so a modality this profile has not been taught about drops out of the picker rather than appearing and failing on first use. Verified against the live catalogue: 8 entries in, the 5 chat models out. --- plugins/model-providers/infersia/__init__.py | 96 ++++++++- .../model_providers/test_infersia_profile.py | 187 ++++++++++++++++++ 2 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 tests/plugins/model_providers/test_infersia_profile.py diff --git a/plugins/model-providers/infersia/__init__.py b/plugins/model-providers/infersia/__init__.py index 6fb692ee4ecc4..c3c52601edd92 100644 --- a/plugins/model-providers/infersia/__init__.py +++ b/plugins/model-providers/infersia/__init__.py @@ -13,11 +13,103 @@ long-context work. """ +import json +import logging +import urllib.request + from providers import register_provider -from providers.base import ProviderProfile +from providers.base import ProviderProfile, _profile_user_agent + +logger = logging.getLogger(__name__) + + +def _is_chat_model(entry: dict) -> bool: + """Return True when *entry* is a model the chat_completions transport can use. + + An OpenAI-shaped ``/v1/models`` list describes an account's whole catalogue, + not one endpoint's worth of it, and the model object carries no capability + field — so an entry alone cannot say which route answers it. Infersia + publishes an ``architecture`` block for exactly this, and the test is + ``text`` on BOTH sides: a chat model takes text in and emits text out. + + Checking the output side alone is not sufficient. Speech-to-text is + ``audio->text``, so its output modality *is* ``["text"]`` and it passes a + one-sided test while answering ``/v1/audio/transcriptions`` rather than + ``/v1/chat/completions``. + + This is an allow-list, so a modality we have not accounted for is excluded + by default: a future non-text model drops out of the picker until this + profile is taught about it, instead of appearing and failing on first use. + + An entry with no ``architecture`` block is kept. Absence is no information + rather than evidence of a non-chat model, and ``fallback_models`` is empty + here, so failing closed on it would leave an empty picker with nothing to + explain it. + """ + architecture = entry.get("architecture") + if not isinstance(architecture, dict): + return True + + def _has_text(key: str) -> bool: + modalities = architecture.get(key) + if not isinstance(modalities, list): + return True + return any(str(m).strip().lower() == "text" for m in modalities) + + return _has_text("input_modalities") and _has_text("output_modalities") + + +class InfersiaProfile(ProviderProfile): + """Infersia — live catalogue, narrowed to the chat-capable models.""" + + def fetch_models( + self, + *, + api_key: str | None = None, + base_url: str | None = None, + timeout: float = 8.0, + ) -> list[str] | None: + """Fetch the live catalogue and keep only its chat models. + + Same request as the base implementation; it is repeated here rather + than delegated because the base returns bare ID strings, and the + capability signal being filtered on lives in the fields those strings + are read out of. + + ``fallback_models`` is empty, so whatever this returns *is* the picker. + """ + effective_base = base_url or self.base_url + url = (self.models_url or "").strip() + if not url: + if not effective_base: + return None + url = effective_base.rstrip("/") + "/models" + + from hermes_cli.urllib_security import open_credentialed_url + + req = urllib.request.Request(url) + if api_key: + req.add_header("Authorization", f"Bearer {api_key}") + req.add_header("Accept", "application/json") + req.add_header("User-Agent", _profile_user_agent()) + for k, v in self.default_headers.items(): + req.add_header(k, v) + + try: + with open_credentialed_url(req, timeout=timeout) as resp: + data = json.loads(resp.read().decode()) + items = data if isinstance(data, list) else data.get("data", []) + return [ + m["id"] + for m in items + if isinstance(m, dict) and "id" in m and _is_chat_model(m) + ] + except Exception as exc: + logger.debug("fetch_models(infersia): %s", exc) + return None -infersia = ProviderProfile( +infersia = InfersiaProfile( name="infersia", aliases=("infersia-ai",), display_name="Infersia", diff --git a/tests/plugins/model_providers/test_infersia_profile.py b/tests/plugins/model_providers/test_infersia_profile.py new file mode 100644 index 0000000000000..72b0f2cb0f687 --- /dev/null +++ b/tests/plugins/model_providers/test_infersia_profile.py @@ -0,0 +1,187 @@ +"""Unit tests for the Infersia provider profile's model discovery. + +``fallback_models`` is empty, so ``fetch_models`` *is* the picker. The +catalogue served at ``/v1/models`` describes the whole account, so it also +carries models answering ``/v1/rerank``, ``/v1/audio/transcriptions`` and +``/v1/audio/speech``. Only the chat models may reach the picker. + +The regression these tests exist for is the one-sided filter: speech-to-text +is ``audio->text``, so its ``output_modalities`` is ``["text"]`` and any test +written on the output side alone waves it through. +""" + +from __future__ import annotations + +import json +from http.server import BaseHTTPRequestHandler, HTTPServer +from threading import Thread +from typing import Any + +import pytest + +# Verbatim ``/v1/models`` entries, trimmed to the fields under test. Keeping +# the real shapes means a change to what the catalogue publishes shows up here +# as a failure rather than as a model appearing in someone's picker. +_CATALOGUE: list[dict[str, Any]] = [ + { + "id": "deepseek/deepseek-v4-flash-0731", + "architecture": { + "modality": "text->text", + "input_modalities": ["text"], + "output_modalities": ["text"], + }, + }, + { + "id": "hexgrad/kokoro-82m", + "architecture": { + "modality": "text->audio", + "input_modalities": ["text"], + "output_modalities": ["audio"], + }, + }, + { + "id": "openai/whisper-large-v3-turbo", + "architecture": { + "modality": "audio->text", + "input_modalities": ["audio"], + "output_modalities": ["text"], + }, + }, + { + "id": "zeroentropy/zerank-2", + "architecture": { + "modality": "text->embedding", + "input_modalities": ["text"], + "output_modalities": ["embedding"], + }, + }, + { + "id": "qwen/qwen3.6-35b-a3b", + "architecture": { + "modality": "text+image->text", + "input_modalities": ["text", "image"], + "output_modalities": ["text"], + }, + }, + { + "id": "qwen/qwen3-8b", + "architecture": { + "modality": "text->text", + "input_modalities": ["text"], + "output_modalities": ["text"], + }, + }, +] + + +class _CatalogueHandler(BaseHTTPRequestHandler): + """Serves ``/models`` with a configurable catalogue.""" + + models: list = _CATALOGUE + + def do_GET(self): + if self.path.rstrip("/") == "/models": + body = json.dumps({"data": type(self).models}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(body) + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + pass # suppress noise + + +def _serve(models): + _CatalogueHandler.models = models + server = HTTPServer(("127.0.0.1", 0), _CatalogueHandler) + Thread(target=server.serve_forever, daemon=True).start() + return server, server.server_address[1] + + +@pytest.fixture +def infersia_profile(): + """Resolve the registered Infersia profile via the provider registry. + + Going through ``get_provider_profile`` keeps the test honest: if the + registered object is ever swapped back for a plain ``ProviderProfile`` the + filter disappears and these assertions collapse. + """ + import model_tools # noqa: F401 (importing triggers plugin discovery) + import providers + + profile = providers.get_provider_profile("infersia") + assert profile is not None, "infersia provider profile must be registered" + return profile + + +def _fetch(profile, models): + server, port = _serve(models) + try: + return profile.fetch_models( + api_key="test-key", base_url=f"http://127.0.0.1:{port}" + ) + finally: + server.shutdown() + + +class TestInfersiaModelDiscovery: + def test_only_chat_models_reach_the_picker(self, infersia_profile): + assert _fetch(infersia_profile, _CATALOGUE) == [ + "deepseek/deepseek-v4-flash-0731", + "qwen/qwen3.6-35b-a3b", + "qwen/qwen3-8b", + ] + + def test_speech_to_text_is_excluded_despite_text_output(self, infersia_profile): + """The regression guard: ``audio->text`` outputs text but is not chat.""" + entry = [m for m in _CATALOGUE if m["id"] == "openai/whisper-large-v3-turbo"] + assert entry[0]["architecture"]["output_modalities"] == ["text"] + assert _fetch(infersia_profile, entry) == [] + + @pytest.mark.parametrize( + "model_id", + ["hexgrad/kokoro-82m", "zeroentropy/zerank-2"], + ) + def test_non_text_output_is_excluded(self, infersia_profile, model_id): + entry = [m for m in _CATALOGUE if m["id"] == model_id] + assert _fetch(infersia_profile, entry) == [] + + def test_unknown_modality_is_excluded_by_default(self, infersia_profile): + """The filter is an allow-list, so a modality it has never seen drops.""" + assert ( + _fetch( + infersia_profile, + [ + { + "id": "example/video-model", + "architecture": { + "input_modalities": ["video"], + "output_modalities": ["video"], + }, + } + ], + ) + == [] + ) + + def test_default_aux_model_survives_the_filter(self, infersia_profile): + """``default_aux_model`` is the one hardcoded id; it must still be live.""" + assert infersia_profile.default_aux_model in _fetch( + infersia_profile, _CATALOGUE + ) + + def test_entry_without_architecture_is_kept(self, infersia_profile): + """Absence is no information, and an empty picker explains nothing.""" + assert _fetch(infersia_profile, [{"id": "some/model"}]) == ["some/model"] + + def test_unreachable_endpoint_returns_none(self, infersia_profile): + """None means "no catalogue", which is distinct from "no chat models".""" + assert ( + infersia_profile.fetch_models( + api_key="test-key", base_url="http://127.0.0.1:1", timeout=1.0 + ) + is None + )