LLM Pivot
A unified interface for interacting with both online API-based and local LLM models.
Overview
LLM Pivot provides a consistent API for working with different types of models:
- Online Models: OpenAI and OpenAI-compatible API services
- Local Models: Ollama (LLM), Infinity (Embedding / Reranking), and other locally-hosted models
Quick Start
Installation
Install the latest release from PyPI:
pip install aq-llm-pivot
To install this release explicitly:
pip install aq-llm-pivot==0.1.0
Local Development
# Clone the repository
git clone https://github.com/Aquarids/llm-pivot.git
cd llm-pivot
# Create environment
conda env create -f environment.yml
conda activate llm-pivot
# Install in editable mode
pip install -e .
In your project:
cd /path/to/your/project
conda activate your-project-env
pip install -e /path/to/llm-pivot
Basic Usage
Online LLM API Models (Chat Completions)
from llmpivot import LLMPivot, PivotConfig
# Basic configuration
config = PivotConfig(
model_type="online",
api_type="chat_completions", # Default
model_id="gpt-4",
api_key="sk-xxx",
base_url="https://api.openai.com/v1/"
)
# Use context manager for automatic cleanup
with LLMPivot(config) as llm:
messages = [{"role": "user", "content": "Hello, how are you?"}]
response = llm.dialogue(messages)
print(response)
OpenAI Responses API
Set api_type="responses" to use OpenAI's recommended /v1/responses
endpoint. The public generation, function-calling, and streaming interfaces
remain unchanged.
from llmpivot import LLMPivot, PivotConfig
config = PivotConfig(
model_type="online",
api_type="responses",
model_id="gpt-5.4",
api_key="sk-xxx",
base_url="https://api.openai.com/v1",
)
with LLMPivot(config) as llm:
result = llm.generate([{"role": "user", "content": "Hello"}])
print(result["content"])
print(result["response_id"])
Responses mode provides the following compatibility mappings:
| Public parameter | Responses API parameter |
|---|---|
messages |
input |
max_tokens |
max_output_tokens |
response_format |
text.format |
reasoning_effort |
reasoning.effort |
| Chat-style function tools | Responses-style function tools |
The normalized generate() result keeps the existing LLM Pivot fields and
adds response_id:
{
"content": "Hello!",
"role": "assistant",
"tool_calls": None,
"finish_reason": "completed",
"usage": {
"prompt_tokens": 8,
"completion_tokens": 2,
"total_tokens": 10,
},
"model": "gpt-5.4",
"response_id": "resp_...",
}
To chain stored responses, pass the returned ID on the next request:
first = llm.generate(
[{"role": "user", "content": "What is the capital of New Zealand?"}]
)
second = llm.generate(
[{"role": "user", "content": "What is its population?"}],
previous_response_id=first["response_id"],
)
Responses are stored by default by OpenAI. Pass store=False when stateless
operation is required. perplexity() remains a Chat Completions-only operation
because it requires token log probabilities.
See the official OpenAI Responses migration guide.
Local LLM Models (Ollama)
from llmpivot import LLMPivot, PivotConfig
config = PivotConfig(
model_type="local",
model_id="qwen2.5:7b",
base_url="http://localhost:11434"
)
with LLMPivot(config) as llm:
messages = [{"role": "user", "content": "What is machine learning?"}]
response = llm.dialogue(messages)
print(response)
Embedding Models
from llmpivot import EmbedPivot, EmbedConfig
config = EmbedConfig(
model_type="online",
model_id="Qwen3-Embedding-0.6B",
api_key="sk-xxx",
base_url="https://api.example.com"
)
with EmbedPivot(config) as embed:
vector = embed.embedding("Hello, world!")
print(len(vector)) # Vector dimension
vectors = embed.embedding(["Hello", "World"])
print(len(vectors)) # 2
Rerank Models
from llmpivot import RerankPivot, RerankConfig
config = RerankConfig(
model_type="online",
model_id="bce-reranker-base_v1",
api_key="sk-xxx",
base_url="https://api.example.com"
)
with RerankPivot(config) as rerank:
documents = [
"Python is a programming language.",
"The Eiffel Tower is in Paris.",
"Machine learning is a subset of AI.",
]
results = rerank.rerank_documents("What is machine learning?", documents)
print(results)
# [{"index": 2, "text": "...", "relevance_score": 0.98}, ...]
scores = rerank.similarity("AI includes machine learning.", documents)
print(scores) # [0.72, 0.11, 0.95]
Advanced Usage
Custom Default Parameters
Set default parameters for all LLM calls:
config = PivotConfig(
model_type="online",
model_id="gpt-4",
api_key="sk-xxx",
llm_default_params={
"temperature": 0.3,
"max_tokens": 1000,
"top_p": 0.9,
}
)
llm = LLMPivot(config)
# Uses default parameters (temperature=0.3, max_tokens=1000, top_p=0.9)
response = llm.generate(messages=[{"role": "user", "content": "Hello"}])
Override Parameters at Runtime
# Override specific parameters for this call
response = llm.generate(
messages=[{"role": "user", "content": "Hello"}],
temperature=0.8,
max_tokens=2000,
)
# Use streaming internally (still returns complete string)
response = llm.dialogue(
messages=[{"role": "user", "content": "Explain quantum computing"}],
stream=True,
temperature=0.7
)
print(response)
Streaming Response
messages = [{"role": "user", "content": "Write a short story"}]
# Stream generation for chunk-by-chunk processing
for chunk in llm.stream_generate(messages, temperature=0.7):
print(chunk, end='', flush=True)
Function Calling
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
}
]
messages = [{"role": "user", "content": "What's the weather in Beijing?"}]
tool_calls = llm.call_function(messages, tools)
print(tool_calls)
# Output: [{"id": "call_xxx", "name": "get_weather", "arguments": '{"city": "Beijing"}'}]
Provider-Specific Features
DeepSeek Thinking Mode:
response = llm.generate(
messages=[{"role": "user", "content": "Solve this complex problem"}],
extra_body={"thinking": {"type": "enabled"}}
)
JSON Mode:
response = llm.generate(
messages=[{"role": "user", "content": "Generate a JSON object"}],
response_format={"type": "json_object"}
)
Ollama-Specific Parameters:
config = PivotConfig(
model_type="local",
model_id="llama3",
llm_default_params={
"temperature": 0.7,
"top_k": 40,
"repeat_penalty": 1.1,
"num_ctx": 4096,
}
)
Embeddings
# Single text
embedding = llm.embedding("Hello world")
print(len(embedding)) # Vector dimension
# Multiple texts
texts = ["Hello", "World", "AI"]
embeddings = llm.embedding(texts)
print(len(embeddings)) # 3
print(len(embeddings[0])) # Vector dimension
Configuration
PivotConfig Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| model_type | str | "online" | "online" or "local" |
| api_type | str | "chat_completions" | Online generation API: "chat_completions" or "responses" |
| model_id | str | "gpt-4" | Model identifier |
| api_key | str | None | API key for online models |
| base_url | str | None | API base URL or Ollama host |
| timeout | int | 60 | Request timeout in seconds |
| max_retries | int | 1 | Maximum retry attempts |
| llm_default_params | dict | {} | Default parameters for LLM calls |
| log_env | str | "local" | Log environment identifier |
| enable_log | bool | True | Enable logging |
| log_level | str | None | Log level (DEBUG, INFO, WARNING, ERROR) |
EmbedConfig Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| model_type | str | "online" | "online" or "local" |
| model_id | str | "Qwen3-Embedding-0.6B" | Model identifier |
| api_key | str | None | API key for online models |
| base_url | str | None | API base URL or Infinity host |
| timeout | int | 60 | Request timeout in seconds |
| max_retries | int | 1 | Maximum retry attempts |
| embed_default_params | dict | {"encoding_format": "float"} | Default parameters for embedding calls |
| log_env | str | "local" | Log environment identifier |
| enable_log | bool | True | Enable logging |
| log_level | str | None | Log level (DEBUG, INFO, WARNING, ERROR) |
RerankConfig Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| model_type | str | "online" | "online" or "local" |
| model_id | str | "bce-reranker-base_v1" | Model identifier |
| api_key | str | None | API key for online models |
| base_url | str | None | API base URL or Infinity host |
| timeout | int | 60 | Request timeout in seconds |
| max_retries | int | 1 | Maximum retry attempts |
| rerank_default_params | dict | {"top_n": 5, "normalize": true} | Default parameters for rerank calls |
| log_env | str | "local" | Log environment identifier |
| enable_log | bool | True | Enable logging |
| log_level | str | None | Log level (DEBUG, INFO, WARNING, ERROR) |
Common LLM Parameters (via llm_default_params or kwargs)
Universal Parameters:
temperature(float): Sampling temperature (0.0-2.0)max_tokens(int): Maximum tokens to generatetop_p(float): Nucleus sampling threshold
Online API (OpenAI-compatible):
frequency_penalty(float): Frequency penalty (-2.0 to 2.0)presence_penalty(float): Presence penalty (-2.0 to 2.0)response_format(dict): Response format specificationextra_body(dict): Provider-specific parameters
Responses API compatibility:
previous_response_id(str): Chain a request to an earlier stored responsestore(bool): Enable or disable response storagereasoning_effort(str): Mapped toreasoning.effortresponse_format(dict): Mapped totext.formatmax_tokens(int): Mapped tomax_output_tokens
Local (Ollama):
top_k(int): Top-k samplingrepeat_penalty(float): Repetition penaltynum_ctx(int): Context window sizeseed(int): Random seed for reproducibility
API Reference
Core Methods
# Generate with full response details
response = llm.generate(messages, tools=None, **kwargs)
# Returns: {"content": str, "role": str, "tool_calls": list, "usage": dict, ...}
# Simple dialogue (returns only content string)
content = llm.dialogue(messages, stream=False, **kwargs)
# Returns: str
# Dialogue with streaming API (uses streaming internally, returns complete string)
content = llm.dialogue(messages, stream=True, **kwargs)
# Returns: str
# Dialogue with content and usage details
response = llm.dialogue_with_usage(messages, stream=True, **kwargs)
# Returns: {"content": str, "usage": dict | None}
# Function calling
tool_calls = llm.call_function(messages, tools, **kwargs)
# Returns: [{"id": str, "name": str, "arguments": str}, ...]
# Streaming generation
for chunk in llm.stream_generate(messages, tools=None, **kwargs):
print(chunk) # str
# Embeddings
embedding = llm.embedding(text, **kwargs)
# Returns: List[float] or List[List[float]]
# Perplexity calculation
ppl = llm.perplexity(text, **kwargs)
# Returns: float (Chat Completions only)
Development
Configuration File
Create tests/local_config.ini:
[online]
model_id = gpt-5.4
api_key = sk-your-api-key
base_url = https://api.openai.com/v1
api_type = responses
enable_log = false
[local]
model_id = qwen2.5:7b
base_url = http://localhost:11434
enable_log = false
[online_embed]
model_id = Qwen3-Embedding-0.6B
api_key = sk-your-api-key
base_url = https://api.example.com
enable_log = false
[local_embed]
model_id = Qwen3-Embedding-0.6B
base_url = http://localhost:7997
enable_log = false
[online_rerank]
model_id = bce-reranker-base_v1
api_key = sk-your-api-key
base_url = https://api.example.com
enable_log = false
[local_rerank]
model_id = bce-reranker-base_v1
base_url = http://localhost:7997
enable_log = false
Running Tests
Local build in editable mode
pip install -e .
Then run tests
# All tests
pytest
# Only API tests
pytest -m api
# Only local model tests
pytest -m local
# Skip integration tests
pytest -m "not integration"
# Verbose output
pytest -v
Release Guide
Releases are built and uploaded to PyPI by
.github/workflows/release.yml using PyPI Trusted Publishing. The Git tag must
exactly match the version in src/llmpivot/__version__.py with a leading v.
One-time PyPI Setup
- Create a
pypienvironment in the GitHub repository settings. - In PyPI's publishing settings, add a pending GitHub publisher with project
name
aq-llm-pivot, ownerAquarids, repositoryllm-pivot, workflowrelease.yml, and environmentpypi.
No PyPI API token or GitHub secret is required.
Publish a Release
Update the single version source:
# src/llmpivot/__version__.py
__version__ = "0.1.0"
Run the tests and build checks, then commit and push the change:
python -m pytest tests/unit/test_responses_api.py
python -m build
python -m twine check dist/*
git add src/llmpivot/__version__.py
git commit -m "release: v0.1.0"
git push origin main
Create and push the matching annotated tag:
git tag -a v0.1.0 -m "Release v0.1.0"
git push origin v0.1.0
The workflow validates the version/tag match and publishes the wheel and source distribution. A version already uploaded to PyPI cannot be overwritten; publish a new version if a release needs correction.
Examples
Multi-turn Conversation
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is Python?"},
]
response1 = llm.dialogue(messages)
messages.append({"role": "assistant", "content": response1})
messages.append({"role": "user", "content": "Show me an example"})
response2 = llm.dialogue(messages)
print(response2)
Batch Processing
questions = [
"What is AI?",
"What is ML?",
"What is DL?"
]
for question in questions:
response = llm.dialogue([{"role": "user", "content": question}])
print(f"Q: {question}\nA: {response}\n")
Error Handling
from llmpivot import LLMPivot, PivotConfig
config = PivotConfig(
model_type="online",
model_id="gpt-4",
api_key="sk-xxx",
max_retries=3,
timeout=30
)
try:
with LLMPivot(config) as llm:
response = llm.dialogue([{"role": "user", "content": "Hello"}])
print(response)
except Exception as e:
print(f"Error: {e}")
License
LLM Pivot is released under the MIT License.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aq_llm_pivot-0.1.0.tar.gz.
File metadata
- Download URL: aq_llm_pivot-0.1.0.tar.gz
- Upload date:
- Size: 19.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
875f27fc143ba3646c01d4cf5a4611e2d549aecc5c4525ae3229071b7e62d529
|
|
| MD5 |
01142141200fbf06a47b6d1ebdcb3f83
|
|
| BLAKE2b-256 |
27e520c416a7e36c50410fa7f60fb815ea698eecb2f0fb5d44970b5ca62f2637
|
Provenance
The following attestation bundles were made for aq_llm_pivot-0.1.0.tar.gz:
Publisher:
release.yml on Aquarids/llm-pivot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aq_llm_pivot-0.1.0.tar.gz -
Subject digest:
875f27fc143ba3646c01d4cf5a4611e2d549aecc5c4525ae3229071b7e62d529 - Sigstore transparency entry: 2442927232
- Sigstore integration time:
-
Permalink:
Aquarids/llm-pivot@78c0f5fb9af4349eba421f7aab8a72d2417fce04 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Aquarids
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@78c0f5fb9af4349eba421f7aab8a72d2417fce04 -
Trigger Event:
push
-
Statement type:
File details
Details for the file aq_llm_pivot-0.1.0-py3-none-any.whl.
File metadata
- Download URL: aq_llm_pivot-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3ae7c546f70d8b37b8e03e341260b8db16eef75a891b0eb3729799b62a6934e
|
|
| MD5 |
a4a9f4cb02f0823220b0dfb82eb3583c
|
|
| BLAKE2b-256 |
cec74a1897f5c6d36b8c3dfa91ec05d7e7b416c04d59a713b451017064898a57
|
Provenance
The following attestation bundles were made for aq_llm_pivot-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Aquarids/llm-pivot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aq_llm_pivot-0.1.0-py3-none-any.whl -
Subject digest:
e3ae7c546f70d8b37b8e03e341260b8db16eef75a891b0eb3729799b62a6934e - Sigstore transparency entry: 2442927868
- Sigstore integration time:
-
Permalink:
Aquarids/llm-pivot@78c0f5fb9af4349eba421f7aab8a72d2417fce04 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Aquarids
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@78c0f5fb9af4349eba421f7aab8a72d2417fce04 -
Trigger Event:
push
-
Statement type: