fix: switch to OpenAI client for llm-proxy compatibility
- Replace Anthropic SDK with OpenAI SDK (proxy uses OpenAI-compatible API) - Add combined post fetching to include hidden/moderated posts - Update system prompt to be more thorough at catching violations - Add --debug flag to CLI for troubleshooting
This commit is contained in:
@@ -8,7 +8,7 @@ dependencies = [
|
|||||||
"httpx>=0.27",
|
"httpx>=0.27",
|
||||||
"click>=8.1",
|
"click>=8.1",
|
||||||
"rich>=13.0",
|
"rich>=13.0",
|
||||||
"anthropic>=0.30",
|
"openai>=1.0",
|
||||||
"python-dateutil>=2.9",
|
"python-dateutil>=2.9",
|
||||||
"python-dotenv>=1.0",
|
"python-dotenv>=1.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import os
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from anthropic import Anthropic
|
from openai import OpenAI
|
||||||
from anthropic.types import TextBlock
|
|
||||||
|
|
||||||
from .discourse import UserPost
|
from .discourse import UserPost
|
||||||
from .rules import ALL_RULES, RULES_BY_ID, Rule, Severity, format_rules_for_prompt
|
from .rules import ALL_RULES, RULES_BY_ID, Rule, Severity, format_rules_for_prompt
|
||||||
|
|
||||||
# Default model to use
|
# Default model to use (OpenAI-compatible format for the proxy)
|
||||||
DEFAULT_MODEL = "claude-sonnet-4-5-20250514"
|
DEFAULT_MODEL = "antigravity/claude-sonnet-4-5"
|
||||||
|
|
||||||
# Batch size for analyzing posts
|
# Batch size for analyzing posts
|
||||||
BATCH_SIZE = 5
|
BATCH_SIZE = 5
|
||||||
@@ -73,18 +72,23 @@ SYSTEM_PROMPT = """You are an EVE Online forum moderator assistant. Your role is
|
|||||||
## Important Context
|
## Important Context
|
||||||
|
|
||||||
EVE Online is a competitive PvP MMO where trash talk between rival corporations and alliances is part of the culture. However, there are limits:
|
EVE Online is a competitive PvP MMO where trash talk between rival corporations and alliances is part of the culture. However, there are limits:
|
||||||
- In-game rivalry and competitive banter is generally acceptable
|
- In-game rivalry and competitive banter about corporations/alliances is generally acceptable
|
||||||
- Personal attacks that go beyond the game are not acceptable
|
- Personal attacks directed at real players (not just their in-game characters) are NOT acceptable
|
||||||
- Discrimination, hate speech, and harassment are never acceptable regardless of "roleplay"
|
- Discrimination, hate speech, and harassment are NEVER acceptable regardless of "roleplay"
|
||||||
- Criticism of game mechanics or CCP decisions is allowed if constructive
|
- Criticism of game mechanics or CCP decisions is allowed if constructive
|
||||||
|
- Excessive profanity, especially when directed at other players, should be flagged
|
||||||
|
- Trolling and baiting behavior should be identified even if individually posts seem mild
|
||||||
|
|
||||||
## Your Task
|
## Your Task
|
||||||
|
|
||||||
For each post provided, analyze whether it violates any forum rules. Consider:
|
For each post provided, analyze whether it violates any forum rules. Be thorough - moderators need to catch violations, not excuse them. Consider:
|
||||||
1. The context of EVE Online's competitive culture
|
1. The tone and intent of the post
|
||||||
2. Whether the post is directed at in-game entities (acceptable) vs. real people (less acceptable)
|
2. Whether insults are directed at players vs in-game entities
|
||||||
3. The severity of the violation if one exists
|
3. Patterns of behavior (repeated baiting, antagonizing)
|
||||||
4. Your confidence level in the assessment
|
4. The severity of the violation if one exists
|
||||||
|
5. Your confidence level in the assessment
|
||||||
|
|
||||||
|
Flag posts that are borderline - it's better to surface potential issues for human review than to miss violations.
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
@@ -108,7 +112,7 @@ Respond with valid JSON in this exact format:
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
If a post has no violations, return an empty violations array and clean=true.
|
If a post has no violations, return an empty violations array and clean=true.
|
||||||
Be conservative - only flag clear violations. Borderline cases should have lower confidence scores.
|
For borderline cases, include them with lower confidence scores (0.3-0.5) so moderators can review.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -141,8 +145,8 @@ class Analyzer:
|
|||||||
)
|
)
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
# Initialize Anthropic client with custom base URL
|
# Initialize OpenAI client with custom base URL (proxy uses OpenAI-compatible API)
|
||||||
self.client = Anthropic(
|
self.client = OpenAI(
|
||||||
api_key=self.api_key,
|
api_key=self.api_key,
|
||||||
base_url=self.base_url,
|
base_url=self.base_url,
|
||||||
)
|
)
|
||||||
@@ -273,21 +277,19 @@ CONTENT:
|
|||||||
Analyze each post and return your findings in the specified JSON format."""
|
Analyze each post and return your findings in the specified JSON format."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.client.messages.create(
|
response = self.client.chat.completions.create(
|
||||||
model=self.model,
|
model=self.model,
|
||||||
max_tokens=4096,
|
max_tokens=4096,
|
||||||
system=system_prompt,
|
messages=[
|
||||||
messages=[{"role": "user", "content": user_message}],
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_message},
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract text from the response (handle different content block types)
|
# Extract text from the response
|
||||||
text_block = next(
|
response_text = response.choices[0].message.content
|
||||||
(block for block in response.content if isinstance(block, TextBlock)),
|
if response_text is None:
|
||||||
None,
|
|
||||||
)
|
|
||||||
if text_block is None:
|
|
||||||
raise AnalyzerError("No text content in LLM response")
|
raise AnalyzerError("No text content in LLM response")
|
||||||
response_text = text_block.text
|
|
||||||
return self._parse_response(response_text, posts)
|
return self._parse_response(response_text, posts)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ def cli() -> None:
|
|||||||
default=True,
|
default=True,
|
||||||
help="Fetch full post content (slower but more accurate)",
|
help="Fetch full post content (slower but more accurate)",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--debug",
|
||||||
|
is_flag=True,
|
||||||
|
help="Show debug output including sample post content",
|
||||||
|
)
|
||||||
def review(
|
def review(
|
||||||
username: str,
|
username: str,
|
||||||
days: int,
|
days: int,
|
||||||
@@ -69,6 +74,7 @@ def review(
|
|||||||
verbose: bool,
|
verbose: bool,
|
||||||
max_posts: int,
|
max_posts: int,
|
||||||
enrich: bool,
|
enrich: bool,
|
||||||
|
debug: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Review a user's forum posts for potential rule violations.
|
Review a user's forum posts for potential rule violations.
|
||||||
@@ -94,6 +100,7 @@ def review(
|
|||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
max_posts=max_posts,
|
max_posts=max_posts,
|
||||||
enrich=enrich,
|
enrich=enrich,
|
||||||
|
debug=debug,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,6 +125,7 @@ async def _review_user(
|
|||||||
verbose: bool,
|
verbose: bool,
|
||||||
max_posts: int,
|
max_posts: int,
|
||||||
enrich: bool,
|
enrich: bool,
|
||||||
|
debug: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Async implementation of user review."""
|
"""Async implementation of user review."""
|
||||||
try:
|
try:
|
||||||
@@ -134,14 +142,16 @@ async def _review_user(
|
|||||||
console.print(f"[red]User not found:[/red] {username}")
|
console.print(f"[red]User not found:[/red] {username}")
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
# Fetch posts
|
# Fetch posts (using combined method to include hidden posts)
|
||||||
with console.status(f"[bold blue]Fetching posts from last {days} days..."):
|
with console.status(f"[bold blue]Fetching posts from last {days} days..."):
|
||||||
posts = await client.get_user_posts(
|
posts = await client.get_user_posts_combined(
|
||||||
username=username,
|
username=username,
|
||||||
days=days,
|
days=days,
|
||||||
max_posts=max_posts,
|
max_posts=max_posts,
|
||||||
)
|
)
|
||||||
console.print(f"[green]Found {len(posts)} posts[/green]")
|
console.print(
|
||||||
|
f"[green]Found {len(posts)} posts (including hidden)[/green]"
|
||||||
|
)
|
||||||
|
|
||||||
if not posts:
|
if not posts:
|
||||||
console.print(
|
console.print(
|
||||||
@@ -155,6 +165,26 @@ async def _review_user(
|
|||||||
posts = await client.enrich_posts(posts)
|
posts = await client.enrich_posts(posts)
|
||||||
console.print("[green]Enriched posts with full content[/green]")
|
console.print("[green]Enriched posts with full content[/green]")
|
||||||
|
|
||||||
|
# Debug: show sample post content
|
||||||
|
if debug and posts:
|
||||||
|
console.print("\n[bold yellow]DEBUG: Sample post content[/bold yellow]")
|
||||||
|
for i, post in enumerate(posts[:3]): # Show first 3 posts
|
||||||
|
console.print(f"\n[cyan]--- Post {i + 1} ---[/cyan]")
|
||||||
|
console.print(f"[dim]Topic:[/dim] {post.topic_title}")
|
||||||
|
console.print(f"[dim]URL:[/dim] {post.url}")
|
||||||
|
console.print(f"[dim]Date:[/dim] {post.created_at}")
|
||||||
|
console.print(
|
||||||
|
f"[dim]Raw length:[/dim] {len(post.content_raw)} chars"
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
f"[dim]Text length:[/dim] {len(post.content_text)} chars"
|
||||||
|
)
|
||||||
|
content_preview = post.content_text[:500]
|
||||||
|
if len(post.content_text) > 500:
|
||||||
|
content_preview += "..."
|
||||||
|
console.print(f"[dim]Content:[/dim]\n{content_preview}")
|
||||||
|
console.print("\n")
|
||||||
|
|
||||||
except DiscourseError as e:
|
except DiscourseError as e:
|
||||||
console.print(f"[red]Forum Error:[/red] {e}")
|
console.print(f"[red]Forum Error:[/red] {e}")
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|||||||
@@ -338,3 +338,124 @@ class DiscourseClient:
|
|||||||
enriched.append(post)
|
enriched.append(post)
|
||||||
|
|
||||||
return enriched
|
return enriched
|
||||||
|
|
||||||
|
async def get_user_posts_via_search(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
days: int = 30,
|
||||||
|
max_posts: int = 200,
|
||||||
|
include_hidden: bool = True,
|
||||||
|
) -> list[UserPost]:
|
||||||
|
"""
|
||||||
|
Get a user's posts using the search API, which includes hidden posts.
|
||||||
|
|
||||||
|
This method is slower but catches posts that don't appear in the
|
||||||
|
activity feed (e.g., hidden/moderated posts).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: The forum username
|
||||||
|
days: Number of days to look back
|
||||||
|
max_posts: Maximum number of posts to fetch
|
||||||
|
include_hidden: Whether to include hidden posts
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of UserPost objects, newest first
|
||||||
|
"""
|
||||||
|
posts: list[UserPost] = []
|
||||||
|
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
while len(posts) < max_posts:
|
||||||
|
# Use Discourse search API with @username filter
|
||||||
|
data = await self._request(
|
||||||
|
"GET",
|
||||||
|
"/search.json",
|
||||||
|
params={
|
||||||
|
"q": f"@{username} order:latest",
|
||||||
|
"page": page,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
search_posts = data.get("posts", [])
|
||||||
|
if not search_posts:
|
||||||
|
break
|
||||||
|
|
||||||
|
for post_data in search_posts:
|
||||||
|
# Only include posts by this user
|
||||||
|
if post_data.get("username", "").lower() != username.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
created_str = post_data.get("created_at", "")
|
||||||
|
if not created_str:
|
||||||
|
continue
|
||||||
|
|
||||||
|
created_at = parse_date(created_str)
|
||||||
|
if created_at.tzinfo is None:
|
||||||
|
created_at = created_at.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
# Stop if we've gone past the cutoff
|
||||||
|
if created_at < cutoff:
|
||||||
|
return posts
|
||||||
|
|
||||||
|
post = UserPost(
|
||||||
|
post_id=post_data.get("id", 0),
|
||||||
|
post_number=post_data.get("post_number", 1),
|
||||||
|
topic_id=post_data.get("topic_id", 0),
|
||||||
|
topic_title=post_data.get("topic_title", "Unknown Topic"),
|
||||||
|
topic_slug=post_data.get("topic_slug", "topic"),
|
||||||
|
content_raw=post_data.get("blurb", ""),
|
||||||
|
content_cooked=post_data.get("blurb", ""),
|
||||||
|
created_at=created_at,
|
||||||
|
category_name=None,
|
||||||
|
)
|
||||||
|
posts.append(post)
|
||||||
|
|
||||||
|
if len(posts) >= max_posts:
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
# Safety limit on pages
|
||||||
|
if page > 20:
|
||||||
|
break
|
||||||
|
|
||||||
|
return posts
|
||||||
|
|
||||||
|
async def get_user_posts_combined(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
days: int = 30,
|
||||||
|
max_posts: int = 200,
|
||||||
|
) -> list[UserPost]:
|
||||||
|
"""
|
||||||
|
Get a user's posts using multiple methods to ensure hidden posts are included.
|
||||||
|
|
||||||
|
Combines the activity feed (fast) with search API (catches hidden posts).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: The forum username
|
||||||
|
days: Number of days to look back
|
||||||
|
max_posts: Maximum number of posts to fetch
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of UserPost objects, newest first, deduplicated
|
||||||
|
"""
|
||||||
|
# Get posts from activity feed (fast, but misses hidden)
|
||||||
|
activity_posts = await self.get_user_posts(username, days, max_posts)
|
||||||
|
|
||||||
|
# Get posts from search (slower, but includes hidden)
|
||||||
|
search_posts = await self.get_user_posts_via_search(username, days, max_posts)
|
||||||
|
|
||||||
|
# Combine and deduplicate by post_id
|
||||||
|
seen_ids: set[int] = set()
|
||||||
|
combined: list[UserPost] = []
|
||||||
|
|
||||||
|
for post in activity_posts + search_posts:
|
||||||
|
if post.post_id not in seen_ids:
|
||||||
|
seen_ids.add(post.post_id)
|
||||||
|
combined.append(post)
|
||||||
|
|
||||||
|
# Sort by date, newest first
|
||||||
|
combined.sort(key=lambda p: p.created_at, reverse=True)
|
||||||
|
|
||||||
|
return combined[:max_posts]
|
||||||
|
|||||||
63
uv.lock
generated
63
uv.lock
generated
@@ -11,25 +11,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anthropic"
|
|
||||||
version = "0.75.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "anyio" },
|
|
||||||
{ name = "distro" },
|
|
||||||
{ name = "docstring-parser" },
|
|
||||||
{ name = "httpx" },
|
|
||||||
{ name = "jiter" },
|
|
||||||
{ name = "pydantic" },
|
|
||||||
{ name = "sniffio" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "4.12.0"
|
version = "4.12.0"
|
||||||
@@ -82,23 +63,14 @@ 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" },
|
{ 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 = "docstring-parser"
|
|
||||||
version = "0.17.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eve-forum-moderator"
|
name = "eve-forum-moderator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anthropic" },
|
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "openai" },
|
||||||
{ name = "python-dateutil" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
@@ -106,9 +78,9 @@ dependencies = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "anthropic", specifier = ">=0.30" },
|
|
||||||
{ name = "click", specifier = ">=8.1" },
|
{ name = "click", specifier = ">=8.1" },
|
||||||
{ name = "httpx", specifier = ">=0.27" },
|
{ name = "httpx", specifier = ">=0.27" },
|
||||||
|
{ name = "openai", specifier = ">=1.0" },
|
||||||
{ name = "python-dateutil", specifier = ">=2.9" },
|
{ name = "python-dateutil", specifier = ">=2.9" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.0" },
|
{ name = "python-dotenv", specifier = ">=1.0" },
|
||||||
{ name = "rich", specifier = ">=13.0" },
|
{ name = "rich", specifier = ">=13.0" },
|
||||||
@@ -266,6 +238,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" },
|
{ 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.14.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/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.12.5"
|
version = "2.12.5"
|
||||||
@@ -439,6 +430,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tqdm"
|
||||||
|
version = "4.67.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user