lionagi 0.12.2__py3-none-any.whl → 0.12.4__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.
- lionagi/config.py +123 -0
- lionagi/fields/file.py +1 -1
- lionagi/fields/reason.py +1 -1
- lionagi/libs/file/concat.py +1 -6
- lionagi/libs/file/concat_files.py +1 -5
- lionagi/libs/file/save.py +1 -1
- lionagi/libs/package/imports.py +8 -177
- lionagi/libs/parse.py +30 -0
- lionagi/libs/schema/load_pydantic_model_from_schema.py +259 -0
- lionagi/libs/token_transform/perplexity.py +2 -4
- lionagi/libs/token_transform/synthlang_/resources/frameworks/framework_options.json +46 -46
- lionagi/libs/token_transform/synthlang_/translate_to_synthlang.py +1 -1
- lionagi/operations/chat/chat.py +2 -2
- lionagi/operations/communicate/communicate.py +20 -5
- lionagi/operations/parse/parse.py +131 -43
- lionagi/protocols/generic/log.py +1 -2
- lionagi/protocols/generic/pile.py +18 -4
- lionagi/protocols/messages/assistant_response.py +20 -1
- lionagi/protocols/messages/templates/README.md +6 -10
- lionagi/service/connections/__init__.py +15 -0
- lionagi/service/connections/api_calling.py +230 -0
- lionagi/service/connections/endpoint.py +410 -0
- lionagi/service/connections/endpoint_config.py +137 -0
- lionagi/service/connections/header_factory.py +56 -0
- lionagi/service/connections/match_endpoint.py +49 -0
- lionagi/service/connections/providers/__init__.py +3 -0
- lionagi/service/connections/providers/anthropic_.py +87 -0
- lionagi/service/connections/providers/exa_.py +33 -0
- lionagi/service/connections/providers/oai_.py +166 -0
- lionagi/service/connections/providers/ollama_.py +122 -0
- lionagi/service/connections/providers/perplexity_.py +29 -0
- lionagi/service/imodel.py +36 -144
- lionagi/service/manager.py +1 -7
- lionagi/service/{endpoints/rate_limited_processor.py → rate_limited_processor.py} +4 -2
- lionagi/service/resilience.py +545 -0
- lionagi/service/third_party/README.md +71 -0
- lionagi/service/third_party/__init__.py +0 -0
- lionagi/service/third_party/anthropic_models.py +159 -0
- lionagi/service/third_party/exa_models.py +165 -0
- lionagi/service/third_party/openai_models.py +18241 -0
- lionagi/service/third_party/pplx_models.py +156 -0
- lionagi/service/types.py +5 -4
- lionagi/session/branch.py +12 -7
- lionagi/tools/file/reader.py +1 -1
- lionagi/tools/memory/tools.py +497 -0
- lionagi/utils.py +921 -123
- lionagi/version.py +1 -1
- {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/METADATA +33 -16
- {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/RECORD +53 -63
- lionagi/libs/file/create_path.py +0 -80
- lionagi/libs/file/file_util.py +0 -358
- lionagi/libs/parse/__init__.py +0 -3
- lionagi/libs/parse/fuzzy_parse_json.py +0 -117
- lionagi/libs/parse/to_dict.py +0 -336
- lionagi/libs/parse/to_json.py +0 -61
- lionagi/libs/parse/to_num.py +0 -378
- lionagi/libs/parse/to_xml.py +0 -57
- lionagi/libs/parse/xml_parser.py +0 -148
- lionagi/libs/schema/breakdown_pydantic_annotation.py +0 -48
- lionagi/service/endpoints/__init__.py +0 -3
- lionagi/service/endpoints/base.py +0 -706
- lionagi/service/endpoints/chat_completion.py +0 -116
- lionagi/service/endpoints/match_endpoint.py +0 -72
- lionagi/service/providers/__init__.py +0 -3
- lionagi/service/providers/anthropic_/__init__.py +0 -3
- lionagi/service/providers/anthropic_/messages.py +0 -99
- lionagi/service/providers/exa_/models.py +0 -3
- lionagi/service/providers/exa_/search.py +0 -80
- lionagi/service/providers/exa_/types.py +0 -7
- lionagi/service/providers/groq_/__init__.py +0 -3
- lionagi/service/providers/groq_/chat_completions.py +0 -56
- lionagi/service/providers/ollama_/__init__.py +0 -3
- lionagi/service/providers/ollama_/chat_completions.py +0 -134
- lionagi/service/providers/openai_/__init__.py +0 -3
- lionagi/service/providers/openai_/chat_completions.py +0 -101
- lionagi/service/providers/openai_/spec.py +0 -14
- lionagi/service/providers/openrouter_/__init__.py +0 -3
- lionagi/service/providers/openrouter_/chat_completions.py +0 -62
- lionagi/service/providers/perplexity_/__init__.py +0 -3
- lionagi/service/providers/perplexity_/chat_completions.py +0 -44
- lionagi/service/providers/perplexity_/models.py +0 -5
- lionagi/service/providers/types.py +0 -17
- /lionagi/{service/providers/exa_/__init__.py → py.typed} +0 -0
- /lionagi/service/{endpoints/token_calculator.py → token_calculator.py} +0 -0
- {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/WHEEL +0 -0
- {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
import os
|
6
|
+
from typing import Any, TypeVar
|
7
|
+
|
8
|
+
from pydantic import (
|
9
|
+
BaseModel,
|
10
|
+
Field,
|
11
|
+
PrivateAttr,
|
12
|
+
SecretStr,
|
13
|
+
field_serializer,
|
14
|
+
field_validator,
|
15
|
+
model_validator,
|
16
|
+
)
|
17
|
+
|
18
|
+
from .header_factory import AUTH_TYPES
|
19
|
+
|
20
|
+
B = TypeVar("B", bound=type[BaseModel])
|
21
|
+
|
22
|
+
|
23
|
+
class EndpointConfig(BaseModel):
|
24
|
+
name: str
|
25
|
+
provider: str
|
26
|
+
base_url: str | None = None
|
27
|
+
endpoint: str
|
28
|
+
endpoint_params: list[str] | None = None
|
29
|
+
method: str = "POST"
|
30
|
+
params: dict[str, str] = Field(default_factory=dict)
|
31
|
+
content_type: str = "application/json"
|
32
|
+
auth_type: AUTH_TYPES = "bearer"
|
33
|
+
default_headers: dict = {}
|
34
|
+
request_options: B | None = None
|
35
|
+
api_key: str | SecretStr | None = None
|
36
|
+
timeout: int = 300
|
37
|
+
max_retries: int = 3
|
38
|
+
openai_compatible: bool = False
|
39
|
+
requires_tokens: bool = False
|
40
|
+
kwargs: dict = Field(default_factory=dict)
|
41
|
+
client_kwargs: dict = Field(default_factory=dict)
|
42
|
+
_api_key: str | None = PrivateAttr(None)
|
43
|
+
|
44
|
+
@model_validator(mode="before")
|
45
|
+
def _validate_kwargs(cls, data: dict):
|
46
|
+
kwargs = data.pop("kwargs", {})
|
47
|
+
field_keys = list(cls.model_json_schema().get("properties", {}).keys())
|
48
|
+
for k in list(data.keys()):
|
49
|
+
if k not in field_keys:
|
50
|
+
kwargs[k] = data.pop(k)
|
51
|
+
data["kwargs"] = kwargs
|
52
|
+
return data
|
53
|
+
|
54
|
+
@model_validator(mode="after")
|
55
|
+
def _validate_api_key(self):
|
56
|
+
|
57
|
+
if self.api_key is not None:
|
58
|
+
if isinstance(self.api_key, SecretStr):
|
59
|
+
self._api_key = self.api_key.get_secret_value()
|
60
|
+
elif isinstance(self.api_key, str):
|
61
|
+
# Skip settings lookup for ollama special case
|
62
|
+
if self.provider == "ollama" and self.api_key == "ollama_key":
|
63
|
+
self._api_key = "ollama_key"
|
64
|
+
else:
|
65
|
+
from lionagi.config import settings
|
66
|
+
|
67
|
+
try:
|
68
|
+
self._api_key = settings.get_secret(self.api_key)
|
69
|
+
except (AttributeError, ValueError):
|
70
|
+
self._api_key = os.getenv(self.api_key, self.api_key)
|
71
|
+
|
72
|
+
return self
|
73
|
+
|
74
|
+
@property
|
75
|
+
def full_url(self):
|
76
|
+
if not self.endpoint_params:
|
77
|
+
return f"{self.base_url}/{self.endpoint}"
|
78
|
+
return f"{self.base_url}/{self.endpoint.format(**self.params)}"
|
79
|
+
|
80
|
+
@field_validator("request_options", mode="before")
|
81
|
+
def _validate_request_options(cls, v):
|
82
|
+
# Create a simple empty model if None is provided
|
83
|
+
if v is None:
|
84
|
+
return None
|
85
|
+
|
86
|
+
try:
|
87
|
+
if isinstance(v, type) and issubclass(v, BaseModel):
|
88
|
+
return v
|
89
|
+
if isinstance(v, BaseModel):
|
90
|
+
return v.__class__
|
91
|
+
if isinstance(v, dict | str):
|
92
|
+
from lionagi.libs.schema import SchemaUtil
|
93
|
+
|
94
|
+
return SchemaUtil.load_pydantic_model_from_schema(v)
|
95
|
+
except Exception as e:
|
96
|
+
raise ValueError("Invalid request options") from e
|
97
|
+
raise ValueError(
|
98
|
+
"Invalid request options: must be a Pydantic model or a schema dict"
|
99
|
+
)
|
100
|
+
|
101
|
+
@field_serializer("request_options")
|
102
|
+
def _serialize_request_options(self, v: B | None):
|
103
|
+
if v is None:
|
104
|
+
return None
|
105
|
+
return v.model_json_schema()
|
106
|
+
|
107
|
+
def update(self, **kwargs):
|
108
|
+
"""Update the config with new values."""
|
109
|
+
# Handle the special case of kwargs dict
|
110
|
+
if "kwargs" in kwargs:
|
111
|
+
# Merge the kwargs dicts
|
112
|
+
self.kwargs.update(kwargs.pop("kwargs"))
|
113
|
+
|
114
|
+
for key, value in kwargs.items():
|
115
|
+
if hasattr(self, key):
|
116
|
+
setattr(self, key, value)
|
117
|
+
else:
|
118
|
+
# Add to kwargs dict if not a direct attribute
|
119
|
+
self.kwargs[key] = value
|
120
|
+
|
121
|
+
def validate_payload(self, data: dict[str, Any]) -> dict[str, Any]:
|
122
|
+
"""Validate payload data against the request_options model.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
data: The payload data to validate
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
The validated data
|
129
|
+
"""
|
130
|
+
if not self.request_options:
|
131
|
+
return data
|
132
|
+
|
133
|
+
try:
|
134
|
+
self.request_options.model_validate(data)
|
135
|
+
return data
|
136
|
+
except Exception as e:
|
137
|
+
raise ValueError("Invalid payload") from e
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from typing import Literal
|
6
|
+
|
7
|
+
from pydantic import SecretStr
|
8
|
+
|
9
|
+
AUTH_TYPES = Literal["bearer", "x-api-key", "none"]
|
10
|
+
|
11
|
+
|
12
|
+
class HeaderFactory:
|
13
|
+
@staticmethod
|
14
|
+
def get_content_type_header(
|
15
|
+
content_type: str = "application/json",
|
16
|
+
) -> dict[str, str]:
|
17
|
+
return {"Content-Type": content_type}
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def get_bearer_auth_header(api_key: str) -> dict[str, str]:
|
21
|
+
return {"Authorization": f"Bearer {api_key}"}
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def get_x_api_key_header(api_key: str) -> dict[str, str]:
|
25
|
+
return {"x-api-key": api_key}
|
26
|
+
|
27
|
+
@staticmethod
|
28
|
+
def get_header(
|
29
|
+
auth_type: AUTH_TYPES,
|
30
|
+
content_type: str = "application/json",
|
31
|
+
api_key: str | SecretStr | None = None,
|
32
|
+
default_headers: dict[str, str] | None = None,
|
33
|
+
) -> dict[str, str]:
|
34
|
+
dict_ = HeaderFactory.get_content_type_header(content_type)
|
35
|
+
|
36
|
+
if auth_type == "none":
|
37
|
+
# No authentication needed
|
38
|
+
pass
|
39
|
+
elif not api_key:
|
40
|
+
raise ValueError("API key is required for authentication")
|
41
|
+
else:
|
42
|
+
api_key = (
|
43
|
+
api_key.get_secret_value()
|
44
|
+
if isinstance(api_key, SecretStr)
|
45
|
+
else api_key
|
46
|
+
)
|
47
|
+
if auth_type == "bearer":
|
48
|
+
dict_.update(HeaderFactory.get_bearer_auth_header(api_key))
|
49
|
+
elif auth_type == "x-api-key":
|
50
|
+
dict_.update(HeaderFactory.get_x_api_key_header(api_key))
|
51
|
+
else:
|
52
|
+
raise ValueError(f"Unsupported auth type: {auth_type}")
|
53
|
+
|
54
|
+
if default_headers:
|
55
|
+
dict_.update(default_headers)
|
56
|
+
return dict_
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from .endpoint import Endpoint
|
6
|
+
|
7
|
+
|
8
|
+
def match_endpoint(
|
9
|
+
provider: str,
|
10
|
+
endpoint: str,
|
11
|
+
**kwargs,
|
12
|
+
) -> Endpoint:
|
13
|
+
if provider == "openai":
|
14
|
+
if "chat" in endpoint:
|
15
|
+
from .providers.oai_ import OpenaiChatEndpoint
|
16
|
+
|
17
|
+
return OpenaiChatEndpoint(**kwargs)
|
18
|
+
if "response" in endpoint:
|
19
|
+
from .providers.oai_ import OpenaiResponseEndpoint
|
20
|
+
|
21
|
+
return OpenaiResponseEndpoint(**kwargs)
|
22
|
+
if provider == "openrouter" and "chat" in endpoint:
|
23
|
+
from .providers.oai_ import OpenrouterChatEndpoint
|
24
|
+
|
25
|
+
return OpenrouterChatEndpoint(**kwargs)
|
26
|
+
if provider == "ollama" and "chat" in endpoint:
|
27
|
+
from .providers.ollama_ import OllamaChatEndpoint
|
28
|
+
|
29
|
+
return OllamaChatEndpoint(**kwargs)
|
30
|
+
if provider == "exa" and "search" in endpoint:
|
31
|
+
from .providers.exa_ import ExaSearchEndpoint
|
32
|
+
|
33
|
+
return ExaSearchEndpoint(**kwargs)
|
34
|
+
if provider == "anthropic" and (
|
35
|
+
"messages" in endpoint or "chat" in endpoint
|
36
|
+
):
|
37
|
+
from .providers.anthropic_ import AnthropicMessagesEndpoint
|
38
|
+
|
39
|
+
return AnthropicMessagesEndpoint(**kwargs)
|
40
|
+
if provider == "groq" and "chat" in endpoint:
|
41
|
+
from .providers.oai_ import GroqChatEndpoint
|
42
|
+
|
43
|
+
return GroqChatEndpoint(**kwargs)
|
44
|
+
if provider == "perplexity" and "chat" in endpoint:
|
45
|
+
from .providers.perplexity_ import PerplexityChatEndpoint
|
46
|
+
|
47
|
+
return PerplexityChatEndpoint(**kwargs)
|
48
|
+
|
49
|
+
return None
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from lionagi.config import settings
|
8
|
+
from lionagi.service.connections.endpoint import Endpoint
|
9
|
+
from lionagi.service.connections.endpoint_config import EndpointConfig
|
10
|
+
from lionagi.service.third_party.anthropic_models import CreateMessageRequest
|
11
|
+
|
12
|
+
ANTHROPIC_MESSAGES_ENDPOINT_CONFIG = EndpointConfig(
|
13
|
+
name="anthropic_messages",
|
14
|
+
provider="anthropic",
|
15
|
+
base_url="https://api.anthropic.com/v1",
|
16
|
+
endpoint="messages",
|
17
|
+
method="POST",
|
18
|
+
openai_compatible=False,
|
19
|
+
auth_type="x-api-key",
|
20
|
+
default_headers={"anthropic-version": "2023-06-01"},
|
21
|
+
api_key=settings.ANTHROPIC_API_KEY or "dummy-key-for-testing",
|
22
|
+
request_options=CreateMessageRequest,
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
class AnthropicMessagesEndpoint(Endpoint):
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
config: EndpointConfig = ANTHROPIC_MESSAGES_ENDPOINT_CONFIG,
|
30
|
+
**kwargs,
|
31
|
+
):
|
32
|
+
super().__init__(config, **kwargs)
|
33
|
+
|
34
|
+
def create_payload(
|
35
|
+
self,
|
36
|
+
request: dict | BaseModel,
|
37
|
+
extra_headers: dict | None = None,
|
38
|
+
**kwargs,
|
39
|
+
):
|
40
|
+
# Extract system message before validation if present
|
41
|
+
request_dict = (
|
42
|
+
request if isinstance(request, dict) else request.model_dump()
|
43
|
+
)
|
44
|
+
system = None
|
45
|
+
|
46
|
+
if "messages" in request_dict and request_dict["messages"]:
|
47
|
+
first_message = request_dict["messages"][0]
|
48
|
+
if first_message.get("role") == "system":
|
49
|
+
system = first_message["content"]
|
50
|
+
# Remove system message before validation
|
51
|
+
request_dict["messages"] = request_dict["messages"][1:]
|
52
|
+
request = request_dict
|
53
|
+
|
54
|
+
payload, headers = super().create_payload(
|
55
|
+
request, extra_headers=extra_headers, **kwargs
|
56
|
+
)
|
57
|
+
|
58
|
+
# Remove api_key from payload if present
|
59
|
+
payload.pop("api_key", None)
|
60
|
+
|
61
|
+
if "cache_control" in payload:
|
62
|
+
cache_control = payload.pop("cache_control")
|
63
|
+
if cache_control:
|
64
|
+
cache_control = {"type": "ephemeral"}
|
65
|
+
last_message = payload["messages"][-1]["content"]
|
66
|
+
if isinstance(last_message, str):
|
67
|
+
last_message = {
|
68
|
+
"type": "text",
|
69
|
+
"text": last_message,
|
70
|
+
"cache_control": cache_control,
|
71
|
+
}
|
72
|
+
elif isinstance(last_message, list) and isinstance(
|
73
|
+
last_message[-1], dict
|
74
|
+
):
|
75
|
+
last_message[-1]["cache_control"] = cache_control
|
76
|
+
payload["messages"][-1]["content"] = (
|
77
|
+
[last_message]
|
78
|
+
if not isinstance(last_message, list)
|
79
|
+
else last_message
|
80
|
+
)
|
81
|
+
|
82
|
+
# If we extracted a system message earlier, add it to payload
|
83
|
+
if system:
|
84
|
+
system = [{"type": "text", "text": system}]
|
85
|
+
payload["system"] = system
|
86
|
+
|
87
|
+
return (payload, headers)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
from lionagi.config import settings
|
8
|
+
from lionagi.service.connections.endpoint import Endpoint
|
9
|
+
from lionagi.service.connections.endpoint_config import EndpointConfig
|
10
|
+
from lionagi.service.third_party.exa_models import ExaSearchRequest
|
11
|
+
|
12
|
+
__all__ = ("ExaSearchEndpoint",)
|
13
|
+
|
14
|
+
|
15
|
+
ENDPOINT_CONFIG = EndpointConfig(
|
16
|
+
name="exa_search",
|
17
|
+
provider="exa",
|
18
|
+
base_url="https://api.exa.ai",
|
19
|
+
endpoint="search",
|
20
|
+
method="POST",
|
21
|
+
request_options=ExaSearchRequest,
|
22
|
+
api_key=settings.EXA_API_KEY or "dummy-key-for-testing",
|
23
|
+
timeout=120,
|
24
|
+
max_retries=3,
|
25
|
+
auth_type="x-api-key",
|
26
|
+
transport_type="http",
|
27
|
+
content_type="application/json",
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
class ExaSearchEndpoint(Endpoint):
|
32
|
+
def __init__(self, config=ENDPOINT_CONFIG, **kwargs):
|
33
|
+
super().__init__(config=config, **kwargs)
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from lionagi.config import settings
|
8
|
+
from lionagi.service.connections.endpoint import Endpoint
|
9
|
+
from lionagi.service.connections.endpoint_config import EndpointConfig
|
10
|
+
from lionagi.service.third_party.openai_models import (
|
11
|
+
CreateChatCompletionRequest,
|
12
|
+
CreateResponse,
|
13
|
+
)
|
14
|
+
|
15
|
+
__all__ = (
|
16
|
+
"OpenaiChatEndpoint",
|
17
|
+
"OpenaiResponseEndpoint",
|
18
|
+
"OpenrouterChatEndpoint",
|
19
|
+
"OPENROUTER_GEMINI_ENDPOINT_CONFIG",
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
OPENAI_CHAT_ENDPOINT_CONFIG = EndpointConfig(
|
24
|
+
name="openai_chat",
|
25
|
+
provider="openai",
|
26
|
+
base_url="https://api.openai.com/v1",
|
27
|
+
endpoint="chat/completions",
|
28
|
+
kwargs={"model": "gpt-4o"},
|
29
|
+
api_key=settings.OPENAI_API_KEY or "dummy-key-for-testing",
|
30
|
+
auth_type="bearer",
|
31
|
+
content_type="application/json",
|
32
|
+
method="POST",
|
33
|
+
requires_tokens=True,
|
34
|
+
request_options=CreateChatCompletionRequest,
|
35
|
+
)
|
36
|
+
|
37
|
+
OPENAI_RESPONSE_ENDPOINT_CONFIG = EndpointConfig(
|
38
|
+
name="openai_response",
|
39
|
+
provider="openai",
|
40
|
+
base_url="https://api.openai.com/v1",
|
41
|
+
endpoint="chat/completions", # OpenAI responses API uses same endpoint
|
42
|
+
kwargs={"model": "gpt-4o"},
|
43
|
+
api_key=settings.OPENAI_API_KEY or "dummy-key-for-testing",
|
44
|
+
auth_type="bearer",
|
45
|
+
content_type="application/json",
|
46
|
+
method="POST",
|
47
|
+
requires_tokens=True,
|
48
|
+
request_options=CreateResponse,
|
49
|
+
)
|
50
|
+
|
51
|
+
OPENROUTER_CHAT_ENDPOINT_CONFIG = EndpointConfig(
|
52
|
+
name="openrouter_chat",
|
53
|
+
provider="openrouter",
|
54
|
+
base_url="https://openrouter.ai/api/v1",
|
55
|
+
endpoint="chat/completions",
|
56
|
+
kwargs={"model": "google/gemini-2.5-flash-preview-05-20"},
|
57
|
+
api_key=settings.OPENROUTER_API_KEY or "dummy-key-for-testing",
|
58
|
+
auth_type="bearer",
|
59
|
+
content_type="application/json",
|
60
|
+
method="POST",
|
61
|
+
request_options=CreateChatCompletionRequest,
|
62
|
+
)
|
63
|
+
|
64
|
+
OPENROUTER_GEMINI_ENDPOINT_CONFIG = EndpointConfig(
|
65
|
+
name="openrouter_gemini",
|
66
|
+
provider="openrouter",
|
67
|
+
base_url="https://openrouter.ai/api/v1",
|
68
|
+
endpoint="chat/completions",
|
69
|
+
kwargs={"model": "google/gemini-2.5-flash-preview-05-20"},
|
70
|
+
api_key=settings.OPENROUTER_API_KEY or "dummy-key-for-testing",
|
71
|
+
auth_type="bearer",
|
72
|
+
content_type="application/json",
|
73
|
+
method="POST",
|
74
|
+
)
|
75
|
+
|
76
|
+
OPENAI_EMBEDDING_ENDPOINT_CONFIG = EndpointConfig(
|
77
|
+
name="openai_embed",
|
78
|
+
provider="openai",
|
79
|
+
base_url="https://api.openai.com/v1",
|
80
|
+
endpoint="embeddings",
|
81
|
+
kwargs={"model": "text-embedding-3-small"},
|
82
|
+
api_key=settings.OPENAI_API_KEY or "dummy-key-for-testing",
|
83
|
+
auth_type="bearer",
|
84
|
+
content_type="application/json",
|
85
|
+
method="POST",
|
86
|
+
)
|
87
|
+
|
88
|
+
GROQ_CHAT_ENDPOINT_CONFIG = EndpointConfig(
|
89
|
+
name="groq_chat",
|
90
|
+
provider="groq",
|
91
|
+
base_url="https://api.groq.com/openai/v1",
|
92
|
+
endpoint="chat/completions",
|
93
|
+
api_key=settings.GROQ_API_KEY or "dummy-key-for-testing",
|
94
|
+
auth_type="bearer",
|
95
|
+
content_type="application/json",
|
96
|
+
method="POST",
|
97
|
+
)
|
98
|
+
|
99
|
+
|
100
|
+
REASONING_MODELS = (
|
101
|
+
"o3-mini-2025-01-31",
|
102
|
+
"o3-mini",
|
103
|
+
"o1",
|
104
|
+
"o1-2024-12-17",
|
105
|
+
)
|
106
|
+
|
107
|
+
REASONING_NOT_SUPPORT_PARAMS = (
|
108
|
+
"temperature",
|
109
|
+
"top_p",
|
110
|
+
"logit_bias",
|
111
|
+
"logprobs",
|
112
|
+
"top_logprobs",
|
113
|
+
)
|
114
|
+
|
115
|
+
|
116
|
+
class OpenaiChatEndpoint(Endpoint):
|
117
|
+
def __init__(self, config=OPENAI_CHAT_ENDPOINT_CONFIG, **kwargs):
|
118
|
+
super().__init__(config, **kwargs)
|
119
|
+
|
120
|
+
def create_payload(
|
121
|
+
self,
|
122
|
+
request: dict | BaseModel,
|
123
|
+
extra_headers: dict | None = None,
|
124
|
+
**kwargs,
|
125
|
+
):
|
126
|
+
"""Override to handle model-specific parameter filtering."""
|
127
|
+
payload, headers = super().create_payload(
|
128
|
+
request, extra_headers, **kwargs
|
129
|
+
)
|
130
|
+
|
131
|
+
# Handle reasoning models
|
132
|
+
model = payload.get("model")
|
133
|
+
if model in REASONING_MODELS:
|
134
|
+
# Remove unsupported parameters for reasoning models
|
135
|
+
for param in REASONING_NOT_SUPPORT_PARAMS:
|
136
|
+
payload.pop(param, None)
|
137
|
+
|
138
|
+
# Convert system role to developer role for reasoning models
|
139
|
+
if "messages" in payload and payload["messages"]:
|
140
|
+
if payload["messages"][0].get("role") == "system":
|
141
|
+
payload["messages"][0]["role"] = "developer"
|
142
|
+
else:
|
143
|
+
# Remove reasoning_effort for non-reasoning models
|
144
|
+
payload.pop("reasoning_effort", None)
|
145
|
+
|
146
|
+
return (payload, headers)
|
147
|
+
|
148
|
+
|
149
|
+
class OpenaiResponseEndpoint(Endpoint):
|
150
|
+
def __init__(self, config=OPENAI_RESPONSE_ENDPOINT_CONFIG, **kwargs):
|
151
|
+
super().__init__(config, **kwargs)
|
152
|
+
|
153
|
+
|
154
|
+
class OpenrouterChatEndpoint(Endpoint):
|
155
|
+
def __init__(self, config=OPENROUTER_CHAT_ENDPOINT_CONFIG, **kwargs):
|
156
|
+
super().__init__(config, **kwargs)
|
157
|
+
|
158
|
+
|
159
|
+
class GroqChatEndpoint(Endpoint):
|
160
|
+
def __init__(self, config=GROQ_CHAT_ENDPOINT_CONFIG, **kwargs):
|
161
|
+
super().__init__(config, **kwargs)
|
162
|
+
|
163
|
+
|
164
|
+
class OpenaiEmbedEndpoint(Endpoint):
|
165
|
+
def __init__(self, config=OPENAI_EMBEDDING_ENDPOINT_CONFIG, **kwargs):
|
166
|
+
super().__init__(config, **kwargs)
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from lionagi.service.connections.endpoint import Endpoint
|
8
|
+
from lionagi.service.connections.endpoint_config import EndpointConfig
|
9
|
+
from lionagi.service.third_party.openai_models import (
|
10
|
+
CreateChatCompletionRequest,
|
11
|
+
)
|
12
|
+
from lionagi.utils import is_import_installed
|
13
|
+
|
14
|
+
_HAS_OLLAMA = is_import_installed("ollama")
|
15
|
+
|
16
|
+
OLLAMA_CHAT_ENDPOINT_CONFIG = EndpointConfig(
|
17
|
+
name="ollama_chat",
|
18
|
+
provider="ollama",
|
19
|
+
base_url="http://localhost:11434/v1", # Ollama desktop client default
|
20
|
+
endpoint="chat/completions", # Use full OpenAI-compatible endpoint
|
21
|
+
kwargs={}, # Empty kwargs, model will be provided at runtime
|
22
|
+
openai_compatible=False, # Use HTTP transport
|
23
|
+
api_key=None, # No API key needed
|
24
|
+
method="POST",
|
25
|
+
content_type="application/json",
|
26
|
+
auth_type="none", # No authentication
|
27
|
+
default_headers={}, # No auth headers needed
|
28
|
+
request_options=CreateChatCompletionRequest, # Use Pydantic model for validation
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
class OllamaChatEndpoint(Endpoint):
|
33
|
+
"""
|
34
|
+
Documentation: https://platform.openai.com/docs/api-reference/chat/create
|
35
|
+
"""
|
36
|
+
|
37
|
+
def __init__(self, config=OLLAMA_CHAT_ENDPOINT_CONFIG, **kwargs):
|
38
|
+
if not _HAS_OLLAMA:
|
39
|
+
raise ModuleNotFoundError(
|
40
|
+
"ollama is not installed, please install it with `pip install lionagi[ollama]`"
|
41
|
+
)
|
42
|
+
|
43
|
+
# Override api_key for Ollama (not needed)
|
44
|
+
if "api_key" in kwargs:
|
45
|
+
kwargs.pop("api_key")
|
46
|
+
|
47
|
+
super().__init__(config, **kwargs)
|
48
|
+
|
49
|
+
from ollama import list as ollama_list # type: ignore[import]
|
50
|
+
from ollama import pull as ollama_pull # type: ignore[import]
|
51
|
+
|
52
|
+
self._pull = ollama_pull
|
53
|
+
self._list = ollama_list
|
54
|
+
|
55
|
+
def create_payload(
|
56
|
+
self,
|
57
|
+
request: dict | BaseModel,
|
58
|
+
extra_headers: dict | None = None,
|
59
|
+
**kwargs,
|
60
|
+
):
|
61
|
+
"""Override to handle Ollama-specific needs."""
|
62
|
+
payload, headers = super().create_payload(
|
63
|
+
request, extra_headers, **kwargs
|
64
|
+
)
|
65
|
+
|
66
|
+
# Ollama doesn't support reasoning_effort
|
67
|
+
payload.pop("reasoning_effort", None)
|
68
|
+
|
69
|
+
return (payload, headers)
|
70
|
+
|
71
|
+
async def call(
|
72
|
+
self, request: dict | BaseModel, cache_control: bool = False, **kwargs
|
73
|
+
):
|
74
|
+
payload, _ = self.create_payload(request, **kwargs)
|
75
|
+
|
76
|
+
# Check if model exists and pull if needed
|
77
|
+
model = payload["model"]
|
78
|
+
self._check_model(model)
|
79
|
+
|
80
|
+
# The parent call method will handle headers internally
|
81
|
+
return await super().call(
|
82
|
+
payload, cache_control=cache_control, **kwargs
|
83
|
+
)
|
84
|
+
|
85
|
+
def _pull_model(self, model: str):
|
86
|
+
from tqdm import tqdm
|
87
|
+
|
88
|
+
current_digest, bars = "", {}
|
89
|
+
for progress in self._pull(model, stream=True):
|
90
|
+
digest = progress.get("digest", "")
|
91
|
+
if digest != current_digest and current_digest in bars:
|
92
|
+
bars[current_digest].close()
|
93
|
+
|
94
|
+
if not digest:
|
95
|
+
print(progress.get("status"))
|
96
|
+
continue
|
97
|
+
|
98
|
+
if digest not in bars and (total := progress.get("total")):
|
99
|
+
bars[digest] = tqdm(
|
100
|
+
total=total,
|
101
|
+
desc=f"pulling {digest[7:19]}",
|
102
|
+
unit="B",
|
103
|
+
unit_scale=True,
|
104
|
+
)
|
105
|
+
|
106
|
+
if completed := progress.get("completed"):
|
107
|
+
bars[digest].update(completed - bars[digest].n)
|
108
|
+
|
109
|
+
current_digest = digest
|
110
|
+
|
111
|
+
def _check_model(self, model: str):
|
112
|
+
try:
|
113
|
+
available_models = [i.model for i in self._list().models]
|
114
|
+
|
115
|
+
if model not in available_models:
|
116
|
+
print(
|
117
|
+
f"Model '{model}' not found locally. Pulling from Ollama registry..."
|
118
|
+
)
|
119
|
+
self._pull_model(model)
|
120
|
+
print(f"Model '{model}' successfully pulled.")
|
121
|
+
except Exception as e:
|
122
|
+
print(f"Warning: Could not check/pull model '{model}': {e}")
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from lionagi.config import settings
|
6
|
+
from lionagi.service.connections.endpoint import Endpoint
|
7
|
+
from lionagi.service.connections.endpoint_config import EndpointConfig
|
8
|
+
from lionagi.service.third_party.pplx_models import PerplexityChatRequest
|
9
|
+
|
10
|
+
__all__ = ("PerplexityChatEndpoint",)
|
11
|
+
|
12
|
+
|
13
|
+
ENDPOINT_CONFIG = EndpointConfig(
|
14
|
+
name="perplexity_chat",
|
15
|
+
provider="perplexity",
|
16
|
+
base_url="https://api.perplexity.ai",
|
17
|
+
endpoint="chat/completions",
|
18
|
+
method="POST",
|
19
|
+
kwargs={"model": "sonar"},
|
20
|
+
api_key=settings.PERPLEXITY_API_KEY or "dummy-key-for-testing",
|
21
|
+
auth_type="bearer",
|
22
|
+
content_type="application/json",
|
23
|
+
request_options=PerplexityChatRequest,
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
class PerplexityChatEndpoint(Endpoint):
|
28
|
+
def __init__(self, config=ENDPOINT_CONFIG, **kwargs):
|
29
|
+
super().__init__(config, **kwargs)
|