{"slug": "show-hn-padwan-llm-a-lightweight-llm-python-client", "title": "Show HN: Padwan-LLM, a lightweight LLM Python client", "summary": "A developer released Padwan-LLM, a lightweight async Python client that unifies access to OpenAI, Gemini, Mistral, Grok, Anthropic, and any OpenAI-compatible API through a single interface. The package ships with one runtime dependency, niquests, and negotiates HTTP/2 and HTTP/3 automatically, with a separate padwan-cli package providing the full interactive CLI/TUI. Padwan-LLM adds an AgentSession for multi-turn tool-calling conversations, built-in streamable-HTTP and stdio MCP transports, Gemini reasoning-token streaming via an on_thought callback, and a RealtimeClient for bidirectional voice sessions over WebSocket supporting OpenAI gpt-realtime, Gemini Live, and Grok Voice.", "body_md": "Lightweight, unified async client for OpenAI, Gemini, Mistral, Grok, Anthropic, and any OpenAI-compatible API.\nSingle runtime dependency ([niquests](https://github.com/jawah/niquests)), automatic HTTP/2 and HTTP/3 negotiation.\n\nFor the full interactive CLI/TUI, use the separate [`padwan-cli`](https://github.com/polarsen-io/padwan-cli) package.\n\n```\npip install padwan-llm\npython\nfrom padwan_llm import LLMClient\n\nasync with LLMClient(model=\"gpt-4o\") as client:\n    response, usage = await client.complete_chat(\n        [{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response[\"content\"])\npython\nfrom padwan_llm import LLMClient, ConversationState\n\nstate = ConversationState(system=\"You are a concise assistant.\")\n\nasync with LLMClient(model=\"gpt-4o\") as client:\n    state.add_user_message(\"What's Python?\")\n\n    stream = client.stream_chat(state.messages)\n    chunks: list[str] = []\n    async for text in stream:\n        print(text, end=\"\", flush=True)\n        chunks.append(text)\n\n    state.add_assistant_message(\"\".join(chunks))\n    if stream.usage:\n        state.accumulate_usage(stream.usage)\n```\n\n`AgentSession` drives a multi-turn conversation that can dispatch tool calls on each\nround, feed the results back, and repeat until the model returns a plain text answer.\nThe `mcp_tools` list accepts both individual `McpTool` instances and whole\n`McpTransport` servers — transports are entered as part of the session lifecycle:\n\n``` python\nfrom padwan_llm import AgentSession, LLMClient, McpStdio\n\nasync with AgentSession(\n    client=LLMClient(model=\"gpt-4o\"),\n    mcp_tools=[McpStdio(command=\"uvx\", args=[\"my-mcp-server\"])],\n    system=\"You have access to tools. Use them when helpful.\",\n) as session:\n    async for chunk in session.stream(\"What's the weather in Paris?\"):\n        print(chunk, end=\"\", flush=True)\n\n    # Or collect the full response in one call:\n    text = await session.send(\"And in London?\")\n```\n\n`AgentSession` supports sequential or parallel tool execution, approval hooks,\nper-tool error handlers, and optional snapshot persistence via a\n`ConversationStore` protocol — see [docs/agents.md](https://github.com/polarsen-io/padwan-llm/blob/master/docs/agents.md).\n\nBoth streamable-HTTP and stdio MCP transports are built in:\n\n``` python\nfrom padwan_llm import McpStreamable, McpStdio\n\n# Remote MCP server over HTTP (with optional bearer token)\nasync with McpStreamable(url=\"https://mcp.example.com/mcp\", token=\"sk-...\") as mcp:\n    for tool in mcp.tools:\n        print(tool.name, tool.description)\n\n# Local subprocess\nasync with McpStdio(command=\"uvx\", args=[\"my-mcp-server\"]) as mcp:\n    result = await mcp.tools[0].handler({\"query\": \"hello\"})\n```\n\nSee [docs/mcp.md](https://github.com/polarsen-io/padwan-llm/blob/master/docs/mcp.md) for the full feature matrix and architecture.\n\nGemini's reasoning models can stream their internal thought tokens separately from\nthe final answer. Wire an `on_thought` callback to receive them:\n\n``` python\nfrom padwan_llm import GeminiClient\n\nthoughts: list[str] = []\nasync with GeminiClient(\n    model=\"gemini-2.5-flash\",\n    on_thought=thoughts.append,\n    thinking_config={\"thinkingBudget\": 2048, \"includeThoughts\": True},\n) as client:\n    stream = client.stream_chat([{\"role\": \"user\", \"content\": \"What is 7 * 8?\"}])\n    async for chunk in stream:\n        print(chunk, end=\"\")\n\nprint(\"\\n---\\nReasoning:\", \"\".join(thoughts))\n```\n\n`RealtimeClient` opens a bidirectional voice session over a WebSocket and yields\nthe live connection: stream microphone audio in, receive model audio and\ntranscripts back. OpenAI (`gpt-realtime`), Gemini Live, and Grok Voice are\nsupported, dispatched by model name. Requires the `realtime` extra\n(`pip install \"padwan-llm[realtime]\"`):\n\n``` python\nfrom padwan_llm import RealtimeClient\n\nasync with RealtimeClient(instructions=\"Answer briefly.\", voice=\"marin\") as conn:\n    await conn.append_audio(pcm16_chunk)  # mono PCM16 microphone audio\n    async for event in conn:\n        if audio := conn.audio_delta_bytes(event):\n            playback.write(audio)\n```\n\nServer-side VAD drives turn-taking by default; pass `turn_detection=NO_TURN_DETECTION`\nfor manual push-to-talk. See the realtime sections of\n[docs/clients/openai.md](https://github.com/polarsen-io/padwan-llm/blob/master/docs/clients/openai.md),\n[docs/clients/gemini.md](https://github.com/polarsen-io/padwan-llm/blob/master/docs/clients/gemini.md), and\n[docs/clients/grok.md](https://github.com/polarsen-io/padwan-llm/blob/master/docs/clients/grok.md).\n\nOpt-in GenAI spans and metrics for every provider client, following the OTel\nGenAI semantic conventions. Requires the `otel` extra\n(`pip install \"padwan-llm[otel]\"`):\n\n``` python\nfrom padwan_llm import otel\n\notel.instrument()  # uses the global tracer/meter providers\n```\n\nFor a managed trace backend, the Langfuse adapter configures both sides and maps Padwan chat, agent, tool, embedding, and MCP spans to Langfuse observations:\n\n```\npip install \"padwan-llm[langfuse]\"\npython\nfrom padwan_llm.langfuse import instrument\n\ntelemetry = instrument()  # uses the standard LANGFUSE_* environment variables\n```\n\nChat calls emit a `chat <model>` client span (provider, model, server address,\ntoken usage including reasoning tokens, thinking duration, finish reasons,\nrequested tool names) plus the `gen_ai.client.operation.duration` and\n`gen_ai.client.token.usage` histograms. Agent tool execution emits\n`execute_tool` spans; embeddings, batch operations, and realtime sessions get\ntheir own spans. See [docs/observability.md](https://github.com/polarsen-io/padwan-llm/blob/master/docs/observability.md) for the\nfull attribute list.\n\n`just e2e-otel` runs the e2e suite against a local Grafana stack with a\nready-made GenAI dashboard\n([bin/observability/dashboards](https://github.com/polarsen-io/padwan-llm/blob/master/bin/observability/dashboards)):\n\n```\nexport OPENAI_API_KEY=...\n\npadwan-llm \"Hello!\" -m gpt-4o-mini\n\n# Or without installing:\nuvx padwan-llm \"Hello!\" -m gpt-4o-mini\n```\n\nAuto-detected providers: **OpenAI**, **Gemini**, **Mistral**, **Grok**, **Anthropic** (`claude-*`).\n\nAny OpenAI-compatible API (Groq, Together AI, Ollama, vLLM, ...) is supported via `OpenAIClient` with a custom `base_url`.\n\nUnit tests run by default (no API keys needed):\n\n```\nuv run pytest\n```\n\nE2e tests require API keys. Create a `.env` file or pass one with `--env-file`:\n\n```\nuv run pytest tests/e2e/ -m e2e\nuv run pytest tests/e2e/ -m e2e --env-file path/to/.env\n```\n\nTests for providers whose API key is missing are automatically skipped.\n\n```\nOPENAI_API_KEY=...\nGEMINI_API_KEY=...\nMISTRAL_API_KEY=...\nGROK_API_KEY=...\nANTHROPIC_API_KEY=...\n```\n\nAggregators that expose OSS variants of many model families behind a single OpenAI-compatible endpoint and token are supported with two env vars — every model then routes through that gateway, with no per-provider keys or per-call overrides:\n\n```\nPADWAN_BASE_URL=https://your-gateway.example.com/v1/\nPADWAN_API_KEY=...\n# Names that would normally route to a native client (gemini-*, mistral-*, …)\n# go through the gateway as OpenAI-compatible instead.\nasync with LLMClient(model=\"gemini-2.5-flash\") as client:\n    response, usage = await client.complete_chat([{\"role\": \"user\", \"content\": \"Hi!\"}])\n```\n\nPrecedence is explicit `base_url`/` api_key` args → `PADWAN_*` → native\nper-provider env vars. Passing an explicit `base_url` disables gateway mode and\nrestores native provider routing.", "url": "https://wpnews.pro/news/show-hn-padwan-llm-a-lightweight-llm-python-client", "canonical_source": "https://github.com/polarsen-io/padwan-llm", "published_at": "2026-09-16 19:12:12+00:00", "updated_at": "2026-09-16 19:42:17.226636+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "large-language-models", "developer-tools", "ai-products"], "entities": ["Padwan-LLM", "niquests", "padwan-cli", "OpenAI", "Gemini", "Mistral", "Grok", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/show-hn-padwan-llm-a-lightweight-llm-python-client", "markdown": "https://wpnews.pro/news/show-hn-padwan-llm-a-lightweight-llm-python-client.md", "text": "https://wpnews.pro/news/show-hn-padwan-llm-a-lightweight-llm-python-client.txt", "jsonld": "https://wpnews.pro/news/show-hn-padwan-llm-a-lightweight-llm-python-client.jsonld"}}