Skip to content

fix(openai): preserve descriptionless tools#6243

Merged
qqqys merged 5 commits into
QwenLM:mainfrom
VectorPeak:codex/descriptionless-openai-tools
Jul 4, 2026
Merged

fix(openai): preserve descriptionless tools#6243
qqqys merged 5 commits into
QwenLM:mainfrom
VectorPeak:codex/descriptionless-openai-tools

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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 name and 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:

{
  name: "read_project_file",
  parametersJsonSchema: {
    type: "object",
    properties: {
      path: { type: "string" }
    },
    required: ["path"]
  }
}

Before this change, convertGeminiToolsToOpenAI() only serialized a Gemini function declaration when the guard func.name && func.description passed. That meant the declaration above could be accepted by the tool-registration side, converted into a Gemini-style functionDeclaration, and then silently dropped during OpenAI request construction solely because description was missing.

Changes

This PR keeps the behavior narrow. Declarations with a valid name and a description continue to serialize as before. Declarations without a name are 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.description used to filter out the descriptionless named tool before it reached the OpenAI request payload.

Possible call chain / impact

MCP/custom tool registration
-> Gemini-style functionDeclaration with name + parameters, but no description
-> OpenAIContentConverter.convertGeminiToolsToOpenAI()
-> guard requires func.name && func.description
-> tool is omitted from the OpenAI request payload
-> model never sees the tool, so it cannot call it

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

OS Status
🍎 macOS N/A
🪟 Windows ✅ tested
🐧 Linux ✅ tested

Environment (optional)

Windows 11, local Node/npm environment. npm exec -- vitest run src/core/openaiContentGenerator/converter.test.ts -t "convertGeminiToolsToOpenAI" passed in packages/core. npm exec -- vitest run src/core/openaiContentGenerator/converter.test.ts also passed.

Linux validation was also run through WSL2 Ubuntu-22.04 (Linux 6.6.114.1-microsoft-standard-WSL2 x86_64, Node v24.15.0, npm 11.12.1):

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)

npm run typecheck was run after rebasing this branch onto latest upstream/main; it reports unrelated repository-wide failures that are not caused by this PR's changed converter files.

Risk & Scope

  • Main risk or tradeoff: Low. Descriptionless tools provide less guidance to the model, but silently removing them is worse than preserving the registered tool contract.
  • Not validated / out of scope: Live provider/API reproduction is not included; this is covered by focused converter unit tests. Tool schema conversion, parameter conversion, callable tool resolution, and request routing are otherwise unchanged.
  • Breaking changes / migration notes: None.

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 识别一个可调用工具:

{
  name: "read_project_file",
  parametersJsonSchema: {
    type: "object",
    properties: {
      path: { type: "string" }
    },
    required: ["path"]
  }
}

在这个改动之前,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

MCP/custom tool registration
-> Gemini-style functionDeclaration with name + parameters, but no description
-> OpenAIContentConverter.convertGeminiToolsToOpenAI()
-> guard requires func.name && func.description
-> tool is omitted from the OpenAI request payload
-> model never sees the tool, so it cannot call it

在运行时,这会看起来像 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

OS Status
🍎 macOS N/A
🪟 Windows ✅ tested
🐧 Linux ✅ tested

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,Node v24.15.0,npm 11.12.1):

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)

本分支基于最新 upstream/main 后运行了 npm run typecheck;它报告仓库级无关失败,这些失败不是由本 PR 修改的 converter 文件引起。

Risk & Scope

  • Main risk or tradeoff: 风险较低。没有 description 的工具对模型提示较少,但保留已注册工具的 contract,比静默移除更合理。
  • Not validated / out of scope: 未包含真实 provider/API 复现;本 PR 使用 focused converter unit tests 覆盖。tool schema conversion、parameter conversion、callable tool resolution 和 request routing 其他行为均不改变。
  • Breaking changes / migration notes: 无。

Linked Issues

N/A - 这个小型兼容性修复目前没有对应 issue。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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., smoke.test.ts uses { name: 'edit' } without a description), confirming this is a real scenario. Aligned.

On approach: The scope is tight — 2 lines changed in production code, test updated to match. This is exactly the minimal fix needed. The func.name guard remains (OpenAI needs a callable name), and ?? '' for the description field is correct per the OpenAI spec where description is optional. Nothing to cut.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是一个直接的兼容性修复——没有 description 的工具在构造 OpenAI 请求时被静默丢弃,显然是错误的行为。代码库中已有测试 fixture 创建了无 description 的工具声明(如 smoke.test.ts 中的 { name: 'edit' }),确认这是真实场景。方向对齐。

方案:范围紧凑——生产代码改 2 行,测试同步更新。这正是所需的最小修复。func.name 守卫保留(OpenAI 需要可调用函数名),description 用 ?? '' 符合 OpenAI 规范(description 为可选字段)。没有多余内容。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

2a. Code Review

Independent proposal (before reading the diff): The convertGeminiToolsToOpenAI function should require only func.name (since OpenAI function tools need a callable name) and treat description as optional, defaulting to empty string. The test should verify three cases: normal tool preserved, descriptionless tool preserved, nameless tool skipped.

Comparison with PR: The PR's implementation matches this proposal exactly — two-line change in production code (func.name && func.descriptionfunc.name, func.descriptionfunc.description ?? ''), and the test now covers all three cases correctly. Nothing missed, nothing extraneous.

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 Testing

This is a non-UI converter fix — the behavioral proof is the unit test suite. Ran the focused convertGeminiToolsToOpenAI tests on both main and the PR branch.

Before (main — old test expects descriptionless tools to be dropped)

=== BEFORE (main) ===

 RUN  v3.2.4 /home/github-runner/actions-runner-7/_work/qwen-code/qwen-code/packages/core
      Coverage enabled with v8

 ✓ src/core/openaiContentGenerator/converter.test.ts (145 tests | 144 skipped) 3ms

 Test Files  1 passed (1)
      Tests  1 passed | 144 skipped (145)
   Start at  15:27:30
   Duration  5.68s (transform 2.21s, setup 19ms, collect 3.43s, tests 3ms, environment 0ms, prepare 98ms)

Old test title: "should skip functions without name or description" — expects toHaveLength(1), meaning the descriptionless tool was silently dropped.

After (PR #6243 — updated test expects descriptionless tools to be preserved)

=== AFTER (PR #6243) ===

 RUN  v3.2.4 /home/github-runner/actions-runner-7/_work/qwen-code/qwen-code/packages/core
      Coverage enabled with v8

 ✓ src/core/openaiContentGenerator/converter.test.ts (145 tests | 138 skipped) 7ms

 Test Files  1 passed (1)
      Tests  7 passed | 138 skipped (145)
   Start at  15:29:00
   Duration  10.67s (transform 4.34s, setup 34ms, collect 7.03s, tests 7ms, environment 0ms, prepare 199ms)

Updated test now asserts toHaveLength(2) — the descriptionless tool is preserved with description: ''. All 7 convertGeminiToolsToOpenAI tests pass.

Testing verdict: ✅ Behavior change confirmed — descriptionless tools are now preserved as expected.

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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 (smoke.test.ts), so this isn't a hypothetical edge case.

The implementation is exactly what I would have written: relax the guard to func.name only, default description to ''. Two production lines, one test update. No drive-by refactors, no scope creep, no unnecessary abstractions. The before/after test run confirms the behavioral change.

If I had to maintain this in six months, I'd thank the author for keeping it minimal.

Approving. ✅

中文说明

总结:这是一个干净、正确的真实 bug 修复。没有 description 的工具在构造 OpenAI 请求时被静默丢弃——这是 API 边界的数据丢失,而且代码库自身的测试 fixture(smoke.test.ts)已经创建了无 description 的工具声明,所以这不是假设性的边界情况。

实现正是我会写的方式:将守卫放宽到仅需 func.name,description 默认为 ''。生产代码 2 行,测试 1 处更新。没有夹带重构,没有范围蔓延,没有不必要的抽象。前后测试运行确认了行为变化。

如果六个月后要维护这段代码,我会感谢作者保持了最小化。

批准。✅

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. ✅


expect(result).toHaveLength(1);
expect(result).toHaveLength(2);
expect(result[0].function.name).toBe('valid_tool');

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.

[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.

Suggested change
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 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.

No issues found. LGTM! ✅

Downgraded from Approve to Comment: CI still running.

— qwen3.7-max via Qwen Code /review

@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.

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 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.

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 doudouOUC 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.

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

@wenshao

wenshao commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — real-binary E2E confirms the fix

I verified this PR by driving the real bundled qwen binary (dist/cli.js, built from PR head 23eb546b78 via npm ci && npm run build && npm run bundle) end-to-end in tmux against a fake OpenAI‑compatible endpoint, with a real stdio MCP server as the descriptionless‑tool source. I then ran a mutation A/B that reverts exactly this PR's two converter lines to prove causality.

Verdict: the fix does exactly what it claims. With the base guard func.name && func.description, a descriptionless MCP tool is silently dropped from the OpenAI request payload before the model ever sees it. With this PR's guard func.name, the tool is preserved (description: "") and becomes callable end-to-end. The control tool (with a description) is present in both runs, so this is not a general MCP breakage — it is specifically the descriptionless case.

PR #6243 real-binary E2E

Why this is a faithful real-world reproduction

The descriptionless shape is genuinely reachable end-to-end, not a synthetic unit-test input:

MCP tools/list  (tool has name + inputSchema, NO description key)
  → @google/genai mcpToTool()      → funcDecl.description === undefined   [verified empirically]
  → mcp-client.ts:1039             → DiscoveredMCPTool(…, description ?? '')  → description = ''
  → tools.ts:231 schema getter     → { name, description: '', parametersJsonSchema }
  → pipeline.ts:539                → convertGeminiToolsToOpenAI(request.config.tools)
  → OLD guard `name && description` → '' is falsy → TOOL DROPPED
  → NEW guard `name`               → TOOL KEPT with description: ''

convertGeminiToolsToOpenAI() is the single chokepoint for every OpenAI-compatible request, so any descriptionless tool — MCP or custom adapter — hits this guard.

A/B result (only variable = the 2-line converter guard)

tool in request[0].tools[] BASE guard name && description PR #6243 name
read_described (has description) — control ✅ present ✅ present
read_nodesc (no description) — the fix dropped ✅ present (description:"")
total tools in payload 16 17

The two binaries differ only in the converter guard — confirmed by grepping the flipped text in both packages/core/dist/.../converter.js and the bundled dist/chunks/*.js. Same MCP server, same fake endpoint, same settings.json, same prompt.

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:

  • MCP server log (FIXED): {"event":"tools/call","name":"read_nodesc","args":{"path":"probe.txt"}}
  • request[1] role:tool message: MCP_TOOL_EXECUTED name=read_nodesc … NONCE=nonce-LIVE-2799Q7
  • On the MUTANT binary the MCP server logs no tools/call — the tool never reached the model, so it could not be called.

Unit tests (independently re-run at head): converter.test.ts135 passed, including the renamed regression should preserve functions without description and skip functions without name.

Scope / notes for merge

  • alwaysLoadTools: true in my test config is observability-only. It puts the MCP tools directly in request[0] instead of behind tool_search; the converter is the same single chokepoint either way, so the fix behaves identically for deferred MCP tools too.
  • Nameless declarations are still skipped — the PR preserves that. (MCP can't produce a nameless tool; qwen already skips those at discovery, so this branch is covered by the unit test, not the E2E.)
  • description: "" is spec-valid for OpenAI-compatible providers (function.description is optional), so no downstream regression. The documented tradeoff (a descriptionless tool gives the model less guidance) is strictly better than silently dropping a tool the user registered.
  • No merge conflicts (MERGEABLE); the change is minimal and low-risk.
🇨🇳 中文版(完整对应)

✅ 维护者验证 —— 真实二进制 E2E 确认修复有效

我通过在 tmux 中驱动真实打包的 qwen 二进制dist/cli.js,由 PR head 23eb546b78npm ci && npm run build && npm run bundle 构建)对一个伪造的 OpenAI 兼容端点做端到端验证,并用一个真实的 stdio MCP server 作为「无 description 工具」的来源。随后做了一次变异 A/B:只把本 PR 改动的两行 converter 代码回退,以证明因果关系。

结论:修复完全实现了它所声明的效果。 在旧 guard func.name && func.description 下,一个没有 description 的 MCP 工具在模型看到它之前就被静默地从 OpenAI 请求 payload 中丢弃。在本 PR 的 guard func.name 下,该工具被保留description: "")并且端到端可调用。带 description 的对照工具在两次运行中都存在,所以这不是 MCP 的整体故障 —— 而是专门针对「无 description」这个场景。

(截图见上方英文部分)

为什么这是一次忠实的真实场景复现

「无 description」这个形态是真实可达的端到端路径,而不是人为构造的单测输入:

MCP tools/list(工具有 name + inputSchema,没有 description 字段)
  → @google/genai mcpToTool()      → funcDecl.description === undefined   【已实测确认】
  → mcp-client.ts:1039             → DiscoveredMCPTool(…, description ?? '')  → description = ''
  → tools.ts:231 schema getter     → { name, description: '', parametersJsonSchema }
  → pipeline.ts:539                → convertGeminiToolsToOpenAI(request.config.tools)
  → 旧 guard `name && description`  → '' 为 falsy → 工具被丢弃
  → 新 guard `name`                → 工具保留,description 为 ''

convertGeminiToolsToOpenAI() 是每一个 OpenAI 兼容请求的唯一必经点,所以任何无 description 的工具(MCP 或自定义适配器)都会命中这个 guard。

A/B 结果(唯一变量 = 那两行 converter guard)

request[0].tools[] 中的工具 旧 guard name && description PR #6243 name
read_described(有 description)—— 对照 ✅ 存在 ✅ 存在
read_nodesc(无 description)—— 本次修复 被丢弃 ✅ 存在(description:""
payload 中工具总数 16 17

两个二进制在 converter guard 上不同 —— 已通过在 packages/core/dist/.../converter.js 和打包产物 dist/chunks/*.js 中 grep 翻转后的文本确认。MCP server、伪端点、settings.json、prompt 全部相同。

硬证据(捕获的,而非叙述的)

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 二进制上经由模型请求回传:

  • MCP server 日志(FIXED):{"event":"tools/call","name":"read_nodesc","args":{"path":"probe.txt"}}
  • request[1]role:tool 消息:MCP_TOOL_EXECUTED name=read_nodesc … NONCE=nonce-LIVE-2799Q7
  • 在 MUTANT 二进制上 MCP server 没有 tools/call 日志 —— 工具从未到达模型,因此无法被调用。

单元测试(在 head 独立重跑):converter.test.ts135 通过,含被重命名的回归用例 should preserve functions without description and skip functions without name

合并相关的范围 / 说明

  • 测试配置中的 alwaysLoadTools: true 仅用于可观测性。 它让 MCP 工具直接进入 request[0],而不是藏在 tool_search 后面;converter 是同一个唯一必经点,因此对延迟加载的 MCP 工具,修复行为完全一致。
  • 无 name 的声明仍然被跳过 —— 本 PR 保留了这一点。(MCP 不会产出无 name 的工具;qwen 在发现阶段已经跳过它们,所以这个分支由单测覆盖,而非 E2E。)
  • description: "" 对 OpenAI 兼容 provider 是合法的function.description 是可选的),因此没有下游回归。文档中提到的权衡(无 description 的工具给模型的提示更少)严格优于静默丢弃一个用户已注册的工具。
  • 无合并冲突(MERGEABLE);改动极小且低风险。

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.

@qqqys
qqqys enabled auto-merge July 4, 2026 12:47
@qqqys
qqqys added this pull request to the merge queue Jul 4, 2026
Merged via the queue into QwenLM:main with commit bd9bf50 Jul 4, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants