mojentic 0.7.4__py3-none-any.whl → 0.8.1__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.
@@ -0,0 +1,351 @@
1
+ """
2
+ OpenAI Model Registry for managing model-specific configurations and capabilities.
3
+
4
+ This module provides infrastructure for categorizing OpenAI models and managing
5
+ their specific parameter requirements and capabilities.
6
+ """
7
+
8
+ from enum import Enum
9
+ from typing import Dict, Set, Optional, List, TYPE_CHECKING
10
+ from dataclasses import dataclass
11
+
12
+ import structlog
13
+
14
+ if TYPE_CHECKING:
15
+ from mojentic.llm.gateways.openai import OpenAIGateway
16
+
17
+ logger = structlog.get_logger()
18
+
19
+
20
+ class ModelType(Enum):
21
+ """Classification of OpenAI model types based on their capabilities and parameters."""
22
+ REASONING = "reasoning" # Models like o1, o3 that use max_completion_tokens
23
+ CHAT = "chat" # Standard chat models that use max_tokens
24
+ EMBEDDING = "embedding" # Text embedding models
25
+ MODERATION = "moderation" # Content moderation models
26
+
27
+
28
+ @dataclass
29
+ class ModelCapabilities:
30
+ """Defines the capabilities and parameter requirements for a model."""
31
+ model_type: ModelType
32
+ supports_tools: bool = True
33
+ supports_streaming: bool = True
34
+ supports_vision: bool = False
35
+ max_context_tokens: Optional[int] = None
36
+ max_output_tokens: Optional[int] = None
37
+ supported_temperatures: Optional[List[float]] = None # None means all temperatures supported
38
+
39
+ def get_token_limit_param(self) -> str:
40
+ """Get the correct parameter name for token limits based on model type."""
41
+ if self.model_type == ModelType.REASONING:
42
+ return "max_completion_tokens"
43
+ return "max_tokens"
44
+
45
+ def supports_temperature(self, temperature: float) -> bool:
46
+ """Check if the model supports a specific temperature value."""
47
+ if self.supported_temperatures is None:
48
+ return True # All temperatures supported if not restricted
49
+ if self.supported_temperatures == []:
50
+ return False # No temperature values supported (parameter not allowed)
51
+ return temperature in self.supported_temperatures
52
+
53
+
54
+ class OpenAIModelRegistry:
55
+ """
56
+ Registry for managing OpenAI model configurations and capabilities.
57
+
58
+ This class provides a centralized way to manage model-specific configurations,
59
+ parameter mappings, and capabilities for OpenAI models.
60
+ """
61
+
62
+ def __init__(self):
63
+ self._models: Dict[str, ModelCapabilities] = {}
64
+ self._pattern_mappings: Dict[str, ModelType] = {}
65
+ self._initialize_default_models()
66
+
67
+ def _initialize_default_models(self):
68
+ """Initialize the registry with known OpenAI models and their capabilities."""
69
+
70
+ # Reasoning Models (o1, o3, o4, gpt-5 series) - Updated 2025-09-28
71
+ reasoning_models = [
72
+ "o1", "o1-2024-12-17", "o1-mini", "o1-mini-2024-09-12",
73
+ "o1-pro", "o1-pro-2025-03-19",
74
+ "o3", "o3-2025-04-16", "o3-deep-research", "o3-deep-research-2025-06-26",
75
+ "o3-mini", "o3-mini-2025-01-31", "o3-pro", "o3-pro-2025-06-10",
76
+ "o4-mini", "o4-mini-2025-04-16", "o4-mini-deep-research",
77
+ "o4-mini-deep-research-2025-06-26",
78
+ "gpt-5", "gpt-5-2025-08-07", "gpt-5-chat-latest", "gpt-5-codex",
79
+ "gpt-5-mini", "gpt-5-mini-2025-08-07", "gpt-5-nano", "gpt-5-nano-2025-08-07"
80
+ ]
81
+
82
+ for model in reasoning_models:
83
+ # Deep research models and GPT-5 might have different capabilities
84
+ is_deep_research = "deep-research" in model
85
+ is_gpt5 = "gpt-5" in model
86
+ is_o1_series = model.startswith("o1")
87
+ is_o3_series = model.startswith("o3")
88
+ is_o4_series = model.startswith("o4")
89
+ is_mini_or_nano = ("mini" in model or "nano" in model)
90
+
91
+ # GPT-5 models may support more features than o1/o3/o4
92
+ supports_tools = is_gpt5 # GPT-5 might support tools
93
+ supports_streaming = is_gpt5 # GPT-5 might support streaming
94
+
95
+ # Set context and output tokens based on model tier
96
+ if is_gpt5:
97
+ context_tokens = 300000 if not is_mini_or_nano else 200000
98
+ output_tokens = 50000 if not is_mini_or_nano else 32768
99
+ elif is_deep_research:
100
+ context_tokens = 200000
101
+ output_tokens = 100000
102
+ else:
103
+ context_tokens = 128000
104
+ output_tokens = 32768
105
+
106
+ # Temperature restrictions based on model series
107
+ if is_gpt5 or is_o1_series or is_o4_series:
108
+ # GPT-5, o1, and o4 series only support temperature=1.0
109
+ supported_temps = [1.0]
110
+ elif is_o3_series:
111
+ # o3 series doesn't support temperature parameter at all
112
+ supported_temps = []
113
+ else:
114
+ # Other reasoning models support all temperatures
115
+ supported_temps = None
116
+
117
+ self._models[model] = ModelCapabilities(
118
+ model_type=ModelType.REASONING,
119
+ supports_tools=supports_tools,
120
+ supports_streaming=supports_streaming,
121
+ supports_vision=False, # Vision support would need to be confirmed for GPT-5
122
+ max_context_tokens=context_tokens,
123
+ max_output_tokens=output_tokens,
124
+ supported_temperatures=supported_temps
125
+ )
126
+
127
+ # Chat Models (GPT-4 and GPT-4.1 series) - Updated 2025-09-28
128
+ # Note: GPT-5 series moved to reasoning models
129
+ gpt4_and_newer_models = [
130
+ "chatgpt-4o-latest",
131
+ "gpt-4", "gpt-4-0125-preview", "gpt-4-0613", "gpt-4-1106-preview",
132
+ "gpt-4-turbo", "gpt-4-turbo-2024-04-09", "gpt-4-turbo-preview",
133
+ "gpt-4.1", "gpt-4.1-2025-04-14", "gpt-4.1-mini", "gpt-4.1-mini-2025-04-14",
134
+ "gpt-4.1-nano", "gpt-4.1-nano-2025-04-14",
135
+ "gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06", "gpt-4o-2024-11-20",
136
+ "gpt-4o-audio-preview", "gpt-4o-audio-preview-2024-10-01",
137
+ "gpt-4o-audio-preview-2024-12-17", "gpt-4o-audio-preview-2025-06-03",
138
+ "gpt-4o-mini", "gpt-4o-mini-2024-07-18",
139
+ "gpt-4o-mini-audio-preview", "gpt-4o-mini-audio-preview-2024-12-17",
140
+ "gpt-4o-mini-realtime-preview", "gpt-4o-mini-realtime-preview-2024-12-17",
141
+ "gpt-4o-mini-search-preview", "gpt-4o-mini-search-preview-2025-03-11",
142
+ "gpt-4o-mini-transcribe", "gpt-4o-mini-tts",
143
+ "gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01",
144
+ "gpt-4o-realtime-preview-2024-12-17", "gpt-4o-realtime-preview-2025-06-03",
145
+ "gpt-4o-search-preview", "gpt-4o-search-preview-2025-03-11",
146
+ "gpt-4o-transcribe"
147
+ ]
148
+
149
+ for model in gpt4_and_newer_models:
150
+ # Determine capabilities based on model features
151
+ vision_support = ("gpt-4o" in model or "audio-preview" in model or "realtime" in model)
152
+ is_mini_or_nano = ("mini" in model or "nano" in model)
153
+ is_audio = "audio" in model or "realtime" in model or "transcribe" in model
154
+ is_gpt41 = "gpt-4.1" in model
155
+
156
+ # Set context and output tokens based on model tier
157
+ if is_gpt41:
158
+ context_tokens = 200000 if not is_mini_or_nano else 128000
159
+ output_tokens = 32768 if not is_mini_or_nano else 16384
160
+ elif "gpt-4o" in model:
161
+ context_tokens = 128000
162
+ output_tokens = 16384
163
+ else: # GPT-4 series
164
+ context_tokens = 32000
165
+ output_tokens = 8192
166
+
167
+ self._models[model] = ModelCapabilities(
168
+ model_type=ModelType.CHAT,
169
+ supports_tools=True,
170
+ supports_streaming=not is_audio, # Audio models may not support streaming
171
+ supports_vision=vision_support,
172
+ max_context_tokens=context_tokens,
173
+ max_output_tokens=output_tokens
174
+ )
175
+
176
+ # Chat Models (GPT-3.5 series) - Updated 2025-09-28
177
+ gpt35_models = [
178
+ "gpt-3.5-turbo", "gpt-3.5-turbo-0125", "gpt-3.5-turbo-1106",
179
+ "gpt-3.5-turbo-16k", "gpt-3.5-turbo-instruct", "gpt-3.5-turbo-instruct-0914"
180
+ ]
181
+
182
+ for model in gpt35_models:
183
+ context_tokens = 16385 if "16k" not in model else 16385
184
+ self._models[model] = ModelCapabilities(
185
+ model_type=ModelType.CHAT,
186
+ supports_tools="instruct" not in model, # Instruct models don't support tools
187
+ supports_streaming="instruct" not in model, # Instruct models don't support streaming
188
+ supports_vision=False,
189
+ max_context_tokens=context_tokens,
190
+ max_output_tokens=4096
191
+ )
192
+
193
+ # Embedding Models - Updated 2025-09-28
194
+ embedding_models = [
195
+ "text-embedding-3-large", "text-embedding-3-small", "text-embedding-ada-002"
196
+ ]
197
+
198
+ for model in embedding_models:
199
+ self._models[model] = ModelCapabilities(
200
+ model_type=ModelType.EMBEDDING,
201
+ supports_tools=False,
202
+ supports_streaming=False,
203
+ supports_vision=False
204
+ )
205
+
206
+ # Pattern mappings for unknown models - Updated 2025-09-28
207
+ self._pattern_mappings = {
208
+ "o1": ModelType.REASONING,
209
+ "o3": ModelType.REASONING,
210
+ "o4": ModelType.REASONING,
211
+ "gpt-5": ModelType.REASONING, # GPT-5 is a reasoning model
212
+ "gpt-4": ModelType.CHAT,
213
+ "gpt-4.1": ModelType.CHAT,
214
+ "gpt-3.5": ModelType.CHAT,
215
+ "chatgpt": ModelType.CHAT,
216
+ "text-embedding": ModelType.EMBEDDING,
217
+ "text-moderation": ModelType.MODERATION
218
+ }
219
+
220
+ def get_model_capabilities(self, model_name: str) -> ModelCapabilities:
221
+ """
222
+ Get the capabilities for a specific model.
223
+
224
+ Parameters
225
+ ----------
226
+ model_name : str
227
+ The name of the model to look up.
228
+
229
+ Returns
230
+ -------
231
+ ModelCapabilities
232
+ The capabilities for the model.
233
+ """
234
+ # Direct lookup first
235
+ if model_name in self._models:
236
+ return self._models[model_name]
237
+
238
+ # Pattern matching for unknown models
239
+ model_lower = model_name.lower()
240
+ for pattern, model_type in self._pattern_mappings.items():
241
+ if pattern in model_lower:
242
+ logger.warning(
243
+ "Using pattern matching for unknown model",
244
+ model=model_name,
245
+ pattern=pattern,
246
+ inferred_type=model_type.value
247
+ )
248
+ # Return default capabilities for the inferred type
249
+ return self._get_default_capabilities_for_type(model_type)
250
+
251
+ # Default to chat model if no pattern matches
252
+ logger.warning(
253
+ "Unknown model, defaulting to chat model capabilities",
254
+ model=model_name
255
+ )
256
+ return self._get_default_capabilities_for_type(ModelType.CHAT)
257
+
258
+ def _get_default_capabilities_for_type(self, model_type: ModelType) -> ModelCapabilities:
259
+ """Get default capabilities for a model type."""
260
+ if model_type == ModelType.REASONING:
261
+ return ModelCapabilities(
262
+ model_type=ModelType.REASONING,
263
+ supports_tools=False,
264
+ supports_streaming=False,
265
+ supports_vision=False
266
+ )
267
+ elif model_type == ModelType.CHAT:
268
+ return ModelCapabilities(
269
+ model_type=ModelType.CHAT,
270
+ supports_tools=True,
271
+ supports_streaming=True,
272
+ supports_vision=False
273
+ )
274
+ elif model_type == ModelType.EMBEDDING:
275
+ return ModelCapabilities(
276
+ model_type=ModelType.EMBEDDING,
277
+ supports_tools=False,
278
+ supports_streaming=False,
279
+ supports_vision=False
280
+ )
281
+ else: # MODERATION
282
+ return ModelCapabilities(
283
+ model_type=ModelType.MODERATION,
284
+ supports_tools=False,
285
+ supports_streaming=False,
286
+ supports_vision=False
287
+ )
288
+
289
+ def is_reasoning_model(self, model_name: str) -> bool:
290
+ """
291
+ Check if a model is a reasoning model.
292
+
293
+ Parameters
294
+ ----------
295
+ model_name : str
296
+ The name of the model to check.
297
+
298
+ Returns
299
+ -------
300
+ bool
301
+ True if the model is a reasoning model, False otherwise.
302
+ """
303
+ capabilities = self.get_model_capabilities(model_name)
304
+ return capabilities.model_type == ModelType.REASONING
305
+
306
+ def get_registered_models(self) -> List[str]:
307
+ """
308
+ Get a list of all explicitly registered models.
309
+
310
+ Returns
311
+ -------
312
+ List[str]
313
+ List of registered model names.
314
+ """
315
+ return list(self._models.keys())
316
+
317
+ def register_model(self, model_name: str, capabilities: ModelCapabilities):
318
+ """
319
+ Register a new model with its capabilities.
320
+
321
+ Parameters
322
+ ----------
323
+ model_name : str
324
+ The name of the model to register.
325
+ capabilities : ModelCapabilities
326
+ The capabilities of the model.
327
+ """
328
+ self._models[model_name] = capabilities
329
+ logger.info("Registered new model", model=model_name, type=capabilities.model_type.value)
330
+
331
+ def register_pattern(self, pattern: str, model_type: ModelType):
332
+ """
333
+ Register a pattern for inferring model types.
334
+
335
+ Parameters
336
+ ----------
337
+ pattern : str
338
+ The pattern to match in model names.
339
+ model_type : ModelType
340
+ The type to infer for matching models.
341
+ """
342
+ self._pattern_mappings[pattern] = model_type
343
+ logger.info("Registered new pattern", pattern=pattern, type=model_type.value)
344
+
345
+
346
+ # Global registry instance
347
+ _registry = OpenAIModelRegistry()
348
+
349
+ def get_model_registry() -> OpenAIModelRegistry:
350
+ """Get the global OpenAI model registry instance."""
351
+ return _registry
@@ -0,0 +1,181 @@
1
+ """
2
+ Tests for the OpenAI Model Registry system.
3
+ """
4
+
5
+ import pytest
6
+ from mojentic.llm.gateways.openai_model_registry import (
7
+ OpenAIModelRegistry,
8
+ ModelType,
9
+ ModelCapabilities,
10
+ get_model_registry
11
+ )
12
+
13
+
14
+ class DescribeOpenAIModelRegistry:
15
+ """Specification for the OpenAI Model Registry."""
16
+
17
+ def should_initialize_with_default_models(self):
18
+ """
19
+ Given a new model registry
20
+ When initialized
21
+ Then it should contain default models for all major OpenAI model families
22
+ """
23
+ registry = OpenAIModelRegistry()
24
+ registered_models = registry.get_registered_models()
25
+
26
+ # Check that we have reasoning models
27
+ assert "o1" in registered_models
28
+ assert "o1-mini" in registered_models
29
+
30
+ # Check that we have chat models
31
+ assert "gpt-4o" in registered_models
32
+ assert "gpt-4o-mini" in registered_models
33
+ assert "gpt-3.5-turbo" in registered_models
34
+
35
+ # Check that we have embedding models
36
+ assert "text-embedding-3-large" in registered_models
37
+ assert "text-embedding-3-small" in registered_models
38
+
39
+ def should_identify_reasoning_models_correctly(self):
40
+ """
41
+ Given various model names
42
+ When checking if they are reasoning models
43
+ Then it should correctly classify known reasoning models
44
+ """
45
+ registry = OpenAIModelRegistry()
46
+
47
+ # Test known reasoning models
48
+ assert registry.is_reasoning_model("o1-preview") is True
49
+ assert registry.is_reasoning_model("o1-mini") is True
50
+ assert registry.is_reasoning_model("o3-mini") is True
51
+
52
+ # Test chat models
53
+ assert registry.is_reasoning_model("gpt-4o") is False
54
+ assert registry.is_reasoning_model("gpt-4o-mini") is False
55
+ assert registry.is_reasoning_model("gpt-3.5-turbo") is False
56
+
57
+ def should_use_pattern_matching_for_unknown_models(self):
58
+ """
59
+ Given an unknown model name that matches a pattern
60
+ When getting model capabilities
61
+ Then it should infer the correct model type
62
+ """
63
+ registry = OpenAIModelRegistry()
64
+
65
+ # Test unknown reasoning model
66
+ capabilities = registry.get_model_capabilities("o1-super-new")
67
+ assert capabilities.model_type == ModelType.REASONING
68
+ assert capabilities.get_token_limit_param() == "max_completion_tokens"
69
+
70
+ # Test unknown chat model
71
+ capabilities = registry.get_model_capabilities("gpt-4-future")
72
+ assert capabilities.model_type == ModelType.CHAT
73
+ assert capabilities.get_token_limit_param() == "max_tokens"
74
+
75
+ def should_return_correct_token_limit_parameters(self):
76
+ """
77
+ Given models of different types
78
+ When getting their token limit parameters
79
+ Then it should return the correct parameter name
80
+ """
81
+ registry = OpenAIModelRegistry()
82
+
83
+ # Reasoning models should use max_completion_tokens
84
+ o1_capabilities = registry.get_model_capabilities("o1-mini")
85
+ assert o1_capabilities.get_token_limit_param() == "max_completion_tokens"
86
+
87
+ # Chat models should use max_tokens
88
+ gpt4_capabilities = registry.get_model_capabilities("gpt-4o")
89
+ assert gpt4_capabilities.get_token_limit_param() == "max_tokens"
90
+
91
+ def should_allow_registering_new_models(self):
92
+ """
93
+ Given a new model with specific capabilities
94
+ When registering it in the registry
95
+ Then it should be available for lookup
96
+ """
97
+ registry = OpenAIModelRegistry()
98
+
99
+ new_capabilities = ModelCapabilities(
100
+ model_type=ModelType.REASONING,
101
+ supports_tools=True,
102
+ supports_streaming=True,
103
+ max_output_tokens=50000
104
+ )
105
+
106
+ registry.register_model("o5-preview", new_capabilities)
107
+
108
+ retrieved_capabilities = registry.get_model_capabilities("o5-preview")
109
+ assert retrieved_capabilities.model_type == ModelType.REASONING
110
+ assert retrieved_capabilities.supports_tools is True
111
+ assert retrieved_capabilities.max_output_tokens == 50000
112
+
113
+ def should_allow_registering_new_patterns(self):
114
+ """
115
+ Given a new pattern for model type inference
116
+ When registering it in the registry
117
+ Then it should be used for unknown models matching the pattern
118
+ """
119
+ registry = OpenAIModelRegistry()
120
+
121
+ registry.register_pattern("claude", ModelType.CHAT)
122
+
123
+ capabilities = registry.get_model_capabilities("claude-3-opus")
124
+ assert capabilities.model_type == ModelType.CHAT
125
+
126
+ def should_handle_completely_unknown_models(self):
127
+ """
128
+ Given a completely unknown model name with no matching patterns
129
+ When getting model capabilities
130
+ Then it should default to chat model capabilities
131
+ """
132
+ registry = OpenAIModelRegistry()
133
+
134
+ capabilities = registry.get_model_capabilities("completely-unknown-model-xyz")
135
+ assert capabilities.model_type == ModelType.CHAT
136
+ assert capabilities.get_token_limit_param() == "max_tokens"
137
+
138
+ def should_provide_global_registry_instance(self):
139
+ """
140
+ Given the global registry function
141
+ When called multiple times
142
+ Then it should return the same instance
143
+ """
144
+ registry1 = get_model_registry()
145
+ registry2 = get_model_registry()
146
+
147
+ assert registry1 is registry2
148
+
149
+ def should_handle_model_capabilities_dataclass_correctly(self):
150
+ """
151
+ Given model capabilities
152
+ When created with different parameters
153
+ Then it should handle defaults and customizations correctly
154
+ """
155
+ # Test with defaults
156
+ default_caps = ModelCapabilities(model_type=ModelType.CHAT)
157
+ assert default_caps.supports_tools is True
158
+ assert default_caps.supports_streaming is True
159
+ assert default_caps.supports_vision is False
160
+
161
+ # Test with custom values
162
+ custom_caps = ModelCapabilities(
163
+ model_type=ModelType.REASONING,
164
+ supports_tools=False,
165
+ supports_vision=True,
166
+ max_context_tokens=100000
167
+ )
168
+ assert custom_caps.supports_tools is False
169
+ assert custom_caps.supports_vision is True
170
+ assert custom_caps.max_context_tokens == 100000
171
+
172
+ def should_have_correct_model_type_enum_values(self):
173
+ """
174
+ Given the ModelType enum
175
+ When accessing its values
176
+ Then it should have all expected model types
177
+ """
178
+ assert ModelType.REASONING.value == "reasoning"
179
+ assert ModelType.CHAT.value == "chat"
180
+ assert ModelType.EMBEDDING.value == "embedding"
181
+ assert ModelType.MODERATION.value == "moderation"