1#!/bin/bash
  2
  3# ask - Simple OpenRouter API CLI tool
  4# Usage: ask [OPTIONS] [PROMPT]
  5
  6set -euo pipefail
  7
  8# Check for API key
  9if [ -z "${OPENROUTER_API_KEY:-}" ]; then
 10    echo "Error: OPENROUTER_API_KEY environment variable is not set" >&2
 11    exit 1
 12fi
 13
 14# Model shortcuts function
 15get_model() {
 16    case "$1" in
 17        c) echo "inception/mercury-coder:nitro" ;;
 18        g) echo "google/gemini-2.5-flash:nitro" ;;
 19        s) echo "anthropic/claude-sonnet-4:nitro" ;;
 20        k) echo "moonshotai/kimi-k2:nitro" ;;
 21        q) echo "qwen/qwen3-235b-a22b-2507:nitro" ;;
 22    esac
 23}
 24
 25# Default values
 26MODEL="inception/mercury-coder"
 27SYSTEM_PROMPT=""
 28PROMPT=""
 29STREAMING=false
 30NO_SYSTEM=false
 31PROVIDER_ORDER=""
 32
 33# Default system prompt (direct answers)
 34DEFAULT_PROMPT="You are a direct answer engine. Output ONLY the requested information.
 35
 36For commands: Output executable syntax only. No explanations, no comments.
 37For questions: Output the answer only. No context, no elaboration.
 38
 39Rules:
 40- If asked for a command, provide ONLY the command
 41- If asked a question, provide ONLY the answer
 42- Never include markdown formatting or code blocks
 43- Never add explanatory text before or after
 44- Assume output will be piped or executed directly
 45- For multi-step commands, use && or ; to chain them
 46- Make commands robust and handle edge cases silently"
 47
 48# Function to show help
 49show_help() {
 50    cat << EOF
 51ask - Query AI models via OpenRouter API
 52
 53Usage: ask [OPTIONS] [PROMPT]
 54
 55Options:
 56  -c          Use inception/mercury-coder (default)
 57  -g          Use google/gemini-2.5-flash
 58  -s          Use anthropic/claude-sonnet-4
 59  -k          Use moonshotai/kimi-k2
 60  -q          Use qwen/qwen3-235b-a22b-2507
 61  -m MODEL    Use custom model
 62  -r          Disable system prompt (raw model behavior)
 63  --stream    Enable streaming output
 64  --system    Set system prompt for the conversation
 65  --provider  Comma-separated list of providers for routing
 66  -h, --help  Show this help message
 67
 68Examples:
 69  ask "Write a hello world in Python"
 70  ask -g "Explain quantum computing"
 71  ask -m openai/gpt-4o "What is 2+2?"
 72  echo "Fix this code" | ask
 73  ask --system "You are a pirate" "Tell me about sailing"
 74
 75EOF
 76    exit 0
 77}
 78
 79# Parse command line arguments
 80while [ $# -gt 0 ]; do
 81    case "$1" in
 82        -h|--help) show_help ;;
 83        -[cgskq])
 84            MODEL="$(get_model "${1:1}")"
 85            shift ;;
 86        -m)
 87            MODEL="${2:?Error: -m requires a model name}"
 88            shift 2 ;;
 89        -r)
 90            NO_SYSTEM=true
 91            shift ;;
 92        --stream)
 93            STREAMING=true
 94            shift ;;
 95        --system)
 96            SYSTEM_PROMPT="${2:?Error: --system requires a prompt}"
 97            shift 2 ;;
 98        --provider)
 99            PROVIDER_ORDER="${2:?Error: --provider requires providers}"
100            shift 2 ;;
101        *)
102            PROMPT="$*"
103            break ;;
104    esac
105done
106
107# If no prompt provided as argument, read from stdin
108if [ -z "$PROMPT" ]; then
109    if [ -t 0 ]; then
110        echo "Error: No prompt provided. Use 'ask -h' for help." >&2
111        exit 1
112    fi
113    # Read all stdin, preserving multi-line format
114    PROMPT=$(cat)
115fi
116
117# Apply default system prompt unless disabled or custom prompt provided
118if [ "$NO_SYSTEM" = false ] && [ -z "$SYSTEM_PROMPT" ]; then
119    SYSTEM_PROMPT="$DEFAULT_PROMPT"
120fi
121
122# Build messages array with proper JSON escaping
123if [ -n "$SYSTEM_PROMPT" ]; then
124    MESSAGES='[{"role":"system","content":'"$(printf '%s' "$SYSTEM_PROMPT" | jq -Rs .)"'},{"role":"user","content":'"$(printf '%s' "$PROMPT" | jq -Rs .)"'}]'
125else
126    MESSAGES='[{"role":"user","content":'"$(printf '%s' "$PROMPT" | jq -Rs .)"'}]'
127fi
128
129# Record start time
130START_TIME=$(date +%s.%N)
131
132# Build JSON payload once
133PROVIDER_JSON=""
134if [ -n "$PROVIDER_ORDER" ]; then
135    PROVIDER_JSON=',"provider":{"order":['$(echo "$PROVIDER_ORDER" | awk -F, '{for(i=1;i<=NF;i++) printf "\"%s\"%s", $i, (i<NF?",":"")}')']}'
136fi
137
138JSON_PAYLOAD='{
139    "model": "'"$MODEL"'",
140    "messages": '"$MESSAGES"',
141    "stream": '$([ "$STREAMING" = true ] && echo true || echo false)"$PROVIDER_JSON"'
142}'
143
144API_URL="https://openrouter.ai/api/v1/chat/completions"
145
146# Add newline before answer
147echo
148
149# Make API request
150if [ "$STREAMING" = true ]; then
151    # Streaming mode
152    curl -sS "$API_URL" \
153      -H "Content-Type: application/json" \
154      -H "Authorization: Bearer $OPENROUTER_API_KEY" \
155      -d "$JSON_PAYLOAD" 2>&1 | while IFS= read -r line; do
156        # Check for errors
157        if echo "$line" | grep -q '"error"'; then
158            echo "Error: $(echo "$line" | jq -r '.error.message // .error // "Unknown error"')" >&2
159            exit 1
160        fi
161
162        # Process SSE data lines
163        if [[ "$line" == data:* ]]; then
164            json="${line#data: }"
165            [ "$json" = "" ] || [ "$json" = "[DONE]" ] && continue
166            
167            content=$(echo "$json" | jq -r '.choices[0].delta.content // ""' 2>/dev/null)
168            [ -n "$content" ] && printf '%s' "$content"
169        fi
170    done
171    echo
172    
173    # Show metadata
174    ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc))
175    echo
176    echo "[$MODEL - ${ELAPSED}s]" >&2
177else
178    # Non-streaming mode
179    response=$(curl -sS "$API_URL" \
180      -H "Content-Type: application/json" \
181      -H "Authorization: Bearer $OPENROUTER_API_KEY" \
182      -d "$JSON_PAYLOAD" 2>&1)
183
184    # Check for errors
185    if echo "$response" | grep -q '"error"'; then
186        echo "Error: $(echo "$response" | jq -r '.error.message // .error // "Unknown error"')" >&2
187        exit 1
188    fi
189
190    # Extract and print content
191    echo "$response" | jq -r '.choices[0].message.content // "No response received"'
192
193    # Show metadata
194    ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc))
195    TOKENS=$(echo "$response" | jq -r '.usage.completion_tokens // 0')
196    PROVIDER=$(echo "$response" | jq -r '.provider // "Unknown"')
197    TPS=$(echo "scale=1; $TOKENS / $ELAPSED" | bc 2>/dev/null || echo "0.0")
198    
199    echo
200    echo "[$MODEL via $PROVIDER - ${ELAPSED}s - ${TPS} tok/s]" >&2
201fi