Repository: Shopify/roast (Ruby)

Issue:
NoMethodError: undefined method `chat' for nil when using openrouter
When using openrouter setup:

```
api_provider: openrouter
api_token: $(echo $OPENROUTER_API_KEY)
```

I get the following error message:
```
🔥🔥🔥 Everyone loves a good roast 🔥🔥🔥

Configuring OpenRouter client with token from workflow
[2025-05-18 15:58:49] ERROR: Error configuring API client: unknown keyword: :api_key
Starting workflow...
Workflow: /Users/caleb/development/playground/text-adventure/.roast/git_commit.yml
Options: {}
Running targetless workflow
Prompt file for git_commit not found: 
Executing: $(git add .) (Resource type: none)
Executing: $(git diff origin/main) (Resource type: none)
Executing: Summarize the changes and update CHANGELOG.md (Resource type: none)
bundler: failed to load command: roast (/Users/caleb/.asdf/installs/ruby/3.3.5/bin/roast)


Exiting due to error: NoMethodError: undefined method `chat' for nil
/Users/caleb/.asdf/installs/ruby/3.3.5/lib/ruby/gems/3.3.0/gems/raix-0.8.4/lib/raix/chat_completion.rb:230:in `openai_request': undefined method `chat' for nil (NoMethodError)

      Raix.configuration.openai_client.chat(parameters: params.compact.merge(model:, messages:))
```


It appears that this line is not correct in `configuration_parser`:
`OpenRouter::Client.new(api_key: configuration.api_token)`. I believe it should be using `access_token` instead.

When I change that to use `access_token`, I get this error instead.

```
🔥🔥🔥 Everyone loves a good roast 🔥🔥🔥

Configuring OpenRouter client with token from workflow
Starting workflow...
Workflow: /Users/caleb/development/playground/text-adventure/.roast/git_commit.yml
Options: {}
Running targetless workflow
Prompt file for git_commit not found: 
Executing: $(git add .) (Resource type: none)
Executing: $(git diff origin/main) (Resource type: none)
Executing: Summarize the changes and update CHANGELOG.md (Resource type: none)
bundler: failed to load command: roast (/Users/caleb/.asdf/installs/ruby/3.3.5/bin/roast)


Exiting due to error: NoMethodError: undefined method `chat' for nil
/Users/caleb/.asdf/installs/ruby/3.3.5/lib/ruby/gems/3.3.0/gems/raix-0.8.4/lib/raix/chat_completion.rb:230:in `openai_request': undefined method `chat' for nil (NoMethodError)

      Raix.configuration.openai_client.chat(parameters: params.compact.merge(model:, messages:))
```

It appears that Roast is still trying to using an openai_client even when openrouter is specified. I'll try to get a fix up for this. Wanted to describe it in case someone else was able to get to it first.


Discussion:
I fixed this in #54 



Patch:
diff --git a/examples/openrouter_example/workflow.yml b/examples/openrouter_example/workflow.yml
index 60d4164b..16138a27 100644
--- a/examples/openrouter_example/workflow.yml
+++ b/examples/openrouter_example/workflow.yml
@@ -1,7 +1,7 @@
 name: OpenRouter Example
 api_provider: openrouter
 api_token: $(echo $OPENROUTER_API_KEY)
-model: anthropic/claude-3-haiku-20240307
+model: google/gemini-2.0-flash-001
 
 tools:
   - Roast::Tools::ReadFile
@@ -9,4 +9,4 @@ tools:
 
 steps:
   - analyze_input
-  - generate_response
\ No newline at end of file
+  - generate_response
diff --git a/lib/roast/workflow/base_step.rb b/lib/roast/workflow/base_step.rb
index 14ed810c..5006350b 100644
--- a/lib/roast/workflow/base_step.rb
+++ b/lib/roast/workflow/base_step.rb
@@ -35,7 +35,7 @@ def call
       protected
 
       def chat_completion(print_response: false, auto_loop: true, json: false, params: {})
-        workflow.chat_completion(openai: model, loop: auto_loop, json:, params:).then do |response|
+        workflow.chat_completion(openai: workflow.openai? && model, loop: auto_loop, model: model, json:, params:).then do |response|
           case response
           in Array
             response.map(&:presence).compact.join("\n")
diff --git a/lib/roast/workflow/base_workflow.rb b/lib/roast/workflow/base_workflow.rb
index 5c69818b..b2dcad45 100644
--- a/lib/roast/workflow/base_workflow.rb
+++ b/lib/roast/workflow/base_workflow.rb
@@ -22,7 +22,10 @@ class BaseWorkflow
         :resource,
         :session_name,
         :session_timestamp,
-        :configuration
+        :configuration,
+        :model
+
+      delegate :api_provider, :openai?, to: :configuration
 
       def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, configuration: nil)
         @file = file
@@ -65,36 +68,46 @@ def final_output
         end
       end
 
+      def with_model(model)
+        previous_model = @model
+        @model = model
+        yield
+      ensure
+        @model = previous_model
+      end
+
       # Override chat_completion to add instrumentation
       def chat_completion(**kwargs)
         start_time = Time.now
-        model = kwargs[:openai] || "default"
 
-        ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
-          model: model,
-          parameters: kwargs.except(:openai),
-        })
-
-        result = super(**kwargs)
-        execution_time = Time.now - start_time
-
-        ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
-          success: true,
-          model: model,
-          parameters: kwargs.except(:openai),
-          execution_time: execution_time,
-          response_size: result.to_s.length,
-        })
-
-        result
+        step_model = kwargs[:model]
+        with_model(step_model) do
+          ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
+            model: model,
+            parameters: kwargs.except(:openai, :model),
+          })
+
+          # skip model because it is read directly from the model method
+          result = super(**kwargs.except(:model))
+          execution_time = Time.now - start_time
+
+          ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
+            success: true,
+            model: model,
+            parameters: kwargs.except(:openai, :model),
+            execution_time: execution_time,
+            response_size: result.to_s.length,
+          })
+          result
+        end
       rescue => e
         execution_time = Time.now - start_time
 
         ActiveSupport::Notifications.instrument("roast.chat_completion.error", {
           error: e.class.name,
           message: e.message,
-          model: model,
-          parameters: kwargs.except(:openai),
+          model: step_model,
+          parameters: kwargs.except(:openai, :model),
           execution_time: execution_time,
         })
         raise
diff --git a/lib/roast/workflow/configuration_parser.rb b/lib/roast/workflow/configuration_parser.rb
index ab141bc0..f1d2c7ae 100644
--- a/lib/roast/workflow/configuration_parser.rb
+++ b/lib/roast/workflow/configuration_parser.rb
@@ -112,7 +112,7 @@ def configure_api_client
             require "open_router"
 
             Raix.configure do |config|
-              config.openrouter_client = OpenRouter::Client.new(api_key: configuration.api_token)
+              config.openrouter_client = OpenRouter::Client.new(access_token: configuration.api_token)
             end
           else
             $stderr.puts "Configuring OpenAI client with token from workflow"
diff --git a/lib/roast/workflow/session_manager.rb b/lib/roast/workflow/session_manager.rb
index 9978045c..61927013 100644
--- a/lib/roast/workflow/session_manager.rb
+++ b/lib/roast/workflow/session_manager.rb
@@ -67,8 +67,9 @@ def create_new_session(workflow_id)
 
       def workflow_directory(session_name, file_path)
         workflow_dir_name = session_name.parameterize.underscore
-        file_id = Digest::MD5.hexdigest(file_path)
-        file_basename = File.basename(file_path).parameterize.underscore
+        # For targetless sessions we don't have a file_path
+        file_id = Digest::MD5.hexdigest(file_path || Dir.pwd)
+        file_basename = File.basename(file_path || Dir.pwd).parameterize.underscore
         human_readable_id = "#{file_basename}_#{file_id[0..7]}"
         File.join(Dir.pwd, ".roast", "sessions", workflow_dir_name, human_readable_id)
       end


Test patch:
diff --git a/test/roast/workflow/base_step_test.rb b/test/roast/workflow/base_step_test.rb
index df8e0319..dad5ee57 100644
--- a/test/roast/workflow/base_step_test.rb
+++ b/test/roast/workflow/base_step_test.rb
@@ -37,6 +37,9 @@ def setup
     @workflow.stubs(:chat_completion)
       .returns("Test chat completion response")
 
+    @workflow.stubs(:openai?)
+      .returns(true)
+
     result = @step.call
     assert_equal({ user: "Test prompt" }, @workflow.transcript.last)
     assert_equal "Test chat completion response", result
diff --git a/test/roast/workflow/configuration_parser_openrouter_test.rb b/test/roast/workflow/configuration_parser_openrouter_test.rb
index e1b47743..1719007c 100644
--- a/test/roast/workflow/configuration_parser_openrouter_test.rb
+++ b/test/roast/workflow/configuration_parser_openrouter_test.rb
@@ -14,7 +14,7 @@ def test_configure_openrouter_client
         setup_openrouter_constants
 
         mock_openrouter_client = mock
-        OpenRouter::Client.stubs(:new).with(api_key: "test_openrouter_token").returns(mock_openrouter_client)
+        OpenRouter::Client.stubs(:new).with(access_token: "test_openrouter_token").returns(mock_openrouter_client)
 
         ConfigurationParser.new(@workflow_path)
       end