fix(llm): omit empty tools and keep tool_choice off the wire (#21)

Co-authored-by: Alice <alice@prismshadow.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-22 18:53:39 +08:00
committed by GitHub
parent 62a81bc712
commit ea0549b8bb
3 changed files with 68 additions and 5 deletions
+14 -4
View File
@@ -1095,11 +1095,21 @@ export function toolDefinitionsToSchemas(tools: ToolDefinition[]): ToolSchema[]
}));
}
/** Pre-builds UniConfig from GenerativeModelConfig (called once at construction time). */
/**
* Pre-builds UniConfig from GenerativeModelConfig (called once at construction time).
*
* When the tool list is empty (connectivity probe, bare/meta LLM, vision describer), `tools`
* is omitted entirely instead of set to `[]`: strict OpenAI-compatible servers (e.g. vLLM)
* reject an empty array with a 400 ("tools must not be an empty array"), and omission is the
* protocol equivalent. `tool_choice` is likewise never set — AgentHub only puts it on the wire
* when UniConfig defines it, and leaving it off preserves the protocol default ("auto" when
* tools are present).
*/
export function buildUniConfig(config: GenerativeModelConfig): UniConfig {
const uniConfig: UniConfig = {
tools: toolDefinitionsToSchemas(config.tools),
};
const uniConfig: UniConfig = {};
if (config.tools.length > 0) {
uniConfig.tools = toolDefinitionsToSchemas(config.tools);
}
if (config.systemPrompt !== undefined) {
uniConfig.system_prompt = config.systemPrompt;
}
+14 -1
View File
@@ -1048,11 +1048,24 @@ describe("config helpers", () => {
expect(cfg.tools).toEqual([{ name: "t", description: "d" }]);
const minimal = buildUniConfig({ modelId: "m", tools: [] });
expect(minimal.tools).toEqual([]);
expect("tools" in minimal).toBe(false);
expect("system_prompt" in minimal).toBe(false);
expect("max_tokens" in minimal).toBe(false);
expect("thinking_level" in minimal).toBe(false);
});
it("omits tools when empty and never sets tool_choice (strict endpoints reject both)", () => {
// Empty tool list (connectivity probe, bare/meta LLM, vision describer): the `tools` key
// must be absent, not `[]` — AgentHub forwards any defined array verbatim, and strict
// OpenAI-compatible servers (e.g. vLLM) reject `tools: []` with a 400.
const empty = buildUniConfig({ modelId: "m", tools: [] });
expect("tools" in empty).toBe(false);
// `tool_choice` must never be set: AgentHub only emits it on the wire when UniConfig
// defines it, and leaving it off preserves the protocol default.
expect("tool_choice" in empty).toBe(false);
const withTools = buildUniConfig({ modelId: "m", tools: [{ name: "t", description: "d" }] });
expect("tool_choice" in withTools).toBe(false);
});
});
describe("isRetryableError", () => {