This release is a pre-release and may not be stable for production use.
Chat Completions Conversation With Tools
LLM tool calling for environments that modern SDKs left behind.
Python 2. Python 3. Windows XP. Embedded CPython. Air-gapped networks. If you can run import json, you can run this package.
Why This Exists
LLM tool calling has become essential — but the ecosystem has raced toward modern stacks. The official OpenAI Python SDK requires Python 3.8+. LangChain wants 3.9+. They pull in httpx, pydantic, anyio, and a tree of native extensions. None of that runs on a Windows XP box or an old Debian embedded controller.
Meanwhile, the API itself hasn't changed: it's still JSON over HTTP, with the same OpenAI Chat Completions function-calling schema that every major provider speaks. The barrier is purely in the client libraries.
This package removes that barrier. It's a single-file, zero-dependency stateful wrapper around any OpenAI Chat Completions-compatible HTTP API. No requests. No httpx. No native code. Just urllib/urllib2, json, and the typing module — all of which ship with Python 2.7 and later.
What It Gives You
| Capability | How |
|---|---|
| Tool definitions | Python TypedDict → JSON Schema (automatic, recursive, cycle-safe) |
| Non-streaming responses | Parse tool_calls[] from the response message |
| Streaming responses | SSE event parser (CRLF, comments, [DONE]) with tool call delta accumulation |
| Conversation history | Automatic message bookkeeping (user, assistant, tool roles) |
| Multimodal | Image URLs via OpenAI content-parts format |
| Providers | OpenAI, DeepSeek, Groq, Together, xAI, OpenRouter — anything that speaks Chat Completions |
Function Calling Convention
This package implements the OpenAI native function calling convention — an API-level protocol where tools and tool calls are first-class fields in the request/response schema. This is the industry standard, supported by OpenAI and every major compatible provider.
Request format
{
"model": "...",
"messages": [...],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Look up the weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"],
"additionalProperties": false
}
}
}
],
"tool_choice": "auto"
}
Response format
{
"choices": [{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Tokyo\"}"
}
}
]
}
}]
}
Tool results
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "Sunny, 22°C"
}
Streaming deltas
When stream: true, tool calls arrive incrementally via SSE chunks keyed by index. The package accumulates fragments and finalizes them when the stream completes.
The stream is parsed as SSE events, not raw lines: \r\n and \n endings, comment lines, data: fields with or without a space, multi-line data blocks, and the data: [DONE] sentinel are all handled. Servers that omit the blank line between JSON events are tolerated, and chunks without a choices entry (for example usage payloads) are skipped.
This is an API-native convention — tools and tool calls are structural fields in the HTTP bodies, not prompt-engineered text that requires client-side regex or XML parsing.
Installation
pip install chat-completions-conversation-with-tools
No native extensions. No system dependencies. Works with pip on Python 2.7 and Python 3.x.
Usage
Actual tool execution is application-defined. Provide a callable per tool, or bridge to an external process — the package only handles the API conversation.
Basic example (OpenAI)
from typing import TypedDict
from chat_completions_conversation_with_tools import (
ChatCompletionsConversationWithTools,
Tool,
)
class WeatherArgs(TypedDict):
city: str
conversation = ChatCompletionsConversationWithTools(
api_key="sk-...",
base_url="https://api.openai.com/v1",
model="gpt-4o",
system_prompt="Call the get_weather tool when asked about weather.",
tools_by_name={
"get_weather": Tool("Look up the current weather for a city.", WeatherArgs),
},
)
# Non-streaming
response = conversation.send_and_receive_response("What's the weather in Tokyo?")
print(response.tool_calls)
# [ToolCall(id='...', name='get_weather', arguments={'city': 'Tokyo'})]
conversation.append_tool_message(response.tool_calls[0].id, "Sunny, 22°C")
final = conversation.send_and_receive_response()
print(final.content)
# "The weather in Tokyo is sunny with a temperature of 22°C."
Streaming (DeepSeek)
conversation = ChatCompletionsConversationWithTools(
api_key="sk-...",
base_url="https://api.deepseek.com",
model="deepseek-chat",
system_prompt="Call the get_weather tool when asked about weather.",
tools_by_name={
"get_weather": Tool("Look up the current weather for a city.", WeatherArgs),
},
)
response = conversation.send_and_stream_response(
text="What's the weather in Tokyo?",
on_content_delta=lambda text: print(text, end="", flush=True),
)
# Let me check the weather in Tokyo for you!
print(response.tool_calls)
# [ToolCall(id='call_00_...', name='get_weather', arguments={'city': 'Tokyo'})]
conversation.append_tool_message(response.tool_calls[0].id, "Sunny, 22°C")
final = conversation.send_and_stream_response(
on_content_delta=lambda text: print(text, end="", flush=True),
)
# The weather in Tokyo is sunny with a temperature of 22°C.
Image input
conversation.append_user_message(
"What's in this image?",
image_url="https://example.com/photo.jpg",
)
response = conversation.send_and_receive_response()
Public API
| Class | Purpose |
|---|---|
Tool(description, typeddict_class) |
Wraps a tool description and a TypedDict parameter schema. Automatically converts the TypedDict to JSON Schema. |
ToolCall |
Stores a parsed tool call: .id, .name, .arguments. |
AssistantResponse |
Stores the assistant turn: .content (text) and .tool_calls (list of ToolCall). |
ChatCompletionsConversationWithTools |
Manages message history and API communication. |
ChatCompletionsConversationWithTools
__init__(api_key, base_url, model, system_prompt, tools_by_name)
| Method | Description |
|---|---|
send_and_receive_response(text=None, image_url=None) |
Send a user message (or continue after tool results) and return the assistant response. |
send_and_stream_response(text=None, image_url=None, on_content_delta=None, on_tool_call_delta=None) |
Same, but streams the response via callbacks. |
append_user_message(text, image_url=None) |
Add a user message to history without sending. |
append_assistant_message(content, tool_calls=None) |
Add an assistant message to history. |
append_tool_message(tool_call_id, content) |
Add a tool result to history. |
to_messages() |
Return a deep, JSON-safe copy of the conversation history. |
reset() |
Clear conversation history and re-insert the system prompt. |
set_system_prompt(prompt, update_messages=True) |
Change the system prompt. |
build_payload(messages, stream=False) |
Build the raw API request payload (for debugging or custom transport). |
Saving and loading conversations
The conversation history is a plain JSON-safe list of messages, so it can be
saved and restored without any custom wire format. Serialization and
deserialization between this list and files or strings is left to the standard
library json module.
import json
from chat_completions_conversation_with_tools import (
ChatCompletionsConversationWithTools,
append_messages,
load_messages,
)
conversation = ChatCompletionsConversationWithTools(
api_key="...", base_url="...", model="...",
system_prompt="...", tools_by_name={},
)
conversation.append_user_message("Hello")
# Save: get a deep, JSON-safe copy of the history...
messages = conversation.to_messages()
# ...and hand it to the json module for file or string I/O.
with open("conversation.json", "w", encoding="utf-8") as handle:
json.dump(messages, handle, indent=2)
# Load: deserialize with json, validate with load_messages...
with open("conversation.json", "r", encoding="utf-8") as handle:
saved = load_messages(json.load(handle))
# ...and append it to a new (or existing) conversation to continue it.
continued = ChatCompletionsConversationWithTools(
api_key="...", base_url="...", model="...",
system_prompt="...", tools_by_name={},
)
append_messages(continued, saved)
The file format is a bare JSON array of API message objects, so a saved transcript can also be used directly as an API request body.
| Function | Description |
|---|---|
conversation.to_messages() |
Deep, JSON-safe copy of the history (mutating it does not affect the conversation). |
load_messages(messages) |
Validate a message list (roles, tool_call_id on tool messages) and return an independent copy. |
append_messages(conversation, messages) |
Append validated messages to a conversation; skips system messages, since the system prompt is configured at construction. Returns the number of messages appended. |
Constraints (By Design)
No requests or httpx. This package uses only urllib/urllib2 from the standard library. It installs and runs anywhere Python does — no native compilation, no system libraries.
Python 2.7 compatible. Every string, every import, every type annotation is written to work on Python 2.7 and Python 3.x from the same source file.
Single file. The entire library is one module. Drop chat_completions_conversation_with_tools.py into any project. No package structure to navigate, no implicit namespace dependencies.
No framework dependencies. No pydantic, no attrs, no dataclasses. Tool parameter schemas use standard TypedDict, automatically converted to JSON Schema at call time.
Provider Compatibility
Tested and working with:
| Provider | Model | Status |
|---|---|---|
| DeepSeek | deepseek-chat |
✅ Non-streaming + streaming |
| OpenAI | gpt-4o, gpt-4.1 |
✅ Compatible API |
| Groq, Together, xAI, OpenRouter | Any Chat Completions model | ✅ Compatible API |
| Local (vLLM, Ollama, LM Studio) | Any tool-capable model | ✅ Compatible API |
License
MIT — see LICENSE.
Release files for chat-completions-conversation-with-tools 0.1.0a3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| chat_completions_conversation_with_tools-0.1.0a3.tar.gz | 15.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| chat_completions_conversation_with_tools-0.1.0a3-py2.py3-none-any.whl | Python 3, Python 2 | none | any | Details |
Total release size: 26.9 kB
Release files / chat_completions_conversation_with_tools-0.1.0a3.tar.gz
| Download URL | chat_completions_conversation_with_tools-0.1.0a3.tar.gz |
|---|---|
| Size | 15.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
27e696d255dfad8bb8a4a2b3e96a89efa15f497510668345756a445b716d6aa6
|
|
BLAKE2b-256 checksum How to use checksums |
8cb81aceffc879efd7c63a2dcb61225c3b6e26c8a58945e61b4cc04116e2df94
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|
Release files / chat_completions_conversation_with_tools-0.1.0a3-py2.py3-none-any.whl
| Download URL | chat_completions_conversation_with_tools-0.1.0a3-py2.py3-none-any.whl |
|---|---|
| Size | 11.6 kB |
| Tags | Python 2 Python 3 |
|
SHA-256 checksum How to use checksums |
3fe710817120a13bdc67b7baf18f7dc8121eda30548b860110bdb015770c28de
|
|
BLAKE2b-256 checksum How to use checksums |
9c9dea1eaa1b7dbe605ca5fea697150f58a57674828118bc0f3800a6654470b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|