Connectors
The model interface, provider inference, and pricing.
agent_arena/connectors/ — the uniform model interface and the price book.
The contract
Every provider looks the same to the runner: hand it a GenerationRequest, get a
GenerationResult. Adding a provider means implementing one method.
class Connector(ABC):
provider: str
@abstractmethod
def generate(self, request: GenerationRequest) -> GenerationResult: ...
GenerationRequest carries messages, system, max_tokens, temperature,
provider-specific params, and metadata — bookkeeping such as the test id,
never sent to a real provider. Its prompt property flattens the message list
for providers and scorers that want one string.
GenerationResult carries the text plus what it cost to produce: token counts,
cache read/write tokens, latency_ms, finish_reason, and two fields worth
calling out:
cost_usd— what this call actually cost, when the connector knows better than the catalog. A pipeline target knows its real end-to-end spend across every internal call; the price book cannot. When set, the runner trusts it over the catalog.metrics— extra numbers the connector measured, weightable inmetrics.weightsby name exactly like a builtin.
The six connectors
| Provider | Class | Notes |
|---|---|---|
anthropic |
AnthropicConnector |
Streams above 16,000 output tokens to avoid an SDK HTTP timeout. Drops temperature for model families that reject it, since sending it turns a working config into a 400 |
openai |
OpenAIConnector |
Configurable token parameter (max_completion_tokens by default) and a send_temperature switch for models that refuse it |
gemini |
GeminiConnector |
Maps assistant to model roles |
litellm |
LiteLLMConnector |
The escape hatch — anything LiteLLM can reach, via bedrock/…, together_ai/…, azure/… |
local |
LocalConnector |
Ollama, LM Studio, vLLM, llama.cpp — any OpenAI-compatible endpoint. Built on urllib, so evaluating a model on your own laptop needs no SDK at all |
mock |
MockConnector |
Deterministic, offline, with fixed accuracy/latency/price |
callable |
CallableConnector |
A run: target — an arbitrary Python callable on the leaderboard |
Lazy imports
Every SDK is imported inside the method that needs it, through _require:
def _require(module: str, extra: str):
try:
return __import__(module)
except ImportError as exc:
raise ConnectorError(
f"the {extra!r} provider needs the {module!r} package: "
f"pip install 'agent-arena[{extra}]'"
) from exc
Two consequences: pip install agent-arena pulls in no vendor client, and the
error you get for a missing one names the exact command to fix it. CI installs no
provider SDK on purpose — if the suite ever needs anthropic present to pass,
invariant 1 has been broken.
Provider inference
registry.infer_provider resolves a bare model id, so
models: [claude-opus-5, gpt-4o, gemini-2.5-flash] works with no provider:
field. Resolution order:
- An explicit
provider:on the model entry - The id's prefix —
claude-*→ anthropic,gpt-*/o3-*→ openai,gemini-*→ gemini,mock:*→ mock,ollama//vllm//barellama,qwen,mistral… → local - A
vendor/modelid → LiteLLM - An explicit
api_basewith an unrecognisable id → local, because giving a URL is itself the answer: it is an OpenAI-compatible server and the model name can be anything
Unresolvable ids raise ConnectorError showing the YAML that would fix it.
The price book
connectors/pricing.py plus model_cards.json. A card carries input/output
price per million tokens, context window, feature flags and privacy properties.
Three rules govern it:
- Sourced or absent. A model whose current list price cannot be sourced is
left out.
has_pricingis then false and the model gets no cost metric — which, because cost is nulled for the whole run unless every call is priced, removes the cost axis for everyone. That is the honest behaviour: a guessed price silently corrupts a purchasing decision. privacyis deliberately empty for real models. Whether a DPA is in place or zero-data-retention is enabled is a property of your contract, not of the model. Declare it in your own pricing file.as_ofis recorded. Prices move. The catalog states when it was checked.
Override it per project with pricing.path (a file) or per model with a card:
block. Those beat the shipped catalog, which is how you price a model the arena
does not know or a negotiated rate that differs from list.
Provider profiles (v2)
providers: in config.yaml declares named connection profiles, so two API keys
for the same vendor can compete in one run, and a gateway can carry custom
headers, a private CA and a model-prefix rewrite. ProjectConfig.provider_for()
resolves a model to its profile.
build_connector(spec, defaults, profile=...) applies it: the profile supplies
the endpoint, the credential (resolved from its reference, so no caller holds
the raw value), extra headers, the TLS context, a proxy, a timeout, and a
model-id prefix rewrite. The model entry still wins where it is explicit — a
profile is a default for the connection, not an override of a deliberate choice.
Per-provider rate limits parse but are not yet enforced. See ../guides/api-keys-and-gateways.md.
Source: docs/architecture/connectors.md