casual-llm 0.4.1__tar.gz → 0.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {casual_llm-0.4.1/src/casual_llm.egg-info → casual_llm-0.4.3}/PKG-INFO +191 -9
- {casual_llm-0.4.1 → casual_llm-0.4.3}/README.md +190 -8
- {casual_llm-0.4.1 → casual_llm-0.4.3}/pyproject.toml +1 -1
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/__init__.py +1 -1
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/tools.py +8 -2
- {casual_llm-0.4.1 → casual_llm-0.4.3/src/casual_llm.egg-info}/PKG-INFO +191 -9
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_tools.py +64 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/LICENSE +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/setup.cfg +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/config.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/message_converters/__init__.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/message_converters/anthropic.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/message_converters/ollama.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/message_converters/openai.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/messages.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/providers/__init__.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/providers/anthropic.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/providers/base.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/providers/ollama.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/providers/openai.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/py.typed +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/tool_converters/__init__.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/tool_converters/anthropic.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/tool_converters/ollama.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/tool_converters/openai.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/usage.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/utils/__init__.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm/utils/image.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm.egg-info/SOURCES.txt +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm.egg-info/dependency_links.txt +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm.egg-info/requires.txt +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/src/casual_llm.egg-info/top_level.txt +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_anthropic_provider.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_backward_compatibility.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_image_utils.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_messages.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_providers.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_vision_integration.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_vision_ollama.py +0 -0
- {casual_llm-0.4.1 → casual_llm-0.4.3}/tests/test_vision_openai.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casual-llm
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Lightweight LLM provider abstraction with standardized message models
|
|
5
5
|
Author-email: Alex Stansfield <alex@casualgenius.com>
|
|
6
6
|
License: MIT
|
|
@@ -43,13 +43,15 @@ Part of the [casual-*](https://github.com/AlexStansfield/casual-mcp) ecosystem o
|
|
|
43
43
|
## Features
|
|
44
44
|
|
|
45
45
|
- 🎯 **Protocol-based** - Uses `typing.Protocol`, no inheritance required
|
|
46
|
-
- 🔌 **
|
|
47
|
-
- 📦 **Lightweight** - Minimal dependencies (pydantic, ollama)
|
|
46
|
+
- 🔌 **Multi-provider** - Works with OpenAI, Anthropic (Claude), Ollama, or your custom provider
|
|
47
|
+
- 📦 **Lightweight** - Minimal dependencies (pydantic, ollama, httpx)
|
|
48
48
|
- 🔄 **Async-first** - Built for modern async Python
|
|
49
49
|
- 🛡️ **Type-safe** - Full type hints with py.typed marker
|
|
50
50
|
- 📊 **OpenAI-compatible** - Standard message format used across the industry
|
|
51
51
|
- 🔧 **Tool calling** - First-class support for function/tool calling
|
|
52
52
|
- 📈 **Usage tracking** - Track token usage for cost monitoring
|
|
53
|
+
- 🖼️ **Vision support** - Send images to vision-capable models
|
|
54
|
+
- ⚡ **Streaming** - Stream responses in real-time with `AsyncIterator`
|
|
53
55
|
|
|
54
56
|
## Installation
|
|
55
57
|
|
|
@@ -60,11 +62,18 @@ uv add casual-llm
|
|
|
60
62
|
# With OpenAI support
|
|
61
63
|
uv add casual-llm[openai]
|
|
62
64
|
|
|
65
|
+
# With Anthropic (Claude) support
|
|
66
|
+
uv add casual-llm[anthropic]
|
|
67
|
+
|
|
68
|
+
# With all providers
|
|
69
|
+
uv add casual-llm[openai,anthropic]
|
|
70
|
+
|
|
63
71
|
# Development dependencies
|
|
64
72
|
uv add casual-llm[dev]
|
|
65
73
|
|
|
66
74
|
# Or using pip
|
|
67
75
|
pip install casual-llm
|
|
76
|
+
pip install casual-llm[openai,anthropic]
|
|
68
77
|
```
|
|
69
78
|
|
|
70
79
|
## Quick Start
|
|
@@ -122,6 +131,32 @@ if usage:
|
|
|
122
131
|
print(f"Total tokens: {usage.total_tokens}")
|
|
123
132
|
```
|
|
124
133
|
|
|
134
|
+
### Using Anthropic (Claude)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from casual_llm import create_provider, ModelConfig, Provider, UserMessage
|
|
138
|
+
|
|
139
|
+
# Create Anthropic provider
|
|
140
|
+
config = ModelConfig(
|
|
141
|
+
name="claude-3-5-sonnet-20241022",
|
|
142
|
+
provider=Provider.ANTHROPIC,
|
|
143
|
+
api_key="sk-ant-...", # or set ANTHROPIC_API_KEY env var
|
|
144
|
+
temperature=0.7
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
provider = create_provider(config)
|
|
148
|
+
|
|
149
|
+
# Generate response
|
|
150
|
+
messages = [UserMessage(content="Explain quantum computing in one sentence.")]
|
|
151
|
+
response = await provider.chat(messages, response_format="text")
|
|
152
|
+
print(response.content)
|
|
153
|
+
|
|
154
|
+
# Check token usage
|
|
155
|
+
usage = provider.get_usage()
|
|
156
|
+
if usage:
|
|
157
|
+
print(f"Total tokens: {usage.total_tokens}")
|
|
158
|
+
```
|
|
159
|
+
|
|
125
160
|
### Using OpenAI-Compatible APIs (OpenRouter, LM Studio, etc.)
|
|
126
161
|
|
|
127
162
|
```python
|
|
@@ -136,6 +171,107 @@ config = ModelConfig(
|
|
|
136
171
|
provider = create_provider(config)
|
|
137
172
|
```
|
|
138
173
|
|
|
174
|
+
### Vision Support
|
|
175
|
+
|
|
176
|
+
Send images to vision-capable models (GPT-4o, Claude 3.5 Sonnet, llava):
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from casual_llm import (
|
|
180
|
+
create_provider,
|
|
181
|
+
ModelConfig,
|
|
182
|
+
Provider,
|
|
183
|
+
UserMessage,
|
|
184
|
+
TextContent,
|
|
185
|
+
ImageContent,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Works with OpenAI, Anthropic, and Ollama
|
|
189
|
+
config = ModelConfig(
|
|
190
|
+
name="gpt-4o", # or "claude-3-5-sonnet-20241022" or "llava"
|
|
191
|
+
provider=Provider.OPENAI,
|
|
192
|
+
api_key="sk-...",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
provider = create_provider(config)
|
|
196
|
+
|
|
197
|
+
# Send an image URL
|
|
198
|
+
messages = [
|
|
199
|
+
UserMessage(
|
|
200
|
+
content=[
|
|
201
|
+
TextContent(text="What's in this image?"),
|
|
202
|
+
ImageContent(source="https://example.com/image.jpg"),
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
response = await provider.chat(messages)
|
|
208
|
+
print(response.content) # "I see a cat sitting on a windowsill..."
|
|
209
|
+
|
|
210
|
+
# Or send a base64-encoded image
|
|
211
|
+
import base64
|
|
212
|
+
|
|
213
|
+
with open("image.jpg", "rb") as f:
|
|
214
|
+
image_data = base64.b64encode(f.read()).decode("ascii")
|
|
215
|
+
|
|
216
|
+
messages = [
|
|
217
|
+
UserMessage(
|
|
218
|
+
content=[
|
|
219
|
+
TextContent(text="Describe this image"),
|
|
220
|
+
ImageContent(
|
|
221
|
+
source={"type": "base64", "data": image_data},
|
|
222
|
+
media_type="image/jpeg",
|
|
223
|
+
),
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
response = await provider.chat(messages)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Streaming Responses
|
|
232
|
+
|
|
233
|
+
Stream responses in real-time for better UX:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from casual_llm import create_provider, ModelConfig, Provider, UserMessage
|
|
237
|
+
|
|
238
|
+
config = ModelConfig(
|
|
239
|
+
name="gpt-4o", # Works with all providers
|
|
240
|
+
provider=Provider.OPENAI,
|
|
241
|
+
api_key="sk-...",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
provider = create_provider(config)
|
|
245
|
+
|
|
246
|
+
messages = [UserMessage(content="Write a short poem about coding.")]
|
|
247
|
+
|
|
248
|
+
# Stream the response
|
|
249
|
+
async for chunk in provider.stream(messages):
|
|
250
|
+
if chunk.content:
|
|
251
|
+
print(chunk.content, end="", flush=True)
|
|
252
|
+
|
|
253
|
+
print() # New line after streaming
|
|
254
|
+
|
|
255
|
+
# Check usage after streaming
|
|
256
|
+
usage = provider.get_usage()
|
|
257
|
+
if usage:
|
|
258
|
+
print(f"\nTokens used: {usage.total_tokens}")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Examples
|
|
262
|
+
|
|
263
|
+
Looking for more examples? Check out the [`examples/`](examples) directory for comprehensive demonstrations of all features:
|
|
264
|
+
|
|
265
|
+
- **[`basic_ollama.py`](examples/basic_ollama.py)** - Get started with Ollama (local LLMs)
|
|
266
|
+
- **[`basic_openai.py`](examples/basic_openai.py)** - Use OpenAI API and compatible services
|
|
267
|
+
- **[`basic_anthropic.py`](examples/basic_anthropic.py)** - Work with Claude models
|
|
268
|
+
- **[`vision_example.py`](examples/vision_example.py)** - Send images to vision-capable models
|
|
269
|
+
- **[`stream_example.py`](examples/stream_example.py)** - Stream responses in real-time
|
|
270
|
+
- **[`tool_calling.py`](examples/tool_calling.py)** - Complete tool/function calling workflow
|
|
271
|
+
- **[`message_formatting.py`](examples/message_formatting.py)** - All message types and structures
|
|
272
|
+
|
|
273
|
+
See the **[Examples README](examples/README.md)** for detailed descriptions, requirements, and usage instructions for each example.
|
|
274
|
+
|
|
139
275
|
## Message Models
|
|
140
276
|
|
|
141
277
|
casual-llm provides OpenAI-compatible message models that work with any provider:
|
|
@@ -148,14 +284,24 @@ from casual_llm import (
|
|
|
148
284
|
ToolResultMessage,
|
|
149
285
|
AssistantToolCall,
|
|
150
286
|
ChatMessage, # Type alias for any message type
|
|
287
|
+
TextContent, # For multimodal messages
|
|
288
|
+
ImageContent, # For vision support
|
|
151
289
|
)
|
|
152
290
|
|
|
153
291
|
# System message (sets behavior)
|
|
154
292
|
system_msg = SystemMessage(content="You are a helpful assistant.")
|
|
155
293
|
|
|
156
|
-
# User message
|
|
294
|
+
# User message (simple text)
|
|
157
295
|
user_msg = UserMessage(content="Hello!")
|
|
158
296
|
|
|
297
|
+
# User message (multimodal - text + image)
|
|
298
|
+
vision_msg = UserMessage(
|
|
299
|
+
content=[
|
|
300
|
+
TextContent(text="What's in this image?"),
|
|
301
|
+
ImageContent(source="https://example.com/image.jpg"),
|
|
302
|
+
]
|
|
303
|
+
)
|
|
304
|
+
|
|
159
305
|
# Assistant message (with optional tool calls)
|
|
160
306
|
assistant_msg = AssistantMessage(
|
|
161
307
|
content="I'll help you with that.",
|
|
@@ -186,7 +332,15 @@ messages: list[ChatMessage] = [system_msg, user_msg, assistant_msg, tool_msg]
|
|
|
186
332
|
Implement the `LLMProvider` protocol to add your own provider:
|
|
187
333
|
|
|
188
334
|
```python
|
|
189
|
-
from
|
|
335
|
+
from typing import Literal, AsyncIterator
|
|
336
|
+
from casual_llm import (
|
|
337
|
+
LLMProvider,
|
|
338
|
+
ChatMessage,
|
|
339
|
+
AssistantMessage,
|
|
340
|
+
StreamChunk,
|
|
341
|
+
Tool,
|
|
342
|
+
Usage,
|
|
343
|
+
)
|
|
190
344
|
|
|
191
345
|
class MyCustomProvider:
|
|
192
346
|
"""Custom LLM provider implementation."""
|
|
@@ -194,13 +348,26 @@ class MyCustomProvider:
|
|
|
194
348
|
async def chat(
|
|
195
349
|
self,
|
|
196
350
|
messages: list[ChatMessage],
|
|
197
|
-
response_format: Literal["json", "text"] = "text",
|
|
351
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
198
352
|
max_tokens: int | None = None,
|
|
199
353
|
tools: list[Tool] | None = None,
|
|
354
|
+
temperature: float | None = None,
|
|
200
355
|
) -> AssistantMessage:
|
|
201
356
|
# Your implementation here
|
|
202
357
|
...
|
|
203
358
|
|
|
359
|
+
async def stream(
|
|
360
|
+
self,
|
|
361
|
+
messages: list[ChatMessage],
|
|
362
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
363
|
+
max_tokens: int | None = None,
|
|
364
|
+
tools: list[Tool] | None = None,
|
|
365
|
+
temperature: float | None = None,
|
|
366
|
+
) -> AsyncIterator[StreamChunk]:
|
|
367
|
+
# Your streaming implementation here
|
|
368
|
+
...
|
|
369
|
+
yield StreamChunk(content="chunk", finish_reason=None)
|
|
370
|
+
|
|
204
371
|
def get_usage(self) -> Usage | None:
|
|
205
372
|
"""Return token usage from last call."""
|
|
206
373
|
return self._last_usage
|
|
@@ -246,10 +413,13 @@ Both OpenAI and Ollama providers support usage tracking.
|
|
|
246
413
|
|
|
247
414
|
| Feature | casual-llm | LangChain | litellm |
|
|
248
415
|
|---------|-----------|-----------|---------|
|
|
249
|
-
| **Dependencies** |
|
|
416
|
+
| **Dependencies** | 3 (pydantic, ollama, httpx) | 100+ | 50+ |
|
|
250
417
|
| **Protocol-based** | ✅ | ❌ | ❌ |
|
|
251
418
|
| **Type-safe** | ✅ Full typing | Partial | Partial |
|
|
252
419
|
| **Message models** | ✅ Included | ❌ Separate | ❌ |
|
|
420
|
+
| **Vision support** | ✅ All providers | ✅ | ✅ |
|
|
421
|
+
| **Streaming** | ✅ All providers | ✅ | ✅ |
|
|
422
|
+
| **Providers** | OpenAI, Anthropic, Ollama | Many | Many |
|
|
253
423
|
| **Learning curve** | ⚡ Minutes | 📚 Hours | 📖 Medium |
|
|
254
424
|
| **OpenAI compatible** | ✅ | ✅ | ✅ |
|
|
255
425
|
|
|
@@ -282,11 +452,21 @@ class LLMProvider(Protocol):
|
|
|
282
452
|
async def chat(
|
|
283
453
|
self,
|
|
284
454
|
messages: list[ChatMessage],
|
|
285
|
-
response_format: Literal["json", "text"] = "text",
|
|
455
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
286
456
|
max_tokens: int | None = None,
|
|
287
457
|
tools: list[Tool] | None = None,
|
|
458
|
+
temperature: float | None = None,
|
|
288
459
|
) -> AssistantMessage: ...
|
|
289
460
|
|
|
461
|
+
async def stream(
|
|
462
|
+
self,
|
|
463
|
+
messages: list[ChatMessage],
|
|
464
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
465
|
+
max_tokens: int | None = None,
|
|
466
|
+
tools: list[Tool] | None = None,
|
|
467
|
+
temperature: float | None = None,
|
|
468
|
+
) -> AsyncIterator[StreamChunk]: ...
|
|
469
|
+
|
|
290
470
|
def get_usage(self) -> Usage | None: ...
|
|
291
471
|
```
|
|
292
472
|
|
|
@@ -321,11 +501,13 @@ class Usage(BaseModel):
|
|
|
321
501
|
|
|
322
502
|
All message models are Pydantic `BaseModel` instances with full validation:
|
|
323
503
|
|
|
324
|
-
- `UserMessage(content: str | None)`
|
|
504
|
+
- `UserMessage(content: str | list[TextContent | ImageContent] | None)` - Supports simple text or multimodal content
|
|
325
505
|
- `AssistantMessage(content: str | None, tool_calls: list[AssistantToolCall] | None = None)`
|
|
326
506
|
- `SystemMessage(content: str)`
|
|
327
507
|
- `ToolResultMessage(name: str, tool_call_id: str, content: str)`
|
|
328
508
|
- `ChatMessage` - Type alias for any message type
|
|
509
|
+
- `TextContent(text: str)` - Text block for multimodal messages
|
|
510
|
+
- `ImageContent(source: str | dict, media_type: str | None = None)` - Image block for vision support
|
|
329
511
|
|
|
330
512
|
## Contributing
|
|
331
513
|
|
|
@@ -11,13 +11,15 @@ Part of the [casual-*](https://github.com/AlexStansfield/casual-mcp) ecosystem o
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- 🎯 **Protocol-based** - Uses `typing.Protocol`, no inheritance required
|
|
14
|
-
- 🔌 **
|
|
15
|
-
- 📦 **Lightweight** - Minimal dependencies (pydantic, ollama)
|
|
14
|
+
- 🔌 **Multi-provider** - Works with OpenAI, Anthropic (Claude), Ollama, or your custom provider
|
|
15
|
+
- 📦 **Lightweight** - Minimal dependencies (pydantic, ollama, httpx)
|
|
16
16
|
- 🔄 **Async-first** - Built for modern async Python
|
|
17
17
|
- 🛡️ **Type-safe** - Full type hints with py.typed marker
|
|
18
18
|
- 📊 **OpenAI-compatible** - Standard message format used across the industry
|
|
19
19
|
- 🔧 **Tool calling** - First-class support for function/tool calling
|
|
20
20
|
- 📈 **Usage tracking** - Track token usage for cost monitoring
|
|
21
|
+
- 🖼️ **Vision support** - Send images to vision-capable models
|
|
22
|
+
- ⚡ **Streaming** - Stream responses in real-time with `AsyncIterator`
|
|
21
23
|
|
|
22
24
|
## Installation
|
|
23
25
|
|
|
@@ -28,11 +30,18 @@ uv add casual-llm
|
|
|
28
30
|
# With OpenAI support
|
|
29
31
|
uv add casual-llm[openai]
|
|
30
32
|
|
|
33
|
+
# With Anthropic (Claude) support
|
|
34
|
+
uv add casual-llm[anthropic]
|
|
35
|
+
|
|
36
|
+
# With all providers
|
|
37
|
+
uv add casual-llm[openai,anthropic]
|
|
38
|
+
|
|
31
39
|
# Development dependencies
|
|
32
40
|
uv add casual-llm[dev]
|
|
33
41
|
|
|
34
42
|
# Or using pip
|
|
35
43
|
pip install casual-llm
|
|
44
|
+
pip install casual-llm[openai,anthropic]
|
|
36
45
|
```
|
|
37
46
|
|
|
38
47
|
## Quick Start
|
|
@@ -90,6 +99,32 @@ if usage:
|
|
|
90
99
|
print(f"Total tokens: {usage.total_tokens}")
|
|
91
100
|
```
|
|
92
101
|
|
|
102
|
+
### Using Anthropic (Claude)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from casual_llm import create_provider, ModelConfig, Provider, UserMessage
|
|
106
|
+
|
|
107
|
+
# Create Anthropic provider
|
|
108
|
+
config = ModelConfig(
|
|
109
|
+
name="claude-3-5-sonnet-20241022",
|
|
110
|
+
provider=Provider.ANTHROPIC,
|
|
111
|
+
api_key="sk-ant-...", # or set ANTHROPIC_API_KEY env var
|
|
112
|
+
temperature=0.7
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
provider = create_provider(config)
|
|
116
|
+
|
|
117
|
+
# Generate response
|
|
118
|
+
messages = [UserMessage(content="Explain quantum computing in one sentence.")]
|
|
119
|
+
response = await provider.chat(messages, response_format="text")
|
|
120
|
+
print(response.content)
|
|
121
|
+
|
|
122
|
+
# Check token usage
|
|
123
|
+
usage = provider.get_usage()
|
|
124
|
+
if usage:
|
|
125
|
+
print(f"Total tokens: {usage.total_tokens}")
|
|
126
|
+
```
|
|
127
|
+
|
|
93
128
|
### Using OpenAI-Compatible APIs (OpenRouter, LM Studio, etc.)
|
|
94
129
|
|
|
95
130
|
```python
|
|
@@ -104,6 +139,107 @@ config = ModelConfig(
|
|
|
104
139
|
provider = create_provider(config)
|
|
105
140
|
```
|
|
106
141
|
|
|
142
|
+
### Vision Support
|
|
143
|
+
|
|
144
|
+
Send images to vision-capable models (GPT-4o, Claude 3.5 Sonnet, llava):
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from casual_llm import (
|
|
148
|
+
create_provider,
|
|
149
|
+
ModelConfig,
|
|
150
|
+
Provider,
|
|
151
|
+
UserMessage,
|
|
152
|
+
TextContent,
|
|
153
|
+
ImageContent,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Works with OpenAI, Anthropic, and Ollama
|
|
157
|
+
config = ModelConfig(
|
|
158
|
+
name="gpt-4o", # or "claude-3-5-sonnet-20241022" or "llava"
|
|
159
|
+
provider=Provider.OPENAI,
|
|
160
|
+
api_key="sk-...",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
provider = create_provider(config)
|
|
164
|
+
|
|
165
|
+
# Send an image URL
|
|
166
|
+
messages = [
|
|
167
|
+
UserMessage(
|
|
168
|
+
content=[
|
|
169
|
+
TextContent(text="What's in this image?"),
|
|
170
|
+
ImageContent(source="https://example.com/image.jpg"),
|
|
171
|
+
]
|
|
172
|
+
)
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
response = await provider.chat(messages)
|
|
176
|
+
print(response.content) # "I see a cat sitting on a windowsill..."
|
|
177
|
+
|
|
178
|
+
# Or send a base64-encoded image
|
|
179
|
+
import base64
|
|
180
|
+
|
|
181
|
+
with open("image.jpg", "rb") as f:
|
|
182
|
+
image_data = base64.b64encode(f.read()).decode("ascii")
|
|
183
|
+
|
|
184
|
+
messages = [
|
|
185
|
+
UserMessage(
|
|
186
|
+
content=[
|
|
187
|
+
TextContent(text="Describe this image"),
|
|
188
|
+
ImageContent(
|
|
189
|
+
source={"type": "base64", "data": image_data},
|
|
190
|
+
media_type="image/jpeg",
|
|
191
|
+
),
|
|
192
|
+
]
|
|
193
|
+
)
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
response = await provider.chat(messages)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Streaming Responses
|
|
200
|
+
|
|
201
|
+
Stream responses in real-time for better UX:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from casual_llm import create_provider, ModelConfig, Provider, UserMessage
|
|
205
|
+
|
|
206
|
+
config = ModelConfig(
|
|
207
|
+
name="gpt-4o", # Works with all providers
|
|
208
|
+
provider=Provider.OPENAI,
|
|
209
|
+
api_key="sk-...",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
provider = create_provider(config)
|
|
213
|
+
|
|
214
|
+
messages = [UserMessage(content="Write a short poem about coding.")]
|
|
215
|
+
|
|
216
|
+
# Stream the response
|
|
217
|
+
async for chunk in provider.stream(messages):
|
|
218
|
+
if chunk.content:
|
|
219
|
+
print(chunk.content, end="", flush=True)
|
|
220
|
+
|
|
221
|
+
print() # New line after streaming
|
|
222
|
+
|
|
223
|
+
# Check usage after streaming
|
|
224
|
+
usage = provider.get_usage()
|
|
225
|
+
if usage:
|
|
226
|
+
print(f"\nTokens used: {usage.total_tokens}")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Examples
|
|
230
|
+
|
|
231
|
+
Looking for more examples? Check out the [`examples/`](examples) directory for comprehensive demonstrations of all features:
|
|
232
|
+
|
|
233
|
+
- **[`basic_ollama.py`](examples/basic_ollama.py)** - Get started with Ollama (local LLMs)
|
|
234
|
+
- **[`basic_openai.py`](examples/basic_openai.py)** - Use OpenAI API and compatible services
|
|
235
|
+
- **[`basic_anthropic.py`](examples/basic_anthropic.py)** - Work with Claude models
|
|
236
|
+
- **[`vision_example.py`](examples/vision_example.py)** - Send images to vision-capable models
|
|
237
|
+
- **[`stream_example.py`](examples/stream_example.py)** - Stream responses in real-time
|
|
238
|
+
- **[`tool_calling.py`](examples/tool_calling.py)** - Complete tool/function calling workflow
|
|
239
|
+
- **[`message_formatting.py`](examples/message_formatting.py)** - All message types and structures
|
|
240
|
+
|
|
241
|
+
See the **[Examples README](examples/README.md)** for detailed descriptions, requirements, and usage instructions for each example.
|
|
242
|
+
|
|
107
243
|
## Message Models
|
|
108
244
|
|
|
109
245
|
casual-llm provides OpenAI-compatible message models that work with any provider:
|
|
@@ -116,14 +252,24 @@ from casual_llm import (
|
|
|
116
252
|
ToolResultMessage,
|
|
117
253
|
AssistantToolCall,
|
|
118
254
|
ChatMessage, # Type alias for any message type
|
|
255
|
+
TextContent, # For multimodal messages
|
|
256
|
+
ImageContent, # For vision support
|
|
119
257
|
)
|
|
120
258
|
|
|
121
259
|
# System message (sets behavior)
|
|
122
260
|
system_msg = SystemMessage(content="You are a helpful assistant.")
|
|
123
261
|
|
|
124
|
-
# User message
|
|
262
|
+
# User message (simple text)
|
|
125
263
|
user_msg = UserMessage(content="Hello!")
|
|
126
264
|
|
|
265
|
+
# User message (multimodal - text + image)
|
|
266
|
+
vision_msg = UserMessage(
|
|
267
|
+
content=[
|
|
268
|
+
TextContent(text="What's in this image?"),
|
|
269
|
+
ImageContent(source="https://example.com/image.jpg"),
|
|
270
|
+
]
|
|
271
|
+
)
|
|
272
|
+
|
|
127
273
|
# Assistant message (with optional tool calls)
|
|
128
274
|
assistant_msg = AssistantMessage(
|
|
129
275
|
content="I'll help you with that.",
|
|
@@ -154,7 +300,15 @@ messages: list[ChatMessage] = [system_msg, user_msg, assistant_msg, tool_msg]
|
|
|
154
300
|
Implement the `LLMProvider` protocol to add your own provider:
|
|
155
301
|
|
|
156
302
|
```python
|
|
157
|
-
from
|
|
303
|
+
from typing import Literal, AsyncIterator
|
|
304
|
+
from casual_llm import (
|
|
305
|
+
LLMProvider,
|
|
306
|
+
ChatMessage,
|
|
307
|
+
AssistantMessage,
|
|
308
|
+
StreamChunk,
|
|
309
|
+
Tool,
|
|
310
|
+
Usage,
|
|
311
|
+
)
|
|
158
312
|
|
|
159
313
|
class MyCustomProvider:
|
|
160
314
|
"""Custom LLM provider implementation."""
|
|
@@ -162,13 +316,26 @@ class MyCustomProvider:
|
|
|
162
316
|
async def chat(
|
|
163
317
|
self,
|
|
164
318
|
messages: list[ChatMessage],
|
|
165
|
-
response_format: Literal["json", "text"] = "text",
|
|
319
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
166
320
|
max_tokens: int | None = None,
|
|
167
321
|
tools: list[Tool] | None = None,
|
|
322
|
+
temperature: float | None = None,
|
|
168
323
|
) -> AssistantMessage:
|
|
169
324
|
# Your implementation here
|
|
170
325
|
...
|
|
171
326
|
|
|
327
|
+
async def stream(
|
|
328
|
+
self,
|
|
329
|
+
messages: list[ChatMessage],
|
|
330
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
331
|
+
max_tokens: int | None = None,
|
|
332
|
+
tools: list[Tool] | None = None,
|
|
333
|
+
temperature: float | None = None,
|
|
334
|
+
) -> AsyncIterator[StreamChunk]:
|
|
335
|
+
# Your streaming implementation here
|
|
336
|
+
...
|
|
337
|
+
yield StreamChunk(content="chunk", finish_reason=None)
|
|
338
|
+
|
|
172
339
|
def get_usage(self) -> Usage | None:
|
|
173
340
|
"""Return token usage from last call."""
|
|
174
341
|
return self._last_usage
|
|
@@ -214,10 +381,13 @@ Both OpenAI and Ollama providers support usage tracking.
|
|
|
214
381
|
|
|
215
382
|
| Feature | casual-llm | LangChain | litellm |
|
|
216
383
|
|---------|-----------|-----------|---------|
|
|
217
|
-
| **Dependencies** |
|
|
384
|
+
| **Dependencies** | 3 (pydantic, ollama, httpx) | 100+ | 50+ |
|
|
218
385
|
| **Protocol-based** | ✅ | ❌ | ❌ |
|
|
219
386
|
| **Type-safe** | ✅ Full typing | Partial | Partial |
|
|
220
387
|
| **Message models** | ✅ Included | ❌ Separate | ❌ |
|
|
388
|
+
| **Vision support** | ✅ All providers | ✅ | ✅ |
|
|
389
|
+
| **Streaming** | ✅ All providers | ✅ | ✅ |
|
|
390
|
+
| **Providers** | OpenAI, Anthropic, Ollama | Many | Many |
|
|
221
391
|
| **Learning curve** | ⚡ Minutes | 📚 Hours | 📖 Medium |
|
|
222
392
|
| **OpenAI compatible** | ✅ | ✅ | ✅ |
|
|
223
393
|
|
|
@@ -250,11 +420,21 @@ class LLMProvider(Protocol):
|
|
|
250
420
|
async def chat(
|
|
251
421
|
self,
|
|
252
422
|
messages: list[ChatMessage],
|
|
253
|
-
response_format: Literal["json", "text"] = "text",
|
|
423
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
254
424
|
max_tokens: int | None = None,
|
|
255
425
|
tools: list[Tool] | None = None,
|
|
426
|
+
temperature: float | None = None,
|
|
256
427
|
) -> AssistantMessage: ...
|
|
257
428
|
|
|
429
|
+
async def stream(
|
|
430
|
+
self,
|
|
431
|
+
messages: list[ChatMessage],
|
|
432
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
433
|
+
max_tokens: int | None = None,
|
|
434
|
+
tools: list[Tool] | None = None,
|
|
435
|
+
temperature: float | None = None,
|
|
436
|
+
) -> AsyncIterator[StreamChunk]: ...
|
|
437
|
+
|
|
258
438
|
def get_usage(self) -> Usage | None: ...
|
|
259
439
|
```
|
|
260
440
|
|
|
@@ -289,11 +469,13 @@ class Usage(BaseModel):
|
|
|
289
469
|
|
|
290
470
|
All message models are Pydantic `BaseModel` instances with full validation:
|
|
291
471
|
|
|
292
|
-
- `UserMessage(content: str | None)`
|
|
472
|
+
- `UserMessage(content: str | list[TextContent | ImageContent] | None)` - Supports simple text or multimodal content
|
|
293
473
|
- `AssistantMessage(content: str | None, tool_calls: list[AssistantToolCall] | None = None)`
|
|
294
474
|
- `SystemMessage(content: str)`
|
|
295
475
|
- `ToolResultMessage(name: str, tool_call_id: str, content: str)`
|
|
296
476
|
- `ChatMessage` - Type alias for any message type
|
|
477
|
+
- `TextContent(text: str)` - Text block for multimodal messages
|
|
478
|
+
- `ImageContent(source: str | dict, media_type: str | None = None)` - Image block for vision support
|
|
297
479
|
|
|
298
480
|
## Contributing
|
|
299
481
|
|
|
@@ -7,7 +7,7 @@ A simple, protocol-based library for working with different LLM providers
|
|
|
7
7
|
Part of the casual-* ecosystem of lightweight AI tools.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.4.
|
|
10
|
+
__version__ = "0.4.3"
|
|
11
11
|
|
|
12
12
|
# Model configuration
|
|
13
13
|
from casual_llm.config import ModelConfig, Provider
|
|
@@ -7,6 +7,7 @@ Provides unified tool models compatible with Ollama, OpenAI, and MCP.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from typing import Any
|
|
10
|
+
|
|
10
11
|
from pydantic import BaseModel, Field
|
|
11
12
|
|
|
12
13
|
|
|
@@ -14,10 +15,13 @@ class ToolParameter(BaseModel):
|
|
|
14
15
|
"""
|
|
15
16
|
A single tool parameter definition following JSON Schema.
|
|
16
17
|
|
|
17
|
-
Supports common JSON Schema fields for describing function parameters
|
|
18
|
+
Supports common JSON Schema fields for describing function parameters,
|
|
19
|
+
including union types via anyOf/oneOf schemas.
|
|
18
20
|
"""
|
|
19
21
|
|
|
20
|
-
type: str
|
|
22
|
+
type: str | None = Field(
|
|
23
|
+
None, description="JSON Schema type (string, number, object, array, etc.)"
|
|
24
|
+
)
|
|
21
25
|
description: str | None = Field(None, description="Parameter description")
|
|
22
26
|
enum: list[Any] | None = Field(None, description="Allowed values for enum types")
|
|
23
27
|
items: dict[str, Any] | None = Field(None, description="Array item schema")
|
|
@@ -26,6 +30,8 @@ class ToolParameter(BaseModel):
|
|
|
26
30
|
)
|
|
27
31
|
required: list[str] | None = Field(None, description="Required properties for nested objects")
|
|
28
32
|
default: Any | None = Field(None, description="Default value")
|
|
33
|
+
anyOf: list[dict[str, Any]] | None = Field(None, description="Union type schemas")
|
|
34
|
+
oneOf: list[dict[str, Any]] | None = Field(None, description="Exclusive union type schemas")
|
|
29
35
|
|
|
30
36
|
model_config = {"extra": "allow"} # Allow additional JSON Schema fields
|
|
31
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casual-llm
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Lightweight LLM provider abstraction with standardized message models
|
|
5
5
|
Author-email: Alex Stansfield <alex@casualgenius.com>
|
|
6
6
|
License: MIT
|
|
@@ -43,13 +43,15 @@ Part of the [casual-*](https://github.com/AlexStansfield/casual-mcp) ecosystem o
|
|
|
43
43
|
## Features
|
|
44
44
|
|
|
45
45
|
- 🎯 **Protocol-based** - Uses `typing.Protocol`, no inheritance required
|
|
46
|
-
- 🔌 **
|
|
47
|
-
- 📦 **Lightweight** - Minimal dependencies (pydantic, ollama)
|
|
46
|
+
- 🔌 **Multi-provider** - Works with OpenAI, Anthropic (Claude), Ollama, or your custom provider
|
|
47
|
+
- 📦 **Lightweight** - Minimal dependencies (pydantic, ollama, httpx)
|
|
48
48
|
- 🔄 **Async-first** - Built for modern async Python
|
|
49
49
|
- 🛡️ **Type-safe** - Full type hints with py.typed marker
|
|
50
50
|
- 📊 **OpenAI-compatible** - Standard message format used across the industry
|
|
51
51
|
- 🔧 **Tool calling** - First-class support for function/tool calling
|
|
52
52
|
- 📈 **Usage tracking** - Track token usage for cost monitoring
|
|
53
|
+
- 🖼️ **Vision support** - Send images to vision-capable models
|
|
54
|
+
- ⚡ **Streaming** - Stream responses in real-time with `AsyncIterator`
|
|
53
55
|
|
|
54
56
|
## Installation
|
|
55
57
|
|
|
@@ -60,11 +62,18 @@ uv add casual-llm
|
|
|
60
62
|
# With OpenAI support
|
|
61
63
|
uv add casual-llm[openai]
|
|
62
64
|
|
|
65
|
+
# With Anthropic (Claude) support
|
|
66
|
+
uv add casual-llm[anthropic]
|
|
67
|
+
|
|
68
|
+
# With all providers
|
|
69
|
+
uv add casual-llm[openai,anthropic]
|
|
70
|
+
|
|
63
71
|
# Development dependencies
|
|
64
72
|
uv add casual-llm[dev]
|
|
65
73
|
|
|
66
74
|
# Or using pip
|
|
67
75
|
pip install casual-llm
|
|
76
|
+
pip install casual-llm[openai,anthropic]
|
|
68
77
|
```
|
|
69
78
|
|
|
70
79
|
## Quick Start
|
|
@@ -122,6 +131,32 @@ if usage:
|
|
|
122
131
|
print(f"Total tokens: {usage.total_tokens}")
|
|
123
132
|
```
|
|
124
133
|
|
|
134
|
+
### Using Anthropic (Claude)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from casual_llm import create_provider, ModelConfig, Provider, UserMessage
|
|
138
|
+
|
|
139
|
+
# Create Anthropic provider
|
|
140
|
+
config = ModelConfig(
|
|
141
|
+
name="claude-3-5-sonnet-20241022",
|
|
142
|
+
provider=Provider.ANTHROPIC,
|
|
143
|
+
api_key="sk-ant-...", # or set ANTHROPIC_API_KEY env var
|
|
144
|
+
temperature=0.7
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
provider = create_provider(config)
|
|
148
|
+
|
|
149
|
+
# Generate response
|
|
150
|
+
messages = [UserMessage(content="Explain quantum computing in one sentence.")]
|
|
151
|
+
response = await provider.chat(messages, response_format="text")
|
|
152
|
+
print(response.content)
|
|
153
|
+
|
|
154
|
+
# Check token usage
|
|
155
|
+
usage = provider.get_usage()
|
|
156
|
+
if usage:
|
|
157
|
+
print(f"Total tokens: {usage.total_tokens}")
|
|
158
|
+
```
|
|
159
|
+
|
|
125
160
|
### Using OpenAI-Compatible APIs (OpenRouter, LM Studio, etc.)
|
|
126
161
|
|
|
127
162
|
```python
|
|
@@ -136,6 +171,107 @@ config = ModelConfig(
|
|
|
136
171
|
provider = create_provider(config)
|
|
137
172
|
```
|
|
138
173
|
|
|
174
|
+
### Vision Support
|
|
175
|
+
|
|
176
|
+
Send images to vision-capable models (GPT-4o, Claude 3.5 Sonnet, llava):
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from casual_llm import (
|
|
180
|
+
create_provider,
|
|
181
|
+
ModelConfig,
|
|
182
|
+
Provider,
|
|
183
|
+
UserMessage,
|
|
184
|
+
TextContent,
|
|
185
|
+
ImageContent,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Works with OpenAI, Anthropic, and Ollama
|
|
189
|
+
config = ModelConfig(
|
|
190
|
+
name="gpt-4o", # or "claude-3-5-sonnet-20241022" or "llava"
|
|
191
|
+
provider=Provider.OPENAI,
|
|
192
|
+
api_key="sk-...",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
provider = create_provider(config)
|
|
196
|
+
|
|
197
|
+
# Send an image URL
|
|
198
|
+
messages = [
|
|
199
|
+
UserMessage(
|
|
200
|
+
content=[
|
|
201
|
+
TextContent(text="What's in this image?"),
|
|
202
|
+
ImageContent(source="https://example.com/image.jpg"),
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
response = await provider.chat(messages)
|
|
208
|
+
print(response.content) # "I see a cat sitting on a windowsill..."
|
|
209
|
+
|
|
210
|
+
# Or send a base64-encoded image
|
|
211
|
+
import base64
|
|
212
|
+
|
|
213
|
+
with open("image.jpg", "rb") as f:
|
|
214
|
+
image_data = base64.b64encode(f.read()).decode("ascii")
|
|
215
|
+
|
|
216
|
+
messages = [
|
|
217
|
+
UserMessage(
|
|
218
|
+
content=[
|
|
219
|
+
TextContent(text="Describe this image"),
|
|
220
|
+
ImageContent(
|
|
221
|
+
source={"type": "base64", "data": image_data},
|
|
222
|
+
media_type="image/jpeg",
|
|
223
|
+
),
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
response = await provider.chat(messages)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Streaming Responses
|
|
232
|
+
|
|
233
|
+
Stream responses in real-time for better UX:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from casual_llm import create_provider, ModelConfig, Provider, UserMessage
|
|
237
|
+
|
|
238
|
+
config = ModelConfig(
|
|
239
|
+
name="gpt-4o", # Works with all providers
|
|
240
|
+
provider=Provider.OPENAI,
|
|
241
|
+
api_key="sk-...",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
provider = create_provider(config)
|
|
245
|
+
|
|
246
|
+
messages = [UserMessage(content="Write a short poem about coding.")]
|
|
247
|
+
|
|
248
|
+
# Stream the response
|
|
249
|
+
async for chunk in provider.stream(messages):
|
|
250
|
+
if chunk.content:
|
|
251
|
+
print(chunk.content, end="", flush=True)
|
|
252
|
+
|
|
253
|
+
print() # New line after streaming
|
|
254
|
+
|
|
255
|
+
# Check usage after streaming
|
|
256
|
+
usage = provider.get_usage()
|
|
257
|
+
if usage:
|
|
258
|
+
print(f"\nTokens used: {usage.total_tokens}")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Examples
|
|
262
|
+
|
|
263
|
+
Looking for more examples? Check out the [`examples/`](examples) directory for comprehensive demonstrations of all features:
|
|
264
|
+
|
|
265
|
+
- **[`basic_ollama.py`](examples/basic_ollama.py)** - Get started with Ollama (local LLMs)
|
|
266
|
+
- **[`basic_openai.py`](examples/basic_openai.py)** - Use OpenAI API and compatible services
|
|
267
|
+
- **[`basic_anthropic.py`](examples/basic_anthropic.py)** - Work with Claude models
|
|
268
|
+
- **[`vision_example.py`](examples/vision_example.py)** - Send images to vision-capable models
|
|
269
|
+
- **[`stream_example.py`](examples/stream_example.py)** - Stream responses in real-time
|
|
270
|
+
- **[`tool_calling.py`](examples/tool_calling.py)** - Complete tool/function calling workflow
|
|
271
|
+
- **[`message_formatting.py`](examples/message_formatting.py)** - All message types and structures
|
|
272
|
+
|
|
273
|
+
See the **[Examples README](examples/README.md)** for detailed descriptions, requirements, and usage instructions for each example.
|
|
274
|
+
|
|
139
275
|
## Message Models
|
|
140
276
|
|
|
141
277
|
casual-llm provides OpenAI-compatible message models that work with any provider:
|
|
@@ -148,14 +284,24 @@ from casual_llm import (
|
|
|
148
284
|
ToolResultMessage,
|
|
149
285
|
AssistantToolCall,
|
|
150
286
|
ChatMessage, # Type alias for any message type
|
|
287
|
+
TextContent, # For multimodal messages
|
|
288
|
+
ImageContent, # For vision support
|
|
151
289
|
)
|
|
152
290
|
|
|
153
291
|
# System message (sets behavior)
|
|
154
292
|
system_msg = SystemMessage(content="You are a helpful assistant.")
|
|
155
293
|
|
|
156
|
-
# User message
|
|
294
|
+
# User message (simple text)
|
|
157
295
|
user_msg = UserMessage(content="Hello!")
|
|
158
296
|
|
|
297
|
+
# User message (multimodal - text + image)
|
|
298
|
+
vision_msg = UserMessage(
|
|
299
|
+
content=[
|
|
300
|
+
TextContent(text="What's in this image?"),
|
|
301
|
+
ImageContent(source="https://example.com/image.jpg"),
|
|
302
|
+
]
|
|
303
|
+
)
|
|
304
|
+
|
|
159
305
|
# Assistant message (with optional tool calls)
|
|
160
306
|
assistant_msg = AssistantMessage(
|
|
161
307
|
content="I'll help you with that.",
|
|
@@ -186,7 +332,15 @@ messages: list[ChatMessage] = [system_msg, user_msg, assistant_msg, tool_msg]
|
|
|
186
332
|
Implement the `LLMProvider` protocol to add your own provider:
|
|
187
333
|
|
|
188
334
|
```python
|
|
189
|
-
from
|
|
335
|
+
from typing import Literal, AsyncIterator
|
|
336
|
+
from casual_llm import (
|
|
337
|
+
LLMProvider,
|
|
338
|
+
ChatMessage,
|
|
339
|
+
AssistantMessage,
|
|
340
|
+
StreamChunk,
|
|
341
|
+
Tool,
|
|
342
|
+
Usage,
|
|
343
|
+
)
|
|
190
344
|
|
|
191
345
|
class MyCustomProvider:
|
|
192
346
|
"""Custom LLM provider implementation."""
|
|
@@ -194,13 +348,26 @@ class MyCustomProvider:
|
|
|
194
348
|
async def chat(
|
|
195
349
|
self,
|
|
196
350
|
messages: list[ChatMessage],
|
|
197
|
-
response_format: Literal["json", "text"] = "text",
|
|
351
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
198
352
|
max_tokens: int | None = None,
|
|
199
353
|
tools: list[Tool] | None = None,
|
|
354
|
+
temperature: float | None = None,
|
|
200
355
|
) -> AssistantMessage:
|
|
201
356
|
# Your implementation here
|
|
202
357
|
...
|
|
203
358
|
|
|
359
|
+
async def stream(
|
|
360
|
+
self,
|
|
361
|
+
messages: list[ChatMessage],
|
|
362
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
363
|
+
max_tokens: int | None = None,
|
|
364
|
+
tools: list[Tool] | None = None,
|
|
365
|
+
temperature: float | None = None,
|
|
366
|
+
) -> AsyncIterator[StreamChunk]:
|
|
367
|
+
# Your streaming implementation here
|
|
368
|
+
...
|
|
369
|
+
yield StreamChunk(content="chunk", finish_reason=None)
|
|
370
|
+
|
|
204
371
|
def get_usage(self) -> Usage | None:
|
|
205
372
|
"""Return token usage from last call."""
|
|
206
373
|
return self._last_usage
|
|
@@ -246,10 +413,13 @@ Both OpenAI and Ollama providers support usage tracking.
|
|
|
246
413
|
|
|
247
414
|
| Feature | casual-llm | LangChain | litellm |
|
|
248
415
|
|---------|-----------|-----------|---------|
|
|
249
|
-
| **Dependencies** |
|
|
416
|
+
| **Dependencies** | 3 (pydantic, ollama, httpx) | 100+ | 50+ |
|
|
250
417
|
| **Protocol-based** | ✅ | ❌ | ❌ |
|
|
251
418
|
| **Type-safe** | ✅ Full typing | Partial | Partial |
|
|
252
419
|
| **Message models** | ✅ Included | ❌ Separate | ❌ |
|
|
420
|
+
| **Vision support** | ✅ All providers | ✅ | ✅ |
|
|
421
|
+
| **Streaming** | ✅ All providers | ✅ | ✅ |
|
|
422
|
+
| **Providers** | OpenAI, Anthropic, Ollama | Many | Many |
|
|
253
423
|
| **Learning curve** | ⚡ Minutes | 📚 Hours | 📖 Medium |
|
|
254
424
|
| **OpenAI compatible** | ✅ | ✅ | ✅ |
|
|
255
425
|
|
|
@@ -282,11 +452,21 @@ class LLMProvider(Protocol):
|
|
|
282
452
|
async def chat(
|
|
283
453
|
self,
|
|
284
454
|
messages: list[ChatMessage],
|
|
285
|
-
response_format: Literal["json", "text"] = "text",
|
|
455
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
286
456
|
max_tokens: int | None = None,
|
|
287
457
|
tools: list[Tool] | None = None,
|
|
458
|
+
temperature: float | None = None,
|
|
288
459
|
) -> AssistantMessage: ...
|
|
289
460
|
|
|
461
|
+
async def stream(
|
|
462
|
+
self,
|
|
463
|
+
messages: list[ChatMessage],
|
|
464
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
465
|
+
max_tokens: int | None = None,
|
|
466
|
+
tools: list[Tool] | None = None,
|
|
467
|
+
temperature: float | None = None,
|
|
468
|
+
) -> AsyncIterator[StreamChunk]: ...
|
|
469
|
+
|
|
290
470
|
def get_usage(self) -> Usage | None: ...
|
|
291
471
|
```
|
|
292
472
|
|
|
@@ -321,11 +501,13 @@ class Usage(BaseModel):
|
|
|
321
501
|
|
|
322
502
|
All message models are Pydantic `BaseModel` instances with full validation:
|
|
323
503
|
|
|
324
|
-
- `UserMessage(content: str | None)`
|
|
504
|
+
- `UserMessage(content: str | list[TextContent | ImageContent] | None)` - Supports simple text or multimodal content
|
|
325
505
|
- `AssistantMessage(content: str | None, tool_calls: list[AssistantToolCall] | None = None)`
|
|
326
506
|
- `SystemMessage(content: str)`
|
|
327
507
|
- `ToolResultMessage(name: str, tool_call_id: str, content: str)`
|
|
328
508
|
- `ChatMessage` - Type alias for any message type
|
|
509
|
+
- `TextContent(text: str)` - Text block for multimodal messages
|
|
510
|
+
- `ImageContent(source: str | dict, media_type: str | None = None)` - Image block for vision support
|
|
329
511
|
|
|
330
512
|
## Contributing
|
|
331
513
|
|
|
@@ -71,6 +71,35 @@ class TestToolParameter:
|
|
|
71
71
|
assert dumped["maxLength"] == 100
|
|
72
72
|
assert dumped["pattern"] == "^[a-z]+$"
|
|
73
73
|
|
|
74
|
+
def test_anyof_parameter(self):
|
|
75
|
+
"""Test parameter with anyOf union type."""
|
|
76
|
+
param = ToolParameter(
|
|
77
|
+
anyOf=[{"type": "string"}, {"type": "null"}],
|
|
78
|
+
description="Optional string",
|
|
79
|
+
)
|
|
80
|
+
assert param.type is None
|
|
81
|
+
assert param.anyOf is not None
|
|
82
|
+
|
|
83
|
+
def test_oneof_parameter(self):
|
|
84
|
+
"""Test parameter with oneOf exclusive union type."""
|
|
85
|
+
param = ToolParameter(
|
|
86
|
+
oneOf=[{"type": "string"}, {"type": "number"}],
|
|
87
|
+
description="String or number",
|
|
88
|
+
)
|
|
89
|
+
assert param.type is None
|
|
90
|
+
assert param.oneOf is not None
|
|
91
|
+
|
|
92
|
+
def test_anyof_serialization(self):
|
|
93
|
+
"""Test that anyOf is properly serialized."""
|
|
94
|
+
param = ToolParameter(
|
|
95
|
+
anyOf=[{"type": "string"}, {"type": "null"}],
|
|
96
|
+
description="Optional",
|
|
97
|
+
)
|
|
98
|
+
dumped = param.model_dump(exclude_none=True)
|
|
99
|
+
assert "anyOf" in dumped
|
|
100
|
+
assert len(dumped["anyOf"]) == 2
|
|
101
|
+
assert "type" not in dumped # type is None, excluded
|
|
102
|
+
|
|
74
103
|
|
|
75
104
|
class TestTool:
|
|
76
105
|
"""Tests for Tool model."""
|
|
@@ -188,6 +217,41 @@ class TestTool:
|
|
|
188
217
|
assert reconstructed.required == original_tool.required
|
|
189
218
|
assert len(reconstructed.parameters) == len(original_tool.parameters)
|
|
190
219
|
|
|
220
|
+
def test_input_schema_with_anyof(self):
|
|
221
|
+
"""Test that input_schema preserves anyOf without injecting type."""
|
|
222
|
+
tool = Tool(
|
|
223
|
+
name="test",
|
|
224
|
+
description="Test",
|
|
225
|
+
parameters={
|
|
226
|
+
"optional_field": ToolParameter(
|
|
227
|
+
anyOf=[{"type": "string"}, {"type": "null"}],
|
|
228
|
+
description="An optional string",
|
|
229
|
+
)
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
schema = tool.input_schema
|
|
233
|
+
# anyOf should be preserved as-is
|
|
234
|
+
assert "anyOf" in schema["properties"]["optional_field"]
|
|
235
|
+
# type should NOT be injected (anyOf is valid JSON Schema)
|
|
236
|
+
assert "type" not in schema["properties"]["optional_field"]
|
|
237
|
+
|
|
238
|
+
def test_input_schema_with_oneof(self):
|
|
239
|
+
"""Test that input_schema preserves oneOf without injecting type."""
|
|
240
|
+
tool = Tool(
|
|
241
|
+
name="test",
|
|
242
|
+
description="Test",
|
|
243
|
+
parameters={
|
|
244
|
+
"mixed_field": ToolParameter(
|
|
245
|
+
oneOf=[{"type": "number"}, {"type": "string"}],
|
|
246
|
+
)
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
schema = tool.input_schema
|
|
250
|
+
# oneOf should be preserved as-is
|
|
251
|
+
assert "oneOf" in schema["properties"]["mixed_field"]
|
|
252
|
+
# type should NOT be injected
|
|
253
|
+
assert "type" not in schema["properties"]["mixed_field"]
|
|
254
|
+
|
|
191
255
|
|
|
192
256
|
class TestOllamaConverters:
|
|
193
257
|
"""Tests for Ollama format converters."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|