Skip to main content

rwu-llmconnector

Unified Python connectors for calling large language models through a common interface.

Supported backends:

  • OpenAI (official API, tool/function calling)
  • OpenRouter and other OpenAI-compatible endpoints
  • Google Gemini (google-genai)
  • Ollama (local server, tool/function calling)
  • LM Studio (local or remote server, stateful conversations, MCP integrations)

Installation

pip install rwu-llmconnector

With a virtual environment (recommended):

python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install rwu-llmconnector

Quick start

All provider clients inherit from LLMClient and share send_request():

response, prompt_tokens, completion_tokens, reasoning_tokens, runtime = llm.send_request(
    user_message,
    system_message=None,
    ignore_history=False,
    temperature=None,
    model=None,
    tools=None,          # OpenAI-compatible tool definitions
    integrations=None,   # LM Studio MCP / integration specs (per request)
    max_output_tokens=None,  # generation cap (alias: max_tokens via kwargs)
    logger=None,         # per-request logger; default: client instance logger
    **kwargs,            # e.g. tool_mapping, timeout, context_length (LM Studio)
)

send_request_perf(...) accepts the same arguments and returns (runtime, tokens_per_second, response_message) instead of token breakdown.

Logging: Clients use default_logger at INFO (request start/end, token summary, errors). Pass logger= to the client constructor or to send_request / send_request_perf / call_client / call_client_with_tools to override for a single call. For a custom sink, use get_print_logger(name, msg_producer=..., level=logging.DEBUG).

import logging

from rwu_llmconnector import OpenAIClient, get_print_logger

verbose = get_print_logger("my-app", level=logging.DEBUG)
llm = OpenAIClient(client, model="gpt-4o-mini", logger=verbose)
llm.send_request("Hi", logger=verbose)  # same or different logger per call

Output token cap (max_output_tokens): limits generated tokens only (not prompt/context size). Per-call values override the client default; the instance attribute is not mutated. Mapping: OpenAI → max_completion_tokens; LM Studio → max_output_tokens; Ollama → num_predict. Alias: max_tokens.

LM Studio context_length: omit by default so the server uses the context of the already loaded model (avoid sending a mismatched value, e.g. 8000 vs 8192). Set on the client or per request when you need an explicit cap:

llm.send_request("...", max_output_tokens=512)           # or max_tokens=512
llm.send_request("...", context_length=8192)             # LM Studio only

Public exports

from rwu_llmconnector import (
    LLMClient,
    OpenAIClient,
    GeminiClient,
    OllamaClient,
    LMStudioClient,
    StatefulLMStudioClient,
    call_requested_tools,
    default_logger,
    get_print_logger,
)

list_models() is implemented on OpenAI, Ollama, and LM Studio clients (Gemini raises NotImplementedError).

OpenAI

from openai import OpenAI
from rwu_llmconnector import OpenAIClient

client = OpenAI(api_key="your-openai-api-key")
llm = OpenAIClient(client, model="gpt-4o-mini")

response, prompt_tokens, completion_tokens, reasoning_tokens, runtime = llm.send_request(
    "What is PyPI?",
    system_message="you are a helpful assistant",
)
print(response)

Pass max_output_tokens (or max_tokens) to cap the assistant reply; OpenAI receives max_completion_tokens:

response, *_ = llm.send_request("What is PyPI?", max_output_tokens=256)

Tool calling

When tools is set, OpenAIClient runs the tool loop automatically: model requests tools → handlers execute → follow-up completion.

Pass tool_mapping as {tool_name: callable} (keyword args match the tool JSON schema):

tools = [
    {
        "type": "function",
        "function": {
            "name": "echo",
            "description": "Echo text back",
            "parameters": {
                "type": "object",
                "properties": {"text": {"type": "string"}},
                "required": ["text"],
            },
        },
    }
]

response, *_ = llm.send_request(
    "echo hello",
    tools=tools,
    tool_mapping={"echo": lambda text: {"text": text}},
    ignore_history=True,
)

You can also call llm.call_client_with_tools() directly. call_client_with_tool() is a backward-compatible alias.

Optional: slim_history=True omits the assistant tool-call message from the follow-up context (keeps history lean).

To reuse tool execution outside OpenAIClient:

from rwu_llmconnector import call_requested_tools

tool_messages = call_requested_tools(assistant_message, tool_mapping=handlers, logger=my_logger)

OpenRouter

OpenRouter is OpenAI-compatible — use OpenAIClient with a custom base_url (tools work the same way):

import os

from openai import OpenAI
from rwu_llmconnector import OpenAIClient

client = OpenAI(
    base_url="https://openrouter.ai/api/v1/",
    api_key=os.environ["OPEN_ROUTER_API_KEY"],
)
llm = OpenAIClient(client, model="gpt-4o-mini")

response, prompt_tokens, completion_tokens, reasoning_tokens, runtime = llm.send_request(
    "What is PyPI?",
    system_message="you are a helpful assistant",
)
print(response)

Same limits as OpenAI: optional max_output_tokens on OpenAIClient(..., max_output_tokens=256) or on each send_request(..., max_output_tokens=256) (alias max_tokens); forwarded as max_completion_tokens to the API.

Set OPEN_ROUTER_API_KEY in your environment (API key from openrouter.ai/keys).

Google Gemini

from google import genai
from rwu_llmconnector import GeminiClient

client = genai.Client(api_key="your-gemini-api-key")
llm = GeminiClient(client, model="gemini-2.5-flash-lite")

response, *_ = llm.send_request(
    "What is PyPI?",
    system_message="you are a helpful assistant",
)
print(response)

Ollama

from rwu_llmconnector import OllamaClient

llm = OllamaClient(model="llama3.2", host="http://localhost:11434")

response, *_ = llm.send_request(
    "What is PyPI?",
    system_message="you are a helpful assistant",
)
print(response)

max_output_tokens maps to Ollama num_predict in chat options.

Tool calling uses the same tools / tool_mapping pattern as OpenAI (Ollama chat API):

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the weather in a given city",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    }
]

response, *_ = llm.send_request(
    "what is the weather in Toronto?",
    tools=tools,
    tool_mapping={"get_weather": lambda city: f"{city}: 11 degrees celsius"},
    ignore_history=True,
)

LM Studio

Default server: http://localhost:1234 (override with LMSTUDIO_BASE_URL or env var). If LM Studio Developer → Authentication is enabled, set LMSTUDIO_API_KEY in .env or pass api_key= (the client also reads LMSTUDIO_API_KEY / MAC_STUDIO_API_KEY from the environment when api_key is omitted).

from rwu_llmconnector import LMStudioClient, StatefulLMStudioClient

# local_history: send conversation from self.history (default)
llm = LMStudioClient(
    model="qwen/qwen3-4b-2507",
    base_url="http://localhost:1234",
    # api_key=os.environ["LMSTUDIO_API_KEY"],  # when Bearer auth is required
    # Optional defaults (only sent to the API when set):
    # context_length=8192,
    # max_output_tokens=512,
)

response, prompt_tokens, completion_tokens, reasoning_tokens, runtime = llm.send_request(
    "What is PyPI?",
    system_message="you are a helpful assistant",
)
print(response)

Context and limits

Parameter Constructor send_request / call_client Payload
context_length LMStudioClient(..., context_length=8192) same name; overrides for that call only context_length (omitted if unset)
max_output_tokens max_output_tokens=512 same; alias max_tokens max_output_tokens

Do not set context_length unless you mean to — a value that does not match the loaded model can force LM Studio to reload and fail (e.g. OOM). Per-request context_length / max_output_tokens do not change self.context_length / self.max_output_tokens.

For server-side context (store=True, previous_response_id chain), use StatefulLMStudioClient.

MCP / integrations

Pass integration specs in the constructor, per request via send_request(integrations=...), or add them later with add_integration(). Each entry is either a dict (LM Studio integration JSON) or an object with to_lmstudio_integration(). Per-request integrations override the client-level list for that call only.

Hugging Face MCP (same pattern as the live test in tests/test_lmstudio_client.py):

from rwu_llmconnector import LMStudioClient

mcp_hf = {
    "type": "ephemeral_mcp",
    "server_label": "huggingface",
    "server_url": "https://huggingface.co/mcp",
}

llm = LMStudioClient(
    base_url="http://localhost:1234",
    model="openai/gpt-oss-20b",
    temperature=0.0,
    integrations=[mcp_hf],
)

response_msg, prompt_tokens, completion_tokens, reasoning_tokens, runtime = llm.send_request(
    "list 3 new papers",
    ignore_history=True,
    timeout=120,
)
print(response_msg)
print(
    f"prompt tokens: {prompt_tokens}, completion_tokens: {completion_tokens}, "
    f"reasoning_tokens: {reasoning_tokens}, runtime: {runtime:.2f} seconds"
)

If Hugging Face is already configured in ~/.lmstudio/mcp.json, use a plugin id instead:

mcp_hf = {"type": "plugin", "id": "mcp/huggingface"}

Custom integration wrapper (same hook as test_integration_to_lmstudio_integration_hook):

class MyMCP:
    def to_lmstudio_integration(self):
        return {"type": "mcp", "server": "demo"}

llm = LMStudioClient(model="your-model")
llm.add_integration(MyMCP())
response, *_ = llm.send_request("hi", ignore_history=True)

Model loader helpers

from rwu_llmconnector.model_loader import load_openai_model, load_ollama_model

llm = load_openai_model("gpt-4o-mini", env="openai")
ollama = load_ollama_model("llama3.2")

Set OPENAI_API_KEY in the environment when using env="openai".

Development

Clone the repository, then:

uv sync --group dev
cp .env.example .env   # add your API keys locally; never commit .env

Run tests (unit tests always; live tests skip if the backend is unreachable):

uv run pytest -v

Focused test runs:

uv run pytest tests/test_tool_calls.py -v
uv run pytest tests/test_lmstudio_client.py -v
uv run pytest tests/test_lmstudio_client.py::test_lmstudio_mcp_hf_lists_new_papers -v

Tests load variables from .env via python-dotenv. See .env.example for supported names.

Variable Used for
OPENAI_API_KEY OpenAI
OPEN_ROUTER_API_KEY OpenRouter (OpenAI-compatible)
GEMINI_API_KEY or GOOGLE_API_KEY Gemini
LMSTUDIO_BASE_URL, LMSTUDIO_MODEL LM Studio live tests (defaults: http://localhost:1234, qwen/qwen3-4b-2507)
LMSTUDIO_API_KEY or MAC_STUDIO_API_KEY LM Studio Bearer token (required when local auth is enabled)
OLLAMA_MODEL, OLLAMA_HOST Ollama (optional)
LMSTUDIO_MCP_MODEL MCP live test model (default openai/gpt-oss-20b)
LMSTUDIO_MCP_INTEGRATION_JSON Override MCP spec (e.g. plugin id from mcp.json)

Project layout

src/rwu_llmconnector/   # library code
tests/                  # unit + live integration tests
pyproject.toml
uv.lock                 # pinned dev dependencies
.env.example            # template for local secrets (commit this)

Local scratch scripts can live in tmp/ (gitignored). Use cp .env.example .env and load secrets from .env; see .vscode/launch.json for PYTHONPATH=src when debugging scripts there.

Do not commit: .env, .venv/, dist/, __pycache__/, .pytest_cache/, .idea/, tmp/

Publishing to PyPI

Build and upload (requires PyPI account and API token):

uv build
uv publish --token pypi-AgEIcHlwaS...

Or set UV_PUBLISH_TOKEN in the environment and run uv publish. Upload only the files in dist/; do not add dist/ to git.

License

MIT — see LICENSE.

Metadata

Release files for rwu-llmconnector 0.2.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 rwu-llmconnector 0.2.0
File Size Uploaded
rwu_llmconnector-0.2.0.tar.gz 84.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rwu-llmconnector 0.2.0
File Interpreter ABI Platform
rwu_llmconnector-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 107.5 kB

Release files / rwu_llmconnector-0.2.0.tar.gz

Download URL rwu_llmconnector-0.2.0.tar.gz
Size 84.4 kB
Tags Source
SHA-256 checksum
How to use checksums
2cc22678e07afe3b6e9af9b7a4c3229d19010a07cd4bb351c75c274f16816260
BLAKE2b-256 checksum
How to use checksums
0e43232b5cd3962c1fb25c96baf594048b8dde581f310a12d59ee992f5424b63
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.25 {"installer":{"name":"uv","version":"0.9.25","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}

Release files / rwu_llmconnector-0.2.0-py3-none-any.whl

Download URL rwu_llmconnector-0.2.0-py3-none-any.whl
Size 23.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
14768d3f557a256934f4f738a94b4835abeacae0e7eb1aa3d3de4a78946e4281
BLAKE2b-256 checksum
How to use checksums
ad6b455810ee0052591934ab970d5866d360936f16caeb90736a04e5d0eea706
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.25 {"installer":{"name":"uv","version":"0.9.25","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}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.1

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