Pluggable LLM adapters for Python
Find a file
tegwick 8fbf74fba0
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Add Forgejo CI smoke workflow (enablement template)
2026-07-08 12:34:07 +02:00
.claude/rules Normalize agent instructions and workplan frontmatter (STATE-WP-0067) 2026-06-22 23:16:27 +02:00
.forgejo/workflows Add Forgejo CI smoke workflow (enablement template) 2026-07-08 12:34:07 +02:00
.github/workflows feat: WP-0001 foundation + WP-0002 core extensions 2026-04-01 22:24:14 +00:00
contracts Add activity-core LLM endpoint support 2026-06-07 19:24:45 +02:00
deploy/k8s/activity-core-llm-connect activity-core: ExternalSecret for llm-connect-provider-secrets via openbao-activity-core CSS (CCR-2026-0003) 2026-07-02 12:56:21 +02:00
docs docs: record railiance01 llm-connect smoke evidence 2026-06-19 15:58:04 +02:00
examples Adaptive routing initial version 2026-05-18 11:38:12 +02:00
fixtures/activity_core Add activity-core LLM endpoint support 2026-06-07 19:24:45 +02:00
llm_connect Complete activity-core LLM endpoint handoff (LLM-WP-0006) 2026-06-19 13:51:12 +02:00
registry Draft capability entry (reuse-surface REUSE-WP-0017-T04, cohort 2) 2026-07-06 19:41:37 +02:00
scripts Add activity-core LLM endpoint support 2026-06-07 19:24:45 +02:00
tests Complete activity-core LLM endpoint handoff (LLM-WP-0006) 2026-06-19 13:51:12 +02:00
workplans Normalize agent instructions and workplan frontmatter (STATE-WP-0067) 2026-06-22 23:16:27 +02:00
.custodian-brief.md chore(consistency): sync task status from DB [auto] 2026-07-03 18:47:25 +02:00
.dockerignore Add activity-core LLM endpoint support 2026-06-07 19:24:45 +02:00
.gitignore chore: add .gitignore, remove pycache 2026-02-27 07:54:53 +01:00
.repo-classification.yaml Mark .repo-classification.yaml human-reviewed (CUST-WP-0050 T02) 2026-06-22 11:40:44 +02:00
AGENTS.md Normalize agent instructions and workplan frontmatter (STATE-WP-0067) 2026-06-22 23:16:27 +02:00
ARCHITECTURE-LAYERS.md Implement llm-connect ADHOC diagnostics 2026-06-03 11:56:21 +02:00
CLAUDE.md Add credential routing instructions for all agent runtimes 2026-06-18 22:48:46 +02:00
Containerfile Add activity-core LLM endpoint support 2026-06-07 19:24:45 +02:00
FEATURE_REQUESTS.md added feature requests 2026-04-01 21:08:15 +00:00
INTENT.md Added INTENT.md file and reviewed scope 2026-05-03 17:46:24 +02:00
Makefile Add Makefile with start target for HTTP serve mode 2026-07-07 19:59:35 +02:00
pyproject.toml Implement-LLM-WP-0005-cost-model-estimators 2026-05-19 05:02:20 +02:00
README.md Complete activity-core LLM endpoint handoff (LLM-WP-0006) 2026-06-19 13:51:12 +02:00
SCOPE.md Scope update from repo-scoping refactor 2026-05-01 12:26:51 +02:00
tpsc.yaml Third party services catalog declaration 2026-03-25 00:10:13 +01:00
uv.lock Preserve llm-connect run config in server mode 2026-05-19 20:55:02 +02:00

llm-connect

Pluggable LLM adapters for Python and the commandline. Supports OpenRouter, Gemini, OpenAI, and the Claude Code CLI out of the box, with a clean abstract interface for adding your own.

Quick start

from llm_connect import create_adapter, RunConfig

adapter = create_adapter("gemini", model="gemini-2.5-flash")
config = RunConfig(temperature=0.7, max_tokens=1000)
response = adapter.execute_prompt("Summarise the value chain concept.", config)
print(response.content)

Installation

pip install -e /path/to/llm-connect     # local editable install
# or, once published:
pip install llm-connect

Requires: Python 3.10+, toml

Providers

Provider key Class Notes
"openrouter" OpenRouterAdapter OpenAI-compatible endpoint; supports all OpenRouter models
"gemini" GeminiAdapter Google Generative Language REST API; supports free tier
from llm_connect import create_adapter

# OpenRouter
adapter = create_adapter("openrouter", model="anthropic/claude-sonnet-4")

# Gemini (uses GEMINI_API_KEY env var or apikey-geminifree.txt)
adapter = create_adapter("gemini", model="gemini-2.5-flash")

# OpenAI (uses OPENAI_API_KEY env var)
adapter = create_adapter("openai", model="gpt-4.1-mini")

# Claude Code CLI (uses locally installed claude binary)
adapter = create_adapter("claude-code")

API keys

Keys are resolved in this order (first found wins):

  1. Explicit api_key argument to the constructor
  2. Environment variable (e.g. OPENROUTER_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY)
  3. Key file in the project root (e.g. apikey-openrouter.txt, apikey-geminifree.txt)

Core types

RunConfig

Controls a single LLM call.

from llm_connect import RunConfig

config = RunConfig(
    model_name="gemini-2.5-flash",  # overrides adapter default
    temperature=0.3,
    max_tokens=2000,
    timeout_seconds=60,
)
Field Default Description
model_name "gpt-4" Model identifier (adapter may override)
temperature 0.7 Sampling temperature
max_tokens 2000 Maximum output tokens
model_params {} Portable extras translated by each adapter; see docs/adapter-model-params.md
max_depth 3 Max nesting depth for recursive calls
skip_if_exists True Skip if identical input hash already processed
timeout_seconds 300 Request timeout

LLMResponse

Returned by every execute_prompt call.

response = adapter.execute_prompt(prompt, config)
print(response.content)       # generated text
print(response.model)         # model actually used
print(response.usage)         # {"prompt_tokens": …, "completion_tokens": …, "total_tokens": …}
print(response.finish_reason) # "stop", "length", etc.

Server diagnostics

Serve mode can include a debug envelope without changing normal responses:

LLM_CONNECT_DEBUG=1 python -m llm_connect.server --provider openrouter
curl 'http://127.0.0.1:8080/execute?debug=1' -d '{"prompt":"hi"}'

Set LLM_CONNECT_AUDIT_DIR=/path/to/audit to write per-call replay records, then parse one without another provider call:

python -m llm_connect.replay /path/to/audit/record.json --json

Server runtime profiles

Serve mode enables named runtime profiles by default. A client can send config.model_name="custodian-triage-balanced" and the server resolves it to the configured provider/model before calling the adapter.

Useful runtime environment variables:

LLM_CONNECT_HOST=0.0.0.0
LLM_CONNECT_PORT=8080
LLM_CONNECT_PROVIDER=openrouter
LLM_CONNECT_MODEL=google/gemini-2.5-flash
LLM_CONNECT_CUSTODIAN_TRIAGE_PROVIDER=openrouter
LLM_CONNECT_CUSTODIAN_TRIAGE_MODEL=google/gemini-2.5-flash

For local smoke tests without provider credentials:

export LLM_CONNECT_MOCK_RESPONSE="$(python -c 'import json; print(json.dumps(json.load(open("fixtures/activity_core/daily-triage-valid-content.json"))))')"
python -m llm_connect.server --provider mock
python scripts/smoke_activity_core_endpoint.py --url http://127.0.0.1:8080

Disable profile dispatch with --disable-profiles. Set LLM_CONNECT_STRICT_PROFILES=1 or pass --strict-profiles to reject direct model names that are not configured profiles.

Writing your own adapter

from llm_connect import LLMAdapter, RunConfig, LLMResponse

class MyAdapter(LLMAdapter):
    def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse:
        # call your API here
        return LLMResponse(content="...", model="my-model")

    def validate_config(self, config: RunConfig) -> bool:
        return True

TOML configuration chain

The resolve_llm() function walks a 7-level priority chain to pick a provider and model. This is used by the llm-helper integration but is also available standalone:

from llm_connect.toml_config import resolve_llm

resolved = resolve_llm(app_name="myapp")
print(resolved.provider, resolved.model, resolved.provider_source)

Priority order (highest first):

  1. CLI flags (cli_provider, cli_model arguments)
  2. Env var {APP_NAME}_HELPER_MODEL (model only)
  3. User preference — ~/.config/{app_name}/config.toml [llm.preference]
  4. Directory preference — .{app_name}.toml [llm.preference]
  5. Directory default — .{app_name}.toml [llm.default]
  6. User default — ~/.config/{app_name}/config.toml [llm.default]
  7. Hardcoded fallback — gemini / gemini-2.5-flash

Example config file (~/.config/myapp/config.toml):

[llm.default]
provider = "gemini"
model = "gemini-2.5-flash"

[llm.preference]
provider = "openrouter"
model = "anthropic/claude-sonnet-4"

Embeddings

from llm_connect import create_embedding_adapter, EmbeddingCache

adapter = create_embedding_adapter("openai", model="text-embedding-3-small")
cache = EmbeddingCache(cache_dir=".embeddings")

# Get embedding (cached after first call)
vec = cache.get_or_compute("my text", lambda t: adapter.embed([t])[0])

Exceptions

from llm_connect.exceptions import (
    LLMError,             # base
    LLMConfigurationError,# bad key, unknown provider
    LLMAPIError,          # HTTP error from provider (has .status_code)
    LLMRateLimitError,    # 429
    LLMTimeoutError,      # request timed out
    LLMSubprocessError,   # claude CLI failed (has .return_code, .stderr)
)

Testing

from llm_connect import MockLLMAdapter, RunConfig

mock = MockLLMAdapter(mock_response="Test response")
config = RunConfig()
response = mock.execute_prompt("any prompt", config)
assert response.content == "Test response"
assert mock.call_count == 1

Origin

Extracted from the markitect project. The markitect.llm module remains a re-export shim pointing here.