Hopper
A unified Python library for AI model API calls.
Named after Grace Hopper — the original abstraction layer between human intent and machine execution.
Supported providers
Anthropic, OpenAI, Google Gemini, Together AI, Perplexity, xAI Grok, Kimi (Moonshot AI), Z.AI (GLM), Fugu (Sakana AI), OpenRouter, Meta (Muse Spark), Tinker (Thinking Machines), Qwen (Alibaba DashScope).
Installation
pip install medicalsphere-hopper
Install only the provider SDKs you need:
pip install "medicalsphere-hopper[anthropic]" # Anthropic
pip install "medicalsphere-hopper[openai]" # OpenAI, Perplexity, Grok, Kimi, Z.AI, Fugu
pip install "medicalsphere-hopper[openrouter]" # OpenRouter
pip install "medicalsphere-hopper[meta]" # Meta (Muse Spark)
pip install "medicalsphere-hopper[tinker]" # Tinker (Thinking Machines)
pip install "medicalsphere-hopper[qwen]" # Qwen (Alibaba DashScope)
pip install "medicalsphere-hopper[google]" # Google Gemini
pip install "medicalsphere-hopper[together]" # Together AI
pip install "medicalsphere-hopper[anthropic,openai,google,together]" # all
For local development:
git clone <repo>
cd hopper
uv sync --all-extras
Usage
import asyncio
import hopper
from hopper import CanonicalRequest, CanonicalMessage, Credentials
request = CanonicalRequest(
model="claude-sonnet", # model ID or alias
messages=[CanonicalMessage(role="user", content="Hello!")],
system="You are a helpful assistant.",
)
credentials = Credentials(api_key="sk-ant-...")
# single response
envelope = asyncio.run(hopper.complete(request, credentials))
print(envelope.response.content)
# streaming
async def stream():
async for chunk in hopper.stream(request, credentials):
print(chunk.delta, end="", flush=True)
asyncio.run(stream())
Image input
from hopper import ImagePart, TextPart
request = CanonicalRequest(
model="claude-sonnet",
messages=[
CanonicalMessage(
role="user",
content=[
ImagePart(data="<base64>", media_type="image/jpeg"),
TextPart(text="What is in this image?"),
],
)
],
)
Multi-turn conversations
messages = [
CanonicalMessage(role="user", content="My name is Alice."),
CanonicalMessage(role="assistant", content="Got it, Alice!"),
CanonicalMessage(role="user", content="What's my name?"),
]
request = CanonicalRequest(model="claude-sonnet", messages=messages)
Model aliases
Every model has short aliases so you don't need to remember full IDs:
"claude-sonnet" → claude-sonnet-4-6
"claude-haiku" → claude-haiku-4-5-20251001
"gemini-3-flash" → gemini-3-flash-preview
"gpt-5.4-mini" → gpt-5.4-mini-2026-03-17
"grok" → grok-4.20
"sonar" → perplexity/sonar
"kimi" → kimi-k2.6
"glm" → glm-5.2
"zai" → glm-5.2
"fugu" → fugu (model ID, no alias needed)
"fugu-ultra" → fugu-ultra
"fusion" → openrouter/fusion
"muse-spark" → muse-spark-1.1
"inkling" → thinkingmachines/Inkling
"inkling-256k" → thinkingmachines/Inkling:peft:262144
"qwen" → qwen3.8-max
Calling models not in the registry
Hopper ships with a curated model registry, but providers release new models
frequently. You can call any model from a supported provider without waiting
for the registry to be updated — just pass provider=:
request = CanonicalRequest(
model="claude-sonnet-5-new", # not in the registry yet
provider="anthropic", # tells Hopper which adapter to use
messages=[CanonicalMessage(role="user", content="Hello!")],
)
Use extra_params to pass any parameters alongside it:
request = CanonicalRequest(
model="claude-sonnet-5-new",
provider="anthropic",
messages=[...],
extra_params={"temperature": 0.7, "top_p": 0.9},
)
extra_params works for registered models too — anything in there is forwarded
to the provider API without filtering.
Tool calling
Supported providers: anthropic, openai. Passing tools to any other
provider raises a ValueError — tool requests are never silently dropped.
from hopper import CanonicalMessage, CanonicalRequest, ToolDefinition, ToolResultPart, ToolUsePart
weather = ToolDefinition(
name="get_weather",
description="Get the current weather for a city.",
input_schema={
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
)
request = CanonicalRequest(
model="claude-sonnet",
messages=[CanonicalMessage(role="user", content="Weather in Paris?")],
tools=[weather],
)
envelope = await hopper.complete(request, creds)
if envelope.response.finish_reason == "tool_use":
call = envelope.response.tool_calls[0] # ToolCall(id, name, arguments)
result = my_get_weather(**call.arguments)
# Feed the result back on the next turn:
request.messages += [
CanonicalMessage(role="assistant", content=[
ToolUsePart(id=call.id, name=call.name, input=call.arguments),
]),
CanonicalMessage(role="user", content=[
ToolResultPart(tool_use_id=call.id, content=result),
]),
]
envelope = await hopper.complete(request, creds)
finish_reason is normalized to "tool_use" whenever tool_calls is
non-empty, across providers. When streaming, tool calls arrive on the final
StreamChunk.
MCP servers
Hopper normalizes remote MCP server configuration across providers, including auth headers the providers' native connectors don't all support:
from hopper import MCPServer
request = CanonicalRequest(
model="claude-sonnet",
messages=[CanonicalMessage(role="user", content="Search for papers on X.")],
mcp_servers=[MCPServer(
name="paperclip",
url="https://paperclip.gxl.ai/mcp",
headers={"X-API-Key": "gxl_..."}, # any headers — not just Bearer
)],
)
envelope = await hopper.complete(request, creds)
print(envelope.response.content) # final answer; MCP calls already executed
How each provider handles it:
- openai — always native: the Responses API MCP tool accepts arbitrary headers, so the server runs entirely on OpenAI's side.
- anthropic — native connector (
mcp-client-2025-11-20beta) when every server's auth fitsAuthorization: Bearer ...(or no auth); otherwise Hopper runs a client-side tool loop: it connects to the MCP server itself, exposes its tools to the model, executes the calls, and iterates until the model finishes. Theparam_resolution_logrecords which path was taken. Usage across loop iterations is summed intoenvelope.usage.
Anthropic beta flags can also be passed directly via
CanonicalRequest(betas=[...]), which routes the call through
client.beta.messages.*.
Current limitations: mcp_servers is not supported with stream() on the
anthropic adapter, and caller-defined tools can't be combined with
loop-mode (non-Bearer) MCP servers on anthropic yet — both raise a clear
ValueError.
Smoke tests
Hopper never reads API keys from the environment — credentials are always passed explicitly by the caller. This keeps secret management entirely outside the library.
The smoke test is the one exception: it's a developer tool for verifying real API connectivity, so it reads keys from a local .env file that is never committed.
Setup:
cp .env.example .env
# fill in keys for the providers you want to test:
# ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, TOGETHER_API_KEY,
# PERPLEXITY_API_KEY, XAI_API_KEY, MOONSHOT_API_KEY, ZAI_API_KEY, FUGU_API_KEY,
# OPENROUTER_API_KEY, META_API_KEY, TINKER_API_KEY, DASHSCOPE_API_KEY
Providers without a key are skipped automatically.
Run:
uv run python tests/smoke_test.py # all sections (basic + image + multi-turn)
uv run python tests/smoke_test.py --stream # streaming mode
uv run python tests/smoke_test.py --no-image # skip image tests
uv run python tests/smoke_test.py --no-multi # skip multi-turn tests
The image test uses tests/assets/image_example.jpeg and verifies that models can count the five asterisk markers in the image.
Unit tests
uv run pytest
Live tests
The pytest live suite makes real API calls for every provider across four
scenarios: single-turn completion, streaming, multi-turn conversation, and
image input (skipped for models without vision support). Like the smoke
test, it reads keys from .env; providers without a key are skipped.
uv run pytest --live -m live -v # all providers
uv run pytest --live -m live -v -k meta # one provider
Adding a provider
- Add
hopper/models/<provider>.yaml - Add
hopper/adapters/<provider>.pyexposing anADAPTERinstance
The router picks them up automatically — no other files need to change.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file medicalsphere_hopper-0.10.0.tar.gz.
File metadata
- Download URL: medicalsphere_hopper-0.10.0.tar.gz
- Upload date:
- Size: 275.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be9ac946cea50b7ec3e790434f50ba1db85a23965da64abd0e2e21215a46b897
|
|
| MD5 |
83e1fdf5914a21ad46122b6f0d2c0ef6
|
|
| BLAKE2b-256 |
a2a9724b33f02bcf75b8d96c3b249e22b22fd168e47cde8f26dc5c5b2bad09a8
|
File details
Details for the file medicalsphere_hopper-0.10.0-py3-none-any.whl.
File metadata
- Download URL: medicalsphere_hopper-0.10.0-py3-none-any.whl
- Upload date:
- Size: 54.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a195ddbb2b13b82b1399c9e59ed8f1781e254a6e9af063e17b62d730b660f239
|
|
| MD5 |
fadb394af67b30b63c80f9577b7faee3
|
|
| BLAKE2b-256 |
902e0fbfa6894626d67ad1ca159bd6dab9bd4dee5daa74b5b901250813d46d99
|