{"repo": "browser-use/browser-use", "files": {"browser_use/llm/vercel/chat.py": {"f0aa3a8bb03779c71a5aa262d389e3bfe6b77cdc": {"t": 1785595995.860372, "n": 21593, "h": "3eb81402b7e8bd9d", "c": "import json\nimport os\nfrom collections.abc import Mapping\nfrom dataclasses import dataclass, field\nfrom typing import Any, Literal, TypeAlias, TypeVar, overload\n\nimport httpx\nfrom openai import APIConnectionError, APIStatusError, AsyncOpenAI, RateLimitError\nfrom openai.types.chat.chat_completion import ChatCompletion\nfrom openai.types.shared_params.response_format_json_schema import (\n\tJSONSchema,\n\tResponseFormatJSONSchema,\n)\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.messages import BaseMessage, ContentPartTextParam, SystemMessage\nfrom browser_use.llm.schema import SchemaOptimizer\nfrom browser_use.llm.vercel.serializer import VercelMessageSerializer\nfrom browser_use.llm.views import ChatInvokeCompletion, ChatInvokeUsage\n\nT = TypeVar('T', bound=BaseModel)\n\nChatVercelModel: TypeAlias = Literal[\n\t'alibaba/qwen-3-14b',\n\t'alibaba/qwen-3-235b',\n\t'alibaba/qwen-3-30b',\n\t'alibaba/qwen-3-32b',\n\t'alibaba/qwen3-235b-a22b-thinking',\n\t'alibaba/qwen3-coder',\n\t'alibaba/qwen3-coder-30b-a3b',\n\t'alibaba/qwen3-coder-next',\n\t'alibaba/qwen3-coder-plus',\n\t'alibaba/qwen3-embedding-0.6b',\n\t'alibaba/qwen3-embedding-4b',\n\t'alibaba/qwen3-embedding-8b',\n\t'alibaba/qwen3-max',\n\t'alibaba/qwen3-max-preview',\n\t'alibaba/qwen3-max-thinking',\n\t'alibaba/qwen3-next-80b-a3b-instruct',\n\t'alibaba/qwen3-next-80b-a3b-thinking',\n\t'alibaba/qwen3-vl-instruct',\n\t'alibaba/qwen3-vl-thinking',\n\t'alibaba/qwen3.5-flash',\n\t'alibaba/qwen3.5-plus',\n\t'alibaba/wan-v2.5-t2v-preview',\n\t'alibaba/wan-v2.6-i2v',\n\t'alibaba/wan-v2.6-i2v-flash',\n\t'alibaba/wan-v2.6-r2v',\n\t'alibaba/wan-v2.6-r2v-flash',\n\t'alibaba/wan-v2.6-t2v',\n\t'amazon/nova-2-lite',\n\t'amazon/nova-lite',\n\t'amazon/nova-micro',\n\t'amazon/nova-pro',\n\t'amazon/titan-embed-text-v2',\n\t'anthropic/claude-3-haiku',\n\t'anthropic/claude-3-opus',\n\t'anthropic/claude-3.5-haiku',\n\t'anthropic/claude-3.5-sonnet',\n\t'anthropic/claude-3.5-sonnet-20240620',\n\t'anthropic/claude-3.7-sonnet',\n\t'anthropic/claude-fable-5',\n\t'anthropic/claude-haiku-4.5',\n\t'anthropic/claude-opus-4',\n\t'anthropic/claude-opus-4.1',\n\t'anthropic/claude-opus-4.5',\n\t'anthropic/claude-opus-4.6',\n\t'anthropic/claude-sonnet-4',\n\t'anthropic/claude-sonnet-4.5',\n\t'anthropic/claude-sonnet-4.6',\n\t'arcee-ai/trinity-large-preview',\n\t'arcee-ai/trinity-mini',\n\t'bfl/flux-kontext-max',\n\t'bfl/flux-kontext-pro',\n\t'bfl/flux-pro-1.0-fill',\n\t'bfl/flux-pro-1.1',\n\t'bfl/flux-pro-1.1-ultra',\n\t'bytedance/seed-1.6',\n\t'bytedance/seed-1.8',\n\t'bytedance/seedance-v1.0-lite-i2v',\n\t'bytedance/seedance-v1.0-lite-t2v',\n\t'bytedance/seedance-v1.0-pro',\n\t'bytedance/seedance-v1.0-pro-fast',\n\t'bytedance/seedance-v1.5-pro',\n\t'cohere/command-a',\n\t'cohere/embed-v4.0',\n\t'deepseek/deepseek-r1',\n\t'deepseek/deepseek-v3',\n\t'deepseek/deepseek-v3.1',\n\t'deepseek/deepseek-v3.1-terminus',\n\t'deepseek/deepseek-v3.2',\n\t'deepseek/deepseek-v3.2-thinking',\n\t'google/gemini-2.0-flash',\n\t'google/gemini-2.0-flash-lite',\n\t'google/gemini-2.5-flash',\n\t'google/gemini-2.5-flash-image',\n\t'google/gemini-2.5-flash-lite',\n\t'google/gemini-2.5-flash-lite-preview-09-2025',\n\t'google/gemini-2.5-flash-preview-09-2025',\n\t'google/gemini-2.5-pro',\n\t'google/gemini-3-flash',\n\t'google/gemini-3-pro-image',\n\t'google/gemini-3-pro-preview',\n\t'google/gemini-3.1-flash-image-preview',\n\t'google/gemini-3.1-flash-lite-preview',\n\t'google/gemini-3.1-pro-preview',\n\t'google/gemini-embedding-001',\n\t'google/imagen-4.0-fast-generate-001',\n\t'google/imagen-4.0-generate-001',\n\t'google/imagen-4.0-ultra-generate-001',\n\t'google/text-embedding-005',\n\t'google/text-multilingual-embedding-002',\n\t'google/veo-3.0-fast-generate-001',\n\t'google/veo-3.0-generate-001',\n\t'google/veo-3.1-fast-generate-001',\n\t'google/veo-3.1-generate-001',\n\t'inception/mercury-2',\n\t'inception/mercury-coder-small',\n\t'klingai/kling-v2.5-turbo-i2v',\n\t'klingai/kling-v2.5-turbo-t2v',\n\t'klingai/kling-v2.6-i2v',\n\t'klingai/kling-v2.6-motion-control',\n\t'klingai/kling-v2.6-t2v',\n\t'klingai/kling-v3.0-i2v',\n\t'klingai/kling-v3.0-t2v',\n\t'kwaipilot/kat-coder-pro-v1',\n\t'meituan/longcat-flash-chat',\n\t'meituan/longcat-flash-thinking',\n\t'meta/llama-3.1-70b',\n\t'meta/llama-3.1-8b',\n\t'meta/llama-3.2-11b',\n\t'meta/llama-3.2-1b',\n\t'meta/llama-3.2-3b',\n\t'meta/llama-3.2-90b',\n\t'meta/llama-3.3-70b',\n\t'meta/llama-4-maverick',\n\t'meta/llama-4-scout',\n\t'minimax/minimax-m2',\n\t'minimax/minimax-m2.1',\n\t'minimax/minimax-m2.1-lightning',\n\t'minimax/minimax-m2.5',\n\t'minimax/minimax-m2.5-highspeed',\n\t'mistral/codestral',\n\t'mistral/codestral-embed',\n\t'mistral/devstral-2',\n\t'mistral/devstral-small',\n\t'mistral/devstral-small-2',\n\t'mistral/magistral-medium',\n\t'mistral/magistral-small',\n\t'mistral/ministral-14b',\n\t'mistral/ministral-3b',\n\t'mistral/ministral-8b',\n\t'mistral/mistral-embed',\n\t'mistral/mistral-large-3',\n\t'mistral/mistral-medium',\n\t'mistral/mistral-nemo',\n\t'mistral/mistral-small',\n\t'mistral/mixtral-8x22b-instruct',\n\t'mistral/pixtral-12b',\n\t'mistral/pixtral-large',\n\t'moonshotai/kimi-k2',\n\t'moonshotai/kimi-k2-0905',\n\t'moonshotai/kimi-k2-thinking',\n\t'moonshotai/kimi-k2-thinking-turbo',\n\t'moonshotai/kimi-k2-turbo',\n\t'moonshotai/kimi-k2.5',\n\t'morph/morph-v3-fast',\n\t'morph/morph-v3-large',\n\t'nvidia/nemotron-3-nano-30b-a3b',\n\t'nvidia/nemotron-nano-12b-v2-vl',\n\t'nvidia/nemotron-nano-9b-v2',\n\t'openai/gpt-3.5-turbo',\n\t'openai/gpt-3.5-turbo-instruct',\n\t'openai/gpt-4-turbo',\n\t'openai/gpt-4.1',\n\t'openai/gpt-4.1-mini',\n\t'openai/gpt-4.1-nano',\n\t'openai/gpt-4o',\n\t'openai/gpt-4o-mini',\n\t'openai/gpt-4o-mini-search-preview',\n\t'openai/gpt-5',\n\t'openai/gpt-5-chat',\n\t'openai/gpt-5-codex',\n\t'openai/gpt-5-mini',\n\t'openai/gpt-5-nano',\n\t'openai/gpt-5-pro',\n\t'openai/gpt-5.1-codex',\n\t'openai/gpt-5.1-codex-max',\n\t'openai/gpt-5.1-codex-mini',\n\t'openai/gpt-5.1-instant',\n\t'openai/gpt-5.1-thinking',\n\t'openai/gpt-5.2',\n\t'openai/gpt-5.2-chat',\n\t'openai/gpt-5.2-codex',\n\t'openai/gpt-5.2-pro',\n\t'openai/gpt-5.3-chat',\n\t'openai/gpt-5.3-codex',\n\t'openai/gpt-5.4',\n\t'openai/gpt-5.4-pro',\n\t'openai/gpt-image-1',\n\t'openai/gpt-image-1-mini',\n\t'openai/gpt-image-1.5',\n\t'openai/gpt-oss-120b',\n\t'openai/gpt-oss-20b',\n\t'openai/gpt-oss-safeguard-20b',\n\t'openai/o1',\n\t'openai/o3',\n\t'openai/o3-deep-research',\n\t'openai/o3-mini',\n\t'openai/o3-pro',\n\t'openai/o4-mini',\n\t'openai/text-embedding-3-large',\n\t'openai/text-embedding-3-small',\n\t'openai/text-embedding-ada-002',\n\t'perplexity/sonar',\n\t'perplexity/sonar-pro',\n\t'perplexity/sonar-reasoning',\n\t'perplexity/sonar-reasoning-pro',\n\t'prime-intellect/intellect-3',\n\t'recraft/recraft-v2',\n\t'recraft/recraft-v3',\n\t'recraft/recraft-v4',\n\t'recraft/recraft-v4-pro',\n\t'stealth/sonoma-dusk-alpha',\n\t'stealth/sonoma-sky-alpha',\n\t'vercel/v0-1.0-md',\n\t'vercel/v0-1.5-md',\n\t'voyage/voyage-3-large',\n\t'voyage/voyage-3.5',\n\t'voyage/voyage-3.5-lite',\n\t'voyage/voyage-4',\n\t'voyage/voyage-4-large',\n\t'voyage/voyage-4-lite',\n\t'voyage/voyage-code-2',\n\t'voyage/voyage-code-3',\n\t'voyage/voyage-finance-2',\n\t'voyage/voyage-law-2',\n\t'xai/grok-2-vision',\n\t'xai/grok-3',\n\t'xai/grok-3-fast',\n\t'xai/grok-3-mini',\n\t'xai/grok-3-mini-fast',\n\t'xai/grok-4',\n\t'xai/grok-4-fast-non-reasoning',\n\t'xai/grok-4-fast-reasoning',\n\t'xai/grok-4.1-fast-non-reasoning',\n\t'xai/grok-4.1-fast-reasoning',\n\t'xai/grok-4.20-multi-agent-beta',\n\t'xai/grok-4.20-non-reasoning-beta',\n\t'xai/grok-4.20-reasoning-beta',\n\t'xai/grok-code-fast-1',\n\t'xai/grok-imagine-image',\n\t'xai/grok-imagine-image-pro',\n\t'xai/grok-imagine-video',\n\t'xiaomi/mimo-v2-flash',\n\t'zai/glm-4.5',\n\t'zai/glm-4.5-air',\n\t'zai/glm-4.5v',\n\t'zai/glm-4.6',\n\t'zai/glm-4.6v',\n\t'zai/glm-4.6v-flash',\n\t'zai/glm-4.7',\n\t'zai/glm-4.7-flashx',\n\t'zai/glm-5',\n]\n\n\n@dataclass\nclass ChatVercel(BaseChatModel):\n\t\"\"\"\n\tA wrapper around Vercel AI Gateway's API, which provides OpenAI-compatible access\n\tto various LLM models with features like rate limiting, caching, and monitoring.\n\n\tExamples:\n\t\t```python\n\t from browser_use import Agent, ChatVercel\n\n\t llm = ChatVercel(model='openai/gpt-4o', api_key='your_vercel_api_key')\n\n\t agent = Agent(task='Your task here', llm=llm)\n\t\t```\n\n\tArgs:\n\t model: The model identifier\n\t api_key: Your Vercel AI Gateway API key. If not provided, falls back to\n\t AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN environment variables.\n\t base_url: The Vercel AI Gateway endpoint (defaults to https://ai-gateway.vercel.sh/v1)\n\t temperature: Sampling temperature (0-2)\n\t max_tokens: Maximum tokens to generate\n\t reasoning_models: List of reasoning model patterns (e.g., 'o1', 'gpt-oss') that need\n\t prompt-based JSON extraction. Auto-detects common reasoning models by default.\n\t timeout: Request timeout in seconds\n\t max_retries: Maximum number of retries for failed requests\n\t provider_options: Provider routing options for the gateway. Use this to control which\n\t providers are used and in what order. Example: {'gateway': {'order': ['vertex', 'anthropic']}}\n\t reasoning: Optional provider-specific reasoning configuration. Merged into\n\t providerOptions under the appropriate provider key. Example for Anthropic:\n\t {'anthropic': {'thinking': {'type': 'adaptive'}}}. Example for OpenAI:\n\t {'openai': {'reasoningEffort': 'high', 'reasoningSummary': 'detailed'}}.\n\t model_fallbacks: Optional list of fallback model IDs tried in order if the primary\n\t model fails. Passed as providerOptions.gateway.models.\n\t caching: Optional caching mode for the gateway. Currently supports 'auto', which\n\t enables provider-specific prompt caching via providerOptions.gateway.caching.\n\t\"\"\"\n\n\t# Model configuration\n\tmodel: ChatVercelModel | str\n\n\t# Model params\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\ttop_p: float | None = None\n\treasoning_models: list[str] | None = field(\n\t\tdefault_factory=lambda: [\n\t\t\t'o1',\n\t\t\t'o3',\n\t\t\t'o4',\n\t\t\t'gpt-oss',\n\t\t\t'gpt-5.2-pro',\n\t\t\t'gpt-5.4-pro',\n\t\t\t'deepseek-r1',\n\t\t\t'-thinking',\n\t\t\t'perplexity/sonar-reasoning',\n\t\t]\n\t)\n\n\t# Client initialization parameters\n\tapi_key: str | None = None\n\tbase_url: str | httpx.URL = 'https://ai-gateway.vercel.sh/v1'\n\ttimeout: float | httpx.Timeout | None = None\n\tmax_retries: int = 5\n\tdefault_headers: Mapping[str, str] | None = None\n\tdefault_query: Mapping[str, object] | None = None\n\thttp_client: httpx.AsyncClient | None = None\n\t_strict_response_validation: bool = False\n\tprovider_options: dict[str, Any] | None = None\n\treasoning: dict[str, dict[str, Any]] | None = None\n\tmodel_fallbacks: list[str] | None = None\n\tcaching: Literal['auto'] | None = None\n\n\t# Static\n\t@property\n\tdef provider(self) -> str:\n\t\treturn 'vercel'\n\n\tdef _get_client_params(self) -> dict[str, Any]:\n\t\t\"\"\"Prepare client parameters dictionary.\"\"\"\n\t\tapi_key = self.api_key or os.getenv('AI_GATEWAY_API_KEY') or os.getenv('VERCEL_OIDC_TOKEN')\n\n\t\tbase_params = {\n\t\t\t'api_key': api_key,\n\t\t\t'base_url': self.base_url,\n\t\t\t'timeout': self.timeout,\n\t\t\t'max_retries': self.max_retries,\n\t\t\t'default_headers': self.default_headers,\n\t\t\t'default_query': self.default_query,\n\t\t\t'_strict_response_validation': self._strict_response_validation,\n\t\t}\n\n\t\tclient_params = {k: v for k, v in base_params.items() if v is not None}\n\n\t\tif self.http_client is not None:\n\t\t\tclient_params['http_client'] = self.http_client\n\n\t\treturn client_params\n\n\tdef get_client(self) -> AsyncOpenAI:\n\t\t\"\"\"\n\t\tReturns an AsyncOpenAI client configured for Vercel AI Gateway.\n\n\t\tReturns:\n\t\t AsyncOpenAI: An instance of the AsyncOpenAI client with Vercel base URL.\n\t\t\"\"\"\n\t\tif not hasattr(self, '_client'):\n\t\t\tclient_params = self._get_client_params()\n\t\t\tself._client = AsyncOpenAI(**client_params)\n\t\treturn self._client\n\n\t@property\n\tdef name(self) -> str:\n\t\treturn str(self.model)\n\n\tdef _get_usage(self, response: ChatCompletion) -> ChatInvokeUsage | None:\n\t\t\"\"\"Extract usage information from the Vercel response.\"\"\"\n\t\tif response.usage is None:\n\t\t\treturn None\n\n\t\tprompt_details = getattr(response.usage, 'prompt_tokens_details', None)\n\t\tcached_tokens = prompt_details.cached_tokens if prompt_details else None\n\n\t\treturn ChatInvokeUsage(\n\t\t\tprompt_tokens=response.usage.prompt_tokens,\n\t\t\tprompt_cached_tokens=cached_tokens,\n\t\t\tprompt_cache_creation_tokens=None,\n\t\t\tprompt_image_tokens=None,\n\t\t\tcompletion_tokens=response.usage.completion_tokens,\n\t\t\ttotal_tokens=response.usage.total_tokens,\n\t\t)\n\n\tdef _fix_gemini_schema(self, schema: dict[str, Any]) -> dict[str, Any]:\n\t\t\"\"\"\n\t\tConvert a Pydantic model to a Gemini-compatible schema.\n\n\t\tThis function removes unsupported properties like 'additionalProperties' and resolves\n\t\t$ref references that Gemini doesn't support.\n\t\t\"\"\"\n\n\t\t# Handle $defs and $ref resolution\n\t\tif '$defs' in schema:\n\t\t\tdefs = schema.pop('$defs')\n\n\t\t\tdef resolve_refs(obj: Any) -> Any:\n\t\t\t\tif isinstance(obj, dict):\n\t\t\t\t\tif '$ref' in obj:\n\t\t\t\t\t\tref = obj.pop('$ref')\n\t\t\t\t\t\tref_name = ref.split('/')[-1]\n\t\t\t\t\t\tif ref_name in defs:\n\t\t\t\t\t\t\t# Replace the reference with the actual definition\n\t\t\t\t\t\t\tresolved = defs[ref_name].copy()\n\t\t\t\t\t\t\t# Merge any additional properties from the reference\n\t\t\t\t\t\t\tfor key, value in obj.items():\n\t\t\t\t\t\t\t\tif key != '$ref':\n\t\t\t\t\t\t\t\t\tresolved[key] = value\n\t\t\t\t\t\t\treturn resolve_refs(resolved)\n\t\t\t\t\t\treturn obj\n\t\t\t\t\telse:\n\t\t\t\t\t\t# Recursively process all dictionary values\n\t\t\t\t\t\treturn {k: resolve_refs(v) for k, v in obj.items()}\n\t\t\t\telif isinstance(obj, list):\n\t\t\t\t\treturn [resolve_refs(item) for item in obj]\n\t\t\t\treturn obj\n\n\t\t\tschema = resolve_refs(schema)\n\n\t\t# Remove unsupported properties\n\t\tdef clean_schema(obj: Any) -> Any:\n\t\t\tif isinstance(obj, dict):\n\t\t\t\t# Remove unsupported properties\n\t\t\t\tcleaned = {}\n\t\t\t\tfor key, value in obj.items():\n\t\t\t\t\tif key not in ['additionalProperties', 'title', 'default']:\n\t\t\t\t\t\tcleaned_value = clean_schema(value)\n\t\t\t\t\t\t# Handle empty object properties - Gemini doesn't allow empty OBJECT types\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tkey == 'properties'\n\t\t\t\t\t\t\tand isinstance(cleaned_value, dict)\n\t\t\t\t\t\t\tand len(cleaned_value) == 0\n\t\t\t\t\t\t\tand isinstance(obj.get('type', ''), str)\n\t\t\t\t\t\t\tand obj.get('type', '').upper() == 'OBJECT'\n\t\t\t\t\t\t):\n\t\t\t\t\t\t\t# Convert empty object to have at least one property\n\t\t\t\t\t\t\tcleaned['properties'] = {'_placeholder': {'type': 'string'}}\n\t\t\t\t\t\telse:\n\t\t\t\t\t\t\tcleaned[key] = cleaned_value\n\n\t\t\t\t# If this is an object type with empty properties, add a placeholder\n\t\t\t\tif (\n\t\t\t\t\tisinstance(cleaned.get('type', ''), str)\n\t\t\t\t\tand cleaned.get('type', '').upper() == 'OBJECT'\n\t\t\t\t\tand 'properties' in cleaned\n\t\t\t\t\tand isinstance(cleaned['properties'], dict)\n\t\t\t\t\tand len(cleaned['properties']) == 0\n\t\t\t\t):\n\t\t\t\t\tcleaned['properties'] = {'_placeholder': {'type': 'string'}}\n\n\t\t\t\t# Also remove 'title' from the required list if it exists\n\t\t\t\tif 'required' in cleaned and isinstance(cleaned.get('required'), list):\n\t\t\t\t\tcleaned['required'] = [p for p in cleaned['required'] if p != 'title']\n\n\t\t\t\treturn cleaned\n\t\t\telif isinstance(obj, list):\n\t\t\t\treturn [clean_schema(item) for item in obj]\n\t\t\treturn obj\n\n\t\treturn clean_schema(schema)\n\n\t@overload\n\tasync def ainvoke(\n\t\tself, messages: list[BaseMessage], output_format: None = None, **kwargs: Any\n\t) -> ChatInvokeCompletion[str]: ...\n\n\t@overload\n\tasync def ainvoke(self, messages: list[BaseMessage], output_format: type[T], **kwargs: Any) -> ChatInvokeCompletion[T]: ...\n\n\tasync def ainvoke(\n\t\tself, messages: list[BaseMessage], output_format: type[T] | None = None, **kwargs: Any\n\t) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n\t\t\"\"\"\n\t\tInvoke the model with the given messages through Vercel AI Gateway.\n\n\t\tArgs:\n\t\t messages: List of chat messages\n\t\t output_format: Optional Pydantic model class for structured output\n\n\t\tReturns:\n\t\t Either a string response or an instance of output_format\n\t\t\"\"\"\n\t\tvercel_messages = VercelMessageSerializer.serialize_messages(messages)\n\n\t\ttry:\n\t\t\tmodel_params: dict[str, Any] = {}\n\t\t\tif self.temperature is not None:\n\t\t\t\tmodel_params['temperature'] = self.temperature\n\t\t\tif self.max_tokens is not None:\n\t\t\t\tmodel_params['max_tokens'] = self.max_tokens\n\t\t\tif self.top_p is not None:\n\t\t\t\tmodel_params['top_p'] = self.top_p\n\n\t\t\textra_body: dict[str, Any] = {}\n\n\t\t\tprovider_opts: dict[str, Any] = {}\n\t\t\tif self.provider_options:\n\t\t\t\tprovider_opts.update(self.provider_options)\n\n\t\t\tif self.reasoning:\n\t\t\t\t# Merge provider-specific reasoning options (ex: {'anthropic': {'thinking': ...}})\n\t\t\t\tfor provider_name, opts in self.reasoning.items():\n\t\t\t\t\texisting = provider_opts.get(provider_name, {})\n\t\t\t\t\texisting.update(opts)\n\t\t\t\t\tprovider_opts[provider_name] = existing\n\n\t\t\tgateway_opts: dict[str, Any] = provider_opts.get('gateway', {})\n\n\t\t\tif self.model_fallbacks:\n\t\t\t\tgateway_opts['models'] = self.model_fallbacks\n\n\t\t\tif self.caching:\n\t\t\t\tgateway_opts['caching'] = self.caching\n\n\t\t\tif gateway_opts:\n\t\t\t\tprovider_opts['gateway'] = gateway_opts\n\n\t\t\tif provider_opts:\n\t\t\t\textra_body['providerOptions'] = provider_opts\n\n\t\t\tif extra_body:\n\t\t\t\tmodel_params['extra_body'] = extra_body\n\n\t\t\tif output_format is None:\n\t\t\t\t# Return string response\n\t\t\t\tresponse = await self.get_client().chat.completions.create(\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=vercel_messages,\n\t\t\t\t\t**model_params,\n\t\t\t\t)\n\n\t\t\t\tusage = self._get_usage(response)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=response.choices[0].message.content or '',\n\t\t\t\t\tusage=usage,\n\t\t\t\t\tstop_reason=response.choices[0].finish_reason if response.choices else None,\n\t\t\t\t)\n\n\t\t\telse:\n\t\t\t\tis_google_model = self.model.startswith('google/')\n\t\t\t\tis_anthropic_model = self.model.startswith('anthropic/')\n\t\t\t\tis_reasoning_model = self.reasoning_models and any(\n\t\t\t\t\tstr(pattern).lower() in str(self.model).lower() for pattern in self.reasoning_models\n\t\t\t\t)\n\n\t\t\t\tif is_google_model or is_anthropic_model or is_reasoning_model:\n\t\t\t\t\tmodified_messages = [m.model_copy(deep=True) for m in messages]\n\n\t\t\t\t\tschema = SchemaOptimizer.create_gemini_optimized_schema(output_format)\n\t\t\t\t\tjson_instruction = f'\\n\\nIMPORTANT: You must respond with ONLY a valid JSON object (no markdown, no code blocks, no explanations) that exactly matches this schema:\\n{json.dumps(schema, indent=2)}'\n\n\t\t\t\t\tinstruction_added = False\n\t\t\t\t\tif modified_messages and modified_messages[0].role == 'system':\n\t\t\t\t\t\tif isinstance(modified_messages[0].content, str):\n\t\t\t\t\t\t\tmodified_messages[0].content += json_instruction\n\t\t\t\t\t\t\tinstruction_added = True\n\t\t\t\t\t\telif isinstance(modified_messages[0].content, list):\n\t\t\t\t\t\t\tmodified_messages[0].content.append(ContentPartTextParam(text=json_instruction))\n\t\t\t\t\t\t\tinstruction_added = True\n\t\t\t\t\telif modified_messages and modified_messages[-1].role == 'user':\n\t\t\t\t\t\tif isinstance(modified_messages[-1].content, str):\n\t\t\t\t\t\t\tmodified_messages[-1].content += json_instruction\n\t\t\t\t\t\t\tinstruction_added = True\n\t\t\t\t\t\telif isinstance(modified_messages[-1].content, list):\n\t\t\t\t\t\t\tmodified_messages[-1].content.append(ContentPartTextParam(text=json_instruction))\n\t\t\t\t\t\t\tinstruction_added = True\n\n\t\t\t\t\tif not instruction_added:\n\t\t\t\t\t\tmodified_messages.insert(0, SystemMessage(content=json_instruction))\n\n\t\t\t\t\tvercel_messages = VercelMessageSerializer.serialize_messages(modified_messages)\n\n\t\t\t\t\tresponse = await self.get_client().chat.completions.create(\n\t\t\t\t\t\tmodel=self.model,\n\t\t\t\t\t\tmessages=vercel_messages,\n\t\t\t\t\t\t**model_params,\n\t\t\t\t\t)\n\n\t\t\t\t\tcontent = response.choices[0].message.content if response.choices else None\n\n\t\t\t\t\tif not content:\n\t\t\t\t\t\traise ModelProviderError(\n\t\t\t\t\t\t\tmessage='No response from model',\n\t\t\t\t\t\t\tstatus_code=500,\n\t\t\t\t\t\t\tmodel=self.name,\n\t\t\t\t\t\t)\n\n\t\t\t\t\ttry:\n\t\t\t\t\t\ttext = content.strip()\n\t\t\t\t\t\tif text.startswith('```json') and text.endswith('```'):\n\t\t\t\t\t\t\ttext = text[7:-3].strip()\n\t\t\t\t\t\telif text.startswith('```') and text.endswith('```'):\n\t\t\t\t\t\t\ttext = text[3:-3].strip()\n\n\t\t\t\t\t\tparsed_data = json.loads(text)\n\t\t\t\t\t\tparsed = output_format.model_validate(parsed_data)\n\n\t\t\t\t\t\tusage = self._get_usage(response)\n\t\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\t\t\tusage=usage,\n\t\t\t\t\t\t\tstop_reason=response.choices[0].finish_reason if response.choices else None,\n\t\t\t\t\t\t)\n\n\t\t\t\t\texcept (json.JSONDecodeError, ValueError) as e:\n\t\t\t\t\t\traise ModelProviderError(\n\t\t\t\t\t\t\tmessage=f'Failed to parse JSON response: {str(e)}. Raw response: {content[:200]}',\n\t\t\t\t\t\t\tstatus_code=500,\n\t\t\t\t\t\t\tmodel=self.name,\n\t\t\t\t\t\t) from e\n\n\t\t\t\telse:\n\t\t\t\t\tschema = SchemaOptimizer.create_optimized_json_schema(output_format)\n\n\t\t\t\t\tresponse_format_schema: JSONSchema = {\n\t\t\t\t\t\t'name': 'agent_output',\n\t\t\t\t\t\t'strict': True,\n\t\t\t\t\t\t'schema': schema,\n\t\t\t\t\t}\n\n\t\t\t\t\tresponse = await self.get_client().chat.completions.create(\n\t\t\t\t\t\tmodel=self.model,\n\t\t\t\t\t\tmessages=vercel_messages,\n\t\t\t\t\t\tresponse_format=ResponseFormatJSONSchema(\n\t\t\t\t\t\t\tjson_schema=response_format_schema,\n\t\t\t\t\t\t\ttype='json_schema',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t**model_params,\n\t\t\t\t\t)\n\n\t\t\t\t\tcontent = response.choices[0].message.content if response.choices else None\n\n\t\t\t\t\tif not content:\n\t\t\t\t\t\traise ModelProviderError(\n\t\t\t\t\t\t\tmessage='Failed to parse structured output from model response - empty or null content',\n\t\t\t\t\t\t\tstatus_code=500,\n\t\t\t\t\t\t\tmodel=self.name,\n\t\t\t\t\t\t)\n\n\t\t\t\t\tusage = self._get_usage(response)\n\t\t\t\t\tparsed = output_format.model_validate_json(content)\n\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\t\tusage=usage,\n\t\t\t\t\t\tstop_reason=response.choices[0].finish_reason if response.choices else None,\n\t\t\t\t\t)\n\n\t\texcept RateLimitError as e:\n\t\t\traise ModelRateLimitError(message=e.message, model=self.name) from e\n\n\t\texcept APIConnectionError as e:\n\t\t\traise ModelProviderError(message=str(e), model=self.name) from e\n\n\t\texcept APIStatusError as e:\n\t\t\traise ModelProviderError(message=e.message, status_code=e.status_code, model=self.name) from e\n\n\t\texcept Exception as e:\n\t\t\traise ModelProviderError(message=str(e), model=self.name) from e\n"}}}}