fix(openai): preserve descriptionless tools#6243
Conversation
|
Thanks for the PR! Template looks good ✓ On direction: This is a straightforward compatibility fix — tools registered without a description were being silently dropped from OpenAI requests, which is clearly wrong. The existing codebase already has test fixtures that create descriptionless tool declarations (e.g., On approach: The scope is tight — 2 lines changed in production code, test updated to match. This is exactly the minimal fix needed. The Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 方向:这是一个直接的兼容性修复——没有 description 的工具在构造 OpenAI 请求时被静默丢弃,显然是错误的行为。代码库中已有测试 fixture 创建了无 description 的工具声明(如 方案:范围紧凑——生产代码改 2 行,测试同步更新。这正是所需的最小修复。 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
2a. Code ReviewIndependent proposal (before reading the diff): The Comparison with PR: The PR's implementation matches this proposal exactly — two-line change in production code ( Reuse check: No new logic introduced — just a guard relaxation and nullish coalescing. Nothing to extract or consolidate. Verdict: No blockers. The change is correct, minimal, and well-tested. 2b. Real-Scenario TestingThis is a non-UI converter fix — the behavioral proof is the unit test suite. Ran the focused Before (main — old test expects descriptionless tools to be dropped)Old test title: "should skip functions without name or description" — expects After (PR #6243 — updated test expects descriptionless tools to be preserved)Updated test now asserts Testing verdict: ✅ Behavior change confirmed — descriptionless tools are now preserved as expected. — Qwen Code · qwen3.7-max |
|
Stepping back: this is a clean, correct fix for a real bug. Tools without descriptions were silently dropped from OpenAI requests — that's data loss at the API boundary, and the existing codebase already creates descriptionless tool declarations in its own test fixtures ( The implementation is exactly what I would have written: relax the guard to If I had to maintain this in six months, I'd thank the author for keeping it minimal. Approving. ✅ 中文说明总结:这是一个干净、正确的真实 bug 修复。没有 description 的工具在构造 OpenAI 请求时被静默丢弃——这是 API 边界的数据丢失,而且代码库自身的测试 fixture( 实现正是我会写的方式:将守卫放宽到仅需 如果六个月后要维护这段代码,我会感谢作者保持了最小化。 批准。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
|
||
| expect(result).toHaveLength(1); | ||
| expect(result).toHaveLength(2); | ||
| expect(result[0].function.name).toBe('valid_tool'); |
There was a problem hiding this comment.
[Suggestion] The test asserts result[0].function.name but not result[0].function.description. Without that assertion, a regression that clobbers every description to '' would pass undetected.
| expect(result[0].function.name).toBe('valid_tool'); | |
| expect(result[0].function.name).toBe('valid_tool'); | |
| expect(result[0].function.description).toBe('A valid tool'); |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Thanks for the careful catch. I added the missing assertion so the regression test now verifies both sides of the behavior: existing descriptions stay preserved, while descriptionless named tools are serialized with an empty description.
Validation run in WSL2 Ubuntu-22.04:
npm exec -- vitest run src/core/openaiContentGenerator/converter.test.ts -t convertGeminiToolsToOpenAI
# 7 passed | 138 skipped (145)
npm exec -- vitest run src/core/openaiContentGenerator/converter.test.ts
# 145 passed (145)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Clean, minimal fix — the guard relaxation and nullish coalescing fallback are correct and well-tested. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Clean, minimal fix — the guard relaxation and nullish coalescing fallback are correct and well-tested. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
Clean, minimal fix — the guard relaxation from func.name && func.description to func.name correctly preserves descriptionless tools in the OpenAI payload, and the ?? '' fallback is sound. Test coverage is adequate with 135/135 passing. CI 21/21 green. LGTM! ✅
— qwen3.7-max via Qwen Code /review
✅ Maintainer verification — real-binary E2E confirms the fixI verified this PR by driving the real bundled Verdict: the fix does exactly what it claims. With the base guard Why this is a faithful real-world reproductionThe descriptionless shape is genuinely reachable end-to-end, not a synthetic unit-test input:
A/B result (only variable = the 2-line converter guard)
The two binaries differ only in the converter guard — confirmed by grepping the flipped text in both Hard evidence (captured, not narrated)Descriptionless tool as serialized into the FIXED request payload: {"type":"function","function":{"name":"mcp__descserver__read_nodesc","description":"","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}}Genuine execution (not fabricated): a nonce known only to the MCP server (injected via its env, never in the prompt or the fake) round-trips back through the model request on the FIXED binary:
Unit tests (independently re-run at head): Scope / notes for merge
🇨🇳 中文版(完整对应)✅ 维护者验证 —— 真实二进制 E2E 确认修复有效我通过在 tmux 中驱动真实打包的 结论:修复完全实现了它所声明的效果。 在旧 guard (截图见上方英文部分) 为什么这是一次忠实的真实场景复现「无 description」这个形态是真实可达的端到端路径,而不是人为构造的单测输入:
A/B 结果(唯一变量 = 那两行 converter guard)
两个二进制仅在 converter guard 上不同 —— 已通过在 硬证据(捕获的,而非叙述的)FIXED 请求 payload 中序列化出来的无 description 工具: {"type":"function","function":{"name":"mcp__descserver__read_nodesc","description":"","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}}真实执行(非伪造): 一个只有 MCP server 知道的 nonce(通过它的 env 注入,从不出现在 prompt 或伪端点里)在 FIXED 二进制上经由模型请求回传:
单元测试(在 head 独立重跑): 合并相关的范围 / 说明
Verified locally with the real bundled binary + a real stdio MCP server + fake OpenAI endpoint; mutation A/B isolates the two-line converter change. Reproduction harness available on request. |

What this PR does
This PR keeps valid tools visible to OpenAI-compatible providers even when a Gemini/MCP function declaration does not include a description. The converter now requires a tool name before serializing the OpenAI tool entry, while preserving description text when present and using an empty string when it is omitted.
Why it's needed
What Problem This Solves
A registered tool can disappear before it reaches an OpenAI-compatible model when its Gemini/MCP function declaration has a
nameand parameters, but no description text. That is a believable shape for MCP servers or custom tool adapters because some upstream tool contracts treat human-readable descriptions as optional metadata, while the executable identity of the tool is carried by its name and input schema.For example, a declaration like this is still enough for Qwen Code to identify a callable tool:
Before this change,
convertGeminiToolsToOpenAI()only serialized a Gemini function declaration when the guardfunc.name && func.descriptionpassed. That meant the declaration above could be accepted by the tool-registration side, converted into a Gemini-stylefunctionDeclaration, and then silently dropped during OpenAI request construction solely becausedescriptionwas missing.Changes
This PR keeps the behavior narrow. Declarations with a valid
nameand a description continue to serialize as before. Declarations without anameare still skipped because they cannot produce a callable OpenAI tool identity.The only changed case is a named declaration whose description is absent: instead of removing that tool, the converter preserves it in the OpenAI tool list with an empty description string.
Evidence
The focused regression test constructs three declarations: one normal tool with a description, one valid tool without a description, and one invalid declaration without a name. The expected output keeps the first two tools and still skips the nameless declaration.
That test directly covers the old failure condition:
func.name && func.descriptionused to filter out the descriptionless named tool before it reached the OpenAI request payload.Possible call chain / impact
At runtime, this can look like a provider or model tool-calling issue: the tool exists inside Qwen Code, but the OpenAI-compatible request contains a smaller tool list than the user actually configured. The model then has no opportunity to select the missing tool.
Reviewer Test Plan
How to verify
Run the focused converter tests. The updated regression constructs three declarations: one normal tool with a description, one valid tool without a description, and one invalid declaration without a name. The expected output keeps the first two tools and still skips the nameless declaration.
Evidence (Before & After)
N/A - non-UI converter behavior. The before/after proof is covered by the focused unit test in
packages/core/src/core/openaiContentGenerator/converter.test.ts.Tested on
Environment (optional)
Windows 11, local Node/npm environment.
npm exec -- vitest run src/core/openaiContentGenerator/converter.test.ts -t "convertGeminiToolsToOpenAI"passed inpackages/core.npm exec -- vitest run src/core/openaiContentGenerator/converter.test.tsalso passed.Linux validation was also run through WSL2 Ubuntu-22.04 (
Linux 6.6.114.1-microsoft-standard-WSL2 x86_64, Nodev24.15.0, npm11.12.1):npm run typecheckwas run after rebasing this branch onto latestupstream/main; it reports unrelated repository-wide failures that are not caused by this PR's changed converter files.Risk & Scope
Linked Issues
N/A - no issue filed for this small compatibility fix.
中文说明
What this PR does
这个 PR 让 Gemini/MCP function declaration 即使没有 description,也仍然能作为有效工具暴露给 OpenAI-compatible providers。转换器现在只要求工具有 name 才会序列化 OpenAI tool entry;如果 description 存在则保留,如果缺失则使用空字符串。
Why it's needed
What Problem This Solves
当一个 Gemini/MCP function declaration 有
name和参数 schema,但没有 description 文本时,已注册的工具可能会在发送给 OpenAI-compatible model 之前消失。这个输入形态是现实可信的:MCP server 或 custom tool adapter 的上游 tool contract 可能会把人类可读的 description 当作可选元数据,而工具真正可执行的身份通常由 name 和 input schema 承载。例如,下面这样的 declaration 已经足够让 Qwen Code 识别一个可调用工具:
在这个改动之前,
convertGeminiToolsToOpenAI()只有在func.name && func.description这个 guard 通过时,才会把 Gemini function declaration 序列化成 OpenAI tool。这意味着上面的 declaration 可能已经被工具注册侧接受,并转换成 Gemini 风格的functionDeclaration,但在构造 OpenAI request 时,仅仅因为缺少description就被静默丢弃。Changes
这个 PR 的行为边界很窄。带有有效
name和 description 的 declaration 仍然按原逻辑序列化。缺少name的 declaration 仍然会被跳过,因为它无法形成可调用的 OpenAI tool identity。唯一改变的是:当 declaration 有 name 但没有 description 时,converter 不再移除这个工具,而是在 OpenAI tool list 中保留它,并使用空字符串作为 description。
Evidence
focused regression test 构造了三个 declaration:一个带 description 的正常工具、一个没有 description 但合法的工具、以及一个没有 name 的无效 declaration。期望输出保留前两个工具,同时仍然跳过没有 name 的 declaration。
这个测试直接覆盖旧失败条件:
func.name && func.description过去会在工具进入 OpenAI request payload 之前过滤掉没有 description 的 named tool。Possible call chain / impact
在运行时,这会看起来像 provider 或模型的 tool-calling 问题:工具在 Qwen Code 内部已经存在,但 OpenAI-compatible request 里的工具列表比用户实际配置的更少。模型自然也就没有机会选择这个缺失的工具。
Reviewer Test Plan
How to verify
运行 focused converter 测试。更新后的回归测试构造了三个 declaration:一个带 description 的正常工具、一个没有 description 但合法的工具、以及一个没有 name 的无效 declaration。期望输出保留前两个工具,同时仍然跳过没有 name 的 declaration。
Evidence (Before & After)
N/A - 这是非 UI 的转换逻辑。before/after 证据由
packages/core/src/core/openaiContentGenerator/converter.test.ts中的 focused unit test 覆盖。Tested on
Environment (optional)
Windows 11,本地 Node/npm 环境。已在
packages/core下通过npm exec -- vitest run src/core/openaiContentGenerator/converter.test.ts -t "convertGeminiToolsToOpenAI"。也通过了npm exec -- vitest run src/core/openaiContentGenerator/converter.test.ts。也通过 WSL2 Ubuntu-22.04 进行了 Linux 验证(
Linux 6.6.114.1-microsoft-standard-WSL2 x86_64,Nodev24.15.0,npm11.12.1):本分支基于最新
upstream/main后运行了npm run typecheck;它报告仓库级无关失败,这些失败不是由本 PR 修改的 converter 文件引起。Risk & Scope
Linked Issues
N/A - 这个小型兼容性修复目前没有对应 issue。