Repository navigation
fix: carry reasoning effort through to GLM-5.3 on the OpenAI client path - #32
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are a few concrete correctness issues in the new logic (notably CLI arg parsing and GLM-5.3 output_config/effort handling) that should be fixed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR expands the proxy’s pinned GLM model catalog (notably adding glm-5.3-flash) and fixes GLM-5.3-family “reasoning effort” handling by introducing a shared reasoning contract and wiring it through request translators and passthrough body transforms. It also adds a probing-based update-models script (plus tests) to help maintain src/provider/models.ts safely.
Changes:
- Add a GLM-5.3 reasoning-effort contract (
output_config.effort+thinking.budget_tokens) and apply it in OpenAI↔Anthropic translators and passthrough transforms. - Update the pinned model catalog/specs (incl.
glm-5.3-flash) and align configs/docs/tests with the new catalog. - Add
bun run update-modelsprobing script + unit tests and includescripts/**/*in TS compilation.
File summaries
| File | Description |
|---|---|
| tsconfig.json | Includes scripts/**/* so the new probing script typechecks. |
| src/translator/types.ts | Adds output_config typing for Anthropic requests (GLM-5.3 effort channel). |
| src/translator/openai-to-anthropic.ts | Implements GLM-5.3 effort/budget translation and model-aware max_tokens defaulting. |
| src/translator/openai-to-anthropic.test.ts | Adds regression tests covering GLM-5.3(-flash) effort/budget behavior. |
| src/translator/anthropic-to-openai.ts | Carries output_config.effort into OpenAI reasoning_effort and rewrites disabled thinking for GLM-5.3. |
| src/translator/anthropic-to-openai.test.ts | New tests for output_config.effort carry-over and disabled-thinking rewrite. |
| src/proxy/body-transformer.ts | Adds passthrough-route rewrites for GLM-5.3 disabled thinking (OpenAI + Anthropic shapes). |
| src/proxy/body-transformer.test.ts | Adds tests for passthrough-route disabled-thinking rewrites and budget fitting. |
| src/provider/reasoning.ts | New module defining GLM-5.3 effort levels, budgets, model matching, and budget fitting. |
| src/provider/reasoning.test.ts | Unit tests for the GLM-5.3 reasoning contract helpers. |
| src/provider/providers.test.ts | Updates model catalog assertions (context windows, maxOutputTokens, reasoning flags, exclusions). |
| src/provider/models.ts | Updates pinned model list/specs; adds glm-5.3-flash and adjusts other model metadata. |
| src/config/template.ts | Updates template model list to match the updated pinned catalog. |
| scripts/update-models.ts | Adds probing-based catalog regeneration script with a two-stage availability/ceiling probe. |
| scripts/update-models.test.ts | Unit tests for the script’s pure functions (classification/parsing/merge policy). |
| README.md | Updates documented pinned model table/specs and adds notes about aliasing/availability. |
| package.json | Adds update-models script entry. |
| config.example.yaml | Updates example model list to match the updated pinned catalog. |
Review details
- Files reviewed: 17/18 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| case "--provider": { | ||
| const v = argv[++i]; | ||
| if (v !== "zai" && v !== "bigmodel") throw new Error(`--provider must be zai|bigmodel, got "${v}"`); | ||
| args.provider = v; | ||
| break; | ||
| } | ||
| case "--api-key": | ||
| args.apiKey = argv[++i]; | ||
| break; | ||
| case "--config": | ||
| args.configPath = argv[++i]; | ||
| break; |
| const explicitBudget = explicit.budget_tokens ?? explicit.budgetTokens; | ||
| if (typeof explicitBudget === "number" && Number.isFinite(explicitBudget) && explicitBudget > 0) { | ||
| budget = Math.floor(explicitBudget); | ||
| } |
| function applyGlm53AnthropicThinkingCompat(body: Record<string, unknown>): boolean { | ||
| if (!glm53Model(body) || !thinkingIsDisabled(body)) return false; | ||
| const legal = buildGlm53Reasoning(GLM53_DISABLED_REPLACEMENT_EFFORT); | ||
| const budget = fitGlm53Budget(legal.thinking.budget_tokens, body.max_tokens); | ||
| body.thinking = budget === undefined | ||
| ? { type: "enabled" } | ||
| : { type: "enabled", budget_tokens: budget }; | ||
| if (!isPlainObject(body.output_config)) { | ||
| body.output_config = legal.output_config; | ||
| } | ||
| return true; | ||
| } |
| // GLM-5.3+ Anthropic clients set reasoning effort via `output_config.effort` | ||
| // (the Anthropic upstream ignores `reasoning_effort`); the OpenAI-format | ||
| // upstream on start-plan reads `reasoning_effort` instead, so carry it over. | ||
| if (req.output_config?.effort) { | ||
| result.reasoning_effort = req.output_config.effort as OpenAIChatRequest["reasoning_effort"]; | ||
| } |
|
请只提交一个修复OpenAI协议 reasoning_effort PR,模型列表和上下文问题均为 NOT A BUG |
On coding-plan the upstream is Anthropic-format, so an OpenAI client's
request goes through translateRequestOpenAIToAnthropic. That function
only special-cased reasoning_effort:"none"; every other value degraded
to `{type:"enabled"}` with no budget and no output_config. A captured
upstream body:
client sent reasoning_effort:"high", model glm-5.3
-> upstream got {"model":"glm-5.3","thinking":{"type":"enabled"}}
-> 1 character of thinking, where effort:"high" yields ~244
The Anthropic endpoint silently ignores reasoning_effort, so
output_config.effort is the only channel that works there, and the
upstream validates neither an illegal effort value nor a budget larger
than max_tokens — normalizing has to happen here.
New src/provider/reasoning.ts holds the contract in one place, taken
from ZCode's catalog entry for glm-5.3: three levels with budgets
8000 / 16000 / 32000, defaultLevel "max". Effort normalization follows
Z.AI's published table, which rounds up — "medium" maps to "high", not
"low"; there is a named regression test for that.
Also carries output_config.effort into reasoning_effort in the
start-plan direction (Anthropic client, OpenAI upstream), where the
channels are reversed. That carry-over is gated to the GLM-5.3 family:
output_config is this family's extension, and forwarding it for every
model would let a client reroute another model's reasoning through a
channel it never opted into.
Two default-value changes for the GLM-5.3 family only, both following
ZCode: a request that omits reasoning_effort now gets effort "max"
rather than the gateway's near-zero default, and one that omits
max_tokens falls back to the model's catalog maxOutputTokens rather
than the generic 4096. The second is load-bearing for the first —
ZCode's clamp of `min(budget, maxOutputTokens - 1)` is written against
the model ceiling, and applying it to a 4096 fallback left a 4095-token
thinking budget with nothing for the answer. fitGlm53Budget now
reserves 1024 tokens for the answer and floors a fractional max_tokens.
Membership of the pinned catalog is a hint here, never a gate. Model
matching is by id pattern, so a family member the catalog does not list
(glm-5.3-flash today) still gets the effort channel; only the
max_tokens fallback consults the catalog, and it degrades to the
generic 4096 default rather than failing. Tests cover both.
Net effect on a bare OpenAI request for glm-5.3: max_tokens 4096 and
~1 character of thinking becomes max_tokens 128_000 with a 32_000
thinking budget. That is ZCode parity, and it costs more.
2bb2d5f to
675298b
Compare
…ns glm-5.3-flash Merge of master (1646a4c 'feat(models): advertise glm-5.3-flash') invalidated the premise that glm-5.3-flash is absent from the pinned catalog: the fixture now resolves the model-aware default (128,000) instead of the generic 4096. - Point the unlisted-family fixture at a genuinely unlisted id (glm-5.3-air), preserving the original intent: the effort channel is keyed off the model id, not catalog membership. - Pin the new catalog reality: glm-5.3-flash gets the same model-aware default as glm-5.3 (max_tokens 128,000, max-effort budget 32,000).
…nk 最小放宽 - CL-16: handler.ts 顶部注释重写为 v2.6 现实(两套餐均 Anthropic 上游, OpenAI 客户端 O→A 翻译,Anthropic 客户端原生 passthrough); passthroughResponse docstring 改为如实描述 accept-encoding 转发; claim/client.ts 引用去掉不存在的 NOTEPAD 章节名 - R2-11: manager.ts 'refreshing if necessary' 过度承诺改为如实描述 (凭据过期由上游 401 暴露,守卫保留备未来填充 expiresAt) - CL-17: 抽共享 src/proxy/inflate.ts inflateWithCap(DecompressionStream + 上限 + cancel + releaseLock);proxy/handler.ts 与 async/handler.ts 两处解压循环改调,64MiB/4MiB 上限与错误契约(throw vs errorResponse) 保留在各自调用方 - CL-15: /v1/responses userId 门控对齐 handler.ts(startPlan 不注入 metadata.user_id),留档 bundle 为 presence-gated 无套餐概念 - R2-18: parseSSEChunk data: 前缀放宽(至多剥一个前导空格,'data: x' 输出逐字节不变);帧分割容忍 \r\n\r\n(导出 SSE_FRAME_SPLIT,与 3.11.2 bundle Kxo 正则一致),anthropicSseToOpenaiSse 与 async 流适配器 的缓冲分割同步切换——CRLF 流不再永久缓冲;:496 第二解析器与其余解析点 不动(#32)
按 review 意见收窄:只保留 OpenAI 协议 reasoning_effort 的修复。
已移除固定模型目录的改动、
update-models探测脚本,以及随之而来的 README / config / tsconfig 改动。改动面从 18 个文件收敛到 7 个,只剩src/translator与src/provider/reasoning.ts。问题
coding-plan 的上游是 Anthropic 格式,所以 OpenAI 客户端的请求会走
translateRequestOpenAIToAnthropic。该函数只特判了reasoning_effort:"none",其余取值一律退化成{type:"enabled"}—— 既没有 budget,也没有output_config。抓到的上游请求体:Anthropic 端点会静默忽略
reasoning_effort,output_config.effort才是唯一生效的通道,而且必须配对thinking.budget_tokens—— 只给 effort 标签,上游仍停在自己近乎零的默认值上。上游对非法 effort 值和超过max_tokens的 budget 都不做校验,所以归一化只能放在这里做。改动
src/provider/reasoning.ts,把契约集中在一处:三档 effort 配对 budget 8000 / 16000 / 32000,默认档max—— 取自 ZCode 自带目录里的 glm-5.3 条目。effort 归一化遵循 Z.AI 公布的映射表,向上取整:medium映射到high而不是low,这条有具名回归测试。openai-to-anthropic(coding-plan 方向):GLM-5.3 系列请求一律带上显式 effort 与配对 budget,budget 会 fit 进max_tokens并为回答保留 1024 token。anthropic-to-openai(start-plan 方向):两边通道相反,把output_config.effort带进reasoning_effort。默认值改动仅限 GLM-5.3 系列,均对齐 ZCode:未指定
reasoning_effort时取max,未指定max_tokens时回退到该模型目录里的maxOutputTokens而非通用的 4096。后者是前者的前提 —— ZCode 的min(budget, maxOutputTokens - 1)是针对模型上限写的,套到 4096 的兜底值上会把几乎整个额度吃进思考里。其他模型行为完全不变,测试有断言。
output_config的透传也 gate 在 GLM-5.3 系列内。关于模型目录
按 review 意见,本 PR 不改目录。这里的模型匹配走 model id 正则、不依赖目录收录,所以目录里没有的同系列模型(今天的
glm-5.3-flash)一样能拿到 effort 通道;只有max_tokens的兜底会查目录,查不到就退回通用的 4096,不会失败。两种情况都有测试覆盖。Copilot review
explicitBudget > 0之后才 floor —— 已改为先 floor 再判正。否则0.5能通过> 0却 floor 成 0,在max_tokens非有限数时会原样传给上游变成budget_tokens: 0。补了 2 个回归测试。output_config.effort对所有模型透传 —— 已 gate 到 GLM-5.3 系列。补了 glm-4.7 的断言测试。update-models的 CLI 参数校验、body-transformer的output_config处理)所在代码已随本次收窄一并移除。验证
bun test635 pass / 0 fail,tsc --noEmit通过。