Skip to content

fix(core): preserve invalid schema length strings - #5312

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/openai-schema-integer-constraints
Jun 18, 2026
Merged

fix(core): preserve invalid schema length strings#5312
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/openai-schema-integer-constraints

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Tightens the OpenAI schema conversion in convertGeminiToolParametersToOpenAI for the string-valued length/item constraints minLength, maxLength, minItems, and maxItems. These fields are now only coerced from a string to a number when the string is non-empty (after trimming) and represents an actual integer. The conversion uses the parsed Number(...) value directly instead of parseInt(...).

Previously the guard accepted any string that satisfied !isNaN(Number(value)) and then wrote parseInt(value, 10), so a value like "1.5" passed the check and was silently truncated to 1 (and a whitespace-only string like " " coerced to 0). With this change, such non-integer strings are left untouched and the original value is preserved.

Numeric constraints such as minimum, maximum, and multipleOf are not affected by this change.

Why it's needed

convertGeminiToolParametersToOpenAI() checked Number(value) but then converted with parseInt(value, 10). A value such as "1.5" passes the numeric check but is written as 1, silently changing the schema. Length and item constraints should only be converted when the string represents an integer; otherwise the original value must be preserved rather than truncated. See issue #5310.

Reviewer Test Plan

How to verify

Repro: pass a tool parameter schema where a length/item constraint is a non-integer string (e.g. minLength: "1.5", maxLength: " ", minItems: "10px", maxItems: "1.5") through convertGeminiToolParametersToOpenAI.

  • Expected (after fix): the non-integer / non-numeric / whitespace strings are preserved verbatim instead of being truncated to 1/0. Valid integer strings still convert to numbers.
  • Observed (before fix): "1.5" was silently written as 1.

Verification commands (run from packages/core unless noted):

  • npx vitest run src/core/openaiContentGenerator/converter.test.ts -t "convertGeminiToolParametersToOpenAI"
  • npx vitest run src/core/openaiContentGenerator/converter.test.ts
  • npm run typecheck --workspace=packages/core
  • npm run build --workspace=packages/core
  • npm run lint -- --fix packages/core/src/core/openaiContentGenerator/converter.ts packages/core/src/core/openaiContentGenerator/converter.test.ts
  • npx prettier --check packages/core/src/core/openaiContentGenerator/converter.ts packages/core/src/core/openaiContentGenerator/converter.test.ts
  • git diff --check

Evidence (Before & After)

N/A — non-visible logic fix; covered by the unit tests above.

Tested on

OS Status
🍏 macOS ✅ CI
🪟 Windows ✅ CI
🐧 Linux ✅ CI

✅ tested · ⚠️ not tested · N/A

Environment (optional)

Unit tests only (npm workspaces).

Risk & Scope

  • Main risk or tradeoff: Low; scoped to the length/item constraint branch of convertGeminiToolParametersToOpenAI in packages/core. Behavior change is limited to how string-valued minLength/maxLength/minItems/maxItems are handled (non-integer strings are now preserved instead of truncated).
  • Not validated / out of scope: No unrelated refactors, no public API changes, no changes to numeric constraints (minimum/maximum/multipleOf), no UI changes.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #5310

中文说明

这个 PR 做了什么

收紧了 convertGeminiToolParametersToOpenAI 中对字符串型长度/数量约束 minLengthmaxLengthminItemsmaxItems 的转换逻辑。现在只有当字符串在去除首尾空白后非空、且表示一个真正的整数时,才会把它从字符串转换为数字;转换时直接使用解析得到的 Number(...) 值,而不再使用 parseInt(...)

此前的判断条件接受任何满足 !isNaN(Number(value)) 的字符串,然后写入 parseInt(value, 10),因此像 "1.5" 这样的值能通过检查并被静默截断为 1(而像 " " 这样的纯空白字符串会被转换为 0)。改动之后,这类非整数字符串保持不变,原始值得以保留。

minimummaximummultipleOf 等数值约束不受此改动影响。

为什么需要它

convertGeminiToolParametersToOpenAI() 先检查 Number(value),却用 parseInt(value, 10) 进行转换。像 "1.5" 这样的值能通过数值检查,但会被写成 1,从而静默地改变了 schema。长度和数量约束只应在字符串表示整数时才转换;否则必须保留原始值,而不是截断它。详见 issue #5310

评审者测试计划

如何验证

复现:构造一个工具参数 schema,其中长度/数量约束为非整数字符串(例如 minLength: "1.5"maxLength: " "minItems: "10px"maxItems: "1.5"),传入 convertGeminiToolParametersToOpenAI

  • 预期(修复后):非整数 / 非数值 / 纯空白字符串原样保留,而不再被截断为 1/0。合法的整数字符串仍会转换为数字。
  • 实际(修复前):"1.5" 被静默写成 1

验证命令(除非另有说明,均在 packages/core 目录下执行):

  • npx vitest run src/core/openaiContentGenerator/converter.test.ts -t "convertGeminiToolParametersToOpenAI"
  • npx vitest run src/core/openaiContentGenerator/converter.test.ts
  • npm run typecheck --workspace=packages/core
  • npm run build --workspace=packages/core
  • npm run lint -- --fix packages/core/src/core/openaiContentGenerator/converter.ts packages/core/src/core/openaiContentGenerator/converter.test.ts
  • npx prettier --check packages/core/src/core/openaiContentGenerator/converter.ts packages/core/src/core/openaiContentGenerator/converter.test.ts
  • git diff --check

证据(前后对比)

N/A — 这是不可见的逻辑修复;已由上述单元测试覆盖。

测试平台

操作系统 状态
🍏 macOS ✅ CI
🪟 Windows ✅ CI
🐧 Linux ✅ CI

✅ 已测试 · ⚠️ 未测试 · N/A

环境(可选)

仅单元测试(npm workspaces)。

风险与范围

  • 主要风险或取舍:低;改动范围限于 packages/coreconvertGeminiToolParametersToOpenAI 的长度/数量约束分支。行为变更仅限于字符串型 minLength/maxLength/minItems/maxItems 的处理方式(非整数字符串现在会被保留而非截断)。
  • 未验证 / 范围之外:没有无关的重构,没有公共 API 变更,没有改动数值约束(minimum/maximum/multipleOf),没有 UI 变更。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Fixes #5310

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@tt-a1i
tt-a1i marked this pull request as ready for review June 18, 2026 17:00
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @tt-a1i — thanks for the fix! The bug is real and the code change looks reasonable, but the PR body doesn't follow the PR template.

The template requires these sections: What this PR does, Why it's needed, Reviewer Test Plan (with How to verify, Evidence (Before & After), Tested on), Risk & Scope, Linked Issues, and a 中文说明 translation. Your PR uses custom headings (Summary, Test plan, Demo, AI Assistance Disclosure) that don't match.

Could you reformat the body to follow the template? It helps reviewers and keeps triage consistent. The actual content is fine — just needs to fit the expected structure.

中文说明

@tt-a1i 你好——感谢修复!这个 bug 确实存在,代码改动也合理,但 PR 正文没有按照 PR 模板 的格式来写。

模板要求包含以下章节:What this PR doesWhy it's neededReviewer Test Plan(含 How to verifyEvidence (Before & After)Tested on)、Risk & ScopeLinked Issues,以及 中文说明 翻译。你的 PR 使用了自定义标题(Summary、Test plan、Demo、AI Assistance Disclosure),与模板不一致。

请按模板格式重新组织正文内容即可,实际内容没有问题,只需要调整结构。

Qwen Code · qwen3.7-max

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @tt-a1i!

Template looks good ✓

On direction: This is a clear correctness fix in the Gemini→OpenAI schema conversion layer. parseInt("1.5", 10) silently truncating to 1 is a real bug that could produce malformed tool schemas. Solidly within scope — no direction concerns.

On approach: The fix is tight and minimal — swapping the !isNaN(Number(value)) guard for Number.isInteger(numberValue) and using the parsed value directly instead of parseInt. Scope is exactly what the bug calls for, no drive-by changes. Moving on to code review. 🔍

中文说明

感谢贡献,@tt-a1i

模板完整 ✓

方向:这是 Gemini→OpenAI schema 转换层的一个明确的正确性修复。parseInt("1.5", 10) 静默截断为 1 是一个真实存在的 bug,可能导致工具 schema 格式错误。完全在项目范围内——没有方向上的顾虑。

方案:修复紧凑且最小化——将 !isNaN(Number(value)) 判断替换为 Number.isInteger(numberValue),并直接使用解析后的值而非 parseInt。改动范围恰好匹配 bug 所需,没有夹带无关改动。进入代码审查。🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The fix is clean and correct. The core change replaces a loose guard (!isNaN(Number(value)) + parseInt) with a precise one (Number.isInteger(numberValue) + direct assignment). This properly rejects fractional strings like "1.5", whitespace-only strings like " ", and non-numeric strings like "10px" — all of which were silently mangled before.

One minor observation: typeof value === 'string' is checked twice (once in the ternary, once in the if). Not a blocker — it's readable as-is and the compiler will optimize it away.

No correctness issues, no security concerns, no scope creep.

Test Results

Unit tests (packages/core, convertGeminiToolParametersToOpenAI suite): ✅ 8 passed, 123 skipped

 ✓ src/core/openaiContentGenerator/converter.test.ts (131 tests | 123 skipped) 8ms

 Test Files  1 passed (1)
      Tests  8 passed | 123 skipped (131)

The new test covers the key edge cases: "1.5" (fractional), " " (whitespace-only), "10px" (non-numeric), "1.5" (fractional items). All preserved verbatim as expected.

Typecheck (tsc --noEmit): ✅ clean
Lint (eslint): ✅ clean

Real-Scenario Testing

N/A — this is a non-visible internal logic fix in the schema conversion layer. The bug manifests only in the API request body when tool parameter schemas contain string-valued length/item constraints. There is no CLI-visible behavior to capture via tmux. The unit tests are the authoritative verification.

中文说明

代码审查

修复干净且正确。核心改动将宽松的判断(!isNaN(Number(value)) + parseInt)替换为精确判断(Number.isInteger(numberValue) + 直接赋值)。这正确地拒绝了小数字符串(如 "1.5")、纯空白字符串(如 " ")和非数字字符串(如 "10px")——这些在之前都会被静默破坏。

一个小观察:typeof value === 'string' 被检查了两次(一次在三元表达式中,一次在 if 中)。不构成阻塞——可读性良好,编译器也会优化掉。

没有正确性问题,没有安全隐患,没有范围蔓延。

测试结果

单元测试(packages/core,convertGeminiToolParametersToOpenAI 测试组):✅ 8 通过,123 跳过

类型检查:✅ 通过
Lint:✅ 通过

真实场景测试

不适用——这是 schema 转换层内部的不可见逻辑修复。该 bug 仅在工具参数 schema 包含字符串型长度/数量约束时出现在 API 请求体中。没有可通过 tmux 捕获的 CLI 可见行为。单元测试是权威验证。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot added category/core Core engine and logic type/bug Something isn't working as expected labels Jun 18, 2026
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection

This is a textbook minimal bug fix. The PR author identified a real correctness bug — parseInt("1.5", 10) silently truncating to 1 — and fixed it with surgical precision: replace the loose !isNaN(Number(value)) guard with Number.isInteger(Number(value)), and use the parsed Number(value) directly instead of re-parsing with parseInt. The diff is +5/-1 lines in the source, exactly what the bug calls for.

The test is well-chosen: four edge cases ("1.5", " ", "10px", "1.5") covering fractional, whitespace-only, non-numeric, and fractional-items scenarios. All pass. Typecheck and lint are clean.

I can't find a simpler path. The fix is correct, minimal, and well-tested for what it is. If I had to maintain this in six months, I'd thank the author.

✅ Approving.

中文说明

总结

这是一个教科书式的最小化 bug 修复。PR 作者发现了一个真实的正确性 bug——parseInt("1.5", 10) 静默截断为 1——并用精确的修改修复了它:将宽松的 !isNaN(Number(value)) 判断替换为 Number.isInteger(Number(value)),并直接使用 Number(value) 而非 parseInt 重新解析。diff 在源码中为 +5/-1 行,恰好匹配 bug 所需。

测试选择得当:四个边界情况("1.5"" ""10px""1.5")覆盖了小数、纯空白、非数字和小数项的场景。全部通过。类型检查和 lint 均干净。

找不到更简单的方案。修复正确、最小化,且测试充分。如果六个月后需要维护这段代码,我会感谢作者。

✅ 批准。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Verification report — PR #5312 (fixes #5310)

Verdict: verified, recommend merge. Small, correctly-scoped logic fix. I reverse-audited the guard, ran the converter suite with a base A/B (the new test fails without the fix), and — the decisive part — drove the actual compiled converter (PR vs base) on real Gemini tool schemas, at both the function level and the full request-path that builds the tools[] sent to an OpenAI backend. The fix preserves non-integer strings, converts true integers exactly, and doesn't regress numbers or numeric constraints.

What the PR does

convertGeminiToolParametersToOpenAI coerced string-valued minLength/maxLength/minItems/maxItems with !isNaN(Number(v)) then parseInt(v,10) — so "1.5" silently became 1. The PR coerces only when the trimmed string is a real integer, using Number(...):

const numberValue = typeof value === 'string' ? Number(value) : NaN;
if (typeof value === 'string' && value.trim() !== '' && Number.isInteger(numberValue)) {
  result[key] = numberValue;   // exact value, no parseInt truncation
} else {
  result[key] = value;          // preserve verbatim
}

1. Reverse-audit — logic + edge cases

Input Before (parseInt) After (Number + isInteger + trim)
"1.5" 1 (truncated — the bug) "1.5" preserved ✓
" " NaN/0 (mangled) " " preserved ✓ (trim guard)
"10px" "10px" (failed Number) "10px" preserved ✓
"1e3" 1 (parseInt stops at e) 1000 ✓ correct integer
"5" 5 5 ✓ valid integer still converts
5 (number) 5 5 ✓ non-string passthrough

minimum/maximum/multipleOf are untouched (different branch). The guard is tight and order-correct (trim() !== '' before Number.isInteger).

2. Unit suite + base A/B

  • PR: converter.test.ts131 pass (-t convertGeminiToolParametersToOpenAI → 8 pass), PR adds +1 test (117→118).
  • Base (PR parent), same test → fails: AssertionError: expected "minLength": "1.5" … received "minLength": 1 — exactly the silent truncation the fix removes.
  • eslint rc=0 · prettier --check rc=0 · tsc --noEmit (core) 0 errors · git diff --check rc=0 · npm ci build rc=0.

3. Real-data A/B on the actual compiled converter ⭐

Ran the compiled convertGeminiToolParametersToOpenAI and the full request-path convertGeminiToolsToOpenAI (PR vs base) on a real Gemini tool schema:

constraint (input) BASE PR
text.minLength "1.5" 1 "1.5"
text.maxLength " " null " "
items.minItems "1e3" 1 1000
nested.deep.minLength "2.9" 2 (recurses) "2.9"
items.maxItems "5" / already 7 5 / 7 5 / 7 (no regression)

And the wire schema the backend actually receives (tools[0].function.parameters.properties):

  • BASEtext.minLength = 1, items.minItems = 1 (silently corrupted)
  • PRtext.minLength = "1.5", items.minItems = 1000 (preserved / correct)

So the fix propagates end-to-end to the OpenAI request, not just the helper. The nested case shows the converter recurses and base corrupted nested constraints too.

Why no live MCP/tmux wire-capture: the request path sends MCP tools' parametersJsonSchema directly (bypassing this converter) and routes only Gemini-format func.parameters through it; built-in Gemini-format tools don't carry string-valued length constraints. So the faithful way to exercise this function is to call it (and the real tools-array builder) directly on representative schemas, which is what the A/B above does.

Conclusion

Tight, well-tested, no regression to valid integers / numbers / numeric constraints, and verified to carry through to the real OpenAI wire schema. LGTM.

🇨🇳 中文版验证报告(点击展开)

✅ 验证报告 — PR #5312(修复 #5310

结论:已验证,建议合并。 小而精准的逻辑修复。我审计了判断逻辑、跑了 converter 测试并与 base 做了 A/B(不打补丁时新测试会失败),并且——最关键的一步——用真实编译后的 converter(PR vs base)在真实 Gemini 工具 schema 上跑了对比,既在函数层面,也在构建发给 OpenAI 后端 tools[] 的完整请求路径上。修复保留了非整数字符串、精确转换真整数,并且不影响数字或数值约束。

PR 做了什么

convertGeminiToolParametersToOpenAI 之前用 !isNaN(Number(v)) 判断、再用 parseInt(v,10) 转换字符串型 minLength/maxLength/minItems/maxItems,于是 "1.5" 被静默写成 1。PR 改为仅当去空白后的字符串是真整数时才转换,且直接用 Number(...)

const numberValue = typeof value === 'string' ? Number(value) : NaN;
if (typeof value === 'string' && value.trim() !== '' && Number.isInteger(numberValue)) {
  result[key] = numberValue;   // 精确值,无 parseInt 截断
} else {
  result[key] = value;          // 原样保留
}

1. 逆向审计 —— 逻辑 + 边界

输入 修复前(parseInt 修复后(Number + isInteger + trim)
"1.5" 1(截断——bug) "1.5" 保留 ✓
" " NaN/0(损坏) " " 保留 ✓(trim 守卫)
"10px" "10px"(Number 失败) "10px" 保留 ✓
"1e3" 1(parseInt 遇 e 停) 1000 ✓ 正确整数
"5" 5 5 ✓ 合法整数仍转换
5(数字) 5 5 ✓ 非字符串透传

minimum/maximum/multipleOf 不受影响(不同分支)。判断顺序正确(先 trim() !== ''Number.isInteger)。

2. 单元测试 + base A/B

  • PRconverter.test.ts131 通过-t convertGeminiToolParametersToOpenAI → 8 通过),PR 新增 +1 个测试(117→118)。
  • Base(PR 父提交)跑同一测试 → 失败AssertionError: expected "minLength": "1.5" … received "minLength": 1 —— 正是修复要消除的静默截断。
  • eslint rc=0 · prettier --check rc=0 · tsc --noEmit(core)0 错误 · git diff --check rc=0 · npm ci 构建 rc=0。

3. 真实数据 A/B —— 真实编译后的 converter ⭐

用真实 Gemini 工具 schema,对比运行编译后的 convertGeminiToolParametersToOpenAI 以及完整请求路径 convertGeminiToolsToOpenAI(PR vs base):

约束(输入) BASE PR
text.minLength "1.5" 1 "1.5"
text.maxLength " " null " "
items.minItems "1e3" 1 1000
nested.deep.minLength "2.9" 2(递归) "2.9"
items.maxItems "5" / already 7 5 / 7 5 / 7(无回归)

以及后端实际收到的线上 schematools[0].function.parameters.properties):

  • BASEtext.minLength = 1items.minItems = 1(被静默破坏)
  • PRtext.minLength = "1.5"items.minItems = 1000(保留 / 正确)

所以修复会端到端传到 OpenAI 请求,而不只是辅助函数。嵌套用例还表明 converter 会递归、base 连嵌套约束也会破坏。

为何没做实时 MCP/tmux 抓包:请求路径会把 MCP 工具的 parametersJsonSchema 直接发送(绕过本 converter),只有 Gemini 格式的 func.parameters 才走它;而内置的 Gemini 格式工具并不携带字符串型长度约束。因此最忠实地触发该函数的方式,就是用代表性 schema 直接调用它(以及真实的 tools 数组构建函数)——也就是上面 A/B 所做的。

结论

逻辑严谨、测试充分、对合法整数 / 数字 / 数值约束无回归,并已验证能传到真实的 OpenAI 线上 schema。LGTM。

Verification methodology: reverse-audit of the integer guard (Number + Number.isInteger + trim, vs parseInt); PR converter suite + base A/B (new test fails on base); and a real-data A/B running the actual compiled convertGeminiToolParametersToOpenAI and the full convertGeminiToolsToOpenAI request-path builder (PR vs base) on real Gemini schemas, comparing both the helper output and the wire tools[].function.parameters.

@wenshao
wenshao merged commit 5a73dcf into QwenLM:main Jun 18, 2026
36 checks passed
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Real-build verification — safe to merge

I built the actual qwen core from this PR's head (e089cc2dac) in an isolated worktree and exercised the compiled converter from the built dist in a tmux pane, A/B against a pre-fix build (same tree, only converter.ts reverted to base). Since this is a core data-transform with no TUI surface, the "real run" is the actual shipped function on its real call path — not keystrokes.

1. Unit tests — 131/131 on PR head

npx vitest run converter.test.ts131 passed.

2. Mutation test — the new test isn't vacuous

Reverted only converter.ts to base (kept this PR's test) and re-ran: exactly 1 test fails, the new one, all 130 others pass:

× OpenAIContentConverter > convertGeminiToolParametersToOpenAI > should not truncate non-integer length constraints
  (130 pre-existing tests still pass)

3. Compiled A/B — real dist, run in tmux

Fed a schema with mixed length constraints through both the changed function and the realistic pipeline one level up (convertGeminiToolsToOpenAI → the final OpenAI tool object):

property input PRE-FIX FIXED (this PR)
badFloat minLength: "1.5" 1 — silently truncated "1.5" — preserved
blank maxLength: " " nullparseInt(" ")→NaN→null " " — preserved
junk minItems: "10px" "10px" "10px" (same)
validStr minLength: "5" 5 5 — valid coercion still works
alreadyNum minLength: 3 3 3 — untouched

The full-pipeline output is identical to the direct one, so the fix survives convertSchema into the final OpenAI tools[].function.parameters.

raw tmux capture (fixed vs pre-fix)
# FIXED
DIRECT  convertGeminiToolParametersToOpenAI:
    badFloat    minLength="1.5" (string)
    blank       maxLength="   " (string)
    junk        minItems="10px" (string)
    validStr    minLength=5 (number)
    alreadyNum  minLength=3 (number)

# PRE-FIX
DIRECT  convertGeminiToolParametersToOpenAI:
    badFloat    minLength=1 (number)
    blank       maxLength=null (number)
    junk        minItems="10px" (string)
    validStr    minLength=5 (number)
    alreadyNum  minLength=3 (number)

Reachability & scope (honest framing)

  • This runs on the Gemini-format tool → OpenAI conversion (convertGeminiToolParametersToOpenAI, called from convertGeminiToolsToOpenAI), i.e. when talking to an OpenAI-compatible endpoint. MCP tools take the separate parametersJsonSchema branch and don't reach this code.
  • Built-in tools declare numeric length constraints, so they're unaffected — which is exactly why there's no regression (numbers and valid integer-strings are untouched / still coerced). The fix is defensive hardening: it stops a Gemini-format tool that declares a string length ("1.5", blank) from being silently corrupted (→ 1, → null) in the outgoing tool schema.

Verdict: LGTM — correct, complete, no regression on numeric/valid constraints. 👍

中文说明(完整对应)

✅ 真实构建验证 —— 可以合并

我在隔离 worktree 里从本 PR 的 head(e089cc2dac)构建了真实的 core,并在 tmux 中对编译后的 dist 转换函数做了 A/B(同一棵树,只把 converter.ts 回退到 base)。本 PR 是无 TUI 的核心数据转换,所以"真实运行"是直接跑实际发布的函数及其真实调用链,而非击键。

1. 单元测试 —— PR head 131/131

npx vitest run converter.test.ts131 passed

2. 变异测试 —— 新测试不是空过场

只把 converter.ts 回退到 base(保留本 PR 的测试)再跑:恰好 1 个测试失败(就是新增那个),其余 130 个全过:

× should not truncate non-integer length constraints
  (130 个既有测试仍然全过)

3. 编译产物 A/B —— 真实 dist,在 tmux 中运行

把一个含多种长度约束的 schema 同时喂给被改函数上一层真实流水线(convertGeminiToolsToOpenAI → 最终 OpenAI tool 对象):

属性 输入 PRE-FIX FIXED(本 PR)
badFloat minLength: "1.5" 1 —— 被静默截断 "1.5" —— 保留
blank maxLength: " " null —— parseInt(" ")→NaN→null " " —— 保留
junk minItems: "10px" "10px" "10px"(一致)
validStr minLength: "5" 5 5 —— 合法字符串照常转成数字
alreadyNum minLength: 3 3 3 —— 不动

整条流水线输出与直接调用一致,说明修复能挺过 convertSchema,进入最终的 OpenAI tools[].function.parameters

可达性与范围(如实说明)

  • 这段代码在 Gemini 格式 tool → OpenAI 的转换上(convertGeminiToolParametersToOpenAI,由 convertGeminiToolsToOpenAI 调用),即对接 OpenAI 兼容端点时。MCP tool 走的是另一条 parametersJsonSchema 分支,不经过这里。
  • 内置 tool 的长度约束都是数字,所以不受影响 —— 这也正是无回归的原因(数字与合法整数字符串都不变 / 照常转换)。本修复是防御性加固:防止某个声明了字符串长度("1.5"、空白)的 Gemini 格式 tool 在外发 schema 中被静默破坏(→ 1→ null)。

结论:LGTM —— 正确、完整,对数字/合法约束无回归。👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category/core Core engine and logic type/bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(core): OpenAI schema converter truncates fractional length constraints

3 participants