From fc5ec96129b6a393576997c481eae91e0b24c8cc Mon Sep 17 00:00:00 2001 From: Vladimir Prelovac Date: Wed, 3 Sep 2025 17:23:03 -0700 Subject: [PATCH] Initial commit: Lightweight AI CLI tool for OpenRouter API --- README.md | 230 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ask | 201 ++++++++++++++++++++++++++++++++++++++++++++++ install.sh | 58 ++++++++++++++ 3 files changed, 489 insertions(+) create mode 100644 README.md create mode 100755 ask create mode 100755 install.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..af16bd713124b6c9b6fc6a6267fd5a1c6cea39c6 --- /dev/null +++ b/README.md @@ -0,0 +1,230 @@ +# ask - AI CLI tool + +A lightweight bash script for querying AI models via the OpenRouter API, optimized for direct, executable output. + +## Features + +- **Direct Output** - Returns executable commands and answers without markdown formatting +- **Multiple Models** - Quick access to Mercury Coder, Gemini, Claude Sonnet, Kimi, and Qwen models +- **Streaming Support** - Real-time response streaming for long outputs +- **Provider Routing** - Automatic fallback between providers for reliability +- **Performance Metrics** - Shows response time and tokens/second +- **Pipe Support** - Works seamlessly with Unix pipes and stdin + +## Quick start + +```bash +# Clone and setup +git clone https://github.com/yourusername/ask.git +cd ask +chmod +x ask + +# Set your API key +export OPENROUTER_API_KEY="your-api-key-here" + +# Test it +./ask "What is 2+2?" +``` + +## Installation + +### Option 1: Using install.sh (recommended) +```bash +sudo ./install.sh +``` + +### Option 2: Manual installation +```bash +chmod +x ask +sudo cp ask /usr/local/bin/ +``` + +### Persistent API key setup + +Add to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.): +```bash +export OPENROUTER_API_KEY="your-api-key-here" +``` + +## Usage + +### Basic usage + +```bash +ask "What is 2+2?" +ask "Write a Python hello world" +``` + +### Model selection + +```bash +# Default model (Mercury Coder - optimized for code) +ask "Write a Python function" + +# Shorthand flags for quick model switching +ask -c "prompt" # Mercury Coder (default, best for code) +ask -g "prompt" # Gemini 2.5 Flash (fast, general purpose) +ask -s "prompt" # Claude Sonnet 4 (complex reasoning) +ask -k "prompt" # Kimi K2 (long context) +ask -q "prompt" # Qwen 235B (large model) + +# Custom model by full name +ask -m "openai/gpt-4o" "Explain this concept" +``` + +### Provider routing + +Specify provider order for fallback support: + +```bash +ask --provider "openai,together" "Generate code" +``` + +This will try OpenAI first, then fall back to Together if needed. + +### System prompts + +```bash +# Custom system prompt +ask --system "You are a pirate" "Tell me about sailing" + +# Disable system prompt for raw model behavior +ask -r "What is 2+2?" +``` + +### Streaming mode + +Get responses as they're generated: + +```bash +ask --stream "Tell me a long story" +``` + +### Pipe input + +```bash +echo "Fix this code: print('hello world)" | ask +cat script.py | ask "Review this code" +``` + +## Options + +| Option | Description | +|--------|-------------| +| `-c` | Use Mercury Coder (default) | +| `-g` | Use Google Gemini 2.5 Flash | +| `-s` | Use Claude Sonnet 4 | +| `-k` | Use Moonshotai Kimi K2 | +| `-q` | Use Qwen3 235B | +| `-m MODEL` | Use custom model | +| `-r` | Disable system prompt | +| `--stream` | Enable streaming output | +| `--system` | Set custom system prompt | +| `--provider` | Set provider order (comma-separated) | +| `-h, --help` | Show help message | + +## Common use cases + +### Command generation +```bash +# Get executable commands directly +ask "Command to find files larger than 100MB" +# Output: find . -type f -size +100M + +ask "ffmpeg command to convert mp4 to gif" +# Output: ffmpeg -i input.mp4 -vf "fps=10,scale=320:-1:flags=lanczos" output.gif +``` + +### Code generation +```bash +# Generate code snippets +ask "Python function to calculate factorial" + +# Code review +cat script.py | ask "Find potential bugs in this code" +``` + +### Quick answers +```bash +# Calculations +ask "What is 18% of 2450?" +# Output: 441 + +# Technical questions +ask "What port does PostgreSQL use?" +# Output: 5432 +``` + +### Advanced usage +```bash +# Chain commands +ask "List all Python files" | ask "Generate a script to check syntax of these files" + +# Use with other tools +docker ps -a | ask "Which containers are using the most memory?" + +# Provider fallback for reliability +ask --provider "anthropic,openai" "Complex analysis task" +``` + +## Requirements + +### Dependencies +- `bash` - Shell interpreter +- `curl` - HTTP requests to OpenRouter API +- `jq` - JSON parsing for API responses +- `bc` - Performance metrics calculation + +### API access +- OpenRouter API key (get one at [openrouter.ai](https://openrouter.ai)) +- Set as environment variable: `OPENROUTER_API_KEY` + +## Performance + +The tool displays performance metrics after each query: +- **Model** - Which AI model processed the request +- **Provider** - The infrastructure provider that served it +- **Response Time** - Total time in seconds +- **Token Speed** - Generation speed in tokens/second + +Example output: +``` +$ ask "What is 2+2?" + +4 + +[inception/mercury-coder via Inception - 0.82s - 11.0 tok/s] +``` + +## Troubleshooting + +### API key not set +```bash +Error: OPENROUTER_API_KEY environment variable is not set +# Solution: export OPENROUTER_API_KEY="your-key-here" +``` + +### Missing dependencies +```bash +# Check for required tools +which curl jq bc + +# Install on macOS +brew install jq bc + +# Install on Ubuntu/Debian +sudo apt-get install jq bc +``` + +### No response or errors +```bash +# Test with verbose curl output +curl -v https://openrouter.ai/api/v1/chat/completions \ + -H "Authorization: Bearer $OPENROUTER_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"model":"google/gemini-2.5-flash","messages":[{"role":"user","content":"test"}]}' +``` + +## License + +MIT \ No newline at end of file diff --git a/ask b/ask new file mode 100755 index 0000000000000000000000000000000000000000..0cc2eb20c894cce6f341df86ac36669a52415d1e --- /dev/null +++ b/ask @@ -0,0 +1,201 @@ +#!/bin/bash + +# ask - Simple OpenRouter API CLI tool +# Usage: ask [OPTIONS] [PROMPT] + +set -euo pipefail + +# Check for API key +if [ -z "${OPENROUTER_API_KEY:-}" ]; then + echo "Error: OPENROUTER_API_KEY environment variable is not set" >&2 + exit 1 +fi + +# Model shortcuts function +get_model() { + case "$1" in + c) echo "inception/mercury-coder:nitro" ;; + g) echo "google/gemini-2.5-flash:nitro" ;; + s) echo "anthropic/claude-sonnet-4:nitro" ;; + k) echo "moonshotai/kimi-k2:nitro" ;; + q) echo "qwen/qwen3-235b-a22b-2507:nitro" ;; + esac +} + +# Default values +MODEL="inception/mercury-coder" +SYSTEM_PROMPT="" +PROMPT="" +STREAMING=false +NO_SYSTEM=false +PROVIDER_ORDER="" + +# Default system prompt (direct answers) +DEFAULT_PROMPT="You are a direct answer engine. Output ONLY the requested information. + +For commands: Output executable syntax only. No explanations, no comments. +For questions: Output the answer only. No context, no elaboration. + +Rules: +- If asked for a command, provide ONLY the command +- If asked a question, provide ONLY the answer +- Never include markdown formatting or code blocks +- Never add explanatory text before or after +- Assume output will be piped or executed directly +- For multi-step commands, use && or ; to chain them +- Make commands robust and handle edge cases silently" + +# Function to show help +show_help() { + cat << EOF +ask - Query AI models via OpenRouter API + +Usage: ask [OPTIONS] [PROMPT] + +Options: + -c Use inception/mercury-coder (default) + -g Use google/gemini-2.5-flash + -s Use anthropic/claude-sonnet-4 + -k Use moonshotai/kimi-k2 + -q Use qwen/qwen3-235b-a22b-2507 + -m MODEL Use custom model + -r Disable system prompt (raw model behavior) + --stream Enable streaming output + --system Set system prompt for the conversation + --provider Comma-separated list of providers for routing + -h, --help Show this help message + +Examples: + ask "Write a hello world in Python" + ask -g "Explain quantum computing" + ask -m openai/gpt-4o "What is 2+2?" + echo "Fix this code" | ask + ask --system "You are a pirate" "Tell me about sailing" + +EOF + exit 0 +} + +# Parse command line arguments +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) show_help ;; + -[cgskq]) + MODEL="$(get_model "${1:1}")" + shift ;; + -m) + MODEL="${2:?Error: -m requires a model name}" + shift 2 ;; + -r) + NO_SYSTEM=true + shift ;; + --stream) + STREAMING=true + shift ;; + --system) + SYSTEM_PROMPT="${2:?Error: --system requires a prompt}" + shift 2 ;; + --provider) + PROVIDER_ORDER="${2:?Error: --provider requires providers}" + shift 2 ;; + *) + PROMPT="$*" + break ;; + esac +done + +# If no prompt provided as argument, read from stdin +if [ -z "$PROMPT" ]; then + if [ -t 0 ]; then + echo "Error: No prompt provided. Use 'ask -h' for help." >&2 + exit 1 + fi + # Read all stdin, preserving multi-line format + PROMPT=$(cat) +fi + +# Apply default system prompt unless disabled or custom prompt provided +if [ "$NO_SYSTEM" = false ] && [ -z "$SYSTEM_PROMPT" ]; then + SYSTEM_PROMPT="$DEFAULT_PROMPT" +fi + +# Build messages array with proper JSON escaping +if [ -n "$SYSTEM_PROMPT" ]; then + MESSAGES='[{"role":"system","content":'"$(printf '%s' "$SYSTEM_PROMPT" | jq -Rs .)"'},{"role":"user","content":'"$(printf '%s' "$PROMPT" | jq -Rs .)"'}]' +else + MESSAGES='[{"role":"user","content":'"$(printf '%s' "$PROMPT" | jq -Rs .)"'}]' +fi + +# Record start time +START_TIME=$(date +%s.%N) + +# Build JSON payload once +PROVIDER_JSON="" +if [ -n "$PROVIDER_ORDER" ]; then + PROVIDER_JSON=',"provider":{"order":['$(echo "$PROVIDER_ORDER" | awk -F, '{for(i=1;i<=NF;i++) printf "\"%s\"%s", $i, (i&1 | while IFS= read -r line; do + # Check for errors + if echo "$line" | grep -q '"error"'; then + echo "Error: $(echo "$line" | jq -r '.error.message // .error // "Unknown error"')" >&2 + exit 1 + fi + + # Process SSE data lines + if [[ "$line" == data:* ]]; then + json="${line#data: }" + [ "$json" = "" ] || [ "$json" = "[DONE]" ] && continue + + content=$(echo "$json" | jq -r '.choices[0].delta.content // ""' 2>/dev/null) + [ -n "$content" ] && printf '%s' "$content" + fi + done + echo + + # Show metadata + ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc)) + echo + echo "[$MODEL - ${ELAPSED}s]" >&2 +else + # Non-streaming mode + response=$(curl -sS "$API_URL" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENROUTER_API_KEY" \ + -d "$JSON_PAYLOAD" 2>&1) + + # Check for errors + if echo "$response" | grep -q '"error"'; then + echo "Error: $(echo "$response" | jq -r '.error.message // .error // "Unknown error"')" >&2 + exit 1 + fi + + # Extract and print content + echo "$response" | jq -r '.choices[0].message.content // "No response received"' + + # Show metadata + ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc)) + TOKENS=$(echo "$response" | jq -r '.usage.completion_tokens // 0') + PROVIDER=$(echo "$response" | jq -r '.provider // "Unknown"') + TPS=$(echo "scale=1; $TOKENS / $ELAPSED" | bc 2>/dev/null || echo "0.0") + + echo + echo "[$MODEL via $PROVIDER - ${ELAPSED}s - ${TPS} tok/s]" >&2 +fi diff --git a/install.sh b/install.sh new file mode 100755 index 0000000000000000000000000000000000000000..db9ec358c06696feb6dc790b077eee7dbc33a86f --- /dev/null +++ b/install.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# install.sh - Install ask CLI tool system-wide + +set -euo pipefail + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo "Installing 'ask' CLI tool..." + +# Check if script exists +if [ ! -f "ask" ]; then + echo "Error: 'ask' script not found in current directory" >&2 + exit 1 +fi + +# Make executable +chmod +x ask + +# Install to /usr/local/bin +echo "Installing to /usr/local/bin/ask (requires sudo)..." +sudo cp ask /usr/local/bin/ + +echo -e "${GREEN}✓ Installation complete!${NC}" +echo + +# Get OpenRouter IPs +echo "Resolving OpenRouter DNS..." +IPS=$(dig +short openrouter.ai 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || nslookup openrouter.ai 2>/dev/null | grep -A1 "Name:" | grep "Address:" | awk '{print $2}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$') + +if [ -n "$IPS" ]; then + echo -e "${YELLOW}OpenRouter IP addresses:${NC}" + echo "$IPS" + echo + echo "To improve performance, you can add these to /etc/hosts:" + echo "----------------------------------------" + for IP in $IPS; do + echo "$IP openrouter.ai" + done | head -1 # Only show first IP as hosts file needs single entry + echo "----------------------------------------" + echo + echo "Add with: sudo nano /etc/hosts" + echo "(Only add ONE IP address to avoid conflicts)" +else + echo "Could not resolve OpenRouter IPs. Network may be unavailable." +fi + +echo +echo -e "${GREEN}Usage:${NC}" +echo " ask 'What is 2+2?'" +echo " ask -g 'Explain quantum computing'" +echo " ask --help" +echo +echo "Don't forget to set your API key:" +echo " export OPENROUTER_API_KEY='your-key-here'" \ No newline at end of file