ask

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