mirascope 2.1.0__py3-none-any.whl → 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mirascope/api/_generated/functions/client.py +10 -0
- mirascope/api/_generated/functions/raw_client.py +8 -0
- mirascope/api/_generated/functions/types/functions_create_response.py +25 -8
- mirascope/api/_generated/functions/types/functions_find_by_hash_response.py +25 -10
- mirascope/api/_generated/functions/types/functions_get_by_env_response.py +1 -0
- mirascope/api/_generated/functions/types/functions_get_response.py +25 -8
- mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +1 -0
- mirascope/api/_generated/functions/types/functions_list_response_functions_item.py +22 -7
- mirascope/api/_generated/reference.md +9 -0
- mirascope/llm/__init__.py +42 -0
- mirascope/llm/calls/calls.py +38 -11
- mirascope/llm/exceptions.py +69 -0
- mirascope/llm/prompts/prompts.py +47 -9
- mirascope/llm/providers/__init__.py +3 -0
- mirascope/llm/providers/openai/completions/_utils/__init__.py +3 -0
- mirascope/llm/providers/openai/completions/_utils/encode.py +27 -32
- mirascope/llm/providers/openai/completions/_utils/feature_info.py +50 -0
- mirascope/llm/providers/openai/completions/base_provider.py +21 -0
- mirascope/llm/providers/openai/completions/provider.py +8 -2
- mirascope/llm/providers/openrouter/__init__.py +5 -0
- mirascope/llm/providers/openrouter/provider.py +67 -0
- mirascope/llm/providers/provider_id.py +2 -0
- mirascope/llm/providers/provider_registry.py +6 -0
- mirascope/llm/responses/response.py +217 -0
- mirascope/llm/responses/stream_response.py +234 -0
- mirascope/llm/retries/__init__.py +51 -0
- mirascope/llm/retries/retry_calls.py +159 -0
- mirascope/llm/retries/retry_config.py +168 -0
- mirascope/llm/retries/retry_decorator.py +258 -0
- mirascope/llm/retries/retry_models.py +1313 -0
- mirascope/llm/retries/retry_prompts.py +227 -0
- mirascope/llm/retries/retry_responses.py +340 -0
- mirascope/llm/retries/retry_stream_responses.py +571 -0
- mirascope/llm/retries/utils.py +159 -0
- mirascope/ops/_internal/versioned_calls.py +249 -9
- mirascope/ops/_internal/versioned_functions.py +2 -0
- {mirascope-2.1.0.dist-info → mirascope-2.2.0.dist-info}/METADATA +1 -1
- {mirascope-2.1.0.dist-info → mirascope-2.2.0.dist-info}/RECORD +40 -28
- {mirascope-2.1.0.dist-info → mirascope-2.2.0.dist-info}/WHEEL +0 -0
- {mirascope-2.1.0.dist-info → mirascope-2.2.0.dist-info}/licenses/LICENSE +0 -0
mirascope/llm/prompts/prompts.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Concrete Prompt classes for generating messages with tools and formatting."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable, Sequence
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
4
|
from typing import Any, Generic, TypeVar, overload
|
|
6
5
|
|
|
7
6
|
from ..._utils import copy_function_metadata
|
|
@@ -32,22 +31,20 @@ from .protocols import (
|
|
|
32
31
|
FunctionT = TypeVar("FunctionT", bound=Callable[..., Any])
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
@dataclass(kw_only=True)
|
|
36
34
|
class BasePrompt(Generic[FunctionT]):
|
|
37
35
|
"""Base class for all Prompt types with shared metadata functionality."""
|
|
38
36
|
|
|
39
37
|
fn: FunctionT
|
|
40
38
|
"""The underlying prompt function that generates message content."""
|
|
41
39
|
|
|
42
|
-
__name__: str =
|
|
40
|
+
__name__: str = ""
|
|
43
41
|
"""The name of the underlying function (preserved for decorator stacking)."""
|
|
44
42
|
|
|
45
|
-
def
|
|
46
|
-
|
|
43
|
+
def __init__(self, *, fn: FunctionT) -> None:
|
|
44
|
+
self.fn = fn
|
|
47
45
|
copy_function_metadata(self, self.fn)
|
|
48
46
|
|
|
49
47
|
|
|
50
|
-
@dataclass
|
|
51
48
|
class Prompt(BasePrompt[MessageTemplate[P]], Generic[P, FormattableT]):
|
|
52
49
|
"""A prompt that can be called with a model to generate a response.
|
|
53
50
|
|
|
@@ -64,6 +61,17 @@ class Prompt(BasePrompt[MessageTemplate[P]], Generic[P, FormattableT]):
|
|
|
64
61
|
format: FormatSpec[FormattableT] | None
|
|
65
62
|
"""The response format for the generated response."""
|
|
66
63
|
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
fn: MessageTemplate[P],
|
|
68
|
+
toolkit: Toolkit,
|
|
69
|
+
format: FormatSpec[FormattableT] | None,
|
|
70
|
+
) -> None:
|
|
71
|
+
self.toolkit = toolkit
|
|
72
|
+
self.format = format
|
|
73
|
+
super().__init__(fn=fn)
|
|
74
|
+
|
|
67
75
|
def messages(self, *args: P.args, **kwargs: P.kwargs) -> Sequence[Message]:
|
|
68
76
|
"""Return the `Messages` from invoking this prompt."""
|
|
69
77
|
return promote_to_messages(self.fn(*args, **kwargs))
|
|
@@ -141,7 +149,6 @@ class Prompt(BasePrompt[MessageTemplate[P]], Generic[P, FormattableT]):
|
|
|
141
149
|
return model.stream(messages, tools=self.toolkit, format=self.format)
|
|
142
150
|
|
|
143
151
|
|
|
144
|
-
@dataclass
|
|
145
152
|
class AsyncPrompt(BasePrompt[AsyncMessageTemplate[P]], Generic[P, FormattableT]):
|
|
146
153
|
"""An async prompt that can be called with a model to generate a response.
|
|
147
154
|
|
|
@@ -158,6 +165,17 @@ class AsyncPrompt(BasePrompt[AsyncMessageTemplate[P]], Generic[P, FormattableT])
|
|
|
158
165
|
format: FormatSpec[FormattableT] | None
|
|
159
166
|
"""The response format for the generated response."""
|
|
160
167
|
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
*,
|
|
171
|
+
fn: AsyncMessageTemplate[P],
|
|
172
|
+
toolkit: AsyncToolkit,
|
|
173
|
+
format: FormatSpec[FormattableT] | None,
|
|
174
|
+
) -> None:
|
|
175
|
+
self.toolkit = toolkit
|
|
176
|
+
self.format = format
|
|
177
|
+
super().__init__(fn=fn)
|
|
178
|
+
|
|
161
179
|
async def messages(self, *args: P.args, **kwargs: P.kwargs) -> Sequence[Message]:
|
|
162
180
|
"""Return the `Messages` from invoking this prompt."""
|
|
163
181
|
return promote_to_messages(await self.fn(*args, **kwargs))
|
|
@@ -237,7 +255,6 @@ class AsyncPrompt(BasePrompt[AsyncMessageTemplate[P]], Generic[P, FormattableT])
|
|
|
237
255
|
)
|
|
238
256
|
|
|
239
257
|
|
|
240
|
-
@dataclass
|
|
241
258
|
class ContextPrompt(
|
|
242
259
|
BasePrompt[ContextMessageTemplate[P, DepsT]], Generic[P, DepsT, FormattableT]
|
|
243
260
|
):
|
|
@@ -257,6 +274,17 @@ class ContextPrompt(
|
|
|
257
274
|
format: FormatSpec[FormattableT] | None
|
|
258
275
|
"""The response format for the generated response."""
|
|
259
276
|
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
*,
|
|
280
|
+
fn: ContextMessageTemplate[P, DepsT],
|
|
281
|
+
toolkit: ContextToolkit[DepsT],
|
|
282
|
+
format: FormatSpec[FormattableT] | None,
|
|
283
|
+
) -> None:
|
|
284
|
+
self.toolkit = toolkit
|
|
285
|
+
self.format = format
|
|
286
|
+
super().__init__(fn=fn)
|
|
287
|
+
|
|
260
288
|
def messages(
|
|
261
289
|
self, ctx: Context[DepsT], *args: P.args, **kwargs: P.kwargs
|
|
262
290
|
) -> Sequence[Message]:
|
|
@@ -360,7 +388,6 @@ class ContextPrompt(
|
|
|
360
388
|
)
|
|
361
389
|
|
|
362
390
|
|
|
363
|
-
@dataclass
|
|
364
391
|
class AsyncContextPrompt(
|
|
365
392
|
BasePrompt[AsyncContextMessageTemplate[P, DepsT]], Generic[P, DepsT, FormattableT]
|
|
366
393
|
):
|
|
@@ -380,6 +407,17 @@ class AsyncContextPrompt(
|
|
|
380
407
|
format: FormatSpec[FormattableT] | None
|
|
381
408
|
"""The response format for the generated response."""
|
|
382
409
|
|
|
410
|
+
def __init__(
|
|
411
|
+
self,
|
|
412
|
+
*,
|
|
413
|
+
fn: AsyncContextMessageTemplate[P, DepsT],
|
|
414
|
+
toolkit: AsyncContextToolkit[DepsT],
|
|
415
|
+
format: FormatSpec[FormattableT] | None,
|
|
416
|
+
) -> None:
|
|
417
|
+
self.toolkit = toolkit
|
|
418
|
+
self.format = format
|
|
419
|
+
super().__init__(fn=fn)
|
|
420
|
+
|
|
383
421
|
async def messages(
|
|
384
422
|
self, ctx: Context[DepsT], *args: P.args, **kwargs: P.kwargs
|
|
385
423
|
) -> Sequence[Message]:
|
|
@@ -10,6 +10,7 @@ stub_module_if_missing("mirascope.llm.providers.anthropic", "anthropic")
|
|
|
10
10
|
stub_module_if_missing("mirascope.llm.providers.google", "google")
|
|
11
11
|
stub_module_if_missing("mirascope.llm.providers.mlx", "mlx")
|
|
12
12
|
stub_module_if_missing("mirascope.llm.providers.openai", "openai")
|
|
13
|
+
stub_module_if_missing("mirascope.llm.providers.openrouter", "openai")
|
|
13
14
|
stub_module_if_missing("mirascope.llm.providers.together", "openai")
|
|
14
15
|
stub_module_if_missing("mirascope.llm.providers.ollama", "openai")
|
|
15
16
|
|
|
@@ -30,6 +31,7 @@ from .openai import (
|
|
|
30
31
|
OpenAIProvider,
|
|
31
32
|
)
|
|
32
33
|
from .openai.completions import BaseOpenAICompletionsProvider
|
|
34
|
+
from .openrouter import OpenRouterProvider
|
|
33
35
|
from .provider_id import KNOWN_PROVIDER_IDS, ProviderId
|
|
34
36
|
from .provider_registry import (
|
|
35
37
|
get_provider_for_model,
|
|
@@ -53,6 +55,7 @@ __all__ = [
|
|
|
53
55
|
"OllamaProvider",
|
|
54
56
|
"OpenAIModelId",
|
|
55
57
|
"OpenAIProvider",
|
|
58
|
+
"OpenRouterProvider",
|
|
56
59
|
"Provider",
|
|
57
60
|
"ProviderId",
|
|
58
61
|
"TogetherProvider",
|
|
@@ -5,11 +5,14 @@ from .decode import (
|
|
|
5
5
|
model_name,
|
|
6
6
|
)
|
|
7
7
|
from .encode import encode_request
|
|
8
|
+
from .feature_info import CompletionsModelFeatureInfo, feature_info_for_openai_model
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
11
|
+
"CompletionsModelFeatureInfo",
|
|
10
12
|
"decode_async_stream",
|
|
11
13
|
"decode_response",
|
|
12
14
|
"decode_stream",
|
|
13
15
|
"encode_request",
|
|
16
|
+
"feature_info_for_openai_model",
|
|
14
17
|
"model_name",
|
|
15
18
|
]
|
|
@@ -22,12 +22,7 @@ from .....tools import (
|
|
|
22
22
|
)
|
|
23
23
|
from ....base import _utils as _base_utils
|
|
24
24
|
from ...model_id import OpenAIModelId, model_name
|
|
25
|
-
from
|
|
26
|
-
MODELS_WITHOUT_AUDIO_SUPPORT,
|
|
27
|
-
MODELS_WITHOUT_JSON_OBJECT_SUPPORT,
|
|
28
|
-
MODELS_WITHOUT_JSON_SCHEMA_SUPPORT,
|
|
29
|
-
NON_REASONING_MODELS,
|
|
30
|
-
)
|
|
25
|
+
from .feature_info import CompletionsModelFeatureInfo
|
|
31
26
|
|
|
32
27
|
if TYPE_CHECKING:
|
|
33
28
|
from .....models import Params
|
|
@@ -58,6 +53,8 @@ class ChatCompletionCreateKwargs(TypedDict, total=False):
|
|
|
58
53
|
def _encode_user_message(
|
|
59
54
|
message: UserMessage,
|
|
60
55
|
model_id: OpenAIModelId,
|
|
56
|
+
feature_info: CompletionsModelFeatureInfo,
|
|
57
|
+
provider_id: str,
|
|
61
58
|
) -> list[openai_types.ChatCompletionMessageParam]:
|
|
62
59
|
"""Convert Mirascope `UserMessage` to a list of OpenAI `ChatCompletionMessageParam`.
|
|
63
60
|
|
|
@@ -106,11 +103,10 @@ def _encode_user_message(
|
|
|
106
103
|
)
|
|
107
104
|
current_content.append(content)
|
|
108
105
|
elif part.type == "audio":
|
|
109
|
-
|
|
110
|
-
if base_model_name in MODELS_WITHOUT_AUDIO_SUPPORT:
|
|
106
|
+
if feature_info.audio_support is False:
|
|
111
107
|
raise FeatureNotSupportedError(
|
|
112
108
|
feature="Audio inputs",
|
|
113
|
-
provider_id=
|
|
109
|
+
provider_id=provider_id,
|
|
114
110
|
message=f"Model '{model_id}' does not support audio inputs.",
|
|
115
111
|
)
|
|
116
112
|
|
|
@@ -208,18 +204,13 @@ def _encode_assistant_message(
|
|
|
208
204
|
|
|
209
205
|
|
|
210
206
|
def _encode_message(
|
|
211
|
-
message: Message,
|
|
207
|
+
message: Message,
|
|
208
|
+
model_id: OpenAIModelId,
|
|
209
|
+
encode_thoughts_as_text: bool,
|
|
210
|
+
feature_info: CompletionsModelFeatureInfo,
|
|
211
|
+
provider_id: str,
|
|
212
212
|
) -> list[openai_types.ChatCompletionMessageParam]:
|
|
213
|
-
"""Convert a Mirascope `Message` to OpenAI `ChatCompletionMessageParam` format.
|
|
214
|
-
|
|
215
|
-
Args:
|
|
216
|
-
message: A Mirascope message (system, user, or assistant)
|
|
217
|
-
model_id: The model ID being used
|
|
218
|
-
encode_thoughts: Whether to encode thoughts as text
|
|
219
|
-
|
|
220
|
-
Returns:
|
|
221
|
-
A list of OpenAI `ChatCompletionMessageParam` (may be multiple for tool outputs)
|
|
222
|
-
"""
|
|
213
|
+
"""Convert a Mirascope `Message` to OpenAI `ChatCompletionMessageParam` format."""
|
|
223
214
|
if message.role == "system":
|
|
224
215
|
return [
|
|
225
216
|
openai_types.ChatCompletionSystemMessageParam(
|
|
@@ -227,7 +218,7 @@ def _encode_message(
|
|
|
227
218
|
)
|
|
228
219
|
]
|
|
229
220
|
elif message.role == "user":
|
|
230
|
-
return _encode_user_message(message, model_id)
|
|
221
|
+
return _encode_user_message(message, model_id, feature_info, provider_id)
|
|
231
222
|
elif message.role == "assistant":
|
|
232
223
|
return [_encode_assistant_message(message, model_id, encode_thoughts_as_text)]
|
|
233
224
|
else:
|
|
@@ -302,6 +293,8 @@ def encode_request(
|
|
|
302
293
|
tools: BaseToolkit[AnyToolSchema],
|
|
303
294
|
format: FormatSpec[FormattableT] | None,
|
|
304
295
|
params: Params,
|
|
296
|
+
feature_info: CompletionsModelFeatureInfo,
|
|
297
|
+
provider_id: str,
|
|
305
298
|
) -> tuple[Sequence[Message], Format[FormattableT] | None, ChatCompletionCreateKwargs]:
|
|
306
299
|
"""Prepares a request for the `OpenAI.chat.completions.create` method."""
|
|
307
300
|
if model_id.endswith(":responses"):
|
|
@@ -313,7 +306,11 @@ def encode_request(
|
|
|
313
306
|
)
|
|
314
307
|
base_model_name = model_name(model_id, None)
|
|
315
308
|
|
|
316
|
-
is_reasoning_model =
|
|
309
|
+
is_reasoning_model = feature_info.is_reasoning_model is True
|
|
310
|
+
strict_supported = feature_info.strict_support is True
|
|
311
|
+
strict_allowed = feature_info.strict_support is not False
|
|
312
|
+
supports_json_object = feature_info.json_object_support is True
|
|
313
|
+
|
|
317
314
|
kwargs: ChatCompletionCreateKwargs = ChatCompletionCreateKwargs(
|
|
318
315
|
{"model": base_model_name}
|
|
319
316
|
)
|
|
@@ -321,7 +318,7 @@ def encode_request(
|
|
|
321
318
|
|
|
322
319
|
with _base_utils.ensure_all_params_accessed(
|
|
323
320
|
params=params,
|
|
324
|
-
provider_id=
|
|
321
|
+
provider_id=provider_id,
|
|
325
322
|
unsupported_params=[
|
|
326
323
|
"top_k",
|
|
327
324
|
*(["temperature", "top_p", "stop_sequences"] if is_reasoning_model else []),
|
|
@@ -348,15 +345,14 @@ def encode_request(
|
|
|
348
345
|
|
|
349
346
|
openai_tools = [_convert_tool_to_tool_param(tool) for tool in tools.tools]
|
|
350
347
|
|
|
351
|
-
|
|
352
|
-
default_mode = "strict" if model_supports_strict else "tool"
|
|
348
|
+
default_mode = "strict" if strict_supported else "tool"
|
|
353
349
|
format = resolve_format(format, default_mode=default_mode)
|
|
354
350
|
if format is not None:
|
|
355
351
|
if format.mode == "strict":
|
|
356
|
-
if not
|
|
352
|
+
if not strict_allowed:
|
|
357
353
|
raise FeatureNotSupportedError(
|
|
358
354
|
feature="formatting_mode:strict",
|
|
359
|
-
provider_id=
|
|
355
|
+
provider_id=provider_id,
|
|
360
356
|
model_id=model_id,
|
|
361
357
|
)
|
|
362
358
|
kwargs["response_format"] = _create_strict_response_format(format)
|
|
@@ -371,10 +367,7 @@ def encode_request(
|
|
|
371
367
|
kwargs["parallel_tool_calls"] = False
|
|
372
368
|
format_tool_schema = format.create_tool_schema()
|
|
373
369
|
openai_tools.append(_convert_tool_to_tool_param(format_tool_schema))
|
|
374
|
-
elif
|
|
375
|
-
format.mode == "json"
|
|
376
|
-
and base_model_name not in MODELS_WITHOUT_JSON_OBJECT_SUPPORT
|
|
377
|
-
):
|
|
370
|
+
elif format.mode == "json" and supports_json_object:
|
|
378
371
|
kwargs["response_format"] = {"type": "json_object"}
|
|
379
372
|
|
|
380
373
|
if format.formatting_instructions:
|
|
@@ -388,7 +381,9 @@ def encode_request(
|
|
|
388
381
|
encoded_messages: list[openai_types.ChatCompletionMessageParam] = []
|
|
389
382
|
for message in messages:
|
|
390
383
|
encoded_messages.extend(
|
|
391
|
-
_encode_message(
|
|
384
|
+
_encode_message(
|
|
385
|
+
message, model_id, encode_thoughts_as_text, feature_info, provider_id
|
|
386
|
+
)
|
|
392
387
|
)
|
|
393
388
|
kwargs["messages"] = encoded_messages
|
|
394
389
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Model feature information for OpenAI completions encoding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ...model_info import (
|
|
8
|
+
MODELS_WITHOUT_AUDIO_SUPPORT,
|
|
9
|
+
MODELS_WITHOUT_JSON_OBJECT_SUPPORT,
|
|
10
|
+
MODELS_WITHOUT_JSON_SCHEMA_SUPPORT,
|
|
11
|
+
NON_REASONING_MODELS,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CompletionsModelFeatureInfo:
|
|
17
|
+
"""Model feature information for OpenAI completions encoding.
|
|
18
|
+
|
|
19
|
+
This dataclass encapsulates feature detection for OpenAI-compatible models,
|
|
20
|
+
allowing providers to pass pre-computed feature information rather than
|
|
21
|
+
relying on model name matching in encode_request.
|
|
22
|
+
|
|
23
|
+
None values mean "unknown":
|
|
24
|
+
- audio_support: None → allow audio (permissive)
|
|
25
|
+
- strict_support: None → default to tool mode, but allow explicit strict
|
|
26
|
+
- json_object_support: None → disable (use prompt instructions instead)
|
|
27
|
+
- is_reasoning_model: None → treat as False (allow temperature)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
audio_support: bool | None = None
|
|
31
|
+
"""Whether the model supports audio inputs. None means skip check (allow)."""
|
|
32
|
+
|
|
33
|
+
strict_support: bool | None = None
|
|
34
|
+
"""Whether the model supports strict JSON schema. None allows explicit strict."""
|
|
35
|
+
|
|
36
|
+
json_object_support: bool | None = None
|
|
37
|
+
"""Whether the model supports JSON object response format. None disables."""
|
|
38
|
+
|
|
39
|
+
is_reasoning_model: bool | None = None
|
|
40
|
+
"""Whether the model is a reasoning model. None means False (allow temperature)."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def feature_info_for_openai_model(model_name: str) -> CompletionsModelFeatureInfo:
|
|
44
|
+
"""Get feature info for a base OpenAI model name."""
|
|
45
|
+
return CompletionsModelFeatureInfo(
|
|
46
|
+
audio_support=model_name not in MODELS_WITHOUT_AUDIO_SUPPORT,
|
|
47
|
+
strict_support=model_name not in MODELS_WITHOUT_JSON_SCHEMA_SUPPORT,
|
|
48
|
+
json_object_support=model_name not in MODELS_WITHOUT_JSON_OBJECT_SUPPORT,
|
|
49
|
+
is_reasoning_model=model_name not in NON_REASONING_MODELS,
|
|
50
|
+
)
|
|
@@ -27,6 +27,7 @@ from ...base import BaseProvider
|
|
|
27
27
|
from .. import _utils as _shared_utils
|
|
28
28
|
from ..model_id import model_name as openai_model_name
|
|
29
29
|
from . import _utils
|
|
30
|
+
from ._utils import CompletionsModelFeatureInfo
|
|
30
31
|
|
|
31
32
|
if TYPE_CHECKING:
|
|
32
33
|
from ....models import Params
|
|
@@ -83,6 +84,10 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
83
84
|
"""Extract the model name to send to the API."""
|
|
84
85
|
return openai_model_name(model_id, None)
|
|
85
86
|
|
|
87
|
+
def _model_feature_info(self, model_id: str) -> CompletionsModelFeatureInfo:
|
|
88
|
+
"""Get feature info for the model. Override for provider-specific features."""
|
|
89
|
+
return CompletionsModelFeatureInfo()
|
|
90
|
+
|
|
86
91
|
def _provider_model_name(self, model_id: str) -> str:
|
|
87
92
|
"""Get the model name for tracking in Response."""
|
|
88
93
|
return self._model_name(model_id)
|
|
@@ -114,6 +119,8 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
114
119
|
tools=toolkit,
|
|
115
120
|
format=format,
|
|
116
121
|
params=params,
|
|
122
|
+
feature_info=self._model_feature_info(model_id),
|
|
123
|
+
provider_id=self.id,
|
|
117
124
|
)
|
|
118
125
|
kwargs["model"] = self._model_name(model_id)
|
|
119
126
|
openai_response = self.client.chat.completions.create(**kwargs)
|
|
@@ -168,6 +175,8 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
168
175
|
tools=toolkit,
|
|
169
176
|
format=format,
|
|
170
177
|
params=params,
|
|
178
|
+
feature_info=self._model_feature_info(model_id),
|
|
179
|
+
provider_id=self.id,
|
|
171
180
|
)
|
|
172
181
|
kwargs["model"] = self._model_name(model_id)
|
|
173
182
|
openai_response = self.client.chat.completions.create(**kwargs)
|
|
@@ -220,6 +229,8 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
220
229
|
messages=messages,
|
|
221
230
|
tools=toolkit,
|
|
222
231
|
format=format,
|
|
232
|
+
feature_info=self._model_feature_info(model_id),
|
|
233
|
+
provider_id=self.id,
|
|
223
234
|
)
|
|
224
235
|
kwargs["model"] = self._model_name(model_id)
|
|
225
236
|
openai_response = await self.async_client.chat.completions.create(**kwargs)
|
|
@@ -274,6 +285,8 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
274
285
|
messages=messages,
|
|
275
286
|
tools=toolkit,
|
|
276
287
|
format=format,
|
|
288
|
+
feature_info=self._model_feature_info(model_id),
|
|
289
|
+
provider_id=self.id,
|
|
277
290
|
)
|
|
278
291
|
kwargs["model"] = self._model_name(model_id)
|
|
279
292
|
openai_response = await self.async_client.chat.completions.create(**kwargs)
|
|
@@ -326,6 +339,8 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
326
339
|
tools=toolkit,
|
|
327
340
|
format=format,
|
|
328
341
|
params=params,
|
|
342
|
+
feature_info=self._model_feature_info(model_id),
|
|
343
|
+
provider_id=self.id,
|
|
329
344
|
)
|
|
330
345
|
kwargs["model"] = self._model_name(model_id)
|
|
331
346
|
openai_stream = self.client.chat.completions.create(
|
|
@@ -376,6 +391,8 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
376
391
|
tools=toolkit,
|
|
377
392
|
format=format,
|
|
378
393
|
params=params,
|
|
394
|
+
feature_info=self._model_feature_info(model_id),
|
|
395
|
+
provider_id=self.id,
|
|
379
396
|
)
|
|
380
397
|
kwargs["model"] = self._model_name(model_id)
|
|
381
398
|
|
|
@@ -425,6 +442,8 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
425
442
|
tools=toolkit,
|
|
426
443
|
format=format,
|
|
427
444
|
params=params,
|
|
445
|
+
feature_info=self._model_feature_info(model_id),
|
|
446
|
+
provider_id=self.id,
|
|
428
447
|
)
|
|
429
448
|
kwargs["model"] = self._model_name(model_id)
|
|
430
449
|
openai_stream = await self.async_client.chat.completions.create(
|
|
@@ -478,6 +497,8 @@ class BaseOpenAICompletionsProvider(BaseProvider[OpenAI]):
|
|
|
478
497
|
tools=toolkit,
|
|
479
498
|
format=format,
|
|
480
499
|
params=params,
|
|
500
|
+
feature_info=self._model_feature_info(model_id),
|
|
501
|
+
provider_id=self.id,
|
|
481
502
|
)
|
|
482
503
|
kwargs["model"] = self._model_name(model_id)
|
|
483
504
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""OpenAI Completions API provider implementation."""
|
|
2
2
|
|
|
3
|
-
from ..model_id import model_name
|
|
3
|
+
from ..model_id import model_name as openai_model_name
|
|
4
|
+
from ._utils import CompletionsModelFeatureInfo, feature_info_for_openai_model
|
|
4
5
|
from .base_provider import BaseOpenAICompletionsProvider
|
|
5
6
|
|
|
6
7
|
|
|
@@ -14,9 +15,14 @@ class OpenAICompletionsProvider(BaseOpenAICompletionsProvider):
|
|
|
14
15
|
api_key_required = False
|
|
15
16
|
provider_name = "OpenAI"
|
|
16
17
|
|
|
18
|
+
def _model_feature_info(self, model_id: str) -> CompletionsModelFeatureInfo:
|
|
19
|
+
"""Get feature info for actual OpenAI models."""
|
|
20
|
+
base_name = openai_model_name(model_id, None)
|
|
21
|
+
return feature_info_for_openai_model(base_name)
|
|
22
|
+
|
|
17
23
|
def _provider_model_name(self, model_id: str) -> str:
|
|
18
24
|
"""Get the model name for tracking in Response.
|
|
19
25
|
|
|
20
26
|
Returns the model name with :completions suffix for tracking which API was used.
|
|
21
27
|
"""
|
|
22
|
-
return
|
|
28
|
+
return openai_model_name(model_id, "completions")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""OpenRouter provider implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from ..openai.completions._utils import (
|
|
8
|
+
CompletionsModelFeatureInfo,
|
|
9
|
+
feature_info_for_openai_model,
|
|
10
|
+
)
|
|
11
|
+
from ..openai.completions.base_provider import BaseOpenAICompletionsProvider
|
|
12
|
+
from ..openai.model_id import model_name as openai_model_name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpenRouterProvider(BaseOpenAICompletionsProvider):
|
|
16
|
+
"""Provider for OpenRouter's OpenAI-compatible API.
|
|
17
|
+
|
|
18
|
+
Inherits from BaseOpenAICompletionsProvider with OpenRouter-specific configuration:
|
|
19
|
+
- Uses OpenRouter's API endpoint
|
|
20
|
+
- Requires OPENROUTER_API_KEY
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
Option 1: Use "openrouter/" prefix for explicit OpenRouter models:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from mirascope import llm
|
|
27
|
+
|
|
28
|
+
llm.register_provider("openrouter", scope="openrouter/")
|
|
29
|
+
|
|
30
|
+
@llm.call("openrouter/openai/gpt-4o")
|
|
31
|
+
def my_prompt():
|
|
32
|
+
return [llm.messages.user("Hello!")]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Option 2: Route existing model IDs through OpenRouter:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from mirascope import llm
|
|
39
|
+
|
|
40
|
+
# Register for openai and anthropic models via OpenRouter
|
|
41
|
+
llm.register_provider("openrouter", scope=["openai/", "anthropic/"])
|
|
42
|
+
|
|
43
|
+
# Now openai/ models go through OpenRouter
|
|
44
|
+
@llm.call("openai/gpt-4")
|
|
45
|
+
def my_prompt():
|
|
46
|
+
return [llm.messages.user("Hello!")]
|
|
47
|
+
```
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
id: ClassVar[str] = "openrouter"
|
|
51
|
+
default_scope: ClassVar[str | list[str]] = []
|
|
52
|
+
default_base_url: ClassVar[str | None] = "https://openrouter.ai/api/v1"
|
|
53
|
+
api_key_env_var: ClassVar[str] = "OPENROUTER_API_KEY"
|
|
54
|
+
api_key_required: ClassVar[bool] = True
|
|
55
|
+
provider_name: ClassVar[str | None] = "OpenRouter"
|
|
56
|
+
|
|
57
|
+
def _model_name(self, model_id: str) -> str:
|
|
58
|
+
"""Strip 'openrouter/' prefix from model ID for OpenRouter API."""
|
|
59
|
+
return model_id.removeprefix("openrouter/")
|
|
60
|
+
|
|
61
|
+
def _model_feature_info(self, model_id: str) -> CompletionsModelFeatureInfo:
|
|
62
|
+
"""Return OpenAI feature info for openai/* models, empty info otherwise."""
|
|
63
|
+
base_model_id = model_id.removeprefix("openrouter/")
|
|
64
|
+
if base_model_id.startswith("openai/"):
|
|
65
|
+
openai_name = openai_model_name(base_model_id, None)
|
|
66
|
+
return feature_info_for_openai_model(openai_name)
|
|
67
|
+
return CompletionsModelFeatureInfo()
|
|
@@ -10,6 +10,7 @@ KnownProviderId: TypeAlias = Literal[
|
|
|
10
10
|
"mlx", # Local inference powered by `mlx-lm`, via MLXProvider
|
|
11
11
|
"ollama", # Ollama provider via OllamaProvider
|
|
12
12
|
"openai", # OpenAI provider via OpenAIProvider (prefers Responses routing when available)
|
|
13
|
+
"openrouter", # OpenRouter provider via OpenRouterProvider
|
|
13
14
|
"together", # Together AI provider via TogetherProvider
|
|
14
15
|
]
|
|
15
16
|
KNOWN_PROVIDER_IDS = get_args(KnownProviderId)
|
|
@@ -20,5 +21,6 @@ OpenAICompletionsCompatibleProviderId: TypeAlias = Literal[
|
|
|
20
21
|
"ollama", # Ollama (OpenAI-compatible)
|
|
21
22
|
"openai", # OpenAI via OpenAIProvider (routes to completions)
|
|
22
23
|
"openai:completions", # OpenAI Completions API directly
|
|
24
|
+
"openrouter", # OpenRouter (OpenAI-compatible)
|
|
23
25
|
"together", # Together AI (OpenAI-compatible)
|
|
24
26
|
]
|
|
@@ -16,6 +16,7 @@ from .ollama import OllamaProvider
|
|
|
16
16
|
from .openai import OpenAIProvider
|
|
17
17
|
from .openai.completions.provider import OpenAICompletionsProvider
|
|
18
18
|
from .openai.responses.provider import OpenAIResponsesProvider
|
|
19
|
+
from .openrouter import OpenRouterProvider
|
|
19
20
|
from .provider_id import ProviderId
|
|
20
21
|
from .together import TogetherProvider
|
|
21
22
|
|
|
@@ -71,6 +72,9 @@ DEFAULT_AUTO_REGISTER_SCOPES: dict[str, Sequence[ProviderDefault]] = {
|
|
|
71
72
|
"mlx-community/": [
|
|
72
73
|
ProviderDefault("mlx", None), # No API key required
|
|
73
74
|
],
|
|
75
|
+
"openrouter/": [
|
|
76
|
+
ProviderDefault("openrouter", "OPENROUTER_API_KEY"),
|
|
77
|
+
],
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
|
|
@@ -122,6 +126,8 @@ def provider_singleton(
|
|
|
122
126
|
return OpenAICompletionsProvider(api_key=api_key, base_url=base_url)
|
|
123
127
|
case "openai:responses":
|
|
124
128
|
return OpenAIResponsesProvider(api_key=api_key, base_url=base_url)
|
|
129
|
+
case "openrouter":
|
|
130
|
+
return OpenRouterProvider(api_key=api_key, base_url=base_url)
|
|
125
131
|
case "together":
|
|
126
132
|
return TogetherProvider(api_key=api_key, base_url=base_url)
|
|
127
133
|
case _: # pragma: no cover
|