railtracks.llm

 1from . import retries
 2from .content import ToolCall, ToolResponse
 3from .history import MessageHistory
 4from .message import AssistantMessage, Message, SystemMessage, ToolMessage, UserMessage
 5from .model import ModelBase
 6from .models import (
 7    AnthropicLLM,
 8    AzureAILLM,
 9    CohereLLM,
10    GeminiLLM,
11    HuggingFaceLLM,
12    OllamaLLM,
13    OpenAICompatibleProvider,
14    OpenAILLM,
15    PortKeyLLM,
16    # TelusLLM,
17)
18from .models._model_exception_base import (
19    FunctionCallingNotSupportedError,
20    ModelError,
21    ModelNotFoundError,
22    MutuallyExclusiveHyperparametersError,
23    UnsupportedHyperparameterError,
24)
25from .providers import ModelProvider
26from .tools import (
27    ArrayParameter,
28    ObjectParameter,
29    Parameter,
30    RefParameter,
31    Tool,
32    UnionParameter,
33)
34
35__all__ = [
36    "ModelBase",
37    "ModelError",
38    "ModelNotFoundError",
39    "FunctionCallingNotSupportedError",
40    "UnsupportedHyperparameterError",
41    "MutuallyExclusiveHyperparametersError",
42    "ToolCall",
43    "ToolResponse",
44    "UserMessage",
45    "SystemMessage",
46    "AssistantMessage",
47    "Message",
48    "ToolMessage",
49    "MessageHistory",
50    "ModelProvider",
51    "Tool",
52    "AnthropicLLM",
53    "AzureAILLM",
54    "CohereLLM",
55    "HuggingFaceLLM",
56    "OpenAILLM",
57    "GeminiLLM",
58    "OllamaLLM",
59    "AzureAILLM",
60    "GeminiLLM",
61    # "TelusLLM",
62    "PortKeyLLM",
63    "OpenAICompatibleProvider",
64    "CohereLLM",
65    # Parameter types
66    "Parameter",
67    "UnionParameter",
68    "ArrayParameter",
69    "ObjectParameter",
70    "RefParameter",
71    "retries",
72]
class ModelBase(abc.ABC, typing.Generic[~_TStream]):
 32class ModelBase(ABC, Generic[_TStream]):
 33    """
 34    A simple base that represents the behavior of a model that can be used for chat, structured interactions, and streaming.
 35
 36    The base class allows for the insertion of hooks that can modify the messages before they are sent to the model,
 37    response after they are received, and map exceptions that may occur during the interaction.
 38
 39    All the hooks are optional and can be added or removed as needed.
 40    """
 41
 42    def __init__(
 43        self,
 44        __pre_hooks: List[Callable[[MessageHistory], MessageHistory]] | None = None,
 45        __post_hooks: List[Callable[[MessageHistory, Response], Response]]
 46        | None = None,
 47        __exception_hooks: List[Callable[[MessageHistory, Exception], None]]
 48        | None = None,
 49        stream: _TStream = False,
 50        retry_approach: RetryApproach | None = None,
 51    ):
 52        if __pre_hooks is None:
 53            pre_hooks: List[Callable[[MessageHistory], MessageHistory]] = []
 54        else:
 55            pre_hooks = __pre_hooks
 56
 57        if __post_hooks is None:
 58            post_hooks: List[Callable[[MessageHistory, Response], Response]] = []
 59        else:
 60            post_hooks = __post_hooks
 61
 62        if __exception_hooks is None:
 63            exception_hooks: List[Callable[[MessageHistory, Exception], None]] = []
 64        else:
 65            exception_hooks = __exception_hooks
 66
 67        self._pre_hooks = pre_hooks
 68        self._post_hooks = post_hooks
 69        self._exception_hooks = exception_hooks
 70        self.stream = stream
 71        self.retry_approach = retry_approach
 72
 73    def add_pre_hook(self, hook: Callable[[MessageHistory], MessageHistory]) -> None:
 74        """Adds a pre-hook to modify messages before sending them to the model."""
 75        self._pre_hooks.append(hook)
 76
 77    def add_post_hook(
 78        self, hook: Callable[[MessageHistory, Response], Response]
 79    ) -> None:
 80        """Adds a post-hook to modify the response after receiving it from the model."""
 81        self._post_hooks.append(hook)
 82
 83    def add_exception_hook(
 84        self, hook: Callable[[MessageHistory, Exception], None]
 85    ) -> None:
 86        """Adds an exception hook to handle exceptions during model interactions."""
 87        self._exception_hooks.append(hook)
 88
 89    def remove_pre_hooks(self) -> None:
 90        """Removes all of the hooks that modify messages before sending them to the model."""
 91        self._pre_hooks = []
 92
 93    def remove_post_hooks(self) -> None:
 94        """Removes all of the hooks that modify the response after receiving it from the model."""
 95        self._post_hooks = []
 96
 97    def remove_exception_hooks(self) -> None:
 98        """Removes all of the hooks that handle exceptions during model interactions."""
 99        self._exception_hooks = []
100
101    @abstractmethod
102    def model_name(self) -> str:
103        """
104        Returns the name of the model being used.
105
106        It can be treated as unique identifier for the model when paired with the `model_type`.
107        """
108        pass
109
110    @abstractmethod
111    def model_provider(self) -> ModelProvider:
112        """The name of the provider of this model (The Company that owns the model)"""
113        pass
114
115    @classmethod
116    @abstractmethod
117    def model_gateway(cls) -> ModelProvider:
118        """
119        Gets the API distrubutor of the model. Note nessecarily the same as the model itself.
120
121        E.g. if you are calling openai LLM through Azure AI foundry
122        """
123        pass
124
125    def _run_pre_hooks(self, message_history: MessageHistory) -> MessageHistory:
126        """Runs all pre-hooks on the provided message history."""
127        for hook in self._pre_hooks:
128            message_history = hook(message_history)
129        return message_history
130
131    def _run_post_hooks(
132        self, message_history: MessageHistory, result: Response
133    ) -> Response:
134        """Runs all post-hooks on the provided message history and result."""
135        for hook in self._post_hooks:
136            result = hook(message_history, result)
137        return result
138
139    def _run_exception_hooks(
140        self, message_history: MessageHistory, exception: Exception
141    ) -> None:
142        """Runs all exception hooks on the provided message history and exception."""
143        for hook in self._exception_hooks:
144            hook(message_history, exception)
145
146    def generator_wrapper(
147        self,
148        generator: Generator[str | Response, None, Response],
149        message_history: MessageHistory,
150    ) -> Generator[str | Response, None, Response]:
151        new_response: Response | None = None
152        for g in generator:
153            if isinstance(g, Response):
154                g.message_info
155                new_response = self._run_post_hooks(message_history, g)
156                yield new_response
157
158            yield g
159
160        assert new_response is not None, (
161            "The generator did not yield a final Response object so nothing could be done."
162        )
163
164        return new_response
165
166    @overload
167    def chat(self: ModelBase[Literal[False]], messages: MessageHistory) -> Response:
168        pass
169
170    @overload
171    def chat(
172        self: ModelBase[Literal[True]], messages: MessageHistory
173    ) -> Generator[str | Response, None, Response]:
174        pass
175
176    def chat(
177        self, messages: MessageHistory
178    ) -> Response | Generator[str | Response, None, Response]:
179        """Chat with the model using the provided messages."""
180
181        messages = self._run_pre_hooks(messages)
182
183        try:
184            response = self._chat(messages)
185        except Exception as e:
186            self._run_exception_hooks(messages, e)
187            raise e
188
189        if isinstance(response, Generator):
190            return self.generator_wrapper(response, messages)
191
192        response = self._run_post_hooks(messages, response)
193        return response
194
195    @overload
196    async def achat(
197        self: ModelBase[Literal[False]], messages: MessageHistory
198    ) -> Response:
199        pass
200
201    @overload
202    async def achat(
203        self: ModelBase[Literal[True]], messages: MessageHistory
204    ) -> Generator[str | Response, None, Response]:
205        pass
206
207    async def achat(self, messages: MessageHistory):
208        """Asynchronous chat with the model using the provided messages."""
209        messages = self._run_pre_hooks(messages)
210
211        try:
212            response = await self._achat(messages)
213        except Exception as e:
214            self._run_exception_hooks(messages, e)
215            raise e
216
217        if isinstance(response, Generator):
218            return self.generator_wrapper(response, messages)
219
220        response = self._run_post_hooks(messages, response)
221
222        return response
223
224    @overload
225    def structured(
226        self: ModelBase[Literal[False]],
227        messages: MessageHistory,
228        schema: Type[BaseModel],
229    ) -> Response:
230        pass
231
232    @overload
233    def structured(
234        self: ModelBase[Literal[True]],
235        messages: MessageHistory,
236        schema: Type[BaseModel],
237    ) -> Generator[str | Response, None, Response]:
238        pass
239
240    def structured(self, messages: MessageHistory, schema: Type[BaseModel]):
241        """Structured interaction with the model using the provided messages and output_schema."""
242        messages = self._run_pre_hooks(messages)
243
244        try:
245            response = self._structured(messages, schema)
246        except Exception as e:
247            self._run_exception_hooks(messages, e)
248            raise e
249
250        if isinstance(response, Generator):
251            return self.generator_wrapper(response, messages)
252
253        response = self._run_post_hooks(messages, response)
254
255        return response
256
257    @overload
258    async def astructured(
259        self: ModelBase[Literal[False]],
260        messages: MessageHistory,
261        schema: Type[BaseModel],
262    ) -> Response:
263        pass
264
265    @overload
266    async def astructured(
267        self: ModelBase[Literal[True]],
268        messages: MessageHistory,
269        schema: Type[BaseModel],
270    ) -> Generator[str | Response, None, Response]:
271        pass
272
273    async def astructured(self, messages: MessageHistory, schema: Type[BaseModel]):
274        """Asynchronous structured interaction with the model using the provided messages and output_schema."""
275        messages = self._run_pre_hooks(messages)
276
277        try:
278            response = await self._astructured(messages, schema)
279        except Exception as e:
280            self._run_exception_hooks(messages, e)
281            raise e
282
283        if isinstance(response, Generator):
284            return self.generator_wrapper(response, messages)
285
286        response = self._run_post_hooks(messages, response)
287
288        return response
289
290    @overload
291    def chat_with_tools(
292        self: ModelBase[Literal[False]], messages: MessageHistory, tools: List[Tool]
293    ) -> Response:
294        pass
295
296    @overload
297    def chat_with_tools(
298        self: ModelBase[Literal[True]], messages: MessageHistory, tools: List[Tool]
299    ) -> Generator[str | Response, None, Response]:
300        pass
301
302    def chat_with_tools(self, messages: MessageHistory, tools: List[Tool]):
303        """Chat with the model using the provided messages and tools."""
304        messages = self._run_pre_hooks(messages)
305
306        try:
307            response = self._chat_with_tools(messages, tools)
308        except Exception as e:
309            self._run_exception_hooks(messages, e)
310            raise e
311
312        if isinstance(response, Generator):
313            return self.generator_wrapper(response, messages)
314
315        response = self._run_post_hooks(messages, response)
316        return response
317
318    @overload
319    async def achat_with_tools(
320        self: ModelBase[Literal[False]], messages: MessageHistory, tools: List[Tool]
321    ) -> Response:
322        pass
323
324    @overload
325    async def achat_with_tools(
326        self: ModelBase[Literal[True]], messages: MessageHistory, tools: List[Tool]
327    ) -> Generator[str | Response, None, Response]:
328        pass
329
330    async def achat_with_tools(self, messages: MessageHistory, tools: List[Tool]):
331        """Asynchronous chat with the model using the provided messages and tools."""
332        messages = self._run_pre_hooks(messages)
333
334        try:
335            response = await self._achat_with_tools(messages, tools)
336        except Exception as e:
337            self._run_exception_hooks(messages, e)
338            raise e
339
340        if isinstance(response, Generator):
341            return self.generator_wrapper(response, messages)
342
343        response = self._run_post_hooks(messages, response)
344
345        return response
346
347    @abstractmethod
348    def _chat(
349        self, messages: MessageHistory
350    ) -> Response | Generator[str | Response, None, Response]:
351        pass
352
353    @abstractmethod
354    def _structured(
355        self, messages: MessageHistory, schema: Type[BaseModel]
356    ) -> Response | Generator[str | Response, None, Response]:
357        pass
358
359    @abstractmethod
360    def _chat_with_tools(
361        self, messages: MessageHistory, tools: List[Tool]
362    ) -> Response | Generator[str | Response, None, Response]:
363        pass
364
365    @abstractmethod
366    async def _achat(
367        self, messages: MessageHistory
368    ) -> Response | AsyncGenerator[str | Response, None]:
369        pass
370
371    @abstractmethod
372    async def _astructured(
373        self,
374        messages: MessageHistory,
375        schema: Type[BaseModel],
376    ) -> Response | AsyncGenerator[str | Response, None]:
377        pass
378
379    @abstractmethod
380    async def _achat_with_tools(
381        self, messages: MessageHistory, tools: List[Tool]
382    ) -> Response | AsyncGenerator[str | Response, None]:
383        pass

A simple base that represents the behavior of a model that can be used for chat, structured interactions, and streaming.

The base class allows for the insertion of hooks that can modify the messages before they are sent to the model, response after they are received, and map exceptions that may occur during the interaction.

All the hooks are optional and can be added or removed as needed.

stream
retry_approach
def add_pre_hook( self, hook: Callable[[MessageHistory], MessageHistory]) -> None:
73    def add_pre_hook(self, hook: Callable[[MessageHistory], MessageHistory]) -> None:
74        """Adds a pre-hook to modify messages before sending them to the model."""
75        self._pre_hooks.append(hook)

Adds a pre-hook to modify messages before sending them to the model.

def add_post_hook( self, hook: Callable[[MessageHistory, railtracks.llm.response.Response], railtracks.llm.response.Response]) -> None:
77    def add_post_hook(
78        self, hook: Callable[[MessageHistory, Response], Response]
79    ) -> None:
80        """Adds a post-hook to modify the response after receiving it from the model."""
81        self._post_hooks.append(hook)

Adds a post-hook to modify the response after receiving it from the model.

def add_exception_hook( self, hook: Callable[[MessageHistory, Exception], NoneType]) -> None:
83    def add_exception_hook(
84        self, hook: Callable[[MessageHistory, Exception], None]
85    ) -> None:
86        """Adds an exception hook to handle exceptions during model interactions."""
87        self._exception_hooks.append(hook)

Adds an exception hook to handle exceptions during model interactions.

def remove_pre_hooks(self) -> None:
89    def remove_pre_hooks(self) -> None:
90        """Removes all of the hooks that modify messages before sending them to the model."""
91        self._pre_hooks = []

Removes all of the hooks that modify messages before sending them to the model.

def remove_post_hooks(self) -> None:
93    def remove_post_hooks(self) -> None:
94        """Removes all of the hooks that modify the response after receiving it from the model."""
95        self._post_hooks = []

Removes all of the hooks that modify the response after receiving it from the model.

def remove_exception_hooks(self) -> None:
97    def remove_exception_hooks(self) -> None:
98        """Removes all of the hooks that handle exceptions during model interactions."""
99        self._exception_hooks = []

Removes all of the hooks that handle exceptions during model interactions.

@abstractmethod
def model_name(self) -> str:
101    @abstractmethod
102    def model_name(self) -> str:
103        """
104        Returns the name of the model being used.
105
106        It can be treated as unique identifier for the model when paired with the `model_type`.
107        """
108        pass

Returns the name of the model being used.

It can be treated as unique identifier for the model when paired with the model_type.

@abstractmethod
def model_provider(self) -> ModelProvider:
110    @abstractmethod
111    def model_provider(self) -> ModelProvider:
112        """The name of the provider of this model (The Company that owns the model)"""
113        pass

The name of the provider of this model (The Company that owns the model)

@classmethod
@abstractmethod
def model_gateway(cls) -> ModelProvider:
115    @classmethod
116    @abstractmethod
117    def model_gateway(cls) -> ModelProvider:
118        """
119        Gets the API distrubutor of the model. Note nessecarily the same as the model itself.
120
121        E.g. if you are calling openai LLM through Azure AI foundry
122        """
123        pass

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

def generator_wrapper( self, generator: Generator[str | railtracks.llm.response.Response, NoneType, railtracks.llm.response.Response], message_history: MessageHistory) -> Generator[str | railtracks.llm.response.Response, NoneType, railtracks.llm.response.Response]:
146    def generator_wrapper(
147        self,
148        generator: Generator[str | Response, None, Response],
149        message_history: MessageHistory,
150    ) -> Generator[str | Response, None, Response]:
151        new_response: Response | None = None
152        for g in generator:
153            if isinstance(g, Response):
154                g.message_info
155                new_response = self._run_post_hooks(message_history, g)
156                yield new_response
157
158            yield g
159
160        assert new_response is not None, (
161            "The generator did not yield a final Response object so nothing could be done."
162        )
163
164        return new_response
def chat( self, messages: MessageHistory) -> Union[railtracks.llm.response.Response, Generator[str | railtracks.llm.response.Response, NoneType, railtracks.llm.response.Response]]:
176    def chat(
177        self, messages: MessageHistory
178    ) -> Response | Generator[str | Response, None, Response]:
179        """Chat with the model using the provided messages."""
180
181        messages = self._run_pre_hooks(messages)
182
183        try:
184            response = self._chat(messages)
185        except Exception as e:
186            self._run_exception_hooks(messages, e)
187            raise e
188
189        if isinstance(response, Generator):
190            return self.generator_wrapper(response, messages)
191
192        response = self._run_post_hooks(messages, response)
193        return response

Chat with the model using the provided messages.

async def achat(self, messages: MessageHistory):
207    async def achat(self, messages: MessageHistory):
208        """Asynchronous chat with the model using the provided messages."""
209        messages = self._run_pre_hooks(messages)
210
211        try:
212            response = await self._achat(messages)
213        except Exception as e:
214            self._run_exception_hooks(messages, e)
215            raise e
216
217        if isinstance(response, Generator):
218            return self.generator_wrapper(response, messages)
219
220        response = self._run_post_hooks(messages, response)
221
222        return response

Asynchronous chat with the model using the provided messages.

def structured( self, messages: MessageHistory, schema: Type[pydantic.main.BaseModel]):
240    def structured(self, messages: MessageHistory, schema: Type[BaseModel]):
241        """Structured interaction with the model using the provided messages and output_schema."""
242        messages = self._run_pre_hooks(messages)
243
244        try:
245            response = self._structured(messages, schema)
246        except Exception as e:
247            self._run_exception_hooks(messages, e)
248            raise e
249
250        if isinstance(response, Generator):
251            return self.generator_wrapper(response, messages)
252
253        response = self._run_post_hooks(messages, response)
254
255        return response

Structured interaction with the model using the provided messages and output_schema.

async def astructured( self, messages: MessageHistory, schema: Type[pydantic.main.BaseModel]):
273    async def astructured(self, messages: MessageHistory, schema: Type[BaseModel]):
274        """Asynchronous structured interaction with the model using the provided messages and output_schema."""
275        messages = self._run_pre_hooks(messages)
276
277        try:
278            response = await self._astructured(messages, schema)
279        except Exception as e:
280            self._run_exception_hooks(messages, e)
281            raise e
282
283        if isinstance(response, Generator):
284            return self.generator_wrapper(response, messages)
285
286        response = self._run_post_hooks(messages, response)
287
288        return response

Asynchronous structured interaction with the model using the provided messages and output_schema.

def chat_with_tools( self, messages: MessageHistory, tools: List[Tool]):
302    def chat_with_tools(self, messages: MessageHistory, tools: List[Tool]):
303        """Chat with the model using the provided messages and tools."""
304        messages = self._run_pre_hooks(messages)
305
306        try:
307            response = self._chat_with_tools(messages, tools)
308        except Exception as e:
309            self._run_exception_hooks(messages, e)
310            raise e
311
312        if isinstance(response, Generator):
313            return self.generator_wrapper(response, messages)
314
315        response = self._run_post_hooks(messages, response)
316        return response

Chat with the model using the provided messages and tools.

async def achat_with_tools( self, messages: MessageHistory, tools: List[Tool]):
330    async def achat_with_tools(self, messages: MessageHistory, tools: List[Tool]):
331        """Asynchronous chat with the model using the provided messages and tools."""
332        messages = self._run_pre_hooks(messages)
333
334        try:
335            response = await self._achat_with_tools(messages, tools)
336        except Exception as e:
337            self._run_exception_hooks(messages, e)
338            raise e
339
340        if isinstance(response, Generator):
341            return self.generator_wrapper(response, messages)
342
343        response = self._run_post_hooks(messages, response)
344
345        return response

Asynchronous chat with the model using the provided messages and tools.

class ModelError(railtracks.llm._exceptions.RTLLMError):
 6class ModelError(RTLLMError):
 7    """
 8    Any Large Language Model (LLM) error.
 9    """
10
11    def __init__(
12        self,
13        reason: str,
14        message_history: MessageHistory = None,
15    ):
16        self.reason = reason
17        self.message_history = message_history
18
19        message = f"{self._color('Failure reason: ', self.BOLD_RED)}{self._color(reason, self.RED)}"
20        super().__init__(message)
21
22    def __str__(self):
23        base = super().__str__()
24        details = []
25        if self.message_history:
26            mh_str = str(self.message_history)
27            indented_mh = "\n".join(
28                "    " + line for line in mh_str.splitlines()
29            )  # 2 indents (2-spaces) per indent
30            details.append(
31                self._color("Message History:\n", self.BOLD_GREEN)
32                + self._color(indented_mh, self.GREEN)
33            )
34        if details:
35            notes_str = (
36                "\n"
37                + self._color("Details:\n", self.BOLD_GREEN)
38                + "\n".join(f"  {d}" for d in details)
39            )
40            return f"\n{self._color(base, self.RED)}{notes_str}"
41        return self._color(base, self.RED)

Any Large Language Model (LLM) error.

ModelError( reason: str, message_history: MessageHistory = None)
11    def __init__(
12        self,
13        reason: str,
14        message_history: MessageHistory = None,
15    ):
16        self.reason = reason
17        self.message_history = message_history
18
19        message = f"{self._color('Failure reason: ', self.BOLD_RED)}{self._color(reason, self.RED)}"
20        super().__init__(message)
reason
message_history
class ModelNotFoundError(railtracks.llm._exceptions.RTLLMError):
44class ModelNotFoundError(RTLLMError):
45    def __init__(self, reason: str, notes: list[str] = None):
46        self.reason = reason
47        self.notes = notes or []
48        super().__init__(reason)
49
50    def __str__(self):
51        base = super().__str__()
52        if self.notes:
53            notes_str = (
54                "\n"
55                + self._color("Tips to debug:\n", self.GREEN)
56                + "\n".join(self._color(f"- {note}", self.GREEN) for note in self.notes)
57            )
58            return f"\n{self._color(base, self.RED)}{notes_str}"
59        return self._color(base, self.RED)

A simple base class for all LLM Exceptions to inherit from.

ModelNotFoundError(reason: str, notes: list[str] = None)
45    def __init__(self, reason: str, notes: list[str] = None):
46        self.reason = reason
47        self.notes = notes or []
48        super().__init__(reason)
reason
notes
class FunctionCallingNotSupportedError(railtracks.llm.ModelError):
62class FunctionCallingNotSupportedError(ModelError):
63    """Error raised when a model does not support function calling."""
64
65    def __init__(self, model_name: str):
66        super().__init__(
67            reason=f"Model {model_name} does not support function calling. Chat with tools is not supported."
68        )

Error raised when a model does not support function calling.

FunctionCallingNotSupportedError(model_name: str)
65    def __init__(self, model_name: str):
66        super().__init__(
67            reason=f"Model {model_name} does not support function calling. Chat with tools is not supported."
68        )
class UnsupportedHyperparameterError(railtracks.llm.ModelError):
71class UnsupportedHyperparameterError(ModelError):
72    """Error raised when a model does not support a given common LLM hyperparameter."""
73
74    def __init__(self, model_name: str, hyperparameter: str, value):
75        super().__init__(
76            reason=(
77                f"Model {model_name} does not support '{hyperparameter}' "
78                f"(got {hyperparameter}={value!r})."
79            )
80        )

Error raised when a model does not support a given common LLM hyperparameter.

UnsupportedHyperparameterError(model_name: str, hyperparameter: str, value)
74    def __init__(self, model_name: str, hyperparameter: str, value):
75        super().__init__(
76            reason=(
77                f"Model {model_name} does not support '{hyperparameter}' "
78                f"(got {hyperparameter}={value!r})."
79            )
80        )
class MutuallyExclusiveHyperparametersError(railtracks.llm.ModelError):
83class MutuallyExclusiveHyperparametersError(ModelError):
84    """Error raised when two or more common hyperparameters cannot be combined for
85    this model."""
86
87    def __init__(self, model_name: str, hyperparameters: list[str], values: dict):
88        joined = " and ".join(f"'{p}'" for p in hyperparameters)
89        super().__init__(
90            reason=(
91                f"Model {model_name} does not support specifying {joined} together "
92                f"(got {values!r}). Use only one."
93            )
94        )

Error raised when two or more common hyperparameters cannot be combined for this model.

MutuallyExclusiveHyperparametersError(model_name: str, hyperparameters: list[str], values: dict)
87    def __init__(self, model_name: str, hyperparameters: list[str], values: dict):
88        joined = " and ".join(f"'{p}'" for p in hyperparameters)
89        super().__init__(
90            reason=(
91                f"Model {model_name} does not support specifying {joined} together "
92                f"(got {values!r}). Use only one."
93            )
94        )
class ToolCall(pydantic.main.BaseModel):
12class ToolCall(BaseModel):
13    """
14    A simple model object that represents a tool call.
15
16    This simple model represents a moment when a tool is called.
17    """
18
19    identifier: str = Field(description="The identifier attatched to this tool call.")
20    name: str = Field(description="The name of the tool being called.")
21    arguments: Dict[str, Any] = Field(
22        description="The arguments provided as input to the tool."
23    )
24
25    def __str__(self):
26        return f"{self.name}({self.arguments})"

A simple model object that represents a tool call.

This simple model represents a moment when a tool is called.

identifier: str
name: str
arguments: Dict[str, Any]
model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ToolResponse(pydantic.main.BaseModel):
29class ToolResponse(BaseModel):
30    """
31    A simple model object that represents a tool response.
32
33    This simple model should be used when adding a response to a tool.
34    """
35
36    identifier: str = Field(
37        description="The identifier attached to this tool response. This should match the identifier of the tool call."
38    )
39    name: str = Field(description="The name of the tool that generated this response.")
40    result: AnyStr = Field(description="The result of the tool call.")
41
42    def __str__(self):
43        return f"{self.name} -> {self.result}"

A simple model object that represents a tool response.

This simple model should be used when adding a response to a tool.

identifier: str
name: str
result: ~AnyStr
model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class UserMessage(railtracks.llm.message._StringOnlyContent[ForwardRef(<Role.user: 'user'>)]):
279class UserMessage(_StringOnlyContent[Role.user]):
280    """
281    Note that we only support string input
282
283    Args:
284        content: The content of the user message.
285        attachment: The file attachment(s) for the user message. Can be a single string or a list of strings,
286                    containing file paths, URLs, or data URIs. Defaults to None.
287        inject_prompt: Whether to inject prompt with context variables. Defaults to True.
288        trust_urls: Allow in-process fetch for URL attachments. Defaults to False.
289                    When False, `.pdf` URLs raise and unknown-extension URLs are
290                    handed to the provider unprobed (as `image_url`). When True,
291                    `.pdf` URLs are downloaded and embedded as base64, and
292                    unknown-extension URLs are HEAD-probed to detect PDFs.
293                    Set True only when every URL in `attachment` is
294                    developer-controlled — end-user-supplied URLs are an SSRF
295                    surface once their bytes are fetched in-process.
296        attachment_timeout: Per-request timeout in seconds for the HEAD probe
297                    and the in-process PDF download. Defaults to 10. Only
298                    applies when `trust_urls=True`. Raise this for large PDFs
299                    over slow links.
300    """
301
302    def __init__(
303        self,
304        content: str | None = None,
305        attachment: str | list[str] | None = None,
306        inject_prompt: bool = True,
307        trust_urls: bool = False,
308        attachment_timeout: float = 10.0,
309    ):
310        if attachment is not None:
311            if isinstance(attachment, list):
312                self.attachment = [
313                    Attachment(
314                        att,
315                        trust_urls=trust_urls,
316                        attachment_timeout=attachment_timeout,
317                    )
318                    for att in attachment
319                ]
320            else:
321                self.attachment = [
322                    Attachment(
323                        attachment,
324                        trust_urls=trust_urls,
325                        attachment_timeout=attachment_timeout,
326                    )
327                ]
328
329            if content is None:
330                logger.warning(
331                    "UserMessage initialized without content, setting to empty string."
332                )
333                content = ""
334        else:
335            self.attachment = None
336
337        if content is None:
338            raise ValueError(
339                "UserMessage must have content if no attachment is provided."
340            )
341        super().__init__(content=content, role=Role.user, inject_prompt=inject_prompt)

Note that we only support string input

Arguments:
  • content: The content of the user message.
  • attachment: The file attachment(s) for the user message. Can be a single string or a list of strings, containing file paths, URLs, or data URIs. Defaults to None.
  • inject_prompt: Whether to inject prompt with context variables. Defaults to True.
  • trust_urls: Allow in-process fetch for URL attachments. Defaults to False. When False, .pdf URLs raise and unknown-extension URLs are handed to the provider unprobed (as image_url). When True, .pdf URLs are downloaded and embedded as base64, and unknown-extension URLs are HEAD-probed to detect PDFs. Set True only when every URL in attachment is developer-controlled — end-user-supplied URLs are an SSRF surface once their bytes are fetched in-process.
  • attachment_timeout: Per-request timeout in seconds for the HEAD probe and the in-process PDF download. Defaults to 10. Only applies when trust_urls=True. Raise this for large PDFs over slow links.
UserMessage( content: str | None = None, attachment: str | list[str] | None = None, inject_prompt: bool = True, trust_urls: bool = False, attachment_timeout: float = 10.0)
302    def __init__(
303        self,
304        content: str | None = None,
305        attachment: str | list[str] | None = None,
306        inject_prompt: bool = True,
307        trust_urls: bool = False,
308        attachment_timeout: float = 10.0,
309    ):
310        if attachment is not None:
311            if isinstance(attachment, list):
312                self.attachment = [
313                    Attachment(
314                        att,
315                        trust_urls=trust_urls,
316                        attachment_timeout=attachment_timeout,
317                    )
318                    for att in attachment
319                ]
320            else:
321                self.attachment = [
322                    Attachment(
323                        attachment,
324                        trust_urls=trust_urls,
325                        attachment_timeout=attachment_timeout,
326                    )
327                ]
328
329            if content is None:
330                logger.warning(
331                    "UserMessage initialized without content, setting to empty string."
332                )
333                content = ""
334        else:
335            self.attachment = None
336
337        if content is None:
338            raise ValueError(
339                "UserMessage must have content if no attachment is provided."
340            )
341        super().__init__(content=content, role=Role.user, inject_prompt=inject_prompt)

A simple class that represents a message that an LLM can read.

Arguments:
  • content: The content of the message. It can take on any of the following types:
    • str: A simple string message.
    • List[ToolCall]: A list of tool calls.
    • ToolResponse: A tool response.
    • BaseModel: A custom base model object.
    • Stream: A stream object with a final_message and a generator.
  • role: The role of the message (assistant, user, system, tool, etc.).
  • inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
class SystemMessage(railtracks.llm.message._StringOnlyContent[ForwardRef(<Role.system: 'system'>)]):
344class SystemMessage(_StringOnlyContent[Role.system]):
345    """
346    A simple class that represents a system message.
347
348    Args:
349        content (str): The content of the system message.
350        inject_prompt (bool, optional): Whether to inject prompt with context  variables. Defaults to True.
351    """
352
353    def __init__(self, content: str, inject_prompt: bool = True):
354        super().__init__(content=content, role=Role.system, inject_prompt=inject_prompt)

A simple class that represents a system message.

Arguments:
  • content (str): The content of the system message.
  • inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
SystemMessage(content: str, inject_prompt: bool = True)
353    def __init__(self, content: str, inject_prompt: bool = True):
354        super().__init__(content=content, role=Role.system, inject_prompt=inject_prompt)

A simple class that represents a message that an LLM can read.

Arguments:
  • content: The content of the message. It can take on any of the following types:
    • str: A simple string message.
    • List[ToolCall]: A list of tool calls.
    • ToolResponse: A tool response.
    • BaseModel: A custom base model object.
    • Stream: A stream object with a final_message and a generator.
  • role: The role of the message (assistant, user, system, tool, etc.).
  • inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
class AssistantMessage(railtracks.llm.Message[~_T, ForwardRef()], typing.Generic[~_T]):
357class AssistantMessage(Message[_T, Role.assistant], Generic[_T]):
358    """
359    A simple class that represents a message from the assistant.
360
361    Args:
362        content (_T): The content of the assistant message.
363        inject_prompt (bool, optional): Whether to inject prompt with context  variables. Defaults to True.
364    """
365
366    def __init__(self, content: _T, inject_prompt: bool = True):
367        super().__init__(
368            content=content, role=Role.assistant, inject_prompt=inject_prompt
369        )
370
371        # Optionally stores the raw litellm message object so providers that
372        # attach extra metadata (e.g. Gemini thought_signature) can round-trip
373        # it back without any manual reconstruction.
374        self.raw_litellm_message = None

A simple class that represents a message from the assistant.

Arguments:
  • content (_T): The content of the assistant message.
  • inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
AssistantMessage(content: ~_T, inject_prompt: bool = True)
366    def __init__(self, content: _T, inject_prompt: bool = True):
367        super().__init__(
368            content=content, role=Role.assistant, inject_prompt=inject_prompt
369        )
370
371        # Optionally stores the raw litellm message object so providers that
372        # attach extra metadata (e.g. Gemini thought_signature) can round-trip
373        # it back without any manual reconstruction.
374        self.raw_litellm_message = None

A simple class that represents a message that an LLM can read.

Arguments:
  • content: The content of the message. It can take on any of the following types:
    • str: A simple string message.
    • List[ToolCall]: A list of tool calls.
    • ToolResponse: A tool response.
    • BaseModel: A custom base model object.
    • Stream: A stream object with a final_message and a generator.
  • role: The role of the message (assistant, user, system, tool, etc.).
  • inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
raw_litellm_message
class Message(typing.Generic[~_T, ~_TRole]):
186class Message(Generic[_T, _TRole]):
187    """
188    A base class that represents a message that an LLM can read.
189
190    Note the content may take on a variety of allowable types.
191    """
192
193    def __init__(
194        self,
195        content: _T,
196        role: _TRole,
197        inject_prompt: bool = True,
198    ):
199        """
200        A simple class that represents a message that an LLM can read.
201
202        Args:
203            content: The content of the message. It can take on any of the following types:
204                - str: A simple string message.
205                - List[ToolCall]: A list of tool calls.
206                - ToolResponse: A tool response.
207                - BaseModel: A custom base model object.
208                - Stream: A stream object with a final_message and a generator.
209            role: The role of the message (assistant, user, system, tool, etc.).
210            inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
211        """
212        assert isinstance(role, Role)
213        self.validate_content(content)
214        self._content = content
215        self._role = role
216        self._inject_prompt = inject_prompt
217
218    @classmethod
219    def validate_content(cls, content: _T):
220        pass
221
222    @property
223    def content(self) -> _T:
224        """Collects the content of the message."""
225        return self._content
226
227    @property
228    def role(self) -> _TRole:
229        """Collects the role of the message."""
230        return self._role
231
232    @property
233    def inject_prompt(self) -> bool:
234        """
235        A boolean that indicates whether this message should be injected into from context.
236        """
237        return self._inject_prompt
238
239    @inject_prompt.setter
240    def inject_prompt(self, value: bool):
241        """
242        Sets the inject_prompt property.
243        """
244        self._inject_prompt = value
245
246    def __str__(self):
247        return f"{self.role.value}: {self.content}"
248
249    def __repr__(self):
250        return str(self)
251
252    @property
253    def tool_calls(self):
254        """Gets the tool calls attached to this message, if any. If there are none return and empty list."""
255        tools: list[ToolCall] = []
256        if isinstance(self.content, list):
257            tools.extend(deepcopy(self.content))
258
259        return tools

A base class that represents a message that an LLM can read.

Note the content may take on a variety of allowable types.

Message(content: ~_T, role: ~_TRole, inject_prompt: bool = True)
193    def __init__(
194        self,
195        content: _T,
196        role: _TRole,
197        inject_prompt: bool = True,
198    ):
199        """
200        A simple class that represents a message that an LLM can read.
201
202        Args:
203            content: The content of the message. It can take on any of the following types:
204                - str: A simple string message.
205                - List[ToolCall]: A list of tool calls.
206                - ToolResponse: A tool response.
207                - BaseModel: A custom base model object.
208                - Stream: A stream object with a final_message and a generator.
209            role: The role of the message (assistant, user, system, tool, etc.).
210            inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
211        """
212        assert isinstance(role, Role)
213        self.validate_content(content)
214        self._content = content
215        self._role = role
216        self._inject_prompt = inject_prompt

A simple class that represents a message that an LLM can read.

Arguments:
  • content: The content of the message. It can take on any of the following types:
    • str: A simple string message.
    • List[ToolCall]: A list of tool calls.
    • ToolResponse: A tool response.
    • BaseModel: A custom base model object.
    • Stream: A stream object with a final_message and a generator.
  • role: The role of the message (assistant, user, system, tool, etc.).
  • inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
@classmethod
def validate_content(cls, content: ~_T):
218    @classmethod
219    def validate_content(cls, content: _T):
220        pass
content: ~_T
222    @property
223    def content(self) -> _T:
224        """Collects the content of the message."""
225        return self._content

Collects the content of the message.

role: ~_TRole
227    @property
228    def role(self) -> _TRole:
229        """Collects the role of the message."""
230        return self._role

Collects the role of the message.

inject_prompt: bool
232    @property
233    def inject_prompt(self) -> bool:
234        """
235        A boolean that indicates whether this message should be injected into from context.
236        """
237        return self._inject_prompt

A boolean that indicates whether this message should be injected into from context.

tool_calls
252    @property
253    def tool_calls(self):
254        """Gets the tool calls attached to this message, if any. If there are none return and empty list."""
255        tools: list[ToolCall] = []
256        if isinstance(self.content, list):
257            tools.extend(deepcopy(self.content))
258
259        return tools

Gets the tool calls attached to this message, if any. If there are none return and empty list.

378class ToolMessage(Message[ToolResponse, Role.tool]):
379    """
380    A simple class that represents a message that is a tool call answer.
381
382    Args:
383        content (ToolResponse): The tool response content for the message.
384    """
385
386    def __init__(self, content: ToolResponse):
387        if not isinstance(content, ToolResponse):
388            raise TypeError(
389                f"A {self.__class__.__name__} needs a ToolResponse but got {type(content)}. Check the invoke function of the OutputLessToolCallLLM node. That is the only place to return a ToolMessage."
390            )
391        super().__init__(content=content, role=Role.tool)

A simple class that represents a message that is a tool call answer.

Arguments:
  • content (ToolResponse): The tool response content for the message.
ToolMessage(content: ToolResponse)
386    def __init__(self, content: ToolResponse):
387        if not isinstance(content, ToolResponse):
388            raise TypeError(
389                f"A {self.__class__.__name__} needs a ToolResponse but got {type(content)}. Check the invoke function of the OutputLessToolCallLLM node. That is the only place to return a ToolMessage."
390            )
391        super().__init__(content=content, role=Role.tool)

A simple class that represents a message that an LLM can read.

Arguments:
  • content: The content of the message. It can take on any of the following types:
    • str: A simple string message.
    • List[ToolCall]: A list of tool calls.
    • ToolResponse: A tool response.
    • BaseModel: A custom base model object.
    • Stream: A stream object with a final_message and a generator.
  • role: The role of the message (assistant, user, system, tool, etc.).
  • inject_prompt (bool, optional): Whether to inject prompt with context variables. Defaults to True.
class MessageHistory(typing.List[railtracks.llm.message.Message]):
 9class MessageHistory(List[Message]):
10    """
11    A basic object that represents a history of messages. The object has all the same capability as a list such as
12    `.remove()`, `.append()`, etc.
13    """
14
15    def __str__(self):
16        return "\n".join([str(message) for message in self])
17
18    def removed_system_messages(self) -> MessageHistory:
19        """
20        Returns a new MessageHistory object with all SystemMessages removed.
21        """
22        return MessageHistory([msg for msg in self if msg.role != Role.system])

A basic object that represents a history of messages. The object has all the same capability as a list such as .remove(), .append(), etc.

def removed_system_messages(self) -> MessageHistory:
18    def removed_system_messages(self) -> MessageHistory:
19        """
20        Returns a new MessageHistory object with all SystemMessages removed.
21        """
22        return MessageHistory([msg for msg in self if msg.role != Role.system])

Returns a new MessageHistory object with all SystemMessages removed.

class ModelProvider(builtins.str, enum.Enum):
13class ModelProvider(str, Enum):
14    """
15    Enum of supported LLM model providers for RailTracks.
16
17    Attributes:
18        OPENAI: OpenAI models (e.g., GPT-3, GPT-4).
19        ANTHROPIC: Anthropic models (e.g., Claude).
20        GEMINI: Google Gemini models.
21        HUGGINGFACE: HuggingFace-hosted models.
22        AZUREAI: Azure OpenAI Service models.
23        OLLAMA: Ollama local LLMs.
24        COHERE: Cohere models.
25    """
26
27    OPENAI = "OpenAI"
28    ANTHROPIC = "Anthropic"
29    GEMINI = "Vertex_AI"
30    HUGGINGFACE = "HuggingFace"
31    AZUREAI = "AzureAI"
32    OLLAMA = "Ollama"
33    COHERE = "cohere_chat"
34    TELUS = "Telus"
35    PORTKEY = "PortKey"
36    UNKNOWN = "Unknown"

Enum of supported LLM model providers for RailTracks.

Attributes:
  • OPENAI: OpenAI models (e.g., GPT-3, GPT-4).
  • ANTHROPIC: Anthropic models (e.g., Claude).
  • GEMINI: Google Gemini models.
  • HUGGINGFACE: HuggingFace-hosted models.
  • AZUREAI: Azure OpenAI Service models.
  • OLLAMA: Ollama local LLMs.
  • COHERE: Cohere models.
OPENAI = <ModelProvider.OPENAI: 'OpenAI'>
ANTHROPIC = <ModelProvider.ANTHROPIC: 'Anthropic'>
GEMINI = <ModelProvider.GEMINI: 'Vertex_AI'>
HUGGINGFACE = <ModelProvider.HUGGINGFACE: 'HuggingFace'>
AZUREAI = <ModelProvider.AZUREAI: 'AzureAI'>
OLLAMA = <ModelProvider.OLLAMA: 'Ollama'>
COHERE = <ModelProvider.COHERE: 'cohere_chat'>
TELUS = <ModelProvider.TELUS: 'Telus'>
PORTKEY = <ModelProvider.PORTKEY: 'PortKey'>
UNKNOWN = <ModelProvider.UNKNOWN: 'Unknown'>
class Tool:
 29class Tool:
 30    """
 31    A quasi-immutable class designed to represent a single Tool object.
 32    You pass in key details (name, description, and required parameters).
 33    """
 34
 35    def __init__(
 36        self,
 37        name: str,
 38        detail: str,
 39        parameters: Iterable[Parameter] | Dict[str, Any] | None = None,
 40    ):
 41        """
 42        Creates a new Tool instance.
 43
 44        Args:
 45            name: The name of the tool.
 46            detail: A detailed description of the tool.
 47            parameters: Parameters attached to this tool; a set of Parameter objects, or a dict.
 48        """
 49
 50        if (
 51            isinstance(parameters, dict) and len(parameters) > 0
 52        ):  # if parameters is a JSON-output_schema, convert into Parameter objects (Checks should be done in validate_tool_params)
 53            props = parameters.get("properties")
 54            required_fields = list(parameters.get("required", []))
 55            param_objs: List[Parameter] = []
 56            for name, prop in props.items():
 57                param_objs.append(
 58                    parse_json_schema_to_parameter(name, prop, name in required_fields)
 59                )
 60            parameters = param_objs
 61
 62        self._name = name
 63        self._detail = detail
 64        self._parameters = parameters
 65
 66    @property
 67    def name(self) -> str:
 68        """Get the name of the tool."""
 69        return self._name
 70
 71    @property
 72    def detail(self) -> str:
 73        """Returns the detailed description for this tool."""
 74        return self._detail
 75
 76    @property
 77    def parameters(self) -> List[Parameter] | None:
 78        """Gets the parameters attached to this tool (if any)."""
 79        return self._parameters
 80
 81    def __str__(self) -> str:
 82        """String representation of the tool."""
 83        if self._parameters:
 84            params_str = "{" + ", ".join(str(p) for p in self._parameters) + "}"
 85        return f"Tool(name={self._name}, detail={self._detail}, parameters={params_str if self._parameters else 'None'})"
 86
 87    @classmethod
 88    def from_function(
 89        cls,
 90        func: Callable,
 91        /,
 92        *,
 93        name: str | None = None,
 94        details: str | None = None,
 95        params: Type[BaseModel] | Dict[str, Any] | List[Parameter] | None = None,
 96    ) -> Self:
 97        """
 98        Creates a Tool from a Python callable.
 99        Uses the function's docstring and type annotations to extract details and parameter info.
100
101        KEY NOTE: No checking is done to ensure that the inserted params match the function signature
102
103        Args:
104            func: The function to create a tool from.
105            name: Optional name for the tool. If not provided, uses the function's name.
106            details: Optional detailed description for the tool. If not provided, extracts from the function's docstring.
107            params: Optional parameters for the tool. If not provided, infers from the function's signature and docstring.
108
109        Returns:
110            A Tool instance representing the function.
111        """
112        # TODO: add set verification to ensure that the params match the function signature
113        # Check if the function is a method in a class
114        in_class = bool(func.__qualname__ and "." in func.__qualname__)
115
116        # Parse the docstring to get parameter descriptions
117        arg_descriptions = parse_docstring_args(func.__doc__ or "")
118
119        try:
120            # Get the function signature
121            signature = inspect.signature(func)
122        except ValueError:
123            raise ToolCreationError(
124                message="Cannot convert kwargs for builtin functions.",
125                notes=[
126                    "Please use a cutom made function.",
127                    "Eg.- \ndef my_custom_function(a: int, b: str):\n    pass",
128                ],
129            )
130
131        if name is not None:
132            # TODO: add some checking here to ensure that the name is valid snake case.
133            function_name = name
134        else:
135            function_name = func.__name__
136
137        docstring = func.__doc__.strip() if func.__doc__ else ""
138
139        if params is not None:
140            parameters = params
141        else:
142            # Check for multiple Args sections (warning)
143            # Only need to do this if we need to.
144            if docstring.count("Args:") > 1:
145                warnings.warn("Multiple 'Args:' sections found in the docstring.")
146            # Create parameter handlers
147            handlers: List[ParameterHandler] = [
148                PydanticModelHandler(),
149                SequenceParameterHandler(),
150                UnionParameterHandler(),
151                DefaultParameterHandler(),
152            ]
153
154            parameters: List[Parameter] = []
155
156            for param in signature.parameters.values():
157                # Skip 'self' parameter for class methods
158                if in_class and (param.name == "self" or param.name == "cls"):
159                    continue
160
161                description = arg_descriptions.get(param.name, "")
162
163                # Check if the parameter is required
164                required = param.default == inspect.Parameter.empty
165
166                handler = next(h for h in handlers if h.can_handle(param.annotation))
167
168                param_obj = handler.create_parameter(
169                    param.name, param.annotation, description, required
170                )
171
172                parameters.append(param_obj)
173
174        if details is not None:
175            main_description = details
176        else:
177            main_description = extract_main_description(docstring)
178
179        tool_info = Tool(
180            name=function_name,
181            detail=main_description,
182            parameters=parameters,
183        )
184        return tool_info
185
186    @classmethod
187    def from_mcp(cls, tool) -> Self:
188        """
189        Creates a Tool from an MCP tool object.
190
191        Args:
192            tool: The MCP tool to create a Tool from.
193
194        Returns:
195            A Tool instance representing the MCP tool.
196        """
197        input_schema = getattr(tool, "inputSchema", None)
198        if not input_schema or input_schema["type"] != "object":
199            raise ToolCreationError(
200                message="The inputSchema for an MCP Tool must be 'object'. ",
201                notes=[
202                    "If an MCP tool has a different output_schema, create a GitHub issue and support will be added."
203                ],
204            )
205
206        properties = input_schema.get("properties", {})
207        required_fields = set(input_schema.get("required", []))
208        param_objs = set()
209        for name, prop in properties.items():
210            required = name in required_fields
211            param_objs.add(parse_json_schema_to_parameter(name, prop, required))
212
213        return cls(name=tool.name, detail=tool.description, parameters=param_objs)

A quasi-immutable class designed to represent a single Tool object. You pass in key details (name, description, and required parameters).

Tool( name: str, detail: str, parameters: Union[Iterable[Parameter], Dict[str, Any], NoneType] = None)
35    def __init__(
36        self,
37        name: str,
38        detail: str,
39        parameters: Iterable[Parameter] | Dict[str, Any] | None = None,
40    ):
41        """
42        Creates a new Tool instance.
43
44        Args:
45            name: The name of the tool.
46            detail: A detailed description of the tool.
47            parameters: Parameters attached to this tool; a set of Parameter objects, or a dict.
48        """
49
50        if (
51            isinstance(parameters, dict) and len(parameters) > 0
52        ):  # if parameters is a JSON-output_schema, convert into Parameter objects (Checks should be done in validate_tool_params)
53            props = parameters.get("properties")
54            required_fields = list(parameters.get("required", []))
55            param_objs: List[Parameter] = []
56            for name, prop in props.items():
57                param_objs.append(
58                    parse_json_schema_to_parameter(name, prop, name in required_fields)
59                )
60            parameters = param_objs
61
62        self._name = name
63        self._detail = detail
64        self._parameters = parameters

Creates a new Tool instance.

Arguments:
  • name: The name of the tool.
  • detail: A detailed description of the tool.
  • parameters: Parameters attached to this tool; a set of Parameter objects, or a dict.
name: str
66    @property
67    def name(self) -> str:
68        """Get the name of the tool."""
69        return self._name

Get the name of the tool.

detail: str
71    @property
72    def detail(self) -> str:
73        """Returns the detailed description for this tool."""
74        return self._detail

Returns the detailed description for this tool.

parameters: Optional[List[Parameter]]
76    @property
77    def parameters(self) -> List[Parameter] | None:
78        """Gets the parameters attached to this tool (if any)."""
79        return self._parameters

Gets the parameters attached to this tool (if any).

@classmethod
def from_function( cls, func: Callable, /, *, name: str | None = None, details: str | None = None, params: Union[Type[pydantic.main.BaseModel], Dict[str, Any], List[Parameter], NoneType] = None) -> typing_extensions.Self:
 87    @classmethod
 88    def from_function(
 89        cls,
 90        func: Callable,
 91        /,
 92        *,
 93        name: str | None = None,
 94        details: str | None = None,
 95        params: Type[BaseModel] | Dict[str, Any] | List[Parameter] | None = None,
 96    ) -> Self:
 97        """
 98        Creates a Tool from a Python callable.
 99        Uses the function's docstring and type annotations to extract details and parameter info.
100
101        KEY NOTE: No checking is done to ensure that the inserted params match the function signature
102
103        Args:
104            func: The function to create a tool from.
105            name: Optional name for the tool. If not provided, uses the function's name.
106            details: Optional detailed description for the tool. If not provided, extracts from the function's docstring.
107            params: Optional parameters for the tool. If not provided, infers from the function's signature and docstring.
108
109        Returns:
110            A Tool instance representing the function.
111        """
112        # TODO: add set verification to ensure that the params match the function signature
113        # Check if the function is a method in a class
114        in_class = bool(func.__qualname__ and "." in func.__qualname__)
115
116        # Parse the docstring to get parameter descriptions
117        arg_descriptions = parse_docstring_args(func.__doc__ or "")
118
119        try:
120            # Get the function signature
121            signature = inspect.signature(func)
122        except ValueError:
123            raise ToolCreationError(
124                message="Cannot convert kwargs for builtin functions.",
125                notes=[
126                    "Please use a cutom made function.",
127                    "Eg.- \ndef my_custom_function(a: int, b: str):\n    pass",
128                ],
129            )
130
131        if name is not None:
132            # TODO: add some checking here to ensure that the name is valid snake case.
133            function_name = name
134        else:
135            function_name = func.__name__
136
137        docstring = func.__doc__.strip() if func.__doc__ else ""
138
139        if params is not None:
140            parameters = params
141        else:
142            # Check for multiple Args sections (warning)
143            # Only need to do this if we need to.
144            if docstring.count("Args:") > 1:
145                warnings.warn("Multiple 'Args:' sections found in the docstring.")
146            # Create parameter handlers
147            handlers: List[ParameterHandler] = [
148                PydanticModelHandler(),
149                SequenceParameterHandler(),
150                UnionParameterHandler(),
151                DefaultParameterHandler(),
152            ]
153
154            parameters: List[Parameter] = []
155
156            for param in signature.parameters.values():
157                # Skip 'self' parameter for class methods
158                if in_class and (param.name == "self" or param.name == "cls"):
159                    continue
160
161                description = arg_descriptions.get(param.name, "")
162
163                # Check if the parameter is required
164                required = param.default == inspect.Parameter.empty
165
166                handler = next(h for h in handlers if h.can_handle(param.annotation))
167
168                param_obj = handler.create_parameter(
169                    param.name, param.annotation, description, required
170                )
171
172                parameters.append(param_obj)
173
174        if details is not None:
175            main_description = details
176        else:
177            main_description = extract_main_description(docstring)
178
179        tool_info = Tool(
180            name=function_name,
181            detail=main_description,
182            parameters=parameters,
183        )
184        return tool_info

Creates a Tool from a Python callable. Uses the function's docstring and type annotations to extract details and parameter info.

KEY NOTE: No checking is done to ensure that the inserted params match the function signature

Arguments:
  • func: The function to create a tool from.
  • name: Optional name for the tool. If not provided, uses the function's name.
  • details: Optional detailed description for the tool. If not provided, extracts from the function's docstring.
  • params: Optional parameters for the tool. If not provided, infers from the function's signature and docstring.
Returns:

A Tool instance representing the function.

@classmethod
def from_mcp(cls, tool) -> typing_extensions.Self:
186    @classmethod
187    def from_mcp(cls, tool) -> Self:
188        """
189        Creates a Tool from an MCP tool object.
190
191        Args:
192            tool: The MCP tool to create a Tool from.
193
194        Returns:
195            A Tool instance representing the MCP tool.
196        """
197        input_schema = getattr(tool, "inputSchema", None)
198        if not input_schema or input_schema["type"] != "object":
199            raise ToolCreationError(
200                message="The inputSchema for an MCP Tool must be 'object'. ",
201                notes=[
202                    "If an MCP tool has a different output_schema, create a GitHub issue and support will be added."
203                ],
204            )
205
206        properties = input_schema.get("properties", {})
207        required_fields = set(input_schema.get("required", []))
208        param_objs = set()
209        for name, prop in properties.items():
210            required = name in required_fields
211            param_objs.add(parse_json_schema_to_parameter(name, prop, required))
212
213        return cls(name=tool.name, detail=tool.description, parameters=param_objs)

Creates a Tool from an MCP tool object.

Arguments:
  • tool: The MCP tool to create a Tool from.
Returns:

A Tool instance representing the MCP tool.

class AnthropicLLM(railtracks.llm.models.api_providers._provider_wrapper.ProviderLLMWrapper[~_TStream], typing.Generic[~_TStream]):
10class AnthropicLLM(ProviderLLMWrapper[_TStream], Generic[_TStream]):
11    @classmethod
12    def model_gateway(cls) -> ModelProvider:
13        return ModelProvider.ANTHROPIC

A large base class that wraps around a litellm model.

Note that the model object should be interacted with via the methods provided in the wrapper class:

Each individual API should implement the required abstract_methods in order to allow users to interact with a model of that type.

@classmethod
def model_gateway(cls) -> ModelProvider:
11    @classmethod
12    def model_gateway(cls) -> ModelProvider:
13        return ModelProvider.ANTHROPIC

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

class AzureAILLM(railtracks.llm.models._litellm_wrapper.LiteLLMWrapper[~_TStream]):
 25class AzureAILLM(LiteLLMWrapper[_TStream]):
 26    """Azure Foundry LLM wrapper.
 27
 28    Accepts either litellm prefix:
 29    - ``azure/<deployment>`` — Azure OpenAI Service route; the string after the
 30      slash is the user-chosen deployment name and can be anything.
 31    - ``azure_ai/<model>`` — Azure AI Foundry model-inference route; the string
 32      after the slash is a model identifier from Foundry's catalog.
 33
 34    The model string is forwarded verbatim to litellm — no client-side validation
 35    is done against a static catalog, since deployment names are user-defined and
 36    can't be known ahead of time.
 37    """
 38
 39    @classmethod
 40    def model_gateway(cls):
 41        return ModelProvider.AZUREAI
 42
 43    def model_provider(self) -> ModelProvider:
 44        return self.model_gateway()
 45
 46    def __init__(
 47        self,
 48        model_name: str,
 49        *,
 50        temperature: float | None = None,
 51        top_p: float | None = None,
 52        max_tokens: int | None = None,
 53        frequency_penalty: float | None = None,
 54        presence_penalty: float | None = None,
 55        reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
 56        service_tier: str | None = None,
 57        verbosity: Literal["low", "medium", "high"] | None = None,
 58        retry_approach: RetryApproach | None = None,
 59        **kwargs,
 60    ):
 61        """Initialize an Azure AI LLM instance.
 62
 63        Args:
 64            model_name (str): Full litellm model string, e.g. ``azure/my-deployment``
 65                or ``azure_ai/deepseek-r1``. See the class docstring for the
 66                difference between the two prefixes.
 67            temperature (float | None, optional): Sampling temperature for generation (e.g. 0.0–2.0).
 68                If None, the provider default is used.
 69            top_p (float | None, optional): Nucleus sampling threshold.
 70            max_tokens (int | None, optional): Maximum tokens to generate.
 71            frequency_penalty (float | None, optional): Penalizes tokens by how often
 72                they've already appeared.
 73            presence_penalty (float | None, optional): Penalizes tokens that have
 74                already appeared at all.
 75            reasoning_effort (Literal["minimal", "low", "medium", "high"] | None, optional):
 76                Requested reasoning effort for reasoning-capable models.
 77            service_tier (str | None, optional): Requested service tier. Provider-specific.
 78            verbosity (Literal["low", "medium", "high"] | None, optional): Requested
 79                output verbosity for models that support it.
 80            retry_approach (RetryApproach | None, optional): Retry strategy for transient
 81                failures.
 82            **kwargs: Additional arguments passed to the parent LiteLLMWrapper.
 83
 84        Raises:
 85            AzureAIError: If the specified model is not available or if there are issues with the Azure AI service.
 86        """
 87        if kwargs.get("stream"):
 88            warn_pending_change(
 89                "Constructing a model with `stream=True`",
 90                change="is removed",
 91                instead="rt.astream(agent, ...) to stream an agent run",
 92                detail=(
 93                    "Streaming becomes async in 1.5.0: per-call model methods "
 94                    "(astream_chat, astream_chat_with_tools, astream_structured) "
 95                    "replace the streamed return value of chat()."
 96                ),
 97            )
 98
 99        super().__init__(
100            model_name,
101            temperature=temperature,
102            top_p=top_p,
103            max_tokens=max_tokens,
104            frequency_penalty=frequency_penalty,
105            presence_penalty=presence_penalty,
106            reasoning_effort=reasoning_effort,
107            service_tier=service_tier,
108            verbosity=verbosity,
109            retry_approach=retry_approach,
110            **kwargs,
111        )
112        self.logger = logger
113
114    def chat(self, messages: MessageHistory):
115        try:
116            return super().chat(messages)
117        except InternalServerError as e:
118            raise AzureAIError(
119                reason=f"Azure AI LLM error while processing the request: {e}"
120            ) from e
121
122    def chat_with_tools(self, messages: MessageHistory, tools: List[Tool]):
123        try:
124            return super().chat_with_tools(messages, tools)
125        except InternalServerError as e:
126            raise AzureAIError(
127                reason=f"Azure AI LLM error while processing the request: {e}"
128            ) from e

Azure Foundry LLM wrapper.

Accepts either litellm prefix:

  • azure/<deployment> — Azure OpenAI Service route; the string after the slash is the user-chosen deployment name and can be anything.
  • azure_ai/<model> — Azure AI Foundry model-inference route; the string after the slash is a model identifier from Foundry's catalog.

The model string is forwarded verbatim to litellm — no client-side validation is done against a static catalog, since deployment names are user-defined and can't be known ahead of time.

AzureAILLM( model_name: str, *, temperature: float | None = None, top_p: float | None = None, max_tokens: int | None = None, frequency_penalty: float | None = None, presence_penalty: float | None = None, reasoning_effort: Optional[Literal['minimal', 'low', 'medium', 'high']] = None, service_tier: str | None = None, verbosity: Optional[Literal['low', 'medium', 'high']] = None, retry_approach: railtracks.llm.retries.RetryApproach | None = None, **kwargs)
 46    def __init__(
 47        self,
 48        model_name: str,
 49        *,
 50        temperature: float | None = None,
 51        top_p: float | None = None,
 52        max_tokens: int | None = None,
 53        frequency_penalty: float | None = None,
 54        presence_penalty: float | None = None,
 55        reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
 56        service_tier: str | None = None,
 57        verbosity: Literal["low", "medium", "high"] | None = None,
 58        retry_approach: RetryApproach | None = None,
 59        **kwargs,
 60    ):
 61        """Initialize an Azure AI LLM instance.
 62
 63        Args:
 64            model_name (str): Full litellm model string, e.g. ``azure/my-deployment``
 65                or ``azure_ai/deepseek-r1``. See the class docstring for the
 66                difference between the two prefixes.
 67            temperature (float | None, optional): Sampling temperature for generation (e.g. 0.0–2.0).
 68                If None, the provider default is used.
 69            top_p (float | None, optional): Nucleus sampling threshold.
 70            max_tokens (int | None, optional): Maximum tokens to generate.
 71            frequency_penalty (float | None, optional): Penalizes tokens by how often
 72                they've already appeared.
 73            presence_penalty (float | None, optional): Penalizes tokens that have
 74                already appeared at all.
 75            reasoning_effort (Literal["minimal", "low", "medium", "high"] | None, optional):
 76                Requested reasoning effort for reasoning-capable models.
 77            service_tier (str | None, optional): Requested service tier. Provider-specific.
 78            verbosity (Literal["low", "medium", "high"] | None, optional): Requested
 79                output verbosity for models that support it.
 80            retry_approach (RetryApproach | None, optional): Retry strategy for transient
 81                failures.
 82            **kwargs: Additional arguments passed to the parent LiteLLMWrapper.
 83
 84        Raises:
 85            AzureAIError: If the specified model is not available or if there are issues with the Azure AI service.
 86        """
 87        if kwargs.get("stream"):
 88            warn_pending_change(
 89                "Constructing a model with `stream=True`",
 90                change="is removed",
 91                instead="rt.astream(agent, ...) to stream an agent run",
 92                detail=(
 93                    "Streaming becomes async in 1.5.0: per-call model methods "
 94                    "(astream_chat, astream_chat_with_tools, astream_structured) "
 95                    "replace the streamed return value of chat()."
 96                ),
 97            )
 98
 99        super().__init__(
100            model_name,
101            temperature=temperature,
102            top_p=top_p,
103            max_tokens=max_tokens,
104            frequency_penalty=frequency_penalty,
105            presence_penalty=presence_penalty,
106            reasoning_effort=reasoning_effort,
107            service_tier=service_tier,
108            verbosity=verbosity,
109            retry_approach=retry_approach,
110            **kwargs,
111        )
112        self.logger = logger

Initialize an Azure AI LLM instance.

Arguments:
  • model_name (str): Full litellm model string, e.g. azure/my-deployment or azure_ai/deepseek-r1. See the class docstring for the difference between the two prefixes.
  • temperature (float | None, optional): Sampling temperature for generation (e.g. 0.0–2.0). If None, the provider default is used.
  • top_p (float | None, optional): Nucleus sampling threshold.
  • max_tokens (int | None, optional): Maximum tokens to generate.
  • frequency_penalty (float | None, optional): Penalizes tokens by how often they've already appeared.
  • presence_penalty (float | None, optional): Penalizes tokens that have already appeared at all.
  • reasoning_effort (Literal["minimal", "low", "medium", "high"] | None, optional): Requested reasoning effort for reasoning-capable models.
  • service_tier (str | None, optional): Requested service tier. Provider-specific.
  • verbosity (Literal["low", "medium", "high"] | None, optional): Requested output verbosity for models that support it.
  • retry_approach (RetryApproach | None, optional): Retry strategy for transient failures.
  • **kwargs: Additional arguments passed to the parent LiteLLMWrapper.
Raises:
  • AzureAIError: If the specified model is not available or if there are issues with the Azure AI service.
@classmethod
def model_gateway(cls):
39    @classmethod
40    def model_gateway(cls):
41        return ModelProvider.AZUREAI

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

def model_provider(self) -> ModelProvider:
43    def model_provider(self) -> ModelProvider:
44        return self.model_gateway()

The name of the provider of this model (The Company that owns the model)

logger
def chat(self, messages: MessageHistory):
114    def chat(self, messages: MessageHistory):
115        try:
116            return super().chat(messages)
117        except InternalServerError as e:
118            raise AzureAIError(
119                reason=f"Azure AI LLM error while processing the request: {e}"
120            ) from e

Chat with the model using the provided messages.

def chat_with_tools( self, messages: MessageHistory, tools: List[Tool]):
122    def chat_with_tools(self, messages: MessageHistory, tools: List[Tool]):
123        try:
124            return super().chat_with_tools(messages, tools)
125        except InternalServerError as e:
126            raise AzureAIError(
127                reason=f"Azure AI LLM error while processing the request: {e}"
128            ) from e

Chat with the model using the provided messages and tools.

class CohereLLM(railtracks.llm.models._litellm_wrapper.LiteLLMWrapper[~_TStream], abc.ABC, typing.Generic[~_TStream]):
 6class CohereLLM(ProviderLLMWrapper):
 7    """
 8    A wrapper that provides access to the Cohere API.
 9    """
10
11    @classmethod
12    def model_gateway(cls):
13        return ModelProvider.COHERE

A wrapper that provides access to the Cohere API.

@classmethod
def model_gateway(cls):
11    @classmethod
12    def model_gateway(cls):
13        return ModelProvider.COHERE

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

class HuggingFaceLLM(railtracks.llm.models.api_providers._provider_wrapper.ProviderLLMWrapper[~_TStream]):
11class HuggingFaceLLM(ProviderLLMWrapper[_TStream]):
12    def _pre_init_provider_check(self, model_name):
13        """called by __init__ before the super call in ProviderLLMWrapper"""
14        # for huggingface models there is no good way of using `get_llm_provider` to check if the model is valid.
15        # so we are just goinog to add `huggingface/` to the model name in case it is not there.
16        # if the model name happens to be invalid, the error will be generated at runtime during `litellm.completion`. See `_litellm_wrapper.py`
17        if model_name.startswith(self.model_provider().lower()):
18            model_name = "/".join(model_name.split("/")[1:])
19        try:
20            assert len(model_name.split("/")) == 3, "Invalid model name"
21        except AssertionError as e:
22            raise ModelNotFoundError(
23                reason=e.args[0],
24                notes=[
25                    "Model name must be of the format `huggingface/<provider>/<hf_org_or_user>/<hf_model>` or `<provider>/<hf_org_or_user>/<hf_model>`",
26                    "We only support the huggingface Serverless Inference Provider Models.",
27                    "Provider List: https://docs.litellm.ai/docs/providers",
28                ],
29            )
30        return model_name
31
32    def model_provider(self) -> ModelProvider:
33        # TODO implement logic for all the possible providers attached the hugging face.
34        return ModelProvider.HUGGINGFACE
35
36    def _validate_tool_calling_support(self):
37        # NOTE: special exception case for huggingface
38        # Due to the wide range of huggingface models, `litellm.supports_function_calling` isn't always accurate.
39        # so we are just going to skip the check and the error (if any) will be generated at runtime during `litellm.completion`.
40        pass
41
42    @classmethod
43    def model_gateway(cls):
44        return ModelProvider.HUGGINGFACE

A large base class that wraps around a litellm model.

Note that the model object should be interacted with via the methods provided in the wrapper class:

Each individual API should implement the required abstract_methods in order to allow users to interact with a model of that type.

def model_provider(self) -> ModelProvider:
32    def model_provider(self) -> ModelProvider:
33        # TODO implement logic for all the possible providers attached the hugging face.
34        return ModelProvider.HUGGINGFACE

Returns the name of the provider

@classmethod
def model_gateway(cls):
42    @classmethod
43    def model_gateway(cls):
44        return ModelProvider.HUGGINGFACE

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

class OpenAILLM(railtracks.llm.models.api_providers._provider_wrapper.ProviderLLMWrapper[~_TStream], typing.Generic[~_TStream]):
10class OpenAILLM(ProviderLLMWrapper[_TStream], Generic[_TStream]):
11    """
12    A wrapper that provides access to the OPENAI API.
13    """
14
15    @classmethod
16    def model_gateway(cls):
17        return ModelProvider.OPENAI

A wrapper that provides access to the OPENAI API.

@classmethod
def model_gateway(cls):
15    @classmethod
16    def model_gateway(cls):
17        return ModelProvider.OPENAI

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

class GeminiLLM(railtracks.llm.models.api_providers._provider_wrapper.ProviderLLMWrapper[~_TStream]):
10class GeminiLLM(ProviderLLMWrapper[_TStream]):
11    def full_model_name(self, model_name: str) -> str:
12        # for gemini models through litellm, we need 'gemini/{model_name}' format, but we do this after the checks in ProiLLMWrapper init
13        return f"gemini/{model_name}"
14
15    @classmethod
16    def model_gateway(cls):
17        return ModelProvider.GEMINI  # litellm uses this for the provider for Gemini, we are using this in the checks in _provider_wrapper.py

A large base class that wraps around a litellm model.

Note that the model object should be interacted with via the methods provided in the wrapper class:

Each individual API should implement the required abstract_methods in order to allow users to interact with a model of that type.

def full_model_name(self, model_name: str) -> str:
11    def full_model_name(self, model_name: str) -> str:
12        # for gemini models through litellm, we need 'gemini/{model_name}' format, but we do this after the checks in ProiLLMWrapper init
13        return f"gemini/{model_name}"

After the provider is checked, this method is called to get the full model name

@classmethod
def model_gateway(cls):
15    @classmethod
16    def model_gateway(cls):
17        return ModelProvider.GEMINI  # litellm uses this for the provider for Gemini, we are using this in the checks in _provider_wrapper.py

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

class OllamaLLM(railtracks.llm.models._litellm_wrapper.LiteLLMWrapper[~_TStream]):
 28class OllamaLLM(LiteLLMWrapper[_TStream]):
 29    def __init__(
 30        self,
 31        model_name: str,
 32        stream: _TStream = False,
 33        domain: Literal["default", "auto", "custom"] = "default",
 34        custom_domain: str | None = None,
 35        temperature: float | None = None,
 36        top_p: float | None = None,
 37        max_tokens: int | None = None,
 38        frequency_penalty: float | None = None,
 39        presence_penalty: float | None = None,
 40        reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
 41        service_tier: str | None = None,
 42        verbosity: Literal["low", "medium", "high"] | None = None,
 43        retry_approach: RetryApproach | None = None,
 44        **kwargs,
 45    ):
 46        """Initialize an Ollama LLM instance.
 47
 48        Args:
 49            model_name (str): Name of the Ollama model to use.
 50            stream (bool): Whether to stream the response.
 51            domain (Literal["default", "auto", "custom"], optional): The domain configuration mode.
 52                - "default": Uses the default localhost domain (http://localhost:11434)
 53                - "auto": Uses the OLLAMA_HOST environment variable, raises OllamaError if not set
 54                - "custom": Uses the provided custom_domain parameter, raises OllamaError if not provided
 55                Defaults to "default".
 56            custom_domain (str | None, optional): Custom domain URL to use when domain is set to "custom".
 57                Must be provided if domain="custom". Defaults to None.
 58            temperature (float | None, optional): Sampling temperature for generation (e.g. 0.0–2.0).
 59                If None, the provider default is used.
 60            top_p (float | None, optional): Nucleus sampling threshold.
 61            max_tokens (int | None, optional): Maximum tokens to generate.
 62            frequency_penalty (float | None, optional): Penalizes tokens by how often
 63                they've already appeared.
 64            presence_penalty (float | None, optional): Penalizes tokens that have
 65                already appeared at all.
 66            reasoning_effort (Literal["minimal", "low", "medium", "high"] | None, optional):
 67                Requested reasoning effort for reasoning-capable models.
 68            service_tier (str | None, optional): Requested service tier. Provider-specific.
 69            verbosity (Literal["low", "medium", "high"] | None, optional): Requested
 70                output verbosity for models that support it.
 71            retry_approach (RetryApproach | None, optional): Retry strategy for transient
 72                failures.
 73            **kwargs: Additional arguments passed to the parent LiteLLMWrapper.
 74
 75        Raises:
 76            OllamaError: If:
 77                - domain is "auto" and OLLAMA_HOST environment variable is not set
 78                - domain is "custom" and custom_domain is not provided
 79                - specified model is not available on the server
 80            RequestException: If connection to Ollama server fails
 81        """
 82        if stream:
 83            warn_pending_change(
 84                "Constructing a model with `stream=True`",
 85                change="is removed",
 86                instead="rt.astream(agent, ...) to stream an agent run",
 87                detail=(
 88                    "Streaming becomes async in 1.5.0: per-call model methods "
 89                    "(astream_chat, astream_chat_with_tools, astream_structured) "
 90                    "replace the streamed return value of chat()."
 91                ),
 92            )
 93
 94        if not model_name.startswith("ollama/"):
 95            logger.warning(
 96                f"Prepending 'ollama/' to model name '{model_name}' for Ollama"
 97            )
 98            model_name = f"ollama/{model_name}"
 99        super().__init__(
100            model_name=model_name,
101            stream=stream,
102            temperature=temperature,
103            top_p=top_p,
104            max_tokens=max_tokens,
105            frequency_penalty=frequency_penalty,
106            presence_penalty=presence_penalty,
107            reasoning_effort=reasoning_effort,
108            service_tier=service_tier,
109            verbosity=verbosity,
110            retry_approach=retry_approach,
111            **kwargs,
112        )
113
114        match domain:
115            case "default":
116                self.domain = DEFAULT_DOMAIN
117            case "auto":
118                domain_from_env = os.getenv("OLLAMA_HOST")
119                if domain_from_env is None:
120                    raise OllamaError("OLLAMA_HOST environment variable not set")
121                self.domain = domain_from_env
122            case "custom":
123                if custom_domain is None:
124                    raise OllamaError(
125                        "Custom domain must be provided when domain is set to 'custom'"
126                    )
127                self.domain = custom_domain
128
129        self._run_check(
130            "api/tags"
131        )  # This will crash the workflow if Ollama is not setup properly
132
133    def _run_check(self, endpoint: str):
134        url = f"{self.domain}/{endpoint.lstrip('/')}"
135        try:
136            response = requests.get(url)
137            response.raise_for_status()
138
139            models = response.json()
140
141            model_names = {model["name"] for model in models["models"]}
142
143            model_name = self.model_name().rsplit("/", 1)[
144                -1
145            ]  # extract the model name if the provider is also included
146
147            if model_name not in model_names:
148                error_msg = f"{self.model_name()} not available on server {self.domain}. Avaiable models are: {model_names}"
149                logger.error(error_msg)
150                raise OllamaError(error_msg)
151
152        except OllamaError as e:
153            logger.error(e)
154            raise
155
156        except requests.exceptions.RequestException as e:
157            logger.error(e)
158            raise
159
160    def chat_with_tools(self, messages, tools):
161        if not supports_function_calling(model=self._model_name):
162            raise FunctionCallingNotSupportedError(self._model_name)
163
164        return super().chat_with_tools(messages, tools)
165
166    @classmethod
167    def model_gateway(cls):
168        return ModelProvider.OLLAMA
169
170    def model_provider(self) -> ModelProvider:
171        """Returns the name of the provider"""
172        return self.model_gateway()

A large base class that wraps around a litellm model.

Note that the model object should be interacted with via the methods provided in the wrapper class:

Each individual API should implement the required abstract_methods in order to allow users to interact with a model of that type.

OllamaLLM( model_name: str, stream: ~_TStream = False, domain: Literal['default', 'auto', 'custom'] = 'default', custom_domain: str | None = None, temperature: float | None = None, top_p: float | None = None, max_tokens: int | None = None, frequency_penalty: float | None = None, presence_penalty: float | None = None, reasoning_effort: Optional[Literal['minimal', 'low', 'medium', 'high']] = None, service_tier: str | None = None, verbosity: Optional[Literal['low', 'medium', 'high']] = None, retry_approach: railtracks.llm.retries.RetryApproach | None = None, **kwargs)
 29    def __init__(
 30        self,
 31        model_name: str,
 32        stream: _TStream = False,
 33        domain: Literal["default", "auto", "custom"] = "default",
 34        custom_domain: str | None = None,
 35        temperature: float | None = None,
 36        top_p: float | None = None,
 37        max_tokens: int | None = None,
 38        frequency_penalty: float | None = None,
 39        presence_penalty: float | None = None,
 40        reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
 41        service_tier: str | None = None,
 42        verbosity: Literal["low", "medium", "high"] | None = None,
 43        retry_approach: RetryApproach | None = None,
 44        **kwargs,
 45    ):
 46        """Initialize an Ollama LLM instance.
 47
 48        Args:
 49            model_name (str): Name of the Ollama model to use.
 50            stream (bool): Whether to stream the response.
 51            domain (Literal["default", "auto", "custom"], optional): The domain configuration mode.
 52                - "default": Uses the default localhost domain (http://localhost:11434)
 53                - "auto": Uses the OLLAMA_HOST environment variable, raises OllamaError if not set
 54                - "custom": Uses the provided custom_domain parameter, raises OllamaError if not provided
 55                Defaults to "default".
 56            custom_domain (str | None, optional): Custom domain URL to use when domain is set to "custom".
 57                Must be provided if domain="custom". Defaults to None.
 58            temperature (float | None, optional): Sampling temperature for generation (e.g. 0.0–2.0).
 59                If None, the provider default is used.
 60            top_p (float | None, optional): Nucleus sampling threshold.
 61            max_tokens (int | None, optional): Maximum tokens to generate.
 62            frequency_penalty (float | None, optional): Penalizes tokens by how often
 63                they've already appeared.
 64            presence_penalty (float | None, optional): Penalizes tokens that have
 65                already appeared at all.
 66            reasoning_effort (Literal["minimal", "low", "medium", "high"] | None, optional):
 67                Requested reasoning effort for reasoning-capable models.
 68            service_tier (str | None, optional): Requested service tier. Provider-specific.
 69            verbosity (Literal["low", "medium", "high"] | None, optional): Requested
 70                output verbosity for models that support it.
 71            retry_approach (RetryApproach | None, optional): Retry strategy for transient
 72                failures.
 73            **kwargs: Additional arguments passed to the parent LiteLLMWrapper.
 74
 75        Raises:
 76            OllamaError: If:
 77                - domain is "auto" and OLLAMA_HOST environment variable is not set
 78                - domain is "custom" and custom_domain is not provided
 79                - specified model is not available on the server
 80            RequestException: If connection to Ollama server fails
 81        """
 82        if stream:
 83            warn_pending_change(
 84                "Constructing a model with `stream=True`",
 85                change="is removed",
 86                instead="rt.astream(agent, ...) to stream an agent run",
 87                detail=(
 88                    "Streaming becomes async in 1.5.0: per-call model methods "
 89                    "(astream_chat, astream_chat_with_tools, astream_structured) "
 90                    "replace the streamed return value of chat()."
 91                ),
 92            )
 93
 94        if not model_name.startswith("ollama/"):
 95            logger.warning(
 96                f"Prepending 'ollama/' to model name '{model_name}' for Ollama"
 97            )
 98            model_name = f"ollama/{model_name}"
 99        super().__init__(
100            model_name=model_name,
101            stream=stream,
102            temperature=temperature,
103            top_p=top_p,
104            max_tokens=max_tokens,
105            frequency_penalty=frequency_penalty,
106            presence_penalty=presence_penalty,
107            reasoning_effort=reasoning_effort,
108            service_tier=service_tier,
109            verbosity=verbosity,
110            retry_approach=retry_approach,
111            **kwargs,
112        )
113
114        match domain:
115            case "default":
116                self.domain = DEFAULT_DOMAIN
117            case "auto":
118                domain_from_env = os.getenv("OLLAMA_HOST")
119                if domain_from_env is None:
120                    raise OllamaError("OLLAMA_HOST environment variable not set")
121                self.domain = domain_from_env
122            case "custom":
123                if custom_domain is None:
124                    raise OllamaError(
125                        "Custom domain must be provided when domain is set to 'custom'"
126                    )
127                self.domain = custom_domain
128
129        self._run_check(
130            "api/tags"
131        )  # This will crash the workflow if Ollama is not setup properly

Initialize an Ollama LLM instance.

Arguments:
  • model_name (str): Name of the Ollama model to use.
  • stream (bool): Whether to stream the response.
  • domain (Literal["default", "auto", "custom"], optional): The domain configuration mode.
    • "default": Uses the default localhost domain (http://localhost:11434)
    • "auto": Uses the OLLAMA_HOST environment variable, raises OllamaError if not set
    • "custom": Uses the provided custom_domain parameter, raises OllamaError if not provided Defaults to "default".
  • custom_domain (str | None, optional): Custom domain URL to use when domain is set to "custom". Must be provided if domain="custom". Defaults to None.
  • temperature (float | None, optional): Sampling temperature for generation (e.g. 0.0–2.0). If None, the provider default is used.
  • top_p (float | None, optional): Nucleus sampling threshold.
  • max_tokens (int | None, optional): Maximum tokens to generate.
  • frequency_penalty (float | None, optional): Penalizes tokens by how often they've already appeared.
  • presence_penalty (float | None, optional): Penalizes tokens that have already appeared at all.
  • reasoning_effort (Literal["minimal", "low", "medium", "high"] | None, optional): Requested reasoning effort for reasoning-capable models.
  • service_tier (str | None, optional): Requested service tier. Provider-specific.
  • verbosity (Literal["low", "medium", "high"] | None, optional): Requested output verbosity for models that support it.
  • retry_approach (RetryApproach | None, optional): Retry strategy for transient failures.
  • **kwargs: Additional arguments passed to the parent LiteLLMWrapper.
Raises:
  • OllamaError: If:
    • domain is "auto" and OLLAMA_HOST environment variable is not set
    • domain is "custom" and custom_domain is not provided
    • specified model is not available on the server
  • RequestException: If connection to Ollama server fails
def chat_with_tools(self, messages, tools):
160    def chat_with_tools(self, messages, tools):
161        if not supports_function_calling(model=self._model_name):
162            raise FunctionCallingNotSupportedError(self._model_name)
163
164        return super().chat_with_tools(messages, tools)

Chat with the model using the provided messages and tools.

@classmethod
def model_gateway(cls):
166    @classmethod
167    def model_gateway(cls):
168        return ModelProvider.OLLAMA

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

def model_provider(self) -> ModelProvider:
170    def model_provider(self) -> ModelProvider:
171        """Returns the name of the provider"""
172        return self.model_gateway()

Returns the name of the provider

14class PortKeyLLM(OpenAICompatibleProvider[_TStream]):
15    def __init__(
16        self,
17        model_name: str,
18        *,
19        stream: _TStream = False,
20        api_key: str | None = None,
21        temperature: float | None = None,
22        top_p: float | None = None,
23        max_tokens: int | None = None,
24        frequency_penalty: float | None = None,
25        presence_penalty: float | None = None,
26        reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
27        service_tier: str | None = None,
28        verbosity: Literal["low", "medium", "high"] | None = None,
29        retry_approach: RetryApproach | None = None,
30        **kwargs: Any,
31    ):
32        try:
33            from portkey_ai import Portkey
34        except ImportError:
35            raise ImportError(
36                "Could not import portkey_ai package. Use railtracks[portkey]"
37            )
38
39        if api_key is None:
40            try:
41                api_key = os.environ["PORTKEY_API_KEY"]
42            except KeyError:
43                raise KeyError("Please set your PORTKEY_API_KEY in your .env file.")
44
45        portkey = Portkey(api_key=api_key)
46
47        super().__init__(
48            model_name,
49            stream=stream,
50            api_base=portkey.base_url,
51            api_key=portkey.api_key,
52            temperature=temperature,
53            top_p=top_p,
54            max_tokens=max_tokens,
55            frequency_penalty=frequency_penalty,
56            presence_penalty=presence_penalty,
57            reasoning_effort=reasoning_effort,
58            service_tier=service_tier,
59            verbosity=verbosity,
60            retry_approach=retry_approach,
61            **kwargs,
62        )
63
64    @classmethod
65    def model_gateway(cls):
66        return ModelProvider.PORTKEY
67
68    def model_provider(self):
69        # TODO: Implement specialized logic to determine the model provider
70        return ModelProvider.PORTKEY

A large base class that wraps around a litellm model.

Note that the model object should be interacted with via the methods provided in the wrapper class:

Each individual API should implement the required abstract_methods in order to allow users to interact with a model of that type.

PortKeyLLM( model_name: str, *, stream: ~_TStream = False, api_key: str | None = None, temperature: float | None = None, top_p: float | None = None, max_tokens: int | None = None, frequency_penalty: float | None = None, presence_penalty: float | None = None, reasoning_effort: Optional[Literal['minimal', 'low', 'medium', 'high']] = None, service_tier: str | None = None, verbosity: Optional[Literal['low', 'medium', 'high']] = None, retry_approach: railtracks.llm.retries.RetryApproach | None = None, **kwargs: Any)
15    def __init__(
16        self,
17        model_name: str,
18        *,
19        stream: _TStream = False,
20        api_key: str | None = None,
21        temperature: float | None = None,
22        top_p: float | None = None,
23        max_tokens: int | None = None,
24        frequency_penalty: float | None = None,
25        presence_penalty: float | None = None,
26        reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
27        service_tier: str | None = None,
28        verbosity: Literal["low", "medium", "high"] | None = None,
29        retry_approach: RetryApproach | None = None,
30        **kwargs: Any,
31    ):
32        try:
33            from portkey_ai import Portkey
34        except ImportError:
35            raise ImportError(
36                "Could not import portkey_ai package. Use railtracks[portkey]"
37            )
38
39        if api_key is None:
40            try:
41                api_key = os.environ["PORTKEY_API_KEY"]
42            except KeyError:
43                raise KeyError("Please set your PORTKEY_API_KEY in your .env file.")
44
45        portkey = Portkey(api_key=api_key)
46
47        super().__init__(
48            model_name,
49            stream=stream,
50            api_base=portkey.base_url,
51            api_key=portkey.api_key,
52            temperature=temperature,
53            top_p=top_p,
54            max_tokens=max_tokens,
55            frequency_penalty=frequency_penalty,
56            presence_penalty=presence_penalty,
57            reasoning_effort=reasoning_effort,
58            service_tier=service_tier,
59            verbosity=verbosity,
60            retry_approach=retry_approach,
61            **kwargs,
62        )

Initialize an OpenAI-compatible gateway LLM instance (e.g. via PortKey).

See ProviderLLMWrapper.__init__ for the full per-hyperparameter description of the common hyperparameters below (top_p, max_tokens, frequency_penalty, presence_penalty, reasoning_effort, service_tier, verbosity).

Note:

Gateway-style providers can't be reliably introspected by litellm, so neither per-model hyperparameter support nor mutual-exclusion checks run here (see _validate_common_hyperparameter_support override below) — every hyperparameter, valid or not, is passed straight through and any error surfaces from the gateway or upstream provider directly.

@classmethod
def model_gateway(cls):
64    @classmethod
65    def model_gateway(cls):
66        return ModelProvider.PORTKEY

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

def model_provider(self):
68    def model_provider(self):
69        # TODO: Implement specialized logic to determine the model provider
70        return ModelProvider.PORTKEY

Returns the name of the provider

class OpenAICompatibleProvider(railtracks.llm.models.api_providers._provider_wrapper.ProviderLLMWrapper[~_TStream], abc.ABC):
12class OpenAICompatibleProvider(ProviderLLMWrapper[_TStream], ABC):
13    def __init__(
14        self,
15        model_name: str,
16        *,
17        stream: _TStream = False,
18        api_base: str,
19        api_key: str,
20        temperature: float | None = None,
21        top_p: float | None = None,
22        max_tokens: int | None = None,
23        frequency_penalty: float | None = None,
24        presence_penalty: float | None = None,
25        reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
26        service_tier: str | None = None,
27        verbosity: Literal["low", "medium", "high"] | None = None,
28        retry_approach: RetryApproach | None = None,
29        **kwargs: Any,
30    ):
31        """Initialize an OpenAI-compatible gateway LLM instance (e.g. via PortKey).
32
33        See `ProviderLLMWrapper.__init__` for the full per-hyperparameter description
34        of the common hyperparameters below (`top_p`, `max_tokens`, `frequency_penalty`,
35        `presence_penalty`, `reasoning_effort`, `service_tier`, `verbosity`).
36
37        Note:
38            Gateway-style providers can't be reliably introspected by litellm, so
39            neither per-model hyperparameter support nor mutual-exclusion checks run
40            here (see `_validate_common_hyperparameter_support` override below) —
41            every hyperparameter, valid or not, is passed straight through and any
42            error surfaces from the gateway or upstream provider directly.
43        """
44        super().__init__(
45            model_name,
46            stream=stream,
47            api_base=api_base,
48            api_key=api_key,
49            temperature=temperature,
50            top_p=top_p,
51            max_tokens=max_tokens,
52            frequency_penalty=frequency_penalty,
53            presence_penalty=presence_penalty,
54            reasoning_effort=reasoning_effort,
55            service_tier=service_tier,
56            verbosity=verbosity,
57            retry_approach=retry_approach,
58            **kwargs,
59        )
60
61    def full_model_name(self, model_name: str) -> str:
62        return f"openai/{model_name}"
63
64    @classmethod
65    def model_gateway(cls) -> ModelProvider:
66        return ModelProvider.UNKNOWN
67
68    def _pre_init_provider_check(self, model_name: str):
69        # For OpenAI compatible providers, we skip the provider check since there is no way to do it.
70        return model_name
71
72    def _validate_tool_calling_support(self):
73        # For OpenAI compatible providers, we skip the tool calling support check since there is no way to do it.
74        return
75
76    def _validate_common_hyperparameter_support(self) -> None:
77        # For OpenAI compatible providers, litellm can't reliably introspect
78        # gateway-style providers, so we skip the common hyperparameter support check.
79        return

A large base class that wraps around a litellm model.

Note that the model object should be interacted with via the methods provided in the wrapper class:

Each individual API should implement the required abstract_methods in order to allow users to interact with a model of that type.

OpenAICompatibleProvider( model_name: str, *, stream: ~_TStream = False, api_base: str, api_key: str, temperature: float | None = None, top_p: float | None = None, max_tokens: int | None = None, frequency_penalty: float | None = None, presence_penalty: float | None = None, reasoning_effort: Optional[Literal['minimal', 'low', 'medium', 'high']] = None, service_tier: str | None = None, verbosity: Optional[Literal['low', 'medium', 'high']] = None, retry_approach: railtracks.llm.retries.RetryApproach | None = None, **kwargs: Any)
13    def __init__(
14        self,
15        model_name: str,
16        *,
17        stream: _TStream = False,
18        api_base: str,
19        api_key: str,
20        temperature: float | None = None,
21        top_p: float | None = None,
22        max_tokens: int | None = None,
23        frequency_penalty: float | None = None,
24        presence_penalty: float | None = None,
25        reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
26        service_tier: str | None = None,
27        verbosity: Literal["low", "medium", "high"] | None = None,
28        retry_approach: RetryApproach | None = None,
29        **kwargs: Any,
30    ):
31        """Initialize an OpenAI-compatible gateway LLM instance (e.g. via PortKey).
32
33        See `ProviderLLMWrapper.__init__` for the full per-hyperparameter description
34        of the common hyperparameters below (`top_p`, `max_tokens`, `frequency_penalty`,
35        `presence_penalty`, `reasoning_effort`, `service_tier`, `verbosity`).
36
37        Note:
38            Gateway-style providers can't be reliably introspected by litellm, so
39            neither per-model hyperparameter support nor mutual-exclusion checks run
40            here (see `_validate_common_hyperparameter_support` override below) —
41            every hyperparameter, valid or not, is passed straight through and any
42            error surfaces from the gateway or upstream provider directly.
43        """
44        super().__init__(
45            model_name,
46            stream=stream,
47            api_base=api_base,
48            api_key=api_key,
49            temperature=temperature,
50            top_p=top_p,
51            max_tokens=max_tokens,
52            frequency_penalty=frequency_penalty,
53            presence_penalty=presence_penalty,
54            reasoning_effort=reasoning_effort,
55            service_tier=service_tier,
56            verbosity=verbosity,
57            retry_approach=retry_approach,
58            **kwargs,
59        )

Initialize an OpenAI-compatible gateway LLM instance (e.g. via PortKey).

See ProviderLLMWrapper.__init__ for the full per-hyperparameter description of the common hyperparameters below (top_p, max_tokens, frequency_penalty, presence_penalty, reasoning_effort, service_tier, verbosity).

Note:

Gateway-style providers can't be reliably introspected by litellm, so neither per-model hyperparameter support nor mutual-exclusion checks run here (see _validate_common_hyperparameter_support override below) — every hyperparameter, valid or not, is passed straight through and any error surfaces from the gateway or upstream provider directly.

def full_model_name(self, model_name: str) -> str:
61    def full_model_name(self, model_name: str) -> str:
62        return f"openai/{model_name}"

After the provider is checked, this method is called to get the full model name

@classmethod
def model_gateway(cls) -> ModelProvider:
64    @classmethod
65    def model_gateway(cls) -> ModelProvider:
66        return ModelProvider.UNKNOWN

Gets the API distrubutor of the model. Note nessecarily the same as the model itself.

E.g. if you are calling openai LLM through Azure AI foundry

class Parameter(abc.ABC):
 53class Parameter(ABC):
 54    """
 55    Abstract Base Parameter class with default simple parameter behavior.
 56    """
 57
 58    param_type: Optional[Union[str, List[str]]] = None  # class var for default type
 59
 60    def __init__(
 61        self,
 62        name: str,
 63        description: Optional[str] = None,
 64        required: bool = True,
 65        default: Any = None,
 66        enum: Optional[List[Any]] = None,
 67        default_present: bool = False,
 68        param_type: ParameterTypeInput = None,
 69    ):
 70        """
 71        Initialize a Parameter instance.
 72
 73        Args:
 74            name (str): Name of the parameter.
 75            description (Optional[str]): Description of the parameter.
 76            required (bool): Whether the parameter is required.
 77            default (Any): Default value for the parameter.
 78            enum (Optional[List[Any]]): Allowed values for the parameter.
 79            default_present (bool): Whether a default value is explicitly set.
 80            param_type: JSON schema type string (e.g. ``\"string\"``), :class:`ParameterType`,
 81                a Python builtin type (e.g. ``str`` → ``\"string\"``), or a list for unions.
 82        """
 83        self.name = name
 84        self.description = description or ""
 85        self.required = required
 86        self.default = default
 87        self.enum = enum
 88        self.default_present = default_present
 89        if param_type is not None:
 90            if isinstance(param_type, list):
 91                self.param_type = [
 92                    _normalize_param_type_scalar(pt) for pt in param_type
 93                ]
 94            else:
 95                self.param_type = _normalize_param_type_scalar(param_type)
 96        elif hasattr(self, "param_type") and self.param_type is None:
 97            self.param_type = None
 98
 99    def to_json_schema(self) -> Dict[str, Any]:
100        # Base dictionary with type and optional description
101        schema_dict: Dict[str, Any] = {
102            "type": self.param_type.value
103            if isinstance(self.param_type, ParameterType)
104            else self.param_type
105        }
106        if self.description:
107            schema_dict["description"] = self.description
108
109        # Handle enum
110        if self.enum:
111            schema_dict["enum"] = self.enum
112
113        # Handle default
114        # default can be None, 0, False; None means optional parameter
115        if self.default_present:
116            schema_dict["default"] = self.default
117        elif isinstance(self.param_type, list) and "none" in self.param_type:
118            schema_dict["default"] = None
119
120        return schema_dict
121
122    def __repr__(self) -> str:
123        return (
124            f"Parameter(name={self.name!r}, param_type={self.param_type!r}, "
125            f"description={self.description!r}, required={self.required!r}, "
126            f"default={self.default!r}, enum={self.enum!r})"
127        )

Abstract Base Parameter class with default simple parameter behavior.

Parameter( name: str, description: Optional[str] = None, required: bool = True, default: Any = None, enum: Optional[List[Any]] = None, default_present: bool = False, param_type: Union[str, railtracks.llm.tools.parameters._base.ParameterType, type, List[Union[str, railtracks.llm.tools.parameters._base.ParameterType, type]], NoneType] = None)
60    def __init__(
61        self,
62        name: str,
63        description: Optional[str] = None,
64        required: bool = True,
65        default: Any = None,
66        enum: Optional[List[Any]] = None,
67        default_present: bool = False,
68        param_type: ParameterTypeInput = None,
69    ):
70        """
71        Initialize a Parameter instance.
72
73        Args:
74            name (str): Name of the parameter.
75            description (Optional[str]): Description of the parameter.
76            required (bool): Whether the parameter is required.
77            default (Any): Default value for the parameter.
78            enum (Optional[List[Any]]): Allowed values for the parameter.
79            default_present (bool): Whether a default value is explicitly set.
80            param_type: JSON schema type string (e.g. ``\"string\"``), :class:`ParameterType`,
81                a Python builtin type (e.g. ``str`` → ``\"string\"``), or a list for unions.
82        """
83        self.name = name
84        self.description = description or ""
85        self.required = required
86        self.default = default
87        self.enum = enum
88        self.default_present = default_present
89        if param_type is not None:
90            if isinstance(param_type, list):
91                self.param_type = [
92                    _normalize_param_type_scalar(pt) for pt in param_type
93                ]
94            else:
95                self.param_type = _normalize_param_type_scalar(param_type)
96        elif hasattr(self, "param_type") and self.param_type is None:
97            self.param_type = None

Initialize a Parameter instance.

Arguments:
  • name (str): Name of the parameter.
  • description (Optional[str]): Description of the parameter.
  • required (bool): Whether the parameter is required.
  • default (Any): Default value for the parameter.
  • enum (Optional[List[Any]]): Allowed values for the parameter.
  • default_present (bool): Whether a default value is explicitly set.
  • param_type: JSON schema type string (e.g. "string"), ParameterType, a Python builtin type (e.g. str → "string"), or a list for unions.
param_type: Union[str, List[str], NoneType] = None
name
description
required
default
enum
default_present
def to_json_schema(self) -> Dict[str, Any]:
 99    def to_json_schema(self) -> Dict[str, Any]:
100        # Base dictionary with type and optional description
101        schema_dict: Dict[str, Any] = {
102            "type": self.param_type.value
103            if isinstance(self.param_type, ParameterType)
104            else self.param_type
105        }
106        if self.description:
107            schema_dict["description"] = self.description
108
109        # Handle enum
110        if self.enum:
111            schema_dict["enum"] = self.enum
112
113        # Handle default
114        # default can be None, 0, False; None means optional parameter
115        if self.default_present:
116            schema_dict["default"] = self.default
117        elif isinstance(self.param_type, list) and "none" in self.param_type:
118            schema_dict["default"] = None
119
120        return schema_dict
class UnionParameter(railtracks.llm.Parameter):
 9class UnionParameter(Parameter):
10    """Parameter representing a union type."""
11
12    param_type: List[str]
13
14    def __init__(
15        self,
16        name: str,
17        options: List[Parameter],
18        description: Optional[str] = None,
19        required: bool = True,
20        default: Any = None,
21        enum: Optional[list] = None,
22        default_present: bool = False,
23    ):
24        """Initialize a UnionParameter instance.
25
26        Args:
27            name (str): Name of the parameter.
28            options (List[Parameter]): List of Parameter instances representing the union types.
29            description (Optional[str]): Description of the parameter.
30            required (bool): Whether the parameter is required.
31            default (Any): Default value for the parameter.
32            enum (Optional[list]): Allowed values for the parameter.
33            default_present (bool): Whether a default value is explicitly set.
34        """
35        super().__init__(name, description, required, default, enum, default_present)
36        self.options = options
37        for opt in options:
38            if isinstance(opt, UnionParameter):
39                raise TypeError(
40                    "UnionParameter cannot contain another UnionParameter in its options"
41                )
42
43        # param_type here is the list of inner types as strings, e.g. ["string", "null"]
44        # flatten and deduplicate types (order does not matter for schema)
45        flattened_types = []
46        for opt in options:
47            pt = opt.param_type
48            if hasattr(pt, "value"):
49                pt = pt.__getattribute__("value")
50            if isinstance(pt, list):
51                flattened_types.extend(p for p in pt if p is not None)
52            elif pt is not None:
53                flattened_types.append(pt)
54
55        # Deduplicate while preserving order
56        self.param_type = list(set(flattened_types))
57
58    def to_json_schema(self) -> Dict[str, Any]:
59        """Convert the union parameter to a JSON schema representation."""
60        schema = {
61            "anyOf": [opt.to_json_schema() for opt in self.options],
62        }
63
64        if self.description:
65            schema["description"] = self.description  # type: ignore
66
67        if self.default_present:
68            schema["default"] = self.default
69
70        return schema
71
72    def __repr__(self) -> str:
73        """Return a string representation of the UnionParameter."""
74        return (
75            f"UnionParameter(name={self.name!r}, options={self.options!r}, "
76            f"description={self.description!r}, required={self.required!r}, default={self.default!r})"
77        )

Parameter representing a union type.

UnionParameter( name: str, options: List[Parameter], description: Optional[str] = None, required: bool = True, default: Any = None, enum: Optional[list] = None, default_present: bool = False)
14    def __init__(
15        self,
16        name: str,
17        options: List[Parameter],
18        description: Optional[str] = None,
19        required: bool = True,
20        default: Any = None,
21        enum: Optional[list] = None,
22        default_present: bool = False,
23    ):
24        """Initialize a UnionParameter instance.
25
26        Args:
27            name (str): Name of the parameter.
28            options (List[Parameter]): List of Parameter instances representing the union types.
29            description (Optional[str]): Description of the parameter.
30            required (bool): Whether the parameter is required.
31            default (Any): Default value for the parameter.
32            enum (Optional[list]): Allowed values for the parameter.
33            default_present (bool): Whether a default value is explicitly set.
34        """
35        super().__init__(name, description, required, default, enum, default_present)
36        self.options = options
37        for opt in options:
38            if isinstance(opt, UnionParameter):
39                raise TypeError(
40                    "UnionParameter cannot contain another UnionParameter in its options"
41                )
42
43        # param_type here is the list of inner types as strings, e.g. ["string", "null"]
44        # flatten and deduplicate types (order does not matter for schema)
45        flattened_types = []
46        for opt in options:
47            pt = opt.param_type
48            if hasattr(pt, "value"):
49                pt = pt.__getattribute__("value")
50            if isinstance(pt, list):
51                flattened_types.extend(p for p in pt if p is not None)
52            elif pt is not None:
53                flattened_types.append(pt)
54
55        # Deduplicate while preserving order
56        self.param_type = list(set(flattened_types))

Initialize a UnionParameter instance.

Arguments:
  • name (str): Name of the parameter.
  • options (List[Parameter]): List of Parameter instances representing the union types.
  • description (Optional[str]): Description of the parameter.
  • required (bool): Whether the parameter is required.
  • default (Any): Default value for the parameter.
  • enum (Optional[list]): Allowed values for the parameter.
  • default_present (bool): Whether a default value is explicitly set.
param_type: List[str] = None
options
def to_json_schema(self) -> Dict[str, Any]:
58    def to_json_schema(self) -> Dict[str, Any]:
59        """Convert the union parameter to a JSON schema representation."""
60        schema = {
61            "anyOf": [opt.to_json_schema() for opt in self.options],
62        }
63
64        if self.description:
65            schema["description"] = self.description  # type: ignore
66
67        if self.default_present:
68            schema["default"] = self.default
69
70        return schema

Convert the union parameter to a JSON schema representation.

class ArrayParameter(railtracks.llm.Parameter):
 9class ArrayParameter(Parameter):
10    """Parameter representing an array type."""
11
12    param_type: ParameterType = ParameterType.ARRAY
13
14    def __init__(
15        self,
16        name: str,
17        items: Parameter,
18        description: Optional[str] = None,
19        required: bool = True,
20        default: Any = None,
21        max_items: Optional[int] = None,
22        additional_properties: bool = False,
23    ):
24        """Initialize an ArrayParameter instance.
25
26        Args:
27            name (str): Name of the parameter.
28            items (Parameter): Parameter instance describing the type of array elements.
29            description (Optional[str]): Description of the parameter.
30            required (bool): Whether the parameter is required.
31            default (Any): Default value for the parameter.
32            max_items (Optional[int]): Maximum number of items allowed in the array.
33            additional_properties (bool): Whether additional properties are allowed (relevant if items are objects).
34        """
35        super().__init__(name, description, required, default)
36        self.items = items
37        self.max_items = max_items
38        self.additional_properties = (
39            additional_properties  # might be relevant if items is object type
40        )
41
42    def to_json_schema(self) -> Dict[str, Any]:
43        """Convert the array parameter to a JSON schema representation."""
44        # Base property for items inside the array
45        items_schema = self.items.to_json_schema()
46
47        schema = {
48            "type": "array",
49            "items": items_schema,
50        }
51        if self.description:
52            schema["description"] = self.description
53
54        if self.max_items is not None:
55            schema["maxItems"] = self.max_items
56
57        # Set defaults and enum if present at the array level
58        if self.default is not None:
59            schema["default"] = self.default
60
61        # Note: enum on arrays is uncommon but if you want to support:
62        if self.enum:
63            schema["enum"] = self.enum
64
65        return schema
66
67    def __repr__(self) -> str:
68        """Return a string representation of the ArrayParameter."""
69        return (
70            f"ArrayParameter(name={self.name!r}, items={self.items!r}, "
71            f"description={self.description!r}, required={self.required!r}, "
72            f"default={self.default!r}, max_items={self.max_items!r}, "
73            f"additional_properties={self.additional_properties!r})"
74        )

Parameter representing an array type.

ArrayParameter( name: str, items: Parameter, description: Optional[str] = None, required: bool = True, default: Any = None, max_items: Optional[int] = None, additional_properties: bool = False)
14    def __init__(
15        self,
16        name: str,
17        items: Parameter,
18        description: Optional[str] = None,
19        required: bool = True,
20        default: Any = None,
21        max_items: Optional[int] = None,
22        additional_properties: bool = False,
23    ):
24        """Initialize an ArrayParameter instance.
25
26        Args:
27            name (str): Name of the parameter.
28            items (Parameter): Parameter instance describing the type of array elements.
29            description (Optional[str]): Description of the parameter.
30            required (bool): Whether the parameter is required.
31            default (Any): Default value for the parameter.
32            max_items (Optional[int]): Maximum number of items allowed in the array.
33            additional_properties (bool): Whether additional properties are allowed (relevant if items are objects).
34        """
35        super().__init__(name, description, required, default)
36        self.items = items
37        self.max_items = max_items
38        self.additional_properties = (
39            additional_properties  # might be relevant if items is object type
40        )

Initialize an ArrayParameter instance.

Arguments:
  • name (str): Name of the parameter.
  • items (Parameter): Parameter instance describing the type of array elements.
  • description (Optional[str]): Description of the parameter.
  • required (bool): Whether the parameter is required.
  • default (Any): Default value for the parameter.
  • max_items (Optional[int]): Maximum number of items allowed in the array.
  • additional_properties (bool): Whether additional properties are allowed (relevant if items are objects).
param_type: railtracks.llm.tools.parameters._base.ParameterType = <ParameterType.ARRAY: 'array'>
items
max_items
additional_properties
def to_json_schema(self) -> Dict[str, Any]:
42    def to_json_schema(self) -> Dict[str, Any]:
43        """Convert the array parameter to a JSON schema representation."""
44        # Base property for items inside the array
45        items_schema = self.items.to_json_schema()
46
47        schema = {
48            "type": "array",
49            "items": items_schema,
50        }
51        if self.description:
52            schema["description"] = self.description
53
54        if self.max_items is not None:
55            schema["maxItems"] = self.max_items
56
57        # Set defaults and enum if present at the array level
58        if self.default is not None:
59            schema["default"] = self.default
60
61        # Note: enum on arrays is uncommon but if you want to support:
62        if self.enum:
63            schema["enum"] = self.enum
64
65        return schema

Convert the array parameter to a JSON schema representation.

class ObjectParameter(railtracks.llm.Parameter):
 9class ObjectParameter(Parameter):
10    """Parameter representing an object type."""
11
12    param_type: ParameterType = ParameterType.OBJECT
13
14    def __init__(
15        self,
16        name: str,
17        properties: list[Parameter],
18        description: Optional[str] = None,
19        required: bool = True,
20        additional_properties: bool = False,
21        default: Any = None,
22    ):
23        """Initialize an ObjectParameter instance.
24
25        Args:
26            name (str): Name of the parameter.
27            properties (list[Parameter]): List of Parameter instances describing object properties.
28            description (Optional[str]): Description of the parameter.
29            required (bool): Whether the parameter is required.
30            additional_properties (bool): Whether additional properties are allowed.
31            default (Any): Default value for the parameter.
32        """
33        super().__init__(name, description, required, default)
34        self.properties = properties
35        self.additional_properties = additional_properties
36
37    def to_json_schema(self) -> Dict[str, Any]:
38        """Convert the object parameter to a JSON schema representation."""
39        schema = {
40            "type": "object",
41            "properties": {},
42            "additionalProperties": self.additional_properties,
43        }
44
45        if self.description:
46            schema["description"] = self.description
47
48        required_props = []
49        for prop in self.properties:
50            schema["properties"][prop.name] = prop.to_json_schema()
51            if prop.required:
52                required_props.append(prop.name)
53
54        if required_props:
55            schema["required"] = required_props
56
57        if self.default is not None:
58            schema["default"] = self.default
59
60        if self.enum:
61            schema["enum"] = self.enum
62
63        return schema
64
65    def __repr__(self) -> str:
66        """Return a string representation of the ObjectParameter."""
67        return (
68            f"ObjectParameter(name={self.name!r}, properties={self.properties!r}, "
69            f"description={self.description!r}, required={self.required!r}, "
70            f"additional_properties={self.additional_properties!r}, default={self.default!r})"
71        )

Parameter representing an object type.

ObjectParameter( name: str, properties: list[Parameter], description: Optional[str] = None, required: bool = True, additional_properties: bool = False, default: Any = None)
14    def __init__(
15        self,
16        name: str,
17        properties: list[Parameter],
18        description: Optional[str] = None,
19        required: bool = True,
20        additional_properties: bool = False,
21        default: Any = None,
22    ):
23        """Initialize an ObjectParameter instance.
24
25        Args:
26            name (str): Name of the parameter.
27            properties (list[Parameter]): List of Parameter instances describing object properties.
28            description (Optional[str]): Description of the parameter.
29            required (bool): Whether the parameter is required.
30            additional_properties (bool): Whether additional properties are allowed.
31            default (Any): Default value for the parameter.
32        """
33        super().__init__(name, description, required, default)
34        self.properties = properties
35        self.additional_properties = additional_properties

Initialize an ObjectParameter instance.

Arguments:
  • name (str): Name of the parameter.
  • properties (list[Parameter]): List of Parameter instances describing object properties.
  • description (Optional[str]): Description of the parameter.
  • required (bool): Whether the parameter is required.
  • additional_properties (bool): Whether additional properties are allowed.
  • default (Any): Default value for the parameter.
param_type: railtracks.llm.tools.parameters._base.ParameterType = <ParameterType.OBJECT: 'object'>
properties
additional_properties
def to_json_schema(self) -> Dict[str, Any]:
37    def to_json_schema(self) -> Dict[str, Any]:
38        """Convert the object parameter to a JSON schema representation."""
39        schema = {
40            "type": "object",
41            "properties": {},
42            "additionalProperties": self.additional_properties,
43        }
44
45        if self.description:
46            schema["description"] = self.description
47
48        required_props = []
49        for prop in self.properties:
50            schema["properties"][prop.name] = prop.to_json_schema()
51            if prop.required:
52                required_props.append(prop.name)
53
54        if required_props:
55            schema["required"] = required_props
56
57        if self.default is not None:
58            schema["default"] = self.default
59
60        if self.enum:
61            schema["enum"] = self.enum
62
63        return schema

Convert the object parameter to a JSON schema representation.

class RefParameter(railtracks.llm.Parameter):
 9class RefParameter(Parameter):
10    """Parameter representing a reference type."""
11
12    param_type: str = "object"  # referenced schemas are always 'object' type
13
14    def __init__(
15        self,
16        name: str,
17        ref_path: str,
18        description: Optional[str] = None,
19        required: bool = True,
20        default: Any = None,
21    ):
22        """Initialize a RefParameter instance.
23
24        Args:
25            name (str): Name of the parameter.
26            ref_path (str): Reference path to the schema definition.
27            description (Optional[str]): Description of the parameter.
28            required (bool): Whether the parameter is required.
29            default (Any): Default value for the parameter.
30        """
31        super().__init__(name, description, required, default)
32        self.ref_path = ref_path
33
34    def to_json_schema(self) -> Dict[str, Any]:
35        """Convert the reference parameter to a JSON schema representation."""
36        schema = {"$ref": self.ref_path}
37        if self.description:
38            schema["description"] = self.description
39
40        if self.default is not None:
41            schema["default"] = self.default
42
43        if self.enum:
44            schema["enum"] = self.enum
45
46        return schema
47
48    def __repr__(self) -> str:
49        """Return a string representation of the RefParameter."""
50        return (
51            f"RefParameter(name={self.name!r}, ref_path={self.ref_path!r}, "
52            f"description={self.description!r}, required={self.required!r}, default={self.default!r})"
53        )

Parameter representing a reference type.

RefParameter( name: str, ref_path: str, description: Optional[str] = None, required: bool = True, default: Any = None)
14    def __init__(
15        self,
16        name: str,
17        ref_path: str,
18        description: Optional[str] = None,
19        required: bool = True,
20        default: Any = None,
21    ):
22        """Initialize a RefParameter instance.
23
24        Args:
25            name (str): Name of the parameter.
26            ref_path (str): Reference path to the schema definition.
27            description (Optional[str]): Description of the parameter.
28            required (bool): Whether the parameter is required.
29            default (Any): Default value for the parameter.
30        """
31        super().__init__(name, description, required, default)
32        self.ref_path = ref_path

Initialize a RefParameter instance.

Arguments:
  • name (str): Name of the parameter.
  • ref_path (str): Reference path to the schema definition.
  • description (Optional[str]): Description of the parameter.
  • required (bool): Whether the parameter is required.
  • default (Any): Default value for the parameter.
param_type: str = 'object'
ref_path
def to_json_schema(self) -> Dict[str, Any]:
34    def to_json_schema(self) -> Dict[str, Any]:
35        """Convert the reference parameter to a JSON schema representation."""
36        schema = {"$ref": self.ref_path}
37        if self.description:
38            schema["description"] = self.description
39
40        if self.default is not None:
41            schema["default"] = self.default
42
43        if self.enum:
44            schema["enum"] = self.enum
45
46        return schema

Convert the reference parameter to a JSON schema representation.