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:nitro"
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