proxilion 0.0.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.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider-agnostic tool call interface.
|
|
3
|
+
|
|
4
|
+
Provides a unified representation of tool calls and responses
|
|
5
|
+
across different LLM providers (OpenAI, Anthropic, Google Gemini).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import uuid
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Provider(Enum):
|
|
23
|
+
"""Supported LLM providers."""
|
|
24
|
+
|
|
25
|
+
OPENAI = "openai"
|
|
26
|
+
ANTHROPIC = "anthropic"
|
|
27
|
+
GEMINI = "gemini"
|
|
28
|
+
BEDROCK = "bedrock"
|
|
29
|
+
OLLAMA = "ollama"
|
|
30
|
+
UNKNOWN = "unknown"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class UnifiedToolCall:
|
|
35
|
+
"""
|
|
36
|
+
Provider-agnostic tool call representation.
|
|
37
|
+
|
|
38
|
+
Normalizes tool calls from different providers into a common format
|
|
39
|
+
for authorization and execution.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
id: Unique identifier for the tool call.
|
|
43
|
+
name: Name of the tool being called.
|
|
44
|
+
arguments: Dictionary of arguments passed to the tool.
|
|
45
|
+
provider: The provider that generated this call.
|
|
46
|
+
raw: Original provider-specific object (for debugging).
|
|
47
|
+
timestamp: When the tool call was extracted.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> # From OpenAI response
|
|
51
|
+
>>> call = UnifiedToolCall.from_openai(response.choices[0].message.tool_calls[0])
|
|
52
|
+
>>> print(f"Tool: {call.name}, Args: {call.arguments}")
|
|
53
|
+
>>>
|
|
54
|
+
>>> # From Anthropic response
|
|
55
|
+
>>> call = UnifiedToolCall.from_anthropic(tool_use_block)
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
id: str
|
|
59
|
+
name: str
|
|
60
|
+
arguments: dict[str, Any]
|
|
61
|
+
provider: Provider = Provider.UNKNOWN
|
|
62
|
+
raw: Any = None
|
|
63
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_openai(cls, tool_call: Any) -> UnifiedToolCall:
|
|
67
|
+
"""
|
|
68
|
+
Create from OpenAI tool call.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
tool_call: OpenAI ChatCompletionMessageToolCall object.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
UnifiedToolCall instance.
|
|
75
|
+
"""
|
|
76
|
+
# Handle both dict and object forms
|
|
77
|
+
if isinstance(tool_call, dict):
|
|
78
|
+
call_id = tool_call.get("id", str(uuid.uuid4()))
|
|
79
|
+
function = tool_call.get("function", {})
|
|
80
|
+
name = function.get("name", "unknown")
|
|
81
|
+
arguments_str = function.get("arguments", "{}")
|
|
82
|
+
else:
|
|
83
|
+
call_id = getattr(tool_call, "id", str(uuid.uuid4()))
|
|
84
|
+
function = getattr(tool_call, "function", None)
|
|
85
|
+
if function:
|
|
86
|
+
name = getattr(function, "name", "unknown")
|
|
87
|
+
arguments_str = getattr(function, "arguments", "{}")
|
|
88
|
+
else:
|
|
89
|
+
name = "unknown"
|
|
90
|
+
arguments_str = "{}"
|
|
91
|
+
|
|
92
|
+
# Parse arguments JSON
|
|
93
|
+
try:
|
|
94
|
+
if isinstance(arguments_str, str):
|
|
95
|
+
arguments = json.loads(arguments_str)
|
|
96
|
+
else:
|
|
97
|
+
arguments = arguments_str or {}
|
|
98
|
+
except json.JSONDecodeError:
|
|
99
|
+
logger.warning(f"Failed to parse tool call arguments: {arguments_str}")
|
|
100
|
+
arguments = {}
|
|
101
|
+
|
|
102
|
+
return cls(
|
|
103
|
+
id=call_id,
|
|
104
|
+
name=name,
|
|
105
|
+
arguments=arguments,
|
|
106
|
+
provider=Provider.OPENAI,
|
|
107
|
+
raw=tool_call,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_anthropic(cls, tool_use: Any) -> UnifiedToolCall:
|
|
112
|
+
"""
|
|
113
|
+
Create from Anthropic tool use block.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
tool_use: Anthropic ToolUseBlock object.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
UnifiedToolCall instance.
|
|
120
|
+
"""
|
|
121
|
+
# Handle both dict and object forms
|
|
122
|
+
if isinstance(tool_use, dict):
|
|
123
|
+
call_id = tool_use.get("id", str(uuid.uuid4()))
|
|
124
|
+
name = tool_use.get("name", "unknown")
|
|
125
|
+
arguments = tool_use.get("input", {})
|
|
126
|
+
else:
|
|
127
|
+
call_id = getattr(tool_use, "id", str(uuid.uuid4()))
|
|
128
|
+
name = getattr(tool_use, "name", "unknown")
|
|
129
|
+
arguments = getattr(tool_use, "input", {})
|
|
130
|
+
|
|
131
|
+
return cls(
|
|
132
|
+
id=call_id,
|
|
133
|
+
name=name,
|
|
134
|
+
arguments=arguments if isinstance(arguments, dict) else {},
|
|
135
|
+
provider=Provider.ANTHROPIC,
|
|
136
|
+
raw=tool_use,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_gemini(cls, function_call: Any) -> UnifiedToolCall:
|
|
141
|
+
"""
|
|
142
|
+
Create from Gemini function call.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
function_call: Gemini FunctionCall object.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
UnifiedToolCall instance.
|
|
149
|
+
"""
|
|
150
|
+
# Handle both dict and object forms
|
|
151
|
+
if isinstance(function_call, dict):
|
|
152
|
+
name = function_call.get("name", "unknown")
|
|
153
|
+
args = function_call.get("args", {})
|
|
154
|
+
else:
|
|
155
|
+
name = getattr(function_call, "name", "unknown")
|
|
156
|
+
# Gemini args can be a Struct protobuf object
|
|
157
|
+
args_raw = getattr(function_call, "args", {})
|
|
158
|
+
if hasattr(args_raw, "items"):
|
|
159
|
+
args = dict(args_raw.items())
|
|
160
|
+
elif isinstance(args_raw, dict):
|
|
161
|
+
args = args_raw
|
|
162
|
+
else:
|
|
163
|
+
# Try to convert Struct to dict
|
|
164
|
+
try:
|
|
165
|
+
args = dict(args_raw)
|
|
166
|
+
except (TypeError, ValueError):
|
|
167
|
+
args = {}
|
|
168
|
+
|
|
169
|
+
# Gemini doesn't provide call IDs, generate one
|
|
170
|
+
return cls(
|
|
171
|
+
id=str(uuid.uuid4()),
|
|
172
|
+
name=name,
|
|
173
|
+
arguments=args,
|
|
174
|
+
provider=Provider.GEMINI,
|
|
175
|
+
raw=function_call,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_dict(cls, data: dict[str, Any]) -> UnifiedToolCall:
|
|
180
|
+
"""
|
|
181
|
+
Create from dictionary.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
data: Dictionary with tool call data.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
UnifiedToolCall instance.
|
|
188
|
+
"""
|
|
189
|
+
return cls(
|
|
190
|
+
id=data.get("id", str(uuid.uuid4())),
|
|
191
|
+
name=data.get("name", "unknown"),
|
|
192
|
+
arguments=data.get("arguments", {}),
|
|
193
|
+
provider=Provider(data.get("provider", "unknown")),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def to_dict(self) -> dict[str, Any]:
|
|
197
|
+
"""Convert to dictionary representation."""
|
|
198
|
+
return {
|
|
199
|
+
"id": self.id,
|
|
200
|
+
"name": self.name,
|
|
201
|
+
"arguments": self.arguments,
|
|
202
|
+
"provider": self.provider.value,
|
|
203
|
+
"timestamp": self.timestamp.isoformat(),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class UnifiedToolResult:
|
|
209
|
+
"""
|
|
210
|
+
Provider-agnostic tool result representation.
|
|
211
|
+
|
|
212
|
+
Attributes:
|
|
213
|
+
tool_call_id: ID of the corresponding tool call.
|
|
214
|
+
result: The result value.
|
|
215
|
+
is_error: Whether the result is an error.
|
|
216
|
+
error_message: Error message if is_error is True.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
tool_call_id: str
|
|
220
|
+
result: Any
|
|
221
|
+
is_error: bool = False
|
|
222
|
+
error_message: str | None = None
|
|
223
|
+
|
|
224
|
+
def to_dict(self) -> dict[str, Any]:
|
|
225
|
+
"""Convert to dictionary."""
|
|
226
|
+
return {
|
|
227
|
+
"tool_call_id": self.tool_call_id,
|
|
228
|
+
"result": self.result,
|
|
229
|
+
"is_error": self.is_error,
|
|
230
|
+
"error_message": self.error_message,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class UnifiedResponse:
|
|
236
|
+
"""
|
|
237
|
+
Provider-agnostic response representation.
|
|
238
|
+
|
|
239
|
+
Attributes:
|
|
240
|
+
content: Text content of the response.
|
|
241
|
+
tool_calls: List of tool calls in the response.
|
|
242
|
+
finish_reason: Why the response ended.
|
|
243
|
+
provider: The provider that generated this response.
|
|
244
|
+
usage: Token usage information.
|
|
245
|
+
raw: Original provider-specific response.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
content: str | None = None
|
|
249
|
+
tool_calls: list[UnifiedToolCall] = field(default_factory=list)
|
|
250
|
+
finish_reason: str | None = None
|
|
251
|
+
provider: Provider = Provider.UNKNOWN
|
|
252
|
+
usage: dict[str, int] = field(default_factory=dict)
|
|
253
|
+
raw: Any = None
|
|
254
|
+
|
|
255
|
+
def has_tool_calls(self) -> bool:
|
|
256
|
+
"""Check if response contains tool calls."""
|
|
257
|
+
return len(self.tool_calls) > 0
|
|
258
|
+
|
|
259
|
+
def to_dict(self) -> dict[str, Any]:
|
|
260
|
+
"""Convert to dictionary."""
|
|
261
|
+
return {
|
|
262
|
+
"content": self.content,
|
|
263
|
+
"tool_calls": [tc.to_dict() for tc in self.tool_calls],
|
|
264
|
+
"finish_reason": self.finish_reason,
|
|
265
|
+
"provider": self.provider.value,
|
|
266
|
+
"usage": self.usage,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@runtime_checkable
|
|
271
|
+
class ProviderAdapter(Protocol):
|
|
272
|
+
"""
|
|
273
|
+
Protocol for provider adapters.
|
|
274
|
+
|
|
275
|
+
Each adapter translates between provider-specific formats
|
|
276
|
+
and the unified format used by Proxilion.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def provider(self) -> Provider:
|
|
281
|
+
"""Get the provider type."""
|
|
282
|
+
...
|
|
283
|
+
|
|
284
|
+
def extract_tool_calls(self, response: Any) -> list[UnifiedToolCall]:
|
|
285
|
+
"""
|
|
286
|
+
Extract tool calls from a provider response.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
response: Provider-specific response object.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
List of unified tool calls.
|
|
293
|
+
"""
|
|
294
|
+
...
|
|
295
|
+
|
|
296
|
+
def extract_response(self, response: Any) -> UnifiedResponse:
|
|
297
|
+
"""
|
|
298
|
+
Extract full response including content and tool calls.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
response: Provider-specific response object.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
UnifiedResponse instance.
|
|
305
|
+
"""
|
|
306
|
+
...
|
|
307
|
+
|
|
308
|
+
def format_tool_result(
|
|
309
|
+
self,
|
|
310
|
+
tool_call: UnifiedToolCall,
|
|
311
|
+
result: Any,
|
|
312
|
+
is_error: bool = False,
|
|
313
|
+
) -> Any:
|
|
314
|
+
"""
|
|
315
|
+
Format tool result for the provider.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
tool_call: The original tool call.
|
|
319
|
+
result: The result to format.
|
|
320
|
+
is_error: Whether the result is an error.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Provider-specific message format.
|
|
324
|
+
"""
|
|
325
|
+
...
|
|
326
|
+
|
|
327
|
+
def format_tools(
|
|
328
|
+
self,
|
|
329
|
+
tools: list[Any], # list[ToolDefinition]
|
|
330
|
+
) -> list[dict[str, Any]]:
|
|
331
|
+
"""
|
|
332
|
+
Format tool definitions for the provider.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
tools: List of ToolDefinition objects.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
List of provider-specific tool definitions.
|
|
339
|
+
"""
|
|
340
|
+
...
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class BaseAdapter(ABC):
|
|
344
|
+
"""
|
|
345
|
+
Base class for provider adapters.
|
|
346
|
+
|
|
347
|
+
Provides common functionality and enforces the adapter interface.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
@abstractmethod
|
|
352
|
+
def provider(self) -> Provider:
|
|
353
|
+
"""Get the provider type."""
|
|
354
|
+
...
|
|
355
|
+
|
|
356
|
+
@abstractmethod
|
|
357
|
+
def extract_tool_calls(self, response: Any) -> list[UnifiedToolCall]:
|
|
358
|
+
"""Extract tool calls from response."""
|
|
359
|
+
...
|
|
360
|
+
|
|
361
|
+
@abstractmethod
|
|
362
|
+
def extract_response(self, response: Any) -> UnifiedResponse:
|
|
363
|
+
"""Extract full response."""
|
|
364
|
+
...
|
|
365
|
+
|
|
366
|
+
@abstractmethod
|
|
367
|
+
def format_tool_result(
|
|
368
|
+
self,
|
|
369
|
+
tool_call: UnifiedToolCall,
|
|
370
|
+
result: Any,
|
|
371
|
+
is_error: bool = False,
|
|
372
|
+
) -> Any:
|
|
373
|
+
"""Format tool result for provider."""
|
|
374
|
+
...
|
|
375
|
+
|
|
376
|
+
@abstractmethod
|
|
377
|
+
def format_tools(
|
|
378
|
+
self,
|
|
379
|
+
tools: list[Any],
|
|
380
|
+
) -> list[dict[str, Any]]:
|
|
381
|
+
"""Format tool definitions for provider."""
|
|
382
|
+
...
|
|
383
|
+
|
|
384
|
+
def _serialize_result(self, result: Any) -> str:
|
|
385
|
+
"""Serialize a result to string format."""
|
|
386
|
+
if isinstance(result, str):
|
|
387
|
+
return result
|
|
388
|
+
try:
|
|
389
|
+
return json.dumps(result, default=str)
|
|
390
|
+
except (TypeError, ValueError):
|
|
391
|
+
return str(result)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def detect_provider(response: Any) -> Provider:
|
|
395
|
+
"""
|
|
396
|
+
Auto-detect provider from response object.
|
|
397
|
+
|
|
398
|
+
Examines the response type and module to determine
|
|
399
|
+
which provider generated it.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
response: Provider response object.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Detected Provider enum value.
|
|
406
|
+
|
|
407
|
+
Raises:
|
|
408
|
+
ValueError: If provider cannot be detected.
|
|
409
|
+
|
|
410
|
+
Example:
|
|
411
|
+
>>> from openai import OpenAI
|
|
412
|
+
>>> response = client.chat.completions.create(...)
|
|
413
|
+
>>> provider = detect_provider(response)
|
|
414
|
+
>>> assert provider == Provider.OPENAI
|
|
415
|
+
"""
|
|
416
|
+
type_name = type(response).__name__
|
|
417
|
+
module = type(response).__module__
|
|
418
|
+
|
|
419
|
+
# Check module name
|
|
420
|
+
module_lower = module.lower()
|
|
421
|
+
|
|
422
|
+
if "openai" in module_lower:
|
|
423
|
+
return Provider.OPENAI
|
|
424
|
+
elif "anthropic" in module_lower:
|
|
425
|
+
return Provider.ANTHROPIC
|
|
426
|
+
elif (
|
|
427
|
+
"vertexai" in module_lower
|
|
428
|
+
or "google.generativeai" in module_lower
|
|
429
|
+
or ("google" in module_lower and "aiplatform" in module_lower)
|
|
430
|
+
):
|
|
431
|
+
return Provider.GEMINI
|
|
432
|
+
|
|
433
|
+
# Check type name patterns
|
|
434
|
+
if "ChatCompletion" in type_name:
|
|
435
|
+
return Provider.OPENAI
|
|
436
|
+
elif "Message" in type_name and hasattr(response, "content"):
|
|
437
|
+
# Anthropic messages have content attribute that's a list
|
|
438
|
+
content = getattr(response, "content", None)
|
|
439
|
+
if isinstance(content, list):
|
|
440
|
+
return Provider.ANTHROPIC
|
|
441
|
+
elif "GenerateContentResponse" in type_name or "GenerationResponse" in type_name:
|
|
442
|
+
return Provider.GEMINI
|
|
443
|
+
|
|
444
|
+
# Check for specific attributes
|
|
445
|
+
if hasattr(response, "choices") and hasattr(response, "model"):
|
|
446
|
+
return Provider.OPENAI
|
|
447
|
+
elif hasattr(response, "candidates"):
|
|
448
|
+
return Provider.GEMINI
|
|
449
|
+
elif hasattr(response, "stop_reason"):
|
|
450
|
+
return Provider.ANTHROPIC
|
|
451
|
+
|
|
452
|
+
raise ValueError(f"Unknown provider for response type: {module}.{type_name}")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def detect_provider_safe(response: Any) -> Provider:
|
|
456
|
+
"""
|
|
457
|
+
Safely detect provider, returning UNKNOWN if detection fails.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
response: Provider response object.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Detected Provider or Provider.UNKNOWN.
|
|
464
|
+
"""
|
|
465
|
+
try:
|
|
466
|
+
return detect_provider(response)
|
|
467
|
+
except ValueError:
|
|
468
|
+
return Provider.UNKNOWN
|