VisvoAI Docs
visvoai-ai

Providers & the model registry

One build_chat_model() call reaches Gemini, Claude, GPT and any OpenAI-compatible endpoint — plus a registry of pricing, context windows and capabilities.

Supported providers & models

visvoai-ai is a provider-agnostic AI agent framework layer for Python: one build_chat_model() call reaches Google Gemini, Anthropic Claude, OpenAI GPT, and every OpenAI-compatible inference endpoint — the same function-/tool-calling interface regardless of vendor. It sits underneath visvoai-core's LangGraph agent loop, but it's equally usable standalone in any LangChain-based pipeline, a FastAPI service, or a one-off script — it returns a plain langchain_core.language_models.BaseChatModel.

FamilyHow it's reachedStreamingTool/function callingNative grounded searchThinking / reasoning
Google GeminiGeminiProvider -> langchain-google-genaiyesyesyes - Google Search groundingthinking_budget
Anthropic ClaudeAnthropicProvider -> langchain-anthropicyesyesno (raises NotSupported)extended-thinking token budget
OpenAI GPTOpenAICompatProvider("openai") -> langchain-openaiyesyesnoreasoning_effort
Together AI, Groq, OpenRouterOpenAICompatProvider(name), built-in base_urlyesyesnoextra_body.reasoning (captured non-standard delta.reasoning)
Any OpenAI-compatible endpoint - vLLM, LM Studio, llama.cpp servers, self-hosted, or a private gatewayOpenAICompatProvider(name) + your own base_urlyesyesnoprovider-dependent

This table is the real Provider surface in visvoai-ai/src/visvoai/ai/providers/ - not a marketing claim. Every row maps to an actual class: gemini.py, anthropic.py, openai_compat.py, resolved by factory.py's get_provider() / get_provider_for_model().

Why this matters for building an AI agent: LangChain and LangGraph give you the graph and the message primitives, but choosing and paying for a model is left as an exercise - every team ends up hardcoding a provider switch statement and a spreadsheet of per-million-token prices that drifts out of date. visvoai-ai treats "which model, at what cost, with how much reasoning" as first-class, registry-driven data — see the models.dev catalog described below — instead of constants sprinkled through application code.

from visvoai.ai import build_chat_model

gemini   = build_chat_model("gemini:gemini-2.5-flash")
claude   = build_chat_model("anthropic:claude-sonnet-4-5", level="high")
gpt      = build_chat_model("openai:gpt-4o-mini")
groq     = build_chat_model("groq:llama-3.3-70b-versatile")
together = build_chat_model("together:llama-3.3-70b")

Function calling (tool use) works identically across every row in the table above - bind tools with .bind_tools([...]) on the returned model exactly as LangChain documents it, and visvoai-core does this for you automatically inside the agent loop. If you're coming from a raw google-genai, anthropic, or openai SDK integration, or from LangChain's own per-provider Chat* classes, see Migrating from LangChain for the direct translation.

The one entry point: build_chat_model

from visvoai.ai import build_chat_model

model = build_chat_model(
    deployment_id: str,
    *,
    level: str | None = None,
    thinking_raw: dict | None = None,
    api_key: str | None = None,
    base_url: str | None = None,
    codec: IdentityCodec = DEFAULT_CODEC,
) -> BaseChatModel

This is the whole surface (visvoai-ai/src/visvoai/ai/resolve.py). It:

  1. Parses deployment_id with the codec (raises on a malformed id).
  2. Looks up the Deployment in the registry — raises ValueError if unknown.
  3. Resolves the thinking level. Precedence is explicit level= arg → the id's @effort suffix → the deployment's own default → offthinking_raw= bypasses all of this and passes your dict straight through as provider kwargs, for callers who want to skip the enum entirely.
  4. Resolves the API key: explicit api_key= → the deployment's carried key_env (for catalog-sourced deployments — see below) → the static per-provider environment variable.
  5. Dispatches to the right Provider facade's .build().

The return value is a genuine LangChain BaseChatModel.stream(), .ainvoke(), .with_structured_output(Schema), and anything else the LangChain/LangGraph ecosystem expects work unchanged.

model = build_chat_model("gemini:gemini-2.5-flash")
model = build_chat_model("gemini:gemini-2.5-flash", level="high")
model = build_chat_model("anthropic:claude-sonnet-4-5", level="medium")
model = build_chat_model("together:llama-3.3-70b")

Deployment identity: provider:model[@effort]

A Model is a provider-agnostic identity (llama-3.3-70b). A Deployment is that model served by one specific provider — the callable, billable unit (together:llama-3.3-70b, openrouter:llama-3.3-70b). One model can have several deployments; each is priced and resolved independently.

The string form is produced/parsed by an IdentityCodec (visvoai/ai/identity.py). The default, ColonAtCodec, implements:

"<provider>:<model>[@<effort>]"
gemini:gemini-3-flash@medium
together:llama-3.3-70b
openrouter:deepseek-r1@high

It splits on the first : and first @, so model slugs containing / (OpenRouter's vendor/model convention) pass through untouched. DeploymentId is a NamedTuple(provider, model, effort). The codec is a Protocol — a consumer that needs an opaque/stable id scheme (e.g. for a datastore) can implement its own and pass it as codec= everywhere a deployment id is parsed. Contract: parse(build(p, m, e)) == DeploymentId(p, m, e), and a single codec must own a given storage surface — don't mix codecs against one store.

The registry: list_deployments, get_deployment_info

from visvoai.ai import list_deployments, get_deployment_info, Capability

for d in list_deployments(Capability.CHAT):
    print(d.id, d.display_name, d.provider, d.input_cost_per_million,
          d.output_cost_per_million, d.context_window,
          d.supports_thinking, [l.value for l in d.thinking_levels],
          d.default_thinking)

info = get_deployment_info("gemini:gemini-3-flash-preview")

list_deployments and get_deployment_info return read-only DeploymentInfo dataclasses — the only model-data type consumers should touch (visvoai/ai/deployments.py):

@dataclass(frozen=True)
class DeploymentInfo:
    id: str
    model: str
    display_name: str
    provider: str
    family: str
    capabilities: List[Capability]
    reasoning: bool
    input_cost_per_million: float
    output_cost_per_million: float
    context_window: int                    # 0 = unknown
    supports_thinking: bool
    thinking_levels: List[ThinkingLevel]   # [] when unsupported
    default_thinking: ThinkingLevel

Both listing and lookup only ever surface deployments that are enabled and not deprecated — the registry keeps deprecated/disabled entries around internally (for historical cost lookups on old calls) without ever offering them for new use.

default_deployment(capability=Capability.CHAT, provider=None) returns the composite id of the default deployment for a capability, optionally scoped to one provider. Precedence:

  1. a consumer override set with set_default_deployment()
  2. a curated DEFAULT_MODEL_FOR pick
  3. the registry's single default=True deployment
  4. the first enabled one

Which model is default is your decision, not this package's. The package ships a fallback so it works out of the box; override it once at startup:

from visvoai.ai import set_default_deployment, Capability

set_default_deployment(Capability.CHAT, "gemini:gemini-3.7-flash")

Validated when set — an unknown id, or one that does not declare the capability, raises immediately rather than surfacing later as a surprising model choice. Pass None to clear it, and get_default_overrides() to inspect what is set.

Set it after install_catalog(): that rebuilds the registry, and the override is validated against whatever is installed at the time.

A provider filter ignores an override belonging to another provider — asking for the default Anthropic chat model will not return a Gemini one.

Capabilities

Capability is a string enum used for routing/validation, not pricing:

class Capability(str, Enum):
    CHAT = "chat"
    SEARCH = "search"
    IMAGE_GEN = "image_gen"
    AUDIO_GEN = "audio_gen"
    EMBEDDING = "embedding"

IMAGE_GEN / AUDIO_GEN / EMBEDDING models are defined in the registry for inventory and cost lookup, but their tools currently call the underlying SDK directly rather than through a facade method — routing those through Provider capability methods is a deferred pass, per the registry's own module docstring.

The Provider facade

Every provider family implements one class (visvoai/ai/providers/base.py):

class Provider(ABC):
    def build(self, slug: str, api_key=None, base_url=None, **extra) -> BaseChatModel: ...
    def normalize_content(self, chunk) -> Generator[Dict[str, Any], None, None]: ...
    def search(self, query, *, slug, api_key=None, system=None) -> "SearchResult": ...
    def fetch_url(self, url, *, slug, api_key=None) -> str: ...

Every method is optional — the default implementation raises NotSupported (a NotImplementedError subclass), so a provider implements only what it actually has. normalize_content defaults to default_content_events, which understands the langchain-core list-of-content-blocks shape shared by Gemini and Anthropic — it yields {"type": "text"|"thinking"|"thinking_redacted"|"compaction", "content": str} events from a streamed chunk.

Three facades ship in the box:

  • GeminiProviderlangchain_google_genai.ChatGoogleGenerativeAI, temperature=1.0, streaming=True. Also implements search() (Google Search grounding via the google-genai SDK directly, since grounding metadata is cleanest there) and fetch_url() (Gemini URL Context — the page is fetched server-side by Google, not on your machine).
  • AnthropicProviderlangchain_anthropic.ChatAnthropic, temperature=1.0, max_tokens=16000, streaming=True.
  • OpenAICompatProvider(provider_name) — any OpenAI Chat-Completions- shaped endpoint. See below.

OpenAI-compatible providers

OpenAICompatProvider is constructed with a provider name string ("together", "groq", "openrouter", "openai", or anything else) and resolves its base URL via resolve_base_url() (visvoai/ai/providers/config.py):

_PROVIDER_BASE_URL = {
    "together":   "https://api.together.xyz/v1",
    "groq":       "https://api.groq.com/openai/v1",
    "openrouter": "https://openrouter.ai/api/v1",
}

"openai" itself has no entry — it rides langchain-openai's library default. Any other provider name must resolve a base_url (explicit arg, the static map above, or a carried base_url from a catalog-sourced deployment) or .build() raises ValueError loudly rather than silently hitting OpenAI's real API with a foreign model id.

Two more things OpenAICompatProvider does that matter in practice:

  • Pins Chat Completions, not Responses. For any non-"openai" provider it sets use_responses_api=False. langchain-openai auto-switches to the /responses endpoint the instant a reasoning dict (or other Responses-only arg) appears in the payload — which Together/OpenRouter either reject with a 400 or accept and return block-list content that corrupts the next turn. Reasoning is threaded through extra_body instead (see thinking levels).
  • ReasoningChatOpenAI — a memoized ChatOpenAI subclass that captures the non-standard delta.reasoning field Together/DeepSeek/ GLM/Qwen stream in Chat Completions responses (langchain-openai drops it). It lands in message.additional_kwargs["reasoning"], and OpenAICompatProvider.normalize_content surfaces it as a "thinking" event — uniform with Gemini/Claude's native thinking blocks.
from visvoai.ai.providers.openai_compat import OpenAICompatProvider

model = OpenAICompatProvider("groq").build(
    slug="llama-3.3-70b-versatile",
    api_key=os.environ["GROQ_API_KEY"],
    base_url="https://api.groq.com/openai/v1",   # optional — groq is a built-in default
)

Or, for anything registered in the model registry, go through build_chat_model and skip constructing the provider yourself:

model = build_chat_model("together:llama-3.3-70b")

Resolving a provider by name or by model id

visvoai/ai/providers/factory.py exposes two key-less resolvers (they pick the facade class; they never touch API keys):

from visvoai.ai import get_provider, get_provider_for_model

get_provider("gemini")           # → GeminiProvider() (bespoke families are singletons)
get_provider("together")         # → OpenAICompatProvider("together") (everything else)
get_provider_for_model("llama-3.3-70b", capability=Capability.CHAT)
# registry-driven: looks up the model, validates the capability, returns its facade.
# Raises ValueError if the model id is unregistered or lacks the capability.

Writing your own provider

Subclass Provider and implement only what you have — build() is the only method the agent loop strictly needs:

from visvoai.ai.providers.base import Provider, NotSupported

class EchoProvider(Provider):
    def build(self, slug, api_key=None, base_url=None, **extra):
        from langchain_core.language_models.fake_chat_models import FakeListChatModel
        return FakeListChatModel(responses=[f"[{slug}] echo: hello"])

model = EchoProvider().build("demo-1")
model.invoke("hi").content   # "[demo-1] echo: hello"

try:
    EchoProvider().search("anything", slug="demo-1")
except NotSupported:
    print("unimplemented capabilities raise NotSupported — consumers can probe")

(Full runnable version: visvoai-ai/examples/03_openai_compatible_and_custom.py.)

The models.dev catalog — new models without a package upgrade

The baked MODELS list in model_registry.py is a curated, hand-verified set (pricing comments cite ai.google.dev/gemini-api/docs/pricing, last verified per the module docstring). It's always present and works offline. On top of it, visvoai-ai ships a catalog engine (visvoai/ai/catalog/) that can merge in the live models.dev catalog for providers you hold keys for.

from visvoai.ai.catalog import BakedSource, RemoteModelsDevSource, build_catalog
from visvoai.ai import install_catalog

catalog = build_catalog([
    BakedSource(),                                    # the in-package floor
    RemoteModelsDevSource(cache_path="~/.myapp/models_dev_cache.json"),
])
install_catalog(catalog)   # swaps the module-level default registry

How it works:

  • CatalogSource is an ABC with one method, models() -> list[ModelDefinition]. BakedSource wraps the registry's own MODELS.
  • build_catalog(sources, gate=None) merges sources wholesale by (provider, api_id) — later sources in the list win entirely for a matching key, and contribute new keys otherwise. There is no per-field merge. An optional gate predicate drops models whose provider isn't actually callable (logged, not raised). Models whose id can't round-trip through the identity codec (e.g. Cloudflare's @cf/… slugs, which start with the codec's @-effort marker) are dropped automatically — admitting them would list fine and then crash on get_deployment. The result is validated (validate(): no duplicate keys, no negative costs/context) and returned as a plain list[ModelDefinition] — the exact type DeploymentRegistry already consumes, so it's a drop-in replacement for the static list.
  • RemoteModelsDevSource (catalog/sources/remote.py) is the opt-in network source: fetches models.dev/api.json once, caches it at a path you own, and serves from cache within a TTL (default 24h). It degrades rather than raising: fresh cache → use it; fetch fails → fall back to a stale cache → fall back to a bundled offline snapshot (catalog/data/modelsdev_snapshot.json.gz) → empty (the baked floor still applies). Stdlib-only (urllib), so visvoai-ai stays dependency-light even with this source available; the fetch function is injectable (fetcher=) for tests.
  • ModelsDevSource / to_definitions() (catalog/sources/modelsdev.py) adapt models.dev's per-provider JSON into ModelDefinitions. Admission is by callability, not by SDK branding: Gemini/Anthropic are always skipped here (they come from the baked source, via their bespoke facades); every other provider is admitted if models.dev exposes a Chat Completions api URL, or the provider is in a small BRANDED_BASE_URL map for providers models.dev knows are OpenAI-compatible but omits the endpoint for (Together, Groq, Mistral, xAI, DeepInfra, Cerebras, Perplexity). Non-text-in/text-out models (image/audio-only) are filtered out — they aren't chat deployments.

A catalog-sourced ModelDefinition carries its own base_url and key_env (since its provider isn't in the static providers/config.py maps) — resolve.py uses these automatically when present, so build_chat_model works identically whether a deployment came from the baked list or from models.dev.

Where things live

ConcernModule
build_chat_model, cost_ofvisvoai/ai/resolve.py
Deployment id codecvisvoai/ai/identity.py
Model / Deployment / DeploymentInfo / DeploymentRegistryvisvoai/ai/deployments.py
Raw rate-card data (ModelDefinition, MODELS, Capability)visvoai/ai/model_registry.py
Provider base class + NotSupportedvisvoai/ai/providers/base.py
GeminiProvidervisvoai/ai/providers/gemini.py
AnthropicProvidervisvoai/ai/providers/anthropic.py
OpenAICompatProvider, ReasoningChatOpenAIvisvoai/ai/providers/openai_compat.py
API key / base URL resolutionvisvoai/ai/providers/config.py
get_provider, get_provider_for_modelvisvoai/ai/providers/factory.py
Catalog engine (build_catalog, BakedSource)visvoai/ai/catalog/engine.py
models.dev adaptervisvoai/ai/catalog/sources/modelsdev.py
Cached remote models.dev sourcevisvoai/ai/catalog/sources/remote.py

Next: Cost, usage & thinking levels.

On this page