{"record":{"id":"171cf9de2d0fdc84","repo":"we-promise/sure","slug":"field-must-be-a-whole-number-minimum","errorCode":null,"errorMessage":"%{field} must be a whole number ≥ %{minimum}.","messagePattern":"%(.+?) must be a whole number ≥ %(.+?)\\.","errorType":"validation","errorClass":"Setting::ValidationError","httpStatus":422,"severity":"error","filePath":"app/controllers/settings/hostings_controller.rb","lineNumber":225,"sourceCode":"\n    if hosting_params.key?(:llm_provider)\n      provider = hosting_params[:llm_provider].to_s\n      if %w[openai anthropic].include?(provider)\n        Setting.llm_provider = provider\n      end\n    end\n\n    LLM_NUMERIC_MINIMUMS.each do |key, minimum|\n      next unless hosting_params.key?(key)\n      raw = hosting_params[key].to_s.strip\n      if raw.blank?\n        Setting.public_send(\"#{key}=\", nil)\n        next\n      end\n      parsed = Integer(raw, 10) rescue nil\n      if parsed.nil? || parsed < minimum\n        label = t(\"settings.hostings.openai_settings.#{key}_label\")\n        raise Setting::ValidationError, t(\".invalid_llm_budget\", field: label, minimum: minimum)\n      end\n      Setting.public_send(\"#{key}=\", parsed)\n    end\n\n    if hosting_params.key?(:external_assistant_url)\n      Setting.external_assistant_url = hosting_params[:external_assistant_url]\n    end\n\n    update_encrypted_setting(:external_assistant_token)\n\n    if hosting_params.key?(:external_assistant_agent_id)\n      Setting.external_assistant_agent_id = hosting_params[:external_assistant_agent_id]\n    end\n\n    update_assistant_type\n\n    redirect_to settings_hosting_path, notice: t(\".success\")\n  rescue Setting::ValidationError => error","sourceCodeStart":207,"sourceCodeEnd":243,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/controllers/settings/hostings_controller.rb#L207-L243","documentation":"The hosting controller validates four numeric LLM settings — llm_context_window (min 256), llm_max_response_tokens (min 64), llm_max_items_per_call (min 1), and ai_response_timeout (min Chat::MIN_RESPONSE_TIMEOUT) — by parsing the raw param with Integer(raw, 10) and comparing against LLM_NUMERIC_MINIMUMS. Anything that strict base-10 Integer() rejects (decimals, exponent notation, thousands separators, hex) parses to nil, and values below the minimum fail the comparison; both raise Setting::ValidationError with '.invalid_llm_budget'. A blank value is valid and clears the setting (sets it to nil), so the error only fires on present-but-wrong input. The constants mirror the min: attributes on the form inputs so server-side rejects what the browser validator would.","triggerScenarios":"Typing 10.5 or 1e3 into the context-window field; entering 100 for llm_context_window (below 256); 32 for llm_max_response_tokens (below 64); 0 for llm_max_items_per_call; \"1,024\" with a comma; \"128 \" is fine after strip but \"12 8\" is not; submitting \"5s\" or \"30sec\" for ai_response_timeout instead of a bare number; a curl/API client bypassing the form's min validation entirely.","commonSituations":"Copy-pasting model spec-sheet numbers like \"200K\" or \"128k tokens\" with a unit suffix; assuming the timeout field takes seconds-with-unit like Docker or nginx configs; automated config management (Ansible/terraform templates) rendering quoted decimals; browser validation skipped because the form was submitted programmatically.","solutions":["Enter a plain base-10 integer with no unit, comma, or decimal point (e.g. 200000 not \"200K\", 30 not \"30s\")","Raise the value to the field minimum: context window ≥ 256, max response tokens ≥ 64, max items per call ≥ 1, response timeout ≥ Chat::MIN_RESPONSE_TIMEOUT","To unset a budget, leave the field empty rather than typing 0 — blank clears the setting, 0 raises","Check app/controllers/settings/hostings_controller.rb:7 LLM_NUMERIC_MINIMUMS for the authoritative floors if the form's error message is unclear"],"exampleFix":"# before\nSetting.llm_context_window = \"128k\"   # or 100, or \"1,024\"\n# => Setting::ValidationError: Context Window must be a whole number ≥ 256.\n\n# after\nSetting.llm_context_window = 128000    # plain Integer, ≥ minimum\nSetting.llm_context_window = \"\"         # also valid: clears the setting (nil)","handlingStrategy":"validation","validationCode":"MINS = { llm_context_window: 256, llm_max_response_tokens: 64,\n          llm_max_items_per_call: 1, ai_response_timeout: Chat::MIN_RESPONSE_TIMEOUT.to_i }\n\nvalue = params[:llm_context_window].to_s.strip\nparsed = Integer(value, 10) rescue nil\nok = value.empty? || (parsed && parsed >= MINS[:llm_context_window])\nraise \"out of range\" unless ok","typeGuard":"def valid_llm_integer?(raw, minimum)\n  return true if raw.to_s.strip.empty? # blank clears\n  parsed = Integer(raw.to_s.strip, 10) rescue nil\n  !parsed.nil? && parsed >= minimum\nend","tryCatchPattern":"rescue Setting::ValidationError => e\n  # message already includes the field label and minimum; re-render form with values\n  render :edit, status: :unprocessable_entity\nend","preventionTips":["Never type units, commas, or decimals into numeric setting fields","Use the browser's min validation as a hint but never rely on it server-side","In automation, emit plain integers from templates (no quoting, no 'k' suffix)","Leave a field empty to clear it — zero is not 'empty' to this validator"],"tags":["rails","settings","validation","integer-parsing","llm"],"backgroundTag":"numeric-validation-failed","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}