Fix xAI sentiment: use Agent Tools web_search (Live Search deprecated)
xAI returned 410 — search_parameters/Live Search is retired. Route xAI through the Responses API web_search tool instead (same path as OpenAI): - OpenAISentimentProvider parametrized with base_url / tool_type / source - xAI builds it against https://api.x.ai/v1 with the web_search tool - Drop the dead Live Search code from the generic compatible provider - Frontend label: "xAI Grok — web search" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,17 +55,34 @@ VALID_CLASSIFICATIONS = {"bullish", "bearish", "neutral"}
|
||||
|
||||
|
||||
class OpenAISentimentProvider:
|
||||
"""Fetches sentiment analysis from OpenAI Responses API with live web search."""
|
||||
"""Sentiment via the Responses API + web-search tool, with live grounding.
|
||||
|
||||
def __init__(self, api_key: str, model: str = "gpt-4o-mini") -> None:
|
||||
Works against any provider implementing the OpenAI Responses API. OpenAI
|
||||
uses the ``web_search_preview`` tool; xAI Grok uses ``web_search`` at the
|
||||
``https://api.x.ai/v1`` base URL.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
model: str = "gpt-4o-mini",
|
||||
base_url: str | None = None,
|
||||
tool_type: str = "web_search_preview",
|
||||
source: str = "openai",
|
||||
) -> None:
|
||||
if not api_key:
|
||||
raise ProviderError("OpenAI API key is required")
|
||||
raise ProviderError(f"{source} API key is required")
|
||||
http_kwargs: dict = {}
|
||||
if _CA_BUNDLE and Path(_CA_BUNDLE).exists():
|
||||
http_kwargs["verify"] = _CA_BUNDLE
|
||||
http_client = httpx.AsyncClient(**http_kwargs)
|
||||
self._client = AsyncOpenAI(api_key=api_key, http_client=http_client)
|
||||
client_kwargs: dict = {"api_key": api_key, "http_client": http_client}
|
||||
if base_url:
|
||||
client_kwargs["base_url"] = base_url
|
||||
self._client = AsyncOpenAI(**client_kwargs)
|
||||
self._model = model
|
||||
self._tool_type = tool_type
|
||||
self._source = source
|
||||
|
||||
@staticmethod
|
||||
def _extract_raw_text(response: object, ticker_context: str) -> str:
|
||||
@@ -89,12 +106,11 @@ class OpenAISentimentProvider:
|
||||
clean = clean[:-3]
|
||||
return clean.strip()
|
||||
|
||||
@staticmethod
|
||||
def _normalize_single_result(parsed: dict, ticker: str, citations: list[dict[str, str]]) -> SentimentData:
|
||||
def _normalize_single_result(self, parsed: dict, ticker: str, citations: list[dict[str, str]]) -> SentimentData:
|
||||
classification = str(parsed.get("classification", "")).lower()
|
||||
if classification not in VALID_CLASSIFICATIONS:
|
||||
raise ProviderError(
|
||||
f"Invalid classification '{classification}' from OpenAI for {ticker}"
|
||||
f"Invalid classification '{classification}' from {self._source} for {ticker}"
|
||||
)
|
||||
|
||||
confidence = int(parsed.get("confidence", 50))
|
||||
@@ -103,7 +119,8 @@ class OpenAISentimentProvider:
|
||||
|
||||
if reasoning:
|
||||
logger.info(
|
||||
"OpenAI sentiment for %s: %s (confidence=%d) — %s",
|
||||
"%s sentiment for %s: %s (confidence=%d) — %s",
|
||||
self._source,
|
||||
ticker,
|
||||
classification,
|
||||
confidence,
|
||||
@@ -114,7 +131,7 @@ class OpenAISentimentProvider:
|
||||
ticker=ticker,
|
||||
classification=classification,
|
||||
confidence=confidence,
|
||||
source="openai",
|
||||
source=self._source,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
reasoning=reasoning,
|
||||
citations=citations,
|
||||
@@ -125,7 +142,7 @@ class OpenAISentimentProvider:
|
||||
try:
|
||||
response = await self._client.responses.create(
|
||||
model=self._model,
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
tools=[{"type": self._tool_type}],
|
||||
instructions="You are a financial sentiment analyst. Always respond with valid JSON only, no markdown fences.",
|
||||
input=_SENTIMENT_PROMPT.format(ticker=ticker),
|
||||
)
|
||||
@@ -172,7 +189,7 @@ class OpenAISentimentProvider:
|
||||
try:
|
||||
response = await self._client.responses.create(
|
||||
model=self._model,
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
tools=[{"type": self._tool_type}],
|
||||
instructions="You are a financial sentiment analyst. Always respond with valid JSON only, no markdown fences.",
|
||||
input=_SENTIMENT_BATCH_PROMPT.format(tickers_csv=", ".join(normalized)),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user