Add Self-hosted STT/TTS/Embedding providers that read baseUrl per connection
instead of a fixed registry endpoint, so 9Router can point at whisper.cpp,
faster-whisper, Kokoro-FastAPI, llama-server, vLLM, Infinity, and similar
OpenAI-compatible local servers.
Self-hosted Embedding refuses to run without a baseUrl rather than falling
back to api.openai.com like openaiCompatNode does, since that fallback would
silently send input text and the API key to OpenAI under a provider named
"Self-hosted". Also fixes embeddingsCore to catch adapter build errors as a
400 instead of letting them escape uncaught, and bounds the upstream fetch
with FETCH_CONNECT_TIMEOUT_MS to avoid hanging forever on a dead endpoint.
Self-hosted TTS treats a bare model value as the model rather than the voice,
since the generic OpenAI TTS convention (bare = voice) is backwards for a
provider where the model is the variable part.
handleForcedSSEToJson dropped cached prompt tokens in two ways: the
Responses branch summed only input_tokens, which excludes cache_read
and cache_creation on cache-capable upstreams (measured 2012 reported
vs ~5344 actual, 5332 from cache); and the Chat Completions branch
computed usage correctly but it didn't always reach the client (an
Anthropic response with cache_read_input_tokens: 11022 arrived with no
usage field at all). Now folds cache counters into prompt_tokens,
surfaces them via prompt_tokens_details, and re-attaches usage before
serialisation.
The previous blanket strip in GithubExecutor.transformRequest removed
`thinking` AND `reasoning_effort` for every GitHub-routed model to avoid
Claude-on-Copilot 400s from OpenClaw. That regressed GPT-5 family support
(gh/gpt-5-mini honors reasoning_effort: low/medium/high).
Make supportsThinking(model) model-aware — return false only for Claude
models, so the strip fires only where the upstream actually rejects these
fields.
Benchmarks on /v1/chat/completions via GitHub Copilot:
effort=(none) → 64 reasoning_tokens, ~2.0s
effort=low → 0 reasoning_tokens, ~1.55s
effort=medium → 64 reasoning_tokens, ~1.9s
effort=high → 128 reasoning_tokens, ~2.2s
Made-with: Cursor
The fetchCompatibleModelIds() function had no timeout on its fetch() call,
causing /v1/models to hang indefinitely when an openai-compatible provider
was unreachable or slow to respond.
Additionally, upstream/cross-instance connections (provider IDs containing
a UUID suffix like openai-compatible-chat-XXXXXXXX) would trigger recursive
/models fetches between instances, creating infinite loops.
Fixes:
- Add AbortController with 5-second timeout to the fetch() call
- Skip dynamic model fetching for upstream/cross-instance connections
(detected by UUID suffix pattern in provider ID)
- Existing try/catch already handles abort errors gracefully
Co-authored-by: Agent Zero <agent@agent-zero.local>
sseToJsonHandler.js unconditionally deleted reasoning_content from all
non-streaming responses (added for Firecrawl SDK compatibility). This
breaks thinking models (Qwen3.5, Claude extended thinking, etc.) where
the model may use all tokens for reasoning, leaving content empty.
When reasoning_content is stripped in that case, the response appears
completely empty to the client.
Fix: only strip reasoning_content when the response also has non-empty
content, so that reasoning output is preserved when it is the only
useful output.
Co-authored-by: Agent Zero <agent@agent-zero.local>