Skip to main content

KeyCall

One consistent interface for validating API keys across AI and service providers, listing and filtering the models available to them, and making normalized calls, so every product stops rebuilding the same model-picker filters and provider wrappers.

Key validation, model listing and filtering, text generation, streaming, tool calling (with tool search, custom freeform tools, and OpenAI's apply_patch convention), native web search with normalized citations, hosted code execution, structured JSON output, reasoning-effort control, prompt caching, embeddings, image, speech, realtime voice, and video generation, streaming speech-to-text, image, audio, and document input, and service-key probing (Google Maps Platform, LiveKit Cloud) all work and are live-verified against every provider that supports them. The API is stable.

Docs: USAGE.md for the full API and CLI reference · ARCHITECTURE.md for the layer diagram and component contracts · CHANGELOG.md for version history.

Install

Before you start: check you have Python with python3 --version. If that fails, see USAGE.md for how to install it.

pip install keycall

See it work in 30 seconds

No config file, no signup, nothing to export. Type the command, name your provider, paste your key:

keycall verify
Provider (openai, anthropic, gemini, deepseek, perplexity, moonshot, xai, assemblyai, deepgram, elevenlabs, google_maps): openai
API key:
✓ openai (openai): key accepted, 79 text model(s), list digest 6d356bc3f4c24389, selection rule v4

The provider name is case-insensitive and the key is hidden while you type it. Add --generate to also make one small billable call, which reports the model that answered, its position in the provider's own list, the elapsed time, and the tokens spent.

For scripts, skip the prompts by pointing at an environment variable:

keycall verify --provider openai --source env:OPENAI_API_KEY --generate

Prefer to click around? Same key, one word different:

keycall view --provider openai --source env:OPENAI_API_KEY

That opens a local web app in your browser with your key already loaded: a dashboard that checks it live, a browsable model list with category filters, and a Playground where you can chat, show a model a picture, record a voice message straight from the page, hold a live voice conversation, transcribe your speech live, transcribe an audio file, attach a PDF, offer it a tool, have it draw you a picture or render a video, or have it speak text aloud. Keys stay in the local server process and never reach the browser.

Got several keys? Put them in a file and load them all at once — see keycall-test-keys.example.toml for the format:

keycall view --source ./keys.toml

From a fresh clone with no Python set up at all, double-click a launcher instead: launch.command (macOS), launch.sh (Linux/macOS), launch.bat (Windows). Each creates the venv, installs KeyCall, finds your key file, and opens the viewer.

Use it from Python

from keycall import KeyCall, Message, ModelCategory, TextInput

with KeyCall(provider="openai", api_key=secret) as client:
    discovery = client.list_models(categories={ModelCategory.TEXT_GENERATION})

    result = client.generate_text(
        model=discovery.models[0].id,
        messages=[Message(role="user", content=[TextInput(text="Hello.")])],
    )

print(result.text)
print(result.usage.total_tokens)
print(result.round_trip_duration_ms)

What you get

  • Explicit provider, always. KeyCall never guesses which vendor issued a key and never sends a credential to more than the one provider you name.
  • No credential storage. Keys live in memory for the client's lifetime, wrapped in a redacting type that keeps them out of reprs, logs, traces, exceptions, and pickles. Your app decides how to store them.
  • Model filtering built in. Text-generation models by default; embeddings, image, audio, and other categories on request; unknown models never silently enter the default picker.
  • Rolling-alias detection. alias_fact(provider, model_id) says whether an id is a rolling alias under the provider's recorded naming convention — no credential needed — including whether the provider maintains it aimed at a live model (Gemini) or was seen retiring the family (OpenAI's -chat-latest). Model.alias carries the same fact at discovery; ids with no recorded convention return None rather than a guess.
  • Typed errors. Invalid key, rate limit, provider outage, timeout, and malformed response are distinguishable, never collapsed into "invalid key."
  • Retired-model registry. The catalog records the models each provider has shut down, with dated live-verified evidence. Naming one in any call refuses before the network with a typed MODEL_RETIRED error that carries the retirement date and the provider's recommended replacement, and list_models() withholds them with a visible warning per model instead of offering ids that fail on use. Release probes re-verify that every recorded id is still refused by its provider and every recorded replacement is still listed.
  • Streaming. stream_text() yields typed events (text increments, visible reasoning progress, citations, tool calls, finish) across all four wire protocols, and refuses to call a stream complete without the provider's own terminal signal.
  • Tool calling. Define tools once and KeyCall normalizes all four call/result wire formats, streamed or not, carrying the provider echo data some models require back verbatim. It never executes a tool.
  • Image generation. generate_image() returns the picture as bytes with the media type the provider produced, on OpenAI, Gemini, and xAI; the rest refuse before the network.
  • Speech generation. generate_speech() speaks text aloud on OpenAI, Gemini, and ElevenLabs. The result carries the media type the provider sent, including Gemini's raw PCM, never a container the provider didn't produce. list_voices() returns the same normalized Voice records on all three — catalog-listed for OpenAI and Gemini, fetched live from ElevenLabs, so cloned voices on your account appear too. ElevenLabs requires a voice on every call, and a call without one refuses before the network, naming voices from your key.
  • Video generation. start_video() returns a job handle, check_video() polls it, fetch_video() downloads the finished MP4 — or generate_video() runs all three against a timeout you choose. Gemini (Veo) and xAI (Grok Imagine) support it; the rest refuse before the network. A timeout hands back the still-valid job, so a slow render is never lost.
  • Embeddings. embed() returns one vector per input, in input order, on OpenAI and Gemini; providers without an embeddings endpoint refuse before the network instead of 404ing.
  • Batch generation. start_batch() submits many requests to the provider's discounted asynchronous batch lane (OpenAI, Anthropic, Gemini, Moonshot, xAI), check_batch() polls the job, fetch_batch_results() returns every outcome in submission order — or generate_batch() runs all three against a timeout you choose. Per-request failures arrive as results, never exceptions; embedding batches run on OpenAI and Gemini.
  • Images, audio, and documents. Pass bytes (or a URL where the provider fetches one) beside your text; KeyCall maps each provider's shape and detects the media type from the content. Support varies by provider and by form, so a refusal happens before the network and names who does accept that kind.
  • Web search with citations. web_search=True turns on the provider's native search tool (OpenAI, Anthropic, Gemini, xAI, Moonshot; Perplexity always searches) and returns sources normalized to one Citation shape (Moonshot reports none).
  • Sampling and reproducibility controls. Set temperature and top_p explicitly rather than leaving a call on the model's own default. Where a model fixes or refuses an explicit value (OpenAI's reasoning and gpt-5 families, Moonshot's Kimi, Anthropic's newest models), KeyCall refuses before the network naming the one permitted value instead of letting the provider 400. seed is forwarded for repeatable sampling on the providers whose API defines it (Gemini, DeepSeek, Moonshot, xAI) and refused before the network on those without it (OpenAI's Responses API, Anthropic, Perplexity), so a seed set for reproducibility never silently vanishes; determinism from a seed is best-effort everywhere, as every provider states.
  • Reasoning effort control. reasoning_effort="low" (or "medium" / "high") maps to the provider's native thinking control on OpenAI, Anthropic, DeepSeek, Gemini, Perplexity, and xAI, each verified live to move reasoning-token spend. Providers that accept the parameter without honoring it refuse instead of silently ignoring it. The spend itself is normalized into usage.reasoning_tokens wherever the provider reports a count (OpenAI, Gemini, DeepSeek, Moonshot, xAI).
  • Realtime voice sessions. realtime() opens a live WebSocket conversation on OpenAI, xAI, or Gemini — text or microphone audio up, normalized audio/transcript/turn events down, sync and async. The credential rides the handshake headers and never enters a URL.
  • Live full-duplex sessions. live() opens a full-duplex voice conversation with OpenAI's gpt-live on its own v1/live/sessions endpoint, a sibling of realtime(): caller and model audio overlap, the model endpoints the caller's turn itself and delegates reasoning to a separately-billed backend model, and normalized events cover the caller's transcript, the model's audio and words, barge-in, and the running billed seconds. The wire is probe-confirmed end to end against gpt-live, and a committed-fixture drift probe holds it to that shape on every release.
  • File transcription. transcribe() turns a stored audio file into text with millisecond word timings on OpenAI, ElevenLabs, Deepgram, and AssemblyAI — bytes everywhere, a URL where the provider fetches one, speaker labels where the wire reports them. AssemblyAI processes as a job, so there the same call polls against a timeout you choose, with start_transcription()/check_transcription()/fetch_transcription() for driving the job yourself.
  • Streaming transcription. transcribe_stream() opens a live speech-to-text session on AssemblyAI, Deepgram, or ElevenLabs — raw PCM audio up, normalized interim/final transcripts with per-word millisecond timings down, and the provider's billable audio seconds on the session-ended event where the provider reports them (ElevenLabs sends no duration summary). diarize=True labels each finalized word with its speaker on the two providers whose streaming wire reports one (AssemblyAI, Deepgram); the third refuses the flag before connecting rather than returning unlabelled words. Sync and async, same header-auth rule as realtime.
  • Hosted code execution. code_interpreter=True lets the model write and run code on the provider's own sandbox (OpenAI, Anthropic, Gemini, xAI), with each run returned as a typed part — nothing executes on your machine.
  • Tool search. Tool(defer_loading=True) keeps a large tool library out of the model's context until it searches for what it needs (OpenAI, Anthropic); discovered tools call and reply like ordinary ones.
  • File-editing tool convention. apply_patch=True enables OpenAI's built-in file-editing tool: the model proposes create/update/delete operations as V4A diffs, arriving as ordinary ToolCall/ToolResult parts named "apply_patch" in the same replay loop as any other tool. OpenAI-only; other providers refuse before the network.
  • Custom (freeform) tools. Tool(input_schema=None) declares a tool with no JSON Schema: the model's call arrives as a plain string instead of parsed arguments. OpenAI-only; other providers refuse before the network.
  • Prompt caching. TextInput(cacheable=True) marks a stable prefix (a big system prompt, reference material) for caching. Anthropic is the one provider where caching doesn't happen at all without this marker; OpenAI already caches automatically and the marker opts into its optional explicit mode; every other provider ignores the flag and keeps caching automatically on its own. Usage.cached_input_tokens reports a cache hit uniformly everywhere, marked or not.
  • Structured output. response_schema=<JSON Schema> is enforced provider-side on OpenAI, Anthropic, Gemini, Moonshot, Perplexity, and xAI; on providers without enforcement (DeepSeek, unverified custom targets) KeyCall falls back to guaranteed-valid-JSON mode and adds a result warning rather than claiming a guarantee it can't back. result.text is always the JSON string, regardless of which mechanism produced it.
  • Service-provider keys. A key with no models behind it — Google Maps Platform, or a LiveKit Cloud key/secret pair — verifies through probe_services(): one cheapest-possible request per service category, answered as per-category standings (enabled, denied with the provider's own reason, or unknown), with a rejected credential still raising the same typed INVALID_API_KEY as everywhere else. The verify CLI, key files (an optional secret field), and the viewer's dashboard all take these targets.
  • Hardened transport. TLS always verified, redirects refused, response sizes capped, SSRF and DNS-rebinding guards on custom endpoints that fail closed when a proxy would bypass them, and generation is never silently retried.

Provider support

Live-verified 2026-09-08, with the two service rows (Google Maps Platform, LiveKit Cloud) added and live-probed 2026-09-10. Every release re-runs a model list, a bounded generation, a stream, a full tool round (streamed and not, including apply_patch, custom tools, and tool search), hosted code execution, an image, sound, and document read, embeddings, image generation, a speech generation with a listed voice, a video render, a batch of generations on every batch-capable provider, a file transcription on every prerecorded-STT provider, a prompt-caching round trip, an async round trip, a live streaming-transcription session, capability-drift probes against previously observed provider behavior, and a probe that each provider still reaches a working model well inside the attempt budget, against every provider that supports them, and blocks publishing if any of it fails:

Provider Protocol Listing Generation
OpenAI openai verified verified
Anthropic anthropic verified verified
Google Gemini gemini verified verified
DeepSeek openai-compatible verified verified
Perplexity openai-compatible verified verified
Moonshot/Kimi openai-compatible verified verified
xAI (Grok) openai-compatible verified verified
AssemblyAI stt verified streaming transcription verified
Deepgram stt verified streaming transcription verified
ElevenLabs elevenlabs verified speech + streaming transcription verified
Custom endpoint (explicit base_url) openai-compatible fixtures only fixtures only
Google Maps Platform google_maps no models (service) service probes verified
LiveKit Cloud livekit no models (service) service probe verified

AssemblyAI and Deepgram are speech-to-text providers: their generation column is transcribe_stream(), since they have no text-generation API, and their model lists are maintained catalog data behind a live credential check. ElevenLabs is a speech platform with the same posture on text: its generation column is generate_speech() plus transcribe_stream(), and its model list comes live from its own models endpoint, with the streaming-transcription model as maintained catalog data since that endpoint omits it. Google Maps Platform and LiveKit Cloud are service providers with no models at all: their column is probe_services(), one live request per service category, and release probes re-verify each provider's observed auth and error behavior.

OpenAI advertises -latest aliases its own account can't invoke, and is retiring that family wholesale. On 2026-08-10 all four were dead: gpt-5-chat-latest and gpt-5.1-chat-latest returned "Model not found", and gpt-5.2-chat-latest and gpt-5.3-chat-latest were newly deprecated hours after both had worked. The numbered models were healthy throughout. This is the same failure as Gemini's retired models, on a provider people assume is tidier, and it is why verify walks the candidates and reports every attempt rather than trusting the first listed model. The models OpenAI has since shut down while still listing them are recorded in the retired-model registry, so they refuse pre-flight and never enter a listing.

Because of that, candidate order follows the provider's own dates where it publishes them. OpenAI, Anthropic, and Moonshot date every model they list, and a model a provider published recently is one it hasn't yet retired, so KeyCall tries the newest first. Gemini and DeepSeek publish no dates, and there maintained -latest aliases lead instead, which is right for Gemini because it keeps those aimed at a live model. Sorting aliases first everywhere was the earlier rule, and on OpenAI it put the four worst candidates at the front of every walk. A release probe now checks each provider still reaches a working model well inside the attempt budget, so this kind of drift is caught before it reaches a key.

Two further provider quirks to know, both handled:

Gemini keeps retired models in its list endpoint with no lifecycle field to pre-filter on, and withdraws them per account ahead of the published shutdown date: on 2026-08-09 the first six text models it advertised to a new key were all refused, gemini-2.5-* with "no longer available to new users" months before its documented shutdown. Gemini dates none of its models, so KeyCall tries its maintained -latest aliases first there, and verification reaches a model that works instead of walking a list of withdrawn ones; the error for a retired model names those aliases. It also meters quota per model and tier, so one model's 429 says nothing about the next. Its supportedGenerationMethods is a transport signal rather than a modality claim: TTS variants advertise generateContent and then refuse a text response, and so do the Interactions-only, computer-use, and music families, so KeyCall lets the identifier outrank it and keeps those out of the default text picker.

Perplexity's GET /v1/models is scoped to the Agent API and returns vendor-prefixed router models (anthropic/..., perplexity/sonar) that the Sonar route rejects. Sonar's own models aren't API-discoverable, so KeyCall maintains them in its catalog and uses the list call purely as a credential check. Note the version prefix: the unversioned https://api.perplexity.ai/models returns 404 for every key, valid or not, so anything validating a key against that path rejects good credentials. /v1/models answers 401 for a bad key and 200 for a good one, which is what makes it usable as a check (verified 2026-08-09).

Structured output notes, per provider

  • OpenAI requires additionalProperties: false on every object level of the schema for its strict json_schema mode, or the request 400s. This is an OpenAI requirement, not a KeyCall one — write schemas with it from the start.
  • Anthropic enforces the schema natively via output_config.format, which combines with web_search=True and with caller tools in the same request (live-verified 2026-09-10). Separately, claude-fable-5-1 refuses forced tool selection, so tool_choice="required" on that model raises MODEL_NOT_SUITABLE before any network call.
  • Gemini's equivalent combination (web_search=True with response_schema) isn't gated — no live-verified evidence either way that Gemini rejects it, so KeyCall passes it through rather than guessing.
  • DeepSeek hard-requires the literal word "json" somewhere in the prompt for its json_object fallback mode, or it 400s. KeyCall detects this and injects a short system instruction automatically when needed, and always says so via a result warning.
  • Moonshot/Kimi reasoning-capable models can spend the entire max_output_tokens budget on a visible reasoning trace and never emit a final answer if the budget is too small. KeyCall detects the resulting empty-content-with-reasoning-trace response and adds a warning rather than returning a silent empty result; give these models a larger budget than you'd expect a short answer to need.

Because of quirks like these, keycall verify --generate walks the filtered models in provider order and prints the outcome of every attempt until one succeeds, so drift stays visible rather than being masked by a silent retry.

The viewer and the verify CLI in full

The viewer is token-protected and binds 127.0.0.1. Opening the printed link trades its token for an httpOnly, SameSite=Strict session cookie and strips it from the address bar, so the secret never reaches page script or browser history, and state-changing requests are CSRF-checked. Inside: a dashboard with live key checks, a sortable model browser that also names any models withheld because the provider retired them, a Playground (text, pictures in and out, video generation, speech generation, recordings, live voice conversations, file transcription, documents, tool calling, web search), a verify report that walks every key, and a Traces tab logging every request this run has made (timing and outcome only, never prompts or replies), searchable and sortable by column. An attachment the selected key can't send is turned off with a line naming a key that can.

keycall verify takes the same sources as the viewer — TXT, JSON, or TOML files, an env:VAR_NAME reference, or an interactive prompt — and --generate adds one small bounded call per target. Keys never appear in output, and KeyCall never writes to or deletes your credential file. Full reference in USAGE.md.

Development

pip install -e ".[dev]"
pytest

Author

Built by Mo Shehu.

License

AGPL-3.0-or-later. See LICENSE.

Release files for keycall 1.13.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for keycall 1.13.0
File Size Uploaded
keycall-1.13.0.tar.gz 1.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for keycall 1.13.0
File Interpreter ABI Platform
keycall-1.13.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.4 MB

Release files / keycall-1.13.0.tar.gz

Download URL keycall-1.13.0.tar.gz
Size 1.1 MB
Tags Source
SHA-256 checksum
How to use checksums
8ddb47e5c16238af451227e592d92c929f04eb4073c63a1cfbd06b4a4c7e20a1
BLAKE2b-256 checksum
How to use checksums
efabb848d14e712e02f3e510eb5f99c33640c7282ba4af56535c75b11d427eb7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Release files / keycall-1.13.0-py3-none-any.whl

Download URL keycall-1.13.0-py3-none-any.whl
Size 322.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7060b15ba5ab8c3860ce96e52c5e00324b37ee23b2a2f65556cebe05d7ffbea1
BLAKE2b-256 checksum
How to use checksums
9f984ba8fd96503a7c67de7b161b3cfb17a1225ea9f27323acf08921f26117b7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Release history Release notifications | RSS feed

1.15.0

2 release files

1.14.0

2 release files

1.13.1

2 release files

This release

1.13.0 This release

2 release files

1.12.0

2 release files

1.11.2

2 release files

1.9.0

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.1

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page