diff --git a/CLAUDE.md b/CLAUDE.md index 7d99a7d..0f9184e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,8 +57,8 @@ External LLM → sequentialthinking tool → ThoughtProcessor → WorkflowExecut ### Configuration & Data Flow **Environment Variables:** -- `LLM_PROVIDER`: Provider selection (deepseek, groq, openrouter, ollama, github, anthropic) -- `{PROVIDER}_API_KEY`: API keys (e.g., `DEEPSEEK_API_KEY`, `GITHUB_TOKEN`) +- `LLM_PROVIDER`: Provider selection (deepseek, groq, openrouter, ollama, github, anthropic, claude-agent-sdk) +- `{PROVIDER}_API_KEY`: API keys (e.g., `DEEPSEEK_API_KEY`, `GITHUB_TOKEN`) - **Not required for claude-agent-sdk** - `{PROVIDER}_ENHANCED_MODEL_ID`: Enhanced model for complex synthesis (Blue Hat) - `{PROVIDER}_STANDARD_MODEL_ID`: Standard model for individual hat processing - `EXA_API_KEY`: Research capabilities (if using research agents) @@ -122,6 +122,13 @@ External LLM → sequentialthinking tool → ThoughtProcessor → WorkflowExecut **Examples:** ```bash +# Claude Agent SDK (uses local Claude Code - no API key needed!) +LLM_PROVIDER="claude-agent-sdk" +# No API key required - uses locally installed Claude Code +# Model IDs are informational - Claude Code uses its internal models +CLAUDE_AGENT_SDK_ENHANCED_MODEL_ID="claude-sonnet-4-5" # Both synthesis and processing +CLAUDE_AGENT_SDK_STANDARD_MODEL_ID="claude-sonnet-4-5" + # GitHub Models GITHUB_ENHANCED_MODEL_ID="openai/gpt-5" # Blue Hat synthesis GITHUB_STANDARD_MODEL_ID="openai/gpt-5-min" # Individual hats diff --git a/README.md b/README.md index 4b81d9d..0875d8d 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ Research is **optional** - requires `EXA_API_KEY` environment variable. The syst - **AI Selection**: System automatically chooses the right model based on task complexity ### Supported Providers: +- **Claude Agent SDK** - Use local Claude Code (no API key required!) - **DeepSeek** (default) - High performance, cost-effective - **Groq** - Ultra-fast inference - **OpenRouter** - Access to multiple models @@ -339,9 +340,12 @@ Create a `.env` file or set these variables: ```bash # LLM Provider (required) -LLM_PROVIDER="deepseek" # deepseek, groq, openrouter, github, anthropic, ollama +LLM_PROVIDER="deepseek" # deepseek, groq, openrouter, github, anthropic, ollama, claude-agent-sdk DEEPSEEK_API_KEY="sk-..." +# Or use Claude Agent SDK (no API key needed!) +# LLM_PROVIDER="claude-agent-sdk" # Requires Claude Code installed locally + # Optional: Enhanced/Standard Model Selection # DEEPSEEK_ENHANCED_MODEL_ID="deepseek-chat" # For synthesis # DEEPSEEK_STANDARD_MODEL_ID="deepseek-chat" # For other agents diff --git a/pyproject.toml b/pyproject.toml index f35a5d1..6812700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "openrouter", "httpx[socks]>=0.28.1", "sqlalchemy", + "claude-agent-sdk", ] [project.optional-dependencies] diff --git a/src/mcp_server_mas_sequential_thinking/config/modernized_config.py b/src/mcp_server_mas_sequential_thinking/config/modernized_config.py index a3ca071..d52ca50 100644 --- a/src/mcp_server_mas_sequential_thinking/config/modernized_config.py +++ b/src/mcp_server_mas_sequential_thinking/config/modernized_config.py @@ -16,6 +16,10 @@ from agno.models.openai import OpenAIChat from agno.models.openrouter import OpenRouter +from mcp_server_mas_sequential_thinking.models.claude_agent_sdk import ( + ClaudeAgentSDKModel, +) + class GitHubOpenAI(OpenAIChat): """OpenAI provider configured for GitHub Models API with enhanced validation.""" @@ -83,7 +87,7 @@ def _validate_github_token(token: str) -> None: "use a real GitHub token." ) - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # noqa: ANN401 # Set GitHub Models configuration kwargs.setdefault("base_url", "https://models.github.ai/inference") @@ -117,7 +121,8 @@ def create_enhanced_model(self) -> Model: if self.provider_class == Claude: return self.provider_class( id=self.enhanced_model_id, - # Note: cache_system_prompt removed - not available in current Agno version + # Note: cache_system_prompt removed - not available in + # current Agno version ) return self.provider_class(id=self.enhanced_model_id) @@ -127,7 +132,8 @@ def create_standard_model(self) -> Model: if self.provider_class == Claude: return self.provider_class( id=self.standard_model_id, - # Note: cache_system_prompt removed - not available in current Agno version + # Note: cache_system_prompt removed - not available in + # current Agno version ) return self.provider_class(id=self.standard_model_id) @@ -289,6 +295,34 @@ def api_key_name(self) -> str: return "ANTHROPIC_API_KEY" +class ClaudeAgentSDKStrategy(BaseProviderStrategy): + """Claude Agent SDK provider strategy (uses local Claude Code). + + This provider uses the Claude Agent SDK to communicate with locally installed + Claude Code, eliminating the need for API keys and enabling the use of + Claude Code's capabilities within the Multi-Thinking framework. + """ + + @property + def provider_class(self) -> type[Model]: + return ClaudeAgentSDKModel + + @property + def default_enhanced_model(self) -> str: + # Claude Code uses internal models, but we specify sonnet as default + return "claude-sonnet-4-5" + + @property + def default_standard_model(self) -> str: + # Both enhanced and standard use the same model in Claude Code + return "claude-sonnet-4-5" + + @property + def api_key_name(self) -> str | None: + # Claude Agent SDK doesn't require API keys - uses local Claude Code + return None + + class ConfigurationManager: """Manages configuration strategies with dependency injection.""" @@ -300,6 +334,7 @@ def __init__(self) -> None: "ollama": OllamaStrategy(), "github": GitHubStrategy(), "anthropic": AnthropicStrategy(), + "claude-agent-sdk": ClaudeAgentSDKStrategy(), } self._default_strategy = "deepseek" @@ -342,7 +377,7 @@ def validate_environment(self, provider_name: str | None = None) -> dict[str, st exa_key = os.environ.get("EXA_API_KEY") if not exa_key: # Don't fail startup - just log warning that research will be disabled - import logging + import logging # noqa: PLC0415 logging.getLogger(__name__).warning( "EXA_API_KEY not found. Research tools will be disabled." diff --git a/src/mcp_server_mas_sequential_thinking/models/__init__.py b/src/mcp_server_mas_sequential_thinking/models/__init__.py new file mode 100644 index 0000000..f772f08 --- /dev/null +++ b/src/mcp_server_mas_sequential_thinking/models/__init__.py @@ -0,0 +1,7 @@ +"""Models module for custom model implementations.""" + +from mcp_server_mas_sequential_thinking.models.claude_agent_sdk import ( + ClaudeAgentSDKModel, +) + +__all__ = ["ClaudeAgentSDKModel"] diff --git a/src/mcp_server_mas_sequential_thinking/models/claude_agent_sdk.py b/src/mcp_server_mas_sequential_thinking/models/claude_agent_sdk.py new file mode 100644 index 0000000..083a81f --- /dev/null +++ b/src/mcp_server_mas_sequential_thinking/models/claude_agent_sdk.py @@ -0,0 +1,530 @@ +"""Claude Agent SDK Model Wrapper for Agno Framework. + +This module provides integration between Claude Agent SDK and Agno framework, +allowing the use of local Claude Code as a model provider within Multi-Thinking. +""" + +import logging +from collections.abc import AsyncIterator, Awaitable, Callable +from pathlib import Path +from typing import Any, Literal + +from agno.models.base import Model +from agno.models.message import Message +from agno.models.response import ModelResponse + +logger = logging.getLogger(__name__) + + +class ClaudeAgentSDKModel(Model): + """Claude Agent SDK model wrapper that implements Agno Model interface. + + This class bridges Claude Agent SDK (local Claude Code) with the Agno framework, + enabling it to be used as a model provider in the Multi-Thinking architecture. + + Features: + - Converts Agno messages to Claude Agent SDK query format + - Executes queries through local Claude Code + - Returns responses in Agno ModelResponse format + - Supports reasoning tools (Think tool in Claude Agent SDK) + - Tool permission management (allowed_tools, can_use_tool callback) + - MCP server integration + - Environment variables and working directory control + - Event hooks (PreToolUse, PostToolUse, UserPromptSubmit, etc.) + - Additional directory access for context + + Example: + Basic usage: + >>> model = ClaudeAgentSDKModel( + ... model_id="claude-sonnet-4-5", + ... permission_mode="bypassPermissions" + ... ) + + With MCP servers: + >>> model = ClaudeAgentSDKModel( + ... mcp_servers={"filesystem": {...}}, + ... env={"DEBUG": "1"}, + ... add_dirs=["/path/to/project"] + ... ) + + With hooks: + >>> hooks = { + ... "PreToolUse": [lambda ctx: print(f"Using {ctx.tool_name}")], + ... "PostToolUse": [lambda ctx: print(f"Completed {ctx.tool_name}")] + ... } + >>> model = ClaudeAgentSDKModel(hooks=hooks) + + With permission callback: + >>> async def check_permission(tool_name, args, context): + ... if tool_name == "dangerous_tool": + ... return {"allow": False, "reason": "Not allowed"} + ... return {"allow": True} + >>> model = ClaudeAgentSDKModel(can_use_tool=check_permission) + """ + + def __init__( + self, + model_id: str = "claude-sonnet-4-5", # Default model ID + name: str | None = None, + permission_mode: Literal[ + "default", "acceptEdits", "plan", "bypassPermissions" + ] = "bypassPermissions", + cwd: str | None = None, + mcp_servers: dict[str, Any] | str | Path | None = None, + env: dict[str, str] | None = None, + add_dirs: list[str | Path] | None = None, + hooks: dict[str, list[Any]] | None = None, + can_use_tool: Callable[[str, dict[str, Any], Any], Awaitable[Any]] + | None = None, + **kwargs: Any, # noqa: ANN401 + ) -> None: + """Initialize Claude Agent SDK model. + + Args: + model_id: Model identifier (e.g., "claude-sonnet-4-5") + name: Optional human-readable name + permission_mode: Permission mode for Claude Code operations + - 'default': Standard permissions with prompts + - 'acceptEdits': Auto-accept file edits + - 'plan': Plan mode for reviewing actions + - 'bypassPermissions': Bypass all permission checks (default) + cwd: Working directory for Claude Code (default: current directory) + mcp_servers: MCP servers configuration (dict, path to config, or None) + env: Environment variables to pass to Claude Code + add_dirs: Additional directories for context/file access + hooks: Event hooks (PreToolUse, PostToolUse, UserPromptSubmit, etc.) + can_use_tool: Runtime callback for tool permission checks + **kwargs: Additional arguments passed to base Model class + """ + super().__init__( + id=model_id, + name=name or "Claude Agent SDK", + provider="claude-agent-sdk", + **kwargs, + ) + + # Store configuration + self.permission_mode = permission_mode + self.cwd = cwd or str(Path.cwd()) + self.mcp_servers = mcp_servers + self.env = env or {} + self.add_dirs = [str(d) for d in add_dirs] if add_dirs else [] + self.hooks = hooks + self.can_use_tool = can_use_tool + + # Lazy import to avoid issues if SDK not installed + try: + # Import both classes from claude_agent_sdk + from claude_agent_sdk import ( # noqa: PLC0415, I001 + ClaudeAgentOptions, + query as claude_query, + ) + + self._claude_query = claude_query + self._claude_options_class = ClaudeAgentOptions + except ImportError as e: + logger.exception( + "claude-agent-sdk not installed. Please install it: " + "pip install claude-agent-sdk" + ) + raise ImportError( + "claude-agent-sdk is required for ClaudeAgentSDKModel. " + "Install it with: pip install claude-agent-sdk" + ) from e + + logger.info( + "Initialized Claude Agent SDK model: %s (provider: claude-agent-sdk, " + "permission_mode: %s, cwd: %s, mcp_servers: %s, env_vars: %d, " + "add_dirs: %d, hooks: %s, can_use_tool: %s)", + model_id, + permission_mode, + self.cwd, + "configured" if self.mcp_servers else "none", + len(self.env), + len(self.add_dirs), + list(self.hooks.keys()) if self.hooks else "none", + "configured" if self.can_use_tool else "none", + ) + + def _extract_system_and_messages( + self, messages: list[Message] + ) -> tuple[str | None, str]: + """Extract system prompt and convert messages to prompt string. + + System messages are separated and used for ClaudeAgentOptions.system_prompt. + Other messages are converted to the main prompt. + + Args: + messages: List of Agno Message objects + + Returns: + Tuple of (system_prompt, user_prompt) + """ + system_parts = [] + prompt_parts = [] + + for msg in messages: + role = msg.role if hasattr(msg, "role") else "user" + content = msg.content if hasattr(msg, "content") else str(msg) + + # Separate system messages for ClaudeAgentOptions + if role == "system": + system_parts.append(content) + elif role == "user": + prompt_parts.append(f"User: {content}") + elif role == "assistant": + prompt_parts.append(f"Assistant: {content}") + else: + prompt_parts.append(str(content)) + + system_prompt = "\n\n".join(system_parts) if system_parts else None + user_prompt = "\n\n".join(prompt_parts) if prompt_parts else "" + + return system_prompt, user_prompt + + def _map_tools_to_allowed_tools(self, tools: list[Any] | None) -> list[str]: + """Map Agno tools to Claude Agent SDK allowed_tools list. + + Args: + tools: List of Agno tools (ReasoningTools, ExaTools, Functions, etc.) + + Returns: + List of tool names for Claude Agent SDK + """ + if not tools: + return [] + + allowed_tools = [] + + for tool in tools: + # Handle tool classes + if hasattr(tool, "__name__"): + tool_name = tool.__name__ + # Map known Agno tools to Claude Agent SDK tools + if "reasoning" in tool_name.lower(): + allowed_tools.append("Think") + elif "exa" in tool_name.lower(): + allowed_tools.append("search_exa") + else: + # For other tools, use class name as-is + allowed_tools.append(tool_name) + # Handle Function objects + elif hasattr(tool, "name"): + allowed_tools.append(tool.name) + # Handle dict-based tools + elif isinstance(tool, dict) and "name" in tool: + allowed_tools.append(tool["name"]) + + logger.debug("Mapped tools to allowed_tools: %s", allowed_tools) + return allowed_tools + + def _extract_tool_calls(self, message: Any) -> list[dict[str, Any]]: # noqa: ANN401 + """Extract tool calls from Claude Agent SDK message. + + Args: + message: Message object from Claude Agent SDK + + Returns: + List of tool call dictionaries + """ + tool_calls = [] + + # Check if message has tool_use blocks + if hasattr(message, "content") and isinstance(message.content, list): + for block in message.content: + # Handle tool_use blocks + if hasattr(block, "type") and block.type == "tool_use": + tool_call = { + "id": getattr(block, "id", "unknown"), + "type": "tool_use", + "name": getattr(block, "name", "unknown"), + "input": getattr(block, "input", {}), + } + tool_calls.append(tool_call) + elif isinstance(block, dict) and block.get("type") == "tool_use": + tool_calls.append( + { + "id": block.get("id", "unknown"), + "type": "tool_use", + "name": block.get("name", "unknown"), + "input": block.get("input", {}), + } + ) + + return tool_calls + + async def aresponse( # noqa: PLR0912 + self, + messages: list[Message], + response_format: dict[str, Any] | type | None = None, # noqa: ARG002 + tools: list[Any] | None = None, + tool_choice: str | dict[str, Any] | None = None, # noqa: ARG002 + tool_call_limit: int | None = None, + run_response: Any = None, # noqa: ARG002, ANN401 + send_media_to_model: bool = True, # noqa: ARG002 + ) -> ModelResponse: + """Generate async response using Claude Agent SDK. + + This is the core method that Agno Agent uses to get model responses. + + Args: + messages: List of conversation messages + response_format: Optional response format specification + tools: Optional list of tools (ReasoningTools mapped to Think tool) + tool_choice: Optional tool selection strategy + tool_call_limit: Optional limit on tool calls + run_response: Optional run response object + send_media_to_model: Whether to send media content + + Returns: + ModelResponse with generated content + """ + try: + # Extract system prompt and convert messages + system_prompt, prompt = self._extract_system_and_messages(messages) + + # Map Agno tools to Claude Agent SDK allowed_tools + allowed_tools = self._map_tools_to_allowed_tools(tools) + + # Create Claude Agent SDK options + options_kwargs: dict[str, Any] = { + "system_prompt": system_prompt, + "max_turns": tool_call_limit or 10, + "model": self.id, + "permission_mode": self.permission_mode, + "cwd": self.cwd, + } + + # Add optional parameters if provided + if allowed_tools: + options_kwargs["allowed_tools"] = allowed_tools + + if self.mcp_servers is not None: + options_kwargs["mcp_servers"] = self.mcp_servers + + if self.env: + options_kwargs["env"] = self.env + + if self.add_dirs: + options_kwargs["add_dirs"] = self.add_dirs + + if self.hooks is not None: + options_kwargs["hooks"] = self.hooks + + if self.can_use_tool is not None: + options_kwargs["can_use_tool"] = self.can_use_tool + + options = self._claude_options_class(**options_kwargs) + + logger.debug( + "Claude Agent SDK query - prompt: %d chars, system: %d chars, " + "allowed_tools: %s", + len(prompt), + len(system_prompt) if system_prompt else 0, + allowed_tools, + ) + + # Collect response from Claude Agent SDK + full_response = "" + collected_tool_calls = [] + async for message in self._claude_query(prompt=prompt, options=options): + # Extract tool calls from message + tool_calls = self._extract_tool_calls(message) + if tool_calls: + collected_tool_calls.extend(tool_calls) + + # Claude Agent SDK returns message objects + # Extract text content + if hasattr(message, "content"): + # Handle different content formats + if isinstance(message.content, str): + full_response += message.content + elif isinstance(message.content, list): + # Handle content blocks + for block in message.content: + if hasattr(block, "text"): + full_response += block.text + elif isinstance(block, dict) and "text" in block: + full_response += block["text"] + else: + # Fallback: convert to string + full_response += str(message) + + logger.debug( + "Claude Agent SDK response - length: %d chars, tool_calls: %d", + len(full_response), + len(collected_tool_calls), + ) + + # Create Agno ModelResponse with tool calls + return ModelResponse( + role="assistant", + content=full_response, + tool_calls=collected_tool_calls if collected_tool_calls else [], + provider_data={ + "model_id": self.id, + "provider": "claude-agent-sdk", + "permission_mode": self.permission_mode, + "cwd": self.cwd, + "mcp_servers_configured": self.mcp_servers is not None, + "env_vars_count": len(self.env), + "add_dirs_count": len(self.add_dirs), + "hooks_configured": list(self.hooks.keys()) if self.hooks else [], + "can_use_tool_configured": self.can_use_tool is not None, + }, + ) + + except Exception as e: + logger.exception("Claude Agent SDK query failed") + # Return error response + return ModelResponse( + role="assistant", + content=f"Error querying Claude Agent SDK: {e!s}", + provider_data={ + "error": str(e), + "model_id": self.id, + "provider": "claude-agent-sdk", + }, + ) + + async def aresponse_stream( # noqa: PLR0912 + self, + messages: list[Message], + response_format: dict[str, Any] | type | None = None, # noqa: ARG002 + tools: list[Any] | None = None, + tool_choice: str | dict[str, Any] | None = None, # noqa: ARG002 + tool_call_limit: int | None = None, + stream_model_response: bool = True, # noqa: ARG002 + run_response: Any = None, # noqa: ARG002, ANN401 + send_media_to_model: bool = True, # noqa: ARG002 + ) -> AsyncIterator[ModelResponse]: + """Generate streaming async response using Claude Agent SDK. + + Args: + messages: List of conversation messages + response_format: Optional response format specification + tools: Optional list of tools + tool_choice: Optional tool selection strategy + tool_call_limit: Optional limit on tool calls + stream_model_response: Whether to stream responses + run_response: Optional run response object + send_media_to_model: Whether to send media content + + Yields: + ModelResponse objects as they arrive + """ + try: + # Extract system prompt and convert messages + system_prompt, prompt = self._extract_system_and_messages(messages) + + # Map Agno tools to Claude Agent SDK allowed_tools + allowed_tools = self._map_tools_to_allowed_tools(tools) + + # Create Claude Agent SDK options + options_kwargs: dict[str, Any] = { + "system_prompt": system_prompt, + "max_turns": tool_call_limit or 10, + "model": self.id, + "permission_mode": self.permission_mode, + "cwd": self.cwd, + } + + # Add optional parameters if provided + if allowed_tools: + options_kwargs["allowed_tools"] = allowed_tools + + if self.mcp_servers is not None: + options_kwargs["mcp_servers"] = self.mcp_servers + + if self.env: + options_kwargs["env"] = self.env + + if self.add_dirs: + options_kwargs["add_dirs"] = self.add_dirs + + if self.hooks is not None: + options_kwargs["hooks"] = self.hooks + + if self.can_use_tool is not None: + options_kwargs["can_use_tool"] = self.can_use_tool + + options = self._claude_options_class(**options_kwargs) + + logger.debug( + "Claude Agent SDK streaming - prompt: %d chars, system: %d chars, " + "allowed_tools: %s", + len(prompt), + len(system_prompt) if system_prompt else 0, + allowed_tools, + ) + + async for message in self._claude_query(prompt=prompt, options=options): + # Extract tool calls from message + tool_calls = self._extract_tool_calls(message) + + # Extract content from message + content = "" + if hasattr(message, "content"): + if isinstance(message.content, str): + content = message.content + elif isinstance(message.content, list): + for block in message.content: + if hasattr(block, "text"): + content += block.text + elif isinstance(block, dict) and "text" in block: + content += block["text"] + else: + content = str(message) + + # Yield response with content or tool calls + if content or tool_calls: + yield ModelResponse( + role="assistant", + content=content, + tool_calls=tool_calls if tool_calls else [], + provider_data={ + "model_id": self.id, + "provider": "claude-agent-sdk", + "streaming": True, + "permission_mode": self.permission_mode, + }, + ) + + except Exception as e: + logger.exception("Claude Agent SDK streaming query failed") + yield ModelResponse( + role="assistant", + content=f"Error in Claude Agent SDK streaming: {e!s}", + provider_data={ + "error": str(e), + "model_id": self.id, + "provider": "claude-agent-sdk", + }, + ) + + def response( + self, + messages: list[Message], + response_format: dict[str, Any] | type | None = None, + tools: list[Any] | None = None, + tool_choice: str | dict[str, Any] | None = None, + tool_call_limit: int | None = None, + run_response: Any = None, # noqa: ANN401 + send_media_to_model: bool = True, + ) -> ModelResponse: + """Synchronous response (not implemented for async-only SDK). + + Claude Agent SDK is async-only, so this method raises NotImplementedError. + Use aresponse() instead. + """ + raise NotImplementedError( + "Claude Agent SDK only supports async operations. Use aresponse() instead." + ) + + def get_provider(self) -> str: + """Get provider name. + + Returns: + Provider name string + """ + return "claude-agent-sdk" diff --git a/uv.lock b/uv.lock index 3fd0417..0c25656 100644 --- a/uv.lock +++ b/uv.lock @@ -171,6 +171,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "claude-agent-sdk" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/b6/b73279eb875333fcc3e14c28fc080f815abaf35d2a65b132be0c8b05851c/claude_agent_sdk-0.1.6.tar.gz", hash = "sha256:3090a595896d65a5d951e158e191b462759aafc97399e700e4f857d5265a8f23", size = 49328, upload-time = "2025-10-31T05:15:55.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/12/38e4e9f7f79f2c04c7be34cd995ef4a56681f8266e382a5288ce815ede71/claude_agent_sdk-0.1.6-py3-none-any.whl", hash = "sha256:54227b096e8c7cfb60fc8b570082fce1f91ea060413092f65a08a2824cb9cb4b", size = 36369, upload-time = "2025-10-31T05:15:54.767Z" }, +] + [[package]] name = "click" version = "8.2.1" @@ -548,6 +562,7 @@ source = { editable = "." } dependencies = [ { name = "agno" }, { name = "asyncio" }, + { name = "claude-agent-sdk" }, { name = "exa-py" }, { name = "groq" }, { name = "httpx", extra = ["socks"] }, @@ -577,6 +592,7 @@ requires-dist = [ { name = "agno", specifier = ">=2.0.5" }, { name = "asyncio" }, { name = "black", marker = "extra == 'dev'" }, + { name = "claude-agent-sdk" }, { name = "exa-py" }, { name = "groq" }, { name = "httpx", extras = ["socks"], specifier = ">=0.28.1" }, @@ -692,15 +708,16 @@ wheels = [ [[package]] name = "openrouter" -version = "1.0" +version = "0.0.16" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, - { name = "urllib3" }, + { name = "httpcore" }, + { name = "httpx" }, + { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/3c/e2471e2bf3de74cb1832e52706a8c7288378c03f6060bda8b6f86e6ca2f3/openrouter-1.0.tar.gz", hash = "sha256:1f120ba67d85fa8ef7d4f47d10aeff8bea1d657f02a05b3cbad4e3a60382fff2", size = 3855, upload-time = "2024-08-28T20:30:40.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/d5/650560397cd9a1d9f40c561e1b584d5a794fc17936613f48f2c9565c9ccd/openrouter-0.0.16.tar.gz", hash = "sha256:7c5a5ed7f6f046d6db3fc5886a4e7971945f82fa3e12bf26e0ca3693f59bb245", size = 110272, upload-time = "2025-11-15T18:52:37.288Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/91/60b826a2499c81dec10642182e8531d4588d059d91f145d1923ca2bf6a6b/openrouter-1.0-py3-none-any.whl", hash = "sha256:4f3d50991dbf4b269d270622ab38be75e6162da9e09acdaead8697081ffe6167", size = 3791, upload-time = "2024-08-28T20:30:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ee/2fa3dc5db531d65e21e9c8538f748f2baafd79f91968386c50f443d98636/openrouter-0.0.16-py3-none-any.whl", hash = "sha256:b69bcb5a401d6932ef80740b449859601e5371ce9184052a5cd7c56a0f03c770", size = 234484, upload-time = "2025-11-15T18:52:36.065Z" }, ] [[package]]