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