feat(llm): añade AzureOpenAIProvider y OpenAIProvider con retry exponencial
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
"""Proveedor Azure OpenAI con retry exponencial y fallback configurable."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from openai import AsyncAzureOpenAI
|
||||||
|
|
||||||
|
from agentforge_core.llm.base import CompletionResult, Message
|
||||||
|
|
||||||
|
|
||||||
|
class AzureOpenAIProvider:
|
||||||
|
"""Cliente Azure OpenAI con retry 3x backoff exponencial."""
|
||||||
|
|
||||||
|
name = "azure"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
endpoint: str,
|
||||||
|
api_key: str,
|
||||||
|
deployment: str,
|
||||||
|
api_version: str,
|
||||||
|
) -> None:
|
||||||
|
if not (endpoint and api_key and deployment):
|
||||||
|
raise ValueError("Azure OpenAI requiere endpoint, api_key y deployment.")
|
||||||
|
self._client = AsyncAzureOpenAI(
|
||||||
|
azure_endpoint=endpoint,
|
||||||
|
api_key=api_key,
|
||||||
|
api_version=api_version,
|
||||||
|
)
|
||||||
|
self._deployment = deployment
|
||||||
|
|
||||||
|
async def complete(
|
||||||
|
self,
|
||||||
|
messages: list[Message],
|
||||||
|
schema: dict[str, Any] | None = None,
|
||||||
|
temperature: float = 0.2,
|
||||||
|
max_tokens: int = 2000,
|
||||||
|
) -> CompletionResult:
|
||||||
|
delays = [1.0, 2.0, 4.0]
|
||||||
|
last_exc: Exception | None = None
|
||||||
|
for attempt, delay in enumerate([0.0, *delays]):
|
||||||
|
if delay:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
try:
|
||||||
|
return await self._call_once(messages, temperature, max_tokens)
|
||||||
|
except Exception as exc:
|
||||||
|
last_exc = exc
|
||||||
|
if attempt >= len(delays):
|
||||||
|
break
|
||||||
|
assert last_exc is not None
|
||||||
|
raise last_exc
|
||||||
|
|
||||||
|
async def _call_once(
|
||||||
|
self,
|
||||||
|
messages: list[Message],
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> CompletionResult:
|
||||||
|
start = time.perf_counter()
|
||||||
|
resp = await self._client.chat.completions.create( # type: ignore[call-overload]
|
||||||
|
model=self._deployment,
|
||||||
|
messages=[m.model_dump() for m in messages],
|
||||||
|
temperature=temperature,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
response_format={"type": "json_object"},
|
||||||
|
)
|
||||||
|
elapsed = int((time.perf_counter() - start) * 1000)
|
||||||
|
choice = resp.choices[0]
|
||||||
|
return CompletionResult(
|
||||||
|
content=choice.message.content or "",
|
||||||
|
model=resp.model,
|
||||||
|
tokens_in=resp.usage.prompt_tokens if resp.usage else 0,
|
||||||
|
tokens_out=resp.usage.completion_tokens if resp.usage else 0,
|
||||||
|
latency_ms=elapsed,
|
||||||
|
)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Proveedor OpenAI directo (fallback)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from openai import AsyncOpenAI
|
||||||
|
|
||||||
|
from agentforge_core.llm.base import CompletionResult, Message
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIProvider:
|
||||||
|
"""Cliente OpenAI con misma política de retry que Azure."""
|
||||||
|
|
||||||
|
name = "openai"
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, model: str = "gpt-4o") -> None:
|
||||||
|
if not api_key:
|
||||||
|
raise ValueError("OpenAIProvider requiere api_key.")
|
||||||
|
self._client = AsyncOpenAI(api_key=api_key)
|
||||||
|
self._model = model
|
||||||
|
|
||||||
|
async def complete(
|
||||||
|
self,
|
||||||
|
messages: list[Message],
|
||||||
|
schema: dict[str, Any] | None = None,
|
||||||
|
temperature: float = 0.2,
|
||||||
|
max_tokens: int = 2000,
|
||||||
|
) -> CompletionResult:
|
||||||
|
delays = [1.0, 2.0, 4.0]
|
||||||
|
last_exc: Exception | None = None
|
||||||
|
for attempt, delay in enumerate([0.0, *delays]):
|
||||||
|
if delay:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
try:
|
||||||
|
start = time.perf_counter()
|
||||||
|
resp = await self._client.chat.completions.create( # type: ignore[call-overload]
|
||||||
|
model=self._model,
|
||||||
|
messages=[m.model_dump() for m in messages],
|
||||||
|
temperature=temperature,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
response_format={"type": "json_object"},
|
||||||
|
)
|
||||||
|
elapsed = int((time.perf_counter() - start) * 1000)
|
||||||
|
choice = resp.choices[0]
|
||||||
|
return CompletionResult(
|
||||||
|
content=choice.message.content or "",
|
||||||
|
model=resp.model,
|
||||||
|
tokens_in=resp.usage.prompt_tokens if resp.usage else 0,
|
||||||
|
tokens_out=resp.usage.completion_tokens if resp.usage else 0,
|
||||||
|
latency_ms=elapsed,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
last_exc = exc
|
||||||
|
if attempt >= len(delays):
|
||||||
|
break
|
||||||
|
assert last_exc is not None
|
||||||
|
raise last_exc
|
||||||
Reference in New Issue
Block a user