Module: Rubino::LLM::ThinkingSupport

Defined in:
lib/rubino/llm/thinking_support.rb

Overview

Session-scoped memory of providers that rejected an Anthropic-style thinking budget, plus the detector for that rejection (#75), plus the static per-provider capability gate (#2).

Process-level (not per-adapter) because Lifecycle rebuilds the adapter every turn — and one CLI process serves one chat session, so this is exactly “remember for the session”. RubyLLMAdapter consults it before rendering a budget and marks it on a recognised rejection, so the provider is never sent a budget again this session.

Class Method Summary collapse

Class Method Details

.budget_via_params?(provider_cfg, chat) ⇒ Boolean

providers.<name>.supports_thinking: true is the user’s explicit promise that the backend accepts an Anthropic-style thinking block. ruby_llm 1.16 only renders with_thinking when the model’s REGISTRY entry declares a budget_tokens reasoning option; an assume-model-exists model (MiniMax-M3 on the anthropic-compatible path) declares none, so with_thinking raised client-side before any request, the #75 rejection detector matched the message, and the documented opt-in silently died every turn (#175). On that path the adapter puts the wire payload on with_params instead, which ruby_llm deep-merges into the request body unconditionally.

Returns:

  • (Boolean)


50
51
52
53
54
55
56
57
# File 'lib/rubino/llm/thinking_support.rb', line 50

def budget_via_params?(provider_cfg, chat)
  return false unless provider_cfg["supports_thinking"] == true

  model = chat.respond_to?(:model) ? chat.model : nil
  !(model.respond_to?(:reasoning_option) && model.reasoning_option("budget_tokens"))
rescue StandardError
  true
end

.mark_unsupported!(provider, notify: nil) ⇒ Object

Records the rejection and tells the user once with a dim note (only the marking path emits it). Cosmetic: a UI failure must never break the retried turn.



62
63
64
65
66
67
# File 'lib/rubino/llm/thinking_support.rb', line 62

def mark_unsupported!(provider, notify: nil)
  @unsupported[provider.to_s] = true
  notify&.note("provider doesn't support thinking — effort off")
rescue StandardError
  nil
end

.rejection?(error) ⇒ Boolean

True when error reads as a provider’s “thinking (budget) is not supported” rejection. Kept narrow: the message must name thinking plus a not-supported phrasing.

Returns:

  • (Boolean)


77
78
79
80
81
# File 'lib/rubino/llm/thinking_support.rb', line 77

def rejection?(error)
  msg = error.message.to_s.downcase
  msg.include?("thinking") &&
    (msg.include?("not support") || msg.include?("unsupported"))
end

.reset!Object

Test seam: forget all recorded rejections (a fresh “session”).



70
71
72
# File 'lib/rubino/llm/thinking_support.rb', line 70

def reset!
  @unsupported = {}
end

.supports?(provider_cfg, model_id) ⇒ Boolean

Per-provider thinking CAPABILITY gate (#2). #unsupported?/#rejection? (#75) handle a provider that REJECTS a budget (hard 400 → retry + session memo); this handles one that ACCEPTS it and then, lacking a separate reasoning channel, dumps its chain-of-thought as plain content deltas — observed live on MiniMax. providers.<name>.supports_thinking (true/false) is the explicit override; unset, MiniMax-family model ids default to false (they return no thinking blocks and leak reasoning when sent a budget), everything else to true.

Returns:

  • (Boolean)


33
34
35
36
37
38
# File 'lib/rubino/llm/thinking_support.rb', line 33

def supports?(provider_cfg, model_id)
  configured = provider_cfg["supports_thinking"]
  return configured unless configured.nil?

  !model_id.to_s.match?(ProviderResolver::PROVIDER_PATTERNS["minimax"])
end

.unsupported?(provider) ⇒ Boolean

Returns:

  • (Boolean)


21
22
23
# File 'lib/rubino/llm/thinking_support.rb', line 21

def unsupported?(provider)
  @unsupported.key?(provider.to_s)
end