casual-llm 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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/__init__.py +9 -1
- casual_llm/message_converters/ollama.py +91 -3
- casual_llm/message_converters/openai.py +57 -1
- casual_llm/messages.py +53 -2
- casual_llm/providers/base.py +48 -2
- casual_llm/providers/ollama.py +93 -5
- casual_llm/providers/openai.py +97 -3
- casual_llm/utils/__init__.py +9 -0
- casual_llm/utils/image.py +162 -0
- {casual_llm-0.2.0.dist-info → casual_llm-0.3.0.dist-info}/METADATA +2 -1
- casual_llm-0.3.0.dist-info/RECORD +23 -0
- casual_llm-0.2.0.dist-info/RECORD +0 -21
- {casual_llm-0.2.0.dist-info → casual_llm-0.3.0.dist-info}/WHEEL +0 -0
- {casual_llm-0.2.0.dist-info → casual_llm-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {casual_llm-0.2.0.dist-info → casual_llm-0.3.0.dist-info}/top_level.txt +0 -0
casual_llm/__init__.py
CHANGED
|
@@ -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.3.0"
|
|
11
11
|
|
|
12
12
|
# Model configuration
|
|
13
13
|
from casual_llm.config import ModelConfig, Provider
|
|
@@ -29,6 +29,10 @@ from casual_llm.messages import (
|
|
|
29
29
|
ToolResultMessage,
|
|
30
30
|
AssistantToolCall,
|
|
31
31
|
AssistantToolCallFunction,
|
|
32
|
+
StreamChunk,
|
|
33
|
+
# Multimodal content types
|
|
34
|
+
TextContent,
|
|
35
|
+
ImageContent,
|
|
32
36
|
)
|
|
33
37
|
|
|
34
38
|
# Tool models
|
|
@@ -71,6 +75,10 @@ __all__ = [
|
|
|
71
75
|
"ToolResultMessage",
|
|
72
76
|
"AssistantToolCall",
|
|
73
77
|
"AssistantToolCallFunction",
|
|
78
|
+
"StreamChunk",
|
|
79
|
+
# Multimodal content types
|
|
80
|
+
"TextContent",
|
|
81
|
+
"ImageContent",
|
|
74
82
|
# Tools
|
|
75
83
|
"Tool",
|
|
76
84
|
"ToolParameter",
|
|
@@ -13,6 +13,12 @@ from casual_llm.messages import (
|
|
|
13
13
|
ChatMessage,
|
|
14
14
|
AssistantToolCall,
|
|
15
15
|
AssistantToolCallFunction,
|
|
16
|
+
TextContent,
|
|
17
|
+
ImageContent,
|
|
18
|
+
)
|
|
19
|
+
from casual_llm.utils.image import (
|
|
20
|
+
strip_base64_prefix,
|
|
21
|
+
fetch_image_as_base64,
|
|
16
22
|
)
|
|
17
23
|
|
|
18
24
|
if TYPE_CHECKING:
|
|
@@ -21,23 +27,101 @@ if TYPE_CHECKING:
|
|
|
21
27
|
logger = logging.getLogger(__name__)
|
|
22
28
|
|
|
23
29
|
|
|
24
|
-
def
|
|
30
|
+
async def _convert_image_to_ollama(image: ImageContent) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Convert ImageContent to Ollama base64 format.
|
|
33
|
+
|
|
34
|
+
Ollama expects images as raw base64 strings (no data URI prefix).
|
|
35
|
+
|
|
36
|
+
For URL sources, this function fetches the image and converts to base64.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ImageFetchError: If a URL image cannot be fetched.
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(image.source, str):
|
|
42
|
+
# Check if it's a data URI or a URL
|
|
43
|
+
if image.source.startswith("data:"):
|
|
44
|
+
# Data URI - extract base64 data
|
|
45
|
+
return strip_base64_prefix(image.source)
|
|
46
|
+
else:
|
|
47
|
+
# Regular URL - fetch and convert to base64
|
|
48
|
+
logger.debug(f"Fetching image from URL for Ollama: {image.source}")
|
|
49
|
+
base64_data, _ = await fetch_image_as_base64(image.source)
|
|
50
|
+
return base64_data
|
|
51
|
+
else:
|
|
52
|
+
# Base64 dict source - use directly
|
|
53
|
+
base64_data = image.source.get("data", "")
|
|
54
|
+
# Strip any data URI prefix that might be present
|
|
55
|
+
return strip_base64_prefix(base64_data)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _convert_user_content_to_ollama(
|
|
59
|
+
content: str | list[TextContent | ImageContent] | None,
|
|
60
|
+
) -> tuple[str, list[str]]:
|
|
61
|
+
"""
|
|
62
|
+
Convert UserMessage content to Ollama format.
|
|
63
|
+
|
|
64
|
+
Handles both simple string content (backward compatible) and
|
|
65
|
+
multimodal content arrays (text + images).
|
|
66
|
+
|
|
67
|
+
Ollama uses a format where text goes in "content" and images
|
|
68
|
+
go in a separate "images" array as raw base64 strings.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A tuple of (text_content, images_list) where:
|
|
72
|
+
- text_content: Combined text from all TextContent items
|
|
73
|
+
- images_list: List of base64-encoded image strings
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ImageFetchError: If a URL image cannot be fetched.
|
|
77
|
+
"""
|
|
78
|
+
if content is None:
|
|
79
|
+
return "", []
|
|
80
|
+
|
|
81
|
+
if isinstance(content, str):
|
|
82
|
+
# Simple string content
|
|
83
|
+
return content, []
|
|
84
|
+
|
|
85
|
+
# Multimodal content array
|
|
86
|
+
text_parts: list[str] = []
|
|
87
|
+
images: list[str] = []
|
|
88
|
+
|
|
89
|
+
for item in content:
|
|
90
|
+
if isinstance(item, TextContent):
|
|
91
|
+
text_parts.append(item.text)
|
|
92
|
+
elif isinstance(item, ImageContent):
|
|
93
|
+
images.append(await _convert_image_to_ollama(item))
|
|
94
|
+
|
|
95
|
+
# Join text parts with newlines
|
|
96
|
+
text_content = "\n".join(text_parts) if text_parts else ""
|
|
97
|
+
|
|
98
|
+
return text_content, images
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def convert_messages_to_ollama(messages: list[ChatMessage]) -> list[dict[str, Any]]:
|
|
25
102
|
"""
|
|
26
103
|
Convert casual-llm ChatMessage list to Ollama format.
|
|
27
104
|
|
|
28
105
|
Unlike OpenAI which expects tool call arguments as JSON strings,
|
|
29
106
|
Ollama expects them as dictionaries. This function handles that conversion.
|
|
30
107
|
|
|
108
|
+
Supports multimodal messages with images. Ollama expects images as raw
|
|
109
|
+
base64 strings in a separate "images" array.
|
|
110
|
+
|
|
31
111
|
Args:
|
|
32
112
|
messages: List of ChatMessage objects
|
|
33
113
|
|
|
34
114
|
Returns:
|
|
35
115
|
List of dictionaries in Ollama message format
|
|
36
116
|
|
|
117
|
+
Raises:
|
|
118
|
+
ImageFetchError: If a URL image cannot be fetched.
|
|
119
|
+
|
|
37
120
|
Examples:
|
|
121
|
+
>>> import asyncio
|
|
38
122
|
>>> from casual_llm import UserMessage
|
|
39
123
|
>>> messages = [UserMessage(content="Hello")]
|
|
40
|
-
>>> ollama_msgs = convert_messages_to_ollama(messages)
|
|
124
|
+
>>> ollama_msgs = asyncio.run(convert_messages_to_ollama(messages))
|
|
41
125
|
>>> ollama_msgs[0]["role"]
|
|
42
126
|
'user'
|
|
43
127
|
"""
|
|
@@ -97,7 +181,11 @@ def convert_messages_to_ollama(messages: list[ChatMessage]) -> list[dict[str, An
|
|
|
97
181
|
)
|
|
98
182
|
|
|
99
183
|
case "user":
|
|
100
|
-
|
|
184
|
+
text_content, images = await _convert_user_content_to_ollama(msg.content)
|
|
185
|
+
user_message: dict[str, Any] = {"role": "user", "content": text_content}
|
|
186
|
+
if images:
|
|
187
|
+
user_message["images"] = images
|
|
188
|
+
ollama_messages.append(user_message)
|
|
101
189
|
|
|
102
190
|
case _:
|
|
103
191
|
logger.warning(f"Unknown message role: {msg.role}")
|
|
@@ -11,6 +11,8 @@ from casual_llm.messages import (
|
|
|
11
11
|
ChatMessage,
|
|
12
12
|
AssistantToolCall,
|
|
13
13
|
AssistantToolCallFunction,
|
|
14
|
+
TextContent,
|
|
15
|
+
ImageContent,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
if TYPE_CHECKING:
|
|
@@ -19,6 +21,55 @@ if TYPE_CHECKING:
|
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
20
22
|
|
|
21
23
|
|
|
24
|
+
def _convert_image_to_openai(image: ImageContent) -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Convert ImageContent to OpenAI image_url format.
|
|
27
|
+
|
|
28
|
+
OpenAI expects images in the format:
|
|
29
|
+
{"type": "image_url", "image_url": {"url": "..."}}
|
|
30
|
+
|
|
31
|
+
For base64 images, the URL should be a data URI:
|
|
32
|
+
data:image/jpeg;base64,...
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(image.source, str):
|
|
35
|
+
# URL source - use directly
|
|
36
|
+
image_url = image.source
|
|
37
|
+
else:
|
|
38
|
+
# Base64 dict source - construct data URI
|
|
39
|
+
base64_data = image.source.get("data", "")
|
|
40
|
+
image_url = f"data:{image.media_type};base64,{base64_data}"
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"type": "image_url",
|
|
44
|
+
"image_url": {"url": image_url},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _convert_user_content_to_openai(
|
|
49
|
+
content: str | list[TextContent | ImageContent] | None,
|
|
50
|
+
) -> str | list[dict[str, Any]] | None:
|
|
51
|
+
"""
|
|
52
|
+
Convert UserMessage content to OpenAI format.
|
|
53
|
+
|
|
54
|
+
Handles both simple string content (backward compatible) and
|
|
55
|
+
multimodal content arrays (text + images).
|
|
56
|
+
"""
|
|
57
|
+
if content is None or isinstance(content, str):
|
|
58
|
+
# Simple string content or None - pass through
|
|
59
|
+
return content
|
|
60
|
+
|
|
61
|
+
# Multimodal content array
|
|
62
|
+
openai_content: list[dict[str, Any]] = []
|
|
63
|
+
|
|
64
|
+
for item in content:
|
|
65
|
+
if isinstance(item, TextContent):
|
|
66
|
+
openai_content.append({"type": "text", "text": item.text})
|
|
67
|
+
elif isinstance(item, ImageContent):
|
|
68
|
+
openai_content.append(_convert_image_to_openai(item))
|
|
69
|
+
|
|
70
|
+
return openai_content
|
|
71
|
+
|
|
72
|
+
|
|
22
73
|
def convert_messages_to_openai(messages: list[ChatMessage]) -> list[dict[str, Any]]:
|
|
23
74
|
"""
|
|
24
75
|
Convert casual-llm ChatMessage list to OpenAI format.
|
|
@@ -86,7 +137,12 @@ def convert_messages_to_openai(messages: list[ChatMessage]) -> list[dict[str, An
|
|
|
86
137
|
)
|
|
87
138
|
|
|
88
139
|
case "user":
|
|
89
|
-
openai_messages.append(
|
|
140
|
+
openai_messages.append(
|
|
141
|
+
{
|
|
142
|
+
"role": "user",
|
|
143
|
+
"content": _convert_user_content_to_openai(msg.content),
|
|
144
|
+
}
|
|
145
|
+
)
|
|
90
146
|
|
|
91
147
|
case _:
|
|
92
148
|
logger.warning(f"Unknown message role: {msg.role}")
|
casual_llm/messages.py
CHANGED
|
@@ -10,6 +10,37 @@ from typing import Literal, TypeAlias
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class TextContent(BaseModel):
|
|
14
|
+
"""Text content block for multimodal messages."""
|
|
15
|
+
|
|
16
|
+
type: Literal["text"] = "text"
|
|
17
|
+
text: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ImageContent(BaseModel):
|
|
21
|
+
"""Image content block for multimodal messages.
|
|
22
|
+
|
|
23
|
+
Supports both URL strings and base64-encoded image data.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
# URL source
|
|
27
|
+
ImageContent(type="image", source="https://example.com/image.jpg")
|
|
28
|
+
|
|
29
|
+
# Base64 source (dict format)
|
|
30
|
+
ImageContent(
|
|
31
|
+
type="image",
|
|
32
|
+
source={"type": "base64", "data": "...base64..."},
|
|
33
|
+
media_type="image/png"
|
|
34
|
+
)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
type: Literal["image"] = "image"
|
|
38
|
+
source: str | dict[str, str]
|
|
39
|
+
"""URL string or dict with {type: "base64", data: "..."} format."""
|
|
40
|
+
media_type: str = "image/jpeg"
|
|
41
|
+
"""MIME type of the image (e.g., image/jpeg, image/png, image/gif, image/webp)."""
|
|
42
|
+
|
|
43
|
+
|
|
13
44
|
class AssistantToolCallFunction(BaseModel):
|
|
14
45
|
"""Function call within an assistant tool call."""
|
|
15
46
|
|
|
@@ -50,10 +81,30 @@ class ToolResultMessage(BaseModel):
|
|
|
50
81
|
|
|
51
82
|
|
|
52
83
|
class UserMessage(BaseModel):
|
|
53
|
-
"""Message from the user.
|
|
84
|
+
"""Message from the user.
|
|
85
|
+
|
|
86
|
+
Supports both simple text content and multimodal content (text + images).
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
# Simple text content
|
|
90
|
+
UserMessage(content="Hello, world!")
|
|
91
|
+
|
|
92
|
+
# Multimodal content
|
|
93
|
+
UserMessage(content=[
|
|
94
|
+
TextContent(type="text", text="What's in this image?"),
|
|
95
|
+
ImageContent(type="image", source="https://example.com/image.jpg")
|
|
96
|
+
])
|
|
97
|
+
"""
|
|
54
98
|
|
|
55
99
|
role: Literal["user"] = "user"
|
|
56
|
-
content: str | None
|
|
100
|
+
content: str | list[TextContent | ImageContent] | None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class StreamChunk(BaseModel):
|
|
104
|
+
"""A chunk of streamed response content from an LLM provider."""
|
|
105
|
+
|
|
106
|
+
content: str
|
|
107
|
+
finish_reason: str | None = None
|
|
57
108
|
|
|
58
109
|
|
|
59
110
|
ChatMessage: TypeAlias = AssistantMessage | SystemMessage | ToolResultMessage | UserMessage
|
casual_llm/providers/base.py
CHANGED
|
@@ -7,11 +7,11 @@ using standard OpenAI-compatible message formats.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from typing import Protocol, Literal
|
|
10
|
+
from typing import Protocol, Literal, AsyncIterator
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel
|
|
13
13
|
|
|
14
|
-
from casual_llm.messages import ChatMessage, AssistantMessage
|
|
14
|
+
from casual_llm.messages import ChatMessage, AssistantMessage, StreamChunk
|
|
15
15
|
from casual_llm.tools import Tool
|
|
16
16
|
from casual_llm.usage import Usage
|
|
17
17
|
|
|
@@ -77,6 +77,52 @@ class LLMProvider(Protocol):
|
|
|
77
77
|
"""
|
|
78
78
|
...
|
|
79
79
|
|
|
80
|
+
def stream(
|
|
81
|
+
self,
|
|
82
|
+
messages: list[ChatMessage],
|
|
83
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
84
|
+
max_tokens: int | None = None,
|
|
85
|
+
tools: list[Tool] | None = None,
|
|
86
|
+
temperature: float | None = None,
|
|
87
|
+
) -> AsyncIterator[StreamChunk]:
|
|
88
|
+
"""
|
|
89
|
+
Stream a chat response from the LLM.
|
|
90
|
+
|
|
91
|
+
This method yields response chunks in real-time as they are generated,
|
|
92
|
+
enabling progressive display in chat interfaces.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
messages: List of ChatMessage (UserMessage, AssistantMessage, SystemMessage, etc.)
|
|
96
|
+
response_format: Expected response format. Can be "json", "text", or a Pydantic
|
|
97
|
+
BaseModel class for JSON Schema-based structured output. When a Pydantic model
|
|
98
|
+
is provided, the LLM will be instructed to return JSON matching the schema.
|
|
99
|
+
max_tokens: Maximum tokens to generate (optional)
|
|
100
|
+
tools: List of tools available for the LLM to call (optional, may not work
|
|
101
|
+
with all providers during streaming)
|
|
102
|
+
temperature: Temperature for this request (optional, overrides instance temperature)
|
|
103
|
+
|
|
104
|
+
Yields:
|
|
105
|
+
StreamChunk objects containing content fragments as tokens are generated.
|
|
106
|
+
Each chunk has a `content` attribute with the text fragment.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
Provider-specific exceptions (httpx.HTTPError, openai.OpenAIError, etc.)
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
>>> from casual_llm import UserMessage
|
|
113
|
+
>>>
|
|
114
|
+
>>> # Stream response and print tokens as they arrive
|
|
115
|
+
>>> async for chunk in provider.stream([UserMessage(content="Tell me a story")]):
|
|
116
|
+
... print(chunk.content, end="", flush=True)
|
|
117
|
+
>>>
|
|
118
|
+
>>> # Collect full response from stream
|
|
119
|
+
>>> chunks = []
|
|
120
|
+
>>> async for chunk in provider.stream([UserMessage(content="Hello")]):
|
|
121
|
+
... chunks.append(chunk.content)
|
|
122
|
+
>>> full_response = "".join(chunks)
|
|
123
|
+
"""
|
|
124
|
+
...
|
|
125
|
+
|
|
80
126
|
def get_usage(self) -> Usage | None:
|
|
81
127
|
"""
|
|
82
128
|
Get token usage statistics from the last chat() call.
|
casual_llm/providers/ollama.py
CHANGED
|
@@ -5,11 +5,11 @@ Ollama LLM provider using the official ollama library.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Any, Literal
|
|
8
|
+
from typing import Any, AsyncIterator, Literal
|
|
9
9
|
from ollama import AsyncClient
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
|
-
from casual_llm.messages import ChatMessage, AssistantMessage
|
|
12
|
+
from casual_llm.messages import ChatMessage, AssistantMessage, StreamChunk
|
|
13
13
|
from casual_llm.tools import Tool
|
|
14
14
|
from casual_llm.usage import Usage
|
|
15
15
|
from casual_llm.tool_converters import tools_to_ollama
|
|
@@ -42,7 +42,8 @@ class OllamaProvider:
|
|
|
42
42
|
Args:
|
|
43
43
|
model: Model name (e.g., "qwen2.5:7b-instruct")
|
|
44
44
|
host: Ollama server URL (e.g., "http://localhost:11434")
|
|
45
|
-
temperature: Temperature for generation (0.0-1.0, optional - uses Ollama
|
|
45
|
+
temperature: Temperature for generation (0.0-1.0, optional - uses Ollama
|
|
46
|
+
default if not set)
|
|
46
47
|
timeout: HTTP request timeout in seconds
|
|
47
48
|
"""
|
|
48
49
|
self.model = model
|
|
@@ -108,8 +109,8 @@ class OllamaProvider:
|
|
|
108
109
|
... response_format=PersonInfo # Pass the class, not an instance
|
|
109
110
|
... )
|
|
110
111
|
"""
|
|
111
|
-
# Convert messages to Ollama format using converter
|
|
112
|
-
chat_messages = convert_messages_to_ollama(messages)
|
|
112
|
+
# Convert messages to Ollama format using converter (async for image support)
|
|
113
|
+
chat_messages = await convert_messages_to_ollama(messages)
|
|
113
114
|
logger.debug(f"Converted {len(messages)} messages to Ollama format")
|
|
114
115
|
|
|
115
116
|
# Use provided temperature or fall back to instance temperature
|
|
@@ -173,3 +174,90 @@ class OllamaProvider:
|
|
|
173
174
|
content = response_message.content.strip() if response_message.content else ""
|
|
174
175
|
logger.debug(f"Generated {len(content)} characters")
|
|
175
176
|
return AssistantMessage(content=content, tool_calls=tool_calls)
|
|
177
|
+
|
|
178
|
+
async def stream(
|
|
179
|
+
self,
|
|
180
|
+
messages: list[ChatMessage],
|
|
181
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
182
|
+
max_tokens: int | None = None,
|
|
183
|
+
tools: list[Tool] | None = None,
|
|
184
|
+
temperature: float | None = None,
|
|
185
|
+
) -> AsyncIterator[StreamChunk]:
|
|
186
|
+
"""
|
|
187
|
+
Stream a chat response from Ollama.
|
|
188
|
+
|
|
189
|
+
This method yields response chunks in real-time as they are generated,
|
|
190
|
+
enabling progressive display in chat interfaces.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
messages: Conversation messages (ChatMessage format)
|
|
194
|
+
response_format: "json" for JSON output, "text" for plain text, or a Pydantic
|
|
195
|
+
BaseModel class for JSON Schema-based structured output. When a Pydantic
|
|
196
|
+
model is provided, the LLM will be instructed to return JSON matching the
|
|
197
|
+
schema.
|
|
198
|
+
max_tokens: Maximum tokens to generate (optional)
|
|
199
|
+
tools: List of tools available for the LLM to call (optional, may not work
|
|
200
|
+
with all streaming scenarios)
|
|
201
|
+
temperature: Temperature for this request (optional, overrides instance temperature)
|
|
202
|
+
|
|
203
|
+
Yields:
|
|
204
|
+
StreamChunk objects containing content fragments as tokens are generated.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
ResponseError: If the request could not be fulfilled
|
|
208
|
+
RequestError: If the request was invalid
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
>>> async for chunk in provider.stream([UserMessage(content="Hello")]):
|
|
212
|
+
... print(chunk.content, end="", flush=True)
|
|
213
|
+
"""
|
|
214
|
+
# Convert messages to Ollama format using converter (async for image support)
|
|
215
|
+
chat_messages = await convert_messages_to_ollama(messages)
|
|
216
|
+
logger.debug(f"Converted {len(messages)} messages to Ollama format for streaming")
|
|
217
|
+
|
|
218
|
+
# Use provided temperature or fall back to instance temperature
|
|
219
|
+
temp = temperature if temperature is not None else self.temperature
|
|
220
|
+
|
|
221
|
+
# Build options
|
|
222
|
+
options: dict[str, Any] = {}
|
|
223
|
+
if temp is not None:
|
|
224
|
+
options["temperature"] = temp
|
|
225
|
+
if max_tokens:
|
|
226
|
+
options["num_predict"] = max_tokens
|
|
227
|
+
|
|
228
|
+
# Build request kwargs
|
|
229
|
+
request_kwargs: dict[str, Any] = {
|
|
230
|
+
"model": self.model,
|
|
231
|
+
"messages": chat_messages,
|
|
232
|
+
"stream": True,
|
|
233
|
+
"options": options,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Handle response_format: "json", "text", or Pydantic model class
|
|
237
|
+
if response_format == "json":
|
|
238
|
+
request_kwargs["format"] = "json"
|
|
239
|
+
elif isinstance(response_format, type) and issubclass(response_format, BaseModel):
|
|
240
|
+
# Extract JSON Schema from Pydantic model and pass directly to format
|
|
241
|
+
schema = response_format.model_json_schema()
|
|
242
|
+
request_kwargs["format"] = schema
|
|
243
|
+
logger.debug(f"Using JSON Schema from Pydantic model: {response_format.__name__}")
|
|
244
|
+
# "text" is the default - no format parameter needed
|
|
245
|
+
|
|
246
|
+
# Add tools if provided
|
|
247
|
+
if tools:
|
|
248
|
+
converted_tools = tools_to_ollama(tools)
|
|
249
|
+
request_kwargs["tools"] = converted_tools
|
|
250
|
+
logger.debug(f"Added {len(converted_tools)} tools to streaming request")
|
|
251
|
+
|
|
252
|
+
logger.debug(f"Starting stream with model {self.model}")
|
|
253
|
+
stream = await self.client.chat(**request_kwargs)
|
|
254
|
+
|
|
255
|
+
async for chunk in stream:
|
|
256
|
+
# Extract content from the message if present
|
|
257
|
+
if chunk.message and chunk.message.content:
|
|
258
|
+
content = chunk.message.content
|
|
259
|
+
# Ollama uses 'done' field to indicate completion
|
|
260
|
+
finish_reason = "stop" if getattr(chunk, "done", False) else None
|
|
261
|
+
yield StreamChunk(content=content, finish_reason=finish_reason)
|
|
262
|
+
|
|
263
|
+
logger.debug("Stream completed")
|
casual_llm/providers/openai.py
CHANGED
|
@@ -5,11 +5,11 @@ OpenAI LLM provider (compatible with OpenAI API and compatible services).
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Literal, Any
|
|
8
|
+
from typing import Literal, Any, AsyncIterator
|
|
9
9
|
from openai import AsyncOpenAI
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
|
-
from casual_llm.messages import ChatMessage, AssistantMessage
|
|
12
|
+
from casual_llm.messages import ChatMessage, AssistantMessage, StreamChunk
|
|
13
13
|
from casual_llm.tools import Tool
|
|
14
14
|
from casual_llm.usage import Usage
|
|
15
15
|
from casual_llm.tool_converters import tools_to_openai
|
|
@@ -46,7 +46,8 @@ class OpenAIProvider:
|
|
|
46
46
|
api_key: API key (optional, can use OPENAI_API_KEY env var)
|
|
47
47
|
base_url: Base URL for API (e.g., "https://openrouter.ai/api/v1")
|
|
48
48
|
organization: OpenAI organization ID (optional)
|
|
49
|
-
temperature: Temperature for generation (0.0-1.0, optional - uses OpenAI
|
|
49
|
+
temperature: Temperature for generation (0.0-1.0, optional - uses OpenAI
|
|
50
|
+
default if not set)
|
|
50
51
|
timeout: HTTP request timeout in seconds
|
|
51
52
|
extra_kwargs: Additional kwargs to pass to client.chat.completions.create()
|
|
52
53
|
"""
|
|
@@ -191,3 +192,96 @@ class OpenAIProvider:
|
|
|
191
192
|
content = response_message.content or ""
|
|
192
193
|
logger.debug(f"Generated {len(content)} characters")
|
|
193
194
|
return AssistantMessage(content=content, tool_calls=tool_calls)
|
|
195
|
+
|
|
196
|
+
async def stream(
|
|
197
|
+
self,
|
|
198
|
+
messages: list[ChatMessage],
|
|
199
|
+
response_format: Literal["json", "text"] | type[BaseModel] = "text",
|
|
200
|
+
max_tokens: int | None = None,
|
|
201
|
+
tools: list[Tool] | None = None,
|
|
202
|
+
temperature: float | None = None,
|
|
203
|
+
) -> AsyncIterator[StreamChunk]:
|
|
204
|
+
"""
|
|
205
|
+
Stream a chat response from OpenAI API.
|
|
206
|
+
|
|
207
|
+
This method yields response chunks in real-time as they are generated,
|
|
208
|
+
enabling progressive display in chat interfaces.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
messages: Conversation messages (ChatMessage format)
|
|
212
|
+
response_format: "json" for JSON output, "text" for plain text, or a Pydantic
|
|
213
|
+
BaseModel class for JSON Schema-based structured output. When a Pydantic
|
|
214
|
+
model is provided, the LLM will be instructed to return JSON matching the
|
|
215
|
+
schema.
|
|
216
|
+
max_tokens: Maximum tokens to generate (optional)
|
|
217
|
+
tools: List of tools available for the LLM to call (optional, may not work
|
|
218
|
+
with all streaming scenarios)
|
|
219
|
+
temperature: Temperature for this request (optional, overrides instance temperature)
|
|
220
|
+
|
|
221
|
+
Yields:
|
|
222
|
+
StreamChunk objects containing content fragments as tokens are generated.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
openai.OpenAIError: If request fails
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
>>> async for chunk in provider.stream([UserMessage(content="Hello")]):
|
|
229
|
+
... print(chunk.content, end="", flush=True)
|
|
230
|
+
"""
|
|
231
|
+
# Convert messages to OpenAI format using converter
|
|
232
|
+
chat_messages = convert_messages_to_openai(messages)
|
|
233
|
+
logger.debug(f"Converted {len(messages)} messages to OpenAI format for streaming")
|
|
234
|
+
|
|
235
|
+
# Use provided temperature or fall back to instance temperature
|
|
236
|
+
temp = temperature if temperature is not None else self.temperature
|
|
237
|
+
|
|
238
|
+
# Build request kwargs
|
|
239
|
+
request_kwargs: dict[str, Any] = {
|
|
240
|
+
"model": self.model,
|
|
241
|
+
"messages": chat_messages,
|
|
242
|
+
"stream": True,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Only add temperature if specified
|
|
246
|
+
if temp is not None:
|
|
247
|
+
request_kwargs["temperature"] = temp
|
|
248
|
+
|
|
249
|
+
# Handle response_format: "json", "text", or Pydantic model class
|
|
250
|
+
if response_format == "json":
|
|
251
|
+
request_kwargs["response_format"] = {"type": "json_object"}
|
|
252
|
+
elif isinstance(response_format, type) and issubclass(response_format, BaseModel):
|
|
253
|
+
# Extract JSON Schema from Pydantic model
|
|
254
|
+
schema = response_format.model_json_schema()
|
|
255
|
+
request_kwargs["response_format"] = {
|
|
256
|
+
"type": "json_schema",
|
|
257
|
+
"json_schema": {
|
|
258
|
+
"name": response_format.__name__,
|
|
259
|
+
"schema": schema,
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
logger.debug(f"Using JSON Schema from Pydantic model: {response_format.__name__}")
|
|
263
|
+
# "text" is the default - no response_format needed
|
|
264
|
+
|
|
265
|
+
if max_tokens:
|
|
266
|
+
request_kwargs["max_tokens"] = max_tokens
|
|
267
|
+
|
|
268
|
+
# Add tools if provided
|
|
269
|
+
if tools:
|
|
270
|
+
converted_tools = tools_to_openai(tools)
|
|
271
|
+
request_kwargs["tools"] = converted_tools
|
|
272
|
+
logger.debug(f"Added {len(converted_tools)} tools to streaming request")
|
|
273
|
+
|
|
274
|
+
# Merge extra kwargs
|
|
275
|
+
request_kwargs.update(self.extra_kwargs)
|
|
276
|
+
|
|
277
|
+
logger.debug(f"Starting stream with model {self.model}")
|
|
278
|
+
stream = await self.client.chat.completions.create(**request_kwargs)
|
|
279
|
+
|
|
280
|
+
async for chunk in stream:
|
|
281
|
+
# Extract content from the delta if present
|
|
282
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
283
|
+
content = chunk.choices[0].delta.content
|
|
284
|
+
finish_reason = chunk.choices[0].finish_reason
|
|
285
|
+
yield StreamChunk(content=content, finish_reason=finish_reason)
|
|
286
|
+
|
|
287
|
+
logger.debug("Stream completed")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image utilities for fetching and processing images.
|
|
3
|
+
|
|
4
|
+
Provides async utilities for downloading images from URLs and converting
|
|
5
|
+
them to base64 format for use in multimodal LLM messages.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
HTTPX_AVAILABLE = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
HTTPX_AVAILABLE = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ImageFetchError(Exception):
|
|
19
|
+
"""Raised when image fetching fails."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Default timeout for image fetching (in seconds)
|
|
25
|
+
DEFAULT_TIMEOUT = 30.0
|
|
26
|
+
|
|
27
|
+
# Maximum image size in bytes (10 MB)
|
|
28
|
+
MAX_IMAGE_SIZE = 10 * 1024 * 1024
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def fetch_image_as_base64(
|
|
32
|
+
url: str,
|
|
33
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
34
|
+
max_size: int = MAX_IMAGE_SIZE,
|
|
35
|
+
) -> tuple[str, str]:
|
|
36
|
+
"""Fetch an image from a URL and return it as base64-encoded data.
|
|
37
|
+
|
|
38
|
+
Downloads the image from the given URL and returns the base64-encoded
|
|
39
|
+
content along with the detected media type.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
url: The URL of the image to fetch.
|
|
43
|
+
timeout: Request timeout in seconds. Defaults to 30 seconds.
|
|
44
|
+
max_size: Maximum allowed image size in bytes. Defaults to 10 MB.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A tuple of (base64_data, media_type) where:
|
|
48
|
+
- base64_data: The raw base64-encoded image data (no data: prefix)
|
|
49
|
+
- media_type: The MIME type of the image (e.g., "image/jpeg")
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ImageFetchError: If the image cannot be fetched, is too large,
|
|
53
|
+
or if httpx is not installed.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> base64_data, media_type = await fetch_image_as_base64(
|
|
57
|
+
... "https://example.com/image.jpg"
|
|
58
|
+
... )
|
|
59
|
+
>>> print(media_type)
|
|
60
|
+
image/jpeg
|
|
61
|
+
"""
|
|
62
|
+
if not HTTPX_AVAILABLE:
|
|
63
|
+
raise ImageFetchError(
|
|
64
|
+
"httpx is required for fetching images from URLs. "
|
|
65
|
+
"Install it with: pip install 'httpx[http2]'"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Use a browser-like User-Agent and HTTP/2 to avoid being blocked by sites like Wikipedia
|
|
70
|
+
headers = {
|
|
71
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
72
|
+
}
|
|
73
|
+
async with httpx.AsyncClient(timeout=timeout, headers=headers, http2=True) as client:
|
|
74
|
+
response = await client.get(url)
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
|
|
77
|
+
# Check content length if available
|
|
78
|
+
content_length = response.headers.get("content-length")
|
|
79
|
+
if content_length and int(content_length) > max_size:
|
|
80
|
+
raise ImageFetchError(
|
|
81
|
+
f"Image size ({int(content_length)} bytes) exceeds "
|
|
82
|
+
f"maximum allowed size ({max_size} bytes)"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Read content and check actual size
|
|
86
|
+
content = response.content
|
|
87
|
+
if len(content) > max_size:
|
|
88
|
+
raise ImageFetchError(
|
|
89
|
+
f"Image size ({len(content)} bytes) exceeds "
|
|
90
|
+
f"maximum allowed size ({max_size} bytes)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Extract media type from Content-Type header
|
|
94
|
+
content_type = response.headers.get("content-type", "image/jpeg")
|
|
95
|
+
# Remove any charset or boundary info (e.g., "image/jpeg; charset=utf-8")
|
|
96
|
+
media_type = content_type.split(";")[0].strip()
|
|
97
|
+
|
|
98
|
+
# Validate that it looks like an image type
|
|
99
|
+
if not media_type.startswith("image/"):
|
|
100
|
+
# Default to image/jpeg if content-type doesn't indicate an image
|
|
101
|
+
media_type = "image/jpeg"
|
|
102
|
+
|
|
103
|
+
# Encode to base64
|
|
104
|
+
base64_data = base64.b64encode(content).decode("ascii")
|
|
105
|
+
|
|
106
|
+
return base64_data, media_type
|
|
107
|
+
|
|
108
|
+
except httpx.HTTPStatusError as e:
|
|
109
|
+
raise ImageFetchError(
|
|
110
|
+
f"HTTP error fetching image from {url}: {e.response.status_code}"
|
|
111
|
+
) from e
|
|
112
|
+
except httpx.TimeoutException as e:
|
|
113
|
+
raise ImageFetchError(f"Timeout fetching image from {url}") from e
|
|
114
|
+
except httpx.RequestError as e:
|
|
115
|
+
raise ImageFetchError(f"Error fetching image from {url}: {e}") from e
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def strip_base64_prefix(data: str) -> str:
|
|
119
|
+
"""Strip the data URI prefix from a base64-encoded string.
|
|
120
|
+
|
|
121
|
+
Removes the 'data:<media_type>;base64,' prefix if present,
|
|
122
|
+
returning only the raw base64 data.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
data: A base64 string, optionally with a data URI prefix.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The raw base64 data without any prefix.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> strip_base64_prefix("data:image/png;base64,abc123")
|
|
132
|
+
'abc123'
|
|
133
|
+
>>> strip_base64_prefix("abc123")
|
|
134
|
+
'abc123'
|
|
135
|
+
"""
|
|
136
|
+
# Check for data URI format: data:<media_type>;base64,<data>
|
|
137
|
+
if data.startswith("data:") and ";base64," in data:
|
|
138
|
+
# Split on ";base64," and return the data portion
|
|
139
|
+
return data.split(";base64,", 1)[1]
|
|
140
|
+
return data
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def add_base64_prefix(base64_data: str, media_type: str = "image/png") -> str:
|
|
144
|
+
"""Add a data URI prefix to raw base64 data.
|
|
145
|
+
|
|
146
|
+
Creates a complete data URI by prepending the appropriate prefix
|
|
147
|
+
to raw base64-encoded data.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
base64_data: The raw base64-encoded data (without prefix).
|
|
151
|
+
media_type: The MIME type of the data. Defaults to "image/png".
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A complete data URI string.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> add_base64_prefix("abc123", "image/png")
|
|
158
|
+
'data:image/png;base64,abc123'
|
|
159
|
+
>>> add_base64_prefix("xyz789", "image/jpeg")
|
|
160
|
+
'data:image/jpeg;base64,xyz789'
|
|
161
|
+
"""
|
|
162
|
+
return f"data:{media_type};base64,{base64_data}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casual-llm
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Lightweight LLM provider abstraction with standardized message models
|
|
5
5
|
Author-email: Alex Stansfield <alex@casualgenius.com>
|
|
6
6
|
License: MIT
|
|
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: pydantic>=2.0.0
|
|
25
25
|
Requires-Dist: ollama>=0.6.1
|
|
26
|
+
Requires-Dist: httpx[http2]>=0.28.1
|
|
26
27
|
Provides-Extra: openai
|
|
27
28
|
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
28
29
|
Dynamic: license-file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
casual_llm/__init__.py,sha256=z6NcQ71k1nRY5bPYeBLcpyEsFJ3RIEpoxEq4xgO5lB8,2117
|
|
2
|
+
casual_llm/config.py,sha256=ofGJeHfbJupGSSdMZlsLGZ3RH07A26nQso-4XDMBkVA,1808
|
|
3
|
+
casual_llm/messages.py,sha256=x593dOc2K1Yoj_nbqPP6oewKBSlwVZTUeWKpmJM2WlA,2881
|
|
4
|
+
casual_llm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
casual_llm/tools.py,sha256=AQiGhjJgbwEVd0mHP2byqJ8U4F5XgTK0P9LAmkpdCpA,4045
|
|
6
|
+
casual_llm/usage.py,sha256=-_Dj_do4Fgn3TVrcohG68Giy6SmXaF0p7_GxzKEK2jw,936
|
|
7
|
+
casual_llm/message_converters/__init__.py,sha256=5cuQLm2A7hfzZipoDXItdgJqLLME1KEYCE5dvuqyQ48,608
|
|
8
|
+
casual_llm/message_converters/ollama.py,sha256=PKy0jexhWHnYsuZBrU55W80RyUL5RZyTrwqWWono3Yg,8006
|
|
9
|
+
casual_llm/message_converters/openai.py,sha256=UMj11jCZRj4B1hlIGs_ORkrZjJFs2-ZcMzZNRV_5vxk,5706
|
|
10
|
+
casual_llm/providers/__init__.py,sha256=lVsN9GqU1cQjIEJEL6DbsIzPkknBDggx-_5q_yxtDAA,2525
|
|
11
|
+
casual_llm/providers/base.py,sha256=W7Heb0nW0RfiGkBv2qYb3jV6RfCZyvrou6Vf8X4RkLg,5483
|
|
12
|
+
casual_llm/providers/ollama.py,sha256=en3NuUgVMYCWlFsUT1WRwh_vdfsU4puuESyUu3EkhCE,10390
|
|
13
|
+
casual_llm/providers/openai.py,sha256=NSbCwGac6dOdrf3vG9BnurlJkMiZixubia0K6OWq8FY,11059
|
|
14
|
+
casual_llm/tool_converters/__init__.py,sha256=4FN_r7drXk_prc2EOqrXfO8DNOAZwB0w3qvqhySQnXY,635
|
|
15
|
+
casual_llm/tool_converters/ollama.py,sha256=jaSco33-Px_KPUIzqIQC3BqkXG8180fyMeet3b7OUdo,1926
|
|
16
|
+
casual_llm/tool_converters/openai.py,sha256=n4Yi3rN19CwhwevwNb_XC4RagFrS3HohkUsu5A7b6Hw,1880
|
|
17
|
+
casual_llm/utils/__init__.py,sha256=E-XvHlYDjMidAW--CRVJHwvNnUJkiL88G9mVq1w71o0,142
|
|
18
|
+
casual_llm/utils/image.py,sha256=nlE7CoPSfc0cFUrFMzyVefJ8nAN3FAHYDicAIA4YK8I,5465
|
|
19
|
+
casual_llm-0.3.0.dist-info/licenses/LICENSE,sha256=-PvmTd5xNNGaePz8xckUDBswdIrPWi4L6m7EsyKLmb8,1072
|
|
20
|
+
casual_llm-0.3.0.dist-info/METADATA,sha256=CUF-Q7L3_HFWUZ6d7r3IiusTuxAj5LkiyXDeAqcNmRM,9883
|
|
21
|
+
casual_llm-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
casual_llm-0.3.0.dist-info/top_level.txt,sha256=KKXzOGm04MbK756RPCswL3zeJ6Z-DvaCWy48Mch3HAo,11
|
|
23
|
+
casual_llm-0.3.0.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
casual_llm/__init__.py,sha256=i8lKHDY8NWGDwozd5q52FtzfCkp5XPmxi0PwUxtVJ24,1945
|
|
2
|
-
casual_llm/config.py,sha256=ofGJeHfbJupGSSdMZlsLGZ3RH07A26nQso-4XDMBkVA,1808
|
|
3
|
-
casual_llm/messages.py,sha256=vLx46YzBn_P8UEplKq6vVe1oF2sJpQuJ67u2gU1asJQ,1436
|
|
4
|
-
casual_llm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
casual_llm/tools.py,sha256=AQiGhjJgbwEVd0mHP2byqJ8U4F5XgTK0P9LAmkpdCpA,4045
|
|
6
|
-
casual_llm/usage.py,sha256=-_Dj_do4Fgn3TVrcohG68Giy6SmXaF0p7_GxzKEK2jw,936
|
|
7
|
-
casual_llm/message_converters/__init__.py,sha256=5cuQLm2A7hfzZipoDXItdgJqLLME1KEYCE5dvuqyQ48,608
|
|
8
|
-
casual_llm/message_converters/ollama.py,sha256=Py4S22ObZcPmAAYMjyAlYMUeiGNxvahpBR0E6KTtm0g,5076
|
|
9
|
-
casual_llm/message_converters/openai.py,sha256=4KLgiDpmlcBnG4tRH_lRhUAYvuQKcB6J_1sW6ym6C7E,4028
|
|
10
|
-
casual_llm/providers/__init__.py,sha256=lVsN9GqU1cQjIEJEL6DbsIzPkknBDggx-_5q_yxtDAA,2525
|
|
11
|
-
casual_llm/providers/base.py,sha256=K4aiOiiCvAQ-mXnlurmo-gaX0fRrnrFLjdR0t7TXeuQ,3383
|
|
12
|
-
casual_llm/providers/ollama.py,sha256=fC2gmHvFn5Og9T8Ge6_e1gGEc_W15X28S4ay3TD8kL0,6492
|
|
13
|
-
casual_llm/providers/openai.py,sha256=Fg4YhEoXWxryorFJfDdFbTTcESyY-KNzgJwWQ_iJ-P0,7098
|
|
14
|
-
casual_llm/tool_converters/__init__.py,sha256=4FN_r7drXk_prc2EOqrXfO8DNOAZwB0w3qvqhySQnXY,635
|
|
15
|
-
casual_llm/tool_converters/ollama.py,sha256=jaSco33-Px_KPUIzqIQC3BqkXG8180fyMeet3b7OUdo,1926
|
|
16
|
-
casual_llm/tool_converters/openai.py,sha256=n4Yi3rN19CwhwevwNb_XC4RagFrS3HohkUsu5A7b6Hw,1880
|
|
17
|
-
casual_llm-0.2.0.dist-info/licenses/LICENSE,sha256=-PvmTd5xNNGaePz8xckUDBswdIrPWi4L6m7EsyKLmb8,1072
|
|
18
|
-
casual_llm-0.2.0.dist-info/METADATA,sha256=mOZxtsvp_S8C_VTm-JEycQ_zD5b-x2ruYMwYnE57F34,9847
|
|
19
|
-
casual_llm-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
-
casual_llm-0.2.0.dist-info/top_level.txt,sha256=KKXzOGm04MbK756RPCswL3zeJ6Z-DvaCWy48Mch3HAo,11
|
|
21
|
-
casual_llm-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|