Current section

Files

Jump to
claude_agent_sdk examples runtime_control model_switcher.exs
Raw

examples/runtime_control/model_switcher.exs

Code.require_file(Path.expand("support/mock_transport.exs", __DIR__))
defmodule Examples.RuntimeControl.ModelSwitcher do
@moduledoc false
alias ClaudeAgentSDK.{Client, Model, Options}
alias Examples.RuntimeControl.MockTransport
def main(args \\ System.argv()) do
case Enum.member?(args, "--live") do
true -> run_live()
false -> run_mock()
end
end
defp run_mock do
IO.puts("\n== Runtime Control Demo (Mock Transport) ==")
{:ok, client} =
Client.start_link(%Options{model: "haiku"},
transport: MockTransport,
transport_opts: [owner: self()]
)
transport_pid = wait_for(:mock_transport_started, 500)
stream_task =
Task.async(fn ->
Client.stream_messages(client)
|> Enum.take(3)
end)
initial =
message_frame("assistant", %{
"content" => "Hello! Starting on haiku.",
"role" => "assistant"
})
MockTransport.push_json(transport_pid, initial)
switch_task = Task.async(fn -> Client.set_model(client, "opus") end)
set_model_frame = wait_for_set_model_request(500)
{:ok, request_id} = acknowledge_model_change(set_model_frame, transport_pid)
:ok = Task.await(switch_task, 1_000)
follow_up =
message_frame("assistant", %{
"content" => "Model switched! Now responding as claude-opus-4-20250514.",
"role" => "assistant"
})
MockTransport.push_json(transport_pid, follow_up)
result_frame = %{
"type" => "result",
"subtype" => "success",
"session_id" => "demo-session",
"total_cost_usd" => 0.0
}
MockTransport.push_json(transport_pid, result_frame)
messages = Task.await(stream_task, 1_000)
print_summary(messages, request_id, client)
Client.stop(client)
end
defp run_live do
IO.puts("\n== Runtime Control Demo (CLI Transport) ==")
IO.puts("Attempting to use the Claude CLI. Make sure `claude login` has been executed.\n")
case Client.start_link(%Options{model: "haiku"}) do
{:ok, client} ->
try do
live_demo(client)
after
Client.stop(client)
end
{:error, reason} ->
IO.puts("Failed to start live client: #{inspect(reason)}")
:ok
end
end
defp live_demo(client) do
case await_model(client, 5_000) do
{:ok, model} ->
IO.puts("Current model before switch: #{model}")
{:error, :timeout} ->
IO.puts("Current model before switch: unavailable (timeout)")
{:error, reason} ->
IO.puts("Unable to read current model: #{inspect(reason)}")
end
{:ok, normalized_target} = Model.validate("opus")
case Client.set_model(client, "opus") do
:ok ->
IO.puts("Requested model switch to #{normalized_target}. Waiting for confirmation...")
case await_model_change(client, normalized_target, 5_000) do
{:ok, model} ->
IO.puts("Model after switch: #{model}")
{:timeout, observed} ->
IO.puts("Model did not update within 5s (last observed: #{observed || "unknown"})")
{:error, reason} ->
IO.puts("Failed to read model after switch: #{inspect(reason)}")
end
{:error, reason} ->
IO.puts("Live model switch failed: #{inspect(reason)}")
end
end
defp wait_for(tag, timeout) do
receive do
{^tag, payload} -> payload
after
timeout ->
raise "Timed out waiting for #{inspect(tag)}"
end
end
defp wait_for_set_model_request(timeout) do
frame = wait_for(:mock_transport_send, timeout)
decoded = Jason.decode!(frame)
case get_in(decoded, ["request", "subtype"]) do
"set_model" ->
frame
_other ->
wait_for_set_model_request(timeout)
end
end
defp acknowledge_model_change(encoded_frame, transport_pid) do
decoded = Jason.decode!(encoded_frame)
request = decoded["request"] || %{}
if request["subtype"] == "set_model" do
request_id = decoded["request_id"]
{:ok, normalized} = Model.validate(request["model"])
MockTransport.push_json(transport_pid, %{
"type" => "control_response",
"response" => %{
"request_id" => request_id,
"subtype" => "success",
"result" => %{"model" => normalized}
}
})
{:ok, request_id}
else
{:error, :unexpected_frame}
end
end
defp message_frame(subtype, message) do
%{
"type" => "assistant",
"subtype" => subtype,
"message" => message,
"session_id" => "demo-session"
}
end
defp print_summary(messages, request_id, client) do
IO.puts("\nMessages received by subscribers:")
Enum.each(messages, fn message ->
case {message.type, message.subtype} do
{:assistant, _} ->
IO.puts(" • Assistant: #{inspect(message.data[:message]["content"])}")
{:result, :success} ->
IO.puts(" • Result: success (cost: $#{message.data[:total_cost_usd] || 0})")
other ->
IO.puts(" • #{inspect(other)}")
end
end)
IO.puts("\nSwitch confirmed with request_id=#{request_id}")
{:ok, model} = Client.get_model(client)
IO.puts("Client reports current model: #{model}\n")
end
defp await_model(client, timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
poll_for_model(client, deadline)
end
defp poll_for_model(client, deadline) do
case Client.get_model(client) do
{:ok, model} ->
{:ok, model}
{:error, :model_not_set} ->
if System.monotonic_time(:millisecond) < deadline do
Process.sleep(100)
poll_for_model(client, deadline)
else
{:error, :timeout}
end
{:error, reason} ->
{:error, reason}
end
end
defp await_model_change(client, expected_model, timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
poll_for_model_change(client, expected_model, deadline)
end
defp poll_for_model_change(client, expected_model, deadline) do
case Client.get_model(client) do
{:ok, ^expected_model} = ok ->
ok
{:ok, model} ->
if System.monotonic_time(:millisecond) < deadline do
Process.sleep(100)
poll_for_model_change(client, expected_model, deadline)
else
{:timeout, model}
end
{:error, :model_not_set} ->
if System.monotonic_time(:millisecond) < deadline do
Process.sleep(100)
poll_for_model_change(client, expected_model, deadline)
else
{:timeout, nil}
end
{:error, reason} ->
{:error, reason}
end
end
end
Examples.RuntimeControl.ModelSwitcher.main()