mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-04-28 03:20:01 +00:00
migrated from httpx client to openai client
This commit is contained in:
parent
3acabcb8e0
commit
7c54008a95
5 changed files with 342 additions and 367 deletions
|
|
@ -78,7 +78,6 @@ class RequestBuilderMixin:
|
|||
"model": request_data.model,
|
||||
"messages": messages,
|
||||
"max_tokens": request_data.max_tokens,
|
||||
"stream": stream,
|
||||
}
|
||||
|
||||
if request_data.temperature is not None:
|
||||
|
|
@ -90,21 +89,24 @@ class RequestBuilderMixin:
|
|||
if request_data.tools:
|
||||
body["tools"] = AnthropicToOpenAIConverter.convert_tools(request_data.tools)
|
||||
|
||||
# Handle non-standard parameters via extra_body
|
||||
extra_params = request_data.extra_body.copy() if request_data.extra_body else {}
|
||||
|
||||
# Handle thinking/reasoning mode
|
||||
extra_body = request_data.extra_body.copy() if request_data.extra_body else {}
|
||||
if request_data.thinking and getattr(request_data.thinking, "enabled", True):
|
||||
extra_body.setdefault("thinking", {"type": "enabled"})
|
||||
extra_body.setdefault("reasoning_split", True)
|
||||
extra_body.setdefault(
|
||||
extra_params.setdefault("thinking", {"type": "enabled"})
|
||||
extra_params.setdefault("reasoning_split", True)
|
||||
extra_params.setdefault(
|
||||
"chat_template_kwargs",
|
||||
{"thinking": True, "reasoning_split": True, "clear_thinking": False},
|
||||
)
|
||||
|
||||
body.update(extra_body)
|
||||
if extra_params:
|
||||
body["extra_body"] = extra_params
|
||||
|
||||
# Apply NIM defaults
|
||||
for key, val in self._nim_params.items():
|
||||
if key not in body:
|
||||
if key not in body and key not in extra_params:
|
||||
body[key] = val
|
||||
|
||||
return body
|
||||
|
|
@ -117,38 +119,38 @@ class ErrorMapperMixin:
|
|||
ProviderError subclasses for standardized error handling.
|
||||
"""
|
||||
|
||||
def _map_error(self, response_status: int, error_text: str) -> Exception:
|
||||
"""Map HTTP status and error body to specific ProviderError.
|
||||
def _map_error(self, e: Exception) -> Exception:
|
||||
"""Map OpenAI exception to specific ProviderError.
|
||||
|
||||
Args:
|
||||
response_status: HTTP status code
|
||||
error_text: Raw error response body
|
||||
e: The OpenAI exception to map
|
||||
|
||||
Returns:
|
||||
Appropriate ProviderError subclass instance
|
||||
"""
|
||||
try:
|
||||
error_data = json.loads(error_text)
|
||||
message = error_data.get("error", {}).get("message", error_text)
|
||||
except Exception:
|
||||
message = error_text
|
||||
import openai
|
||||
|
||||
if response_status == 401:
|
||||
return AuthenticationError(message, raw_error=error_text)
|
||||
if response_status == 429:
|
||||
if isinstance(e, openai.AuthenticationError):
|
||||
return AuthenticationError(str(e), raw_error=str(e))
|
||||
if isinstance(e, openai.RateLimitError):
|
||||
# Trigger global rate limit block
|
||||
from .rate_limit import GlobalRateLimiter
|
||||
|
||||
GlobalRateLimiter.get_instance().set_blocked(60) # Default 60s cooldown
|
||||
return RateLimitError(message, raw_error=error_text)
|
||||
if response_status in (400, 422):
|
||||
return InvalidRequestError(message, raw_error=error_text)
|
||||
if response_status >= 500:
|
||||
return RateLimitError(str(e), raw_error=str(e))
|
||||
if isinstance(e, openai.BadRequestError):
|
||||
return InvalidRequestError(str(e), raw_error=str(e))
|
||||
if isinstance(e, openai.InternalServerError):
|
||||
message = str(e)
|
||||
if "overloaded" in message.lower() or "capacity" in message.lower():
|
||||
return OverloadedError(message, raw_error=error_text)
|
||||
return APIError(message, status_code=response_status, raw_error=error_text)
|
||||
return OverloadedError(message, raw_error=str(e))
|
||||
return APIError(message, status_code=500, raw_error=str(e))
|
||||
if isinstance(e, openai.APIError):
|
||||
return APIError(
|
||||
str(e), status_code=getattr(e, "status_code", 500), raw_error=str(e)
|
||||
)
|
||||
|
||||
return APIError(message, status_code=response_status, raw_error=error_text)
|
||||
return e
|
||||
|
||||
|
||||
class ResponseConverterMixin:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
"""NVIDIA NIM provider - converts Anthropic format to OpenAI format for NIM."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
from typing import Any, AsyncIterator
|
||||
|
||||
import httpx
|
||||
from httpx import TimeoutException, ConnectTimeout
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from .base import BaseProvider, ProviderConfig
|
||||
from .utils import (
|
||||
|
|
@ -23,7 +20,6 @@ from .exceptions import (
|
|||
)
|
||||
from .nvidia_mixins import (
|
||||
RequestBuilderMixin,
|
||||
StreamProcessorMixin,
|
||||
ErrorMapperMixin,
|
||||
ResponseConverterMixin,
|
||||
)
|
||||
|
|
@ -34,12 +30,11 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class NvidiaNimProvider(
|
||||
RequestBuilderMixin,
|
||||
StreamProcessorMixin,
|
||||
ErrorMapperMixin,
|
||||
ResponseConverterMixin,
|
||||
BaseProvider,
|
||||
):
|
||||
"""NVIDIA NIM provider using direct httpx requests."""
|
||||
"""NVIDIA NIM provider using official OpenAI client."""
|
||||
|
||||
def __init__(self, config: ProviderConfig):
|
||||
super().__init__(config)
|
||||
|
|
@ -50,20 +45,21 @@ class NvidiaNimProvider(
|
|||
).rstrip("/")
|
||||
self._nim_params = self._load_nim_params()
|
||||
self._global_rate_limiter = GlobalRateLimiter.get_instance()
|
||||
self._client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(300.0, connect=30.0, read=60.0),
|
||||
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
||||
self._client = AsyncOpenAI(
|
||||
api_key=self._api_key,
|
||||
base_url=self._base_url,
|
||||
max_retries=2,
|
||||
timeout=300.0,
|
||||
)
|
||||
|
||||
async def stream_response(
|
||||
self, request: Any, input_tokens: int = 0
|
||||
) -> AsyncIterator[str]:
|
||||
"""Stream response in Anthropic SSE format."""
|
||||
# Wait if globally rate limited (proactive throttle + reactive block)
|
||||
# Wait if globally rate limited
|
||||
waited_reactively = await self._global_rate_limiter.wait_if_blocked()
|
||||
|
||||
if waited_reactively:
|
||||
# Yield error event for reactive rate limit blocking (user feedback)
|
||||
message_id = f"msg_{uuid.uuid4()}"
|
||||
sse = SSEBuilder(message_id, request.model, input_tokens)
|
||||
error_msg = "⏱️ Global rate limit active. Resuming now..."
|
||||
|
|
@ -71,20 +67,12 @@ class NvidiaNimProvider(
|
|||
yield sse.message_start()
|
||||
for event in sse.emit_error(error_msg):
|
||||
yield event
|
||||
# After notification, we continue to the actual request
|
||||
|
||||
body = self._build_request_body(request, stream=True)
|
||||
# Log compact request summary
|
||||
logger.info(
|
||||
f"NIM_STREAM: model={body.get('model')} msgs={len(body.get('messages', []))} tools={len(body.get('tools', []))}"
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "text/event-stream",
|
||||
}
|
||||
|
||||
message_id = f"msg_{uuid.uuid4()}"
|
||||
sse = SSEBuilder(message_id, request.model, input_tokens)
|
||||
think_parser = ThinkTagParser()
|
||||
|
|
@ -98,48 +86,46 @@ class NvidiaNimProvider(
|
|||
error_message = ""
|
||||
|
||||
try:
|
||||
async for chunk_json in self._stream_chunks(headers, body):
|
||||
# Process metadata
|
||||
if "usage" in chunk_json:
|
||||
usage_info = chunk_json["usage"]
|
||||
stream = await self._client.chat.completions.create(**body, stream=True)
|
||||
async for chunk in stream:
|
||||
# OpenAI client returns objects, not JSON
|
||||
if getattr(chunk, "usage", None):
|
||||
usage_info = chunk.usage
|
||||
|
||||
if "choices" not in chunk_json or not chunk_json["choices"]:
|
||||
if not chunk.choices:
|
||||
continue
|
||||
|
||||
choice = chunk_json["choices"][0]
|
||||
delta = choice.get("delta", {})
|
||||
choice = chunk.choices[0]
|
||||
delta = choice.delta
|
||||
|
||||
if choice.get("finish_reason"):
|
||||
finish_reason = choice["finish_reason"]
|
||||
if choice.finish_reason:
|
||||
finish_reason = choice.finish_reason
|
||||
logger.debug(f"NIM finish_reason: {finish_reason}")
|
||||
|
||||
# Handle reasoning content from delta
|
||||
reasoning = extract_reasoning_from_delta(delta)
|
||||
reasoning = getattr(delta, "reasoning_content", None)
|
||||
if reasoning:
|
||||
for event in sse.ensure_thinking_block():
|
||||
yield event
|
||||
yield sse.emit_thinking_delta(reasoning)
|
||||
|
||||
# Handle text content with think tag and heuristic tool parsing
|
||||
if delta.get("content"):
|
||||
for chunk in think_parser.feed(delta["content"]):
|
||||
if chunk.type == ContentType.THINKING:
|
||||
# Handle text content
|
||||
if delta.content:
|
||||
for part in think_parser.feed(delta.content):
|
||||
if part.type == ContentType.THINKING:
|
||||
for event in sse.ensure_thinking_block():
|
||||
yield event
|
||||
yield sse.emit_thinking_delta(chunk.content)
|
||||
yield sse.emit_thinking_delta(part.content)
|
||||
else:
|
||||
# Pass non-thinking text through heuristic tool parser
|
||||
filtered_text, detected_tools = heuristic_parser.feed(
|
||||
chunk.content
|
||||
part.content
|
||||
)
|
||||
|
||||
# Emit filtered text if any
|
||||
if filtered_text:
|
||||
for event in sse.ensure_text_block():
|
||||
yield event
|
||||
yield sse.emit_text_delta(filtered_text)
|
||||
|
||||
# Emit detected heuristic tool calls
|
||||
for tool_use in detected_tools:
|
||||
for event in sse.close_content_blocks():
|
||||
yield event
|
||||
|
|
@ -159,34 +145,34 @@ class NvidiaNimProvider(
|
|||
yield sse.content_block_stop(block_idx)
|
||||
|
||||
# Handle native tool calls
|
||||
if delta.get("tool_calls"):
|
||||
if delta.tool_calls:
|
||||
for event in sse.close_content_blocks():
|
||||
yield event
|
||||
for tc in delta["tool_calls"]:
|
||||
for event in self._process_tool_call(tc, sse):
|
||||
for tc in delta.tool_calls:
|
||||
# Convert OpenAI tool call object to dict for existing logic
|
||||
tc_info = {
|
||||
"index": tc.index,
|
||||
"id": tc.id,
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments,
|
||||
},
|
||||
}
|
||||
for event in self._process_tool_call(tc_info, sse):
|
||||
yield event
|
||||
|
||||
except TimeoutException as e:
|
||||
timeout_type = "connect" if isinstance(e, ConnectTimeout) else "read"
|
||||
logger.error(f"NIM_TIMEOUT: type={timeout_type} model={body.get('model')}")
|
||||
error_occurred = True
|
||||
error_message = (
|
||||
f"⏱️ API Timeout ({timeout_type}): Request exceeded time limit"
|
||||
)
|
||||
logger.info("NIM_STREAM: Emitting SSE error event for timeout")
|
||||
except Exception as e:
|
||||
logger.error(f"NIM_ERROR: {type(e).__name__}: {e}")
|
||||
mapped_e = self._map_error(e)
|
||||
error_occurred = True
|
||||
error_message = str(e)
|
||||
logger.info("NIM_STREAM: Emitting SSE error event for exception")
|
||||
error_message = str(mapped_e)
|
||||
logger.info(f"NIM_STREAM: Emitting SSE error event for {type(e).__name__}")
|
||||
|
||||
# Handle errors
|
||||
if error_occurred:
|
||||
for event in sse.emit_error(error_message):
|
||||
yield event
|
||||
logger.info("NIM_STREAM: Error event yielded, total events emitted")
|
||||
|
||||
# Flush remaining content from parsers
|
||||
# Flush remaining content
|
||||
remaining = think_parser.flush()
|
||||
if remaining:
|
||||
if remaining.type == ContentType.THINKING:
|
||||
|
|
@ -198,7 +184,6 @@ class NvidiaNimProvider(
|
|||
yield event
|
||||
yield sse.emit_text_delta(remaining.content)
|
||||
|
||||
# Flush heuristic tool calls
|
||||
for tool_use in heuristic_parser.flush():
|
||||
for event in sse.close_content_blocks():
|
||||
yield event
|
||||
|
|
@ -217,8 +202,6 @@ class NvidiaNimProvider(
|
|||
)
|
||||
yield sse.content_block_stop(block_idx)
|
||||
|
||||
# Ensure at least one text block exists to avoid "(no content)" in Claude Code
|
||||
# This handles cases where model only returns thinking or empty responses
|
||||
if (
|
||||
not error_occurred
|
||||
and sse.blocks.text_index == -1
|
||||
|
|
@ -226,82 +209,76 @@ class NvidiaNimProvider(
|
|||
):
|
||||
for event in sse.ensure_text_block():
|
||||
yield event
|
||||
yield sse.emit_text_delta(" ") # Single space placeholder
|
||||
yield sse.emit_text_delta(" ")
|
||||
|
||||
# Close all blocks
|
||||
for event in sse.close_all_blocks():
|
||||
yield event
|
||||
|
||||
# Final events
|
||||
output_tokens = (
|
||||
usage_info.get("completion_tokens", 0)
|
||||
if usage_info
|
||||
usage_info.completion_tokens
|
||||
if usage_info and hasattr(usage_info, "completion_tokens")
|
||||
else sse.estimate_output_tokens()
|
||||
)
|
||||
yield sse.message_delta(map_stop_reason(finish_reason), output_tokens)
|
||||
yield sse.message_stop()
|
||||
yield sse.done()
|
||||
|
||||
async def _stream_chunks(self, headers: dict, body: dict):
|
||||
"""Generator that yields parsed SSE data chunks from the API."""
|
||||
async with self._client.stream(
|
||||
"POST", f"{self._base_url}/chat/completions", headers=headers, json=body
|
||||
) as response:
|
||||
if response.status_code != 200:
|
||||
error_text = await response.aread()
|
||||
raise self._map_error(
|
||||
response_status=response.status_code,
|
||||
error_text=error_text.decode("utf-8", errors="replace"),
|
||||
)
|
||||
|
||||
buffer = ""
|
||||
async for chunk in response.aiter_text():
|
||||
buffer += chunk
|
||||
while "\n\n" in buffer:
|
||||
event_end = buffer.index("\n\n")
|
||||
event_data = buffer[:event_end]
|
||||
buffer = buffer[event_end + 2 :]
|
||||
|
||||
parsed = self._parse_sse_event(event_data)
|
||||
if parsed is not None:
|
||||
yield parsed
|
||||
|
||||
# Flush remaining non-empty buffer
|
||||
if buffer.strip():
|
||||
parsed = self._parse_sse_event(buffer)
|
||||
if parsed is not None:
|
||||
yield parsed
|
||||
|
||||
async def complete(self, request: Any) -> dict:
|
||||
"""Make a non-streaming completion request."""
|
||||
# Wait if globally rate limited (proactive throttle + reactive block)
|
||||
await self._global_rate_limiter.wait_if_blocked()
|
||||
|
||||
body = self._build_request_body(request, stream=False)
|
||||
# Log compact request summary
|
||||
logger.info(
|
||||
f"NIM_COMPLETE: model={body.get('model')} msgs={len(body.get('messages', []))} tools={len(body.get('tools', []))}"
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self._client.post(
|
||||
f"{self._base_url}/chat/completions", headers=headers, json=body
|
||||
)
|
||||
except TimeoutException as e:
|
||||
timeout_type = "connect" if isinstance(e, ConnectTimeout) else "read"
|
||||
logger.error(f"NIM_TIMEOUT: type={timeout_type} model={body.get('model')}")
|
||||
raise APIError(
|
||||
f"API Timeout ({timeout_type}): Request exceeded time limit",
|
||||
status_code=504,
|
||||
response = await self._client.chat.completions.create(**body)
|
||||
# ResponseconverterMixin expects a dict
|
||||
return response.model_dump()
|
||||
except Exception as e:
|
||||
logger.error(f"NIM_ERROR: {type(e).__name__}: {e}")
|
||||
raise self._map_error(e)
|
||||
|
||||
def _process_tool_call(self, tc: dict, sse: Any):
|
||||
"""Process a single tool call delta and yield SSE events.
|
||||
|
||||
Args:
|
||||
tc: Tool call delta info dict
|
||||
sse: SSEBuilder instance
|
||||
"""
|
||||
import uuid
|
||||
|
||||
tc_index = tc.get("index", 0)
|
||||
if tc_index < 0:
|
||||
tc_index = len(sse.blocks.tool_indices)
|
||||
|
||||
fn_delta = tc.get("function", {})
|
||||
if fn_delta.get("name") is not None:
|
||||
sse.blocks.tool_names[tc_index] = (
|
||||
sse.blocks.tool_names.get(tc_index, "") + fn_delta["name"]
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise self._map_error(
|
||||
response_status=response.status_code, error_text=response.text
|
||||
)
|
||||
return response.json()
|
||||
if tc_index not in sse.blocks.tool_indices:
|
||||
name = sse.blocks.tool_names.get(tc_index, "")
|
||||
if name or tc.get("id"):
|
||||
tool_id = tc.get("id") or f"tool_{uuid.uuid4()}"
|
||||
yield sse.start_tool_block(tc_index, tool_id, name)
|
||||
sse.blocks.tool_started[tc_index] = True
|
||||
elif not sse.blocks.tool_started.get(tc_index) and sse.blocks.tool_names.get(
|
||||
tc_index
|
||||
):
|
||||
tool_id = tc.get("id") or f"tool_{uuid.uuid4()}"
|
||||
name = sse.blocks.tool_names[tc_index]
|
||||
yield sse.start_tool_block(tc_index, tool_id, name)
|
||||
sse.blocks.tool_started[tc_index] = True
|
||||
|
||||
args = fn_delta.get("arguments", "")
|
||||
if args:
|
||||
if not sse.blocks.tool_started.get(tc_index):
|
||||
tool_id = tc.get("id") or f"tool_{uuid.uuid4()}"
|
||||
name = sse.blocks.tool_names.get(tc_index, "tool_call") or "tool_call"
|
||||
yield sse.start_tool_block(tc_index, tool_id, name)
|
||||
sse.blocks.tool_started[tc_index] = True
|
||||
|
||||
yield sse.emit_tool_delta(tc_index, args)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ dependencies = [
|
|||
"python-telegram-bot>=21.0",
|
||||
"pydantic-settings>=2.12.0",
|
||||
"aiolimiter>=1.2.1",
|
||||
"openai>=2.16.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import pytest
|
||||
import json
|
||||
import httpx
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
from providers.nvidia_nim import (
|
||||
NvidiaNimProvider,
|
||||
|
|
@ -39,19 +38,6 @@ class MockRequest:
|
|||
setattr(self, k, v)
|
||||
|
||||
|
||||
class MockThinking:
|
||||
def __init__(self, enabled=True, budget_tokens=1000):
|
||||
self.enabled = enabled
|
||||
self.budget_tokens = budget_tokens
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_httpx_ssl():
|
||||
"""Mock ssl context for older httpx versions if needed, or just bypass."""
|
||||
# This is often needed if the code under test creates an SSL context
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_rate_limiter():
|
||||
"""Mock the global rate limiter to prevent waiting."""
|
||||
|
|
@ -64,9 +50,11 @@ def mock_rate_limiter():
|
|||
@pytest.mark.asyncio
|
||||
async def test_init(provider_config):
|
||||
"""Test provider initialization."""
|
||||
provider = NvidiaNimProvider(provider_config)
|
||||
assert provider._api_key == "test_key"
|
||||
assert provider._base_url == "https://test.api.nvidia.com/v1"
|
||||
with patch("providers.nvidia_nim.AsyncOpenAI") as mock_openai:
|
||||
provider = NvidiaNimProvider(provider_config)
|
||||
assert provider._api_key == "test_key"
|
||||
assert provider._base_url == "https://test.api.nvidia.com/v1"
|
||||
mock_openai.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -76,91 +64,58 @@ async def test_build_request_body(nim_provider):
|
|||
body = nim_provider._build_request_body(req, stream=True)
|
||||
|
||||
assert body["model"] == "test-model"
|
||||
assert body["stream"] is True
|
||||
assert body["temperature"] == 0.5
|
||||
assert len(body["messages"]) == 2 # System + User
|
||||
assert body["messages"][0]["role"] == "system"
|
||||
assert body["messages"][0]["content"] == "System prompt"
|
||||
assert "thinking" in body
|
||||
assert body["thinking"]["type"] == "enabled"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_request_body_with_tools(nim_provider):
|
||||
"""Test request body with tools."""
|
||||
tool = MockTool(
|
||||
name="test_tool",
|
||||
description="A test tool",
|
||||
input_schema={"type": "object", "properties": {"arg": {"type": "string"}}},
|
||||
)
|
||||
req = MockRequest(tools=[tool])
|
||||
body = nim_provider._build_request_body(req)
|
||||
|
||||
assert "tools" in body
|
||||
assert len(body["tools"]) == 1
|
||||
assert body["tools"][0]["function"]["name"] == "test_tool"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_request_body_deepseek(nim_provider):
|
||||
"""Test request body with DeepSeek model."""
|
||||
req = MockRequest(model="deepseek-ai/deepseek-r1")
|
||||
body = nim_provider._build_request_body(req)
|
||||
|
||||
assert "chat_template_kwargs" in body
|
||||
assert body["chat_template_kwargs"] == {"thinking": True}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_request_body_non_deepseek(nim_provider):
|
||||
"""Test request body with non-DeepSeek model."""
|
||||
req = MockRequest(model="meta/llama-3.3-70b-instruct")
|
||||
body = nim_provider._build_request_body(req)
|
||||
|
||||
assert "chat_template_kwargs" not in body
|
||||
assert "extra_body" in body
|
||||
assert "thinking" in body["extra_body"]
|
||||
assert body["extra_body"]["thinking"]["type"] == "enabled"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_text(nim_provider):
|
||||
"""Test streaming text response."""
|
||||
# Mock stream response
|
||||
req = MockRequest()
|
||||
|
||||
mock_chunks = [
|
||||
'data: {"id":"1","choices":[{"delta":{"content":"Hello"}}]}\n\n',
|
||||
'data: {"id":"1","choices":[{"delta":{"content":" World"}}]}\n\n',
|
||||
"data: [DONE]\n\n",
|
||||
# Create mock chunks
|
||||
mock_chunk1 = MagicMock()
|
||||
mock_chunk1.choices = [
|
||||
MagicMock(
|
||||
delta=MagicMock(content="Hello", reasoning_content=""), finish_reason=None
|
||||
)
|
||||
]
|
||||
mock_chunk1.usage = None
|
||||
|
||||
# Create a mock response that yields chunks
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 200
|
||||
mock_chunk2 = MagicMock()
|
||||
mock_chunk2.choices = [
|
||||
MagicMock(
|
||||
delta=MagicMock(content=" World", reasoning_content=""),
|
||||
finish_reason="stop",
|
||||
)
|
||||
]
|
||||
mock_chunk2.usage = MagicMock(completion_tokens=10)
|
||||
|
||||
async def mock_aiter():
|
||||
for chunk in mock_chunks:
|
||||
yield chunk
|
||||
async def mock_stream():
|
||||
yield mock_chunk1
|
||||
yield mock_chunk2
|
||||
|
||||
mock_response.aiter_text = mock_aiter
|
||||
mock_response.aread = AsyncMock(return_value=b"")
|
||||
with patch.object(
|
||||
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
|
||||
# Mock the stream context manager
|
||||
mock_stream_ctx = AsyncMock()
|
||||
mock_stream_ctx.__aenter__.return_value = mock_response
|
||||
|
||||
with patch.object(nim_provider._client, "stream", return_value=mock_stream_ctx):
|
||||
events = []
|
||||
async for event in nim_provider.stream_response(req):
|
||||
events.append(event)
|
||||
|
||||
# Verify events
|
||||
assert len(events) > 0
|
||||
assert "event: message_start" in events[0]
|
||||
|
||||
# Check text content
|
||||
text_content = ""
|
||||
for e in events:
|
||||
if "event: content_block_delta" in e and '"text_delta"' in e:
|
||||
# Extract json from data: ...
|
||||
for line in e.splitlines():
|
||||
if line.startswith("data: "):
|
||||
data = json.loads(line[6:])
|
||||
|
|
@ -171,107 +126,38 @@ async def test_stream_response_text(nim_provider):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_thinking_interleaved(nim_provider):
|
||||
"""Test streaming with interleaved thinking tags."""
|
||||
async def test_stream_response_thinking_reasoning_content(nim_provider):
|
||||
"""Test streaming with native reasoning_content."""
|
||||
req = MockRequest()
|
||||
|
||||
mock_chunks = [
|
||||
'data: {"choices":[{"delta":{"content":"<think>Thinking process..."}}]}\n\n',
|
||||
'data: {"choices":[{"delta":{"content":"...</think>Answer"}}]}\n\n',
|
||||
"data: [DONE]\n\n",
|
||||
mock_chunk = MagicMock()
|
||||
mock_chunk.choices = [
|
||||
MagicMock(
|
||||
delta=MagicMock(content=None, reasoning_content="Thinking..."),
|
||||
finish_reason=None,
|
||||
)
|
||||
]
|
||||
mock_chunk.usage = None
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.aiter_text = lambda: (
|
||||
c for c in mock_chunks
|
||||
) # sync iterator wrapper check? No, must be async generator
|
||||
async def mock_stream():
|
||||
yield mock_chunk
|
||||
|
||||
async def mock_aiter():
|
||||
for chunk in mock_chunks:
|
||||
yield chunk
|
||||
with patch.object(
|
||||
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
|
||||
mock_response.aiter_text = mock_aiter
|
||||
|
||||
mock_stream_ctx = AsyncMock()
|
||||
mock_stream_ctx.__aenter__.return_value = mock_response
|
||||
|
||||
with patch.object(nim_provider._client, "stream", return_value=mock_stream_ctx):
|
||||
events = []
|
||||
async for event in nim_provider.stream_response(req):
|
||||
events.append(event)
|
||||
|
||||
# Should have thinking events
|
||||
think_deltas = [
|
||||
e
|
||||
for e in events
|
||||
if "event: content_block_delta" in e and '"thinking_delta"' in e
|
||||
]
|
||||
assert len(think_deltas) > 0
|
||||
|
||||
# Should have text events
|
||||
text_deltas = [
|
||||
e
|
||||
for e in events
|
||||
if "event: content_block_delta" in e and '"text_delta"' in e
|
||||
]
|
||||
assert len(text_deltas) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_error_429(nim_provider):
|
||||
"""Test 429 Rate Limit error."""
|
||||
req = MockRequest()
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 429
|
||||
mock_response.aread = AsyncMock(
|
||||
return_value=b'{"error": {"message": "Too many requests"}}'
|
||||
)
|
||||
|
||||
mock_stream_ctx = AsyncMock()
|
||||
mock_stream_ctx.__aenter__.return_value = mock_response
|
||||
|
||||
with patch.object(nim_provider._client, "stream", return_value=mock_stream_ctx):
|
||||
# The provider yields an error event rather than raising
|
||||
events = []
|
||||
async for event in nim_provider.stream_response(req):
|
||||
events.append(event)
|
||||
|
||||
# Check for error message in text delta
|
||||
found_error = False
|
||||
# Check for thinking_delta
|
||||
found_thinking = False
|
||||
for e in events:
|
||||
if "event: content_block_delta" in e and '"text_delta"' in e:
|
||||
if "Too many requests" in e:
|
||||
found_error = True
|
||||
break
|
||||
assert found_error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_timeout(nim_provider):
|
||||
"""Test timeout handling."""
|
||||
req = MockRequest()
|
||||
|
||||
# Mock stream to raise TimeoutException
|
||||
mock_stream_ctx = AsyncMock()
|
||||
mock_stream_ctx.__aenter__.side_effect = httpx.ConnectTimeout(
|
||||
"Connection timed out"
|
||||
)
|
||||
|
||||
with patch.object(nim_provider._client, "stream", return_value=mock_stream_ctx):
|
||||
events = []
|
||||
async for event in nim_provider.stream_response(req):
|
||||
events.append(event)
|
||||
|
||||
# Check for error message in text delta
|
||||
found_error = False
|
||||
for e in events:
|
||||
if "event: content_block_delta" in e and '"text_delta"' in e:
|
||||
if "Timeout" in e:
|
||||
found_error = True
|
||||
break
|
||||
assert found_error
|
||||
if "event: content_block_delta" in e and '"thinking_delta"' in e:
|
||||
if "Thinking..." in e:
|
||||
found_thinking = True
|
||||
assert found_thinking
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -280,8 +166,7 @@ async def test_complete_success(nim_provider):
|
|||
req = MockRequest()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
mock_response.model_dump.return_value = {
|
||||
"id": "test_id",
|
||||
"choices": [
|
||||
{
|
||||
|
|
@ -293,9 +178,9 @@ async def test_complete_success(nim_provider):
|
|||
}
|
||||
|
||||
with patch.object(
|
||||
nim_provider._client, "post", new_callable=AsyncMock
|
||||
) as mock_post:
|
||||
mock_post.return_value = mock_response
|
||||
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_response
|
||||
|
||||
result = await nim_provider.complete(req)
|
||||
assert result["id"] == "test_id"
|
||||
|
|
@ -303,52 +188,20 @@ async def test_complete_success(nim_provider):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_error_500(nim_provider):
|
||||
"""Test 500 error on completion."""
|
||||
async def test_complete_error_handling(nim_provider):
|
||||
"""Test error handling on completion."""
|
||||
req = MockRequest()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 500
|
||||
mock_response.text = '{"error": {"message": "Internal Server Error"}}'
|
||||
import openai
|
||||
|
||||
with patch.object(
|
||||
nim_provider._client, "post", new_callable=AsyncMock
|
||||
) as mock_post:
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
nim_provider._client.chat.completions,
|
||||
"create",
|
||||
side_effect=openai.APIError("API Error", request=MagicMock(), body=None),
|
||||
):
|
||||
with pytest.raises(APIError) as exc:
|
||||
await nim_provider.complete(req)
|
||||
assert "Internal Server Error" in str(exc.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_convert_response(nim_provider):
|
||||
"""Test response conversion."""
|
||||
req = MockRequest()
|
||||
openai_resp = {
|
||||
"id": "resp_1",
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Response text",
|
||||
"reasoning_content": "Found logic",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {"prompt_tokens": 50, "completion_tokens": 20},
|
||||
}
|
||||
|
||||
result = nim_provider.convert_response(openai_resp, req)
|
||||
|
||||
assert result["id"] == "resp_1"
|
||||
assert result["type"] == "message"
|
||||
assert len(result["content"]) == 2
|
||||
assert result["content"][0]["type"] == "thinking"
|
||||
assert result["content"][0]["thinking"] == "Found logic"
|
||||
assert result["content"][1]["type"] == "text"
|
||||
assert result["content"][1]["text"] == "Response text"
|
||||
assert "API Error" in str(exc.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -356,31 +209,34 @@ async def test_tool_call_stream(nim_provider):
|
|||
"""Test streaming tool calls."""
|
||||
req = MockRequest()
|
||||
|
||||
mock_chunks = [
|
||||
'data: {"choices":[{"delta":{"content":null,"tool_calls":[{"index":0,"id":"call_1","function":{"name":"search"}}]}}]}\n\n',
|
||||
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"q\\": "}}]}}]}\n\n',
|
||||
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"test\\"}"}}]}}]}\n\n',
|
||||
"data: [DONE]\n\n",
|
||||
# Mock tool call delta
|
||||
mock_tc = MagicMock()
|
||||
mock_tc.index = 0
|
||||
mock_tc.id = "call_1"
|
||||
mock_tc.function.name = "search"
|
||||
mock_tc.function.arguments = '{"q": "test"}'
|
||||
|
||||
mock_chunk = MagicMock()
|
||||
mock_chunk.choices = [
|
||||
MagicMock(
|
||||
delta=MagicMock(content=None, reasoning_content="", tool_calls=[mock_tc]),
|
||||
finish_reason=None,
|
||||
)
|
||||
]
|
||||
mock_chunk.usage = None
|
||||
|
||||
async def mock_aiter():
|
||||
for chunk in mock_chunks:
|
||||
yield chunk
|
||||
async def mock_stream():
|
||||
yield mock_chunk
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.aiter_text = mock_aiter
|
||||
with patch.object(
|
||||
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
|
||||
mock_stream_ctx = AsyncMock()
|
||||
mock_stream_ctx.__aenter__.return_value = mock_response
|
||||
|
||||
with patch.object(nim_provider._client, "stream", return_value=mock_stream_ctx):
|
||||
events = []
|
||||
async for event in nim_provider.stream_response(req):
|
||||
events.append(event)
|
||||
|
||||
# Should have content_block_start for tool_use
|
||||
# Looking for event: content_block_start ... type: tool_use
|
||||
starts = [
|
||||
e for e in events if "event: content_block_start" in e and '"tool_use"' in e
|
||||
]
|
||||
|
|
|
|||
139
uv.lock
generated
139
uv.lock
generated
|
|
@ -58,6 +58,7 @@ dependencies = [
|
|||
{ name = "aiolimiter" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "httpx" },
|
||||
{ name = "openai" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-dotenv" },
|
||||
|
|
@ -79,6 +80,7 @@ requires-dist = [
|
|||
{ name = "aiolimiter", specifier = ">=1.2.1" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.11" },
|
||||
{ name = "httpx", specifier = ">=0.25.0" },
|
||||
{ name = "openai", specifier = ">=2.16.0" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.12.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||
|
|
@ -318,6 +320,15 @@ toml = [
|
|||
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.7.0"
|
||||
|
|
@ -534,6 +545,103 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
|
|
@ -613,6 +721,25 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/e4c964fcf1d527fdf4739e7cc940c60075a4114d50d03871d5d5b1e13a88/openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12", size = 629649, upload-time = "2026-01-27T23:28:02.579Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b", size = 1068612, upload-time = "2026-01-27T23:28:00.356Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
|
|
@ -1173,6 +1300,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/27/89/4b0001b2dab8df0a5ee2787dcbe771de75ded01f18f1f8d53dedeea2882b/tqdm-4.67.2.tar.gz", hash = "sha256:649aac53964b2cb8dec76a14b405a4c0d13612cb8933aae547dd144eacc99653", size = 169514, upload-time = "2026-01-30T23:12:06.555Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e2/31eac96de2915cf20ccaed0225035db149dfb9165a9ed28d4b252ef3f7f7/tqdm-4.67.2-py3-none-any.whl", hash = "sha256:9a12abcbbff58b6036b2167d9d3853042b9d436fe7330f06ae047867f2f8e0a7", size = 78354, upload-time = "2026-01-30T23:12:04.368Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.2"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue