| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- """
- OpenRouter Provider
- 使用 OpenRouter API 调用各种模型(包括 Claude Sonnet 4.5)
- 支持 OpenAI 兼容的 API 格式
- OpenRouter 转发多种模型,需要根据实际模型处理不同的 usage 格式:
- - OpenAI 模型: prompt_tokens, completion_tokens, completion_tokens_details.reasoning_tokens
- - Claude 模型: input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens
- - DeepSeek 模型: prompt_tokens, completion_tokens, reasoning_tokens
- """
- import os
- import json
- import httpx
- from typing import List, Dict, Any, Optional
- from .usage import TokenUsage, create_usage_from_response
- from .pricing import calculate_cost
- def _detect_provider_from_model(model: str) -> str:
- """根据模型名称检测提供商"""
- model_lower = model.lower()
- if model_lower.startswith("anthropic/") or "claude" in model_lower:
- return "anthropic"
- elif model_lower.startswith("openai/") or model_lower.startswith("gpt") or model_lower.startswith("o1") or model_lower.startswith("o3"):
- return "openai"
- elif model_lower.startswith("deepseek/") or "deepseek" in model_lower:
- return "deepseek"
- elif model_lower.startswith("google/") or "gemini" in model_lower:
- return "gemini"
- else:
- return "openai" # 默认使用 OpenAI 格式
- def _parse_openrouter_usage(usage: Dict[str, Any], model: str) -> TokenUsage:
- """
- 解析 OpenRouter 返回的 usage
- OpenRouter 会根据底层模型返回不同格式的 usage
- """
- provider = _detect_provider_from_model(model)
- # OpenRouter 通常返回 OpenAI 格式,但可能包含额外字段
- if provider == "anthropic":
- # Claude 模型可能有缓存字段
- return TokenUsage(
- input_tokens=usage.get("prompt_tokens") or usage.get("input_tokens", 0),
- output_tokens=usage.get("completion_tokens") or usage.get("output_tokens", 0),
- cache_creation_tokens=usage.get("cache_creation_input_tokens", 0),
- cache_read_tokens=usage.get("cache_read_input_tokens", 0),
- )
- elif provider == "deepseek":
- # DeepSeek 可能有 reasoning_tokens
- return TokenUsage(
- input_tokens=usage.get("prompt_tokens", 0),
- output_tokens=usage.get("completion_tokens", 0),
- reasoning_tokens=usage.get("reasoning_tokens", 0),
- )
- else:
- # OpenAI 格式(包括 o1/o3 的 reasoning_tokens)
- reasoning = 0
- if details := usage.get("completion_tokens_details"):
- reasoning = details.get("reasoning_tokens", 0)
- return TokenUsage(
- input_tokens=usage.get("prompt_tokens", 0),
- output_tokens=usage.get("completion_tokens", 0),
- reasoning_tokens=reasoning,
- )
- async def openrouter_llm_call(
- messages: List[Dict[str, Any]],
- model: str = "anthropic/claude-sonnet-4.5",
- tools: Optional[List[Dict]] = None,
- **kwargs
- ) -> Dict[str, Any]:
- """
- OpenRouter LLM 调用函数
- Args:
- messages: OpenAI 格式消息列表
- model: 模型名称(如 "anthropic/claude-sonnet-4.5")
- tools: OpenAI 格式工具定义
- **kwargs: 其他参数(temperature, max_tokens 等)
- Returns:
- {
- "content": str,
- "tool_calls": List[Dict] | None,
- "prompt_tokens": int,
- "completion_tokens": int,
- "finish_reason": str,
- "cost": float
- }
- """
- api_key = os.getenv("OPEN_ROUTER_API_KEY")
- if not api_key:
- raise ValueError("OPEN_ROUTER_API_KEY environment variable not set")
- base_url = "https://openrouter.ai/api/v1"
- endpoint = f"{base_url}/chat/completions"
- # 构建请求
- payload = {
- "model": model,
- "messages": messages,
- }
- # 添加可选参数
- if tools:
- payload["tools"] = tools
- if "temperature" in kwargs:
- payload["temperature"] = kwargs["temperature"]
- if "max_tokens" in kwargs:
- payload["max_tokens"] = kwargs["max_tokens"]
- # OpenRouter 特定参数
- headers = {
- "Authorization": f"Bearer {api_key}",
- "HTTP-Referer": "https://github.com/your-repo", # 可选,用于统计
- "X-Title": "Agent Framework", # 可选,显示在 OpenRouter dashboard
- }
- # 调用 API
- async with httpx.AsyncClient(timeout=120.0) as client:
- try:
- response = await client.post(endpoint, json=payload, headers=headers)
- response.raise_for_status()
- result = response.json()
- except httpx.HTTPStatusError as e:
- error_body = e.response.text
- print(f"[OpenRouter] Error {e.response.status_code}: {error_body}")
- raise
- except Exception as e:
- print(f"[OpenRouter] Request failed: {e}")
- raise
- # 解析响应(OpenAI 格式)
- choice = result["choices"][0] if result.get("choices") else {}
- message = choice.get("message", {})
- content = message.get("content", "")
- tool_calls = message.get("tool_calls")
- finish_reason = choice.get("finish_reason") # stop, length, tool_calls, content_filter 等
- # 提取 usage(完整版,根据模型类型解析)
- raw_usage = result.get("usage", {})
- usage = _parse_openrouter_usage(raw_usage, model)
- # 计算费用
- cost = calculate_cost(model, usage)
- return {
- "content": content,
- "tool_calls": tool_calls,
- "prompt_tokens": usage.input_tokens,
- "completion_tokens": usage.output_tokens,
- "reasoning_tokens": usage.reasoning_tokens,
- "cache_creation_tokens": usage.cache_creation_tokens,
- "cache_read_tokens": usage.cache_read_tokens,
- "finish_reason": finish_reason,
- "cost": cost,
- "usage": usage, # 完整的 TokenUsage 对象
- }
- def create_openrouter_llm_call(
- model: str = "anthropic/claude-sonnet-4.5"
- ):
- """
- 创建 OpenRouter LLM 调用函数
- Args:
- model: 模型名称
- - "anthropic/claude-sonnet-4.5"
- - "anthropic/claude-opus-4.5"
- - "openai/gpt-4o"
- 等等
- Returns:
- 异步 LLM 调用函数
- """
- async def llm_call(
- messages: List[Dict[str, Any]],
- model: str = model,
- tools: Optional[List[Dict]] = None,
- **kwargs
- ) -> Dict[str, Any]:
- return await openrouter_llm_call(messages, model, tools, **kwargs)
- return llm_call
|