fix(core): stringify const-derived enums in toOpenAPI30 - #7547
Conversation
toOpenAPI30 maps const to a single-value enum, then separately stringifies
enums because — per its own comment — Gemini strictly requires enums to be
strings. The two never met: the stringification keys off source['enum'],
which a const-only schema never sets, so the const value went through raw.
{ const: 5 } -> { enum: [5] } // number, breaks the rule
{ const: true } -> { enum: [true] } // boolean, likewise
{ enum: [1, 2] } -> { enum: ['1', '2'] } // the intended behavior
Build the const-derived enum with String() so both paths produce the same
kind of value.
|
Thanks for the PR! Template looks good ✓ — all required sections are present, including a filled-in Tested-on table and a full Chinese translation. Problem: this is an observed, demonstrable inconsistency, not theoretical hardening. The converter documents "Gemini strictly requires enums to be strings" and stringifies every Direction: aligned. Making the const-derived path obey the same string-enum rule the module already enforces is squarely within this converter's stated purpose. No auth/sandbox/model-selection/telemetry/public-contract surface involved. Size: core path touched ( Approach: about as minimal as it gets — a one-line Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 所有必需小节齐全,包括填写好的 Tested-on 表格和完整中文翻译。 问题:这是一个已观测到、可复现的不一致,而非理论性加固。转换器明确声明「Gemini 严格要求 enum 为字符串」,并对每个 方向:对齐。让 const 派生路径遵守该模块已强制执行的同一条字符串 enum 规则,完全在此转换器的既定职责之内。不涉及 auth/sandbox/模型选择/telemetry/公共契约。 规模:触及核心路径( 方案:已经尽可能精简——一行 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewReading just the title + "Why it's needed", my independent fix would have been one of two things: wrap the const value in Correctness checks out. I traced One tiny note for the maintainer, not a blocker: the PR description calls Real-scenario testing
Before (main
|
|
Confidence: 5/5 — clean across every stage; I'd merge this without hesitation. Stepping back: this is exactly the kind of PR the gate should wave through quickly. The problem is real and I verified it independently on The only reservation, and it's cosmetic, is the description calling the anthropic converter "the only consumer" when the openai converter also calls The fork- 中文说明置信度:5/5 —— 每个阶段都干净;我会毫不犹豫地合并。 退一步看:这正是 gate 应当快速放行的那类 PR。问题真实存在,且我已在 唯一的保留意见,且属措辞层面:描述把 anthropic converter 称为「唯一消费方」,而 openai converter 同样调用了 fork- — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
Review & Local Verification Report代码审查设计评价:一致性修复。 本 PR 修复了 问题: 修复: 测试覆盖: 结论LGTM。 单行修复,与现有 enum 字符串化行为保持一致。注释解释了为何步骤 5 无法覆盖此场景。 |
doudouOUC
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
ZijianZhang989
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
|
Released in v0.21.1. |
|
|
What this PR does
toOpenAPI30builds itsconst-derived enum withString(), so it obeys the same string-enum rule the converter already enforces forenum. One-line change plus tests.Why it's needed
The converter does two related things in separate steps:
Step 5 keys off
source['enum']— the source schema — which a const-only schema never sets. So the enum step 2 just created is never stringified, and the converter violates its own documented invariant. Verified viaconvertSchema(schema, 'openapi_30')onmain:main{ const: 5 }{ enum: [5] }{ const: true }{ enum: [true] }{ const: 'x' }{ enum: ['x'] }{ enum: [1, 2] }{ enum: ['1', '2'] }So two schemas that mean the same thing —
{ const: 5 }and{ enum: [5] }— convert to different value types, and the one that goes throughconstis the one the comment says Gemini will reject.Reviewer Test Plan
How to verify
npx vitest run --root packages/core src/utils/schemaConverter.test.ts→ 23/23.should stringify a non-string const like any other enumcovers{ const: 5 },{ const: true }, and{ type: 'integer', const: 0 }(the last one also pins thattypeis left alone). Reverting onlyschemaConverter.tsfails it withexpected { enum: [ 5 ] } to deeply equal { enum: [ '5' ] }.should convert const to enum({ const: 'foo' }→{ enum: ['foo'] }) andshould stringify enumstests are untouched and still pass.anthropicContentGenerator/converter.ts, is unaffected:schemaConverter+anthropic convertersuites are 90/90 together.Evidence (Before & After)
convertSchema({ const: 5 }, 'openapi_30')→{ enum: [5] }.{ enum: ['5'] }, matchingconvertSchema({ enum: [5] }, 'openapi_30')→{ enum: ['5'] }.Tested on
macOS:
schemaConverter(23) and theanthropicContentGeneratorconverter suite pass locally with proven fail-before/pass-after; typecheck and eslint clean. Pure value transformation with no platform-dependent behavior, so no manual QA is required; CI covers Windows/Linux.Environment (optional)
Node v24;
@qwen-code/qwen-code-coreworkspace; vitest 3.2.Risk & Scope
constnow reaches the model as a string. That is the point — the module states Gemini requires string enums and already does exactly this to everyenumarray, so this makes the two paths agree rather than introducing a new policy.typeis deliberately left untouched, so{ type: 'integer', const: 0 }becomes{ type: 'integer', enum: ['0'] }. That mirrors what the existingenumstringification already produces for{ type: 'integer', enum: [0] }— whether the converter should also coercetypeto'string'alongside a stringified enum is a pre-existing question that applies equally to both paths, so it did not belong in this change.Linked Issues
None — found by reading step 2 against step 5 in
toOpenAPI30.中文说明
本 PR 的作用
toOpenAPI30在构造由const派生的 enum 时改用String(),使其遵守该转换器已对enum强制执行的同一条「字符串 enum」规则。一行改动,外加测试。为什么需要
转换器在两个独立步骤中做了两件相关的事:
第 5 步依据的是
source['enum']——源 schema——而只含const的 schema 从不设置它。于是第 2 步刚刚创建的 enum 永远不会被字符串化,转换器违反了自己文档化的不变式。在main上通过convertSchema(schema, 'openapi_30')验证:main上的输出{ const: 5 }{ enum: [5] }{ const: true }{ enum: [true] }{ const: 'x' }{ enum: ['x'] }{ enum: [1, 2] }{ enum: ['1', '2'] }于是语义相同的两个 schema——
{ const: 5 }与{ enum: [5] }——会转换成不同的值类型,而走const的那条恰恰是注释所说 Gemini 会拒绝的形式。复核测试计划
如何验证
npx vitest run --root packages/core src/utils/schemaConverter.test.ts→ 23/23 通过。should stringify a non-string const like any other enum覆盖{ const: 5 }、{ const: true }与{ type: 'integer', const: 0 }(最后一项同时固定了type不被改动)。仅还原schemaConverter.ts时该测试失败:expected { enum: [ 5 ] } to deeply equal { enum: [ '5' ] }。should convert const to enum({ const: 'foo' }→{ enum: ['foo'] })与should stringify enums未作改动且仍然通过。anthropicContentGenerator/converter.ts不受影响:schemaConverter与 anthropic converter 两个套件合计 90/90 通过。证据(修复前后对比)
convertSchema({ const: 5 }, 'openapi_30')→{ enum: [5] }。{ enum: ['5'] },与convertSchema({ enum: [5] }, 'openapi_30')→{ enum: ['5'] }一致。测试环境
macOS:
schemaConverter(23)与anthropicContentGeneratorconverter 套件本地通过,并验证了 fail-before/pass-after;typecheck 与 eslint 干净。纯值变换,无平台相关行为,因此无需人工 QA;Windows/Linux 由 CI 覆盖。运行环境(可选)
Node v24;
@qwen-code/qwen-code-core工作区;vitest 3.2。风险与影响范围
const现在会以字符串形式送达模型。这正是本意——该模块明确声明 Gemini 要求字符串 enum,并且已对每个enum数组做了完全相同的处理;本改动只是让两条路径保持一致,而非引入新策略。type,因此{ type: 'integer', const: 0 }变为{ type: 'integer', enum: ['0'] }。这与既有的 enum 字符串化对{ type: 'integer', enum: [0] }的产出完全一致——转换器是否应在字符串化 enum 的同时把type也改为'string',是一个对两条路径同样适用的既有问题,不属于本次改动。关联 Issue
无——通过将
toOpenAPI30的第 2 步与第 5 步对照阅读发现。