feat(llmhubs): support tool calling and structured output in the Gemi…#45
feat(llmhubs): support tool calling and structured output in the Gemi…#45xinkuleee wants to merge 2 commits into
Conversation
| outputs.append( | ||
| OutputItem( | ||
| type="function_call", | ||
| call_id=_synthesize_call_id(name, index), |
There was a problem hiding this comment.
Blocking: This discards Gemini's actual functionCall.id and unconditionally replaces it with an internal synthetic ID. Gemini 3 returns a unique ID for each function call, and the exact ID must be echoed in the corresponding function response. This is also required to disambiguate parallel calls to the same function. Please preserve function_call["id"] when present and use synthesis only as a legacy fallback.
There was a problem hiding this comment.
Fixed. _parse_response now preserves Gemini's id when present and only synthesizes as a legacy fallback:
call_id = function_call.get("id") or _synthesize_call_id(name, index)
The real id is carried end-to-end and echoed in the matching functionResponse, so parallel calls to the same function stay disambiguated. Covered by test_parse_response_preserves_real_call_id_and_part and test_parse_response_synthesizes_call_id_when_missing.
| return mapping | ||
|
|
||
|
|
||
| def _content_to_part(c: InputContent, call_id_to_name: dict[str, str]) -> dict[str, Any] | None: |
There was a problem hiding this comment.
The provider call ID is omitted from both the reconstructed functionCall and the functionResponse. For calls where Gemini returned an ID, the original ID needs to survive the full round trip and be included in the matching functionResponse. Sending only the function name is ambiguous when the same function is called more than once. Please add an end-to-end test covering response parsing, tool execution, and next-turn request construction with a real Gemini call ID.
There was a problem hiding this comment.
Fixed — the provider id now survives the full round trip. _build_call_id_realid_map recovers each call's real id from the stored Part, and _content_to_part adds id to the functionResponse (omitted for synthesized ids so a fabricated id is never sent back).
Added tests: test_parallel_same_function_calls_stay_distinct asserts each functionResponse carries the matching id for two same-named parallel calls, and test_chat_client_round_trips_provider_metadata exercises the parse → Content → next-turn request path.
| return contents | ||
|
|
||
| @staticmethod | ||
| def _parse_response(data: dict[str, Any]) -> Response: |
There was a problem hiding this comment.
This conversion drops the Part-level thoughtSignature. Because this adapter manually reconstructs conversation history, the signature cannot be returned in its original function-call Part on the next request. Gemini 2.5 returns thought signatures when thinking and function calling are enabled, and Gemini 3 requires function-call signatures; omitting them can result in MISSING_THOUGHT_SIGNATURE. Please preserve this opaque metadata through the internal content types and replay it unchanged. A two-turn round-trip test with thoughtSignature should be added.
There was a problem hiding this comment.
Fixed via an opaque, namespaced provider_metadata passthrough. _parse_response stores Gemini's original Part under provider_metadata["gemini"]["part"]; the shared layers only transport it through Content.additional_properties and never interpret it; _content_to_part replays the original Part verbatim, so thoughtSignature (and functionCall.id) survive unchanged. Falls back to reconstructing from neutral fields only when no original Part is available (non-Gemini / cross-turn history).
Two-turn round-trip covered by test_content_to_part_replays_original_part_verbatim and test_chat_client_round_trips_provider_metadata.
…a provider_metadata
Summary
Adds Google Gemini function/tool calling and JSON-schema structured output to the
LLMHub Gemini adapter, so Gemini models can drive the chat agent (tools) and
structured routing (intent check).
tools/function_call/function_resultto GeminifunctionDeclarations/functionCall/functionResponse(role mappingassistant→model/tool→user, andcall_id↔function-name correlation since Gemini responses carry no call id);parse Gemini
functionCallback intofunction_calloutputs.OpenAPI-subset — inline
$ref/$defs, dropadditionalProperties/$schema,flatten single-element
allOf, and convert OptionalanyOf+{"type":"null"}to
nullable.response_format.type=json_schemato GeminiresponseMimeType+responseSchema(fixes chat intent-check routing on Gemini).supports_tools/supports_structured_outputfor the Gemini provider (core
io_profilederivation + backend equivalent).sanitizer (refs/defs/additionalProperties/allOf/anyOf-null/recursion), and
structured output.
Validation
make precommit-runcd backend && go test ./...cd core && uv run pytestFrontend is untouched, so its build/lint does not apply.
Checklist
CHANGELOG.mdunder## [Unreleased]for user-facing changes.Notes for reviewers
non-streaming path (
generate_streamyields a single chunk). Tool calling works,but without incremental token streaming; a follow-up can add
streamGenerateContent?alt=sse.against the full builtin tool set and the structured intent-check schema.
local default-model switch are gitignored and intentionally excluded from this PR.