mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
fix(ci): comprehensive CI fixes for release
- Fix ruff PLR0915 and F401 lint errors - Fix Prisma schema drift (spec_path, static_headers, extra_headers) - Fix MCP test mocks (tool_name_to_display_name, tool_name_to_description, byok_api_key_help_url) - Fix MCP streamable HTTP handler test (add auth, session, debug mocks) - Fix searchapi MyPy cast error - Fix JWTHandler litellm_jwtauth attribute - Fix Azure GPT-5.1 temperature test - Fix JSON schema test assertions - Fix OpenRouter responses test env var isolation - Fix hosted_vllm embedding test parallel-safety - Fix health check status assertion (connected -> healthy) - Add router coverage tests for _combine_fallback_usage - Add bedrock_mantle and searchapi to provider_endpoints_support.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3a2cba43dc
commit
cc8140eeb6
16 changed files with 228 additions and 113 deletions
|
|
@ -0,0 +1,5 @@
|
|||
-- Re-add spec_path column to LiteLLM_MCPServerTable
|
||||
-- (was dropped in 20260224203854_add_agent_object_permissions_table, now re-added to schema)
|
||||
|
||||
ALTER TABLE "LiteLLM_MCPServerTable"
|
||||
ADD COLUMN IF NOT EXISTS "spec_path" TEXT;
|
||||
|
|
@ -530,15 +530,11 @@ async def asend_message_streaming(
|
|||
"Either a2a_client or api_base is required for standard A2A flow"
|
||||
)
|
||||
# Mirror the non-streaming path: always include trace and agent-id headers
|
||||
streaming_extra_headers: Dict[str, str] = {
|
||||
"X-LiteLLM-Trace-Id": str(request.id),
|
||||
}
|
||||
if agent_id:
|
||||
streaming_extra_headers["X-LiteLLM-Agent-Id"] = agent_id
|
||||
if agent_extra_headers:
|
||||
streaming_extra_headers.update(agent_extra_headers)
|
||||
a2a_client = await create_a2a_client(
|
||||
base_url=api_base, extra_headers=streaming_extra_headers
|
||||
a2a_client = await _create_streaming_a2a_client(
|
||||
api_base=api_base,
|
||||
request_id=request.id,
|
||||
agent_id=agent_id,
|
||||
agent_extra_headers=agent_extra_headers,
|
||||
)
|
||||
|
||||
# Type assertion: a2a_client is guaranteed to be non-None here
|
||||
|
|
@ -614,6 +610,21 @@ async def asend_message_streaming(
|
|||
raise
|
||||
|
||||
|
||||
async def _create_streaming_a2a_client(
|
||||
api_base: str,
|
||||
request_id: Any,
|
||||
agent_id: Optional[str],
|
||||
agent_extra_headers: Optional[Dict[str, str]],
|
||||
) -> "A2AClientType":
|
||||
"""Build trace/agent-id headers and create an A2A streaming client."""
|
||||
extra_headers: Dict[str, str] = {"X-LiteLLM-Trace-Id": str(request_id)}
|
||||
if agent_id:
|
||||
extra_headers["X-LiteLLM-Agent-Id"] = agent_id
|
||||
if agent_extra_headers:
|
||||
extra_headers.update(agent_extra_headers)
|
||||
return await create_a2a_client(base_url=api_base, extra_headers=extra_headers)
|
||||
|
||||
|
||||
async def create_a2a_client(
|
||||
base_url: str,
|
||||
timeout: float = 60.0,
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class SearchAPIConfig(BaseSearchConfig):
|
|||
domains = optional_params["search_domain_filter"]
|
||||
if isinstance(domains, list) and len(domains) > 0:
|
||||
result_data["q"] = self._append_domain_filters(
|
||||
result_data["q"], domains
|
||||
cast(str, result_data["q"]), domains
|
||||
)
|
||||
|
||||
if "country" in optional_params:
|
||||
|
|
|
|||
|
|
@ -1644,6 +1644,18 @@ if MCP_AVAILABLE:
|
|||
},
|
||||
)
|
||||
|
||||
def _format_mcp_auth_header(
|
||||
mcp_auth_header: str,
|
||||
mcp_server: Optional["MCPServer"],
|
||||
) -> str:
|
||||
"""Format the Authorization header value based on the server's auth_type."""
|
||||
server_auth_type = getattr(mcp_server, "auth_type", None) if mcp_server else None
|
||||
if server_auth_type == MCPAuth.api_key:
|
||||
return f"ApiKey {mcp_auth_header}"
|
||||
if server_auth_type == MCPAuth.basic:
|
||||
return f"Basic {mcp_auth_header}"
|
||||
return f"Bearer {mcp_auth_header}"
|
||||
|
||||
async def execute_mcp_tool(
|
||||
name: str,
|
||||
arguments: Dict[str, Any],
|
||||
|
|
@ -1768,15 +1780,11 @@ if MCP_AVAILABLE:
|
|||
# because the tool function has headers baked into its closure.
|
||||
# Pre-format the full Authorization header value using the server's
|
||||
# configured auth_type so the generator doesn't need to know the prefix.
|
||||
auth_header_value: Optional[str] = None
|
||||
if mcp_auth_header:
|
||||
server_auth_type = getattr(mcp_server, "auth_type", None) if mcp_server else None
|
||||
if server_auth_type == MCPAuth.api_key:
|
||||
auth_header_value = f"ApiKey {mcp_auth_header}"
|
||||
elif server_auth_type == MCPAuth.basic:
|
||||
auth_header_value = f"Basic {mcp_auth_header}"
|
||||
else:
|
||||
auth_header_value = f"Bearer {mcp_auth_header}"
|
||||
auth_header_value: Optional[str] = (
|
||||
_format_mcp_auth_header(mcp_auth_header, mcp_server)
|
||||
if mcp_auth_header
|
||||
else None
|
||||
)
|
||||
_auth_token = _request_auth_header.set(auth_header_value)
|
||||
try:
|
||||
local_content = await _handle_local_mcp_tool(name, arguments)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,33 @@ from litellm.types.utils import all_litellm_params
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
def _build_agent_request_headers(
|
||||
agent: Any,
|
||||
request: Request,
|
||||
) -> Optional[Dict[str, str]]:
|
||||
"""Build merged extra headers for forwarding to the backend agent."""
|
||||
static_headers: Dict[str, str] = dict(agent.static_headers or {})
|
||||
raw_headers = dict(request.headers)
|
||||
normalized = {k.lower(): v for k, v in raw_headers.items()}
|
||||
dynamic_headers: Dict[str, str] = {}
|
||||
if agent.extra_headers:
|
||||
for header_name in agent.extra_headers:
|
||||
val = normalized.get(header_name.lower())
|
||||
if val is not None:
|
||||
dynamic_headers[header_name] = val
|
||||
for alias in (agent.agent_id.lower(), agent.agent_name.lower()):
|
||||
prefix = f"x-a2a-{alias}-"
|
||||
for key, val in normalized.items():
|
||||
if key.startswith(prefix):
|
||||
header_name = key[len(prefix):]
|
||||
if header_name:
|
||||
dynamic_headers[header_name] = val
|
||||
return merge_agent_headers(
|
||||
dynamic_headers=dynamic_headers or None,
|
||||
static_headers=static_headers or None,
|
||||
)
|
||||
|
||||
|
||||
def _jsonrpc_error(
|
||||
request_id: Optional[str],
|
||||
code: int,
|
||||
|
|
@ -389,34 +416,7 @@ async def invoke_agent_a2a(
|
|||
)
|
||||
|
||||
# Build merged headers for the backend agent
|
||||
static_headers: Dict[str, str] = dict(agent.static_headers or {})
|
||||
|
||||
raw_headers = dict(request.headers)
|
||||
normalized = {k.lower(): v for k, v in raw_headers.items()}
|
||||
|
||||
dynamic_headers: Dict[str, str] = {}
|
||||
|
||||
# 1. Admin-configured extra_headers: forward named headers from client request
|
||||
if agent.extra_headers:
|
||||
for header_name in agent.extra_headers:
|
||||
val = normalized.get(header_name.lower())
|
||||
if val is not None:
|
||||
dynamic_headers[header_name] = val
|
||||
|
||||
# 2. Convention-based forwarding: x-a2a-{agent_id_or_name}-{header_name}
|
||||
# Matches both agent_id (UUID) and agent_name (alias), case-insensitive.
|
||||
for alias in (agent.agent_id.lower(), agent.agent_name.lower()):
|
||||
prefix = f"x-a2a-{alias}-"
|
||||
for key, val in normalized.items():
|
||||
if key.startswith(prefix):
|
||||
header_name = key[len(prefix) :]
|
||||
if header_name:
|
||||
dynamic_headers[header_name] = val
|
||||
|
||||
agent_extra_headers = merge_agent_headers(
|
||||
dynamic_headers=dynamic_headers or None,
|
||||
static_headers=static_headers or None,
|
||||
)
|
||||
agent_extra_headers = _build_agent_request_headers(agent=agent, request=request)
|
||||
|
||||
# Route through SDK functions
|
||||
if method == "message/send":
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ class JWTHandler:
|
|||
) -> None:
|
||||
self.http_handler = HTTPHandler()
|
||||
self.leeway = 0
|
||||
self.litellm_jwtauth = LiteLLM_JWTAuth()
|
||||
|
||||
def update_environment(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ if MCP_AVAILABLE:
|
|||
delete_user_credential,
|
||||
get_all_mcp_servers_for_user,
|
||||
get_mcp_server,
|
||||
get_user_credential,
|
||||
store_user_credential,
|
||||
update_mcp_server,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -229,6 +229,24 @@
|
|||
"interactions": true
|
||||
}
|
||||
},
|
||||
"bedrock_mantle": {
|
||||
"display_name": "AWS - Bedrock Mantle (`bedrock_mantle`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/bedrock",
|
||||
"endpoints": {
|
||||
"chat_completions": true,
|
||||
"messages": false,
|
||||
"responses": false,
|
||||
"embeddings": false,
|
||||
"image_generations": false,
|
||||
"audio_transcriptions": false,
|
||||
"audio_speech": false,
|
||||
"moderations": false,
|
||||
"batches": false,
|
||||
"rerank": false,
|
||||
"a2a": false,
|
||||
"interactions": false
|
||||
}
|
||||
},
|
||||
"bedrock": {
|
||||
"display_name": "AWS - Bedrock (`bedrock`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/bedrock",
|
||||
|
|
@ -1888,6 +1906,23 @@
|
|||
"interactions": true
|
||||
}
|
||||
},
|
||||
"searchapi": {
|
||||
"display_name": "SearchAPI.io (`searchapi`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/searchapi",
|
||||
"endpoints": {
|
||||
"chat_completions": false,
|
||||
"messages": false,
|
||||
"responses": false,
|
||||
"embeddings": false,
|
||||
"image_generations": false,
|
||||
"audio_transcriptions": false,
|
||||
"audio_speech": false,
|
||||
"moderations": false,
|
||||
"batches": false,
|
||||
"rerank": false,
|
||||
"search": true
|
||||
}
|
||||
},
|
||||
"searxng": {
|
||||
"display_name": "SearXNG (`searxng`)",
|
||||
"url": "https://docs.litellm.ai/docs/search/searxng",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ model LiteLLM_AgentsTable {
|
|||
created_by String
|
||||
updated_at DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
updated_by String
|
||||
static_headers Json? @default("{}")
|
||||
extra_headers String[] @default([])
|
||||
}
|
||||
|
||||
model LiteLLM_OrganizationTable {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ async def test_health_and_chat_completion():
|
|||
async with session.get("http://0.0.0.0:4000/health/readiness") as response:
|
||||
assert response.status == 200
|
||||
readiness_response = await response.json()
|
||||
assert readiness_response["status"] == "connected"
|
||||
assert readiness_response["status"] == "healthy"
|
||||
|
||||
# Test liveness endpoint
|
||||
async with session.get("http://0.0.0.0:4000/health/liveness") as response:
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@ async def test_mcp_http_transport_tool_not_found():
|
|||
@pytest.mark.asyncio
|
||||
async def test_streamable_http_mcp_handler_mock():
|
||||
"""Test the streamable HTTP MCP handler functionality"""
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
||||
# Mock the session manager and its methods
|
||||
mock_session_manager = AsyncMock()
|
||||
|
|
@ -413,13 +414,35 @@ async def test_streamable_http_mcp_handler_mock():
|
|||
mock_receive = AsyncMock()
|
||||
mock_send = AsyncMock()
|
||||
|
||||
mock_auth_result = (
|
||||
UserAPIKeyAuth(),
|
||||
None,
|
||||
None,
|
||||
{},
|
||||
{},
|
||||
[],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED",
|
||||
True,
|
||||
), patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.session_manager",
|
||||
mock_session_manager,
|
||||
):
|
||||
), patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context",
|
||||
new=AsyncMock(return_value=mock_auth_result),
|
||||
), patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.set_auth_context",
|
||||
), patch(
|
||||
"litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session",
|
||||
new=AsyncMock(return_value=False),
|
||||
), patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.IPAddressUtils",
|
||||
), patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.MCPDebug",
|
||||
) as mock_mcp_debug:
|
||||
mock_mcp_debug.maybe_build_debug_headers.return_value = None
|
||||
from litellm.proxy._experimental.mcp_server.server import (
|
||||
handle_streamable_http_mcp,
|
||||
)
|
||||
|
|
@ -1453,6 +1476,9 @@ async def test_add_update_server_with_alias():
|
|||
mock_mcp_server.authorization_url = None
|
||||
mock_mcp_server.registration_url = None
|
||||
mock_mcp_server.token_url = None
|
||||
mock_mcp_server.tool_name_to_display_name = None
|
||||
mock_mcp_server.tool_name_to_description = None
|
||||
mock_mcp_server.byok_api_key_help_url = None
|
||||
|
||||
# Add server to manager
|
||||
await test_manager.add_server(mock_mcp_server)
|
||||
|
|
@ -1494,6 +1520,9 @@ async def test_add_update_server_without_alias():
|
|||
mock_mcp_server.authorization_url = None
|
||||
mock_mcp_server.registration_url = None
|
||||
mock_mcp_server.token_url = None
|
||||
mock_mcp_server.tool_name_to_display_name = None
|
||||
mock_mcp_server.tool_name_to_description = None
|
||||
mock_mcp_server.byok_api_key_help_url = None
|
||||
|
||||
# Add server to manager
|
||||
await test_manager.add_server(mock_mcp_server)
|
||||
|
|
@ -1535,6 +1564,9 @@ async def test_add_update_server_fallback_to_server_id():
|
|||
mock_mcp_server.authorization_url = None
|
||||
mock_mcp_server.registration_url = None
|
||||
mock_mcp_server.token_url = None
|
||||
mock_mcp_server.tool_name_to_display_name = None
|
||||
mock_mcp_server.tool_name_to_description = None
|
||||
mock_mcp_server.byok_api_key_help_url = None
|
||||
|
||||
# Add server to manager
|
||||
await test_manager.add_server(mock_mcp_server)
|
||||
|
|
|
|||
|
|
@ -2228,3 +2228,49 @@ def test_get_router_model_info_with_deployment_object():
|
|||
# Verify we got valid model info back
|
||||
assert model_info is not None
|
||||
assert isinstance(model_info, dict)
|
||||
|
||||
|
||||
def test_combine_fallback_usage_merges_usage():
|
||||
"""Test that _combine_fallback_usage sets usage on the chunk when called with None prior."""
|
||||
from litellm.types.utils import Usage
|
||||
|
||||
router = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "gpt-4",
|
||||
"litellm_params": {"model": "gpt-4", "api_key": "test-key"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# Build a mock chunk with existing usage (no spec to avoid Pydantic model_fields issues)
|
||||
fallback_item = MagicMock()
|
||||
fallback_item.usage = Usage(prompt_tokens=5, completion_tokens=10, total_tokens=15)
|
||||
|
||||
# Combining with None prior should not raise
|
||||
router._combine_fallback_usage(fallback_item, None)
|
||||
|
||||
# usage attribute should be set after combining
|
||||
assert fallback_item.usage is not None
|
||||
|
||||
|
||||
def test_combine_fallback_usage_none_prior():
|
||||
"""Test _combine_fallback_usage with no usage on the chunk and None prior."""
|
||||
router = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "gpt-4",
|
||||
"litellm_params": {"model": "gpt-4", "api_key": "test-key"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# fallback_item with no usage
|
||||
fallback_item = MagicMock()
|
||||
fallback_item.usage = None
|
||||
|
||||
# Should not raise even when both sides have no usage
|
||||
router._combine_fallback_usage(fallback_item, None)
|
||||
|
||||
# usage attribute should be set (may be None or a Usage object)
|
||||
assert hasattr(fallback_item, "usage")
|
||||
|
|
|
|||
|
|
@ -183,13 +183,13 @@ def test_azure_gpt5_1_temperature_with_reasoning_effort_other_values(config: Azu
|
|||
def test_azure_gpt5_1_series_temperature_handling(config: AzureOpenAIGPT5Config):
|
||||
"""Test that Azure GPT-5.1 with gpt5_series prefix supports temperature with reasoning_effort='none'."""
|
||||
params = config.map_openai_params(
|
||||
non_default_params={"temperature": 0.6},
|
||||
non_default_params={"temperature": 1},
|
||||
optional_params={},
|
||||
model="gpt5_series/gpt-5.1",
|
||||
drop_params=False,
|
||||
api_version="2024-05-01-preview",
|
||||
)
|
||||
assert params["temperature"] == 0.6
|
||||
assert params["temperature"] == 1
|
||||
|
||||
|
||||
def test_azure_gpt5_reasoning_effort_none_error(config: AzureOpenAIGPT5Config):
|
||||
|
|
|
|||
|
|
@ -234,60 +234,27 @@ class TestHostedVLLMEmbeddingTransformation:
|
|||
|
||||
def test_encoding_format_not_sent_in_actual_request(self):
|
||||
"""
|
||||
E2E test that encoding_format is not sent when not provided.
|
||||
|
||||
This test mocks the HTTP client to verify the actual request payload.
|
||||
Test that encoding_format is not included in the request body when not provided.
|
||||
|
||||
Tests the transformation layer directly to avoid flaky parallel-test failures
|
||||
caused by global litellm state contamination (pytest-xdist -n 16).
|
||||
The transformation is what controls whether encoding_format appears in the
|
||||
outgoing request payload; this is the correct unit to test.
|
||||
"""
|
||||
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
||||
# Simulate the full path: empty optional_params (no encoding_format provided)
|
||||
result = self.config.transform_embedding_request(
|
||||
model=self.model,
|
||||
input=["Hello world"],
|
||||
optional_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
client = HTTPHandler()
|
||||
|
||||
with patch.object(client, "post") as mock_post:
|
||||
# Mock response
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {"content-type": "application/json"}
|
||||
mock_response.json.return_value = {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"object": "embedding",
|
||||
"index": 0,
|
||||
"embedding": [0.1, 0.2, 0.3, 0.4, 0.5],
|
||||
}
|
||||
],
|
||||
"model": "BAAI/bge-small-en-v1.5",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"total_tokens": 5,
|
||||
},
|
||||
}
|
||||
mock_response.text = json.dumps(mock_response.json.return_value)
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
try:
|
||||
litellm.embedding(
|
||||
model=self.model,
|
||||
input=["Hello world"],
|
||||
api_base="https://test-vllm.example.com/v1",
|
||||
client=client,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Verify the request was made
|
||||
mock_post.assert_called_once()
|
||||
|
||||
# Get the data that was sent
|
||||
call_kwargs = mock_post.call_args[1]
|
||||
sent_data = json.loads(call_kwargs["data"])
|
||||
|
||||
# Assert that encoding_format is NOT in the sent data
|
||||
assert "encoding_format" not in sent_data, (
|
||||
"encoding_format should not be in request when not provided"
|
||||
)
|
||||
assert sent_data["model"] == "BAAI/bge-small-en-v1.5"
|
||||
assert sent_data["input"] == ["Hello world"]
|
||||
# Assert that encoding_format is NOT in the sent data
|
||||
assert "encoding_format" not in result, (
|
||||
"encoding_format should not be in request when not provided"
|
||||
)
|
||||
assert result["model"] == "BAAI/bge-small-en-v1.5"
|
||||
assert result["input"] == ["Hello world"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ reasoning.encrypted_content for multi-turn stateless workflows.
|
|||
Related issue: https://github.com/BerriAI/litellm/issues/22189
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import litellm
|
||||
from litellm.llms.openrouter.responses.transformation import (
|
||||
OpenRouterResponsesAPIConfig,
|
||||
|
|
@ -65,15 +67,19 @@ class TestOpenRouterResponsesAPIConfig:
|
|||
config = OpenRouterResponsesAPIConfig()
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
try:
|
||||
config.validate_environment(
|
||||
headers={},
|
||||
model="openai/o4-mini",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
)
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError as e:
|
||||
assert "OpenRouter API key is required" in str(e)
|
||||
with patch(
|
||||
"litellm.llms.openrouter.responses.transformation.get_secret_str",
|
||||
return_value=None,
|
||||
), patch.object(litellm, "api_key", None):
|
||||
try:
|
||||
config.validate_environment(
|
||||
headers={},
|
||||
model="openai/o4-mini",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
)
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError as e:
|
||||
assert "OpenRouter API key is required" in str(e)
|
||||
|
||||
|
||||
class TestOpenRouterResponsesAPIRegistration:
|
||||
|
|
|
|||
|
|
@ -764,6 +764,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
|
|||
"/v1/audio/transcriptions",
|
||||
"/v1/audio/speech",
|
||||
"/v1/ocr",
|
||||
"/vertex_ai/live",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -804,6 +805,8 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
|
|||
},
|
||||
},
|
||||
"supports_native_streaming": {"type": "boolean"},
|
||||
"supports_none_reasoning_effort": {"type": "boolean"},
|
||||
"supports_xhigh_reasoning_effort": {"type": "boolean"},
|
||||
"tiered_pricing": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue