#!/usr/bin/env bash
# AxForge connection doctor (bash, curl only). Seven checks against the API with your key, a tick or a cross each, and
# the fix for every cross: the key, the base URL, the model, a short answer, a stream, a tool call, the MCP door.
# It makes a few tiny calls on your key (a handful of tokens). The key is sent to https://api.axforge.ai/v1 and
# nowhere else: a wrong address is reported, not called.
#
#   curl -fsSL https://dev.axforge.ai/assets/axforge-doctor.sh | bash
#
# Download it and read it first: https://dev.axforge.ai/assets/axforge-doctor.sh (then: bash axforge-doctor.sh; its
# exit code is the number of crosses). It asks for your key (typed unseen), or reads $AXFORGE_API_KEY. Optional:
# $AXFORGE_BASE_URL (what you gave your tool; the right one is https://api.axforge.ai/v1) and $AXFORGE_MODEL (default
# qwen3.8-27b).
set -u
right="https://api.axforge.ai/v1"
key="${AXFORGE_API_KEY:-}"
if [ -z "$key" ]; then
  read -r -s -p "Your AxForge API key (console.axforge.ai/keys; typed unseen, it stays in this run): " key < /dev/tty
  echo
fi
key="$(printf '%s' "$key" | tr -d '[:space:]')"
base="${AXFORGE_BASE_URL:-$right}"
base="${base%/}"
model="${AXFORGE_MODEL:-qwen3.8-27b}"
fails=0
claude=""
tmp="$(mktemp)"
trap 'rm -f "$tmp" "$tmp.err"' EXIT

echo
echo "AxForge connection doctor: $base, model $model. A few tiny calls on your key follow."
echo

say() { # ok|fail, what, fix
  if [ "$1" = ok ]; then echo "  OK    $2"; else echo "  FAIL  $2"; echo "        fix: $3"; fails=$((fails + 1)); fi
}
# one call to the API: the status code in $status, the body in $tmp
call() { # method, path, [body]
  if [ $# -ge 3 ]; then
    status="$(curl -sS -m 120 -o "$tmp" -w '%{http_code}' -X "$1" "$base$2" -H "Authorization: Bearer $key" -H "Content-Type: application/json" -d "$3" 2>"$tmp.err")" || status=0
  else
    status="$(curl -sS -m 120 -o "$tmp" -w '%{http_code}' -X "$1" "$base$2" -H "Authorization: Bearer $key" 2>"$tmp.err")" || status=0
  fi
  [ "$status" != 000 ] || status=0
}
words() { # the server's error message, or the body's start
  local m
  m="$(grep -o '"message":"[^"]*"' "$tmp" | head -1 | sed 's/"message":"//; s/"$//')"
  if [ -n "$m" ]; then printf '%s' "$m"; else head -c 160 "$tmp" | tr '\n' ' '; fi
}
cut_short() { grep -q '"finish_reason":"length"' "$tmp"; } # the model ran out of tokens before it was done

# 1. the base URL's shape: the mistakes we see. After a cross the checks below use the right address: the key is
#    never sent to another host, and the rest still tells you whether the key and the model work.
case "$base" in
  http://*)        say fail "the base URL $base" "use https: $right" ;;
  */chat/completions|*/models|*/responses|*/messages)
                   say fail "the base URL $base" "the base URL is the part before the door: $right" ;;
  *api.openai.com*) say fail "the base URL $base" "that is OpenAI's address; AxForge's is $right" ;;
  */v1)            if [ "$base" = "$right" ]; then say ok "the base URL $base"; else say fail "the base URL $base" "AxForge's address is $right"; fi ;;
  https://api.axforge.ai) claude=1; echo "  NOTE  the base URL $base: right for Claude Code (it adds /v1/messages itself); an OpenAI-style tool wants $right" ;;
  *)               say fail "the base URL $base" "end it with /v1: $right" ;;
esac
if [ "$base" != "$right" ]; then echo "        the checks below use $right (the key goes nowhere else)"; base="$right"; fi

# 2. the key: the model list answers
call GET /models
models=""
case "$status" in
  200) models="$(grep -o '"id":"[^"]*"' "$tmp" | sed 's/"id":"//; s/"$//' | tr '\n' ' ')"; say ok "the key (the model list answers)" ;;
  401) say fail "the key: refused (401)" "paste it whole, or make a new one at console.axforge.ai/keys" ;;
  403) say fail "the key: not allowed (403): $(words)" "the key's scopes or the account's models on console.axforge.ai/keys" ;;
  404) say fail "the key: nothing answers at $base/models (404)" "the base URL is $right" ;;
  0)   say fail "the key: no connection ($(head -c 120 "$tmp.err" 2>/dev/null | tr '\n' ' '))" "the network, a proxy, or the base URL $right" ;;
  *)   say fail "the key: $status $(words)" "try again in a minute; if it stays, https://dev.axforge.ai/docs/test-a-call/" ;;
esac

# 3. the model is one we serve
if [ -n "$models" ]; then
  case " $models " in
    *" $model "*) say ok "the model $model is served" ;;
    *) say fail "the model $model is not on the list" "use one of: $(printf '%s\n' $models | grep -v -E '^(chat|xev|ocr)' | head -8 | tr '\n' ' ')" ;;
  esac
elif [ "$status" = 200 ]; then
  say fail "the model $model: the list at $base/models is empty" "no model is served right now; try again in a minute, or ask us: https://axforge.ai/#talk-engineer"
else
  say fail "the model $model (the list could not be read)" "fix the key or the base URL first"
fi

# the probes ask for light thinking and give room for it: a model that thinks first (GLM) must not run out of tokens
# before its first word
probe='"reasoning_effort":"low",'

# 4. a short answer
call POST /chat/completions '{"model":"'"$model"'",'"$probe"'"max_tokens":200,"messages":[{"role":"user","content":"Say hello in two words."}]}'
content=""
[ "$status" = 200 ] && content="$(grep -o '"content":"[^"]*"' "$tmp" | head -1 | sed 's/"content":"//; s/"$//')"
case "$status" in
  200) if [ -n "$content" ]; then say ok "a short answer: $content"
       elif cut_short; then say ok "a short answer: the model was still thinking when the probe's tokens ran out (it thinks before it writes); the call itself works"
       else say fail "a short answer came back empty" "a bigger max_tokens; a thinking model spends tokens before it answers"; fi ;;
  402) say fail "a short answer: no balance (402)" "top up or pick a plan at console.axforge.ai/billing" ;;
  404) say fail "a short answer: $(words)" "the model name as the list above has it" ;;
  429) say fail "a short answer: rate limited (429)" "wait a minute; your tool may be sending calls in parallel" ;;
  *)   say fail "a short answer: $status $(words)" "https://dev.axforge.ai/docs/test-a-call/ says whether it is the key, the balance or us" ;;
esac

# 5. a stream, in pieces
call POST /chat/completions '{"model":"'"$model"'",'"$probe"'"max_tokens":200,"stream":true,"messages":[{"role":"user","content":"Count from one to five."}]}'
pieces=0
[ "$status" = 200 ] && pieces="$(grep -c '^data: {' "$tmp")"
if [ "$status" = 200 ] && [ "$pieces" -ge 2 ] && grep -q '^data: \[DONE\]' "$tmp"; then say ok "a stream: $pieces pieces, then [DONE]"
elif [ "$status" = 200 ]; then say fail "a stream came back whole ($pieces pieces)" "a proxy or a tool setting buffers the stream; try without the proxy"
else say fail "a stream: $status $(words)" "the same fix as the short answer above"; fi

# 6. a tool call comes back as a tool call
call POST /chat/completions '{"model":"'"$model"'",'"$probe"'"max_tokens":600,"tool_choice":"auto","messages":[{"role":"user","content":"What is the weather in Stockholm right now? Use the tool."}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Current weather in a city","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}]}'
if [ "$status" = 200 ] && grep -q '"tool_calls"' "$tmp" && grep -q '"name":"get_weather"' "$tmp"; then say ok "a tool call comes back as a tool call (get_weather)"
elif [ "$status" = 200 ] && cut_short; then say ok "a tool call: the model was still thinking when the probe's tokens ran out; give your tool a bigger max_tokens"
elif [ "$status" = 200 ]; then say fail "a tool call came back as text, not as a tool call" "a model that does tool calls (qwen3.8-27b, mimo-v2.6-flash, glm-5.3-flash, qwen3.8-flash-next) and tool_choice auto"
else say fail "a tool call: $status $(words)" "the same fix as the short answer above"; fi

# 7. the MCP door (web search, docs search, read a page) for coding agents
call POST /mcp '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
names=""
[ "$status" = 200 ] && names="$(grep -o '"name":"[^"]*"' "$tmp" | sed 's/"name":"//; s/"$//' | tr '\n' ' ')"
if [ -n "$names" ]; then
  say ok "the MCP door lists its tools: $names"
  call POST /mcp '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"docs_search","arguments":{"query":"quickstart","max_results":1}}}'
  if grep -q 'comes with the Coder plans' "$tmp"; then echo "        note: its tools need a Coder or Managed Endpoint plan (console.axforge.ai/billing/token-plan); the API itself works without one"
  elif [ "$status" = 200 ]; then echo "        your plan can use them"; fi
elif [ "$status" = 404 ]; then say fail "the MCP door: nothing at $base/mcp" "the base URL is $right; the door is $right/mcp"
else say fail "the MCP door: $status $(words)" "https://dev.axforge.ai/docs/connect/web-search/"; fi

echo
if [ "$fails" -eq 0 ] && [ -n "$claude" ]; then echo "All good. Claude Code wants base URL https://api.axforge.ai; an OpenAI-style tool wants $right; then your key and model $model."
elif [ "$fails" -eq 0 ]; then echo "All good. Your tool wants: base URL $right, your key, model $model."
else echo "$fails to fix. The guides: https://dev.axforge.ai/docs/connect/ - a real call, stage by stage: https://dev.axforge.ai/docs/test-a-call/"; fi
exit "$fails"
