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.
Files changed (40) hide show
  1. {casual_llm-0.3.0/src/casual_llm.egg-info → casual_llm-0.4.2}/PKG-INFO +193 -9
  2. {casual_llm-0.3.0 → casual_llm-0.4.2}/README.md +190 -8
  3. {casual_llm-0.3.0 → casual_llm-0.4.2}/pyproject.toml +3 -3
  4. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/__init__.py +14 -1
  5. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/config.py +1 -0
  6. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/message_converters/__init__.py +9 -1
  7. casual_llm-0.4.2/src/casual_llm/message_converters/anthropic.py +290 -0
  8. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/providers/__init__.py +23 -2
  9. casual_llm-0.4.2/src/casual_llm/providers/anthropic.py +319 -0
  10. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/tool_converters/__init__.py +8 -1
  11. casual_llm-0.4.2/src/casual_llm/tool_converters/anthropic.py +70 -0
  12. {casual_llm-0.3.0 → casual_llm-0.4.2/src/casual_llm.egg-info}/PKG-INFO +193 -9
  13. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm.egg-info/SOURCES.txt +4 -0
  14. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm.egg-info/requires.txt +3 -0
  15. casual_llm-0.4.2/tests/test_anthropic_provider.py +694 -0
  16. {casual_llm-0.3.0 → casual_llm-0.4.2}/LICENSE +0 -0
  17. {casual_llm-0.3.0 → casual_llm-0.4.2}/setup.cfg +0 -0
  18. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/message_converters/ollama.py +0 -0
  19. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/message_converters/openai.py +0 -0
  20. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/messages.py +0 -0
  21. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/providers/base.py +0 -0
  22. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/providers/ollama.py +0 -0
  23. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/providers/openai.py +0 -0
  24. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/py.typed +0 -0
  25. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/tool_converters/ollama.py +0 -0
  26. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/tool_converters/openai.py +0 -0
  27. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/tools.py +0 -0
  28. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/usage.py +0 -0
  29. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/utils/__init__.py +0 -0
  30. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm/utils/image.py +0 -0
  31. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm.egg-info/dependency_links.txt +0 -0
  32. {casual_llm-0.3.0 → casual_llm-0.4.2}/src/casual_llm.egg-info/top_level.txt +0 -0
  33. {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_backward_compatibility.py +0 -0
  34. {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_image_utils.py +0 -0
  35. {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_messages.py +0 -0
  36. {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_providers.py +0 -0
  37. {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_tools.py +0 -0
  38. {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_vision_integration.py +0 -0
  39. {casual_llm-0.3.0 → casual_llm-0.4.2}/tests/test_vision_ollama.py +0 -0
  40. {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.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
- - 🔌 **Provider-agnostic** - Works with OpenAI, Ollama, or your custom provider
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 casual_llm import LLMProvider, ChatMessage, AssistantMessage, Tool, Usage
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** | 2 (pydantic, ollama) | 100+ | 50+ |
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
- - 🔌 **Provider-agnostic** - Works with OpenAI, Ollama, or your custom provider
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 casual_llm import LLMProvider, ChatMessage, AssistantMessage, Tool, Usage
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** | 2 (pydantic, ollama) | 100+ | 50+ |
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.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.3.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
  ]