Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
6266b8f
fix(images): deliver tool_result images and stop the externalized-con…
Sep 7, 2026
a783f92
fix(images): harvest current-turn images from non-last messages
Sep 8, 2026
e3bd245
fix(openai): harvest current-turn media so a trailing text message ca…
Sep 8, 2026
5019f04
fix(openai): turn boundary is the last final answer, not any assistan…
Sep 8, 2026
526fd49
fix(anthropic): a single tool call no longer erases the image from th…
Sep 8, 2026
1c50b43
fix(openai): dedupe after folding, attach to null content, gate t2i, …
Sep 8, 2026
c6855d5
fix: surface unsupported Anthropic blocks; stop a truncated closer ea…
Sep 8, 2026
c3188d9
fix(agent-context): spend the context budget, and tell the client whe…
Sep 8, 2026
264d562
perf(images): reuse one upload across a turn instead of re-uploading …
Sep 8, 2026
46106c5
merge: image delivery works on both API paths, plus the context budge…
Sep 8, 2026
309da59
test(probes): add a live agent-loop protocol probe
Sep 8, 2026
a5ea559
fix(tool-protocol): number folded tool calls so results are addressable
Sep 8, 2026
46eca74
feat(agent-turn): give the model a ledger of what it already ran
Sep 8, 2026
7c97771
feat: inject the executed-call ledger on both API paths
Sep 8, 2026
e2eb41c
feat(agent-turn): seed the tool ledger from history and warn on repeats
Sep 8, 2026
127ae21
fix(anthropic): a truncated turn reports max_tokens even when a tool …
Sep 8, 2026
ee18c78
fix(anthropic): tool_use ids use the toolu_ prefix
Sep 8, 2026
4d87e70
fix(openai): strip tool-call residue from delivered text
Sep 8, 2026
62ba427
fix(anthropic): keep tool history readable when the request sends no …
Sep 8, 2026
6d9d15c
fix(anthropic): keep inbound thinking blocks in history
Sep 8, 2026
b479529
fix(tool-protocol): recognise the numbered closer the folded history …
Sep 9, 2026
0108eac
fix(agent-turn): the ledger can no longer forge a line or misattribut…
Sep 9, 2026
f2b5a6f
fix(agent-turn): the ledger tells the truth, identically on both paths
Sep 9, 2026
ab89650
fix(openai): a turn that is only protocol residue fails instead of de…
Sep 9, 2026
23c5b2c
fix(T8 repair): land the OpenAI twin and stop forged ordinals colliding
Sep 9, 2026
8a68433
fix(T9 repair): the delimiter stops being forgeable and the budget st…
Sep 9, 2026
4238d0c
chore(probes): drop the dead initializer eslint flags in the agent-lo…
Sep 9, 2026
f0407ce
test(probes): add the duplicate-onset replay harness
Sep 9, 2026
785486d
fix(probes): make the replay harness measure what it claims to measure
Sep 9, 2026
8e45452
test(probes): record that the replay design cannot reproduce the bug
Sep 9, 2026
03b0587
fix(images): stop the folded tool result claiming an image read retur…
Sep 9, 2026
5e62b00
test(probes): make the tool_result image probe score what it claims t…
Sep 9, 2026
3f0f48b
fix(images): bound the upload cache by the URL's own signed expiry, n…
Sep 9, 2026
a9c9f57
fix(openai): stop the turn gate turning a correct answer into HTTP 429
Sep 9, 2026
08e504a
fix(images): say an image is here only when it is, and only when we s…
Sep 9, 2026
e80b54b
fix(images): a cache entry that vanished mid-read must re-upload, not…
Sep 9, 2026
8411eb4
test(probes): give the tool_result image probe a cell that can fail
Sep 9, 2026
aedd124
fix(openai): close the residue leak the tolerant unwrap opened, and s…
Sep 9, 2026
2575b71
fix(images): one HARVEST_MEDIA_CAP, and a test that fails when the tw…
Sep 9, 2026
1c31c6f
test(agent-turn): pin the ledger's default entry capacity at 40
Sep 9, 2026
ec0de7e
test(probes): make the A/B driver's tool simulator honest, and pin it
Sep 9, 2026
08c724f
fix: map Qwen's daily-quota refusal to 429 on both API paths
Sep 9, 2026
e5babf7
fix(agent-turn): raise the ledger byte cap to 12000, and pin the knob…
Sep 9, 2026
5ca8e60
test: make a short run fail instead of exiting 0
Sep 9, 2026
41b2e4d
fix(quota): stop re-picking the dead account, and tell the truth abou…
Sep 9, 2026
258a658
fix(agent-turn): give the ledger its own inline budget, so 12000 reac…
Sep 9, 2026
a493ada
fix(test-gate): a skipped test is not a pass, and bless can go down
Sep 9, 2026
40df21e
revert(agent-turn): the ledger goes back to 6000 B, its benefit was n…
Sep 10, 2026
fb904db
test(agent-turn): pin what the model READS of the ledger when the pre…
Sep 10, 2026
ef1b6b2
test(agent-turn): medir la ceguera del brazo por defecto en vez de co…
Sep 10, 2026
b24335f
chore(probes): the replay harness stops shipping an operator's transc…
Sep 10, 2026
f43af96
fix(context): attachment failure with tools is a retryable 529/503, n…
Sep 10, 2026
16c9207
upload: fail fast on Aliyun WAF captcha at /files/parse + circuit bre…
Sep 10, 2026
f73850f
fix(upload): rate-limit /files/parse before the WAF does
Sep 10, 2026
4c26239
feat(context): reuse baked history prefix across turns
Sep 10, 2026
e7e14d2
Fix prefix-cache misses and 529 fallback gaps found in code review
Sep 11, 2026
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
44 changes: 44 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ SERVICE_PORT=3000
# prose/thinking after it also cuts the turn, regardless of this cap.
# AGENT_TURN_MAX_TOOL_CALLS=24

# /v1/chat/completions 的 Agent 回合门禁。三个变量此前只存在于 src/config/index.js,
# 没有出现在本文件里 —— 而它们正是决定门禁何时把一次协议分歧变成 HTTP 错误的开关。
# AGENT_TURN_MAX_ATTEMPTS:同一回合最多重生几次(默认 3,clamp 到 2..6)。调高不会
# 提高成功率:重试复用同一个 chat_id 且 parentId 指向刚被拒的那条回复,三次抽样高度
# 相关,只会成倍放大对同一个账号的上游压力(实测这正是触发 WAF/RGV587 的原因)。
# AGENT_TURN_ALLOW_PROSE_WITH_TOOLS:允许正文与工具调用共存(默认 false)。Anthropic
# 路径无条件允许(anthropic.js#decideRetryReason),所以打开它就是两条路径对齐。
# AGENT_TURN_ACCEPT_BARE_FINAL:接受没有完成包装的裸正文(默认 false)。默认关闭是
# 刻意的:不能把"计划/进度汇报"当成任务完成交付给 agentic 客户端。
# Agent turn gate on /v1/chat/completions. These three lived only in src/config/index.js.
# AGENT_TURN_MAX_ATTEMPTS: regenerations per turn (default 3, clamped 2..6). Raising it
# does not raise the success rate — retries reuse the same chat_id with parentId pointing
# at the just-rejected response, so the draws are correlated; it mostly multiplies upstream
# load on one account (measured: that is what trips the WAF/RGV587 challenge).
# AGENT_TURN_ALLOW_PROSE_WITH_TOOLS: let prose coexist with tool calls (default false).
# The Anthropic path allows it unconditionally, so enabling it aligns both paths.
# AGENT_TURN_ACCEPT_BARE_FINAL: accept bare prose with no completion wrapper (default
# false). Off by default on purpose: a plan or a progress update must not be delivered to
# an agentic client as a finished task.
# AGENT_TURN_MAX_ATTEMPTS=3
# AGENT_TURN_ALLOW_PROSE_WITH_TOOLS=false
# AGENT_TURN_ACCEPT_BARE_FINAL=false

# 监听地址(非必填)
# Listen address (optional)
LISTEN_ADDRESS=
Expand Down Expand Up @@ -104,6 +127,27 @@ AGENT_CONTEXT_FILE_THRESHOLD_BYTES=92160
# Maximum bytes of tool protocol/current-turn context kept in the live request after externalization.
AGENT_CONTEXT_LIVE_PROMPT_BYTES=49152

# 附件失败且请求不带工具时,压缩回退保留的最大字节数(带工具的请求改为返回可重试的 529/503)。
# Fallback budget when the attachment fails on a request WITHOUT tools (requests with tools get a retryable 529/503 instead).
AGENT_CONTEXT_FALLBACK_PROMPT_BYTES=86016

# 附件解析被 WAF 连续拦截 3 次后,在此秒数内不再上传(529 带 Retry-After);0 关闭。
# After 3 consecutive WAF challenges on the attachment parse, stop uploading for this many seconds (529 + Retry-After); 0 disables.
AGENT_PARSE_BREAKER_SECONDS=300

# 附件解析速率上限:每个进程在 WINDOW 秒内最多 MAX 次 upload+parse,超出即返回短 Retry-After 的 529,避免触发按 IP 计数的 WAF。MAX=0 关闭。
# Attachment parse rate limit: at most MAX upload+parse per WINDOW seconds per process; beyond that a 529 with a short Retry-After, before the per-IP WAF starts challenging. MAX=0 disables.
AGENT_PARSE_MAX_PER_WINDOW=6
AGENT_PARSE_WINDOW_SECONDS=120

# 跨回合复用已解析的历史前缀:同一会话下一回合若历史以已上传文本开头,则复用同一附件,仅新增行内联(每 3-10 回合一次 parse)。false 关闭。
# Reuse the parsed history prefix across turns: when the next turn's history starts with the text already uploaded, the same attachment is reused and only the new lines go inline (one parse per 3-10 turns). false disables.
AGENT_CONTEXT_PREFIX_REUSE=true
# 条目自上传起的绝对存活秒数(过期 file_id 不报错,模型会静默丢失附件,故不宜过长);以及每进程最多缓存条目数。
# Absolute lifetime in seconds since upload (an expired file_id does not error — the model silently loses the attachment — so keep it short); and max cached entries per process.
AGENT_CONTEXT_PREFIX_TTL_SECONDS=1800
AGENT_CONTEXT_PREFIX_MAX_ENTRIES=200

# Redis链接(如果使用redis模式,则必填,当redis使用tls时将redis://替换为rediss://)
# Redis URL (required for redis mode; use rediss:// for TLS)
REDIS_URL=
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
# Must exceed the gate's own worst case (TEST_GATE_TIMEOUT_MS x
# TEST_GATE_ATTEMPTS, pinned below to 3 x 150s = 7.5 min) plus npm ci and
# lint. With the old 10 here and the gate's 10-minute default watchdog, the
# job died at 10 min and the watchdog — the thing that makes the gate unable
# to hang — could never fire.
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand All @@ -34,3 +39,8 @@ jobs:
# unset, killing the five suites that load controllers. The value
# itself is never read by any test.
API_KEY: ci-test-key
# The whole suite runs in ~5s locally. 150s per attempt is ~30x
# headroom for a cold shared runner, and 3 attempts still fit inside
# timeout-minutes above, so a hung runner is reported by the gate as
# TIMEOUT instead of looking like CI infrastructure flake.
TEST_GATE_TIMEOUT_MS: 150000
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"start": "node src/start.js",
"dev": "nodemon src/server.js",
"test": "node --test --test-force-exit tests/*.test.js",
"test": "node tools/test-gate.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"pm2": "pm2 start ecosystem.config.js",
Expand All @@ -18,7 +18,9 @@
"pm2:delete": "pm2 delete qwen2api",
"pm2:logs": "pm2 logs qwen2api",
"pm2:status": "pm2 status",
"pm2:monit": "pm2 monit"
"pm2:monit": "pm2 monit",
"test:raw": "node --test --test-force-exit tests/*.test.js",
"test:bless": "node tools/test-gate.js --bless"
},
"keywords": [],
"author": "",
Expand Down
41 changes: 41 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,47 @@ const config = {
8 * 1024,
parseInt(process.env.AGENT_CONTEXT_LIVE_PROMPT_BYTES, 10) || 48 * 1024
),
// Presupuesto del fallback cuando el adjunto falla y la peticion NO lleva tools
// (con tools no se compacta: se responde 529/503 reintentable). Mas holgado que el
// live prompt porque aqui no hay adjunto que complete el resto.
agentContextFallbackPromptBytes: Math.max(
8 * 1024,
parseInt(process.env.AGENT_CONTEXT_FALLBACK_PROMPT_BYTES, 10) || 84 * 1024
),
// Cortacircuitos del parse de adjuntos (src/utils/upload.js): tras 3 desafios WAF
// seguidos no se sube nada durante estos segundos y el 529 lleva ese Retry-After.
// 0 lo desactiva.
agentParseBreakerSeconds: (() => {
const raw = parseInt(process.env.AGENT_PARSE_BREAKER_SECONDS, 10)
return Number.isFinite(raw) && raw >= 0 ? raw : 300
})(),
// Limitador de ritmo del parse (src/utils/upload.js): como maximo MAX upload+parse por
// ventana de WINDOW segundos por proceso; el resto recibe 529 con Retry-After corto
// ANTES de que el WAF (que cuenta por IP) empiece a desafiar. Medido 2026-09-10:
// 10 en 150 s disparan el desafio. MAX = 0 lo desactiva.
agentParseMaxPerWindow: (() => {
const raw = parseInt(process.env.AGENT_PARSE_MAX_PER_WINDOW, 10)
return Number.isFinite(raw) && raw >= 0 ? raw : 6
})(),
agentParseWindowSeconds: (() => {
const raw = parseInt(process.env.AGENT_PARSE_WINDOW_SECONDS, 10)
return Number.isFinite(raw) && raw > 0 ? raw : 120
})(),
// Reutilizacion del prefijo de historial entre turnos (src/utils/context-prefix-cache.js):
// el historial ya subido y parseado viaja como el mismo adjunto y solo la cola nueva va
// inline — un parse cada 3-10 turnos en vez de uno por turno. 'false' lo apaga.
agentContextPrefixReuse: process.env.AGENT_CONTEXT_PREFIX_REUSE !== 'false',
// Vida ABSOLUTA de una entrada (desde que se subio). Un file_id caducado en Qwen no da
// error: el modelo contesta sin el adjunto (medido 2026-09-10), asi que el TTL es la
// unica cota contra un historial fantasma.
agentContextPrefixTtlSeconds: (() => {
const raw = parseInt(process.env.AGENT_CONTEXT_PREFIX_TTL_SECONDS, 10)
return Number.isFinite(raw) && raw > 0 ? raw : 1800
})(),
agentContextPrefixMaxEntries: (() => {
const raw = parseInt(process.env.AGENT_CONTEXT_PREFIX_MAX_ENTRIES, 10)
return Number.isFinite(raw) && raw > 0 ? raw : 200
})(),
// Antidetect Tier 1: per-account fingerprint & header diversity.
// Set to 'false' to instantly roll back to legacy static headers.
antidetectTier1Enabled: process.env.ANTIDETECT_TIER1_ENABLED !== 'false',
Expand Down
Loading