Repository navigation
fix(eval): raise the simulator's output token limit and make its tool schema work with Gemini - #1974
Conversation
cfdc67d to
4951c90
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Schemas combining a multi-type list with an existing anyOf still produce requests Gemini rejects, and the new test incorrectly asserts that incompatible payload.
1 open issue
What changed in this PR
This PR improves tool simulation and input generation in the UiPath evaluation framework by adapting schemas for Gemini and increasing simulation output-token limits.
Changes:
- Normalize JSON Schema type lists for Gemini while leaving other providers unchanged.
- Default simulation calls to 16,384 tokens, cap inherited agent limits, and preserve explicit model settings across evaluation and CLI paths.
- Add regression tests and bump
uipathto 2.15.1.
| File | Description |
|---|---|
packages/uipath/uv.lock |
Synchronizes the package version to 2.15.1. |
packages/uipath/tests/cli/eval/mocks/test_structured_output.py |
Tests Gemini schema conversion and provider isolation. |
packages/uipath/tests/cli/eval/mocks/test_simulation_model.py |
Tests token defaults, caps, metadata extraction, and request payloads. |
packages/uipath/src/uipath/eval/runtime/runtime.py |
Applies agent-derived limits to tool simulation and input generation. |
packages/uipath/src/uipath/eval/mocks/_structured_output.py |
Converts type lists before Gemini forced-tool calls. |
packages/uipath/src/uipath/eval/mocks/_simulation_model.py |
Defines token defaults and agent-settings helpers. |
packages/uipath/src/uipath/eval/mocks/_mock_runtime.py |
Threads agent token limits through simulation configuration. |
packages/uipath/src/uipath/_cli/cli_run.py |
Passes agent token limits into run simulations. |
packages/uipath/src/uipath/_cli/cli_debug.py |
Passes agent token limits into debug simulations. |
packages/uipath/pyproject.toml |
Bumps the package version to 2.15.1. |
Review effort: Balanced
| elif "anyOf" not in converted: | ||
| del converted["type"] | ||
| converted["anyOf"] = [{"type": t} for t in non_null] |
There was a problem hiding this comment.
Fixed in ad96246. When a node has both, the listed types now apply to the anyOf branches: a branch without a type is copied once per type, and a typed branch is kept only when the list allows its type. No type list is left. The test now asserts that shape, and a new test checks that no type list reaches the Gemini tool schema. Checked against Gemini: the old shape returns the same 400, and the new one returns the tool call.
… schema work with Gemini
4951c90 to
c28d874
Compare
|




Problem
Agent evals that simulate tools failed with Gemini models in the tool simulator: the LLM call in
mocks/_llm_mocker.pythat generates the simulated tool response with the agent's model. There were two causes.Type lists in tool schemas. MCP servers write nullable fields as
"type": ["string", "null"]. The simulator sends that schema as the parameters of its forcedsubmit_tool_responsetool, and Gemini rejects it:Gemini's function declarations take a single
typeand mark nullable fields withnullable: true. OpenAI and Anthropic accept type lists.Output token limit. The simulator never sets
max_tokens, so every simulation call uses the chat service default of 4096. Gemini counts its reasoning tokens against that limit. When simulating a tool response of about 3,000 tokens, the failed calls used 2,600–3,900 reasoning tokens and returned no output. The run then failed withValueError: LLM response contained no tool calls (content=None). Replaying the same request withmax_tokens: 16384used 5,658 tokens (2,657 reasoning + 3,001 output) and returned the expected response.Other reasoning models also count reasoning against this limit. gpt-6-luna's simulation calls in Agent Builder evals reach about 2,700 tokens today, close to the 4096 limit.
Fix
_structured_output.py:GeminiStructuredOutputrewrites the schema with_to_gemini_schema()before the forced tool call. Other providers are unchanged.{"type": [X, "null"]}becomes{"type": X, "nullable": true}.anyOfof single types.["null"]alone becomes{"nullable": true}.It walks nested schemas (
properties,items,anyOf,$defs, ...). It leaves data keywords (default,enum,const,examples,required) untouched and does not modify the caller's schema._simulation_model.py: sets themax_tokensof every simulation call.maxTokensin the model settings is kept.get_agent_max_tokens()reads the agent's value fromschema.metadata["settings"], next to the model.Call sites: tool simulation and input generation in
runtime/runtime.py, anduipath run/uipath debugsimulation throughbuild_mocking_contextandload_simulation_config. Both gain an optionalagent_max_tokensparameter.Placement of
get_agent_max_tokens: it lives inmocks/_simulation_model.py, not ineval/helpers.pynext toget_agent_model.cli_run.pyalready importsuipath.eval.mocks, while importinguipath.eval.helperswould add about 64 ms and 44 modules to everyuipath run.Tests
test_structured_output.py:anyOf; a property nameddefault;enumuntouched.anyOfis kept.test_simulation_model.py:max_tokens.max_tokensandmaxTokensfrom the schema metadata.max_tokensin the request body of an end-to-end tool simulation.uv run pytest(2715 passed),ruff check,ruff format --checkandmypy src testsare clean. Version bumped to 2.15.1.Notes
nullable: true, and the 16384 replay above returned the expected response. This PR has unit tests only. TheanyOfcase for several types has not been checked against Gemini.test_tool_simulation_sends_env_modelerrors at teardown when run alone, on an unregisteredGET …/llm/api/discoveryrequest. It does the same on main and passes in a full run.uipathpin bumped to pick this up.