# AxForge connection doctor (PowerShell 5.1 or 7). 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. # # irm https://dev.axforge.ai/assets/axforge-doctor.ps1 | iex # # Download it and read it first: https://dev.axforge.ai/assets/axforge-doctor.ps1 (then: .\axforge-doctor.ps1; run as # a file, its exit code is the number of crosses). It asks for your key (typed unseen), or reads $env:AXFORGE_API_KEY. # Optional: $env:AXFORGE_BASE_URL (what you gave your tool; the right one is https://api.axforge.ai/v1) and # $env:AXFORGE_MODEL (default qwen3.8-27b). Everything runs inside a block, so nothing stays in your session. $fails = & { $ErrorActionPreference = "Stop" $st = @{ fails = 0 } $claude = $false $right = "https://api.axforge.ai/v1" $key = $env:AXFORGE_API_KEY if (-not $key) { $secret = Read-Host "Your AxForge API key (console.axforge.ai/keys; typed unseen, it stays in this run)" -AsSecureString $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret) try { $key = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) } } $key = $key.Trim() $base = if ($env:AXFORGE_BASE_URL) { $env:AXFORGE_BASE_URL.Trim() } else { $right } $model = if ($env:AXFORGE_MODEL) { $env:AXFORGE_MODEL.Trim() } else { "qwen3.8-27b" } Write-Host "" Write-Host "AxForge connection doctor: $base, model $model. A few tiny calls on your key follow." Write-Host "" function Say($ok, $what, $fix) { if ($ok) { Write-Host " OK $what" } else { Write-Host " FAIL $what"; Write-Host " fix: $fix"; $st.fails++ } } # one call to the API: the status code and the body, whatever the server answered function Call($method, $path, $body) { $req = @{ Method = $method; Uri = ($base.TrimEnd("/") + $path); Headers = @{ Authorization = "Bearer $key" }; TimeoutSec = 120 } if ($body) { $req.ContentType = "application/json"; $req.Body = $body } try { $r = Invoke-WebRequest @req -UseBasicParsing return @{ status = [int]$r.StatusCode; text = [string]$r.Content } } catch { $resp = $_.Exception.Response $status = 0; $text = "" if ($resp) { try { $status = [int]$resp.StatusCode } catch {} if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $text = $_.ErrorDetails.Message } elseif ($resp.GetResponseStream) { try { $text = (New-Object IO.StreamReader($resp.GetResponseStream())).ReadToEnd() } catch {} } } else { $text = $_.Exception.Message } return @{ status = $status; text = $text } } } function ErrorWords($text) { try { $e = ($text | ConvertFrom-Json).error; if ($e.message) { return $e.message } ; if ($e) { return [string]$e } } catch {} if ($text.Length -gt 160) { return $text.Substring(0, 160) } return $text } # 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. $u = $base.TrimEnd("/") if ($u -match '^http://') { Say $false "the base URL $base" "use https: $right" } elseif ($u -match '/chat/completions$|/models$|/responses$|/messages$') { Say $false "the base URL $base" "the base URL is the part before the door: $right" } elseif ($u -match 'api\.openai\.com') { Say $false "the base URL $base" "that is OpenAI's address; AxForge's is $right" } elseif ($u -eq 'https://api.axforge.ai') { $claude = $true; Write-Host " NOTE the base URL ${base}: right for Claude Code (it adds /v1/messages itself); an OpenAI-style tool wants $right" } elseif ($u -notmatch '/v1$') { Say $false "the base URL $base" "end it with /v1: $right" } elseif ($u -ne $right) { Say $false "the base URL $base" "AxForge's address is $right" } else { Say $true "the base URL $base" "" } if ($u -ne $right) { Write-Host " the checks below use $right (the key goes nowhere else)"; $base = $right; $u = $right } # 2. the key: the model list answers $m = Call "Get" "/models" $models = @() if ($m.status -eq 200) { try { $models = @(($m.text | ConvertFrom-Json).data | ForEach-Object { $_.id }) } catch {} Say $true "the key (the model list answers)" "" } elseif ($m.status -eq 401) { Say $false "the key: refused (401)" "paste it whole, or make a new one at console.axforge.ai/keys" } elseif ($m.status -eq 403) { Say $false "the key: not allowed (403): $(ErrorWords $m.text)" "the key's scopes or the account's models on console.axforge.ai/keys" } elseif ($m.status -eq 404) { Say $false "the key: nothing answers at $u/models (404)" "the base URL is $right" } elseif ($m.status -eq 0) { Say $false "the key: no connection ($($m.text))" "the network, a proxy, or the base URL $right" } else { Say $false "the key: $($m.status) $(ErrorWords $m.text)" "try again in a minute; if it stays, https://dev.axforge.ai/docs/test-a-call/" } # 3. the model is one we serve if ($models.Count -gt 0) { if ($models -contains $model) { Say $true "the model $model is served" "" } else { Say $false "the model $model is not on the list" ("use one of: " + (($models | Where-Object { $_ -notmatch '^(chat|xev|ocr)' } | Select-Object -First 8) -join ", ")) } } elseif ($m.status -eq 200) { Say $false "the model ${model}: the list at $u/models is empty" "no model is served right now; try again in a minute, or ask us: https://axforge.ai/#talk-engineer" } else { Say $false "the model $model (the list could not be read)" "fix the key or the base URL first" } # 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",' function Cut($text) { try { return ([string]($text | ConvertFrom-Json).choices[0].finish_reason) -eq "length" } catch { return $false } } # 4. a short answer $body = '{"model":"' + $model + '",' + $probe + '"max_tokens":200,"messages":[{"role":"user","content":"Say hello in two words."}]}' $c = Call "Post" "/chat/completions" $body $content = "" if ($c.status -eq 200) { try { $content = [string]($c.text | ConvertFrom-Json).choices[0].message.content } catch {} } if ($c.status -eq 200 -and $content -ne "") { Say $true "a short answer: $($content.Trim())" "" } elseif ($c.status -eq 200 -and (Cut $c.text)) { Say $true "a short answer: the model was still thinking when the probe's tokens ran out (it thinks before it writes); the call itself works" "" } elseif ($c.status -eq 402) { Say $false "a short answer: no balance (402)" "top up or pick a plan at console.axforge.ai/billing" } elseif ($c.status -eq 404) { Say $false "a short answer: $(ErrorWords $c.text)" "the model name as the list above has it" } elseif ($c.status -eq 429) { Say $false "a short answer: rate limited (429)" "wait a minute; your tool may be sending calls in parallel" } elseif ($c.status -eq 200) { Say $false "a short answer came back empty" "a bigger max_tokens; a thinking model spends tokens before it answers" } else { Say $false "a short answer: $($c.status) $(ErrorWords $c.text)" "https://dev.axforge.ai/docs/test-a-call/ says whether it is the key, the balance or us" } # 5. a stream, in pieces $body = '{"model":"' + $model + '",' + $probe + '"max_tokens":200,"stream":true,"messages":[{"role":"user","content":"Count from one to five."}]}' $s = Call "Post" "/chat/completions" $body $pieces = 0 if ($s.status -eq 200) { $pieces = @([regex]::Matches($s.text, '(?m)^data: \{')).Count } if ($pieces -ge 2 -and $s.text -match 'data: \[DONE\]') { Say $true "a stream: $pieces pieces, then [DONE]" "" } elseif ($s.status -eq 200) { Say $false "a stream came back whole ($pieces pieces)" "a proxy or a tool setting buffers the stream; try without the proxy" } else { Say $false "a stream: $($s.status) $(ErrorWords $s.text)" "the same fix as the short answer above" } # 6. a tool call comes back as a tool call $body = '{"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"]}}}]}' $t = Call "Post" "/chat/completions" $body $tool = "" if ($t.status -eq 200) { try { $tool = [string]($t.text | ConvertFrom-Json).choices[0].message.tool_calls[0].function.name } catch {} } if ($tool -eq "get_weather") { Say $true "a tool call comes back as a tool call (get_weather)" "" } elseif ($t.status -eq 200 -and (Cut $t.text)) { Say $true "a tool call: the model was still thinking when the probe's tokens ran out; give your tool a bigger max_tokens" "" } elseif ($t.status -eq 200) { Say $false "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 $false "a tool call: $($t.status) $(ErrorWords $t.text)" "the same fix as the short answer above" } # 7. the MCP door (web search, docs search, read a page) for coding agents $mcp = Call "Post" "/mcp" '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' $names = @() if ($mcp.status -eq 200) { try { $names = @(($mcp.text | ConvertFrom-Json).result.tools | ForEach-Object { $_.name }) } catch {} } if ($names.Count -gt 0) { Say $true "the MCP door lists its tools: $($names -join ', ')" "" $try = Call "Post" "/mcp" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"docs_search","arguments":{"query":"quickstart","max_results":1}}}' $said = "" if ($try.status -eq 200) { try { $said = [string]($try.text | ConvertFrom-Json).result.content[0].text } catch {} } if ($said -match 'comes with the Coder plans') { Write-Host " note: its tools need a Coder or Managed Endpoint plan (console.axforge.ai/billing/token-plan); the API itself works without one" } elseif ($said -ne "") { Write-Host " your plan can use them" } } elseif ($mcp.status -eq 404) { Say $false "the MCP door: nothing at $u/mcp" "the base URL is $right; the door is $right/mcp" } else { Say $false "the MCP door: $($mcp.status) $(ErrorWords $mcp.text)" "https://dev.axforge.ai/docs/connect/web-search/" } Write-Host "" if ($st.fails -eq 0 -and $claude) { Write-Host "All good. Claude Code wants base URL https://api.axforge.ai; an OpenAI-style tool wants $right; then your key and model $model." } elseif ($st.fails -eq 0) { Write-Host "All good. Your tool wants: base URL $right, your key, model $model." } else { Write-Host "$($st.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/" } $st.fails } # run as a file, the exit code is the number of crosses; piped into iex, your session stays open if ($PSCommandPath) { exit $fails }