Feat: strip sampling params when modelDetails.shouldSkipTemperature is true#74
Merged
cosminacho merged 14 commits intomainfrom Apr 23, 2026
Merged
Feat: strip sampling params when modelDetails.shouldSkipTemperature is true#74cosminacho merged 14 commits intomainfrom
cosminacho merged 14 commits intomainfrom
Conversation
…ipTemperature is true Reasoning-style models (anthropic.claude-opus-4-7) reject any sampling parameter and the gateway returns 400. The discovery endpoint already advertises this via modelDetails.shouldSkipTemperature. We now honor it: - Add model_details: dict | None on UiPathBaseLLMClient. - Lazy-resolve it via client_settings.get_model_info on first _skip_sampling call (backed by the class-cached discovery response). - Strip temperature/top_p/top_k/frequency_penalty/presence_penalty/seed/ logit_bias/logprobs/top_logprobs from invoke kwargs inside UiPathBaseChatModel's _generate/_agenerate/_stream/_astream wrappers. n is intentionally not treated as a sampling knob. - Factory forwards modelDetails to every chat-model constructor so the common path has zero extra network cost. - Each strip logs a warning via self.logger when one is configured. Init-time clearing of already-set instance fields (UiPathChat(temperature=0.5)) is intentionally out of scope for this PR — tracked separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n first invoke Direct instantiation (UiPathChat(model="opus47")) now resolves model_details eagerly during pydantic's post-init hook. get_available_models is class-cached inside the settings layer, so at most one discovery HTTP call fires per process regardless of how many chat models are built. Also guards against overwriting an explicitly-provided model_details (the factory's fast path), and adds a test for that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion to UiPathBaseLLMClient - New _sampling.py module holds DISABLED_SAMPLING_PARAMS, should_skip_sampling, and strip_disabled_sampling_kwargs. Keeps the knowledge of which flags mean what in one place instead of spread across wrappers. - UiPathBaseChatModel's four generate/stream wrappers now delegate to a thin _strip_sampling method that calls the module helper. - model_details resolution moves from UiPathBaseChatModel.model_post_init to a @model_validator(mode="after") on UiPathBaseLLMClient, so embedding wrappers get the same eager-resolve behavior. get_available_models stays class-cached, so this remains at most one network call per process. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… via field_validator
- New src/uipath/llm_client/utils/sampling.py in the core package exports
DISABLED_SAMPLING_PARAMS, should_skip_sampling, and
strip_disabled_sampling_kwargs. These are framework-agnostic and fit the
existing core utils pattern (one file per concern).
- Langchain's utils.py re-exports them, so the public import path
uipath_langchain_client.utils.strip_disabled_sampling_kwargs is preserved.
- model_details resolution is now a @field_validator("model_details",
mode="after") collocated with the field declaration. It reads
already-validated client_settings and model_name off info.data and calls
get_model_info, instead of living in a separate @model_validator method.
- Core version 1.9.9 -> 1.10.0 with changelog entry; langchain's core-dep
floor bumped to >=1.10.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keep field_validator (default_factory with data arg breaks LangChain's internal zero-arg call at langchain_core/utils/pydantic.py). Rename the method to _resolve and inline the fetch so the validator body is one short try/except block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sampling_kwargs calls - Switch back from @field_validator to @model_validator(mode="after") so the resolver uses typed self.* attributes instead of untyped info.data[...] lookups. - Drop the _strip_sampling helper method on UiPathBaseChatModel; call strip_disabled_sampling_kwargs directly in _generate/_agenerate/_stream/ _astream since there's no shared computation to hoist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rt from core directly Callers now import from the core modules where the code actually lives (uipath.llm_client.utils.sampling, uipath.llm_client.utils.model_family) rather than going through the langchain utils.py re-export layer. The re-export for these two was noise — they weren't langchain-specific and the indirection hid the real source. Exceptions and RetryConfig still re-export through langchain utils.py because they're widely re-used across many langchain client files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python 3.11+ is the minimum, so the postponed-evaluation import is noise. Three files had it; none used TYPE_CHECKING-only imports that depended on lazy annotations, so removing it is safe. pyright, ruff, and pytest all remain green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…learing, langchain-openai compatible)
Replace the sampling-specific interface with the langchain-openai
disabled_params convention:
- Core utils: rename strip_disabled_sampling_kwargs -> strip_disabled_kwargs
(operates on {name: None | [values]} spec, same as langchain-openai).
Add disabled_params_from_model_details to derive the spec from a
modelDetails dict (today: shouldSkipTemperature = the full sampling set).
Add is_disabled_value helper.
- UiPathBaseLLMClient: add disabled_params field; swap the
@model_validator(mode="after") for a single @model_validator(mode="before")
that (1) resolves model_details (caller-provided wins, else
client_settings.get_model_info), (2) derives disabled_params when the
caller didn't pass one, (3) pops matching keys from values BEFORE pydantic
field validation. This is the same pattern langchain-openai uses for
o1/GPT-5 temperature at base.py:1093-1123, so init-time clearing happens
without any __pydantic_fields_set__ hackery — the field simply never
enters model_fields_set.
- Runtime stripping in UiPathBaseChatModel's four wrappers delegates to
strip_disabled_kwargs(disabled_params=self.disabled_params, ...).
- Drop the unused disabled_params field declaration on UiPathChat (now
inherited from UiPathBaseLLMClient).
- Tests cover: init-time clearing (fields never enter model_fields_set,
popped from model_kwargs too), user-provided disabled_params wins over
derivation, disabled_params value-list semantics, factory forwarding,
discovery fallback, swallowed errors, and logger-gated warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same observable behavior; the after version is ~9 LOC shorter and uses typed self.* access (self.client_settings.get_model_info(self.model_name, byo_connection_id=self.byo_connection_id)) instead of the mode=before raw-dict alias juggling. The clear step uses object.__setattr__ + self.__pydantic_fields_set__.discard — one line per cleared field — which keeps UiPathChat._default_params's model_fields_set filter happy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…led_params - Rename the hook from _resolve_disabled_params to _finalize_model_metadata to reflect that it owns both model_details and disabled_params resolution (not just the disabled_params side). - Drop the field-clearing (self.temperature = None + __pydantic_fields_set__.discard) and model_kwargs-clearing steps. The validator is now purely data: resolve model_details, derive disabled_params, merge with caller-provided (user keys win). - Runtime invoke-time stripping still handles .invoke(temperature=...). Init-time values set on the instance (UiPathChat(..., temperature=0.5)) remain in model_fields_set and flow into _default_params — tracked as a follow-up in the langchain CHANGELOG. - Tests swap init-time-clearing assertions for merge-semantics cases: derived ∪ user, narrowing-override, no-derivation fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the existing setup_* convention on the other validators (setup_uipath_client, setup_api_flavor_and_version) and ties the name to the get_model_info call it consumes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pins down how our setup_model_info validator composes with
langchain-openai's native disabled_params conventions:
- UiPathChatOpenAI derives disabled_params from model_details
(gateway shouldSkipTemperature) like our normalized UiPathChat does.
- User-supplied disabled_params merge with the derived set; user keys
win on conflict (e.g. narrowing temperature to a value-list spec).
- UiPathAzureChatOpenAI's native auto-init of
{"parallel_tool_calls": None} (for non-gpt-4o models) runs BEFORE our
validator in MRO order. Our merge then treats that as a
caller-provided value and preserves it alongside the derived sampling
set. Neither convention is lost.
- End-to-end: runtime .invoke() strip on UiPathChatOpenAI honors the
merged set — drops both derived sampling kwargs AND
user-supplied parallel_tool_calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w tests Pyright can't see the field-name alias on UiPath*ChatOpenAI constructor calls — it only sees the declared alias "settings". Switch the six new OpenAI interop tests to settings= to match what pyright expects. Runtime behavior is identical (validate_by_name + validate_by_alias both on). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
anthropic.claude-opus-4-7(and other reasoning-style models) reject every sampling parameter. The LLM Gateway returns 400 forllm.invoke(..., temperature=0.2). The discovery endpoint already advertises this viamodelDetails.shouldSkipTemperature, but we weren't reading it.UiPathBaseChatModelnow stripstemperature,top_p,top_k,frequency_penalty,presence_penalty,seed,logit_bias,logprobs,top_logprobsfrom invoke kwargs before forwarding to_uipath_generate/_uipath_agenerate/_uipath_stream/_uipath_astreamwhenmodelDetails.shouldSkipTemperatureis true.nis intentionally not treated as a sampling knob.model_details: dict | Nonefield onUiPathBaseLLMClient.get_chat_modelforwardsmodelDetailsto every chat-model constructor so the common path has zero extra network cost. Direct instantiation lazy-resolves viaclient_settings.get_model_infoon the first_skip_samplingcall (get_available_modelsis already class-cached inside the settings layer, so at most one discovery call per process).WARNINGviaself.loggerwhen one is configured; silent otherwise.uipath-langchain-client1.9.9 → 1.10.0). Core untouched.Out of scope: clearing sampling fields that were set at instantiation time (
UiPathChat(model="opus47", temperature=0.5)) — the cleanest mechanism for that is still under discussion. Tracked for a follow-up.Test plan
tests/langchain/test_disabled_sampling_params.py— 13 new mocked unit tests (no HTTP): sync/async strip, non-samplingnpreserved, pass-through when flag absent/false, warning gated byself.logger, lazy resolution populates and swallows discovery errors, factory forwarding.ruff check && ruff format --check .— green.pyright— 0 errors.pytest tests— 1535 passed, 0 failed.🤖 Generated with Claude Code