llmframe
OpenAI-first Python scaffold for building LLM integrations with a hexagonal architecture.
Requirements
- Python 3.11+
uvfor environment and dependency management
Setup
Install the project and development dependencies:
uv sync --frozen --all-groups
Local quality gate
Run the full local quality gate through uv before handoff or release work:
uv run ruff format .
uv run ruff check .
uv run mypy .
uv run pytest
Pytest enforces a conservative minimum coverage threshold and writes a
timestamped HTML report under test_results/ by default.
LLM adapters
The repository includes reusable LLM output adapters under llmframe.adapters.output.llm.
The package is intentionally OpenAI-first: OpenAI is the only implemented provider today, while the surrounding structure stays hexagonal so additional providers can be added later without leaking provider-specific concerns into the shared or application layers.
Key package areas:
llmframe.application.llm- provider-neutral application service for structured JSON extraction, text generation, multimodal input normalization, and batch orchestrationllmframe.adapters.output.llm.llm_adapter- backward-compatible façade over the application LLM servicellmframe.adapters.output.llm.providers.openai- OpenAI provider adapter, client builder, transport, DTOs, and parsing helpersllmframe.adapters.output.llm.usage_tracker- aggregated token and cost tracking utilities
Example imports:
from llmframe import OpenAIClientSettings, build_openai_llm_adapter
from llmframe.adapters.output.llm.usage_tracker import LlmUsageTrackerConfig, OpenAILlmUsageTracker
Recommended construction for third-party code:
from llmframe import OpenAIClientSettings, build_openai_llm_adapter
adapter = build_openai_llm_adapter(
settings=OpenAIClientSettings(
base_url="https://api.openai.com/v1",
api_key="...",
),
model="gpt-4.1-mini",
debug_json_enabled=True,
)
This keeps third-party callers on a stable, provider-neutral LlmAdapter API while hiding provider assembly details.
Multimodal image and file input
The shared adapter supports image URLs, local image file paths, and supported local document/spreadsheet files as part of a Responses API input through generate_text_from_input(...).
from llmframe import (
LlmFileInputPart,
LlmImageFileInputPart,
LlmImageUrlInputPart,
LlmTextInputPart,
OpenAIClientSettings,
build_openai_llm_adapter,
)
adapter = build_openai_llm_adapter(
settings=OpenAIClientSettings(
base_url="https://api.openai.com/v1",
api_key="...",
),
model="gpt-4.1-mini",
)
result = adapter.generate_text_from_input(
developer_prompt="You are a concise vision assistant.",
user_input_parts=[
LlmTextInputPart(text="Describe the image in one sentence."),
LlmImageUrlInputPart(url="https://example.com/image.png"),
],
)
local_result = adapter.generate_text_from_input(
developer_prompt="You are a concise vision assistant.",
user_input_parts=[
LlmTextInputPart(text="Describe the image in one sentence."),
LlmImageFileInputPart(path="examples/cat.png"),
],
)
file_result = adapter.generate_text_from_input(
developer_prompt="You summarize attached files.",
user_input_parts=[
LlmTextInputPart(text="Summarize this file in one sentence."),
LlmFileInputPart(path="examples/brief.pdf"),
],
)
For local images, the adapter reads the file at the adapter boundary, infers an image MIME type from the filename, and sends it as an inline data URL. For supported non-image files, it reads the file locally and sends it as an input_file content part.
Supported non-image local file extensions:
.pdf.txt,.md,.json,.html,.xml.doc,.docx,.rtf,.odt.ppt,.pptx.csv,.xls,.xlsx
The existing generate_text(...) method remains the simplest text-only convenience API.
OpenAI Responses Batch API
The shared LlmAdapter also supports OpenAI's asynchronous Batch API for the Responses endpoint. This preserves the synchronous generate_text() and extract_json() methods while adding separate batch submission and retrieval methods for lower-cost bulk execution.
Example plain-text batch submission:
from llmframe import LlmBatchTextRequest, OpenAIClientSettings, build_openai_llm_adapter
adapter = build_openai_llm_adapter(
settings=OpenAIClientSettings(
base_url="https://api.openai.com/v1",
api_key="...",
),
model="gpt-4.1-mini",
)
submission = adapter.submit_text_batch(
requests=[
LlmBatchTextRequest(
custom_id="item-1",
developer_prompt="You are a concise assistant.",
user_prompt="Summarize this document.",
)
]
)
status = adapter.get_batch_status(batch_id=submission.batch_id)
Once the batch completes, callers can retrieve parsed plain-text or structured results with get_text_batch_result() or get_structured_batch_result(). Execution is asynchronous and OpenAI-specific under the hood, but it remains exposed through the same shared adapter package.
Submitted batch metadata is also persisted by default to artifacts/llm-batches, with one JSON record per batch ID. This makes batch IDs durable across process restarts so callers can reload a previously submitted batch ID and continue polling or fetching results later.
To override the batch metadata storage location:
from pathlib import Path
from llmframe import OpenAIClientSettings, build_openai_llm_adapter
adapter = build_openai_llm_adapter(
settings=OpenAIClientSettings(
base_url="https://api.openai.com/v1",
api_key="...",
),
model="gpt-4.1-mini",
batch_request_output_dir=Path("custom/batch-dir"),
)
If you need custom persistence behavior, pass your own implementation of the application-layer BatchRequestStorePort to build_openai_llm_adapter().
Debug JSON artifacts
When debug_json_enabled=True, the factory automatically creates a JsonFileWriterAdapter and writes formatted request and response snapshots to artifacts/llm-debug.
To override the output location:
from pathlib import Path
from llmframe import OpenAIClientSettings, build_openai_llm_adapter
adapter = build_openai_llm_adapter(
settings=OpenAIClientSettings(
base_url="https://api.openai.com/v1",
api_key="...",
),
model="gpt-4.1-mini",
debug_json_enabled=True,
debug_json_output_dir=Path("custom/debug-dir"),
)
The shared LLM adapter depends on the application-layer JsonArtifactWriterPort, while the factory wires in the filesystem-backed JsonFileWriterAdapter by default for this convenience path.
Compliance notes
This repository follows the active .clinerules profile for Python hexagonal projects.
The OpenAI transport keeps OpenAIClient as the public import while delegating retry, debug payload, and batch file I/O helpers to focused transport-internal modules.
On-demand live integration tests
The repository also includes opt-in live integration tests for the main OpenAI-backed flows:
- single-request text generation
- single-request image-input text generation
- single-request structured JSON extraction
- batch submission plus status/result retrieval
These tests are intentionally excluded from normal development runs and run only when you opt in with environment variables.
Required environment variables:
LLMFRAME_RUN_ON_DEMAND_INTEGRATION=1OPENAI_API_KEYorLLMFRAME_OPENAI_API_KEY
Optional environment variables:
LLMFRAME_OPENAI_BASE_URL(defaults tohttps://api.openai.com/v1)LLMFRAME_OPENAI_MODEL(defaults togpt-4.1-nano)LLMFRAME_BATCH_WAIT_TIMEOUT_SECONDS(defaults to120)LLMFRAME_BATCH_POLL_INTERVAL_SECONDS(defaults to5)
Run only the on-demand live suite with:
uv run pytest -m "integration and on_demand" tests/integration/openai_live
Run only the image-input live test with:
LLMFRAME_RUN_ON_DEMAND_INTEGRATION=1 OPENAI_API_KEY=... uv run pytest -m "integration and on_demand" tests/integration/openai_live/test_multimodal_input.py
Those tests use tiny hosted, inline, and local image inputs to keep requests cheap while exercising the supported image-input paths.
For the live batch workflow, the submission test persists batch metadata under artifacts/llm-batches. The retrieval test can then read a previously submitted batch either from the newest persisted record or from an explicit batch ID provided via LLMFRAME_TEST_BATCH_ID.
Useful live batch commands:
LLMFRAME_RUN_ON_DEMAND_INTEGRATION=1 OPENAI_API_KEY=... uv run pytest -m "integration and on_demand" tests/integration/openai_live/test_batch_submission.py
LLMFRAME_RUN_ON_DEMAND_INTEGRATION=1 OPENAI_API_KEY=... uv run pytest -m "integration and on_demand" tests/integration/openai_live/test_batch_result_retrieval.py
These tests use short prompts and tiny expected outputs to keep token usage minimal.
Manual GitHub Actions live integration workflow
Maintainers can also run the on-demand OpenAI live suite from GitHub Actions with the manual workflow at .github/workflows/integration_openai_live.yaml.
Before using it, configure the repository secret:
OPENAI_API_KEY
The workflow exposes workflow_dispatch inputs for:
target- chooseall,text_generation,structured_extraction,batch_submission, orbatch_result_retrievalpython_version- choose the Python runtime for the runmodelandbase_url- optional OpenAI configuration overridesbatch_id- optional explicit batch ID for retrieval runsbatch_wait_timeout_secondsandbatch_poll_interval_seconds- optional batch polling controls
For retrieval-only runs, provide batch_id unless the job environment already has access to previously persisted batch metadata. In GitHub Actions, an explicit batch ID is the reliable option because workflow runs do not share local artifacts by default.
Metadata
Release files for llmframe 3.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| llmframe-3.1.0.tar.gz | 194.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| llmframe-3.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 252.4 kB
Release files / llmframe-3.1.0.tar.gz
| Download URL | llmframe-3.1.0.tar.gz |
|---|---|
| Size | 194.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f2bbeb89d7e70e77b6fa6487c166f419bd8f5c9561373192febf341a10a204fb
|
|
BLAKE2b-256 checksum How to use checksums |
bec465212dcb84a2b129b5a7e20dd86a370ac4836d21fa9918fd67ac474192ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
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 May 25, 2026.
Transparency logRelease files / llmframe-3.1.0-py3-none-any.whl
| Download URL | llmframe-3.1.0-py3-none-any.whl |
|---|---|
| Size | 58.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8cf734fc727f32c467056df4f2e89dd7d0de5d129f085457e98c232dcc51dfb4
|
|
BLAKE2b-256 checksum How to use checksums |
3c46bc0b0c38bca8e827d6542b03e8f1a3a45977cb9582e5ecd85f4eee46a933
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
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 May 25, 2026.
Transparency log