thinwrap-llm

thinwrap-llm for Python — one Chat facade over 18 LLM providers (15 OpenAI-compatible + native Anthropic, Bedrock, Gemini) with streaming, plus embeddings on 11. Zero runtime deps (stdlib urllib), synchronous, stateless, BYO transport.

Python constraints

Runtime dependencies
Zero runtime dependencies
BYO HTTP client
Transport protocol: any object with send(HttpRequest) -> HttpResponse, passed as transport=
Provenance
PEP 740 attestations (PyPI Trusted Publishing)
Minimum runtime
Python 3.10+
  • Truly zero dependencies — the standard library urllib.request is the only HTTP surface, and AWS SigV4 (SES, SNS, Bedrock) / JWT signing (FCM RS256, APNs ES256) are implemented in pure Python on hashlib and hmac, with no cryptography package.
  • Synchronous by design. For async or pooled HTTP, inject an httpx or requests based Transport; the library never imports either.
  • The default transport never follows redirects — a 3xx surfaces as an error rather than silently re-sending auth headers to another host.

30-second install

pip install thinwrap-llm

2-minute end-to-end example

One Chat facade, every provider. Switching vendor is the provider id plus the model — the ChatInput andChatResult shapes stay identical, and every failure is aConnectorError with a typed provider_code.

import os
from thinwrap.llm import Chat, ChatInput, ChatMessage, OpenAICompatConfig, ProviderID, ConnectorError

chat = Chat(ProviderID.OPENAI, OpenAICompatConfig(api_key=os.environ["OPENAI_API_KEY"]))

try:
    res = chat.complete(ChatInput(
        model="gpt-5.5",
        messages=[ChatMessage(role="user", content="Say hi in one word.")],
    ))
    print(res.message.content, res.usage.total_tokens)
except ConnectorError as e:
    print(e.provider_code.value, e.provider_message)

The 15 OpenAI-compatible providers all takeOpenAICompatConfig; Anthropic, Bedrock, and Gemini are native adapters with their own config classes. Samecomplete() shape throughout.

Supported providers

18 providers ship with thinwrap-llm@1.1.0.
ProviderOperationsAuthREADME
openai OpenAIchat, embeddingsbearerdocs
azure-openai Azure OpenAIchat, embeddingsheaderdocs
anthropic Anthropicchatheaderdocs
gemini Google Geminichatheaderdocs
bedrock AWS Bedrockchataws-sig-v4docs
mistral Mistralchat, embeddingsbearerdocs
cloudflare Cloudflare Workers AIchat, embeddingsbearerdocs
together Together AIchat, embeddingsbearerdocs
fireworks Fireworks AIchat, embeddingsbearerdocs
deepinfra DeepInfrachat, embeddingsbearerdocs
openrouter OpenRouterchat, embeddingsbearerdocs
groq Groqchatbearerdocs
deepseek DeepSeekchatbearerdocs
xai xAI (Grok)chatbearerdocs
perplexity Perplexitychatbearerdocs
vllm vLLMchat, embeddingsbearerdocs
ollama Ollamachat, embeddingsbearerdocs
lmstudio LM Studiochat, embeddingsbearerdocs

Migration paths

The in-process complement to a gateway

Thinwrap LLM is a thin, in-process, zero-egress facade — it does not proxy your traffic. It normalizes the request/response shape across vendors so your code stays vendor-neutral; routing, budgets, and observability stay wherever you already run them. Provider-specific request fields ride through Passthrough; the decoded vendor body is always onres.raw.

Streaming

for delta in chat.stream(ChatInput(model="gpt-5.5", messages=msgs)):
    print(delta.content_delta or "", end="", flush=True)

A stream cut off before the provider's terminal event raisesConnectorError rather than ending as if it were complete.

Embeddings

from thinwrap.llm import Embeddings, EmbeddingsInput

emb = Embeddings(ProviderID.OPENAI, OpenAICompatConfig(api_key=key))
out = emb.create(EmbeddingsInput(model="text-embedding-3-small", input=["hello", "world"]))
# out.embeddings is list[list[float]] in input order

Embeddings ship on the OpenAI-float subset (11 of the 18 providers — see the table above).

Cross-language siblings

The TypeScript (/packages/llm-ts), PHP (/packages/llm-php), and Go (/packages/llm-go) variants ship the same facade shape and provider set — cross-language parity is a design goal, not a hard release gate.

Read more