Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
## 未发布

- Anthropic 流式 `event: error` 经 `mapUpstreamError` 重映射,Usage Policy 与 non-stream 一致为 `policy_blocked` / `refusal_guard`。(#503)
- 修复网关流路径忽略 `KIN_STREAM_IDLE_TIMEOUT`:与面板 `failover.stream_idle_timeout_ms`、`kernel.json` 同步走同一级联(面板 → cfg.limits → 环境变量 → 默认 180 秒),默认仍为 180 秒(#485)。

## 1.3.182 — 2026-10-17

Expand Down
2 changes: 2 additions & 0 deletions docs/PANEL_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ Claude PATCH `max_sessions` 返回 400 `claude_max_sessions_forbidden`;OpenAI

`PUT` 热更新。`tiers` 必须回传:该块是整体替换而非 patch,缺字段即置空。Claude 分档保存只传播到未手动覆盖的 Claude 槽。OpenAI `codex.quota` 按字段合并,不覆盖其它 Codex 协议/客户端配置,只更新跟随的 OpenAI 限额。

`failover.stream_idle_timeout_ms`(毫秒,30 秒–60 分钟,默认 180 秒):上游流连续无事件即判超时;网关流路径、各槽 `kernel.json` 的 `idle_timeout_seconds` 与 job 看门狗共用。优先级:面板该字段 → 启动时写入 `cfg.limits.stream_idle_timeout_ms` → 控制面 `.env` 的 `KIN_STREAM_IDLE_TIMEOUT` → 180 秒。保存后热写各槽 `kernel.json`;运行中 kernel 在无任务时自动重启以读取新看门狗值。

`compatibility.persona_preset`(`official` / `official_full` / `zero` / `custom`)和 `compatibility.cache_ttl`(`5m` / `1h`)保存后投影到每个 Claude 槽的 `vms/<id>/run/kernel.json`:`persona_preset`、`system_layout`(`zero`→`zero`,其余→`identity`)、`default_cache_ttl`。响应 `kernel_persona.updated` 是本次字节有变化的槽数。`PATCH /vms/:id` 的 `timezone` / `timezone_follow_proxy` 另把 `timezone` 热写进该槽 `kernel.json`,`timezone_sync.kernel_hot` 表示文件有变化。容器 `TZ` 不在这次写入里。Codex 槽不写。

`compatibility.agent_standing`(字符串,≤2000)与四个按档布尔 map `agent_standing_presets` / `agent_standing_hide_presets` / `persona_env_presets` / `persona_hide_presets` 控制常驻约束、约束遮罩、Environment 和整档 usage 遮罩,不投影到 `kernel.json`,Node 每次请求热读。`GET /api/panel/persona/preview-vars?timezone=<IANA>` 返回 system提示词页预览用的真实模板常量(身份句、官方 agent 全文、按该时区渲染的 Environment),不含 billing;时区非法或缺省按 UTC。
Expand Down
8 changes: 8 additions & 0 deletions src/lib/core/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export function streamIdleTimeoutMs(routing, fallbackMs = Number(process.env.KIN
return Math.min(STREAM_IDLE_TIMEOUT_MAX_MS, Math.max(STREAM_IDLE_TIMEOUT_MIN_MS, Math.round(ms)))
}

/** Gateway stream path: panel routing → cfg.limits → KIN_STREAM_IDLE_TIMEOUT → 180s default. */
export function resolveStreamIdleTimeoutMs(routing, cfg) {
const limitsMs = Number(cfg?.limits?.stream_idle_timeout_ms)
const fallback =
Number.isFinite(limitsMs) && limitsMs > 0 ? limitsMs : Number(process.env.KIN_STREAM_IDLE_TIMEOUT) || 180_000
return streamIdleTimeoutMs(routing, fallback)
}

/**
* How long a Claude stream may sit in the queue before the first downstream
* byte. Cloudflare returns 524 when the origin writes nothing for ~100s.
Expand Down
11 changes: 2 additions & 9 deletions src/lib/protocol/handle-protocol.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
* server wiring; this factory owns convert → pool → Go/Rust hop → client.
*/
import { applyIntercept } from '../core/intercept.mjs'
import {
streamIdleTimeoutMs as idleTimeoutFromRouting,
inboundBodyLimit,
queueKeepaliveDelayMs,
} from '../core/config.mjs'
import { resolveStreamIdleTimeoutMs, inboundBodyLimit, queueKeepaliveDelayMs } from '../core/config.mjs'
import { applyEagerToolStreaming } from './eager-tool-streaming.mjs'
import {
inboundRefusalDeviceId,
Expand Down Expand Up @@ -1093,10 +1089,7 @@ export function createHandleProtocol(deps) {
// First downstream byte during a queue wait. Short misses stay HTTP 429/529;
// anything still waiting past this writes SSE so Cloudflare does not 524.
const queueKeepaliveDelay = queueKeepaliveDelayMs(getRouting(), cfg.limits?.queue_keepalive_delay_ms)
const streamIdleTimeoutMs = idleTimeoutFromRouting(
getRouting(),
Number(cfg.limits.stream_idle_timeout_ms) || 180_000,
)
const streamIdleTimeoutMs = resolveStreamIdleTimeoutMs(getRouting(), cfg)
const managedKey = req.apiKeyRecord || null
if (managedKey) {
const gate = apiKeyStore.acquire(managedKey)
Expand Down
29 changes: 29 additions & 0 deletions test/unit/routing-file.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
queueKeepaliveDelayMs,
readRoutingConfigFile,
routingConfigFile,
resolveStreamIdleTimeoutMs,
streamIdleTimeoutMs,
} from '../../src/lib/core/config.mjs'

Expand Down Expand Up @@ -84,6 +85,34 @@ test('streamIdleTimeoutMs prefers the panel value, then the caller fallback', ()
})
})

test('resolveStreamIdleTimeoutMs cascades panel, cfg.limits, env, then 180s default', () => {
withEnv('KIN_STREAM_IDLE_TIMEOUT', '', () => {
assert.equal(resolveStreamIdleTimeoutMs({ failover: {} }, { limits: {} }), 180_000)
assert.equal(
resolveStreamIdleTimeoutMs(
{ failover: { stream_idle_timeout_ms: 600_000 } },
{ limits: { stream_idle_timeout_ms: 2000 } },
),
600_000,
)
assert.equal(resolveStreamIdleTimeoutMs({ failover: {} }, { limits: { stream_idle_timeout_ms: 90_000 } }), 90_000)
})
withEnv('KIN_STREAM_IDLE_TIMEOUT', '420000', () => {
assert.equal(resolveStreamIdleTimeoutMs({ failover: {} }, { limits: {} }), 420_000)
assert.equal(
resolveStreamIdleTimeoutMs({ failover: {} }, { limits: { stream_idle_timeout_ms: Number.NaN } }),
420_000,
)
})
})

test('resolveStreamIdleTimeoutMs avoids hardcoded 180k when cfg.limits is unset but env is set', () => {
withEnv('KIN_STREAM_IDLE_TIMEOUT', '600000', () => {
const cfg = { limits: { max_body_bytes: 1 } }
assert.equal(resolveStreamIdleTimeoutMs({ failover: {} }, cfg), 600_000)
})
})

test('queueKeepaliveDelayMs stays under the Cloudflare window', () => {
withEnv('KIN_QUEUE_KEEPALIVE_DELAY', '', () => {
assert.equal(queueKeepaliveDelayMs(null), DEFAULT_QUEUE_KEEPALIVE_DELAY_MS)
Expand Down
2 changes: 1 addition & 1 deletion web/src/features/settings/pool-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ export function PoolPane({
<SliderSetting
id='failover-stream-idle'
label='流空闲超时'
desc='上游流连续无事件超过此时长即断开;大文件 Write 会被上游整段缓冲,过短会误断。保存后各槽 kernel 会在没有请求时自动重启以应用新值。'
desc='上游流连续无事件超过此时长即断开;大文件 Write 会被上游整段缓冲,过短会误断。未在面板设置时可用控制面环境变量 KIN_STREAM_IDLE_TIMEOUT(毫秒)。保存后各槽 kernel 会在没有请求时自动重启以应用新值。'
value={numOf(
failoverDraft,
'stream_idle_timeout_ms',
Expand Down
Loading