Skip to main content

Hopper

PyPI Python

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-20 beta) when every server's auth fits Authorization: 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. The param_resolution_log records which path was taken. Usage across loop iterations is summed into envelope.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

  1. Add hopper/models/<provider>.yaml
  2. Add hopper/adapters/<provider>.py exposing an ADAPTER instance

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

medicalsphere_hopper-0.10.0.tar.gz (275.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

medicalsphere_hopper-0.10.0-py3-none-any.whl (54.6 kB view details)

Uploaded Python 3

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

Hashes for medicalsphere_hopper-0.10.0.tar.gz
Algorithm Hash digest
SHA256 be9ac946cea50b7ec3e790434f50ba1db85a23965da64abd0e2e21215a46b897
MD5 83e1fdf5914a21ad46122b6f0d2c0ef6
BLAKE2b-256 a2a9724b33f02bcf75b8d96c3b249e22b22fd168e47cde8f26dc5c5b2bad09a8

See more details on using hashes here.

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

Hashes for medicalsphere_hopper-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a195ddbb2b13b82b1399c9e59ed8f1781e254a6e9af063e17b62d730b660f239
MD5 fadb394af67b30b63c80f9577b7faee3
BLAKE2b-256 902e0fbfa6894626d67ad1ca159bd6dab9bd4dee5daa74b5b901250813d46d99

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page