casual-llm 0.3.0__tar.gz → 0.4.1__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.1}/PKG-INFO +3 -1
  2. {casual_llm-0.3.0 → casual_llm-0.4.1}/pyproject.toml +3 -3
  3. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/__init__.py +14 -1
  4. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/config.py +1 -0
  5. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/message_converters/__init__.py +9 -1
  6. casual_llm-0.4.1/src/casual_llm/message_converters/anthropic.py +290 -0
  7. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/providers/__init__.py +23 -2
  8. casual_llm-0.4.1/src/casual_llm/providers/anthropic.py +319 -0
  9. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/tool_converters/__init__.py +8 -1
  10. casual_llm-0.4.1/src/casual_llm/tool_converters/anthropic.py +70 -0
  11. {casual_llm-0.3.0 → casual_llm-0.4.1/src/casual_llm.egg-info}/PKG-INFO +3 -1
  12. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm.egg-info/SOURCES.txt +4 -0
  13. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm.egg-info/requires.txt +3 -0
  14. casual_llm-0.4.1/tests/test_anthropic_provider.py +694 -0
  15. {casual_llm-0.3.0 → casual_llm-0.4.1}/LICENSE +0 -0
  16. {casual_llm-0.3.0 → casual_llm-0.4.1}/README.md +0 -0
  17. {casual_llm-0.3.0 → casual_llm-0.4.1}/setup.cfg +0 -0
  18. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/message_converters/ollama.py +0 -0
  19. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/message_converters/openai.py +0 -0
  20. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/messages.py +0 -0
  21. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/providers/base.py +0 -0
  22. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/providers/ollama.py +0 -0
  23. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/providers/openai.py +0 -0
  24. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/py.typed +0 -0
  25. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/tool_converters/ollama.py +0 -0
  26. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/tool_converters/openai.py +0 -0
  27. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/tools.py +0 -0
  28. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/usage.py +0 -0
  29. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/utils/__init__.py +0 -0
  30. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm/utils/image.py +0 -0
  31. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm.egg-info/dependency_links.txt +0 -0
  32. {casual_llm-0.3.0 → casual_llm-0.4.1}/src/casual_llm.egg-info/top_level.txt +0 -0
  33. {casual_llm-0.3.0 → casual_llm-0.4.1}/tests/test_backward_compatibility.py +0 -0
  34. {casual_llm-0.3.0 → casual_llm-0.4.1}/tests/test_image_utils.py +0 -0
  35. {casual_llm-0.3.0 → casual_llm-0.4.1}/tests/test_messages.py +0 -0
  36. {casual_llm-0.3.0 → casual_llm-0.4.1}/tests/test_providers.py +0 -0
  37. {casual_llm-0.3.0 → casual_llm-0.4.1}/tests/test_tools.py +0 -0
  38. {casual_llm-0.3.0 → casual_llm-0.4.1}/tests/test_vision_integration.py +0 -0
  39. {casual_llm-0.3.0 → casual_llm-0.4.1}/tests/test_vision_ollama.py +0 -0
  40. {casual_llm-0.3.0 → casual_llm-0.4.1}/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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "casual-llm"
3
- version = "0.3.0"
3
+ version = "0.4.1"
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.1"
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
  ]
@@ -14,6 +14,7 @@ class Provider(Enum):
14
14
 
15
15
  OPENAI = "openai"
16
16
  OLLAMA = "ollama"
17
+ ANTHROPIC = "anthropic"
17
18
 
18
19
 
19
20
  @dataclass
@@ -2,7 +2,7 @@
2
2
  Message converters for different LLM provider formats.
3
3
 
4
4
  This package provides converters to translate between casual-llm's unified
5
- ChatMessage format and provider-specific formats (OpenAI, Ollama).
5
+ ChatMessage format and provider-specific formats (OpenAI, Ollama, Anthropic).
6
6
  """
7
7
 
8
8
  from casual_llm.message_converters.openai import (
@@ -13,10 +13,18 @@ from casual_llm.message_converters.ollama import (
13
13
  convert_messages_to_ollama,
14
14
  convert_tool_calls_from_ollama,
15
15
  )
16
+ from casual_llm.message_converters.anthropic import (
17
+ convert_messages_to_anthropic,
18
+ convert_tool_calls_from_anthropic,
19
+ extract_system_message,
20
+ )
16
21
 
17
22
  __all__ = [
18
23
  "convert_messages_to_openai",
19
24
  "convert_messages_to_ollama",
25
+ "convert_messages_to_anthropic",
20
26
  "convert_tool_calls_from_openai",
21
27
  "convert_tool_calls_from_ollama",
28
+ "convert_tool_calls_from_anthropic",
29
+ "extract_system_message",
22
30
  ]
@@ -0,0 +1,290 @@
1
+ """
2
+ Anthropic message converters.
3
+
4
+ Converts casual-llm ChatMessage format to Anthropic API format and vice versa.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from casual_llm.messages import (
12
+ ChatMessage,
13
+ AssistantToolCall,
14
+ AssistantToolCallFunction,
15
+ TextContent,
16
+ ImageContent,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from anthropic.types import ToolUseBlock
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _convert_image_to_anthropic(image: ImageContent) -> dict[str, Any]:
26
+ """
27
+ Convert ImageContent to Anthropic image block format.
28
+
29
+ Anthropic supports both URL and base64 images directly.
30
+
31
+ Args:
32
+ image: ImageContent with either URL or base64 source
33
+
34
+ Returns:
35
+ Dictionary in Anthropic image block format
36
+
37
+ Examples:
38
+ >>> from casual_llm import ImageContent
39
+ >>> img = ImageContent(source="https://example.com/image.jpg")
40
+ >>> block = _convert_image_to_anthropic(img)
41
+ >>> block["type"]
42
+ 'image'
43
+ """
44
+ if isinstance(image.source, str):
45
+ # URL image - Anthropic supports URLs directly
46
+ return {
47
+ "type": "image",
48
+ "source": {
49
+ "type": "url",
50
+ "url": image.source,
51
+ },
52
+ }
53
+ else:
54
+ # Base64 image
55
+ base64_data = image.source.get("data", "")
56
+ media_type = image.media_type or "image/jpeg"
57
+
58
+ return {
59
+ "type": "image",
60
+ "source": {
61
+ "type": "base64",
62
+ "media_type": media_type,
63
+ "data": base64_data,
64
+ },
65
+ }
66
+
67
+
68
+ def _convert_user_content_to_anthropic(
69
+ content: str | list[TextContent | ImageContent] | None,
70
+ ) -> list[dict[str, Any]]:
71
+ """
72
+ Convert UserMessage content to Anthropic format.
73
+
74
+ Handles both simple string content (backward compatible) and
75
+ multimodal content arrays (text + images).
76
+
77
+ Anthropic always uses content blocks (array format), even for simple text.
78
+
79
+ Args:
80
+ content: Message content (string, multimodal array, or None)
81
+
82
+ Returns:
83
+ List of content blocks in Anthropic format
84
+
85
+ Examples:
86
+ >>> content_blocks = _convert_user_content_to_anthropic("Hello")
87
+ >>> content_blocks[0]["type"]
88
+ 'text'
89
+ """
90
+ if content is None:
91
+ return [{"type": "text", "text": ""}]
92
+
93
+ if isinstance(content, str):
94
+ # Simple string content
95
+ return [{"type": "text", "text": content}]
96
+
97
+ # Multimodal content array
98
+ content_blocks: list[dict[str, Any]] = []
99
+
100
+ for item in content:
101
+ if isinstance(item, TextContent):
102
+ content_blocks.append({"type": "text", "text": item.text})
103
+ elif isinstance(item, ImageContent):
104
+ content_blocks.append(_convert_image_to_anthropic(item))
105
+
106
+ return content_blocks
107
+
108
+
109
+ def extract_system_message(messages: list[ChatMessage]) -> str | None:
110
+ """
111
+ Extract system message content from the messages list.
112
+
113
+ Anthropic requires system messages to be passed as a separate parameter,
114
+ not as part of the messages array. This function extracts the first
115
+ system message content for use with the Anthropic API.
116
+
117
+ Args:
118
+ messages: List of ChatMessage objects
119
+
120
+ Returns:
121
+ System message content string, or None if no system message present
122
+
123
+ Examples:
124
+ >>> from casual_llm import SystemMessage, UserMessage
125
+ >>> messages = [SystemMessage(content="You are helpful"), UserMessage(content="Hello")]
126
+ >>> extract_system_message(messages)
127
+ 'You are helpful'
128
+ """
129
+ for msg in messages:
130
+ if msg.role == "system":
131
+ logger.debug("Extracted system message")
132
+ return msg.content
133
+ return None
134
+
135
+
136
+ def convert_messages_to_anthropic(messages: list[ChatMessage]) -> list[dict[str, Any]]:
137
+ """
138
+ Convert casual-llm ChatMessage list to Anthropic format.
139
+
140
+ Handles all message types including tool calls and tool results.
141
+ Note: System messages are excluded - use extract_system_message() to get
142
+ the system message content for the separate `system` parameter.
143
+
144
+ Anthropic format differences:
145
+ - System messages are NOT included (passed separately)
146
+ - Tool results go in user messages with "tool_result" content type
147
+ - Content is always an array of content blocks
148
+
149
+ Args:
150
+ messages: List of ChatMessage objects
151
+
152
+ Returns:
153
+ List of dictionaries in Anthropic MessageParam format
154
+
155
+ Examples:
156
+ >>> from casual_llm import UserMessage, AssistantMessage
157
+ >>> messages = [UserMessage(content="Hello")]
158
+ >>> anthropic_msgs = convert_messages_to_anthropic(messages)
159
+ >>> anthropic_msgs[0]["role"]
160
+ 'user'
161
+ """
162
+ if not messages:
163
+ return []
164
+
165
+ logger.debug(f"Converting {len(messages)} messages to Anthropic format")
166
+
167
+ anthropic_messages: list[dict[str, Any]] = []
168
+
169
+ for msg in messages:
170
+ match msg.role:
171
+ case "assistant":
172
+ # Handle assistant messages with optional tool calls
173
+ content_blocks: list[dict[str, Any]] = []
174
+
175
+ # Add text content if present
176
+ if msg.content:
177
+ content_blocks.append({"type": "text", "text": msg.content})
178
+
179
+ # Add tool use blocks if present
180
+ if msg.tool_calls:
181
+ for tool_call in msg.tool_calls:
182
+ # Parse arguments JSON string back to dict for Anthropic
183
+ try:
184
+ input_data = json.loads(tool_call.function.arguments)
185
+ except json.JSONDecodeError:
186
+ input_data = {}
187
+ logger.warning(
188
+ f"Failed to parse tool call arguments: {tool_call.function.arguments}"
189
+ )
190
+
191
+ content_blocks.append(
192
+ {
193
+ "type": "tool_use",
194
+ "id": tool_call.id,
195
+ "name": tool_call.function.name,
196
+ "input": input_data,
197
+ }
198
+ )
199
+
200
+ anthropic_messages.append(
201
+ {
202
+ "role": "assistant",
203
+ "content": content_blocks,
204
+ }
205
+ )
206
+
207
+ case "system":
208
+ # System messages are excluded - they are passed separately
209
+ # via the `system` parameter in the API call
210
+ logger.debug("Skipping system message (handled separately)")
211
+ continue
212
+
213
+ case "tool":
214
+ # Tool results go in user messages with tool_result content type
215
+ anthropic_messages.append(
216
+ {
217
+ "role": "user",
218
+ "content": [
219
+ {
220
+ "type": "tool_result",
221
+ "tool_use_id": msg.tool_call_id,
222
+ "content": msg.content,
223
+ }
224
+ ],
225
+ }
226
+ )
227
+
228
+ case "user":
229
+ # User messages with text and/or image content
230
+ content_blocks = _convert_user_content_to_anthropic(msg.content)
231
+ anthropic_messages.append(
232
+ {
233
+ "role": "user",
234
+ "content": content_blocks,
235
+ }
236
+ )
237
+
238
+ case _:
239
+ logger.warning(f"Unknown message role: {msg.role}")
240
+
241
+ return anthropic_messages
242
+
243
+
244
+ def convert_tool_calls_from_anthropic(
245
+ response_tool_calls: list["ToolUseBlock"],
246
+ ) -> list[AssistantToolCall]:
247
+ """
248
+ Convert Anthropic ToolUseBlock to casual-llm format.
249
+
250
+ Anthropic returns tool call arguments as a dict in the `input` field,
251
+ which must be serialized to JSON string for AssistantToolCallFunction.
252
+
253
+ Args:
254
+ response_tool_calls: List of ToolUseBlock from Anthropic response
255
+
256
+ Returns:
257
+ List of AssistantToolCall objects
258
+
259
+ Examples:
260
+ >>> # Assuming response has tool_use blocks
261
+ >>> # tool_calls = convert_tool_calls_from_anthropic(tool_use_blocks)
262
+ >>> # assert len(tool_calls) > 0
263
+ pass
264
+ """
265
+ tool_calls = []
266
+
267
+ for tool in response_tool_calls:
268
+ logger.debug(f"Converting tool call: {tool.name}")
269
+
270
+ # Serialize input dict to JSON string for casual-llm format
271
+ arguments = json.dumps(tool.input) if tool.input else "{}"
272
+
273
+ tool_call = AssistantToolCall(
274
+ id=tool.id,
275
+ type="function",
276
+ function=AssistantToolCallFunction(name=tool.name, arguments=arguments),
277
+ )
278
+ tool_calls.append(tool_call)
279
+
280
+ logger.debug(f"Converted {len(tool_calls)} tool calls")
281
+ return tool_calls
282
+
283
+
284
+ __all__ = [
285
+ "convert_messages_to_anthropic",
286
+ "extract_system_message",
287
+ "convert_tool_calls_from_anthropic",
288
+ "_convert_image_to_anthropic",
289
+ "_convert_user_content_to_anthropic",
290
+ ]
@@ -13,6 +13,11 @@ try:
13
13
  except ImportError:
14
14
  OpenAIProvider = None # type: ignore
15
15
 
16
+ try:
17
+ from casual_llm.providers.anthropic import AnthropicProvider
18
+ except ImportError:
19
+ AnthropicProvider = None # type: ignore
20
+
16
21
 
17
22
  def create_provider(
18
23
  model_config: ModelConfig,
@@ -26,11 +31,11 @@ def create_provider(
26
31
  timeout: HTTP timeout in seconds (default: 60.0)
27
32
 
28
33
  Returns:
29
- Configured LLM provider (OllamaProvider or OpenAIProvider)
34
+ Configured LLM provider (OllamaProvider, OpenAIProvider, or AnthropicProvider)
30
35
 
31
36
  Raises:
32
37
  ValueError: If provider type is not supported
33
- ImportError: If openai package is not installed for OpenAI provider
38
+ ImportError: If required package is not installed for the provider
34
39
 
35
40
  Examples:
36
41
  >>> from casual_llm import ModelConfig, Provider, create_provider
@@ -72,6 +77,21 @@ def create_provider(
72
77
  timeout=timeout,
73
78
  )
74
79
 
80
+ elif model_config.provider == Provider.ANTHROPIC:
81
+ if AnthropicProvider is None:
82
+ raise ImportError(
83
+ "Anthropic provider requires the 'anthropic' package. "
84
+ "Install it with: pip install casual-llm[anthropic]"
85
+ )
86
+
87
+ return AnthropicProvider(
88
+ model=model_config.name,
89
+ api_key=model_config.api_key,
90
+ base_url=model_config.base_url,
91
+ temperature=model_config.temperature,
92
+ timeout=timeout,
93
+ )
94
+
75
95
  else:
76
96
  raise ValueError(f"Unsupported provider: {model_config.provider}")
77
97
 
@@ -82,5 +102,6 @@ __all__ = [
82
102
  "Provider",
83
103
  "OllamaProvider",
84
104
  "OpenAIProvider",
105
+ "AnthropicProvider",
85
106
  "create_provider",
86
107
  ]