AI
AI Passport

API reference

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.

OpenAI + Anthropic wire262,144 ctx32,768 max outputthinking on · medium16 images/prompttools · JSON schemano rate limits

1 · Base URL and auth

Wire formatBase URLCredential
OpenAI
/chat/completions /completions /responses
https://aipassport.trirat.co/v1
with the /v1
Authorization: Bearer sk-…
Anthropic
/messages /messages/count_tokens
https://aipassport.trirat.co
no /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.

2 · Endpoints carried

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.

EndpointNotes
GET /v1/models200the ids your key may call
GET /v1/model/info200capability metadata — max_input_tokens, max_output_tokens, supports_reasoning, supports_vision
POST /v1/chat/completions200primary surface · streaming and non-streaming
POST /v1/messages200Anthropic Messages · tools and streaming
POST /v1/messages/count_tokens200token counting
POST /v1/responses200OpenAI Responses · function tools
POST /v1/completions200legacy text completion
GET /health200liveness, no auth — portal process, not the model
/v1/embeddings /v1/rerank
/v1/moderations /v1/audio/*
404not on this lane — one chat model only. Worse than useless: see trap 1
/v1/key/* /v1/user/*
/v1/spend/* /v1/team/*
404management plane is not exposed

3 · Thinking — read this one

The lane runs thinking ON at medium effort by default. Two consequences bite people immediately:

Accepted effort values

ValueWhy
low medium xhigh200what this model's chat template accepts
high400rejected by this model — use xhigh for the deepest tier
offn/anot 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.

Turning thinking off

no thinking
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.

Where the reasoning text goes

WireReasoning textCounted
OpenAI · direct chatchoices[0].message.reasoning, non-empty under thinking-on — measured on this deployment; reasoning_content absentbreakdown unreported; usage.completion_tokens is the total
Anthropic carrieddiscarded — there is no thinking content blockstill 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.

4 · Parameters

ParametersNotes
workstemperature top_p top_k presence_penalty frequency_penalty repetition_penalty seed stop n logprobs top_logprobs max_tokens max_completion_tokens stream stream_options carriedlane 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
400min_p logit_biasnot 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
worksresponse_format {type:"json_object"} response_format {type:"json_schema", strict:true} guided_regex carriedstructured output constrains the answer; thinking still precedes it
workstools tool_choice (auto · required · named) parallel_tool_callsthe tool call itself is correct in every form tested — but a named forced choice mislabels its finish reason, see trap below
worksimage_url (data URL or https)max 16 per prompt · resized to ≤ 1 Mpx longest edge
ignoredguided_choice carriedaccepted and not enforced — it fails open to free prose. Use guided_regex or a JSON schema
400reasoning_effort outside low / medium / xhighsee §3

5 · Limits

LimitValueHow it fails
Context window262,144 tokens, prompt + max_tokens combined400 ContextWindowExceededError
Declared max input228,352 tokens1,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 recallnot guaranteed near the windowthis 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 ceiling150,000 tokens over aipassport.trirat.coprefill 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 output32,768declare 32,768 in clients; the server enforces the window arithmetic above
Images per prompt16400 At most 16 image(s) may be provided in one prompt
Image size≤ 1 Mpx longest edgeresized server-side, no error
Concurrency3 resident sequencesexcess 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 silenceCloudflare 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.

6 · Errors

StatusMeaningWhat to do
400parameter or context-window rejection; the upstream message is preserved verbatimread the message — it names the field
401missing key, or a key not starting with sk-check the credential header for your wire format
429 carriedNo deployments available … Try again in 30 secondsa 30 s router cooldown, not a rate limit — see trap 1
503code: "model_offline" + retry-after: 1800the GPU box is mid-maintenance or training. Back off; it usually serves 07:00-18:00 ICT (Bangkok)
524Cloudflare silence timerstream 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

7 · Traps

  1. Calling an unsupported endpoint poisons the lane for 30 s. carried One 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.
  2. Thinking is on, so cheap probes look broken. carried On the previous model 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).
  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.
  4. Anthropic callers cannot steer thinking and never receive the reasoning text, though it is counted in output_tokens (§3). carried
  5. Anthropic streaming emits duplicate framing carried — a measured stream carried message_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.
  6. guided_choice is a no-op (§4) carried — it fails open instead of erroring.
  7. A named forced 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.

8 · Agent harness setup

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.

Claude Code

Anthropic /v1/messages ✅ run + wire captured
~/.claude/settings.json
{
  "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"
  }
}

opencode

OpenAI /v1/chat/completions ✅ run + wire captured
~/.config/opencode/opencode.json
{
  "$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" }
          }
        }
      }
    }
  }
}

pi

OpenAI /v1/chat/completions ✅ run + every level captured
~/.pi/agent/models.json
{
  "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

Hermes Agent

OpenAI /v1/chat/completions ✅ run + wire captured
~/.hermes/config.yaml + .env
# ~/.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

OpenClaw

OpenAI /v1/chat/completions ⚠ source-verified, not run
~/.openclaw/openclaw.json
{
  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)

Something wrong or missing?

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.