Skip to content

[issue-7526][BE] fix: support Anthropic adaptive-thinking models as online-rule judges#7531

Merged
Nimrod007 merged 5 commits into
mainfrom
Nimrod007/issue-7526-anthropic-adaptive-thinking-judge
Jul 22, 2026
Merged

[issue-7526][BE] fix: support Anthropic adaptive-thinking models as online-rule judges#7531
Nimrod007 merged 5 commits into
mainfrom
Nimrod007/issue-7526-anthropic-adaptive-thinking-judge

Conversation

@Nimrod007

@Nimrod007 Nimrod007 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Details

Online LLM-as-judge rules failed with Anthropic adaptive-thinking models (claude-sonnet-5, claude-opus-4-7/4-8): the judge-path builder forwarded temperature unconditionally (→ 400 "temperature is deprecated for this model") and never set max_tokens or read the rule's custom_parameters, so thinking consumed the whole budget and the judge returned an empty response (finishReason=LENGTH). API-created rules bypass the FE sanitizer added in OPIK_6244, so the fix must live in the backend.

Changes (backend-only, infrastructure/llm/antropic):

  • AnthropicModelName — new supportsSamplingParams capability flag (default true; false for adaptive-thinking models) + static lookup defaulting unknown models to true.
  • AnthropicClientGenerator.newChatLanguageModel (judge path): gate temperature by model capability and whenever thinking is enabled per-rule via custom_parameters; forward custom_parameters (thinking type/budget, max_tokens) onto the native langchain4j builder; always send max_tokens, defaulting to 4096 and stacking it above any thinking budget so max_tokens > thinking.budget_tokens holds; ignore non-positive token values.

Change checklist

  • User facing
  • Documentation update

Issues

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 4.8
  • Scope: full change (implementation + tests), authored under human direction and review
  • Human verification: diff reviewed; build + targeted unit tests run locally (see Testing)

Testing

  • New AnthropicClientGeneratorTest — 21 cases covering the capability lookup, temperature gating (model + thinking-enabled), max_tokens defaulting/forwarding/budget-clearing, and thinking forwarding. All passing.
  • mvn compile clean; mvn spotless:apply clean.
  • Note: the project targets Java 25; verified locally against a Corretto 25 JDK. The full mvn test-compile is currently broken on pristine main (unrelated JDK-25/Lombok annotation-processing mismatch), so the new test was compiled and run in isolation against the built main classes.

Documentation

No user-facing docs change required. Known follow-up (out of scope): the sibling LlmProviderAnthropicMapper.toCreateMessageRequest (playground / online-inference / proxy path) still forwards temperature unconditionally, so adaptive-thinking models 400 there too for API-created requests; this PR is deliberately scoped to the online-rules judge path per the issue.

🤖 Generated with Claude Code

…online-rule judges

Online LLM-as-judge rules failed with Anthropic adaptive-thinking models
(claude-sonnet-5, claude-opus-4-7/4-8): the judge-path builder forwarded
temperature unconditionally (400 "temperature is deprecated for this model")
and never set max_tokens or read the rule's custom_parameters, so thinking
consumed the whole budget and the judge returned an empty response
(finishReason=LENGTH).

In AnthropicClientGenerator.newChatLanguageModel (the judge path):
- Gate temperature server-side via AnthropicModelName.supportsSamplingParams,
  and also skip it whenever thinking is enabled per-rule via custom_parameters
  (Anthropic rejects sampling params while thinking is on) — covers API-created
  rules that bypass the FE sanitizer.
- Forward custom_parameters (thinking type/budget, max_tokens) onto the native
  langchain4j builder.
- Always send max_tokens, defaulting to the Playground default (4096) and
  stacking it above any thinking budget so max_tokens > budget_tokens holds.
- Ignore non-positive max_tokens/budget_tokens from the bypass path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Nimrod007
Nimrod007 requested a review from a team as a code owner July 20, 2026 10:34
@github-actions github-actions Bot added 🟡 size/M java Pull requests that update Java code Backend tests Including test files, or tests related like configuration. labels Jul 20, 2026
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
☕ spotless — java backend Format Java code 5.53s
Total (1 ran) 5.53s
⏭️ 40 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️

Address baz-reviewer comments on the Anthropic adaptive-thinking judge fix:

- Fail closed for unknown model names in AnthropicModelName.supportsSamplingParams
  (default false) so a new adaptive-thinking model can't reintroduce the temperature
  400 before it is added to the enum; back the lookup with a cached static map instead
  of streaming values() on every call.
- Treat thinking as enabled unless the type is explicitly "disabled" (covers "adaptive"
  and any future type), and decode the thinking block once via parseThinking(), reused
  by both the temperature gate and the builder wiring.
- Clamp max_tokens above the thinking budget even when an explicit max_tokens is set, so
  max_tokens <= budget_tokens can no longer reach Anthropic and 400.
- Rename the headroom test to reflect what it asserts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second round of baz-reviewer feedback on the adaptive-thinking judge fix:

- Default unknown model names back to true in supportsSamplingParams so registry-only
  Anthropic models keep temperature support; adaptive models remain explicitly opted out.
- Treat thinking as enabled only for an explicit non-blank type other than "disabled", so
  an empty/blank thinking block no longer suppresses temperature or shapes max_tokens.
- Forward thinking.budget_tokens only when thinking is enabled, so a budget-only (no type)
  block can't reach Anthropic as a partial config and 400.
- Widen to long before adding headroom in resolveMaxTokens so an extreme budget can't
  overflow max_tokens to a negative int.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hinking case

Harden dropsBudgetWhenThinkingDisabled so a regression that changes max_tokens
(not just the thinking budget) is caught.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Python SDK E2E Tests Results (Python 3.14)

286 tests   283 ✅  3m 46s ⏱️
  1 suites    3 💤
  1 files      0 ❌

Results for commit 07bfed4.

♻️ This comment has been updated with latest results.

@Nimrod007 Nimrod007 added the JC label Jul 22, 2026
@Nimrod007 Nimrod007 changed the title [issue-7526] [BE] fix: support Anthropic adaptive-thinking models as online-rule judges [BUG][BE] fix: support Anthropic adaptive-thinking models as online-rule judges Jul 22, 2026
@Nimrod007 Nimrod007 added JC and removed JC labels Jul 22, 2026
@Nimrod007 Nimrod007 changed the title [BUG][BE] fix: support Anthropic adaptive-thinking models as online-rule judges [Bug][BE] fix: support Anthropic adaptive-thinking models as online-rule judges Jul 22, 2026
@Nimrod007 Nimrod007 added JC and removed JC labels Jul 22, 2026
@Nimrod007 Nimrod007 changed the title [Bug][BE] fix: support Anthropic adaptive-thinking models as online-rule judges [ISSUE-7526][BE] fix: support Anthropic adaptive-thinking models as online-rule judges Jul 22, 2026
@Nimrod007 Nimrod007 changed the title [ISSUE-7526][BE] fix: support Anthropic adaptive-thinking models as online-rule judges [issue-7526][BE] fix: support Anthropic adaptive-thinking models as online-rule judges Jul 22, 2026
*/
private void applyCustomParameters(AnthropicChatModel.AnthropicChatModelBuilder builder,
JsonNode customParameters, ThinkingParams thinking) {
Optional.ofNullable(thinking.type()).ifPresent(builder::thinkingType);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 suggestion | Robustness

thinking.type() is forwarded to builder::thinkingType verbatim, and langchain4j does no client-side validation of the value (it passes straight through to the API). So a rule with an unsupported/typo'd type — "adaptive", "enabld", etc. — will still 400 at Anthropic after temperature has already been silently dropped by the gate above. The gate ("any non-disabled type counts as enabled") is a deliberate forward-compat choice, so this is a fair trade-off rather than a bug — just flagging that the failure surfaces later, at request time, with a less obvious cause. If you wanted to fail fast, you could constrain type to the known set (enabled/disabled) here; otherwise fine to leave as-is.

🤖 Review posted via /review-github-pr

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Leaving as-is — this is the deliberate forward-compat choice: treating any non-disabled type as enabled means new Anthropic thinking types (e.g. adaptive) work without a code change, at the cost of a typo'd type surfacing as an Anthropic 400 at request time rather than a fast local rejection. Given Anthropic keeps evolving these values, we'd rather not hard-code the allowed set here. The tradeoff is on the record — thanks for calling it out.

🤖 Reply posted via /address-github-pr-comments

budgetTokens = budgetNode.asInt();
}

return new ThinkingParams(type != null && !"disabled".equals(type), type, budgetTokens);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question | Scope

The PR description notes the sibling LlmProviderAnthropicMapper.toCreateMessageRequest (playground / online-inference / proxy path) still forwards temperature unconditionally, so adaptive-thinking models 400 there too for API-created requests — deliberately left out of scope here. Just confirming there's a tracked follow-up for that path so it doesn't get lost, since the judge fix and the mapper fix share the same root cause.

🤖 Review posted via /review-github-pr

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and tracked: OPIK-7438https://comet-ml.atlassian.net/browse/OPIK-7438 (assigned to Daniel Dimenshtein). It covers gating temperature/top_p/top_k in LlmProviderAnthropicMapper.toCreateMessageRequest for the playground/inference/proxy path, reusing AnthropicModelName.supportsSamplingParams + the thinking-enabled check, so it doesn't get lost.

🤖 Reply posted via /address-github-pr-comments

* guarantees a {@code max_tokens} is always sent. Anthropic requires max_tokens, and without an explicit
* cap adaptive thinking can consume the whole budget, yielding an empty response (finishReason=LENGTH).
*/
private void applyCustomParameters(AnthropicChatModel.AnthropicChatModelBuilder builder,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 nit | Consistency

This path does bespoke, field-by-field extraction of custom_parameters (thinking.type, thinking.budget_tokens, max_tokens), whereas the OpenAI path (OpikOpenAiChatModel) does a generic JsonNode → Map pass-through. The divergence is justified here — langchain4j's Anthropic builder exposes typed setters and there's the hard max_tokens > budget_tokens invariant to enforce, which a blind pass-through wouldn't handle — so no change needed. Noting it only so the difference is a conscious choice on the record.

🤖 Review posted via /review-github-pr

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — the divergence from the OpenAI map pass-through is intentional: langchain4j's Anthropic builder exposes typed setters (thinkingType/thinkingBudgetTokens/maxTokens) and we need to enforce the max_tokens > budget_tokens invariant, which a blind JsonNode → Map pass-through wouldn't handle. Recording it as a conscious choice.

🤖 Reply posted via /address-github-pr-comments

@JetoPistola

Copy link
Copy Markdown
Contributor

👋 Review summary

What looks good

  • Tight, single-purpose fix with a clear root-cause writeup — gates temperature on both the model capability and per-rule thinking, exactly where the two 400 conditions actually arise.
  • Genuinely defensive edge handling: non-positive max_tokens/budget_tokens are ignored, the max_tokens > budget_tokens invariant is enforced, and the overflow guard (Math.min(Integer.MAX_VALUE, (long) budget + default)) is a nice touch most people would miss.
  • supportsSamplingParams defaulting unknown models to true is the right call — registry-only models keep working instead of silently losing temperature.
  • 21-case test suite covers the capability lookup, both temperature gates, max_tokens defaulting/raising/honoring, and thinking forwarding — including the overflow and blank-type paths.

Overall
The approach is sound and correctly scoped to the online-rules judge path. The bespoke custom_parameters extraction (vs. the OpenAI map pass-through) is justified by langchain4j's typed Anthropic setters and the budget invariant. Nothing blocking.

Inline comments: 1 suggestion, 1 question, 1 nit — all minor, none blocking.

🤖 Review posted via /review-github-pr

@JetoPistola JetoPistola left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving — all three review notes are resolved: the verbatim thinking.type suggestion is a deliberate forward-compat trade-off (on the record), the mapper follow-up is tracked as OPIK-7438, and the bespoke-vs-pass-through nit is acknowledged as a conscious choice. The playground/inference-path fix lands separately under OPIK-7438.

🤖 Review posted via /review-github-pr

@Nimrod007 Nimrod007 removed the JC label Jul 22, 2026
@Nimrod007
Nimrod007 merged commit c6f6cd6 into main Jul 22, 2026
95 of 99 checks passed
@Nimrod007
Nimrod007 deleted the Nimrod007/issue-7526-anthropic-adaptive-thinking-judge branch July 22, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend java Pull requests that update Java code 🟠 size/L tests Including test files, or tests related like configuration.

Projects

None yet

2 participants