diff options
Diffstat (limited to 'spec/dispatch/adapter/copilot_spec.rb')
| -rw-r--r-- | spec/dispatch/adapter/copilot_spec.rb | 611 |
1 files changed, 320 insertions, 291 deletions
diff --git a/spec/dispatch/adapter/copilot_spec.rb b/spec/dispatch/adapter/copilot_spec.rb index 13c37be..61766bf 100644 --- a/spec/dispatch/adapter/copilot_spec.rb +++ b/spec/dispatch/adapter/copilot_spec.rb @@ -21,9 +21,9 @@ RSpec.describe Dispatch::Adapter::Copilot do .to_return( status: 200, body: JSON.generate({ - "token" => copilot_token, - "expires_at" => (Time.now.to_i + 3600) - }), + "token" => copilot_token, + "expires_at" => (Time.now.to_i + 3600) + }), headers: { "Content-Type" => "application/json" } ) end @@ -36,7 +36,7 @@ RSpec.describe Dispatch::Adapter::Copilot do describe "VERSION" do it "is accessible" do - expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.1.0") + expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.2.0") end end @@ -70,15 +70,15 @@ RSpec.describe Dispatch::Adapter::Copilot do .to_return( status: 200, body: JSON.generate({ - "id" => "chatcmpl-123", - "model" => "gpt-4.1", - "choices" => [{ - "index" => 0, - "message" => { "role" => "assistant", "content" => "Hello there!" }, - "finish_reason" => "stop" - }], - "usage" => { "prompt_tokens" => 10, "completion_tokens" => 5 } - }), + "id" => "chatcmpl-123", + "model" => "gpt-4.1", + "choices" => [{ + "index" => 0, + "message" => { "role" => "assistant", "content" => "Hello there!" }, + "finish_reason" => "stop" + }], + "usage" => { "prompt_tokens" => 10, "completion_tokens" => 5 } + }), headers: { "Content-Type" => "application/json" } ) end @@ -103,26 +103,26 @@ RSpec.describe Dispatch::Adapter::Copilot do .to_return( status: 200, body: JSON.generate({ - "id" => "chatcmpl-456", - "model" => "gpt-4.1", - "choices" => [{ - "index" => 0, - "message" => { - "role" => "assistant", - "content" => nil, - "tool_calls" => [{ - "id" => "call_abc", - "type" => "function", - "function" => { - "name" => "get_weather", - "arguments" => '{"city":"New York"}' - } - }] - }, - "finish_reason" => "tool_calls" - }], - "usage" => { "prompt_tokens" => 15, "completion_tokens" => 10 } - }), + "id" => "chatcmpl-456", + "model" => "gpt-4.1", + "choices" => [{ + "index" => 0, + "message" => { + "role" => "assistant", + "content" => nil, + "tool_calls" => [{ + "id" => "call_abc", + "type" => "function", + "function" => { + "name" => "get_weather", + "arguments" => '{"city":"New York"}' + } + }] + }, + "finish_reason" => "tool_calls" + }], + "usage" => { "prompt_tokens" => 15, "completion_tokens" => 10 } + }), headers: { "Content-Type" => "application/json" } ) end @@ -149,28 +149,28 @@ RSpec.describe Dispatch::Adapter::Copilot do .to_return( status: 200, body: JSON.generate({ - "choices" => [{ - "index" => 0, - "message" => { - "role" => "assistant", - "content" => nil, - "tool_calls" => [ - { - "id" => "call_1", - "type" => "function", - "function" => { "name" => "get_weather", "arguments" => '{"city":"NYC"}' } - }, - { - "id" => "call_2", - "type" => "function", - "function" => { "name" => "get_time", "arguments" => '{"timezone":"EST"}' } - } - ] - }, - "finish_reason" => "tool_calls" - }], - "usage" => { "prompt_tokens" => 20, "completion_tokens" => 15 } - }), + "choices" => [{ + "index" => 0, + "message" => { + "role" => "assistant", + "content" => nil, + "tool_calls" => [ + { + "id" => "call_1", + "type" => "function", + "function" => { "name" => "get_weather", "arguments" => '{"city":"NYC"}' } + }, + { + "id" => "call_2", + "type" => "function", + "function" => { "name" => "get_time", "arguments" => '{"timezone":"EST"}' } + } + ] + }, + "finish_reason" => "tool_calls" + }], + "usage" => { "prompt_tokens" => 20, "completion_tokens" => 15 } + }), headers: { "Content-Type" => "application/json" } ) end @@ -194,26 +194,26 @@ RSpec.describe Dispatch::Adapter::Copilot do .to_return( status: 200, body: JSON.generate({ - "id" => "chatcmpl-789", - "model" => "gpt-4.1", - "choices" => [{ - "index" => 0, - "message" => { - "role" => "assistant", - "content" => "Let me check that for you.", - "tool_calls" => [{ - "id" => "call_def", - "type" => "function", - "function" => { - "name" => "search", - "arguments" => '{"query":"Ruby gems"}' - } - }] - }, - "finish_reason" => "tool_calls" - }], - "usage" => { "prompt_tokens" => 20, "completion_tokens" => 15 } - }), + "id" => "chatcmpl-789", + "model" => "gpt-4.1", + "choices" => [{ + "index" => 0, + "message" => { + "role" => "assistant", + "content" => "Let me check that for you.", + "tool_calls" => [{ + "id" => "call_def", + "type" => "function", + "function" => { + "name" => "search", + "arguments" => '{"query":"Ruby gems"}' + } + }] + }, + "finish_reason" => "tool_calls" + }], + "usage" => { "prompt_tokens" => 20, "completion_tokens" => 15 } + }), headers: { "Content-Type" => "application/json" } ) end @@ -231,16 +231,16 @@ RSpec.describe Dispatch::Adapter::Copilot do context "with system: parameter" do it "prepends system message in the wire format" do stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["messages"].first == { "role" => "system", "content" => "You are helpful." } - } + .with do |req| + body = JSON.parse(req.body) + body["messages"].first == { "role" => "system", "content" => "You are helpful." } + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "OK" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "OK" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -254,16 +254,16 @@ RSpec.describe Dispatch::Adapter::Copilot do context "with max_tokens: per-call override" do it "uses per-call max_tokens over constructor default" do stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["max_tokens"] == 100 - } + .with do |req| + body = JSON.parse(req.body) + body["max_tokens"] == 100 + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "short" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "short" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -275,16 +275,16 @@ RSpec.describe Dispatch::Adapter::Copilot do it "uses constructor default when max_tokens not specified" do stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["max_tokens"] == 4096 - } + .with do |req| + body = JSON.parse(req.body) + body["max_tokens"] == 4096 + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -304,23 +304,23 @@ RSpec.describe Dispatch::Adapter::Copilot do ) stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["tools"] == [{ - "type" => "function", - "function" => { - "name" => "get_weather", - "description" => "Get weather for a city", - "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } - } - }] - } + .with do |req| + body = JSON.parse(req.body) + body["tools"] == [{ + "type" => "function", + "function" => { + "name" => "get_weather", + "description" => "Get weather for a city", + "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } + } + }] + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -338,23 +338,23 @@ RSpec.describe Dispatch::Adapter::Copilot do } stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["tools"] == [{ - "type" => "function", - "function" => { - "name" => "get_weather", - "description" => "Get weather for a city", - "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } - } - }] - } + .with do |req| + body = JSON.parse(req.body) + body["tools"] == [{ + "type" => "function", + "function" => { + "name" => "get_weather", + "description" => "Get weather for a city", + "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } + } + }] + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -372,23 +372,23 @@ RSpec.describe Dispatch::Adapter::Copilot do } stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["tools"] == [{ - "type" => "function", - "function" => { - "name" => "get_weather", - "description" => "Get weather for a city", - "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } - } - }] - } + .with do |req| + body = JSON.parse(req.body) + body["tools"] == [{ + "type" => "function", + "function" => { + "name" => "get_weather", + "description" => "Get weather for a city", + "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } + } + }] + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -411,18 +411,18 @@ RSpec.describe Dispatch::Adapter::Copilot do } stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["tools"].size == 2 && - body["tools"][0]["function"]["name"] == "get_weather" && - body["tools"][1]["function"]["name"] == "get_time" - } + .with do |req| + body = JSON.parse(req.body) + body["tools"].size == 2 && + body["tools"][0]["function"]["name"] == "get_weather" && + body["tools"][1]["function"]["name"] == "get_time" + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -434,16 +434,16 @@ RSpec.describe Dispatch::Adapter::Copilot do it "does not include tools key when tools array is empty" do stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - !body.key?("tools") - } + .with do |req| + body = JSON.parse(req.body) + !body.key?("tools") + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -470,26 +470,27 @@ RSpec.describe Dispatch::Adapter::Copilot do ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - msgs = body["messages"] - # user message - msgs[0]["role"] == "user" && - # assistant with tool_calls - msgs[1]["role"] == "assistant" && - msgs[1]["tool_calls"].is_a?(Array) && - msgs[1]["tool_calls"][0]["id"] == "call_1" && - # tool result - msgs[2]["role"] == "tool" && - msgs[2]["tool_call_id"] == "call_1" && - msgs[2]["content"] == "72F and sunny" - } + .with do |req| + body = JSON.parse(req.body) + msgs = body["messages"] + # user message + msgs[0]["role"] == "user" && + # assistant with tool_calls + msgs[1]["role"] == "assistant" && + msgs[1]["tool_calls"].is_a?(Array) && + msgs[1]["tool_calls"][0]["id"] == "call_1" && + # tool result + msgs[2]["role"] == "tool" && + msgs[2]["tool_call_id"] == "call_1" && + msgs[2]["content"] == "72F and sunny" + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "It's 72F and sunny in NYC!" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 20, "completion_tokens" => 10 } - }), + "choices" => [{ "message" => { "content" => "It's 72F and sunny in NYC!" }, + "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 20, "completion_tokens" => 10 } + }), headers: { "Content-Type" => "application/json" } ) @@ -516,17 +517,17 @@ RSpec.describe Dispatch::Adapter::Copilot do messages = [Dispatch::Adapter::Message.new(role: "user", content: text_blocks)] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - msgs = body["messages"] - msgs[0]["content"] == "First paragraph.\nSecond paragraph." - } + .with do |req| + body = JSON.parse(req.body) + msgs = body["messages"] + msgs[0]["content"] == "First paragraph.\nSecond paragraph." + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -555,18 +556,18 @@ RSpec.describe Dispatch::Adapter::Copilot do ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - msgs = body["messages"] - tool_msg = msgs.find { |m| m["role"] == "tool" } - tool_msg && tool_msg["content"] == "Result line 1\nResult line 2" - } + .with do |req| + body = JSON.parse(req.body) + msgs = body["messages"] + tool_msg = msgs.find { |m| m["role"] == "tool" } + tool_msg && tool_msg["content"] == "Result line 1\nResult line 2" + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 10, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 10, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -591,18 +592,19 @@ RSpec.describe Dispatch::Adapter::Copilot do ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - msgs = body["messages"] - tool_msg = msgs.find { |m| m["role"] == "tool" } - tool_msg && tool_msg["content"] == "Something went wrong" && tool_msg["tool_call_id"] == "call_err" - } + .with do |req| + body = JSON.parse(req.body) + msgs = body["messages"] + tool_msg = msgs.find { |m| m["role"] == "tool" } + tool_msg && tool_msg["content"] == "Something went wrong" && tool_msg["tool_call_id"] == "call_err" + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "I see the error" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 10, "completion_tokens" => 3 } - }), + "choices" => [{ "message" => { "content" => "I see the error" }, + "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 10, "completion_tokens" => 3 } + }), headers: { "Content-Type" => "application/json" } ) @@ -617,12 +619,12 @@ RSpec.describe Dispatch::Adapter::Copilot do .to_return( status: 200, body: JSON.generate({ - "choices" => [{ - "message" => { "content" => "truncated output..." }, - "finish_reason" => "length" - }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 100 } - }), + "choices" => [{ + "message" => { "content" => "truncated output..." }, + "finish_reason" => "length" + }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 100 } + }), headers: { "Content-Type" => "application/json" } ) @@ -647,21 +649,21 @@ RSpec.describe Dispatch::Adapter::Copilot do ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - msgs = body["messages"] - assistant = msgs.find { |m| m["role"] == "assistant" } - assistant && - assistant["content"] == "Checking..." && - assistant["tool_calls"].is_a?(Array) && - assistant["tool_calls"][0]["id"] == "call_mixed" - } + .with do |req| + body = JSON.parse(req.body) + msgs = body["messages"] + assistant = msgs.find { |m| m["role"] == "assistant" } + assistant && + assistant["content"] == "Checking..." && + assistant["tool_calls"].is_a?(Array) && + assistant["tool_calls"][0]["id"] == "call_mixed" + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 10, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 10, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -678,17 +680,17 @@ RSpec.describe Dispatch::Adapter::Copilot do ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - msgs = body["messages"] - msgs.size == 1 && msgs[0]["role"] == "user" && msgs[0]["content"].include?("First") && msgs[0]["content"].include?("Second") - } + .with do |req| + body = JSON.parse(req.body) + msgs = body["messages"] + msgs.size == 1 && msgs[0]["role"] == "user" && msgs[0]["content"].include?("First") && msgs[0]["content"].include?("Second") + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -700,16 +702,17 @@ RSpec.describe Dispatch::Adapter::Copilot do context "with thinking: parameter" do it "sends reasoning_effort in the request body" do stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["reasoning_effort"] == "high" - } + .with do |req| + body = JSON.parse(req.body) + body["reasoning_effort"] == "high" + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "thought deeply" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 3 } - }), + "choices" => [{ "message" => { "content" => "thought deeply" }, + "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 3 } + }), headers: { "Content-Type" => "application/json" } ) @@ -728,16 +731,16 @@ RSpec.describe Dispatch::Adapter::Copilot do ) stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["reasoning_effort"] == "medium" - } + .with do |req| + body = JSON.parse(req.body) + body["reasoning_effort"] == "medium" + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -756,16 +759,16 @@ RSpec.describe Dispatch::Adapter::Copilot do ) stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - body["reasoning_effort"] == "low" - } + .with do |req| + body = JSON.parse(req.body) + body["reasoning_effort"] == "low" + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -777,16 +780,16 @@ RSpec.describe Dispatch::Adapter::Copilot do it "does not send reasoning_effort when thinking is nil" do stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - !body.key?("reasoning_effort") - } + .with do |req| + body = JSON.parse(req.body) + !body.key?("reasoning_effort") + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -797,16 +800,16 @@ RSpec.describe Dispatch::Adapter::Copilot do end it "raises ArgumentError for invalid thinking level" do - expect { + expect do described_class.new(model: "o3", github_token: github_token, thinking: "extreme") - }.to raise_error(ArgumentError, /Invalid thinking level/) + end.to raise_error(ArgumentError, /Invalid thinking level/) end it "raises ArgumentError for invalid per-call thinking level" do messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] - expect { + expect do adapter.chat(messages, thinking: "extreme") - }.to raise_error(ArgumentError, /Invalid thinking level/) + end.to raise_error(ArgumentError, /Invalid thinking level/) end it "allows disabling constructor default with nil per-call" do @@ -818,16 +821,16 @@ RSpec.describe Dispatch::Adapter::Copilot do ) stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .with { |req| - body = JSON.parse(req.body) - !body.key?("reasoning_effort") - } + .with do |req| + body = JSON.parse(req.body) + !body.key?("reasoning_effort") + end .to_return( status: 200, body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), headers: { "Content-Type" => "application/json" } ) @@ -844,7 +847,8 @@ RSpec.describe Dispatch::Adapter::Copilot do sse_body = [ "data: #{JSON.generate({ "choices" => [{ "delta" => { "content" => "Hello" }, "index" => 0 }] })}\n\n", "data: #{JSON.generate({ "choices" => [{ "delta" => { "content" => " world" }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 2 } })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 2 } })}\n\n", "data: [DONE]\n\n" ].join @@ -873,10 +877,20 @@ RSpec.describe Dispatch::Adapter::Copilot do it "yields tool_use_start and tool_use_delta for tool call streams" do sse_body = [ - "data: #{JSON.generate({ "choices" => [{ "delta" => { "tool_calls" => [{ "index" => 0, "id" => "call_1", "type" => "function", "function" => { "name" => "search", "arguments" => "" } }] }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => { "tool_calls" => [{ "index" => 0, "function" => { "arguments" => "{\"q\":" } }] }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => { "tool_calls" => [{ "index" => 0, "function" => { "arguments" => "\"test\"}" } }] }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "tool_calls" }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, "id" => "call_1", "type" => "function", + "function" => { "name" => "search", "arguments" => "" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, + "function" => { "arguments" => "{\"q\":" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, + "function" => { "arguments" => "\"test\"}" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, + "finish_reason" => "tool_calls" }] })}\n\n", "data: [DONE]\n\n" ].join @@ -909,7 +923,8 @@ RSpec.describe Dispatch::Adapter::Copilot do it "captures usage from streaming response" do sse_body = [ "data: #{JSON.generate({ "choices" => [{ "delta" => { "content" => "hi" }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 42, "completion_tokens" => 7 } })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 42, "completion_tokens" => 7 } })}\n\n", "data: [DONE]\n\n" ].join @@ -929,11 +944,24 @@ RSpec.describe Dispatch::Adapter::Copilot do it "handles multiple parallel tool calls in a stream" do sse_body = [ - "data: #{JSON.generate({ "choices" => [{ "delta" => { "tool_calls" => [{ "index" => 0, "id" => "call_a", "type" => "function", "function" => { "name" => "tool_a", "arguments" => "" } }] }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => { "tool_calls" => [{ "index" => 1, "id" => "call_b", "type" => "function", "function" => { "name" => "tool_b", "arguments" => "" } }] }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => { "tool_calls" => [{ "index" => 0, "function" => { "arguments" => "{\"x\":1}" } }] }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => { "tool_calls" => [{ "index" => 1, "function" => { "arguments" => "{\"y\":2}" } }] }, "index" => 0 }] })}\n\n", - "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "tool_calls" }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, "id" => "call_a", "type" => "function", + "function" => { "name" => "tool_a", "arguments" => "" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 1, "id" => "call_b", "type" => "function", + "function" => { "name" => "tool_b", "arguments" => "" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, + "function" => { "arguments" => "{\"x\":1}" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 1, + "function" => { "arguments" => "{\"y\":2}" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, + "finish_reason" => "tool_calls" }] })}\n\n", "data: [DONE]\n\n" ].join @@ -960,21 +988,22 @@ RSpec.describe Dispatch::Adapter::Copilot do describe "authentication" do it "reuses cached Copilot token for subsequent requests" do token_stub = stub_request(:get, "https://api.github.com/copilot_internal/v2/token") - .to_return( - status: 200, - body: JSON.generate({ "token" => copilot_token, "expires_at" => (Time.now.to_i + 3600) }), - headers: { "Content-Type" => "application/json" } - ) + .to_return( + status: 200, + body: JSON.generate({ "token" => copilot_token, "expires_at" => (Time.now.to_i + 3600) }), + headers: { "Content-Type" => "application/json" } + ) chat_stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") - .to_return( - status: 200, - body: JSON.generate({ - "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], - "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } - }), - headers: { "Content-Type" => "application/json" } - ) + .to_return( + status: 200, + body: JSON.generate({ + "choices" => [{ "message" => { "content" => "ok" }, + "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), + headers: { "Content-Type" => "application/json" } + ) messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] adapter.chat(messages) @@ -1007,11 +1036,11 @@ RSpec.describe Dispatch::Adapter::Copilot do .to_return( status: 200, body: JSON.generate({ - "data" => [ - { "id" => "gpt-4.1", "object" => "model" }, - { "id" => "gpt-4o", "object" => "model" } - ] - }), + "data" => [ + { "id" => "gpt-4.1", "object" => "model" }, + { "id" => "gpt-4o", "object" => "model" } + ] + }), headers: { "Content-Type" => "application/json" } ) |
