casual-llm 0.3.0__tar.gz → 0.4.2__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.3.0/src/casual_llm.egg-info → casual_llm-0.4.2}/PKG-INFO +193 -9
- {casual_llm-0.3.0 → casual_llm-0.4.2}/README.md +190 -8
- {casual_llm-0.3.0 → casual_llm-0.4.2}/pyproject.toml +3 -3
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/__init__.py +14 -1
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/config.py +1 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/message_converters/__init__.py +9 -1
- casual_llm-0.4.2/src/casual_llm/message_converters/anthropic.py +290 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/providers/__init__.py +23 -2
- casual_llm-0.4.2/src/casual_llm/providers/anthropic.py +319 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/tool_converters/__init__.py +8 -1
- casual_llm-0.4.2/src/casual_llm/tool_converters/anthropic.py +70 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2/src/casual_llm.egg-info}/PKG-INFO +193 -9
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm.egg-info/SOURCES.txt +4 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm.egg-info/requires.txt +3 -0
- casual_llm-0.4.2/tests/test_anthropic_provider.py +694 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/LICENSE +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/setup.cfg +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/message_converters/ollama.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/message_converters/openai.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/messages.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/providers/base.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/providers/ollama.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/providers/openai.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/py.typed +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/tool_converters/ollama.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/tool_converters/openai.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/tools.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/usage.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/utils/__init__.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/utils/image.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm.egg-info/dependency_links.txt +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm.egg-info/top_level.txt +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_backward_compatibility.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_image_utils.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_messages.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_providers.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_tools.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_vision_integration.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_vision_ollama.py +0 -0
- {casual_llm-0.3.0 → casual_llm-0.4.2}/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.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Lightweight LLM provider abstraction with standardized message models
|
|
5
5
|
Author-email: Alex Stansfield <alex@casualgenius.com>
|
|
6
6
|
License: MIT
|
|
@@ -26,6 +26,8 @@ Requires-Dist: ollama>=0.6.1
|
|
|
26
26
|
Requires-Dist: httpx[http2]>=0.28.1
|
|
27
27
|
Provides-Extra: openai
|
|
28
28
|
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
29
|
+
Provides-Extra: anthropic
|
|
30
|
+
Requires-Dist: anthropic>=0.20.0; extra == "anthropic"
|
|
29
31
|
Dynamic: license-file
|
|
30
32
|
|
|
31
33
|
# casual-llm
|
|
@@ -41,13 +43,15 @@ Part of the [casual-*](https://github.com/AlexStansfield/casual-mcp) ecosystem o
|
|
|
41
43
|
## Features
|
|
42
44
|
|
|
43
45
|
- 🎯 **Protocol-based** - Uses `typing.Protocol`, no inheritance required
|
|
44
|
-
- 🔌 **
|
|
45
|
-
- 📦 **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)
|
|
46
48
|
- 🔄 **Async-first** - Built for modern async Python
|
|
47
49
|
- 🛡️ **Type-safe** - Full type hints with py.typed marker
|
|
48
50
|
- 📊 **OpenAI-compatible** - Standard message format used across the industry
|
|
49
51
|
- 🔧 **Tool calling** - First-class support for function/tool calling
|
|
50
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`
|
|
51
55
|
|
|
52
56
|
## Installation
|
|
53
57
|
|
|
@@ -58,11 +62,18 @@ uv add casual-llm
|
|
|
58
62
|
# With OpenAI support
|
|
59
63
|
uv add casual-llm[openai]
|
|
60
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
|
+
|
|
61
71
|
# Development dependencies
|
|
62
72
|
uv add casual-llm[dev]
|
|
63
73
|
|
|
64
74
|
# Or using pip
|
|
65
75
|
pip install casual-llm
|
|
76
|
+
pip install casual-llm[openai,anthropic]
|
|
66
77
|
```
|
|
67
78
|
|
|
68
79
|
## Quick Start
|
|
@@ -120,6 +131,32 @@ if usage:
|
|
|
120
131
|
print(f"Total tokens: {usage.total_tokens}")
|
|
121
132
|
```
|
|
122
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
|
+
|
|
123
160
|
### Using OpenAI-Compatible APIs (OpenRouter, LM Studio, etc.)
|
|
124
161
|
|
|
125
162
|
```python
|
|
@@ -134,6 +171,107 @@ config = ModelConfig(
|
|
|
134
171
|
provider = create_provider(config)
|
|
135
172
|
```
|
|
136
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
|
+
|
|
137
275
|
## Message Models
|
|
138
276
|
|
|
139
277
|
casual-llm provides OpenAI-compatible message models that work with any provider:
|
|
@@ -146,14 +284,24 @@ from casual_llm import (
|
|
|
146
284
|
ToolResultMessage,
|
|
147
285
|
AssistantToolCall,
|
|
148
286
|
ChatMessage, # Type alias for any message type
|
|
287
|
+
TextContent, # For multimodal messages
|
|
288
|
+
ImageContent, # For vision support
|
|
149
289
|
)
|
|
150
290
|
|
|
151
291
|
# System message (sets behavior)
|
|
152
292
|
system_msg = SystemMessage(content="You are a helpful assistant.")
|
|
153
293
|
|
|
154
|
-
# User message
|
|
294
|
+
# User message (simple text)
|
|
155
295
|
user_msg = UserMessage(content="Hello!")
|
|
156
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
|
+
|
|
157
305
|
# Assistant message (with optional tool calls)
|
|
158
306
|
assistant_msg = AssistantMessage(
|
|
159
307
|
content="I'll help you with that.",
|
|
@@ -184,7 +332,15 @@ messages: list[ChatMessage] = [system_msg, user_msg, assistant_msg, tool_msg]
|
|
|
184
332
|
Implement the `LLMProvider` protocol to add your own provider:
|
|
185
333
|
|
|
186
334
|
```python
|
|
187
|
-
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
|
+
)
|
|
188
344
|
|
|
189
345
|
class MyCustomProvider:
|
|
190
346
|
"""Custom LLM provider implementation."""
|
|
@@ -192,13 +348,26 @@ class MyCustomProvider:
|
|
|
192
348
|
async def chat(
|
|
193
349
|
self,
|
|
194
350
|
messages: list[ChatMessage],
|
|
195
|
-
response_format: Literal["json", "text"] = "text",
|
|
351
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
196
352
|
max_tokens: int | None = None,
|
|
197
353
|
tools: list[Tool] | None = None,
|
|
354
|
+
temperature: float | None = None,
|
|
198
355
|
) -> AssistantMessage:
|
|
199
356
|
# Your implementation here
|
|
200
357
|
...
|
|
201
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
|
+
|
|
202
371
|
def get_usage(self) -> Usage | None:
|
|
203
372
|
"""Return token usage from last call."""
|
|
204
373
|
return self._last_usage
|
|
@@ -244,10 +413,13 @@ Both OpenAI and Ollama providers support usage tracking.
|
|
|
244
413
|
|
|
245
414
|
| Feature | casual-llm | LangChain | litellm |
|
|
246
415
|
|---------|-----------|-----------|---------|
|
|
247
|
-
| **Dependencies** |
|
|
416
|
+
| **Dependencies** | 3 (pydantic, ollama, httpx) | 100+ | 50+ |
|
|
248
417
|
| **Protocol-based** | ✅ | ❌ | ❌ |
|
|
249
418
|
| **Type-safe** | ✅ Full typing | Partial | Partial |
|
|
250
419
|
| **Message models** | ✅ Included | ❌ Separate | ❌ |
|
|
420
|
+
| **Vision support** | ✅ All providers | ✅ | ✅ |
|
|
421
|
+
| **Streaming** | ✅ All providers | ✅ | ✅ |
|
|
422
|
+
| **Providers** | OpenAI, Anthropic, Ollama | Many | Many |
|
|
251
423
|
| **Learning curve** | ⚡ Minutes | 📚 Hours | 📖 Medium |
|
|
252
424
|
| **OpenAI compatible** | ✅ | ✅ | ✅ |
|
|
253
425
|
|
|
@@ -280,11 +452,21 @@ class LLMProvider(Protocol):
|
|
|
280
452
|
async def chat(
|
|
281
453
|
self,
|
|
282
454
|
messages: list[ChatMessage],
|
|
283
|
-
response_format: Literal["json", "text"] = "text",
|
|
455
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
284
456
|
max_tokens: int | None = None,
|
|
285
457
|
tools: list[Tool] | None = None,
|
|
458
|
+
temperature: float | None = None,
|
|
286
459
|
) -> AssistantMessage: ...
|
|
287
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
|
+
|
|
288
470
|
def get_usage(self) -> Usage | None: ...
|
|
289
471
|
```
|
|
290
472
|
|
|
@@ -319,11 +501,13 @@ class Usage(BaseModel):
|
|
|
319
501
|
|
|
320
502
|
All message models are Pydantic `BaseModel` instances with full validation:
|
|
321
503
|
|
|
322
|
-
- `UserMessage(content: str | None)`
|
|
504
|
+
- `UserMessage(content: str | list[TextContent | ImageContent] | None)` - Supports simple text or multimodal content
|
|
323
505
|
- `AssistantMessage(content: str | None, tool_calls: list[AssistantToolCall] | None = None)`
|
|
324
506
|
- `SystemMessage(content: str)`
|
|
325
507
|
- `ToolResultMessage(name: str, tool_call_id: str, content: str)`
|
|
326
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
|
|
327
511
|
|
|
328
512
|
## Contributing
|
|
329
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "casual-llm"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.2"
|
|
4
4
|
description = "Lightweight LLM provider abstraction with standardized message models"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
|
|
30
30
|
[project.optional-dependencies]
|
|
31
31
|
openai = ["openai>=1.0.0"]
|
|
32
|
+
anthropic = ["anthropic>=0.20.0"]
|
|
32
33
|
|
|
33
34
|
[project.urls]
|
|
34
35
|
Homepage = "https://github.com/casualgenius/casual-llm"
|
|
@@ -64,7 +65,6 @@ strict = true
|
|
|
64
65
|
warn_return_any = true
|
|
65
66
|
warn_unused_configs = true
|
|
66
67
|
|
|
67
|
-
|
|
68
68
|
[dependency-groups]
|
|
69
69
|
dev = [
|
|
70
70
|
"openai>=2.8.1",
|
|
@@ -74,4 +74,4 @@ dev = [
|
|
|
74
74
|
"black>=23.0.0",
|
|
75
75
|
"ruff>=0.1.0",
|
|
76
76
|
"mypy>=1.0.0",
|
|
77
|
-
]
|
|
77
|
+
]
|
|
@@ -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.
|
|
10
|
+
__version__ = "0.4.2"
|
|
11
11
|
|
|
12
12
|
# Model configuration
|
|
13
13
|
from casual_llm.config import ModelConfig, Provider
|
|
@@ -17,6 +17,7 @@ from casual_llm.providers import (
|
|
|
17
17
|
LLMProvider,
|
|
18
18
|
OllamaProvider,
|
|
19
19
|
OpenAIProvider,
|
|
20
|
+
AnthropicProvider,
|
|
20
21
|
create_provider,
|
|
21
22
|
)
|
|
22
23
|
|
|
@@ -47,14 +48,19 @@ from casual_llm.tool_converters import (
|
|
|
47
48
|
tools_to_ollama,
|
|
48
49
|
tool_to_openai,
|
|
49
50
|
tools_to_openai,
|
|
51
|
+
tool_to_anthropic,
|
|
52
|
+
tools_to_anthropic,
|
|
50
53
|
)
|
|
51
54
|
|
|
52
55
|
# Message converters
|
|
53
56
|
from casual_llm.message_converters import (
|
|
54
57
|
convert_messages_to_openai,
|
|
55
58
|
convert_messages_to_ollama,
|
|
59
|
+
convert_messages_to_anthropic,
|
|
56
60
|
convert_tool_calls_from_openai,
|
|
57
61
|
convert_tool_calls_from_ollama,
|
|
62
|
+
convert_tool_calls_from_anthropic,
|
|
63
|
+
extract_system_message,
|
|
58
64
|
)
|
|
59
65
|
|
|
60
66
|
__all__ = [
|
|
@@ -66,6 +72,7 @@ __all__ = [
|
|
|
66
72
|
"Provider",
|
|
67
73
|
"OllamaProvider",
|
|
68
74
|
"OpenAIProvider",
|
|
75
|
+
"AnthropicProvider",
|
|
69
76
|
"create_provider",
|
|
70
77
|
# Messages
|
|
71
78
|
"ChatMessage",
|
|
@@ -84,13 +91,19 @@ __all__ = [
|
|
|
84
91
|
"ToolParameter",
|
|
85
92
|
# Usage
|
|
86
93
|
"Usage",
|
|
94
|
+
# Tool converters
|
|
87
95
|
"tool_to_ollama",
|
|
88
96
|
"tools_to_ollama",
|
|
89
97
|
"tool_to_openai",
|
|
90
98
|
"tools_to_openai",
|
|
99
|
+
"tool_to_anthropic",
|
|
100
|
+
"tools_to_anthropic",
|
|
91
101
|
# Message converters
|
|
92
102
|
"convert_messages_to_openai",
|
|
93
103
|
"convert_messages_to_ollama",
|
|
104
|
+
"convert_messages_to_anthropic",
|
|
94
105
|
"convert_tool_calls_from_openai",
|
|
95
106
|
"convert_tool_calls_from_ollama",
|
|
107
|
+
"convert_tool_calls_from_anthropic",
|
|
108
|
+
"extract_system_message",
|
|
96
109
|
]
|