repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | """LLM Client for Solana Trading Agent - Supports OpenRouter and multiple providers""" |
| #2 | |
| #3 | import json |
| #4 | import logging |
| #5 | from typing import Optional, Any |
| #6 | from dataclasses import dataclass, field |
| #7 | |
| #8 | import httpx |
| #9 | |
| #10 | logger = logging.getLogger(__name__) |
| #11 | |
| #12 | |
| #13 | @dataclass |
| #14 | class ToolCall: |
| #15 | """Tool call from LLM response""" |
| #16 | id: str |
| #17 | name: str |
| #18 | arguments: dict[str, Any] |
| #19 | |
| #20 | |
| #21 | @dataclass |
| #22 | class LLMResponse: |
| #23 | """Response from LLM""" |
| #24 | content: str = "" |
| #25 | thinking: str = "" |
| #26 | tool_calls: list[ToolCall] = field(default_factory=list) |
| #27 | finish_reason: str = "" |
| #28 | usage: Optional[dict] = None |
| #29 | |
| #30 | |
| #31 | @dataclass |
| #32 | class Message: |
| #33 | """Conversation message""" |
| #34 | role: str |
| #35 | content: str = "" |
| #36 | thinking: str = "" |
| #37 | tool_calls: list[ToolCall] = None |
| #38 | tool_call_id: str = "" |
| #39 | name: str = "" |
| #40 | |
| #41 | def to_dict(self) -> dict: |
| #42 | d = {"role": self.role, "content": self.content} |
| #43 | if self.tool_calls: |
| #44 | d["tool_calls"] = [ |
| #45 | { |
| #46 | "id": tc.id, |
| #47 | "type": "function", |
| #48 | "function": { |
| #49 | "name": tc.name, |
| #50 | "arguments": json.dumps(tc.arguments) |
| #51 | } |
| #52 | } |
| #53 | for tc in self.tool_calls |
| #54 | ] |
| #55 | if self.tool_call_id: |
| #56 | d["tool_call_id"] = self.tool_call_id |
| #57 | if self.name: |
| #58 | d["name"] = self.name |
| #59 | return d |
| #60 | |
| #61 | |
| #62 | class LLMClient: |
| #63 | """LLM Client supporting OpenRouter and OpenAI-compatible APIs""" |
| #64 | |
| #65 | def __init__( |
| #66 | self, |
| #67 | api_key: str, |
| #68 | api_base: str = "https://openrouter.ai/api/v1", |
| #69 | model: str = "anthropic/claude-sonnet-4", |
| #70 | timeout: float = 120.0, |
| #71 | max_tokens: int = 4096, |
| #72 | ): |
| #73 | """ |
| #74 | Initialize LLM client. |
| #75 | |
| #76 | Args: |
| #77 | api_key: API key for authentication |
| #78 | api_base: Base URL for the API |
| #79 | model: Model name to use |
| #80 | timeout: Request timeout in seconds |
| #81 | max_tokens: Maximum tokens in response |
| #82 | """ |
| #83 | self.api_key = api_key |
| #84 | self.api_base = api_base.rstrip("/") |
| #85 | self.model = model |
| #86 | self.timeout = timeout |
| #87 | self.max_tokens = max_tokens |
| #88 | |
| #89 | self._client = httpx.AsyncClient( |
| #90 | base_url=self.api_base, |
| #91 | headers={ |
| #92 | "Authorization": f"Bearer {api_key}", |
| #93 | "Content-Type": "application/json", |
| #94 | "HTTP-Referer": "https://github.com/mawd-bot", |
| #95 | "X-Title": "MAWD Solana Trading Agent", |
| #96 | }, |
| #97 | timeout=timeout |
| #98 | ) |
| #99 | |
| #100 | logger.info(f"Initialized LLM client: {model} @ {api_base}") |
| #101 | |
| #102 | async def generate( |
| #103 | self, |
| #104 | messages: list[Message], |
| #105 | tools: list = None, |
| #106 | ) -> LLMResponse: |
| #107 | """ |
| #108 | Generate response from LLM. |
| #109 | |
| #110 | Args: |
| #111 | messages: List of conversation messages |
| #112 | tools: Optional list of Tool objects |
| #113 | |
| #114 | Returns: |
| #115 | LLMResponse with content and optional tool calls |
| #116 | """ |
| #117 | # Format messages |
| #118 | formatted_messages = [] |
| #119 | for msg in messages: |
| #120 | if isinstance(msg, Message): |
| #121 | formatted_messages.append(msg.to_dict()) |
| #122 | elif isinstance(msg, dict): |
| #123 | formatted_messages.append(msg) |
| #124 | else: |
| #125 | formatted_messages.append({"role": "user", "content": str(msg)}) |
| #126 | |
| #127 | # Build payload |
| #128 | payload = { |
| #129 | "model": self.model, |
| #130 | "messages": formatted_messages, |
| #131 | "max_tokens": self.max_tokens, |
| #132 | } |
| #133 | |
| #134 | # Add tools if provided |
| #135 | if tools: |
| #136 | formatted_tools = [] |
| #137 | for tool in tools: |
| #138 | if hasattr(tool, 'to_openai_schema'): |
| #139 | formatted_tools.append(tool.to_openai_schema()) |
| #140 | elif hasattr(tool, 'to_schema'): |
| #141 | schema = tool.to_schema() |
| #142 | formatted_tools.append({ |
| #143 | "type": "function", |
| #144 | "function": { |
| #145 | "name": schema["name"], |
| #146 | "description": schema["description"], |
| #147 | "parameters": schema["input_schema"], |
| #148 | } |
| #149 | }) |
| #150 | elif isinstance(tool, dict): |
| #151 | formatted_tools.append(tool) |
| #152 | |
| #153 | if formatted_tools: |
| #154 | payload["tools"] = formatted_tools |
| #155 | payload["tool_choice"] = "auto" |
| #156 | |
| #157 | # Make request |
| #158 | try: |
| #159 | response = await self._client.post("/chat/completions", json=payload) |
| #160 | response.raise_for_status() |
| #161 | data = response.json() |
| #162 | except httpx.HTTPStatusError as e: |
| #163 | logger.error(f"LLM request failed: {e.response.status_code} - {e.response.text}") |
| #164 | raise |
| #165 | except Exception as e: |
| #166 | logger.error(f"LLM request failed: {e}") |
| #167 | raise |
| #168 | |
| #169 | # Parse response |
| #170 | choice = data.get("choices", [{}])[0] |
| #171 | message = choice.get("message", {}) |
| #172 | |
| #173 | # Extract tool calls |
| #174 | tool_calls = [] |
| #175 | raw_tool_calls = message.get("tool_calls", []) |
| #176 | for tc in raw_tool_calls: |
| #177 | func = tc.get("function", {}) |
| #178 | try: |
| #179 | args = json.loads(func.get("arguments", "{}")) |
| #180 | except json.JSONDecodeError: |
| #181 | args = {} |
| #182 | |
| #183 | tool_calls.append(ToolCall( |
| #184 | id=tc.get("id", ""), |
| #185 | name=func.get("name", ""), |
| #186 | arguments=args, |
| #187 | )) |
| #188 | |
| #189 | # Build response |
| #190 | return LLMResponse( |
| #191 | content=message.get("content", "") or "", |
| #192 | thinking=message.get("thinking", "") or "", |
| #193 | tool_calls=tool_calls, |
| #194 | finish_reason=choice.get("finish_reason", ""), |
| #195 | usage=data.get("usage"), |
| #196 | ) |
| #197 | |
| #198 | async def close(self): |
| #199 | """Close the HTTP client""" |
| #200 | await self._client.aclose() |
| #201 | |
| #202 | async def __aenter__(self): |
| #203 | return self |
| #204 | |
| #205 | async def __aexit__(self, exc_type, exc_val, exc_tb): |
| #206 | await self.close() |
| #207 |