1"""OpenAI adapter (official `openai` SDK, Responses API with a JSON-schema text format).23Reads OPENAI_API_KEY from the environment only. Model IDs come from the run config. Reasoning effort4is passed only for models that accept it (gpt-5 family); temperature only when the config sets it."""5from __future__ import annotations67import os8import time910from . import ProviderResult11from ..schema import DECISION_JSON_SCHEMA121314class OpenAIProvider:15 name = "openai"1617 def __init__(self, timeout_s: float = 60.0, max_retries: int = 3, **_: object) -> None:18 import openai19 if not os.environ.get("OPENAI_API_KEY"):20 raise RuntimeError("OPENAI_API_KEY is not set")21 self._openai = openai22 self.client = openai.OpenAI(timeout=timeout_s, max_retries=max_retries)2324 def complete(self, system: str, user: str, model: str, reasoning_effort: str | None = "low",25 temperature: float | None = None, max_output_tokens: int = 400, cache_key: str | None = None,26 **_: object) -> ProviderResult:27 t0 = time.perf_counter()28 kwargs: dict = dict(29 model=model,30 instructions=system,31 input=[{"role": "user", "content": user}],32 max_output_tokens=max_output_tokens,33 text={"format": {"type": "json_schema", "name": "trading_decision", "schema": DECISION_JSON_SCHEMA, "strict": True}},34 store=False,35 )36 if reasoning_effort and (model.startswith("gpt-5") or model.startswith("o")):37 kwargs["reasoning"] = {"effort": reasoning_effort}38 if temperature is not None and not (model.startswith("gpt-5") or model.startswith("o")):39 kwargs["temperature"] = temperature40 if cache_key:41 kwargs["prompt_cache_key"] = cache_key42 try:43 resp = self.client.responses.create(**kwargs)44 except self._openai.RateLimitError as e:45 return ProviderResult(text="", error=f"rate_limit: {e}", latency_s=time.perf_counter() - t0, model=model)46 except self._openai.APIStatusError as e:47 return ProviderResult(text="", error=f"status_{e.status_code}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)48 except self._openai.APIConnectionError as e:49 return ProviderResult(text="", error=f"connection: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)50 except Exception as e: # noqa: BLE00151 return ProviderResult(text="", error=f"other: {type(e).__name__}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)52 text = getattr(resp, "output_text", "") or ""53 u = getattr(resp, "usage", None)54 inp = int(getattr(u, "input_tokens", 0) or 0)55 out = int(getattr(u, "output_tokens", 0) or 0)56 cached = 057 reasoning = 058 try:59 cached = int(u.input_tokens_details.cached_tokens or 0)60 except Exception: # noqa: BLE00161 pass62 try:63 reasoning = int(u.output_tokens_details.reasoning_tokens or 0)64 except Exception: # noqa: BLE00165 pass66 status = getattr(resp, "status", "")67 err = "" if text else f"empty_output(status={status})"68 return ProviderResult(text=text, input_tokens=inp, cached_input_tokens=cached, output_tokens=out,69 reasoning_tokens=reasoning, latency_s=time.perf_counter() - t0, model=model,70 request_id=getattr(resp, "id", "") or "", error=err)71