abstractcore 2.5.2__py3-none-any.whl → 2.6.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.
- abstractcore/__init__.py +19 -1
- abstractcore/architectures/detection.py +252 -6
- abstractcore/assets/architecture_formats.json +14 -1
- abstractcore/assets/model_capabilities.json +533 -10
- abstractcore/compression/__init__.py +29 -0
- abstractcore/compression/analytics.py +420 -0
- abstractcore/compression/cache.py +250 -0
- abstractcore/compression/config.py +279 -0
- abstractcore/compression/exceptions.py +30 -0
- abstractcore/compression/glyph_processor.py +381 -0
- abstractcore/compression/optimizer.py +388 -0
- abstractcore/compression/orchestrator.py +380 -0
- abstractcore/compression/pil_text_renderer.py +818 -0
- abstractcore/compression/quality.py +226 -0
- abstractcore/compression/text_formatter.py +666 -0
- abstractcore/compression/vision_compressor.py +371 -0
- abstractcore/config/main.py +64 -0
- abstractcore/config/manager.py +100 -5
- abstractcore/core/retry.py +2 -2
- abstractcore/core/session.py +193 -7
- abstractcore/download.py +253 -0
- abstractcore/embeddings/manager.py +2 -2
- abstractcore/events/__init__.py +113 -2
- abstractcore/exceptions/__init__.py +49 -2
- abstractcore/media/auto_handler.py +312 -18
- abstractcore/media/handlers/local_handler.py +14 -2
- abstractcore/media/handlers/openai_handler.py +62 -3
- abstractcore/media/processors/__init__.py +11 -1
- abstractcore/media/processors/direct_pdf_processor.py +210 -0
- abstractcore/media/processors/glyph_pdf_processor.py +227 -0
- abstractcore/media/processors/image_processor.py +7 -1
- abstractcore/media/processors/office_processor.py +2 -2
- abstractcore/media/processors/text_processor.py +18 -3
- abstractcore/media/types.py +164 -7
- abstractcore/media/utils/image_scaler.py +2 -2
- abstractcore/media/vision_fallback.py +2 -2
- abstractcore/providers/__init__.py +18 -0
- abstractcore/providers/anthropic_provider.py +228 -8
- abstractcore/providers/base.py +378 -11
- abstractcore/providers/huggingface_provider.py +563 -23
- abstractcore/providers/lmstudio_provider.py +284 -4
- abstractcore/providers/mlx_provider.py +27 -2
- abstractcore/providers/model_capabilities.py +352 -0
- abstractcore/providers/ollama_provider.py +282 -6
- abstractcore/providers/openai_provider.py +286 -8
- abstractcore/providers/registry.py +85 -13
- abstractcore/providers/streaming.py +2 -2
- abstractcore/server/app.py +91 -81
- abstractcore/tools/common_tools.py +2 -2
- abstractcore/tools/handler.py +2 -2
- abstractcore/tools/parser.py +2 -2
- abstractcore/tools/registry.py +2 -2
- abstractcore/tools/syntax_rewriter.py +2 -2
- abstractcore/tools/tag_rewriter.py +3 -3
- abstractcore/utils/__init__.py +4 -1
- abstractcore/utils/self_fixes.py +2 -2
- abstractcore/utils/trace_export.py +287 -0
- abstractcore/utils/version.py +1 -1
- abstractcore/utils/vlm_token_calculator.py +655 -0
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/METADATA +207 -8
- abstractcore-2.6.0.dist-info/RECORD +108 -0
- abstractcore-2.5.2.dist-info/RECORD +0 -90
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/WHEEL +0 -0
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/entry_points.txt +0 -0
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ OpenAI provider implementation.
|
|
|
5
5
|
import os
|
|
6
6
|
import json
|
|
7
7
|
import time
|
|
8
|
-
from typing import List, Dict, Any, Optional, Union, Iterator, Type
|
|
8
|
+
from typing import List, Dict, Any, Optional, Union, Iterator, AsyncIterator, Type
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
11
|
from pydantic import BaseModel
|
|
@@ -16,7 +16,7 @@ except ImportError:
|
|
|
16
16
|
from .base import BaseProvider
|
|
17
17
|
from ..core.types import GenerateResponse
|
|
18
18
|
from ..media import MediaHandler
|
|
19
|
-
from ..exceptions import AuthenticationError, ProviderAPIError, ModelNotFoundError, format_model_error
|
|
19
|
+
from ..exceptions import AuthenticationError, ProviderAPIError, ModelNotFoundError, format_model_error, format_auth_error
|
|
20
20
|
from ..tools import UniversalToolHandler, execute_tools
|
|
21
21
|
from ..events import EventType
|
|
22
22
|
|
|
@@ -30,7 +30,8 @@ except ImportError:
|
|
|
30
30
|
class OpenAIProvider(BaseProvider):
|
|
31
31
|
"""OpenAI API provider with full integration"""
|
|
32
32
|
|
|
33
|
-
def __init__(self, model: str = "gpt-3.5-turbo", api_key: Optional[str] = None,
|
|
33
|
+
def __init__(self, model: str = "gpt-3.5-turbo", api_key: Optional[str] = None,
|
|
34
|
+
base_url: Optional[str] = None, **kwargs):
|
|
34
35
|
super().__init__(model, **kwargs)
|
|
35
36
|
self.provider = "openai"
|
|
36
37
|
|
|
@@ -42,8 +43,15 @@ class OpenAIProvider(BaseProvider):
|
|
|
42
43
|
if not self.api_key:
|
|
43
44
|
raise ValueError("OpenAI API key required. Set OPENAI_API_KEY environment variable.")
|
|
44
45
|
|
|
45
|
-
#
|
|
46
|
-
self.
|
|
46
|
+
# Get base URL from param or environment
|
|
47
|
+
self.base_url = base_url or os.getenv("OPENAI_BASE_URL")
|
|
48
|
+
|
|
49
|
+
# Initialize client with timeout and optional base_url
|
|
50
|
+
client_kwargs = {"api_key": self.api_key, "timeout": self._timeout}
|
|
51
|
+
if self.base_url:
|
|
52
|
+
client_kwargs["base_url"] = self.base_url
|
|
53
|
+
self.client = openai.OpenAI(**client_kwargs)
|
|
54
|
+
self._async_client = None # Lazy-loaded async client
|
|
47
55
|
|
|
48
56
|
# Initialize tool handler
|
|
49
57
|
self.tool_handler = UniversalToolHandler(model)
|
|
@@ -60,6 +68,16 @@ class OpenAIProvider(BaseProvider):
|
|
|
60
68
|
"""Public generate method that includes telemetry"""
|
|
61
69
|
return self.generate_with_telemetry(*args, **kwargs)
|
|
62
70
|
|
|
71
|
+
@property
|
|
72
|
+
def async_client(self):
|
|
73
|
+
"""Lazy-load AsyncOpenAI client for native async operations."""
|
|
74
|
+
if self._async_client is None:
|
|
75
|
+
client_kwargs = {"api_key": self.api_key, "timeout": self._timeout}
|
|
76
|
+
if self.base_url:
|
|
77
|
+
client_kwargs["base_url"] = self.base_url
|
|
78
|
+
self._async_client = openai.AsyncOpenAI(**client_kwargs)
|
|
79
|
+
return self._async_client
|
|
80
|
+
|
|
63
81
|
def _generate_internal(self,
|
|
64
82
|
prompt: str,
|
|
65
83
|
messages: Optional[List[Dict[str, str]]] = None,
|
|
@@ -188,6 +206,228 @@ class OpenAIProvider(BaseProvider):
|
|
|
188
206
|
# Model validation is done at initialization, so this is likely an API error
|
|
189
207
|
raise ProviderAPIError(f"OpenAI API error: {str(e)}")
|
|
190
208
|
|
|
209
|
+
async def _agenerate_internal(self,
|
|
210
|
+
prompt: str,
|
|
211
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
|
212
|
+
system_prompt: Optional[str] = None,
|
|
213
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
214
|
+
media: Optional[List['MediaContent']] = None,
|
|
215
|
+
stream: bool = False,
|
|
216
|
+
response_model: Optional[Type[BaseModel]] = None,
|
|
217
|
+
**kwargs) -> Union[GenerateResponse, AsyncIterator[GenerateResponse]]:
|
|
218
|
+
"""Native async implementation using AsyncOpenAI - 3-10x faster for batch operations."""
|
|
219
|
+
|
|
220
|
+
# Build messages array (same logic as sync)
|
|
221
|
+
api_messages = []
|
|
222
|
+
|
|
223
|
+
# Add system message if provided
|
|
224
|
+
if system_prompt:
|
|
225
|
+
api_messages.append({"role": "system", "content": system_prompt})
|
|
226
|
+
|
|
227
|
+
# Add conversation history
|
|
228
|
+
if messages:
|
|
229
|
+
for msg in messages:
|
|
230
|
+
# Skip system messages as they're handled separately
|
|
231
|
+
if msg.get("role") != "system":
|
|
232
|
+
api_messages.append({
|
|
233
|
+
"role": msg["role"],
|
|
234
|
+
"content": msg["content"]
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
# Add current prompt as user message
|
|
238
|
+
if prompt and prompt not in [msg.get("content") for msg in (messages or [])]:
|
|
239
|
+
# Handle multimodal message with media content
|
|
240
|
+
if media:
|
|
241
|
+
try:
|
|
242
|
+
from ..media.handlers import OpenAIMediaHandler
|
|
243
|
+
media_handler = OpenAIMediaHandler(self.model_capabilities)
|
|
244
|
+
|
|
245
|
+
# Create multimodal message combining text and media
|
|
246
|
+
multimodal_message = media_handler.create_multimodal_message(prompt, media)
|
|
247
|
+
api_messages.append(multimodal_message)
|
|
248
|
+
except ImportError:
|
|
249
|
+
self.logger.warning("Media processing not available. Install with: pip install abstractcore[media]")
|
|
250
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
251
|
+
except Exception as e:
|
|
252
|
+
self.logger.warning(f"Failed to process media content: {e}")
|
|
253
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
254
|
+
else:
|
|
255
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
256
|
+
|
|
257
|
+
# Prepare API call parameters using unified system (same logic as sync)
|
|
258
|
+
generation_kwargs = self._prepare_generation_kwargs(**kwargs)
|
|
259
|
+
max_output_tokens = self._get_provider_max_tokens_param(generation_kwargs)
|
|
260
|
+
|
|
261
|
+
call_params = {
|
|
262
|
+
"model": self.model,
|
|
263
|
+
"messages": api_messages,
|
|
264
|
+
"stream": stream
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# Add parameters that are supported by this model
|
|
268
|
+
if not self._is_reasoning_model():
|
|
269
|
+
# Reasoning models (o1, gpt-5) don't support many parameters
|
|
270
|
+
call_params["temperature"] = kwargs.get("temperature", self.temperature)
|
|
271
|
+
call_params["top_p"] = kwargs.get("top_p", self.top_p)
|
|
272
|
+
call_params["frequency_penalty"] = kwargs.get("frequency_penalty", self.frequency_penalty)
|
|
273
|
+
call_params["presence_penalty"] = kwargs.get("presence_penalty", self.presence_penalty)
|
|
274
|
+
|
|
275
|
+
# Add seed if provided (OpenAI supports seed for deterministic outputs)
|
|
276
|
+
seed_value = kwargs.get("seed", self.seed)
|
|
277
|
+
if seed_value is not None:
|
|
278
|
+
call_params["seed"] = seed_value
|
|
279
|
+
|
|
280
|
+
# Handle different token parameter names for different model families
|
|
281
|
+
if self._uses_max_completion_tokens():
|
|
282
|
+
call_params["max_completion_tokens"] = max_output_tokens
|
|
283
|
+
else:
|
|
284
|
+
call_params["max_tokens"] = max_output_tokens
|
|
285
|
+
|
|
286
|
+
# Add tools if provided (convert to native format)
|
|
287
|
+
if tools:
|
|
288
|
+
# Convert tools to native format for OpenAI API
|
|
289
|
+
if self.tool_handler.supports_native:
|
|
290
|
+
call_params["tools"] = self.tool_handler.prepare_tools_for_native(tools)
|
|
291
|
+
call_params["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
292
|
+
else:
|
|
293
|
+
# Fallback to manual formatting
|
|
294
|
+
call_params["tools"] = self._format_tools_for_openai(tools)
|
|
295
|
+
call_params["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
296
|
+
|
|
297
|
+
# Add structured output support (OpenAI native)
|
|
298
|
+
if response_model and PYDANTIC_AVAILABLE:
|
|
299
|
+
if self._supports_structured_output():
|
|
300
|
+
json_schema = response_model.model_json_schema()
|
|
301
|
+
|
|
302
|
+
# OpenAI requires additionalProperties: false for strict mode
|
|
303
|
+
self._ensure_strict_schema(json_schema)
|
|
304
|
+
|
|
305
|
+
call_params["response_format"] = {
|
|
306
|
+
"type": "json_schema",
|
|
307
|
+
"json_schema": {
|
|
308
|
+
"name": response_model.__name__,
|
|
309
|
+
"strict": True,
|
|
310
|
+
"schema": json_schema
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Make async API call with proper exception handling
|
|
315
|
+
try:
|
|
316
|
+
if stream:
|
|
317
|
+
return self._async_stream_response(call_params, tools)
|
|
318
|
+
else:
|
|
319
|
+
# Track generation time
|
|
320
|
+
start_time = time.time()
|
|
321
|
+
response = await self.async_client.chat.completions.create(**call_params)
|
|
322
|
+
gen_time = round((time.time() - start_time) * 1000, 1)
|
|
323
|
+
|
|
324
|
+
formatted = self._format_response(response)
|
|
325
|
+
# Add generation time to response
|
|
326
|
+
formatted.gen_time = gen_time
|
|
327
|
+
|
|
328
|
+
# Handle tool execution for OpenAI native responses
|
|
329
|
+
if tools and formatted.has_tool_calls():
|
|
330
|
+
formatted = self._handle_tool_execution(formatted, tools)
|
|
331
|
+
|
|
332
|
+
return formatted
|
|
333
|
+
except Exception as e:
|
|
334
|
+
# Model validation is done at initialization, so this is likely an API error
|
|
335
|
+
raise ProviderAPIError(f"OpenAI API error: {str(e)}")
|
|
336
|
+
|
|
337
|
+
async def _async_stream_response(self, call_params: Dict[str, Any], tools: Optional[List[Dict[str, Any]]] = None) -> AsyncIterator[GenerateResponse]:
|
|
338
|
+
"""Native async streaming responses from OpenAI."""
|
|
339
|
+
try:
|
|
340
|
+
stream = await self.async_client.chat.completions.create(**call_params)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
# Model validation is done at initialization, so this is likely an API error
|
|
343
|
+
raise ProviderAPIError(f"OpenAI API error: {str(e)}")
|
|
344
|
+
|
|
345
|
+
# For streaming with tools, we need to collect the complete response
|
|
346
|
+
collected_content = ""
|
|
347
|
+
collected_tool_calls = {} # Use dict to merge streaming chunks by tool call ID
|
|
348
|
+
final_response = None
|
|
349
|
+
|
|
350
|
+
async for chunk in stream:
|
|
351
|
+
choice = chunk.choices[0] if chunk.choices else None
|
|
352
|
+
if not choice:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
delta = choice.delta
|
|
356
|
+
content = getattr(delta, 'content', None) or ""
|
|
357
|
+
collected_content += content
|
|
358
|
+
|
|
359
|
+
# Handle tool calls in streaming - merge incomplete chunks
|
|
360
|
+
if hasattr(delta, 'tool_calls') and delta.tool_calls:
|
|
361
|
+
for tc in delta.tool_calls:
|
|
362
|
+
tc_id = getattr(tc, 'id', None) or getattr(tc, 'index', 0)
|
|
363
|
+
|
|
364
|
+
# Initialize or get existing tool call
|
|
365
|
+
if tc_id not in collected_tool_calls:
|
|
366
|
+
collected_tool_calls[tc_id] = {
|
|
367
|
+
"id": getattr(tc, 'id', None),
|
|
368
|
+
"type": getattr(tc, 'type', 'function'),
|
|
369
|
+
"name": None,
|
|
370
|
+
"arguments": ""
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# Update with new data from this chunk
|
|
374
|
+
if hasattr(tc, 'function'):
|
|
375
|
+
if hasattr(tc.function, 'name') and tc.function.name:
|
|
376
|
+
collected_tool_calls[tc_id]["name"] = tc.function.name
|
|
377
|
+
if hasattr(tc.function, 'arguments') and tc.function.arguments:
|
|
378
|
+
collected_tool_calls[tc_id]["arguments"] += tc.function.arguments
|
|
379
|
+
|
|
380
|
+
# Create chunk response
|
|
381
|
+
chunk_response = GenerateResponse(
|
|
382
|
+
content=content,
|
|
383
|
+
raw_response=chunk,
|
|
384
|
+
model=chunk.model,
|
|
385
|
+
finish_reason=choice.finish_reason,
|
|
386
|
+
tool_calls=None # Don't include incomplete tool calls in chunks
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# If this is the final chunk and we have tools, handle tool execution
|
|
390
|
+
if choice.finish_reason and tools and collected_tool_calls:
|
|
391
|
+
# Convert dict to list and filter out incomplete tool calls
|
|
392
|
+
complete_tool_calls = []
|
|
393
|
+
for tc in collected_tool_calls.values():
|
|
394
|
+
if tc["name"] and tc["arguments"] is not None: # Include tool calls with empty args
|
|
395
|
+
complete_tool_calls.append(tc)
|
|
396
|
+
|
|
397
|
+
# Create complete response for tool processing
|
|
398
|
+
complete_response = GenerateResponse(
|
|
399
|
+
content=collected_content,
|
|
400
|
+
raw_response=chunk,
|
|
401
|
+
model=chunk.model,
|
|
402
|
+
finish_reason=choice.finish_reason,
|
|
403
|
+
tool_calls=complete_tool_calls if complete_tool_calls else None
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Handle tool execution
|
|
407
|
+
final_response = self._handle_tool_execution(complete_response, tools)
|
|
408
|
+
|
|
409
|
+
# If tools were executed, yield the tool results as final chunk
|
|
410
|
+
if final_response.content != collected_content:
|
|
411
|
+
tool_results_content = final_response.content[len(collected_content):]
|
|
412
|
+
yield GenerateResponse(
|
|
413
|
+
content=tool_results_content,
|
|
414
|
+
raw_response=chunk,
|
|
415
|
+
model=chunk.model,
|
|
416
|
+
finish_reason=choice.finish_reason,
|
|
417
|
+
tool_calls=None
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
# No tools executed but response was processed - yield final response content
|
|
421
|
+
yield GenerateResponse(
|
|
422
|
+
content=final_response.content,
|
|
423
|
+
raw_response=chunk,
|
|
424
|
+
model=chunk.model,
|
|
425
|
+
finish_reason=choice.finish_reason,
|
|
426
|
+
tool_calls=complete_tool_calls if complete_tool_calls else None
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
yield chunk_response
|
|
430
|
+
|
|
191
431
|
def _format_tools_for_openai(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
192
432
|
"""Format tools for OpenAI API format"""
|
|
193
433
|
formatted_tools = []
|
|
@@ -391,6 +631,18 @@ class OpenAIProvider(BaseProvider):
|
|
|
391
631
|
return False
|
|
392
632
|
return True
|
|
393
633
|
|
|
634
|
+
def unload(self) -> None:
|
|
635
|
+
"""Close async client if it was created."""
|
|
636
|
+
if self._async_client is not None:
|
|
637
|
+
import asyncio
|
|
638
|
+
try:
|
|
639
|
+
loop = asyncio.get_running_loop()
|
|
640
|
+
loop.create_task(self._async_client.close())
|
|
641
|
+
except RuntimeError:
|
|
642
|
+
# No running loop, close synchronously
|
|
643
|
+
import asyncio
|
|
644
|
+
asyncio.run(self._async_client.close())
|
|
645
|
+
|
|
394
646
|
def _validate_model_exists(self):
|
|
395
647
|
"""Preflight check to validate model exists before any generation"""
|
|
396
648
|
try:
|
|
@@ -410,7 +662,7 @@ class OpenAIProvider(BaseProvider):
|
|
|
410
662
|
# For other errors (like API failures), handle gracefully
|
|
411
663
|
error_str = str(e).lower()
|
|
412
664
|
if 'api_key' in error_str or 'authentication' in error_str:
|
|
413
|
-
raise AuthenticationError(
|
|
665
|
+
raise AuthenticationError(format_auth_error("openai", str(e)))
|
|
414
666
|
# For other API errors during preflight, continue (model might work)
|
|
415
667
|
# This allows for cases where models.list() fails but generation works
|
|
416
668
|
|
|
@@ -511,9 +763,21 @@ class OpenAIProvider(BaseProvider):
|
|
|
511
763
|
|
|
512
764
|
@classmethod
|
|
513
765
|
def list_available_models(cls, **kwargs) -> List[str]:
|
|
514
|
-
"""
|
|
766
|
+
"""
|
|
767
|
+
List available models from OpenAI API.
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
**kwargs: Optional parameters including:
|
|
771
|
+
- api_key: OpenAI API key
|
|
772
|
+
- input_capabilities: List of ModelInputCapability enums to filter by input capability
|
|
773
|
+
- output_capabilities: List of ModelOutputCapability enums to filter by output capability
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
List of model names, optionally filtered by capabilities
|
|
777
|
+
"""
|
|
515
778
|
try:
|
|
516
779
|
import openai
|
|
780
|
+
from .model_capabilities import filter_models_by_capabilities
|
|
517
781
|
|
|
518
782
|
# Get API key from kwargs or environment
|
|
519
783
|
api_key = kwargs.get('api_key') or os.getenv("OPENAI_API_KEY")
|
|
@@ -542,7 +806,21 @@ class OpenAIProvider(BaseProvider):
|
|
|
542
806
|
]):
|
|
543
807
|
chat_models.append(model_id)
|
|
544
808
|
|
|
545
|
-
|
|
809
|
+
chat_models = sorted(chat_models, reverse=True) # Latest models first
|
|
810
|
+
|
|
811
|
+
# Apply new capability filtering if provided
|
|
812
|
+
input_capabilities = kwargs.get('input_capabilities')
|
|
813
|
+
output_capabilities = kwargs.get('output_capabilities')
|
|
814
|
+
|
|
815
|
+
if input_capabilities or output_capabilities:
|
|
816
|
+
chat_models = filter_models_by_capabilities(
|
|
817
|
+
chat_models,
|
|
818
|
+
input_capabilities=input_capabilities,
|
|
819
|
+
output_capabilities=output_capabilities
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
return chat_models
|
|
546
824
|
|
|
547
825
|
except Exception:
|
|
548
826
|
return []
|
|
@@ -202,10 +202,14 @@ class ProviderRegistry:
|
|
|
202
202
|
|
|
203
203
|
Args:
|
|
204
204
|
provider_name: Name of the provider
|
|
205
|
-
**kwargs: Provider-specific parameters
|
|
205
|
+
**kwargs: Provider-specific parameters including:
|
|
206
|
+
- api_key: API key for authentication (if required)
|
|
207
|
+
- base_url: Base URL for API endpoint (if applicable)
|
|
208
|
+
- input_capabilities: List of ModelInputCapability enums to filter by input capability
|
|
209
|
+
- output_capabilities: List of ModelOutputCapability enums to filter by output capability
|
|
206
210
|
|
|
207
211
|
Returns:
|
|
208
|
-
List of available model names
|
|
212
|
+
List of available model names, optionally filtered by capabilities
|
|
209
213
|
"""
|
|
210
214
|
try:
|
|
211
215
|
provider_class = self.get_provider_class(provider_name)
|
|
@@ -285,13 +289,64 @@ class ProviderRegistry:
|
|
|
285
289
|
for provider_name in self.list_provider_names()
|
|
286
290
|
]
|
|
287
291
|
|
|
288
|
-
def get_providers_with_models(self) -> List[Dict[str, Any]]:
|
|
289
|
-
"""
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
def get_providers_with_models(self, include_models: bool = True) -> List[Dict[str, Any]]:
|
|
293
|
+
"""
|
|
294
|
+
Get only providers that have available models.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
include_models: If True, include actual model lists (slower).
|
|
298
|
+
If False, return metadata only (much faster). Default: True.
|
|
299
|
+
"""
|
|
300
|
+
if include_models:
|
|
301
|
+
# Original behavior - get full status including model lists
|
|
302
|
+
all_providers = self.get_all_providers_status()
|
|
303
|
+
return [
|
|
304
|
+
provider for provider in all_providers
|
|
305
|
+
if provider.get("status") == "available" and provider.get("model_count", 0) > 0
|
|
306
|
+
]
|
|
307
|
+
else:
|
|
308
|
+
# Fast path - get all provider metadata without model enumeration
|
|
309
|
+
# Note: We return all providers since we can't quickly determine which have models
|
|
310
|
+
return self.get_providers_metadata_only()
|
|
311
|
+
|
|
312
|
+
def get_providers_metadata_only(self) -> List[Dict[str, Any]]:
|
|
313
|
+
"""
|
|
314
|
+
Get provider metadata without enumerating models (fast path).
|
|
315
|
+
|
|
316
|
+
This method returns provider information without making API calls
|
|
317
|
+
or scanning for models, making it extremely fast for UI discovery.
|
|
318
|
+
"""
|
|
319
|
+
providers_metadata = []
|
|
320
|
+
|
|
321
|
+
for provider_name in self.list_provider_names():
|
|
322
|
+
provider_info = self.get_provider_info(provider_name)
|
|
323
|
+
if not provider_info:
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
# Basic availability check without model enumeration
|
|
327
|
+
try:
|
|
328
|
+
provider_class = self.get_provider_class(provider_name)
|
|
329
|
+
status = "available" # Assume available if class can be imported
|
|
330
|
+
except Exception:
|
|
331
|
+
status = "error"
|
|
332
|
+
|
|
333
|
+
metadata = {
|
|
334
|
+
"name": provider_info.name,
|
|
335
|
+
"display_name": provider_info.display_name,
|
|
336
|
+
"type": provider_info.provider_type,
|
|
337
|
+
"model_count": "unknown", # Don't enumerate models
|
|
338
|
+
"status": status,
|
|
339
|
+
"description": provider_info.description,
|
|
340
|
+
"local_provider": provider_info.local_provider,
|
|
341
|
+
"authentication_required": provider_info.authentication_required,
|
|
342
|
+
"supported_features": provider_info.supported_features,
|
|
343
|
+
"installation_extras": provider_info.installation_extras,
|
|
344
|
+
"models": [] # Empty list for fast response
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
providers_metadata.append(metadata)
|
|
348
|
+
|
|
349
|
+
return providers_metadata
|
|
295
350
|
|
|
296
351
|
def create_provider_instance(self, provider_name: str, model: Optional[str] = None, **kwargs):
|
|
297
352
|
"""
|
|
@@ -348,7 +403,7 @@ def is_provider_available(provider_name: str) -> bool:
|
|
|
348
403
|
return get_provider_registry().is_provider_available(provider_name)
|
|
349
404
|
|
|
350
405
|
|
|
351
|
-
def get_all_providers_with_models() -> List[Dict[str, Any]]:
|
|
406
|
+
def get_all_providers_with_models(include_models: bool = True) -> List[Dict[str, Any]]:
|
|
352
407
|
"""
|
|
353
408
|
Get comprehensive information about all providers with available models.
|
|
354
409
|
|
|
@@ -356,14 +411,18 @@ def get_all_providers_with_models() -> List[Dict[str, Any]]:
|
|
|
356
411
|
for provider discovery and information. It replaces the manual provider
|
|
357
412
|
lists in factory.py and server/app.py.
|
|
358
413
|
|
|
414
|
+
Args:
|
|
415
|
+
include_models: If True, include actual model lists (slower).
|
|
416
|
+
If False, return metadata only (much faster). Default: True.
|
|
417
|
+
|
|
359
418
|
Returns:
|
|
360
419
|
List of provider dictionaries with comprehensive metadata including:
|
|
361
420
|
- name, display_name, type, description
|
|
362
421
|
- model_count, status, supported_features
|
|
363
422
|
- local_provider, authentication_required
|
|
364
|
-
- installation_extras, sample models
|
|
423
|
+
- installation_extras, sample models (if include_models=True)
|
|
365
424
|
"""
|
|
366
|
-
return get_provider_registry().get_providers_with_models()
|
|
425
|
+
return get_provider_registry().get_providers_with_models(include_models=include_models)
|
|
367
426
|
|
|
368
427
|
|
|
369
428
|
def get_all_providers_status() -> List[Dict[str, Any]]:
|
|
@@ -386,5 +445,18 @@ def create_provider(provider_name: str, model: Optional[str] = None, **kwargs):
|
|
|
386
445
|
|
|
387
446
|
|
|
388
447
|
def get_available_models_for_provider(provider_name: str, **kwargs) -> List[str]:
|
|
389
|
-
"""
|
|
448
|
+
"""
|
|
449
|
+
Get available models for a specific provider.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
provider_name: Name of the provider
|
|
453
|
+
**kwargs: Provider-specific parameters including:
|
|
454
|
+
- api_key: API key for authentication (if required)
|
|
455
|
+
- base_url: Base URL for API endpoint (if applicable)
|
|
456
|
+
- input_capabilities: List of ModelInputCapability enums to filter by input capability
|
|
457
|
+
- output_capabilities: List of ModelOutputCapability enums to filter by output capability
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
List of available model names, optionally filtered by capabilities
|
|
461
|
+
"""
|
|
390
462
|
return get_provider_registry().get_available_models(provider_name, **kwargs)
|
|
@@ -7,15 +7,15 @@ while maintaining real-time streaming performance, with proper tag rewriting sup
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import re
|
|
10
|
-
import logging
|
|
11
10
|
import uuid
|
|
12
11
|
from typing import List, Dict, Any, Optional, Iterator, Tuple
|
|
13
12
|
from enum import Enum
|
|
14
13
|
|
|
15
14
|
from ..core.types import GenerateResponse
|
|
16
15
|
from ..tools.core import ToolCall
|
|
16
|
+
from ..utils.structured_logging import get_logger
|
|
17
17
|
|
|
18
|
-
logger =
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class ToolDetectionState(Enum):
|