The advanced spec for Qwen3.8-Flash-Next: every endpoint, every parameter this lane really accepts, and copy-paste setups for five agent harnesses.
| Wire format | Base URL | Credential |
|---|---|---|
OpenAI/chat/completions /completions /responses | https://aipassport.trirat.co/v1with the /v1 | Authorization: Bearer sk-… |
Anthropic/messages /messages/count_tokens | https://aipassport.trirat.cono /v1 — the client appends it | x-api-key: sk-… or Bearer+ anthropic-version: 2023-06-01 |
One model id: qwen3.8-flash-next. Precision GGUF UD-IQ4_XS, speculative decoding
MTP (Thai draft vocabulary). Keys carry no requests-per-minute, tokens-per-minute or concurrency limit —
and they are scoped to this id, so calling any other name returns an auth-scope error rather than a 404.
The previous id qwen3.8-27b still works on every key and reaches the same model, so existing configs need
no change.
The rows are previous-model measurements, except /v1/completions. Endpoint
availability depends on the engine behind the router lane, not only on the lane: /v1/completions returned 200
on the previous model and returns 404 on this one (re-measured 2026-10-23). The other rows have not been
re-exercised since 2026-09-05.
| Endpoint | Notes | |
|---|---|---|
GET /v1/models | 200 | the ids your key may call |
GET /v1/model/info | 200 | capability metadata — max_input_tokens, max_output_tokens, supports_reasoning, supports_vision |
POST /v1/chat/completions | 200 | primary surface · streaming and non-streaming |
POST /v1/messages | 200 | Anthropic Messages · tools and streaming |
POST /v1/messages/count_tokens | 200 | token counting |
POST /v1/responses | 200 | OpenAI Responses · function tools |
GET /health | 200 | liveness, no auth — portal process, not the model |
POST /v1/completions | 404 | not served by this model's engine — legacy text completion; use POST /v1/chat/completions (re-measured 2026-10-23) |
/v1/embeddings /v1/rerank/v1/moderations /v1/audio/* | 404 | not on this lane — one chat model only. Worse than useless: see trap 1 |
/v1/key/* /v1/user/*/v1/spend/* /v1/team/* | 404 | management plane is not exposed |
Each endpoint links to its own shareable page — /docs/<endpoint> —
which serves this same document with that endpoint's link preview and jumps to its row.
The lane runs thinking ON at medium effort by default. Two consequences bite people
immediately:
max_tokens returns empty content,
because reasoning shares the output budget. Measured on this deployment (2026-10-12):
max_tokens: 8 returned finish_reason: "length" with content: null after 51 characters
of reasoning. Give thinking-on requests 2–4k output regardless.reasoning_effort can express off on this deployment:
none, off and minimal all returned 200 with no reasoning. chat_template_kwargs
with enable_thinking: false also works, and it is the only off switch the previous model had.| Value | Why | |
|---|---|---|
low medium xhigh | 200 | the recommended ladder: accepted here and on the previous model |
high max | 200 | accepted here as the deepest tier, the same as xhigh; a 400 on the previous model |
none off minimal | 200 | accepted here: thinking off |
banana | 400 | "unknown reasoning effort 'banana': use none, low, medium or high" |
⚠️ Effort vocabularies are model-specific and have changed with every model behind this
endpoint. Every row above was measured on this deployment on 2026-10-12. A config that has to
survive a model change should stick to low / medium / xhigh and use chat_template_kwargs for
off.
curl https://aipassport.trirat.co/v1/chat/completions \
-H "Authorization: Bearer sk-YOUR_KEY" -H "Content-Type: application/json" \
-d '{"model":"qwen3.8-flash-next",
"chat_template_kwargs":{"enable_thinking":false},
"messages":[{"role":"user","content":"say ok"}]}'
enable_thinking: false is the portable off switch; on this deployment
reasoning_effort: "none" works too. A client-supplied chat_template_kwargs replaces the lane default
wholesale, so include enable_thinking: true yourself if you send the object and still want
thinking. carried That it also overrides a top-level effort was measured on the previous model.
| Wire | Reasoning text | Counted |
|---|---|---|
| OpenAI · chat | choices[0].message.reasoning_content (streamed: delta.reasoning_content) — measured; reasoning absent | streamed: usage.completion_tokens_details.reasoning_tokens; non-streamed: usage.completion_tokens total only |
| OpenAI · responses | a reasoning output item before the message — measured | usage carried |
| Anthropic | discarded — only a text block comes back — measured | still counted in output_tokens carried |
Anthropic-format thinking controls measured inert on the previous model:
thinking:{type:"disabled"}, budget_tokens and output_config.effort all returned
200 and all left the model thinking at the lane default. The proof they were dropped rather than
honoured was that output_config:{effort:"banana"} returned 200 while
reasoning_effort:"banana" returned 400. This is a router-translation behaviour, not a weights
behaviour, so it is expected to hold — but if you need depth control or thinking off, use the OpenAI
wire, which is unambiguous either way.
| Parameters | Notes | |
|---|---|---|
| works | temperature top_p top_k min_p presence_penalty repetition_penalty seed stop logprobs top_logprobs max_tokens max_completion_tokens stream stream_options; frequency_penalty carried | accepted (200) on this deployment, 2026-10-12. Lane defaults: temperature 1.0, top_p 0.5, presence_penalty 0.0; a value you send replaces the default. For Thai, keep top_p at 0.5 or lower: measured 2026-10-12 at temperature 1.0, replies containing Chinese characters fell from 16/180 at top_p 0.95 to 2/180 at 0.5, with HumanEval, tools and vision unchanged and single-stream decode ~5 % faster. min_p now works at nonzero temperature. Per the engine's documentation the penalties are not applied while several requests decode together, so treat them as best-effort |
| ignored | n | accepted and ignored: n: 2 returned one choice on this deployment |
| 400 | logit_bias | "logit_bias: continuous batching is not supported; run without --batch." Measured on this deployment. Remove it from client plumbing rather than catching the error |
| works | response_format {type:"json_object"} response_format {type:"json_schema"} | measured on this deployment. A JSON schema is validated, not enforced while generating: the model is shown the schema and its answer is checked afterwards, so a non-conforming answer comes back as an error (structured_output_failed, 502 through the router) rather than a corrected answer. Describe the schema in the prompt too, and retry on that error. Structured output cannot be combined with tools |
| unverified | guided_regex | accepted (200); whether it is enforced was not established on this deployment. Do not rely on it |
| works | tools tool_choice (auto · required · named) parallel_tool_calls | the tool call is correct in every form tested on this deployment, and a named forced choice now reports finish_reason: "tool_calls" (see trap 7) |
| works | image_url (data URL or https) | 17 images in one prompt were accepted and counted correctly on this deployment; declare 16 in clients |
| ignored | guided_choice carried | accepted and not enforced — it fails open to free prose. Use a JSON schema |
| 400 | reasoning_effort outside none / off / minimal / low / medium / high / xhigh / max | measured; see §3 |
| Limit | Value | How it fails |
|---|---|---|
| Context window | 262,144 tokens, prompt + max_tokens combined | 400 "request exceeds the context window: prompt (…) + max tokens (…) exceeds the context (262144); requests are never truncated" — measured |
| Declared max input | 228,352 tokens | 1,024 below window − max output on purpose: the chat template adds tokens your tokenizer cannot see. carried A 228,338-token prompt with thinking on and a full 32,768 reservation was accepted; 229,376 at that reservation 400s. Client configs below therefore declare a 261,120 window |
| Deep-context recall | not guaranteed near the window | this is an acceptance limit, not a recall promise. On this deployment a code word planted at ~8 % depth was found in a 259,996-token prompt, and in each of four ~250K prompts read at once (direct route, one sample each). Budget accordingly for retrieval-style prompts |
| Public-path depth ceiling | not re-measured over aipassport.trirat.co | prefill is silent through the router, and Cloudflare cuts at ~125 s. This deployment reads prompts at ~4,400 tok/s (direct: 260K tokens in 59 s), so a request that does not queue behind other long prompts should fit the full 228,352 inside that timer. Prompts are read one at a time: behind other long prompts the wait adds up, so stream and keep prompts lean. The full window is always reachable on a direct/LAN route |
| Declared max output | 32,768 | declare 32,768 in clients; the server enforces the window arithmetic above |
| Images per prompt | no engine cap (17 accepted) | declare 16 in clients — measured |
| Image size | ≤ 1 Mpx longest edge | resized server-side, no error |
| Concurrency | 4 batch slots on the GPU host, shared by every lane | excess queues for a slot, it does not 429 (8 concurrent long agent conversations were all served in testing). The router lane also caps concurrency and queues the rest carried |
| Long requests carried | ~125 s of silence | Cloudflare 524. Stream anything slow: on the previous model the same 30k-token prompt gave 524 at 125 s non-streaming vs 200 at 224 s streaming. The silence timer is Cloudflare's, so it is model-independent; the timings are not |
Prompt caching is on server-side, and usage now reports it: a repeated 2,660-token prompt
came back with usage.prompt_tokens_details.cached_tokens: 2653 (measured 2026-10-12). An agent
conversation is kept for its next turn, so a follow-up re-reads only what was added.
| Status | Meaning | What to do |
|---|---|---|
| 400 | parameter or context-window rejection; the upstream message is preserved verbatim | read the message — it names the field |
| 401 | missing key, or a key not starting with sk- | check the credential header for your wire format |
| 429 carried | No deployments available … Try again in 30 seconds | a 30 s router cooldown, not a rate limit — see trap 1 |
| 502 | structured_output_failed | the answer did not match your JSON schema (§4) — retry |
| 503 | code: "model_offline" + retry-after: 1800 | the GPU box is mid-maintenance or training. Back off; it usually serves 07:00-18:00 ICT (Bangkok) |
| 524 | Cloudflare silence timer | stream the request |
The 429 shape quoted here is a previous-model observation; the status codes and their meanings are router behaviour and are expected to be identical. The 400 and 502 messages were measured on this deployment.
POST /v1/embeddings (404 — there is no embedding model) puts the chat deployment into
the router's cooldown, and the next /v1/chat/completions returns 429 for ~30 s before
recovering on its own. Do not probe endpoints you do not use, and wait the cooldown out.max_tokens: 8 returned content: null (§3). Disable thinking for smoke tests, and expect
reasoning-token counts only in streamed usage (§3).high and max are accepted here and were a
hard 400 on the previous model (§3). Stick to low / medium / xhigh if a config must survive the next
change, and re-read §3 whenever the model behind this endpoint changes.output_tokens (§3). carriedmessage_start twice and content_block_start/content_block_stop twice around a
single delta. This is an upstream router defect. Real SDKs and Claude Code cope; a strict hand-rolled
parser that asserts one message_start will not.guided_choice is a no-op (§4) carried — it fails open instead of erroring.tool_choice used to report finish_reason: "stop". On the previous
model, with tool_choice:{type:"function",function:{name:…}}, the tool call came back correct but the
finish reason was stop. Measured on this deployment, the same request returns
finish_reason: "tool_calls". Branch on the presence of tool_calls in the message
rather than on the finish reason anyway: it is the form that works on both.Claude Code, opencode, pi and Hermes were each run end-to-end against this endpoint through a request-capturing proxy on 2026-09-05, so the wire shapes below are observed rather than inferred — but they were captured against the previous model. The transport is unchanged, so the configs stay valid; what changed is the effort vocabulary, and each config below is already corrected for it. OpenClaw is verified against its pinned schema and a live production config, and was not executed here.
{
"env": {
"ANTHROPIC_BASE_URL": "https://aipassport.trirat.co",
"ANTHROPIC_AUTH_TOKEN": "sk-YOUR_KEY",
"ANTHROPIC_MODEL": "qwen3.8-flash-next",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3.8-flash-next",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3.8-flash-next",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3.8-flash-next",
"ANTHROPIC_CUSTOM_MODEL_OPTION": "qwen3.8-flash-next",
"CLAUDE_CODE_MAX_CONTEXT_TOKENS": "261120",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "32768",
"CLAUDE_CODE_EFFORT_LEVEL": "medium",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
/v1 — Claude Code appends it. Captured: POST /v1/messages?beta=true with 7 anthropic-beta flags, thinking:{type:adaptive}, output_config:{effort}, context_management, cached system blocks and 26 tools. All accepted.CLAUDE_CODE_EFFORT_LEVEL reaches the wire as output_config.effort but this endpoint drops it (§3) — you get the lane default. Thinking cannot be turned off from Claude Code.CLAUDE_CODE_MAX_CONTEXT_TOKENS, an unknown model id is compacted at Claude Code's assumed window instead of 262,144.ANTHROPIC_CUSTOM_MODEL_OPTION puts the id in the /model picker, whose discovery otherwise filters ids without "claude" or "anthropic" in the name.{
"$schema": "https://opencode.ai/config.json",
"model": "aipassport/qwen3.8-flash-next",
"small_model": "aipassport/qwen3.8-flash-next",
"provider": {
"aipassport": {
"npm": "@ai-sdk/openai-compatible",
"name": "AI Passport",
"options": {
"baseURL": "https://aipassport.trirat.co/v1",
"apiKey": "{env:AI_PASSPORT_API_KEY}"
},
"models": {
"qwen3.8-flash-next": {
"name": "Qwen3.8-Flash-Next (AI Passport)",
"attachment": true, "reasoning": true, "tool_call": true,
"limit": { "context": 261120, "output": 32768 },
"modalities": { "input": ["text", "image"], "output": ["text"] },
"options": { "reasoningEffort": "medium" },
"variants": {
"off": { "reasoningEffort": "medium",
"chat_template_kwargs": { "enable_thinking": false } },
"low": { "reasoningEffort": "low" },
"medium": { "reasoningEffort": "medium" },
"high": { "disabled": true },
"xhigh": { "reasoningEffort": "xhigh" }
}
}
}
}
}
}
reasoning_effort: "medium", stream: true, 10 tools.chat_template_kwargs is not in the AI SDK's known option schema, so it is copied straight into the request — observed on the wire. That is what makes the off variant possible: a schema-valid reasoningEffort plus enable_thinking:false, which wins.reasoningEffort: "off" — it serializes to an invalid wire value. Keep the generated high variant disabled: opencode auto-creates low/medium/high for any reasoning model; high is accepted on this deployment but was a 400 on the previous one.chat_template_kwargs into the model's own options — verified to appear on every request.{
"providers": {
"aipassport": {
"baseUrl": "https://aipassport.trirat.co/v1",
"api": "openai-completions",
"apiKey": "$AIPASSPORT_API_KEY",
"models": [{
"id": "qwen3.8-flash-next",
"name": "AI Passport - Qwen3.8-Flash-Next",
"reasoning": true,
"thinkingLevelMap": {
"off": "off", "minimal": null, "low": "low", "medium": "medium",
"high": null, "xhigh": "xhigh", "max": null
},
"input": ["text", "image"],
"contextWindow": 261120,
"maxTokens": 32768,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"compat": {
"thinkingFormat": "chat-template",
"supportsReasoningEffort": true,
"supportsUsageInStreaming": true,
"supportsDeveloperRole": false,
"chatTemplateKwargs": {
"enable_thinking": { "$var": "thinking.enabled" },
"reasoning_effort": { "$var": "thinking.effort" }
}
}
}]
}
}
}
# then: pi --provider aipassport --model qwen3.8-flash-next --thinking medium
thinkingFormat: "chat-template" routes effort inside chat_template_kwargs, the portable place for this endpoint's off switch — so pi is the harness best placed to expose an off level.medium → {enable_thinking:true, reasoning_effort:"medium"}, likewise xhigh, and off → {enable_thinking:false, reasoning_effort:"off"}. No top-level reasoning_effort is sent. carriedoff works natively — the exact shape pi emits, {enable_thinking:false, reasoning_effort:"off"}, returned 200 with no reasoning on this deployment (2026-10-12) and on the previous one. pi is the harness best placed to expose an off level, because thinkingFormat routes effort inside the kwargs.high, minimal and max are mapped to null so pi only offers levels that are valid on this and the previous model.supportsDeveloperRole: false: the developer role is accepted on this deployment (measured) but was not on the previous one.# ~/.hermes/.env (%LOCALAPPDATA%\hermes\.env on native Windows)
AIPASSPORT_API_KEY=sk-YOUR_KEY
# ~/.hermes/config.yaml
providers:
ai-passport:
api: https://aipassport.trirat.co/v1
key_env: AIPASSPORT_API_KEY
transport: chat_completions
default_model: qwen3.8-flash-next
extra_body:
reasoning_effort: medium
model:
provider: custom:ai-passport
default: qwen3.8-flash-next
context_length: 261120
max_tokens: 32768
supports_vision: true
reasoning_effort: "medium" from extra_body, max_tokens: 32768, stream: true, 27 tools.extra_body becomes top-level request fields, which is exactly what this endpoint wants — Hermes's generic reasoning path would otherwise send a reasoning:{enabled,effort} object that this endpoint ignores.reasoning_effort: "none" turns thinking off on this deployment, but the portable off switch is extra_body: {chat_template_kwargs: {enable_thinking: false}}.transport: chat_completions; Hermes also speaks Anthropic and Responses, and auto-detection is only a fallback. Keep the key in .env via key_env.{
models: {
mode: "merge",
providers: {
aipassport: {
baseUrl: "https://aipassport.trirat.co/v1",
apiKey: "${AI_PASSPORT_API_KEY}",
api: "openai-completions",
models: [{
id: "qwen3.8-flash-next",
name: "Qwen3.8-Flash-Next (AI Passport)",
reasoning: true,
input: ["text", "image"],
contextWindow: 261120,
maxTokens: 32768,
thinkingLevelMap: {
off: null, minimal: null, low: "low", medium: "medium",
high: null, xhigh: "xhigh", max: null,
},
compat: {
supportsReasoningEffort: true,
supportedReasoningEfforts: ["low","medium","xhigh"],
},
}],
},
},
},
agents: {
defaults: {
model: { primary: "aipassport/qwen3.8-flash-next", fallbacks: [] },
thinkingDefault: "medium",
},
},
}
// then: openclaw config validate (strict config — an unknown key blocks startup)
supportsReasoningEffort: true is load-bearing: OpenClaw defaults it off for an explicit proxy-style URL, so without it no effort is sent at all.off is mapped to null on purpose — off: "off" would emit top-level reasoning_effort:"off", which this deployment accepts but the previous model rejected. OpenClaw's only body hatch is static (it applies to every request for that model ref), so it cannot serve as a dynamic off while keeping the live levels. /think low is the portable minimum; an always-thinking-off deployment is a separate model entry using that static hatch.provider/model. A fallback chain is model:{primary, fallbacks:[…]}, ordered — but the legacy id qwen3.8-27b reaches the same model, so it is no fallback; a real fallback has to be a different provider.curl is not evidence the agent path works — exercise a real agent turn with tools after openclaw config validate.This is a home lab. The serving contract above is proven for the model deployed on 2026-10-12; rows marked carried were measured on 2026-09-05 against the previous model over the same path and are being re-verified. If anything here disagrees with what you observe, tell me — that is a bug in the docs or the endpoint, and I want to know which.