"""OpenAI adapter (official `openai` SDK, Responses API with a JSON-schema text format). Reads OPENAI_API_KEY from the environment only. Model IDs come from the run config. Reasoning effort is passed only for models that accept it (gpt-5 family); temperature only when the config sets it.""" from __future__ import annotations import os import time from . import ProviderResult from ..schema import DECISION_JSON_SCHEMA class OpenAIProvider: name = "openai" def __init__(self, timeout_s: float = 60.0, max_retries: int = 3, **_: object) -> None: import openai if not os.environ.get("OPENAI_API_KEY"): raise RuntimeError("OPENAI_API_KEY is not set") self._openai = openai self.client = openai.OpenAI(timeout=timeout_s, max_retries=max_retries) def complete(self, system: str, user: str, model: str, reasoning_effort: str | None = "low", temperature: float | None = None, max_output_tokens: int = 400, cache_key: str | None = None, **_: object) -> ProviderResult: t0 = time.perf_counter() kwargs: dict = dict( model=model, instructions=system, input=[{"role": "user", "content": user}], max_output_tokens=max_output_tokens, text={"format": {"type": "json_schema", "name": "trading_decision", "schema": DECISION_JSON_SCHEMA, "strict": True}}, store=False, ) if reasoning_effort and (model.startswith("gpt-5") or model.startswith("o")): kwargs["reasoning"] = {"effort": reasoning_effort} if temperature is not None and not (model.startswith("gpt-5") or model.startswith("o")): kwargs["temperature"] = temperature if cache_key: kwargs["prompt_cache_key"] = cache_key try: resp = self.client.responses.create(**kwargs) except self._openai.RateLimitError as e: return ProviderResult(text="", error=f"rate_limit: {e}", latency_s=time.perf_counter() - t0, model=model) except self._openai.APIStatusError as e: return ProviderResult(text="", error=f"status_{e.status_code}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model) except self._openai.APIConnectionError as e: return ProviderResult(text="", error=f"connection: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model) except Exception as e: # noqa: BLE001 return ProviderResult(text="", error=f"other: {type(e).__name__}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model) text = getattr(resp, "output_text", "") or "" u = getattr(resp, "usage", None) inp = int(getattr(u, "input_tokens", 0) or 0) out = int(getattr(u, "output_tokens", 0) or 0) cached = 0 reasoning = 0 try: cached = int(u.input_tokens_details.cached_tokens or 0) except Exception: # noqa: BLE001 pass try: reasoning = int(u.output_tokens_details.reasoning_tokens or 0) except Exception: # noqa: BLE001 pass status = getattr(resp, "status", "") err = "" if text else f"empty_output(status={status})" return ProviderResult(text=text, input_tokens=inp, cached_input_tokens=cached, output_tokens=out, reasoning_tokens=reasoning, latency_s=time.perf_counter() - t0, model=model, request_id=getattr(resp, "id", "") or "", error=err)