From 4fd86fbc75670878308fd6a56b7778334b52ccd8 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 25 May 2026 20:05:15 +0300 Subject: refactor(manager): extract focused collaborators from Manager God Class Extract VM lifecycle, SSH execution, WireGuard setup, model switching, end-to-end inference tests, and provisioning orchestration into separate collaborator classes. Manager becomes a thin facade (~80 lines). Also fixes CLI edge-cases: status/model-list when no VMs are active, and threads --vllm/--ollama flags through the provisioning pipeline. --- lib/hyperstack/model_switcher.rb | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 lib/hyperstack/model_switcher.rb (limited to 'lib/hyperstack/model_switcher.rb') diff --git a/lib/hyperstack/model_switcher.rb b/lib/hyperstack/model_switcher.rb new file mode 100644 index 0000000..07f6cdb --- /dev/null +++ b/lib/hyperstack/model_switcher.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module HyperstackVM + # Hot-switches the running vLLM model on a provisioned VM. + class ModelSwitcher + def initialize(config:, provisioner:, state_store:, out:) + @config = config + @provisioner = provisioner + @state_store = state_store + @out = out + end + + def switch(preset_name:, dry_run: false) + preset = @config.vllm_preset(preset_name) + state = @state_store.load + + old_container = state&.dig('vllm_container_name') || @config.vllm_container_name + new_container = preset['container_name'] + current_model = state&.dig('vllm_model') + + if dry_run + print_dry_run(preset_name, preset, current_model, old_container, new_container) + return + end + + raise Error, "No tracked VM. Run 'create' first." unless state&.dig('vm_id') + host = state['public_ip'] + raise Error, 'No public IP in state file.' if host.nil? || host.empty? + + @provisioner.decommission_litellm(host) + @provisioner.stop_vllm_container(host, old_container) if old_container != new_container + + info "Starting vLLM with preset '#{preset_name}' (#{preset['model']})..." + @provisioner.install_vllm(host, preset_config: preset, pull_image: false) + + state['vllm_model'] = preset['model'] + state['vllm_container_name'] = new_container + state['vllm_preset'] = preset_name + state['vllm_setup_at'] = Time.now.utc.iso8601 + state['services'] ||= {} + state['services']['vllm_enabled'] = true + state['services']['ollama_enabled'] = state_ollama_enabled?(state) + @state_store.save(state) + + info "Model switched to '#{preset_name}' (#{preset['model']})." + info "Run 'ruby hyperstack.rb test' to verify." + end + + private + + def print_dry_run(preset_name, preset, current_model, old_container, new_container) + info "DRY RUN: model switch to preset '#{preset_name}'" + info " #{current_model || 'none'} → #{preset['model']}" + info " container: #{old_container} → #{new_container}" + trust_note = preset['trust_remote_code'] ? ', trust_remote_code: true' : '' + parser_note = preset['tool_call_parser'].to_s.empty? ? 'none' : preset['tool_call_parser'] + extra_note = preset['extra_vllm_args']&.any? ? ", extra_args: #{preset['extra_vllm_args'].join(' ')}" : '' + info " max_model_len: #{preset['max_model_len']}, tool_call_parser: #{parser_note}#{trust_note}#{extra_note}" + end + + def state_ollama_enabled?(state) + recorded = state&.dig('services', 'ollama_enabled') + return recorded unless recorded.nil? + return true if state&.key?('ollama_installed_at') || state&.key?('ollama_setup_at') + @config.ollama_install_enabled? + end + + def info(msg) + @out.puts(msg) + end + end +end -- cgit v1.2.3