livellm 1.1.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.
- livellm/__init__.py +15 -0
- livellm/livellm.py +236 -0
- livellm/models/__init__.py +41 -0
- livellm/models/agent/__init__.py +19 -0
- livellm/models/agent/agent.py +23 -0
- livellm/models/agent/chat.py +30 -0
- livellm/models/agent/tools.py +39 -0
- livellm/models/audio/__init__.py +12 -0
- livellm/models/audio/speak.py +23 -0
- livellm/models/audio/transcribe.py +48 -0
- livellm/models/common.py +46 -0
- livellm/models/fallback.py +25 -0
- livellm/py.typed +0 -0
- livellm-1.1.0.dist-info/METADATA +573 -0
- livellm-1.1.0.dist-info/RECORD +17 -0
- livellm-1.1.0.dist-info/WHEEL +4 -0
- livellm-1.1.0.dist-info/licenses/LICENSE +21 -0
livellm/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""LiveLLM Client - Python client for the LiveLLM Proxy and Realtime APIs."""
|
|
2
|
+
|
|
3
|
+
from .livellm import LivellmClient
|
|
4
|
+
from . import models
|
|
5
|
+
|
|
6
|
+
__version__ = "1.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
# Version
|
|
10
|
+
"__version__",
|
|
11
|
+
# Classes
|
|
12
|
+
"LivellmClient",
|
|
13
|
+
# Models
|
|
14
|
+
*models.__all__,
|
|
15
|
+
]
|
livellm/livellm.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""LiveLLM Client - Python client for the LiveLLM Proxy and Realtime APIs."""
|
|
2
|
+
import httpx
|
|
3
|
+
import json
|
|
4
|
+
from typing import List, Optional, AsyncIterator, Union
|
|
5
|
+
from .models.common import Settings, SuccessResponse
|
|
6
|
+
from .models.agent.agent import AgentRequest, AgentResponse
|
|
7
|
+
from .models.audio.speak import SpeakRequest
|
|
8
|
+
from .models.audio.transcribe import TranscribeRequest, TranscribeResponse, File
|
|
9
|
+
from .models.fallback import AgentFallbackRequest, AudioFallbackRequest, TranscribeFallbackRequest
|
|
10
|
+
|
|
11
|
+
class LivellmClient:
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
base_url: str,
|
|
16
|
+
timeout: Optional[float] = None,
|
|
17
|
+
configs: Optional[List[Settings]] = None
|
|
18
|
+
):
|
|
19
|
+
base_url = base_url.rstrip("/")
|
|
20
|
+
self.base_url = f"{base_url}/livellm"
|
|
21
|
+
self.timeout = timeout
|
|
22
|
+
self.client = httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout) \
|
|
23
|
+
if self.timeout else httpx.AsyncClient(base_url=self.base_url)
|
|
24
|
+
self.settings = []
|
|
25
|
+
self.headers = {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
}
|
|
28
|
+
if configs:
|
|
29
|
+
self.update_configs_post_init(configs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def update_configs_post_init(self, configs: List[Settings]) -> SuccessResponse:
|
|
33
|
+
"""
|
|
34
|
+
Update the configs after the client is initialized.
|
|
35
|
+
Args:
|
|
36
|
+
configs: The configs to update.
|
|
37
|
+
"""
|
|
38
|
+
with httpx.Client(base_url=self.base_url, timeout=self.timeout) as client:
|
|
39
|
+
for config in configs:
|
|
40
|
+
response = client.post(f"{self.base_url}/providers/config", json=config.model_dump())
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
self.settings.append(config)
|
|
43
|
+
return SuccessResponse(success=True, message="Configs updated successfully")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def delete(self, endpoint: str) -> dict:
|
|
47
|
+
"""
|
|
48
|
+
Delete a resource from the given endpoint and return the response.
|
|
49
|
+
Args:
|
|
50
|
+
endpoint: The endpoint to delete from.
|
|
51
|
+
Returns:
|
|
52
|
+
The response from the endpoint.
|
|
53
|
+
"""
|
|
54
|
+
response = await self.client.delete(endpoint, headers=self.headers)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
return response.json()
|
|
57
|
+
|
|
58
|
+
async def post_multipart(
|
|
59
|
+
self,
|
|
60
|
+
files: dict,
|
|
61
|
+
data: dict,
|
|
62
|
+
endpoint: str
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""
|
|
65
|
+
Post a multipart request to the given endpoint and return the response.
|
|
66
|
+
Args:
|
|
67
|
+
files: The files to send in the request.
|
|
68
|
+
data: The data to send in the request.
|
|
69
|
+
endpoint: The endpoint to post to.
|
|
70
|
+
Returns:
|
|
71
|
+
The response from the endpoint.
|
|
72
|
+
"""
|
|
73
|
+
# Don't pass Content-Type header for multipart - httpx will set it automatically
|
|
74
|
+
response = await self.client.post(endpoint, files=files, data=data)
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
return response.json()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def get(
|
|
80
|
+
self,
|
|
81
|
+
endpoint: str
|
|
82
|
+
) -> dict:
|
|
83
|
+
"""
|
|
84
|
+
Get a request from the given endpoint and return the response.
|
|
85
|
+
Args:
|
|
86
|
+
endpoint: The endpoint to get from.
|
|
87
|
+
Returns:
|
|
88
|
+
The response from the endpoint.
|
|
89
|
+
"""
|
|
90
|
+
response = await self.client.get(endpoint, headers=self.headers)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
return response.json()
|
|
93
|
+
|
|
94
|
+
async def post(
|
|
95
|
+
self,
|
|
96
|
+
json_data: dict,
|
|
97
|
+
endpoint: str,
|
|
98
|
+
expect_stream: bool = False,
|
|
99
|
+
expect_json: bool = True
|
|
100
|
+
) -> Union[dict, bytes, AsyncIterator[Union[dict, bytes]]]:
|
|
101
|
+
"""
|
|
102
|
+
Post a request to the given endpoint and return the response.
|
|
103
|
+
If expect_stream is True, return an AsyncIterator of the response.
|
|
104
|
+
If expect_json is True, return the response as a JSON object.
|
|
105
|
+
Otherwise, return the response as bytes.
|
|
106
|
+
Args:
|
|
107
|
+
json_data: The JSON data to send in the request.
|
|
108
|
+
endpoint: The endpoint to post to.
|
|
109
|
+
expect_stream: Whether to expect a stream response.
|
|
110
|
+
expect_json: Whether to expect a JSON response.
|
|
111
|
+
Returns:
|
|
112
|
+
The response from the endpoint.
|
|
113
|
+
Raises:
|
|
114
|
+
Exception: If the response is not 200 or 201.
|
|
115
|
+
"""
|
|
116
|
+
response = await self.client.post(endpoint, json=json_data, headers=self.headers)
|
|
117
|
+
if response.status_code not in [200, 201]:
|
|
118
|
+
error_response = await response.aread()
|
|
119
|
+
error_response = error_response.decode("utf-8")
|
|
120
|
+
raise Exception(f"Failed to post to {endpoint}: {error_response}")
|
|
121
|
+
if expect_stream:
|
|
122
|
+
async def stream_response() -> AsyncIterator[Union[dict, bytes]]:
|
|
123
|
+
async for chunk in response.aiter_lines():
|
|
124
|
+
if expect_json:
|
|
125
|
+
chunk = chunk.strip()
|
|
126
|
+
if not chunk:
|
|
127
|
+
continue
|
|
128
|
+
yield json.loads(chunk)
|
|
129
|
+
else:
|
|
130
|
+
yield chunk
|
|
131
|
+
return stream_response()
|
|
132
|
+
else:
|
|
133
|
+
if expect_json:
|
|
134
|
+
return response.json()
|
|
135
|
+
else:
|
|
136
|
+
return response.content
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def ping(self) -> SuccessResponse:
|
|
140
|
+
result = await self.get("ping")
|
|
141
|
+
return SuccessResponse(**result)
|
|
142
|
+
|
|
143
|
+
async def update_config(self, config: Settings) -> SuccessResponse:
|
|
144
|
+
result = await self.post(config.model_dump(), "providers/config", expect_json=True)
|
|
145
|
+
self.settings.append(config)
|
|
146
|
+
return SuccessResponse(**result)
|
|
147
|
+
|
|
148
|
+
async def update_configs(self, configs: List[Settings]) -> SuccessResponse:
|
|
149
|
+
for config in configs:
|
|
150
|
+
await self.update_config(config)
|
|
151
|
+
return SuccessResponse(success=True, message="Configs updated successfully")
|
|
152
|
+
|
|
153
|
+
async def get_configs(self) -> List[Settings]:
|
|
154
|
+
result = await self.get("providers/configs")
|
|
155
|
+
return [Settings(**config) for config in result]
|
|
156
|
+
|
|
157
|
+
async def delete_config(self, config_uid: str) -> SuccessResponse:
|
|
158
|
+
result = await self.delete(f"providers/config/{config_uid}")
|
|
159
|
+
return SuccessResponse(**result)
|
|
160
|
+
|
|
161
|
+
async def cleanup(self):
|
|
162
|
+
"""
|
|
163
|
+
Delete all the created settings resources and close the client.
|
|
164
|
+
Should be called when you're done using the client.
|
|
165
|
+
"""
|
|
166
|
+
for config in self.settings:
|
|
167
|
+
await self.delete_config(config.uid)
|
|
168
|
+
await self.client.aclose()
|
|
169
|
+
|
|
170
|
+
async def __aenter__(self):
|
|
171
|
+
"""Async context manager entry."""
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
175
|
+
"""Async context manager exit."""
|
|
176
|
+
await self.cleanup()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def agent_run(
|
|
180
|
+
self,
|
|
181
|
+
request: Union[AgentRequest, AgentFallbackRequest]
|
|
182
|
+
) -> AgentResponse:
|
|
183
|
+
result = await self.post(request.model_dump(), "agent/run", expect_json=True)
|
|
184
|
+
return AgentResponse(**result)
|
|
185
|
+
|
|
186
|
+
async def agent_run_stream(
|
|
187
|
+
self,
|
|
188
|
+
request: Union[AgentRequest, AgentFallbackRequest]
|
|
189
|
+
) -> AsyncIterator[AgentResponse]:
|
|
190
|
+
stream = await self.post(request.model_dump(), "agent/run_stream", expect_stream=True, expect_json=True)
|
|
191
|
+
async for chunk in stream:
|
|
192
|
+
yield AgentResponse(**chunk)
|
|
193
|
+
|
|
194
|
+
async def speak(
|
|
195
|
+
self,
|
|
196
|
+
request: Union[SpeakRequest, AudioFallbackRequest]
|
|
197
|
+
) -> bytes:
|
|
198
|
+
return await self.post(request.model_dump(), "audio/speak", expect_json=False)
|
|
199
|
+
|
|
200
|
+
async def speak_stream(
|
|
201
|
+
self,
|
|
202
|
+
request: Union[SpeakRequest, AudioFallbackRequest]
|
|
203
|
+
) -> AsyncIterator[bytes]:
|
|
204
|
+
return await self.post(request.model_dump(), "audio/speak_stream", expect_stream=True, expect_json=False)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def transcribe(
|
|
208
|
+
self,
|
|
209
|
+
provider_uid: str,
|
|
210
|
+
file: File,
|
|
211
|
+
model: str,
|
|
212
|
+
language: Optional[str] = None,
|
|
213
|
+
gen_config: Optional[dict] = None
|
|
214
|
+
) -> TranscribeResponse:
|
|
215
|
+
files = {
|
|
216
|
+
"file": file
|
|
217
|
+
}
|
|
218
|
+
data = {
|
|
219
|
+
"provider_uid": provider_uid,
|
|
220
|
+
"model": model,
|
|
221
|
+
"language": language,
|
|
222
|
+
"gen_config": json.dumps(gen_config) if gen_config else None
|
|
223
|
+
}
|
|
224
|
+
result = await self.post_multipart(files, data, "audio/transcribe")
|
|
225
|
+
return TranscribeResponse(**result)
|
|
226
|
+
|
|
227
|
+
async def transcribe_json(
|
|
228
|
+
self,
|
|
229
|
+
request: Union[TranscribeRequest, TranscribeFallbackRequest]
|
|
230
|
+
) -> TranscribeResponse:
|
|
231
|
+
result = await self.post(request.model_dump(), "audio/transcribe_json", expect_json=True)
|
|
232
|
+
return TranscribeResponse(**result)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from .common import BaseRequest, ProviderKind, Settings, SuccessResponse
|
|
2
|
+
from .fallback import AgentFallbackRequest, AudioFallbackRequest, TranscribeFallbackRequest, FallbackStrategy
|
|
3
|
+
from .agent.agent import AgentRequest, AgentResponse, AgentResponseUsage
|
|
4
|
+
from .agent.chat import Message, MessageRole, TextMessage, BinaryMessage
|
|
5
|
+
from .agent.tools import Tool, ToolInput, ToolKind, WebSearchInput, MCPStreamableServerInput
|
|
6
|
+
from .audio.speak import SpeakMimeType, SpeakRequest, SpeakStreamResponse
|
|
7
|
+
from .audio.transcribe import TranscribeRequest, TranscribeResponse, File
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
# Common
|
|
12
|
+
"BaseRequest",
|
|
13
|
+
"ProviderKind",
|
|
14
|
+
"Settings",
|
|
15
|
+
"SuccessResponse",
|
|
16
|
+
# Fallback
|
|
17
|
+
"AgentFallbackRequest",
|
|
18
|
+
"AudioFallbackRequest",
|
|
19
|
+
"TranscribeFallbackRequest",
|
|
20
|
+
"FallbackStrategy",
|
|
21
|
+
# Agent
|
|
22
|
+
"AgentRequest",
|
|
23
|
+
"AgentResponse",
|
|
24
|
+
"AgentResponseUsage",
|
|
25
|
+
"Message",
|
|
26
|
+
"MessageRole",
|
|
27
|
+
"TextMessage",
|
|
28
|
+
"BinaryMessage",
|
|
29
|
+
"Tool",
|
|
30
|
+
"ToolInput",
|
|
31
|
+
"ToolKind",
|
|
32
|
+
"WebSearchInput",
|
|
33
|
+
"MCPStreamableServerInput",
|
|
34
|
+
# Audio
|
|
35
|
+
"SpeakMimeType",
|
|
36
|
+
"SpeakRequest",
|
|
37
|
+
"SpeakStreamResponse",
|
|
38
|
+
"TranscribeRequest",
|
|
39
|
+
"TranscribeResponse",
|
|
40
|
+
"File",
|
|
41
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .agent import AgentRequest, AgentResponse, AgentResponseUsage
|
|
2
|
+
from .chat import Message, MessageRole, TextMessage, BinaryMessage
|
|
3
|
+
from .tools import Tool, ToolInput, ToolKind, WebSearchInput, MCPStreamableServerInput
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AgentRequest",
|
|
8
|
+
"AgentResponse",
|
|
9
|
+
"AgentResponseUsage",
|
|
10
|
+
"Message",
|
|
11
|
+
"MessageRole",
|
|
12
|
+
"TextMessage",
|
|
13
|
+
"BinaryMessage",
|
|
14
|
+
"Tool",
|
|
15
|
+
"ToolInput",
|
|
16
|
+
"ToolKind",
|
|
17
|
+
"WebSearchInput",
|
|
18
|
+
"MCPStreamableServerInput",
|
|
19
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# models for full run: AgentRequest, AgentResponse
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import Optional, List, Union
|
|
5
|
+
from .chat import TextMessage, BinaryMessage
|
|
6
|
+
from .tools import WebSearchInput, MCPStreamableServerInput
|
|
7
|
+
from ..common import BaseRequest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentRequest(BaseRequest):
|
|
11
|
+
model: str = Field(..., description="The model to use")
|
|
12
|
+
messages: List[Union[TextMessage, BinaryMessage]]
|
|
13
|
+
tools: List[Union[WebSearchInput, MCPStreamableServerInput]]
|
|
14
|
+
gen_config: Optional[dict] = Field(default=None, description="The configuration for the generation")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AgentResponseUsage(BaseModel):
|
|
18
|
+
input_tokens: int = Field(..., description="The number of input tokens used")
|
|
19
|
+
output_tokens: int = Field(..., description="The number of output tokens used")
|
|
20
|
+
|
|
21
|
+
class AgentResponse(BaseModel):
|
|
22
|
+
output: str = Field(..., description="The output of the response")
|
|
23
|
+
usage: AgentResponseUsage = Field(..., description="The usage of the response")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# models for chat messages
|
|
2
|
+
from pydantic import BaseModel, Field, model_validator
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
class MessageRole(Enum):
|
|
7
|
+
USER = "user"
|
|
8
|
+
MODEL = "model"
|
|
9
|
+
SYSTEM = "system"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Message(BaseModel):
|
|
13
|
+
role: MessageRole = Field(..., description="The role of the message")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextMessage(Message):
|
|
17
|
+
content: str = Field(..., description="The content of the message")
|
|
18
|
+
|
|
19
|
+
class BinaryMessage(Message):
|
|
20
|
+
"""always from user"""
|
|
21
|
+
content: str = Field(..., description="The base64 encoded content of the message")
|
|
22
|
+
mime_type: str = Field(..., description="The MIME type of the content, only user can supply such")
|
|
23
|
+
caption: Optional[str] = Field(None, description="Caption for the binary message")
|
|
24
|
+
|
|
25
|
+
@model_validator(mode="after")
|
|
26
|
+
def validate_content(self) -> "BinaryMessage":
|
|
27
|
+
if self.role == MessageRole.MODEL:
|
|
28
|
+
raise ValueError("MIME type are meant for user messages only")
|
|
29
|
+
return self
|
|
30
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# models for tools
|
|
2
|
+
from pydantic import BaseModel, Field, field_validator
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
class ToolKind(Enum):
|
|
7
|
+
WEB_SEARCH = "web_search"
|
|
8
|
+
MCP_STREAMABLE_SERVER = "mcp_streamable_server"
|
|
9
|
+
|
|
10
|
+
class Tool(BaseModel):
|
|
11
|
+
kind: ToolKind
|
|
12
|
+
input: BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ToolInput(BaseModel):
|
|
16
|
+
kind: ToolKind
|
|
17
|
+
kwargs: dict = Field(default_factory=dict, description="Additional keyword arguments for the MCP server")
|
|
18
|
+
|
|
19
|
+
class WebSearchInput(ToolInput):
|
|
20
|
+
kind: ToolKind = Field(ToolKind.WEB_SEARCH, description="Web search kind of tool")
|
|
21
|
+
search_context_size: Literal['low', 'medium', 'high'] = Field('medium', description="The context size for the search")
|
|
22
|
+
|
|
23
|
+
@field_validator('kind')
|
|
24
|
+
def validate_kind(cls, v):
|
|
25
|
+
if v != ToolKind.WEB_SEARCH:
|
|
26
|
+
raise ValueError(f"Invalid kind: {v}")
|
|
27
|
+
return v
|
|
28
|
+
|
|
29
|
+
class MCPStreamableServerInput(ToolInput):
|
|
30
|
+
kind: ToolKind = Field(ToolKind.MCP_STREAMABLE_SERVER, description="Mcp kind of tool")
|
|
31
|
+
url: str = Field(..., description="The URL of the MCP server")
|
|
32
|
+
prefix: str = Field(..., description="The prefix of the MCP server")
|
|
33
|
+
timeout: int = Field(15, description="The timeout in seconds for the MCP server")
|
|
34
|
+
|
|
35
|
+
@field_validator('kind')
|
|
36
|
+
def validate_kind(cls, v):
|
|
37
|
+
if v != ToolKind.MCP_STREAMABLE_SERVER:
|
|
38
|
+
raise ValueError(f"Invalid kind: {v}")
|
|
39
|
+
return v
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .speak import SpeakMimeType, SpeakRequest, SpeakStreamResponse
|
|
2
|
+
from .transcribe import TranscribeRequest, TranscribeResponse, File
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"SpeakMimeType",
|
|
7
|
+
"SpeakRequest",
|
|
8
|
+
"SpeakStreamResponse",
|
|
9
|
+
"TranscribeRequest",
|
|
10
|
+
"TranscribeResponse",
|
|
11
|
+
"File",
|
|
12
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, field_validator
|
|
2
|
+
from typing import Optional, TypeAlias, Tuple, AsyncIterator
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from ..common import BaseRequest
|
|
5
|
+
|
|
6
|
+
SpeakStreamResponse: TypeAlias = Tuple[AsyncIterator[bytes], str, int]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SpeakMimeType(Enum):
|
|
10
|
+
PCM = "audio/pcm"
|
|
11
|
+
WAV = "audio/wav"
|
|
12
|
+
MP3 = "audio/mpeg"
|
|
13
|
+
ULAW = "audio/ulaw"
|
|
14
|
+
ALAW = "audio/alaw"
|
|
15
|
+
|
|
16
|
+
class SpeakRequest(BaseRequest):
|
|
17
|
+
model: str = Field(..., description="The model to use")
|
|
18
|
+
text: str = Field(..., description="The text to speak")
|
|
19
|
+
voice: str = Field(..., description="The voice to use")
|
|
20
|
+
mime_type: SpeakMimeType = Field(..., description="The MIME type of the output audio")
|
|
21
|
+
sample_rate: int = Field(..., description="The target sample rate of the output audio")
|
|
22
|
+
chunk_size: int = Field(default=20, description="Chunk size in milliseconds for streaming (default: 20ms)")
|
|
23
|
+
gen_config: Optional[dict] = Field(default=None, description="The configuration for the generation")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, field_validator
|
|
2
|
+
from typing import Tuple, TypeAlias, Optional, Union
|
|
3
|
+
from ..common import BaseRequest
|
|
4
|
+
import base64
|
|
5
|
+
|
|
6
|
+
File: TypeAlias = Tuple[str, Union[bytes, str], str] # (filename, file_content, content_type)
|
|
7
|
+
|
|
8
|
+
class TranscribeRequest(BaseRequest):
|
|
9
|
+
model: str = Field(..., description="The model to use")
|
|
10
|
+
file: File = Field(..., description="The file to transcribe")
|
|
11
|
+
language: Optional[str] = Field(default=None, description="The language to transcribe")
|
|
12
|
+
gen_config: Optional[dict] = Field(default=None, description="The configuration for the generation")
|
|
13
|
+
|
|
14
|
+
@field_validator('file', mode='before')
|
|
15
|
+
@classmethod
|
|
16
|
+
def decode_base64_file(cls, v) -> File:
|
|
17
|
+
"""
|
|
18
|
+
Validates and processes the file field.
|
|
19
|
+
|
|
20
|
+
Accepts two formats:
|
|
21
|
+
1. Tuple[str, bytes, str] - Direct bytes (from multipart/form-data)
|
|
22
|
+
2. Tuple[str, str, str] - Base64 encoded string (from JSON)
|
|
23
|
+
|
|
24
|
+
Returns: Tuple[str, bytes, str] with decoded bytes
|
|
25
|
+
"""
|
|
26
|
+
if not isinstance(v, (tuple, list)) or len(v) != 3:
|
|
27
|
+
raise ValueError("file must be a tuple/list of 3 elements: (filename, content, content_type)")
|
|
28
|
+
|
|
29
|
+
filename, content, content_type = v
|
|
30
|
+
|
|
31
|
+
# If content is already bytes, return as-is
|
|
32
|
+
if isinstance(content, bytes):
|
|
33
|
+
return (filename, content, content_type)
|
|
34
|
+
|
|
35
|
+
# If content is a string, assume it's base64 encoded
|
|
36
|
+
elif isinstance(content, str):
|
|
37
|
+
try:
|
|
38
|
+
decoded_content = base64.b64decode(content)
|
|
39
|
+
return (filename, decoded_content, content_type)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise ValueError(f"Failed to decode base64 content: {str(e)}")
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError(f"file content must be either bytes or base64 string, got {type(content)}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TranscribeResponse(BaseModel):
|
|
47
|
+
text: str = Field(..., description="The text of the transcription")
|
|
48
|
+
language: Optional[str] = Field(default=None, description="The language of the transcription")
|
livellm/models/common.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Common models shared across all services"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, SecretStr, model_serializer
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional, Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseRequest(BaseModel):
|
|
9
|
+
"""Base request model that all service requests inherit from"""
|
|
10
|
+
provider_uid: str = Field(..., description="The unique identifier of the provider configuration to use")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProviderKind(Enum):
|
|
14
|
+
"""Unified provider types for both agent and audio services"""
|
|
15
|
+
# Agent providers
|
|
16
|
+
OPENAI = "openai"
|
|
17
|
+
GOOGLE = "google"
|
|
18
|
+
ANTHROPIC = "anthropic"
|
|
19
|
+
GROQ = "groq"
|
|
20
|
+
# Audio providers
|
|
21
|
+
ELEVENLABS = "elevenlabs"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Settings(BaseModel):
|
|
25
|
+
"""Base settings for all service providers"""
|
|
26
|
+
uid: str = Field(..., description="The unique identifier of the provider configuration")
|
|
27
|
+
provider: ProviderKind = Field(..., description="The provider to use")
|
|
28
|
+
api_key: SecretStr = Field(..., description="API key for the provider")
|
|
29
|
+
base_url: Optional[str] = Field(None, description="Optional custom base URL for the provider")
|
|
30
|
+
blacklist_models: Optional[list[str]] = Field(None, description="models selection for blacklist")
|
|
31
|
+
|
|
32
|
+
@model_serializer
|
|
33
|
+
def serialize_model(self) -> dict[str, Any]:
|
|
34
|
+
"""Custom serializer to handle SecretStr properly"""
|
|
35
|
+
return {
|
|
36
|
+
"uid": self.uid,
|
|
37
|
+
"provider": self.provider.value,
|
|
38
|
+
"api_key": self.api_key.get_secret_value(),
|
|
39
|
+
"base_url": self.base_url,
|
|
40
|
+
"blacklist_models": self.blacklist_models,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SuccessResponse(BaseModel):
|
|
45
|
+
success: bool = Field(True, description="Whether the operation was successful")
|
|
46
|
+
message: str = Field("ok", description="The message of the operation")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import List
|
|
3
|
+
from .common import BaseRequest
|
|
4
|
+
from .audio.speak import SpeakRequest
|
|
5
|
+
from .audio.transcribe import TranscribeRequest
|
|
6
|
+
from .agent.agent import AgentRequest
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
class FallbackStrategy(Enum):
|
|
10
|
+
SEQUENTIAL = "sequential"
|
|
11
|
+
PARALLEL = "parallel"
|
|
12
|
+
|
|
13
|
+
class FallbackRequest(BaseModel):
|
|
14
|
+
requests: List[BaseRequest] = Field(..., description="List of requests to try as fallbacks")
|
|
15
|
+
strategy: FallbackStrategy = Field(FallbackStrategy.SEQUENTIAL, description="The strategy to use for fallback")
|
|
16
|
+
timeout_per_request: int = Field(default=360, description="The timeout to use for each request")
|
|
17
|
+
|
|
18
|
+
class AgentFallbackRequest(FallbackRequest):
|
|
19
|
+
requests: List[AgentRequest] = Field(..., description="List of agent requests to try as fallbacks")
|
|
20
|
+
|
|
21
|
+
class AudioFallbackRequest(FallbackRequest):
|
|
22
|
+
requests: List[SpeakRequest] = Field(..., description="List of audio requests to try as fallbacks")
|
|
23
|
+
|
|
24
|
+
class TranscribeFallbackRequest(FallbackRequest):
|
|
25
|
+
requests: List[TranscribeRequest] = Field(..., description="List of transcribe requests to try as fallbacks")
|
livellm/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: livellm
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Python client for the LiveLLM Server
|
|
5
|
+
Project-URL: Homepage, https://github.com/qalby-tech/livellm-client-py
|
|
6
|
+
Project-URL: Repository, https://github.com/qalby-tech/livellm-client-py
|
|
7
|
+
Author: Kamil Saliamov
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0.0
|
|
20
|
+
Provides-Extra: testing
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'testing'
|
|
22
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'testing'
|
|
23
|
+
Requires-Dist: pytest>=8.4.2; extra == 'testing'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# LiveLLM Python Client
|
|
27
|
+
|
|
28
|
+
[](https://www.python.org/downloads/)
|
|
29
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
|
+
|
|
31
|
+
Python client library for the LiveLLM Server - a unified proxy for AI agent, audio, and transcription services.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- 🚀 **Async-first design** - Built on httpx for high-performance async operations
|
|
36
|
+
- 🔒 **Type-safe** - Full type hints and Pydantic validation
|
|
37
|
+
- 🎯 **Multi-provider support** - OpenAI, Google, Anthropic, Groq, ElevenLabs
|
|
38
|
+
- 🔄 **Streaming support** - Real-time streaming for agent and audio responses
|
|
39
|
+
- 🛠️ **Agent tools** - Web search and MCP server integration
|
|
40
|
+
- 🎙️ **Audio services** - Text-to-speech and transcription
|
|
41
|
+
- ⚡ **Fallback strategies** - Sequential and parallel fallback handling
|
|
42
|
+
- 📦 **Context manager support** - Automatic cleanup with async context managers
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install livellm-client
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or with development dependencies:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install livellm-client[testing]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
from livellm import LivellmClient
|
|
61
|
+
from livellm.models import Settings, ProviderKind, AgentRequest, TextMessage, MessageRole
|
|
62
|
+
from pydantic import SecretStr
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
# Initialize the client with context manager for automatic cleanup
|
|
66
|
+
async with LivellmClient(base_url="http://localhost:8000") as client:
|
|
67
|
+
# Configure a provider
|
|
68
|
+
config = Settings(
|
|
69
|
+
uid="my-openai-config",
|
|
70
|
+
provider=ProviderKind.OPENAI,
|
|
71
|
+
api_key=SecretStr("your-api-key")
|
|
72
|
+
)
|
|
73
|
+
await client.update_config(config)
|
|
74
|
+
|
|
75
|
+
# Run an agent query
|
|
76
|
+
request = AgentRequest(
|
|
77
|
+
provider_uid="my-openai-config",
|
|
78
|
+
model="gpt-4",
|
|
79
|
+
messages=[
|
|
80
|
+
TextMessage(role=MessageRole.USER, content="Hello, how are you?")
|
|
81
|
+
],
|
|
82
|
+
tools=[]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
response = await client.agent_run(request)
|
|
86
|
+
print(response.output)
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
### Client Initialization
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from livellm import LivellmClient
|
|
97
|
+
|
|
98
|
+
# Basic initialization
|
|
99
|
+
client = LivellmClient(base_url="http://localhost:8000")
|
|
100
|
+
|
|
101
|
+
# With timeout
|
|
102
|
+
client = LivellmClient(
|
|
103
|
+
base_url="http://localhost:8000",
|
|
104
|
+
timeout=30.0
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# With pre-configured providers (sync operation)
|
|
108
|
+
from livellm.models import Settings, ProviderKind
|
|
109
|
+
from pydantic import SecretStr
|
|
110
|
+
|
|
111
|
+
configs = [
|
|
112
|
+
Settings(
|
|
113
|
+
uid="openai-config",
|
|
114
|
+
provider=ProviderKind.OPENAI,
|
|
115
|
+
api_key=SecretStr("sk-..."),
|
|
116
|
+
base_url="https://api.openai.com/v1" # Optional custom base URL
|
|
117
|
+
),
|
|
118
|
+
Settings(
|
|
119
|
+
uid="anthropic-config",
|
|
120
|
+
provider=ProviderKind.ANTHROPIC,
|
|
121
|
+
api_key=SecretStr("sk-ant-..."),
|
|
122
|
+
blacklist_models=["claude-instant-1"] # Optional model blacklist
|
|
123
|
+
)
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
client = LivellmClient(
|
|
127
|
+
base_url="http://localhost:8000",
|
|
128
|
+
configs=configs
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Provider Configuration
|
|
133
|
+
|
|
134
|
+
Supported providers:
|
|
135
|
+
- `OPENAI` - OpenAI GPT models
|
|
136
|
+
- `GOOGLE` - Google Gemini models
|
|
137
|
+
- `ANTHROPIC` - Anthropic Claude models
|
|
138
|
+
- `GROQ` - Groq models
|
|
139
|
+
- `ELEVENLABS` - ElevenLabs text-to-speech
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# Add a provider configuration
|
|
143
|
+
config = Settings(
|
|
144
|
+
uid="unique-provider-id",
|
|
145
|
+
provider=ProviderKind.OPENAI,
|
|
146
|
+
api_key=SecretStr("your-api-key"),
|
|
147
|
+
base_url="https://custom-endpoint.com", # Optional
|
|
148
|
+
blacklist_models=["deprecated-model"] # Optional
|
|
149
|
+
)
|
|
150
|
+
await client.update_config(config)
|
|
151
|
+
|
|
152
|
+
# Get all configurations
|
|
153
|
+
configs = await client.get_configs()
|
|
154
|
+
|
|
155
|
+
# Delete a configuration
|
|
156
|
+
await client.delete_config("unique-provider-id")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Usage Examples
|
|
160
|
+
|
|
161
|
+
### Agent Services
|
|
162
|
+
|
|
163
|
+
#### Basic Agent Run
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from livellm.models import AgentRequest, TextMessage, MessageRole
|
|
167
|
+
|
|
168
|
+
request = AgentRequest(
|
|
169
|
+
provider_uid="my-openai-config",
|
|
170
|
+
model="gpt-4",
|
|
171
|
+
messages=[
|
|
172
|
+
TextMessage(role=MessageRole.SYSTEM, content="You are a helpful assistant."),
|
|
173
|
+
TextMessage(role=MessageRole.USER, content="Explain quantum computing")
|
|
174
|
+
],
|
|
175
|
+
tools=[],
|
|
176
|
+
gen_config={"temperature": 0.7, "max_tokens": 500}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
response = await client.agent_run(request)
|
|
180
|
+
print(f"Output: {response.output}")
|
|
181
|
+
print(f"Tokens used - Input: {response.usage.input_tokens}, Output: {response.usage.output_tokens}")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Streaming Agent Response
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
request = AgentRequest(
|
|
188
|
+
provider_uid="my-openai-config",
|
|
189
|
+
model="gpt-4",
|
|
190
|
+
messages=[
|
|
191
|
+
TextMessage(role=MessageRole.USER, content="Tell me a story")
|
|
192
|
+
],
|
|
193
|
+
tools=[]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
stream = await client.agent_run_stream(request)
|
|
197
|
+
async for chunk in stream:
|
|
198
|
+
print(chunk.output, end="", flush=True)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Agent with Binary Messages
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
import base64
|
|
205
|
+
|
|
206
|
+
# Read and encode image
|
|
207
|
+
with open("image.jpg", "rb") as f:
|
|
208
|
+
image_data = base64.b64encode(f.read()).decode("utf-8")
|
|
209
|
+
|
|
210
|
+
from livellm.models import BinaryMessage
|
|
211
|
+
|
|
212
|
+
request = AgentRequest(
|
|
213
|
+
provider_uid="my-openai-config",
|
|
214
|
+
model="gpt-4-vision",
|
|
215
|
+
messages=[
|
|
216
|
+
BinaryMessage(
|
|
217
|
+
role=MessageRole.USER,
|
|
218
|
+
content=image_data,
|
|
219
|
+
mime_type="image/jpeg",
|
|
220
|
+
caption="What's in this image?"
|
|
221
|
+
)
|
|
222
|
+
],
|
|
223
|
+
tools=[]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
response = await client.agent_run(request)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### Agent with Web Search Tool
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from livellm.models import WebSearchInput, ToolKind
|
|
233
|
+
|
|
234
|
+
request = AgentRequest(
|
|
235
|
+
provider_uid="my-openai-config",
|
|
236
|
+
model="gpt-4",
|
|
237
|
+
messages=[
|
|
238
|
+
TextMessage(role=MessageRole.USER, content="What's the latest news about AI?")
|
|
239
|
+
],
|
|
240
|
+
tools=[
|
|
241
|
+
WebSearchInput(
|
|
242
|
+
kind=ToolKind.WEB_SEARCH,
|
|
243
|
+
search_context_size="high" # Options: "low", "medium", "high"
|
|
244
|
+
)
|
|
245
|
+
]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
response = await client.agent_run(request)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### Agent with MCP Server Tool
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from livellm.models import MCPStreamableServerInput, ToolKind
|
|
255
|
+
|
|
256
|
+
request = AgentRequest(
|
|
257
|
+
provider_uid="my-openai-config",
|
|
258
|
+
model="gpt-4",
|
|
259
|
+
messages=[
|
|
260
|
+
TextMessage(role=MessageRole.USER, content="Execute tool")
|
|
261
|
+
],
|
|
262
|
+
tools=[
|
|
263
|
+
MCPStreamableServerInput(
|
|
264
|
+
kind=ToolKind.MCP_STREAMABLE_SERVER,
|
|
265
|
+
url="http://mcp-server:8080",
|
|
266
|
+
prefix="mcp_",
|
|
267
|
+
timeout=15,
|
|
268
|
+
kwargs={"custom_param": "value"}
|
|
269
|
+
)
|
|
270
|
+
]
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
response = await client.agent_run(request)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Audio Services
|
|
277
|
+
|
|
278
|
+
#### Text-to-Speech
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from livellm.models import SpeakRequest, SpeakMimeType
|
|
282
|
+
|
|
283
|
+
request = SpeakRequest(
|
|
284
|
+
provider_uid="elevenlabs-config",
|
|
285
|
+
model="eleven_turbo_v2",
|
|
286
|
+
text="Hello, this is a test of text to speech.",
|
|
287
|
+
voice="rachel",
|
|
288
|
+
mime_type=SpeakMimeType.MP3,
|
|
289
|
+
sample_rate=44100,
|
|
290
|
+
gen_config={"stability": 0.5, "similarity_boost": 0.75}
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Get audio as bytes
|
|
294
|
+
audio_bytes = await client.speak(request)
|
|
295
|
+
with open("output.mp3", "wb") as f:
|
|
296
|
+
f.write(audio_bytes)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### Streaming Text-to-Speech
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
request = SpeakRequest(
|
|
303
|
+
provider_uid="elevenlabs-config",
|
|
304
|
+
model="eleven_turbo_v2",
|
|
305
|
+
text="This is a longer text that will be streamed.",
|
|
306
|
+
voice="rachel",
|
|
307
|
+
mime_type=SpeakMimeType.MP3,
|
|
308
|
+
sample_rate=44100,
|
|
309
|
+
chunk_size=20 # Chunk size in milliseconds
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Stream audio chunks
|
|
313
|
+
stream = await client.speak_stream(request)
|
|
314
|
+
with open("output.mp3", "wb") as f:
|
|
315
|
+
async for chunk in stream:
|
|
316
|
+
f.write(chunk)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Audio Transcription (Multipart)
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
# Using multipart upload
|
|
323
|
+
with open("audio.mp3", "rb") as f:
|
|
324
|
+
file_tuple = ("audio.mp3", f.read(), "audio/mpeg")
|
|
325
|
+
|
|
326
|
+
response = await client.transcribe(
|
|
327
|
+
provider_uid="openai-config",
|
|
328
|
+
file=file_tuple,
|
|
329
|
+
model="whisper-1",
|
|
330
|
+
language="en",
|
|
331
|
+
gen_config={"temperature": 0.2}
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
print(f"Transcription: {response.text}")
|
|
335
|
+
print(f"Detected language: {response.language}")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### Audio Transcription (JSON)
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
import base64
|
|
342
|
+
from livellm.models import TranscribeRequest
|
|
343
|
+
|
|
344
|
+
with open("audio.mp3", "rb") as f:
|
|
345
|
+
audio_data = base64.b64encode(f.read()).decode("utf-8")
|
|
346
|
+
|
|
347
|
+
request = TranscribeRequest(
|
|
348
|
+
provider_uid="openai-config",
|
|
349
|
+
model="whisper-1",
|
|
350
|
+
file=("audio.mp3", audio_data, "audio/mpeg"),
|
|
351
|
+
language="en"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
response = await client.transcribe_json(request)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Fallback Strategies
|
|
358
|
+
|
|
359
|
+
#### Sequential Fallback (Try each provider in order)
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from livellm.models import AgentFallbackRequest, FallbackStrategy
|
|
363
|
+
|
|
364
|
+
fallback_request = AgentFallbackRequest(
|
|
365
|
+
requests=[
|
|
366
|
+
AgentRequest(
|
|
367
|
+
provider_uid="primary-provider",
|
|
368
|
+
model="gpt-4",
|
|
369
|
+
messages=[TextMessage(role=MessageRole.USER, content="Hello")],
|
|
370
|
+
tools=[]
|
|
371
|
+
),
|
|
372
|
+
AgentRequest(
|
|
373
|
+
provider_uid="backup-provider",
|
|
374
|
+
model="claude-3",
|
|
375
|
+
messages=[TextMessage(role=MessageRole.USER, content="Hello")],
|
|
376
|
+
tools=[]
|
|
377
|
+
)
|
|
378
|
+
],
|
|
379
|
+
strategy=FallbackStrategy.SEQUENTIAL,
|
|
380
|
+
timeout_per_request=30
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
response = await client.agent_run(fallback_request)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### Parallel Fallback (Try all providers simultaneously)
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
fallback_request = AgentFallbackRequest(
|
|
390
|
+
requests=[
|
|
391
|
+
AgentRequest(provider_uid="provider-1", model="gpt-4", messages=messages, tools=[]),
|
|
392
|
+
AgentRequest(provider_uid="provider-2", model="claude-3", messages=messages, tools=[]),
|
|
393
|
+
AgentRequest(provider_uid="provider-3", model="gemini-pro", messages=messages, tools=[])
|
|
394
|
+
],
|
|
395
|
+
strategy=FallbackStrategy.PARALLEL,
|
|
396
|
+
timeout_per_request=10
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
response = await client.agent_run(fallback_request)
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
#### Audio Fallback
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
from livellm.models import AudioFallbackRequest
|
|
406
|
+
|
|
407
|
+
fallback_request = AudioFallbackRequest(
|
|
408
|
+
requests=[
|
|
409
|
+
SpeakRequest(provider_uid="elevenlabs", model="model-1", text=text, voice="voice1",
|
|
410
|
+
mime_type=SpeakMimeType.MP3, sample_rate=44100),
|
|
411
|
+
SpeakRequest(provider_uid="openai", model="tts-1", text=text, voice="alloy",
|
|
412
|
+
mime_type=SpeakMimeType.MP3, sample_rate=44100)
|
|
413
|
+
],
|
|
414
|
+
strategy=FallbackStrategy.SEQUENTIAL
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
audio = await client.speak(fallback_request)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Context Manager Support
|
|
421
|
+
|
|
422
|
+
The client supports async context managers for automatic cleanup:
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
async with LivellmClient(base_url="http://localhost:8000") as client:
|
|
426
|
+
config = Settings(uid="temp-config", provider=ProviderKind.OPENAI,
|
|
427
|
+
api_key=SecretStr("key"))
|
|
428
|
+
await client.update_config(config)
|
|
429
|
+
|
|
430
|
+
# Use client...
|
|
431
|
+
response = await client.ping()
|
|
432
|
+
|
|
433
|
+
# Automatically cleans up configs and closes HTTP client
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Or manually:
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
client = LivellmClient(base_url="http://localhost:8000")
|
|
440
|
+
try:
|
|
441
|
+
# Use client...
|
|
442
|
+
pass
|
|
443
|
+
finally:
|
|
444
|
+
await client.cleanup()
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## API Reference
|
|
448
|
+
|
|
449
|
+
### Client Methods
|
|
450
|
+
|
|
451
|
+
#### Health Check
|
|
452
|
+
- `ping() -> SuccessResponse` - Check server health
|
|
453
|
+
|
|
454
|
+
#### Configuration Management
|
|
455
|
+
- `update_config(config: Settings) -> SuccessResponse` - Add/update a provider config
|
|
456
|
+
- `update_configs(configs: List[Settings]) -> SuccessResponse` - Add/update multiple configs
|
|
457
|
+
- `get_configs() -> List[Settings]` - Get all provider configurations
|
|
458
|
+
- `delete_config(config_uid: str) -> SuccessResponse` - Delete a provider config
|
|
459
|
+
|
|
460
|
+
#### Agent Services
|
|
461
|
+
- `agent_run(request: AgentRequest | AgentFallbackRequest) -> AgentResponse` - Run agent query
|
|
462
|
+
- `agent_run_stream(request: AgentRequest | AgentFallbackRequest) -> AsyncIterator[AgentResponse]` - Stream agent response
|
|
463
|
+
|
|
464
|
+
#### Audio Services
|
|
465
|
+
- `speak(request: SpeakRequest | AudioFallbackRequest) -> bytes` - Text-to-speech
|
|
466
|
+
- `speak_stream(request: SpeakRequest | AudioFallbackRequest) -> AsyncIterator[bytes]` - Streaming TTS
|
|
467
|
+
- `transcribe(provider_uid, file, model, language?, gen_config?) -> TranscribeResponse` - Multipart transcription
|
|
468
|
+
- `transcribe_json(request: TranscribeRequest | TranscribeFallbackRequest) -> TranscribeResponse` - JSON transcription
|
|
469
|
+
|
|
470
|
+
#### Cleanup
|
|
471
|
+
- `cleanup() -> None` - Clean up resources and close client
|
|
472
|
+
- `__aenter__() / __aexit__()` - Async context manager support
|
|
473
|
+
|
|
474
|
+
### Models
|
|
475
|
+
|
|
476
|
+
#### Common Models
|
|
477
|
+
- `Settings` - Provider configuration
|
|
478
|
+
- `ProviderKind` - Enum of supported providers
|
|
479
|
+
- `SuccessResponse` - Generic success response
|
|
480
|
+
- `BaseRequest` - Base class for all requests
|
|
481
|
+
|
|
482
|
+
#### Agent Models
|
|
483
|
+
- `AgentRequest` - Agent query request
|
|
484
|
+
- `AgentResponse` - Agent query response
|
|
485
|
+
- `AgentResponseUsage` - Token usage information
|
|
486
|
+
- `TextMessage` - Text-based message
|
|
487
|
+
- `BinaryMessage` - Binary message (images, audio, etc.)
|
|
488
|
+
- `MessageRole` - Enum: USER, MODEL, SYSTEM
|
|
489
|
+
|
|
490
|
+
#### Tool Models
|
|
491
|
+
- `ToolKind` - Enum: WEB_SEARCH, MCP_STREAMABLE_SERVER
|
|
492
|
+
- `WebSearchInput` - Web search tool configuration
|
|
493
|
+
- `MCPStreamableServerInput` - MCP server tool configuration
|
|
494
|
+
|
|
495
|
+
#### Audio Models
|
|
496
|
+
- `SpeakRequest` - Text-to-speech request
|
|
497
|
+
- `SpeakMimeType` - Enum: PCM, WAV, MP3, ULAW, ALAW
|
|
498
|
+
- `TranscribeRequest` - Transcription request
|
|
499
|
+
- `TranscribeResponse` - Transcription response
|
|
500
|
+
|
|
501
|
+
#### Fallback Models
|
|
502
|
+
- `FallbackStrategy` - Enum: SEQUENTIAL, PARALLEL
|
|
503
|
+
- `AgentFallbackRequest` - Agent fallback configuration
|
|
504
|
+
- `AudioFallbackRequest` - Audio fallback configuration
|
|
505
|
+
- `TranscribeFallbackRequest` - Transcription fallback configuration
|
|
506
|
+
|
|
507
|
+
## Error Handling
|
|
508
|
+
|
|
509
|
+
The client raises exceptions for HTTP errors:
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
try:
|
|
513
|
+
response = await client.agent_run(request)
|
|
514
|
+
except Exception as e:
|
|
515
|
+
print(f"Error: {e}")
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
For more granular error handling:
|
|
519
|
+
|
|
520
|
+
```python
|
|
521
|
+
import httpx
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
response = await client.ping()
|
|
525
|
+
except httpx.HTTPStatusError as e:
|
|
526
|
+
print(f"HTTP error: {e.response.status_code}")
|
|
527
|
+
except httpx.RequestError as e:
|
|
528
|
+
print(f"Request error: {e}")
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
## Development
|
|
532
|
+
|
|
533
|
+
### Running Tests
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
# Install development dependencies
|
|
537
|
+
pip install -e ".[testing]"
|
|
538
|
+
|
|
539
|
+
# Run tests
|
|
540
|
+
pytest tests/
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Type Checking
|
|
544
|
+
|
|
545
|
+
The library is fully typed. Run type checking with:
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
pip install mypy
|
|
549
|
+
mypy livellm
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Requirements
|
|
553
|
+
|
|
554
|
+
- Python 3.10+
|
|
555
|
+
- httpx >= 0.27.0
|
|
556
|
+
- pydantic >= 2.0.0
|
|
557
|
+
|
|
558
|
+
## License
|
|
559
|
+
|
|
560
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
561
|
+
|
|
562
|
+
## Contributing
|
|
563
|
+
|
|
564
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
565
|
+
|
|
566
|
+
## Links
|
|
567
|
+
|
|
568
|
+
- [GitHub Repository](https://github.com/qalby-tech/livellm-client-py)
|
|
569
|
+
- [Issue Tracker](https://github.com/qalby-tech/livellm-client-py/issues)
|
|
570
|
+
|
|
571
|
+
## Changelog
|
|
572
|
+
|
|
573
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
livellm/__init__.py,sha256=JG_0-UCfQI_3D0Y2PzobZLS5OhJwK76i8t81ye0KpfY,279
|
|
2
|
+
livellm/livellm.py,sha256=0C4LpQy3EOzxNQ6ltIZqStquYuV1WoKcJSFMVtkI4Sk,8635
|
|
3
|
+
livellm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
livellm/models/__init__.py,sha256=JBUd1GkeDexLSdjSOcUet78snu0NNxnhU7mBN3BhqIA,1199
|
|
5
|
+
livellm/models/common.py,sha256=YqRwP6ChWbRdoen4MU6RO4u6HeM0mQJbXiiRV4DuauM,1740
|
|
6
|
+
livellm/models/fallback.py,sha256=AybOrNEP_5JwgForVTWGlK39PWvvapjj4UP7sx3e5qU,1144
|
|
7
|
+
livellm/models/agent/__init__.py,sha256=KVm6AgQoWEaoq47QAG4Ou4NimoXOTkjXC-0-gnMRLZ8,476
|
|
8
|
+
livellm/models/agent/agent.py,sha256=2qCh-SsHPhrL7-phv0HpojNgixXg42FhVW8tOv8K7z8,980
|
|
9
|
+
livellm/models/agent/chat.py,sha256=whDuFo8ddR6dPwKo0mMZS7LzCBkL3sVb4lr7tVXjg-M,983
|
|
10
|
+
livellm/models/agent/tools.py,sha256=gHyVUjK6HzXB6Sd64TIM6pLjmTKSGe-fOEH9ELs5dgg,1398
|
|
11
|
+
livellm/models/audio/__init__.py,sha256=sz2NxCOfFGVvp-XQUsdgOR_TYBO1Wb-8LLXaZDEiAZk,282
|
|
12
|
+
livellm/models/audio/speak.py,sha256=4cJhXonImeohL2Fltc2ub_aCGeTAoew6Hnz-myrrR8k,1001
|
|
13
|
+
livellm/models/audio/transcribe.py,sha256=0XtK_f5cYPO4VMD0lh6tYYQQPFaj4g3N2eK7nzuEjKY,2111
|
|
14
|
+
livellm-1.1.0.dist-info/METADATA,sha256=RuCnzRRlAF5VhAmpyalSqhx2aWYrJlSdPWAmt92GFIo,15207
|
|
15
|
+
livellm-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
livellm-1.1.0.dist-info/licenses/LICENSE,sha256=yapGO2C_00ymEx6TADdbU8Oyc1bWOrZY-fjP-agmFL4,1071
|
|
17
|
+
livellm-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Kamil Saliamov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|