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