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.
| Family | How it's reached | Streaming | Tool/function calling | Native grounded search | Thinking / reasoning |
|---|---|---|---|---|---|
| Google Gemini | GeminiProvider -> langchain-google-genai | yes | yes | yes - Google Search grounding | thinking_budget |
| Anthropic Claude | AnthropicProvider -> langchain-anthropic | yes | yes | no (raises NotSupported) | extended-thinking token budget |
| OpenAI GPT | OpenAICompatProvider("openai") -> langchain-openai | yes | yes | no | reasoning_effort |
| Together AI, Groq, OpenRouter | OpenAICompatProvider(name), built-in base_url | yes | yes | no | extra_body.reasoning (captured non-standard delta.reasoning) |
| Any OpenAI-compatible endpoint - vLLM, LM Studio, llama.cpp servers, self-hosted, or a private gateway | OpenAICompatProvider(name) + your own base_url | yes | yes | no | provider-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,
) -> BaseChatModelThis is the whole surface (visvoai-ai/src/visvoai/ai/resolve.py).
It:
- Parses
deployment_idwith the codec (raises on a malformed id). - Looks up the
Deploymentin the registry — raisesValueErrorif unknown. - Resolves the thinking level. Precedence is explicit
level=arg → the id's@effortsuffix → the deployment's own default → off —thinking_raw=bypasses all of this and passes your dict straight through as provider kwargs, for callers who want to skip the enum entirely. - Resolves the API key: explicit
api_key=→ the deployment's carriedkey_env(for catalog-sourced deployments — see below) → the static per-provider environment variable. - Dispatches to the right
Providerfacade'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@highIt 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: ThinkingLevelBoth 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:
- a consumer override set with
set_default_deployment() - a curated
DEFAULT_MODEL_FORpick - the registry's single
default=Truedeployment - 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:
GeminiProvider—langchain_google_genai.ChatGoogleGenerativeAI,temperature=1.0,streaming=True. Also implementssearch()(Google Search grounding via thegoogle-genaiSDK directly, since grounding metadata is cleanest there) andfetch_url()(Gemini URL Context — the page is fetched server-side by Google, not on your machine).AnthropicProvider—langchain_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 setsuse_responses_api=False. langchain-openai auto-switches to the/responsesendpoint the instant areasoningdict (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 throughextra_bodyinstead (see thinking levels). ReasoningChatOpenAI— a memoizedChatOpenAIsubclass that captures the non-standarddelta.reasoningfield Together/DeepSeek/ GLM/Qwen stream in Chat Completions responses (langchain-openai drops it). It lands inmessage.additional_kwargs["reasoning"], andOpenAICompatProvider.normalize_contentsurfaces 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 registryHow it works:
CatalogSourceis an ABC with one method,models() -> list[ModelDefinition].BakedSourcewraps the registry's ownMODELS.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 optionalgatepredicate 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 onget_deployment. The result is validated (validate(): no duplicate keys, no negative costs/context) and returned as a plainlist[ModelDefinition]— the exact typeDeploymentRegistryalready consumes, so it's a drop-in replacement for the static list.RemoteModelsDevSource(catalog/sources/remote.py) is the opt-in network source: fetchesmodels.dev/api.jsononce, 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), sovisvoai-aistays 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 intoModelDefinitions. 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 CompletionsapiURL, or the provider is in a smallBRANDED_BASE_URLmap 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
| Concern | Module |
|---|---|
build_chat_model, cost_of | visvoai/ai/resolve.py |
| Deployment id codec | visvoai/ai/identity.py |
Model / Deployment / DeploymentInfo / DeploymentRegistry | visvoai/ai/deployments.py |
Raw rate-card data (ModelDefinition, MODELS, Capability) | visvoai/ai/model_registry.py |
Provider base class + NotSupported | visvoai/ai/providers/base.py |
GeminiProvider | visvoai/ai/providers/gemini.py |
AnthropicProvider | visvoai/ai/providers/anthropic.py |
OpenAICompatProvider, ReasoningChatOpenAI | visvoai/ai/providers/openai_compat.py |
| API key / base URL resolution | visvoai/ai/providers/config.py |
get_provider, get_provider_for_model | visvoai/ai/providers/factory.py |
Catalog engine (build_catalog, BakedSource) | visvoai/ai/catalog/engine.py |
| models.dev adapter | visvoai/ai/catalog/sources/modelsdev.py |
| Cached remote models.dev source | visvoai/ai/catalog/sources/remote.py |