Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 41 additions & 31 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
PORT=9766
HOST=0.0.0.0
DEFAULT_PROVIDER=kimi
DEFAULT_MODEL=kimi-k2.5
AUTH_PATH=./auth.json
# Preferred for current GLM web app (chat.z.ai)
GLM_TOKEN=
# Legacy China ChatGLM backend (chatglm.cn); set GLM_BACKEND=chatglm to use this
GLM_REFRESH_TOKEN=
GLM_BACKEND=zai
# Optional: full Cookie header copied from a successful chat.z.ai browser completion request (keeps cdn/acw/ssxmod anti-bot cookies)
ZAI_COOKIE=
# Optional: captcha_verify_param copied from Z.ai browser completion request body. It is usually a base64 string, not JSON.
ZAI_CAPTCHA_VERIFY_PARAM=
# Optional browser fingerprint overrides for Z.ai request query/headers
ZAI_USER_AGENT=
ZAI_ACCEPT_LANGUAGE=en-US
ZAI_LANGUAGE=ru-RU
ZAI_LANGUAGES=ru-RU,ru,en-US,en
ZAI_TIMEZONE=Europe/Samara
ZAI_TIMEZONE_OFFSET=-240
KIMI_TOKEN=
# Set to 1 for local tests without real GLM/Kimi credentials
MOCK_PROVIDER=0
# Optional local keys accepted by proxy; empty disables auth check
API_KEYS=
# Admin / account-pool behavior
# 1 = POST/DELETE /admin/accounts persists auth.json by default; use persist:false per request to keep runtime-only
PERSIST_ADMIN_ACCOUNTS=1
# Milliseconds to skip an account after provider error before retrying it
ACCOUNT_COOLDOWN_MS=60000
PORT=9766
HOST=0.0.0.0
DEFAULT_PROVIDER=kimi
DEFAULT_MODEL=kimi-k2.5
AUTH_PATH=./auth.json
# Preferred for current GLM web app (chat.z.ai)
GLM_TOKEN=
# Legacy China ChatGLM backend (chatglm.cn); set GLM_BACKEND=chatglm to use this
GLM_REFRESH_TOKEN=
GLM_BACKEND=zai
# Optional: full Cookie header copied from a successful chat.z.ai browser completion request (keeps cdn/acw/ssxmod anti-bot cookies)
ZAI_COOKIE=
# Optional: captcha_verify_param copied from Z.ai browser completion request body. It is usually a base64 string, not JSON.
ZAI_CAPTCHA_VERIFY_PARAM=
# Optional browser fingerprint overrides for Z.ai request query/headers
ZAI_USER_AGENT=
ZAI_ACCEPT_LANGUAGE=en-US
ZAI_LANGUAGE=ru-RU
ZAI_LANGUAGES=ru-RU,ru,en-US,en
ZAI_TIMEZONE=Europe/Samara
ZAI_TIMEZONE_OFFSET=-240
KIMI_TOKEN=
# Debugging REASON_CHAT_MESSAGE_NOT_FOUND on Kimi turn 2+: www.kimi.com's
# Connect-RPC Chat endpoint likely needs session/device identity headers we're
# not confirmed to be sending correctly. kimi.js best-effort-derives some from
# the JWT; KIMI_EXTRA_HEADERS (JSON) always overrides those guesses. Capture
# the real ones with `npm run kimi:dump-headers` (see script header for steps).
KIMI_LANGUAGE=en-US
KIMI_TIMEZONE=America/Los_Angeles
KIMI_EXTRA_HEADERS=
# Set to 1 to log every outgoing Kimi request headers + chat_id/parent_id
DEBUG_KIMI=0
# Set to 1 for local tests without real GLM/Kimi credentials
MOCK_PROVIDER=0
# Optional local keys accepted by proxy; empty disables auth check
API_KEYS=
# Admin / account-pool behavior
# 1 = POST/DELETE /admin/accounts persists auth.json by default; use persist:false per request to keep runtime-only
PERSIST_ADMIN_ACCOUNTS=1
# Milliseconds to skip an account after provider error before retrying it
ACCOUNT_COOLDOWN_MS=60000
59 changes: 59 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# FreeGLMKimiAPI — https://github.com/ForgetMeAI/FreeGLMKimiAPI
FROM node:22-bookworm-slim

# The proxy drives a real browser for the Z.ai/GLM browser-fallback login flow
# and anti-bot handling (playwright-core, puppeteer-core, puppeteer-extra +
# stealth plugin, cloakbrowser). None of these packages bundle their own
# Chromium build ("-core" packages never auto-download), so we install one
# system-wide and point every relevant env var at it.
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium \
ca-certificates \
fonts-liberation \
fonts-noto-color-emoji \
tini \
&& rm -rf /var/lib/apt/lists/*

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
CHROME_PATH=/usr/bin/chromium

WORKDIR /app

# Install deps once at build time so the image works standalone even if you
# don't mount a persistent node_modules volume. NOT using --omit=dev here:
# this repo's browser-fallback packages (puppeteer-extra, stealth plugin,
# cloakbrowser) are needed at runtime for the Z.ai/Kimi fallback path even
# though some are listed under devDependencies upstream.
COPY package.json package-lock.json* ./
RUN npm ci || npm install

# Now bring in the rest of the source
COPY . .

# auth.json, .env overrides, and the persistent Z.ai browser profile all live
# under /app/data so they survive image rebuilds via a bind/volume mount.
RUN mkdir -p /app/data

# If /app/node_modules is bind-mounted from the host (recommended — see
# docker-compose.yml), this entrypoint skips `npm ci` on every container
# restart/NAS reboot and only reinstalls when package.json/lock/Node/arch
# actually changed. First boot after mounting an empty volume still installs
# once and writes the sentinel.
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

ENV NODE_ENV=production \
PORT=3364 \
HOST=0.0.0.0 \
AUTH_PATH=/app/data/auth.json \
ZAI_BROWSER_PROFILE_DIR=/app/data/zai-browser-profile

EXPOSE 3364

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+ (process.env.PORT||3364) +'/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"

ENTRYPOINT ["tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
CMD ["node", "src/server.js"]
14 changes: 14 additions & 0 deletions commands.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

sudo docker compose build --no-cache
sudo docker compose up -d

curl http://192.168.31.66:3364/health

curl -X POST http://192.168.31.66:3364/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k2.5",
"messages": [{"role": "user", "content": "hello"}],
"stream": false
}'

51 changes: 51 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
services:
freeglmkimi:
build:
context: ./repo # cloned ForgetMeAI/FreeGLMKimiAPI; Dockerfile + docker-entrypoint.sh live here too
dockerfile: Dockerfile
container_name: freeglmkimi
restart: unless-stopped

# Headless Chromium (Z.ai browser-fallback login / anti-bot handling) needs
# more than the Docker default 64mb of /dev/shm or it will randomly crash.
shm_size: "1gb"

ports:
- "3364:3364"

environment:
PORT: 3364
HOST: 0.0.0.0
DEFAULT_PROVIDER: kimi
DEFAULT_MODEL: kimi-k2.5
GLM_BACKEND: zai
AUTH_PATH: /app/data/auth.json
MOCK_PROVIDER: 0
PERSIST_ADMIN_ACCOUNTS: 1
ACCOUNT_COOLDOWN_MS: 60000
# Single-account shortcut (skip if you're using auth.json instead):
# GLM_TOKEN: ""
# KIMI_TOKEN: ""
# Protect the proxy if you expose it beyond localhost/LAN:
# API_KEYS: ""
# Debugging REASON_CHAT_MESSAGE_NOT_FOUND on Kimi turn 2+ (see .env.example
# and src/providers/kimi.js for context). Capture real values with
# `npm run kimi:dump-headers` before setting KIMI_EXTRA_HEADERS.
# KIMI_EXTRA_HEADERS: '{}'
# DEBUG_KIMI: 1

# Optional: keep real tokens out of this file entirely and drop them in a
# .env next to this compose file instead (same keys as .env.example).
# env_file:
# - .env

volumes:
# Persists auth.json, the admin-added account pool, and the Z.ai browser
# profile across container rebuilds/restarts.
- /volume1/docker/freeglmkimi/data:/app/data
# Persists node_modules across restarts/NAS reboots. The entrypoint's
# sentinel check means `npm ci` only reruns when package.json/lock (or
# Node version/arch) actually changes — a plain restart or reboot skips
# straight to `node src/server.js`. First start after creating this
# volume will still install once, as normal.
- /volume1/docker/freeglmkimi/node_modules:/app/node_modules
28 changes: 28 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/sh
# Speeds up container restarts/NAS reboots by skipping `npm ci` when the
# bind-mounted /app/node_modules already matches the current package.json /
# package-lock.json / Node version / arch / *this script*. Only reinstalls
# when one of those actually changed. Including this script's own contents
# in the hash means editing the install logic here (e.g. adding/removing
# --omit=dev) automatically invalidates old sentinels — no manual `rm`
# needed. Mirrors the sentinel-file pattern used for Hermes' package
# persistence.
set -e

SENTINEL="/app/node_modules/.install-sentinel"
SELF="$0"

CURRENT_HASH=$(cat package.json package-lock.json "$SELF" 2>/dev/null | \
{ command -v sha256sum >/dev/null 2>&1 && sha256sum || md5sum; } | \
awk '{print $1}')
CURRENT_HASH="${CURRENT_HASH}-node$(node --version)-$(uname -m)"

if [ -f "$SENTINEL" ] && [ "$(cat "$SENTINEL")" = "$CURRENT_HASH" ]; then
echo "[entrypoint] node_modules matches sentinel — skipping npm ci"
else
echo "[entrypoint] Installing dependencies (first run, or package.json/lock/Node/arch/entrypoint changed)..."
npm ci
echo "$CURRENT_HASH" > "$SENTINEL"
fi

exec "$@"
75 changes: 75 additions & 0 deletions docs/KIMI_troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# FreeGLMKimiAPI: REASON_CHAT_MESSAGE_NOT_FOUND fix attempt

## Diagnosis

`src/providers/kimi.js` targets `www.kimi.com`'s international Connect-RPC
surface (`/apiv2/kimi.gateway.chat.v1.ChatService/Chat`), but only ever sent
one custom header: `X-Msh-Platform: web`. A `jwtPayload()` helper existed in
the file to decode the account's JWT but was never actually used anywhere —
dead code that looks like an unfinished attempt to derive session/device
identity headers.

That matches how this API behaves in the wild: the international Kimi web
surface is known (from other reverse-engineering projects hitting the same
endpoint) to send additional `x-msh-*` / `x-traffic-id` / `x-language` /
`r-timezone` headers alongside the JWT, with the JWT payload itself carrying
`device_id`, `region`, and a subject/session id that likely need to be
echoed back in headers. Your Kimi JWT (`data/auth.json`) does contain
`device_id` and `region: "overseas"` fields.

Working theory: `chat_id`/`parent_id` are scoped server-side to a specific
device/session identity. Without the matching identity headers, the server
can create/continue the RPC call but can't resolve the chat message under an
unrecognized identity context — hence `REASON_CHAT_MESSAGE_NOT_FOUND`.

**This is a well-supported hypothesis, not a confirmed fix** — the exact
header names are unverified even in the public reverse-engineering notes I
found. Ship it, then use the capture script below to nail down the real
names if it's still failing.

## What changed

`src/providers/kimi.js`:
- Wires the previously-unused `jwtPayload()` into the request: derives
`X-Msh-Device-Id`, `X-Msh-Region`, `X-Traffic-Id` from the JWT's
`device_id` / `region` / `sub` fields, plus static `X-Language` /
`R-Timezone` headers (env-overridable via `KIMI_LANGUAGE` /
`KIMI_TIMEZONE`).
- Adds `KIMI_EXTRA_HEADERS` (JSON env var) that always overrides the guessed
headers above — this is the important one if the guessed header names turn
out to be wrong.
- Adds `DEBUG_KIMI=1` to log every outgoing request's headers + chat_id/
parent_id to `docker logs`, so you can diff against a real browser capture.
- Minor hardening: `parentId` now takes the *last* non-empty message id seen
across response frames instead of the first, in case an early frame echoes
the user's own turn before a later frame carries the assistant reply's id
(this shouldn't be your main issue, since the errors happen consistently,
but it's a plausible secondary contributor and is free to fix).

`scripts/kimi_dump_curl_headers.js` (new): paste a "Copy as cURL" of a real
`www.kimi.com` **second-message** Chat request from Chrome DevTools, and it
prints every header plus a ready-to-paste `KIMI_EXTRA_HEADERS=` line for any
header we're not already sending/guessing.

`package.json`: adds `npm run kimi:dump-headers` alias for the script above.

`.env.example` / `docker-compose.yml`: document the new env vars
(`KIMI_LANGUAGE`, `KIMI_TIMEZONE`, `KIMI_EXTRA_HEADERS`, `DEBUG_KIMI`).

## How to apply

1. Copy `repo/src/providers/kimi.js` and `repo/scripts/kimi_dump_curl_headers.js`
into your `freeglmkimi/repo/` tree, and merge the `package.json` /
`.env.example` / `docker-compose.yml` diffs (or just replace them — they're
additive).
2. `sudo docker compose build --no-cache && sudo docker compose up -d`
3. Send a 2-message conversation through the proxy and watch:
`sudo docker logs freeglmkimi -f --tail 30`
4. Still failing? Set `DEBUG_KIMI=1` in `docker-compose.yml`, restart, and
compare the logged headers against a real browser capture:
- Open www.kimi.com logged in, DevTools > Network, send 2 messages in one
chat, right-click the 2nd `.../ChatService/Chat` request > Copy > Copy as
cURL (bash).
- `npm run kimi:dump-headers path/to/pasted-curl.txt` (or pipe it in).
- Drop any header it flags as missing into `KIMI_EXTRA_HEADERS` (JSON) in
`.env` or `docker-compose.yml`, restart, retest.
78 changes: 40 additions & 38 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
{
"name": "free-glm-kimi-api",
"version": "0.1.0",
"description": "OpenAI/Anthropic-compatible free web API proxy for GLM and Kimi with prompt-simulated tool use.",
"type": "module",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"test": "node --test tests/*.test.js",
"smoke": "node scripts/smoke.js",
"agent:hermes": "node scripts/agent_smoke.js hermes",
"agent:claude": "node scripts/agent_smoke.js claude",
"agent:opencode": "node scripts/agent_smoke.js opencode",
"agent:openclaw": "node scripts/agent_smoke.js openclaw",
"agent:all": "node scripts/agent_smoke.js all",
"doctor": "node scripts/doctor.js",
"smoke:zai": "node scripts/zai_real_smoke.js",
"auth:from-curl": "node scripts/import_zai_curl.js",
"auth:browser": "node scripts/zai_browser_auth.js"
},
"keywords": [
"glm",
"kimi",
"openai",
"anthropic",
"tool-calling",
"proxy"
],
"license": "MIT",
"dependencies": {
"cloakbrowser": "^0.3.31",
"eventsource-parser": "^3.0.6",
"playwright-core": "^1.60.0",
"puppeteer-core": "^25.1.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
}
}
{
"name": "free-glm-kimi-api",
"version": "0.1.0",
"description": "OpenAI/Anthropic-compatible free web API proxy for GLM and Kimi with prompt-simulated tool use.",
"type": "module",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"test": "node --test tests/*.test.js",
"smoke": "node scripts/smoke.js",
"agent:hermes": "node scripts/agent_smoke.js hermes",
"agent:claude": "node scripts/agent_smoke.js claude",
"agent:opencode": "node scripts/agent_smoke.js opencode",
"agent:openclaw": "node scripts/agent_smoke.js openclaw",
"agent:all": "node scripts/agent_smoke.js all",
"doctor": "node scripts/doctor.js",
"smoke:zai": "node scripts/zai_real_smoke.js",
"auth:from-curl": "node scripts/import_zai_curl.js",
"auth:browser": "node scripts/zai_browser_auth.js",
"kimi:dump-headers": "node scripts/kimi_dump_curl_headers.js"
},
"keywords": [
"glm",
"kimi",
"openai",
"anthropic",
"tool-calling",
"proxy"
],
"license": "MIT",
"dependencies": {
"cloakbrowser": "^0.3.31",
"eventsource-parser": "^3.0.6",
"playwright-core": "^1.60.0",
"puppeteer": "^25.3.0",
"puppeteer-core": "^25.1.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
}
}
Loading