The advanced spec for OpenThai 2.0.1 · Qwen3.8-27B: 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-27b. Precision INT8 W8A8, speculative decoding
DFlash2 + KVarN. Keys carry no requests-per-minute, tokens-per-minute or concurrency limit —
and they are scoped to that single id, so calling any other name returns an auth-scope error rather
than a 404.
Every row in this table is a previous-model measurement. Endpoint availability is a property of the router lane rather than the weights, so it is expected to survive the model change — but none of it has 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 |
POST /v1/completions | 200 | legacy text completion |
GET /health | 200 | liveness, no auth — portal process, not the model |
/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 |
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. carried On the previous model max_tokens: 8
spent all eight tokens thinking and returned finish_reason: "length" with content: "".
Give thinking-on requests 2–4k output regardless.reasoning_effort cannot express off.
The only off switch is chat_template_kwargs.| Value | Why | |
|---|---|---|
low medium xhigh | 200 | what this model's chat template accepts |
high | 400 | rejected by this model — use xhigh for the deepest tier |
off | n/a | not an effort value here — see below |
⚠️ These are model-specific and they have inverted before. The endpoint's previous
model accepted high and rejected off; this one is the reverse on high. If you
are reusing a config written against the old lane, re-check this table rather than assuming — a wrong
value is a hard 400, not a silent downgrade. The exact set of values the request schema rejects
ahead of the template is being re-measured and will be published when it is.
curl https://aipassport.trirat.co/v1/chat/completions \
-H "Authorization: Bearer sk-YOUR_KEY" -H "Content-Type: application/json" \
-d '{"model":"qwen3.8-27b",
"chat_template_kwargs":{"enable_thinking":false},
"messages":[{"role":"user","content":"say ok"}]}'
enable_thinking: false is the off switch — reasoning_effort
cannot express it. 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 · direct chat | choices[0].message.reasoning, non-empty under thinking-on — measured on this deployment; reasoning_content absent | breakdown unreported; usage.completion_tokens is the total |
| Anthropic carried | discarded — there is no thinking content block | still counted in output_tokens |
Only the direct chat-completions shape above was observed on this deployment. The
streaming usage chunk, /v1/responses and the Anthropic translation remain carried — those
shapes have not been re-checked.
What the three observed runs looked like, at a deliberately small
max_tokens: 128: low 279 characters of reasoning, finish_reason: "length",
128 completion tokens · medium 360 characters, length, 128 · xhigh 234
characters, stop, 97. Two things not to read into that: those are characters, not
tokens (the token breakdown is unreported), and low and medium were
truncated at the cap — they are not completed answers, so nothing here compares effort levels
for depth or quality.
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 presence_penalty frequency_penalty repetition_penalty seed stop n logprobs top_logprobs max_tokens max_completion_tokens stream stream_options carried | lane defaults: temperature 1.0, top_p 0.95, presence_penalty 0.0. stream_options:{include_usage:true} previously returned a final usage chunk carrying reasoning tokens — see the notice in §3 |
| 400 | min_p logit_bias | not supported with speculative decoding. This deployment runs DFlash2 + KVarN: both return 400 "The min_p and logit_bias sampling parameters are not yet supported with speculative decoding." The two differ in how they present, which matters if you are testing: logit_bias was rejected even at temperature 0, while min_p at temperature 0 returned 200 and only 400s at nonzero temperature — greedy sampling makes min_p inert, so that 200 was never support. Remove both from client plumbing rather than catching the error |
| works | response_format {type:"json_object"} response_format {type:"json_schema", strict:true} guided_regex carried | structured output constrains the answer; thinking still precedes it |
| works | tools tool_choice (auto · required · named) parallel_tool_calls | the tool call itself is correct in every form tested — but a named forced choice mislabels its finish reason, see trap below |
| works | image_url (data URL or https) | max 16 per prompt · resized to ≤ 1 Mpx longest edge |
| ignored | guided_choice carried | accepted and not enforced — it fails open to free prose. Use guided_regex or a JSON schema |
| 400 | reasoning_effort outside low / medium / xhigh | see §3 |
| Limit | Value | How it fails |
|---|---|---|
| Context window | 262,144 tokens, prompt + max_tokens combined | 400 ContextWindowExceededError |
| Declared max input | 228,352 tokens | 1,024 below window − max output on purpose: the chat template adds tokens your tokenizer cannot see. A 228,338-token prompt with thinking on and a full 32,768 reservation is 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. Single needle probes at midpoint depth passed at 36k / 91k / 180k / 229k and failed at 146k and 261k — recall at depth is inconsistent at one sample per point, and no reliable threshold has been established. Budget accordingly for retrieval-style prompts |
| Public-path depth ceiling | ≈ 150,000 tokens over aipassport.trirat.co | prefill is silent, and Cloudflare cuts at ~125 s. Measured time-to-first-byte: 29.9 s @36k, 73.1 s @91k, 118.6 s @146k (≈1,220 tok/s) — so a prompt much past ~150k returns 524 even when streaming. The full 228,352 is 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 | 16 | 400 At most 16 image(s) may be provided in one prompt |
| Image size | ≤ 1 Mpx longest edge | resized server-side, no error |
| Concurrency | 3 resident sequences | excess queues, it does not 429 carried. KVarN can cap the served count below the launch flag — the boot log is authoritative |
| 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 |
Prefix caching is on server-side. carried On the previous model usage did not report cached tokens, so cache hits could not be verified from the response.
| 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 |
| 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 400 and 429 shapes quoted here are previous-model observations; the status codes and their meanings are router behaviour and are expected to be identical. carried
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. There is now one model id, so
unlike the previous deployment there is no sibling id to retry against — wait the cooldown out.max_tokens: 8 returned an empty string (§3). Disable thinking for smoke tests either way —
and note that a probe expecting reasoning-token fields will find them absent on this build (§3).high is a hard 400 here (§3) — and it worked on the previous model. Effort
vocabularies are model-specific; 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 reports finish_reason: "stop". Observed on
this deployment, not carried: with tool_choice:{type:"function",function:{name:…}} the
tool call comes back correct — right tool, right arguments, including Thai argument values —
but the finish reason is stop; the identical request under tool_choice:"auto" returns
the same payload with tool_calls. It is a finish-label quirk on the named-forced path, not an
agentic failure. Branch on the presence of tool_calls in the message, not on the finish
reason — a loop that tests finish_reason == "tool_calls" will silently treat a successful
forced call as a plain text turn. Only the named-function form was probed, so do not generalise this
to every forced variant.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-27b",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3.8-27b",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3.8-27b",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3.8-27b",
"ANTHROPIC_CUSTOM_MODEL_OPTION": "qwen3.8-27b",
"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-27b",
"small_model": "aipassport/qwen3.8-27b",
"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-27b": {
"name": "OpenThai 2.0.1 · Qwen3.8-27B (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. The generated high variant must stay disabled: opencode auto-creates low/medium/high for any reasoning model, and high is a 400 on this 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-27b",
"name": "AI Passport - OpenThai 2.0.1 · Qwen3.8-27B",
"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-27b --thinking medium
thinkingFormat: "chat-template" routes effort inside chat_template_kwargs, which is the only place this endpoint's off switch lives — 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 — verified on this deployment: the exact shape pi emits, {enable_thinking:false, reasoning_effort:"off"}, returns 200 with empty reasoning. pi is the only harness here with a real off level, because thinkingFormat routes effort inside the kwargs where the template skips effort validation once thinking is disabled.high, minimal and max are mapped to null so pi never offers a level this template rejects.supportsDeveloperRole: false — vLLM has no developer role.# ~/.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-27b
extra_body:
reasoning_effort: medium
model:
provider: custom:ai-passport
default: qwen3.8-27b
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.off — to disable thinking use 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-27b",
name: "OpenThai 2.0.1 · Qwen3.8-27B (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-27b", 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 is not an effort value on this endpoint. 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 minimum here; 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 this endpoint now serves a single model id, so there is no sibling id to list; 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-09-06; 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.