Silence annoying OpenAI client shutdown error (#4157)

This commit is contained in:
Stanislav Novosad
2025-12-01 18:02:17 -07:00
committed by GitHub
parent 7100b7e004
commit 4ac82ec25b
3 changed files with 38 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ from openai import AsyncAzureOpenAI, AsyncOpenAI
from skyvern.config import Settings
from skyvern.forge.agent import ForgeAgent
from skyvern.forge.agent_functions import AgentFunction
from skyvern.forge.forge_openai_client import ForgeAsyncHttpxClientWrapper
from skyvern.forge.sdk.api.azure import AzureClientFactory
from skyvern.forge.sdk.api.llm.api_handler_factory import LLMAPIHandlerFactory
from skyvern.forge.sdk.api.llm.models import LLMAPIHandler
@@ -95,7 +96,10 @@ def create_forge_app() -> ForgeApp:
app.EXPERIMENTATION_PROVIDER = NoOpExperimentationProvider()
app.LLM_API_HANDLER = LLMAPIHandlerFactory.get_llm_api_handler(settings.LLM_KEY)
app.OPENAI_CLIENT = AsyncOpenAI(api_key=settings.OPENAI_API_KEY or "")
app.OPENAI_CLIENT = AsyncOpenAI(
api_key=settings.OPENAI_API_KEY or "",
http_client=ForgeAsyncHttpxClientWrapper(),
)
if settings.ENABLE_AZURE_CUA:
app.OPENAI_CLIENT = AsyncAzureOpenAI(
api_key=settings.AZURE_CUA_API_KEY,
@@ -113,6 +117,7 @@ def create_forge_app() -> ForgeApp:
app.UI_TARS_CLIENT = AsyncOpenAI(
api_key=settings.VOLCENGINE_API_KEY,
base_url=settings.VOLCENGINE_API_BASE,
http_client=ForgeAsyncHttpxClientWrapper(),
)
app.SECONDARY_LLM_API_HANDLER = LLMAPIHandlerFactory.get_llm_api_handler(

View File

@@ -0,0 +1,24 @@
import asyncio
from openai import DefaultAsyncHttpxClient
class ForgeAsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
"""
Wrapper around OpenAI's AsyncHttpxClientWrapper to mask teardown races.
The upstream `__del__` checks `self.is_closed`, but during interpreter
shutdown httpx internals may already be None, which raises:
AttributeError: 'NoneType' object has no attribute 'CLOSED'
We defensively swallow that destructor error so shutdown logs stay clean.
"""
def __del__(self) -> None:
try:
if self.is_closed:
return
asyncio.get_running_loop().create_task(self.aclose())
except Exception:
pass

View File

@@ -18,6 +18,7 @@ from pydantic import BaseModel
from skyvern.config import settings
from skyvern.exceptions import SkyvernContextWindowExceededError
from skyvern.forge import app
from skyvern.forge.forge_openai_client import ForgeAsyncHttpxClientWrapper
from skyvern.forge.sdk.api.llm.config_registry import LLMConfigRegistry
from skyvern.forge.sdk.api.llm.exceptions import (
DuplicateCustomLLMProviderError,
@@ -1100,7 +1101,11 @@ class LLMCaller:
self.openai_client = None
if self.llm_key.startswith("openrouter/"):
self.llm_key = self.llm_key.replace("openrouter/", "")
self.openai_client = AsyncOpenAI(api_key=settings.OPENROUTER_API_KEY, base_url=settings.OPENROUTER_API_BASE)
self.openai_client = AsyncOpenAI(
api_key=settings.OPENROUTER_API_KEY,
base_url=settings.OPENROUTER_API_BASE,
http_client=ForgeAsyncHttpxClientWrapper(),
)
def add_tool_result(self, tool_result: dict[str, Any]) -> None:
self.current_tool_results.append(tool_result)
@@ -1513,7 +1518,8 @@ class LLMCaller:
if isinstance(response, UITarsResponse):
ui_tars_usage = response.usage
return LLMCallStats(
llm_cost=0, # TODO: calculate the cost according to the price: https://www.volcengine.com/docs/82379/1544106
llm_cost=0,
# TODO: calculate the cost according to the price: https://www.volcengine.com/docs/82379/1544106
input_tokens=ui_tars_usage.get("prompt_tokens", 0),
output_tokens=ui_tars_usage.get("completion_tokens", 0),
cached_tokens=0, # only part of model support cached tokens