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.
Files changed (86) hide show
  1. lionagi/config.py +123 -0
  2. lionagi/fields/file.py +1 -1
  3. lionagi/fields/reason.py +1 -1
  4. lionagi/libs/file/concat.py +1 -6
  5. lionagi/libs/file/concat_files.py +1 -5
  6. lionagi/libs/file/save.py +1 -1
  7. lionagi/libs/package/imports.py +8 -177
  8. lionagi/libs/parse.py +30 -0
  9. lionagi/libs/schema/load_pydantic_model_from_schema.py +259 -0
  10. lionagi/libs/token_transform/perplexity.py +2 -4
  11. lionagi/libs/token_transform/synthlang_/resources/frameworks/framework_options.json +46 -46
  12. lionagi/libs/token_transform/synthlang_/translate_to_synthlang.py +1 -1
  13. lionagi/operations/chat/chat.py +2 -2
  14. lionagi/operations/communicate/communicate.py +20 -5
  15. lionagi/operations/parse/parse.py +131 -43
  16. lionagi/protocols/generic/log.py +1 -2
  17. lionagi/protocols/generic/pile.py +18 -4
  18. lionagi/protocols/messages/assistant_response.py +20 -1
  19. lionagi/protocols/messages/templates/README.md +6 -10
  20. lionagi/service/connections/__init__.py +15 -0
  21. lionagi/service/connections/api_calling.py +230 -0
  22. lionagi/service/connections/endpoint.py +410 -0
  23. lionagi/service/connections/endpoint_config.py +137 -0
  24. lionagi/service/connections/header_factory.py +56 -0
  25. lionagi/service/connections/match_endpoint.py +49 -0
  26. lionagi/service/connections/providers/__init__.py +3 -0
  27. lionagi/service/connections/providers/anthropic_.py +87 -0
  28. lionagi/service/connections/providers/exa_.py +33 -0
  29. lionagi/service/connections/providers/oai_.py +166 -0
  30. lionagi/service/connections/providers/ollama_.py +122 -0
  31. lionagi/service/connections/providers/perplexity_.py +29 -0
  32. lionagi/service/imodel.py +36 -144
  33. lionagi/service/manager.py +1 -7
  34. lionagi/service/{endpoints/rate_limited_processor.py → rate_limited_processor.py} +4 -2
  35. lionagi/service/resilience.py +545 -0
  36. lionagi/service/third_party/README.md +71 -0
  37. lionagi/service/third_party/__init__.py +0 -0
  38. lionagi/service/third_party/anthropic_models.py +159 -0
  39. lionagi/service/third_party/exa_models.py +165 -0
  40. lionagi/service/third_party/openai_models.py +18241 -0
  41. lionagi/service/third_party/pplx_models.py +156 -0
  42. lionagi/service/types.py +5 -4
  43. lionagi/session/branch.py +12 -7
  44. lionagi/tools/file/reader.py +1 -1
  45. lionagi/tools/memory/tools.py +497 -0
  46. lionagi/utils.py +921 -123
  47. lionagi/version.py +1 -1
  48. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/METADATA +33 -16
  49. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/RECORD +53 -63
  50. lionagi/libs/file/create_path.py +0 -80
  51. lionagi/libs/file/file_util.py +0 -358
  52. lionagi/libs/parse/__init__.py +0 -3
  53. lionagi/libs/parse/fuzzy_parse_json.py +0 -117
  54. lionagi/libs/parse/to_dict.py +0 -336
  55. lionagi/libs/parse/to_json.py +0 -61
  56. lionagi/libs/parse/to_num.py +0 -378
  57. lionagi/libs/parse/to_xml.py +0 -57
  58. lionagi/libs/parse/xml_parser.py +0 -148
  59. lionagi/libs/schema/breakdown_pydantic_annotation.py +0 -48
  60. lionagi/service/endpoints/__init__.py +0 -3
  61. lionagi/service/endpoints/base.py +0 -706
  62. lionagi/service/endpoints/chat_completion.py +0 -116
  63. lionagi/service/endpoints/match_endpoint.py +0 -72
  64. lionagi/service/providers/__init__.py +0 -3
  65. lionagi/service/providers/anthropic_/__init__.py +0 -3
  66. lionagi/service/providers/anthropic_/messages.py +0 -99
  67. lionagi/service/providers/exa_/models.py +0 -3
  68. lionagi/service/providers/exa_/search.py +0 -80
  69. lionagi/service/providers/exa_/types.py +0 -7
  70. lionagi/service/providers/groq_/__init__.py +0 -3
  71. lionagi/service/providers/groq_/chat_completions.py +0 -56
  72. lionagi/service/providers/ollama_/__init__.py +0 -3
  73. lionagi/service/providers/ollama_/chat_completions.py +0 -134
  74. lionagi/service/providers/openai_/__init__.py +0 -3
  75. lionagi/service/providers/openai_/chat_completions.py +0 -101
  76. lionagi/service/providers/openai_/spec.py +0 -14
  77. lionagi/service/providers/openrouter_/__init__.py +0 -3
  78. lionagi/service/providers/openrouter_/chat_completions.py +0 -62
  79. lionagi/service/providers/perplexity_/__init__.py +0 -3
  80. lionagi/service/providers/perplexity_/chat_completions.py +0 -44
  81. lionagi/service/providers/perplexity_/models.py +0 -5
  82. lionagi/service/providers/types.py +0 -17
  83. /lionagi/{service/providers/exa_/__init__.py → py.typed} +0 -0
  84. /lionagi/service/{endpoints/token_calculator.py → token_calculator.py} +0 -0
  85. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/WHEEL +0 -0
  86. {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,3 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
@@ -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)