Models internals ¶
This page lists a few topics that detail some implementation choices made in the
BaseAIModel and its related classes.
See also
More on models:
More on messages:
Conversion Pipelines ¶
The workflow for any model provider (OpenAI, Anthropic, Google, ...) follows a pipeline with two operations:
- Convert the input (prompt, tools, etc.) to the provider's native format
- Convert the provider's response back to Conatus's standardized output.
As a diagram, it would look like this:
AIPrompt
|
| (convert_{messages, tools, etc.}_to_ai_model_format)
v
provider payload --> [call the API]
v
provider-native response
|
| (convert_provider_response_to_ai_response)
v
AIResponse
This includes:
- Detecting the provider and model variant (see modelβname retrieval logic)
- Building the provider-native payload (conversion functions for messages, tools, schemas, etc.)
- Executing the API call (with logging/printing)
- Converting the provider's response back into standardized
AIResponse
Each provider (OpenAI, Anthropic, Google) has a family of conversion functions, designed for clarity, composability, and easy streaming.
Message conversion classes ¶
OpenAI ¶
conatus.models.open_ai.cc_conversion.OpenAIChatCompletionConverters:- Converts prompts/messages/tools for OpenAI Chat Completions API
- Converts OpenAI chat replies and streaming chunks to
AIResponseandIncompleteAIResponse
conatus.models.open_ai.response_conversion.OpenAIResponseConverters:- Analogous conversion functions for Responses API
- There are few major differences with the Chat Completions API:
- Only API to offer reasoning chunks
- Only API to offer OpenAI-specific computer tool calls
from conatus.models.open_ai.cc_conversion import OpenAIChatCompletionConverters
# Example: Convert a Conatus Assistant message to OpenAI's format
# for the Chat Completions API
from conatus.models.inputs_outputs.messages import AssistantAIMessage
msg = AssistantAIMessage.from_text("Give me a Python example.")
openai_msg = OpenAIChatCompletionConverters.assistant_message_to_openai_assistant_message(msg)
assert openai_msg["role"] == "assistant"
Anthropic ¶
conatus.models.anthropic.conversion.AnthropicConverters:- Converts Conatus prompts/messages/tools into Anthropic's
BetaMessageParamand tool data - Converts Anthropic message blocks (text, reasoning, tool use, computer use) to Conatus "incomplete" and finalized forms
- Converts Conatus prompts/messages/tools into Anthropic's
from conatus.models.anthropic.conversion import AnthropicConverters
from conatus.models.inputs_outputs.messages import UserAIMessage
user_msg = UserAIMessage(content="Summarize this paragraph.")
anthropic_msg = AnthropicConverters.ai_message_to_anthropic_message(user_msg)
assert anthropic_msg["role"] == "user"
Google (Gemini) ¶
conatus.models.google.conversion.GoogleConverters:- Converts Conatus prompts/messages/tools to Google Gemini's payloads and function declarations
- Handles Google's
types.Content, streaming, and tool call round-trips
from conatus.models.google.conversion import GoogleConverters
from conatus.models.inputs_outputs.messages import AssistantAIMessage
msg = AssistantAIMessage.from_text("Explain the concept of entropy.")
g_content = GoogleConverters.assistant_ai_message_to_google_msg(msg)
assert g_content.role == "model"
Common structure ¶
Each provider has a family of conversion classes, but they share a similar structure.
- Each class bundles static functions for:
- Message conversion
- Tools conversion
- System message conversion
- Output schema conversion (for structured output)
- Streaming/incomplete chunks conversion
BaseAIModelcalls these as hooks via its core logic (convert_messages_to_ai_model_format,convert_tools_to_ai_model_format, etc.). Each provider is responsible for implementing these hooks.
The message conversion flow in BaseAIModel.prepare_call_args ¶
The method
(prepare_call_args) is at
the heart of every model implementation: all provider-specific payloads are
derived from this standardized step.
What it does ¶
- Merges configs: Combines
self.model_configwith any overrides passed asuser_provided_config - Sets fields: Handles computer use mode, previous messages IDs (for stateful APIs), etc.
- Calls hooks: Invokes the following overridable methods:
- Returns: An instance of
AIModelCallArgs(or a subclass, per model) with all relevant fields
This means that if you want to add a new provider, you only need to implement the conversion hooks. For more information on how do implement a new provider, see How-to: Add a new AI provider.
Example: How this works in-situ (runnable) ¶
import os
from conatus import AIPrompt
from conatus.models.open_ai import OpenAIModel
from conatus.models.base import BaseAIModel, ModelConfig
os.environ["DUMMY_API_KEY"] = "dummy"
class DummyModel(OpenAIModel): # (1)!
api_key_env_variable = "DUMMY_API_KEY"
provider = "dummy"
def default_client(self, model_config, api_key, **kwargs):
return None
def default_config(self):
return ModelConfig()
def convert_system_message_to_ai_model_format(self, system_message, config):
return f"SYSTEM: {system_message.content}"
def convert_messages_to_ai_model_format(self, prompt, config, only_new_messages=False):
return [f"{m.role.upper()}: {getattr(m, 'content', '')}" for m in prompt.messages]
def convert_tools_to_ai_model_format(self, prompt, config):
return []
def convert_output_schema_to_ai_model_format(self, prompt, config):
return super().convert_output_schema_to_ai_model_format(prompt, config)
prompt = AIPrompt(
user="What's 2 + 2",
system="You are a helpful assistant.",
output_schema=int,
)
m = DummyModel()
args = m.prepare_call_args(prompt)
assert args.system_message == "SYSTEM: You are a helpful assistant."
assert args.messages == ["USER: What's 2 + 2"]
assert args.output_schema["format"]["schema"]["properties"] == {
"item": {"type": "number"}
}
- We subclass
OpenAIModelto avoid having to implement the abstract methods ofBaseAIModel:call,acall,call_stream,acall_stream.
Pricing ¶
Conatus tracks per-model pricing rules in a registry. Its goal is to return
accurate cost estimates given
CompletionUsage; this
relies on knowing the pricing model and price per token for each model.
Because different models have different pricing models, we need to be able to look up the right pricing strategy for a given model.
Pricing strategies and the registry ¶
- The
PricingStrategyABCdefines each strategy as.cost(usage) -> float - There are strategies for:
FlatPrice(flat rate per token)CachedAwarePrice(track cache reads/writes)MultiCachedPrice(differentiated cache operations)TieredPrice(multi-tiered pricing with variable per-token cost)
- The global registry (
REGISTRY) is a singleton mappingmodel_nametoPricingStrategy. - The registry is populated at import time (from
conatus.models.prices).
Example:
from conatus.models.inputs_outputs.usage import CompletionUsage
from conatus.models.prices import calculate_cost
usage = CompletionUsage(model_name="openai:gpt-4o", prompt_tokens=2000, completion_tokens=500)
cost = calculate_cost(usage)
assert cost > 0
How cost lookups work ¶
calculate_costnormalizes amodel_name(applies mapping for "unclean" names), retrieves the proper pricing strategy, and invokes.cost(usage)- If the model is unknown or the cost can't be calculated, returns -1
Where to add/update prices ¶
- To change prices, update the relevant
REGISTRY.register()calls inconatus.models.prices - If introducing a new model/provider, register default prices before releasing to users.
This structure is subject to change
For now, we rely on a centralized registry, but we might move to a more decentralized structure in the future, where each provider can declare its names, pricing, etc. within their own package.
Model name retrieval and normalization logic ¶
Conatus provides helpers to resolve fuzzy or shorthand model names to canonical names. This allows users to type "gpt-4o", "claude-3-7" or "gemini-2.5-pro" and always get the right model class and settings.
Main utilities ¶
unclean_model_names_to_model_name:- Maps input strings (e.g.,
"o3-mini") to canonical form ("openai:o3-mini")
- Maps input strings (e.g.,
get_model_from_namereturns a fully initializedBaseAIModelgiven a fuzzy model name
from conatus.models.retrieval import get_model_from_name
model = get_model_from_name("gpt-4o")
assert model.provider == "openai"
get_model_from_providercan build a model from a provider name and model type (see below)
Model types ¶
For some providers like OpenAI, defining a single default model might not be adequate. OpenAI, for instance, have models that are optimized for different tasks (chat, code, etc.) and have different pricing.
To support this, we use the ModelType enum
to specify the type of model (e.g. chat, execution, reasoning, etc.). You
can use this enum to select the right model variant for your use case.
In turn, AI providers can subclass the
default_model_name
method to return the right model name for a given model type. See the OpenAI
model implementation for an
example.
Printing-mixin ¶
The AIModelPrintingMixin class
manages terminal output for streaming and non-streaming model calls.
There are three different modes:
"normal": Print progress indicator (indicates the number of chunks received)"preview": Print a real-time partial response, clearing+updating as new chunks are received (default for most models)"silent": No output to the terminal. This can be useful in CI environments.
Additional notes and tips ¶
- If you're adding a new provider, follow
add_ai_provider.md and base your design on
OpenAIModel,AnthropicAIModel, orGoogleAIModel. - Use the conversion classes as adapters; never mutate prompts in-place.
- Pricing always relies on accurate
model_nameinCompletionUsage; if your provider has variable pricing, prefer a customPricingStrategyclass. - Always be sure to close/init your client in
__del__to avoid resource leaks.
Reference: Key APIs ¶
BaseAIModelAIModelCallArgs- Conversion classes: OpenAIChatCompletionConverters, OpenAIResponseConverters, AnthropicConverters, GoogleConverters
PricingStrategyand pricing typesunclean_model_names_to_model_name,get_model_from_nameAIModelPrintingMixin