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