now reports token usage

This commit is contained in:
Frederico @ VilaRosa02 2025-08-11 13:34:18 +00:00
parent 0497fadab8
commit cd5776963f

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# askChatGPT - A simple Bash client for OpenAI Chat API # askChatGPT - Bash client for OpenAI Chat API with token-usage to stderr
# Requires: curl, jq # Requires: curl, jq
# Env var: OPENAI_API_KEY must be set # Env: OPENAI_API_KEY must be set
set -euo pipefail set -euo pipefail
@ -9,7 +9,8 @@ MODEL="gpt-4o-mini"
PREPEND_TEXT="" PREPEND_TEXT=""
CONTENT_FILE="" CONTENT_FILE=""
LIST_MODELS=0 LIST_MODELS=0
declare -a PROMPT_WORDS=() # <-- important: initialize the array STREAM=0
declare -a PROMPT_WORDS=()
usage() { usage() {
cat <<EOF cat <<EOF
@ -20,42 +21,38 @@ Options:
--list-models List available models from OpenAI --list-models List available models from OpenAI
-prepend <text> Text to prepend before the main prompt -prepend <text> Text to prepend before the main prompt
-content-from-file <file> Read prompt content from file -content-from-file <file> Read prompt content from file
-h, --help Show this help message --stream Stream output (still reports usage to stderr)
-h, --help Show this help
EOF EOF
} }
# Parse args # Parse args
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-model) -model) MODEL="$2"; shift 2 ;;
MODEL="$2"; shift 2 ;; --list-models) LIST_MODELS=1; shift ;;
--list-models) -prepend) PREPEND_TEXT="$2"; shift 2 ;;
LIST_MODELS=1; shift ;; -content-from-file) CONTENT_FILE="$2"; shift 2 ;;
-prepend) --stream) STREAM=1; shift ;;
PREPEND_TEXT="$2"; shift 2 ;; -h|--help) usage; exit 0 ;;
-content-from-file) *) PROMPT_WORDS+=("$1"); shift ;;
CONTENT_FILE="$2"; shift 2 ;;
-h|--help)
usage; exit 0 ;;
*)
PROMPT_WORDS+=("$1"); shift ;;
esac esac
done done
# Check deps # Deps
for dep in curl jq; do for dep in curl jq; do
command -v "$dep" >/dev/null 2>&1 || { echo "Error: '$dep' is required."; exit 1; } command -v "$dep" >/dev/null 2>&1 || { echo "Error: '$dep' is required." >&2; exit 1; }
done done
# Check API key # API key
if [[ -z "${OPENAI_API_KEY:-}" ]]; then if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "Error: OPENAI_API_KEY is not set." echo "Error: OPENAI_API_KEY is not set." >&2
exit 1 exit 1
fi fi
# List models # List models
if [[ "$LIST_MODELS" -eq 1 ]]; then if [[ "$LIST_MODELS" -eq 1 ]]; then
curl -s https://api.openai.com/v1/models \ curl -sS https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Authorization: Bearer $OPENAI_API_KEY" \
| jq -r '.data[].id' | sort | jq -r '.data[].id' | sort
exit 0 exit 0
@ -64,7 +61,7 @@ fi
# Build prompt # Build prompt
PROMPT_MAIN="" PROMPT_MAIN=""
if [[ -n "$CONTENT_FILE" ]]; then if [[ -n "$CONTENT_FILE" ]]; then
[[ -f "$CONTENT_FILE" ]] || { echo "Error: file '$CONTENT_FILE' not found."; exit 1; } [[ -f "$CONTENT_FILE" ]] || { echo "Error: file '$CONTENT_FILE' not found." >&2; exit 1; }
PROMPT_MAIN="$(<"$CONTENT_FILE")" PROMPT_MAIN="$(<"$CONTENT_FILE")"
elif (( ${#PROMPT_WORDS[@]} > 0 )); then elif (( ${#PROMPT_WORDS[@]} > 0 )); then
PROMPT_MAIN="${PROMPT_WORDS[*]}" PROMPT_MAIN="${PROMPT_WORDS[*]}"
@ -72,7 +69,7 @@ else
if ! [ -t 0 ]; then if ! [ -t 0 ]; then
PROMPT_MAIN="$(cat)" PROMPT_MAIN="$(cat)"
else else
echo "Error: No prompt provided." echo "Error: No prompt provided." >&2
usage usage
exit 1 exit 1
fi fi
@ -84,11 +81,14 @@ if [[ -n "$PREPEND_TEXT" ]]; then
fi fi
PROMPT_FULL+="$PROMPT_MAIN" PROMPT_FULL+="$PROMPT_MAIN"
# JSON-escape content safely
JSON_CONTENT=$(printf '%s' "$PROMPT_FULL" | jq -Rs .) JSON_CONTENT=$(printf '%s' "$PROMPT_FULL" | jq -Rs .)
# Send request API_URL="https://api.openai.com/v1/chat/completions"
RESPONSE=$(curl -s https://api.openai.com/v1/chat/completions \
if [[ $STREAM -eq 0 ]]; then
# -------- Non-streaming ----------
RESPONSE=$(
curl -sS "$API_URL" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Authorization: Bearer $OPENAI_API_KEY" \
-d "$(cat <<EOF -d "$(cat <<EOF
@ -99,10 +99,11 @@ RESPONSE=$(curl -s https://api.openai.com/v1/chat/completions \
] ]
} }
EOF EOF
)") )"
)
# Extract and print answer (or error) # Print assistant reply to stdout
echo "$RESPONSE" | jq -r ' echo "$RESPONSE" | jq -r '
if .choices and .choices[0].message.content then if .choices and .choices[0].message.content then
.choices[0].message.content .choices[0].message.content
elif .error and .error.message then elif .error and .error.message then
@ -110,4 +111,64 @@ echo "$RESPONSE" | jq -r '
else else
"Error: No content in response" "Error: No content in response"
end end
' '
# Print token usage to stderr (if present)
if [[ "$(echo "$RESPONSE" | jq 'has("usage")')" == "true" ]]; then
PT=$(echo "$RESPONSE" | jq -r '.usage.prompt_tokens // 0')
CT=$(echo "$RESPONSE" | jq -r '.usage.completion_tokens // 0')
TT=$(echo "$RESPONSE" | jq -r '.usage.total_tokens // 0')
echo "tokens: prompt=$PT completion=$CT total=$TT" >&2
fi
else
# -------- Streaming ----------
# We request usage in the final streamed chunk
# Note: final chunk will have .usage populated and .choices == []
CURL_RESP=$(
curl -sS -N "$API_URL" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d "$(cat <<EOF
{
"model": "$MODEL",
"stream": true,
"stream_options": { "include_usage": true },
"messages": [
{"role": "user", "content": $JSON_CONTENT}
]
}
EOF
)"
)
# Process SSE lines
USAGE_PRINTED=0
# Well print content tokens as they come, and keep a trailing newline at the end
while IFS= read -r line; do
[[ $line != data:* ]] && continue
payload="${line#data: }"
[[ "$payload" == "[DONE]" ]] && break
# Print incremental content (no newline, flush as it streams)
chunk_content=$(printf '%s' "$payload" | jq -r '.choices[0].delta.content // empty' 2>/dev/null || true)
if [[ -n "$chunk_content" ]]; then
printf "%s" "$chunk_content"
fi
# If this chunk carries usage, print to stderr
if [[ "$(printf '%s' "$payload" | jq 'has("usage")' 2>/dev/null || echo false)" == "true" ]]; then
PT=$(printf '%s' "$payload" | jq -r '.usage.prompt_tokens // 0')
CT=$(printf '%s' "$payload" | jq -r '.usage.completion_tokens // 0')
TT=$(printf '%s' "$payload" | jq -r '.usage.total_tokens // 0')
echo "" # ensure the streamed answer ends with newline on stdout
echo "tokens: prompt=$PT completion=$CT total=$TT" >&2
USAGE_PRINTED=1
fi
done <<< "$CURL_RESP"
# Safety: if for some reason usage didnt arrive, still end with newline
if [[ $USAGE_PRINTED -eq 0 ]]; then
echo ""
fi
fi