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.2.1
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.
ChatGPT plan (SIWC)
pip install -U 'aq-llm-pivot[siwc]==0.2.1'
llm-pivot-siwc --config siwc_config.json login --smoke
llm-pivot-siwc --config siwc_config.json models
Create siwc_config.json:
{
"api_type": "siwc",
"model_id": "gpt-6.1-sol",
"timeout": 120,
"enable_log": false,
"siwc": {
"app_name": "My Research",
"credentials_dir": null,
"login_timeout": 300,
"refresh_margin": 60,
"max_concurrency": 8
}
}
Authorization and inference read the same configuration. Set your application's name and use a model available to the authorized account.
from llmpivot import LLMPivot, PivotConfig, SIWCAuth
config = PivotConfig.from_file("siwc_config.json")
SIWCAuth(config.siwc).login()
with LLMPivot(config) as llm:
result = llm.generate([{"role": "user", "content": "Hello"}])
print(result["content"])
Use either the login command or SIWCAuth.login() once during setup; subsequent
calls refresh credentials automatically. No API key or base URL is needed.
import asyncio
from llmpivot import LLMPivot, PivotConfig
async def run(prompts):
with LLMPivot(PivotConfig.from_file("siwc_config.json")) as llm:
return await asyncio.gather(*[
llm.agenerate([{"role": "user", "content": prompt}])
for prompt in prompts
])
siwc.max_concurrency limits requests per instance. Sync calls support thread
pools; agenerate()/adialogue() use worker threads. Multiple processes share
locked, rotating credentials. Each result includes its own usage.
Credentials default to ~/.config/llm-pivot/siwc/<app-id>/ on macOS/Linux
(or XDG_CONFIG_HOME). Experiments in one authorized research application share
one directory. To reuse an existing login, set siwc.credentials_dir to its
original directory; LLM_PIVOT_SIWC_DIR is also supported. Keep credentials private.
SIWC uses streaming Responses, store=false, and verifies completion. It disables
automatic retries and rejects unsupported parameters such as temperature,
max_tokens and previous_response_id. Embedding/perplexity are unavailable.
Function tools are grouped in namespaces; tool_choice accepts auto, required
or none. See preview limits.
Manage access at ChatGPT Usage. Plan eligibility
and SIWC terms apply.
Anthropic Messages API
Set api_type="anthropic" to talk to Anthropic-protocol endpoints
(/v1/messages). The public generation, function-calling, and streaming
interfaces remain unchanged.
from llmpivot import LLMPivot, PivotConfig
config = PivotConfig(
model_type="online",
api_type="anthropic",
model_id="GLM-5.3-Flash",
api_key="xxx",
base_url="http://localhost:18400/v1",
)
with LLMPivot(config) as llm:
result = llm.generate([{"role": "user", "content": "Hello"}])
print(result["content"])
Anthropic mode authenticates with a bearer token (Authorization: Bearer) and
performs the following conversions so callers keep using the OpenAI-style
shapes:
| Public parameter / result | Anthropic Messages API equivalent |
|---|---|
system role messages |
top-level system parameter |
max_tokens |
max_tokens (defaults to 4096 when unset) |
| Chat-style function tools | tools with input_schema |
tool role messages |
tool_result content blocks |
Assistant tool_calls |
tool_use content blocks |
stop_reason |
OpenAI-style finish_reason |
usage.input_tokens (+ cache tokens) |
usage.prompt_tokens |
thinking content blocks emitted by reasoning models are skipped: generate()
returns only the text blocks, and streaming yields only text_delta events.
perplexity() and embedding() are not available in Anthropic mode because
the protocol does not provide token log probabilities or an embeddings
endpoint. A trailing /v1 on base_url is stripped automatically, so
OpenAI-style base URLs work unmodified.
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", "responses" or "anthropic" |
| 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
Anthropic Messages API compatibility:
max_tokens(int): Passed through, defaults to 4096 when unsettool_choice(str/dict): "auto"/"none"/"required" and function dicts are mapped- All other parameters are forwarded to the
/v1/messagesendpoint as-is
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.2.1"
Run the tests and build checks, then commit and push the change:
python -m pytest tests/unit
python -m build
python -m twine check dist/*
git add src/llmpivot/__version__.py
git commit -m "release: v0.2.1"
git push origin main
Create and push the matching annotated tag:
git tag -a v0.2.1 -m "Release v0.2.1"
git push origin v0.2.1
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.
Metadata
Release files for aq-llm-pivot 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aq_llm_pivot-0.2.1.tar.gz | 31.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aq_llm_pivot-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 71.1 kB
Release files / aq_llm_pivot-0.2.1.tar.gz
| Download URL | aq_llm_pivot-0.2.1.tar.gz |
|---|---|
| Size | 31.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c2fd598ac2a2d12034216b73e470105075ba53890d332f9d8d7e24c35d66b3fb
|
|
BLAKE2b-256 checksum How to use checksums |
3b43745621a22da49f416e9436b4b0f87526b53c94ebab13d2ee96f1f895b12c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 12, 2026.
Transparency logRelease files / aq_llm_pivot-0.2.1-py3-none-any.whl
| Download URL | aq_llm_pivot-0.2.1-py3-none-any.whl |
|---|---|
| Size | 39.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
178ac9bc90c9948706daa3c285e2c69bfbadc58673a05ac650f17abdc10e8136
|
|
BLAKE2b-256 checksum How to use checksums |
2ab8bcd31a1d4329dc1541ea2c33d41daae7fd78fdf027b75604457e6f1d19b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 12, 2026.
Transparency log