Skill: aisadocs/openclaw-aisa-llm-router (version 1.0.0)

--- name: llm-router description: "Unified LLM Gateway - One API for 70+ AI models. Route to GPT, Claude, Gemini, Qwen, Deepseek, Grok and more with a single API key." homepage: https://openclaw.ai metadata: {"openclaw":{"emoji":"🧠","requires":{"bins":["curl","python3"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY"}} --- # OpenClaw LLM Router 🧠 **Unified LLM Gateway for autonomous agents. Powered by AIsa.** One API key. 70+ models. OpenAI-compatible. Replace 100+ API keys with one. Access GPT-4, Claude-3, Gemini, Qwen, Deepseek, Grok, and more through a unified, OpenAI-compatible API. ## 🔥 What Can You Do? ### Multi-Model Chat ``` "Chat with GPT-4 for reasoning, switch to Claude for creative writing" ``` ### Model Comparison ``` "Compare responses from GPT-4, Claude, and Gemini for the same question" ``` ### Vision Analysis ``` "Analyze this image with GPT-4o - what objects are in it?" ``` ### Cost Optimization ``` "Route simple queries to fast/cheap models, complex queries to GPT-4" ``` ### Fallback Strategy ``` "If GPT-4 fails, automatically try Claude, then Gemini" ``` ## Why LLM Router? | Feature | LLM Router | Direct APIs | |---------|------------|-------------| | API Keys | 1 | 10+ | | SDK Compatibility | OpenAI SDK | Multiple SDKs | | Billing | Unified | Per-provider | | Model Switching | Change string | Code rewrite | | Fallback Routing | Built-in | DIY | | Cost Tracking | Unified | Fragmented | ## Supported Model Families | Family | Developer | Example Models | |--------|-----------|----------------| | GPT | OpenAI | gpt-4.1, gpt-4o, gpt-4o-mini, o1, o1-mini, o3-mini | | Claude | Anthropic | claude-3-5-sonnet, claude-3-opus, claude-3-sonnet | | Gemini | Google | gemini-2.0-flash, gemini-1.5-pro, gemini-1.5-flash | | Qwen | Alibaba | qwen-max, qwen-plus, qwen2.5-72b-instruct | | Deepseek | Deepseek | deepseek-chat, deepseek-coder, deepseek-v3, deepseek-r1 | | Grok | xAI | grok-2, grok-beta | > **Note**: Model availability may vary. Check [marketplace.aisa.one/pricing](https://marketplace.aisa.one/pricing) for the full list of currently available models and pricing. ## Quick Start ```bash export AISA_API_KEY="your-key" ``` ## API Endpoints ### OpenAI-Compatible Chat Completions ``` POST https://api.aisa.one/v1/chat/completions ``` #### Request ```bash curl -X POST "https://api.aisa.one/v1/chat/completions" \ -H "Authorization: Bearer $AISA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4.1", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain quantum computing in simple terms."} ], "temperature": 0.7, "max_tokens": 1000 }' ``` #### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `model` | string | Yes | Model identifier (e.g., `gpt-4.1`, `claude-3-sonnet`) | | `messages` | array | Yes | Conversation messages | | `temperature` | number | No | Randomness (0-2, default: 1) | | `max_tokens` | integer | No | Maximum response tokens | | `stream` | boolean | No | Enable streaming (default: false) | | `top_p` | number | No | Nucleus sampling (0-1) | | `frequency_penalty` | number | No | Frequency penalty (-2 to 2) | | `presence_penalty` | number | No | Presence penalty (-2 to 2) | | `stop` | string/array | No | Stop sequences | #### Message Format ```json { "role": "user|assistant|system", "content": "message text or array for multimodal" } ``` #### Response ```json { "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1234567890, "model": "gpt-4.1", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Quantum computing uses..." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 50, "completion_tokens": 200, "total_tokens": 250, "cost": 0.0025 } } ``` ### Streaming Response ```bash curl -X POST "https://api.aisa.one/v1/chat/completions" \ -H "Authorization: Bearer $AISA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-sonnet", "messages": [{"role": "user", "content": "Write a poem about AI."}], "stream": true }' ``` Streaming returns Server-Sent Events (SSE): ``` data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"In"}}]} data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":" circuits"}}]} ... data: [DONE] ``` ### Vision / Image Analysis Analyze images by passing image URLs or base64 data: ```bash curl -X POST "https://api.aisa.one/v1/chat/completions" \ -H "Authorization: Bearer $AISA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": [ {"type": "text", "text": "What is in this image?"}, {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}} ] } ] }' ``` ### Function Calling Enable tools/functions for structured outputs: ```bash curl -X POST "https://api.aisa.one/v1/chat/completions" \ -H "Authorization: Bearer $AISA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4.1", "messages": [{"role": "user", "content": "What is the weather in Tokyo?"}], "functions": [ { "name": "get_weather", "description": "Get current weather for a location", "parameters": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} }, "required": ["location"] } } ], "function_call": "auto" }' ``` ### Google Gemini Format For Gemini models, you can also use the native format: ``` POST https://api.aisa.one/v1/models/{model}:generateContent ``` ```bash curl -X POST "https://api.aisa.one/v1/models/gemini-2.0-flash:generateContent" \ -H "Authorization: Bearer $AISA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contents": [ { "role": "user", "parts": [{"text": "Explain machine learning."}] } ], "generationConfig": { "temperature": 0.7, "maxOutputTokens": 1000 } }' ``` ## Python Client ### Installation No installation required - uses standard library only. ### CLI Usage ```bash # Basic completion python3 {baseDir}/scripts/llm_router_client.py chat --model gpt-4.1 --message "Hello, world!" # With system prompt python3 {baseDir}/scripts/llm_router_client.py chat --model claude-3-sonnet --system "You are a poet" --message "Write about the moon" # Streaming python3 {baseDir}/scripts/llm_router_client.py chat --model gpt-4o --message "Tell me a story" --stream # Multi-turn conversation python3 {baseDir}/scripts/llm_router_client.py chat --model qwen-max --messages '[{"role":"user","content":"Hi"},{"role":"assistant","content":"Hello!"},{"role":"user","content":"How are you?"}]' # Vision analysis python3 {baseDir}/scripts/llm_router_client.py vision --model gpt-4o --image "https://example.com/image.jpg" --prompt "Describe this image" # List supported models python3 {baseDir}/scripts/llm_router_client.py models # Compare models python3 {baseDir}/scripts/llm_router_client.py compare --models "gpt-4.1,claude-3-sonnet,gemini-2.0-flash" --message "What is 2+2?" ``` ### Python SDK Usage ```python from llm_router_client import LLMRouterClient client = LLMRouterClient() # Uses AISA_API_KEY env var # Simple chat response = client.chat( model="gpt-4.1", messages=[{"role": "user", "content": "Hello!"}] ) print(response["choices"][0]["message"]["content"]) # With options response = client.chat( model="claude-3-sonnet", messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain relativity."} ], temperature=0.7, max_tokens=500 ) # Streaming for chunk in client.chat_stream( model="gpt-4o", messages=[{"role": "user", "content": "Write a story."}] ): print(chunk, end="", flush=True) # Vision response = client.vision( model="gpt-4o", image_url="https://example.com/image.jpg", prompt="What's in this image?" ) # Compare models results = client.compare_models( models=["gpt-4.1", "claude-3-sonnet", "gemini-2.0-flash"], message="Explain quantum computing" ) for model, result in results.items(): print(f"{model}: {result['response'][:100]}...") ``` ## Use Cases ### 1. Cost-Optimized Routing Use cheaper models for simple tasks: ```python def smart_route(message: str) -> str: # Simple queries -> fast/cheap model if len(message) < 50: model = "gpt-3.5-turbo" # Complex reasoning -> powerful model else: model = "gpt-4.1" return client.chat(model=model, messages=[{"role": "user", "content": message}]) ``` ### 2. Fallback Strategy Automatic fallback on failure: ```python def chat_with_fallback(message: str) -> str: models = ["gpt-4.1", "claude-3-sonnet", "gemini-2.0-flash"] for model in models: try: return client.chat(model=model, messages=[{"role": "user", "content": message}]) except Exception: continue raise Exception("All models failed") ``` ### 3. Model A/B Testing Compare model outputs: ```python results = client.compare_models( models=["gpt-4.1", "claude-3-opus"], message="Analyze this quarterly report..." ) # Log for analysis for model, result in results.items(): log_response(model=model, latency=result["latency"], cost=result["cost"]) ``` ### 4. Specialized Model Selection Choose the best model for each task: ```python MODEL_MAP = { "code": "deepseek-coder", "creative": "claude-3-opus", "fast": "gpt-3.5-turbo", "vision": "gpt-4o", "chinese": "qwen-max", "reasoning": "gpt-4.1" } def route_by_task(task_type: str, message: str) -> str: model = MODEL_MAP.get(task_type, "gpt-4.1") return client.chat(model=model, messages=[{"role": "user", "content": message}]) ``` ## Error Handling Errors return JSON with `error` field: ```json { "error": { "code": "model_not_found", "message": "Model 'xyz' is not available" } } ``` Common error codes: - `401` - Invalid or missing API key - `402` - Insufficient credits - `404` - Model not found - `429` - Rate limit exceeded - `500` - Server error ## Best Practices 1. **Use streaming** for long responses to improve UX 2. **Set max_tokens** to control costs 3. **Implement fallback** for production reliability 4. **Cache responses** for repeated queries 5. **Monitor usage** via response metadata 6. **Use appropriate models** - don't use GPT-4 for simple tasks ## OpenAI SDK Compatibility Just change the base URL and key: ```python import os from openai import OpenAI client = OpenAI( [REDACTED_SECRET]"AISA_API_KEY"], base_url="https://api.aisa.one/v1" ) response = client.chat.completions.create( model="gpt-4.1", messages=[{"role": "user", "content": "Hello!"}] ) print(response.choices[0].message.content) ``` ## Pricing Token-based pricing varies by model. Check [marketplace.aisa.one/pricing](https://marketplace.aisa.one/pricing) for current rates. | Model Family | Approximate Cost | |--------------|------------------| | GPT-4.1 / GPT-4o | ~$0.01 / 1K tokens | | Claude-3-Sonnet | ~$0.01 / 1K tokens | | Gemini-2.0-Flash | ~$0.001 / 1K tokens | | Qwen-Max | ~$0.005 / 1K tokens | | DeepSeek-V3 | ~$0.002 / 1K tokens | Every response includes `usage.cost` and `usage.credits_remaining`. ## Get Started 1. Sign up at [aisa.one](https://aisa.one) 2. Get your API key from the dashboard 3. Add credits (pay-as-you-go) 4. Set environment variable: `export AISA_API_KEY="your-key"` ## Full API Reference See [API Reference](https://aisa.mintlify.app/api-reference/introduction) for complete endpoint documentation.

path: README.md
content: # OpenClaw LLM Router 🧠

**Unified LLM Gateway for autonomous agents. Powered by AIsa.**

One API key. 70+ models. Route requests to GPT, Claude, Gemini, Qwen, Deepseek, Grok and more.

## Quick Start

```bash
# Set your API key
export AISA_API_KEY="your-key"

# Simple chat completion
python3 scripts/llm_router_client.py chat --model gpt-4.1 --message "Hello!"

# Stream response
python3 scripts/llm_router_client.py chat --model claude-3-sonnet --message "Write a poem" --stream

# Vision analysis
python3 scripts/llm_router_client.py vision --model gpt-4o --image "https://example.com/image.jpg" --prompt "Describe this"

# Compare models
python3 scripts/llm_router_client.py compare --models "gpt-4.1,claude-3-sonnet" --message "Explain AI"

# List models
python3 scripts/llm_router_client.py models
```

## Using with OpenAI SDK

```python
from openai import OpenAI
import os

client = OpenAI(
    [REDACTED_SECRET]"AISA_API_KEY"],
    base_url="https://api.aisa.one/v1"
)

response = client.chat.completions.create(
    model="gpt-4.1",  # Or claude-3-sonnet, gemini-2.0-flash, etc.
    messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
```

## Supported Models

| Family | Models |
|--------|--------|
| OpenAI | gpt-4.1, gpt-4o, gpt-4o-mini, o1, o1-mini, o3-mini |
| Anthropic | claude-3-5-sonnet, claude-3-opus, claude-3-sonnet |
| Google | gemini-2.0-flash, gemini-1.5-pro, gemini-1.5-flash |
| Alibaba | qwen-max, qwen-plus, qwen2.5-72b-instruct |
| Deepseek | deepseek-chat, deepseek-coder, deepseek-v3, deepseek-r1 |
| xAI | grok-2, grok-beta |

> Check [marketplace.aisa.one/pricing](https://marketplace.aisa.one/pricing) for full model list.

See [SKILL.md](SKILL.md) for full documentation.


path: scripts/llm_router_client.py
content: #!/usr/bin/env python3
"""
LLM Router - Unified Model Gateway Client
Route requests to 70+ LLMs (GPT, Claude, Gemini, Qwen, Deepseek, Grok) via AIsa API.

Usage:
    python llm_router_client.py chat --model <model> --message <message> [--stream]
    python llm_router_client.py chat --model <model> --messages <json_array>
    python llm_router_client.py vision --model <model> --image <url> --prompt <text>
    python llm_router_client.py compare --models <model1,model2,...> --message <message>
    python llm_router_client.py models
"""

import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from typing import Any, Dict, Generator, List, Optional


class LLMRouterClient:
    """Unified LLM Gateway Client for AIsa API."""
    
    BASE_URL = "https://api.aisa.one/v1"
    
    # Popular models for reference (check marketplace.aisa.one/pricing for full list)
    SUPPORTED_MODELS = {
        "openai": ["gpt-4.1", "gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"],
        "anthropic": ["claude-3-5-sonnet", "claude-3-opus", "claude-3-sonnet", "claude-3-haiku"],
        "google": ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash", "gemini-pro"],
        "alibaba": ["qwen-max", "qwen-plus", "qwen-turbo", "qwen2.5-72b-instruct"],
        "deepseek": ["deepseek-chat", "deepseek-coder", "deepseek-v3", "deepseek-r1"],
        "xai": ["grok-2", "grok-beta"],
    }
    
    def __init__(self, [REDACTED_SECRET] = None):
        """Initialize the client with an API key."""
        self.[REDACTED_SECRET] or os.environ.get("AISA_API_KEY")
        if not self.api_key:
            raise ValueError(
                "AISA_API_KEY is required. Set it via environment variable or pass to constructor."
            )
    
    def _request(
        self, 
        method: str, 
        endpoint: str, 
        data: Optional[Dict[str, Any]] = None,
        stream: bool = False
    ) -> Any:
        """Make an HTTP request to the AIsa API."""
        url = f"{self.BASE_URL}{endpoint}"
        
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
            "User-Agent": "OpenClaw-LLMRouter/1.0",
            "Accept": "application/json"
        }
        
        request_data = None
        if data:
            request_data = json.dumps(data).encode("utf-8")
        
        req = urllib.request.Request(url, data=request_data, headers=headers, method=method)
        
        try:
            response = urllib.request.urlopen(req, timeout=120)
            
            if stream:
                return self._handle_stream(response)
            else:
                return json.loads(response.read().decode("utf-8"))
                
        except urllib.error.HTTPError as e:
            error_body = e.read().decode("utf-8")
            try:
                return json.loads(error_body)
            except json.JSONDecodeError:
                return {"error": {"code": str(e.code), "message": error_body}}
        except urllib.error.URLError as e:
            return {"error": {"code": "NETWORK_ERROR", "message": str(e.reason)}}
    
    def _handle_stream(self, response) -> Generator[str, None, None]:
        """Handle streaming response (SSE)."""
        for line in response:
            line = line.decode("utf-8").strip()
            if line.startswith("data: "):
                data = line[6:]
                if data == "[DONE]":
                    break
                try:
                    chunk = json.loads(data)
                    if "choices" in chunk and chunk["choices"]:
                        delta = chunk["choices"][0].get("delta", {})
                        content = delta.get("content", "")
                        if content:
                            yield content
                except json.JSONDecodeError:
                    continue
    
    def chat(
        self,
        model: str,
        messages: List[Dict[str, str]],
        temperature: Optional[float] = None,
        max_tokens: Optional[int] = None,
        top_p: Optional[float] = None,
        stream: bool = False,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Create a chat completion.
        
        Args:
            model: Model identifier (e.g., gpt-4.1, claude-3-sonnet)
            messages: List of message dicts with 'role' and 'content'
            temperature: Sampling temperature (0-2)
            max_tokens: Maximum tokens to generate
            top_p: Nucleus sampling parameter
            stream: Whether to stream the response
            **kwargs: Additional parameters (functions, function_call, etc.)
        
        Returns:
            Chat completion response
        """
        payload = {
            "model": model,
            "messages": messages,
            "stream": stream
        }
        
        if temperature is not None:
            payload["temperature"] = temperature
        if max_tokens is not None:
            payload["max_tokens"] = max_tokens
        if top_p is not None:
            payload["top_p"] = top_p
        
        # Add any additional kwargs
        payload.update(kwargs)
        
        return self._request("POST", "/chat/completions", data=payload, stream=stream)
    
    def chat_stream(
        self,
        model: str,
        messages: List[Dict[str, str]],
        **kwargs
    ) -> Generator[str, None, None]:
        """
        Create a streaming chat completion.
        
        Yields content chunks as they arrive.
        """
        return self.chat(model=model, messages=messages, stream=True, **kwargs)
    
    def vision(
        self,
        model: str,
        image_url: str,
        prompt: str,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Analyze an image with a vision-capable model.
        
        Args:
            model: Vision-capable model (e.g., gpt-4o, gemini-1.5-pro)
            image_url: URL of the image to analyze
            prompt: Question or instruction about the image
        
        Returns:
            Chat completion response
        """
        messages = [
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {"type": "image_url", "image_url": {"url": image_url}}
                ]
            }
        ]
        return self.chat(model=model, messages=messages, **kwargs)
    
    def compare_models(
        self,
        models: List[str],
        message: str,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Compare responses from multiple models.
        
        Args:
            models: List of model identifiers
            message: The message to send to each model
        
        Returns:
            Dict with model names as keys and results as values
        """
        import time
        
        results = {}
        for model in models:
            start = time.time()
            try:
                response = self.chat(
                    model=model,
                    messages=[{"role": "user", "content": message}],
                    **kwargs
                )
                elapsed = time.time() - start
                
                if "error" in response:
                    results[model] = {
                        "success": False,
                        "error": response["error"],
                        "latency": elapsed
                    }
                else:
                    content = response.get("choices", [{}])[0].get("message", {}).get("content", "")
                    usage = response.get("usage", {})
                    results[model] = {
                        "success": True,
                        "response": content,
                        "latency": elapsed,
                        "tokens": usage.get("total_tokens", 0),
                        "cost": usage.get("cost", 0)
                    }
            except Exception as e:
                results[model] = {
                    "success": False,
                    "error": str(e),
                    "latency": time.time() - start
                }
        
        return results
    
    def list_models(self) -> Dict[str, List[str]]:
        """List supported model families and models."""
        return self.SUPPORTED_MODELS


def main():
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        description="LLM Router - Unified Model Gateway (70+ models)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
    %(prog)s chat --model gpt-4.1 --message "Hello!"
    %(prog)s chat --model claude-3-sonnet --message "Write a poem" --stream
    %(prog)s chat --model gpt-4 --system "You are a pirate" --message "Greet me"
    %(prog)s vision --model gpt-4o --image "https://example.com/img.jpg" --prompt "Describe this"
    %(prog)s compare --models "gpt-4.1,claude-3-sonnet" --message "Explain AI"
    %(prog)s models
        """
    )
    
    subparsers = parser.add_subparsers(dest="command", help="Command")
    
    # Chat command
    chat_parser = subparsers.add_parser("chat", help="Send a chat completion request")
    chat_parser.add_argument("--model", "-m", required=True, help="Model identifier")
    chat_parser.add_argument("--message", help="User message")
    chat_parser.add_argument("--messages", help="Full messages array as JSON")
    chat_parser.add_argument("--system", "-s", help="System prompt")
    chat_parser.add_argument("--temperature", "-t", type=float, help="Temperature (0-2)")
    chat_parser.add_argument("--max-tokens", type=int, help="Max tokens to generate")
    chat_parser.add_argument("--stream", action="store_true", help="Stream the response")
    
    # Vision command
    vision_parser = subparsers.add_parser("vision", help="Analyze an image")
    vision_parser.add_argument("--model", "-m", required=True, help="Vision-capable model")
    vision_parser.add_argument("--image", "-i", required=True, help="Image URL")
    vision_parser.add_argument("--prompt", "-p", required=True, help="Question about the image")
    vision_parser.add_argument("--max-tokens", type=int, help="Max tokens to generate")
    
    # Compare command
    compare_parser = subparsers.add_parser("compare", help="Compare multiple models")
    compare_parser.add_argument("--models", required=True, help="Comma-separated model list")
    compare_parser.add_argument("--message", "-m", required=True, help="Message to send")
    compare_parser.add_argument("--temperature", "-t", type=float, help="Temperature")
    compare_parser.add_argument("--max-tokens", type=int, help="Max tokens")
    
    # Models command
    subparsers.add_parser("models", help="List supported models")
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        sys.exit(1)
    
    # Models command doesn't need API key
    if args.command == "models":
        print(json.dumps(LLMRouterClient.SUPPORTED_MODELS, indent=2))
        sys.exit(0)
    
    try:
        client = LLMRouterClient()
    except ValueError as e:
        print(json.dumps({"error": {"code": "AUTH_ERROR", "message": str(e)}}))
        sys.exit(1)
    
    result = None
    
    if args.command == "chat":
        # Build messages
        if args.messages:
            messages = json.loads(args.messages)
        elif args.message:
            messages = []
            if args.system:
                messages.append({"role": "system", "content": args.system})
            messages.append({"role": "user", "content": args.message})
        else:
            print(json.dumps({"error": {"code": "INVALID_INPUT", "message": "Either --message or --messages is required"}}))
            sys.exit(1)
        
        kwargs = {}
        if args.temperature is not None:
            kwargs["temperature"] = args.temperature
        if args.max_tokens is not None:
            kwargs["max_tokens"] = args.max_tokens
        
        if args.stream:
            # Streaming mode
            try:
                for chunk in client.chat_stream(model=args.model, messages=messages, **kwargs):
                    print(chunk, end="", flush=True)
                print()  # Final newline
                sys.exit(0)
            except Exception as e:
                print(json.dumps({"error": {"code": "STREAM_ERROR", "message": str(e)}}))
                sys.exit(1)
        else:
            result = client.chat(model=args.model, messages=messages, **kwargs)
    
    elif args.command == "vision":
        kwargs = {}
        if args.max_tokens is not None:
            kwargs["max_tokens"] = args.max_tokens
        result = client.vision(
            model=args.model,
            image_url=args.image,
            prompt=args.prompt,
            **kwargs
        )
    
    elif args.command == "compare":
        models = [m.strip() for m in args.models.split(",")]
        kwargs = {}
        if args.temperature is not None:
            kwargs["temperature"] = args.temperature
        if args.max_tokens is not None:
            kwargs["max_tokens"] = args.max_tokens
        result = client.compare_models(models=models, message=args.message, **kwargs)
    
    if result:
        output = json.dumps(result, indent=2, ensure_ascii=False)
        try:
            print(output)
        except UnicodeEncodeError:
            print(json.dumps(result, indent=2, ensure_ascii=True))
        
        # Exit with error code if result contains error
        if isinstance(result, dict) and "error" in result:
            sys.exit(1)
        sys.exit(0)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)