llm_gateway 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.pi/skills/live-provider-testing/SKILL.md +183 -0
- data/.pi/skills/options-development/SKILL.md +131 -0
- data/CHANGELOG.md +17 -0
- data/README.md +16 -0
- data/Rakefile +1 -0
- data/lib/llm_gateway/adapters/adapter.rb +2 -35
- data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +0 -2
- data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +106 -27
- data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +0 -33
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +31 -46
- data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +48 -6
- data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +3 -2
- data/lib/llm_gateway/adapters/groq/input_mapper.rb +44 -0
- data/lib/llm_gateway/adapters/groq/option_mapper.rb +89 -4
- data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +275 -0
- data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +0 -2
- data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +0 -6
- data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +135 -72
- data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +100 -10
- data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +169 -170
- data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +0 -1
- data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +128 -68
- data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +99 -10
- data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +81 -271
- data/lib/llm_gateway/adapters/openai/responses_adapter.rb +0 -1
- data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +3 -3
- data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +0 -5
- data/lib/llm_gateway/adapters/stream_mapper.rb +50 -0
- data/lib/llm_gateway/client.rb +10 -66
- data/lib/llm_gateway/clients/groq.rb +13 -1
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +2 -8
- metadata +7 -10
- data/lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb +0 -111
- data/lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb +0 -110
- data/lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb +0 -40
- data/lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb +0 -120
- data/lib/llm_gateway/adapters/openai/responses/output_mapper.rb +0 -47
- data/lib/llm_gateway/adapters/stream_accumulator.rb +0 -91
- data/scripts/generate_handoff_live_fixture.rb +0 -169
- data/scripts/generate_handoff_media_fixture.rb +0 -167
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
class StreamAccumulator
|
|
4
|
-
attr_accessor :blocks, :message_hash, :usage_hash
|
|
5
|
-
|
|
6
|
-
def initialize
|
|
7
|
-
@message_hash = {}
|
|
8
|
-
@usage_hash = {
|
|
9
|
-
input_tokens: 0,
|
|
10
|
-
cache_creation_input_tokens: 0,
|
|
11
|
-
cache_read_input_tokens: 0,
|
|
12
|
-
output_tokens: 0,
|
|
13
|
-
reasoning_tokens: 0
|
|
14
|
-
}
|
|
15
|
-
@blocks = []
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def result
|
|
19
|
-
message_hash.merge(
|
|
20
|
-
usage: usage_hash,
|
|
21
|
-
content: serialized_blocks
|
|
22
|
-
)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def push(event)
|
|
26
|
-
return unless event
|
|
27
|
-
|
|
28
|
-
case event.type
|
|
29
|
-
when :text_start
|
|
30
|
-
blocks[event.content_index] = {
|
|
31
|
-
type: "text",
|
|
32
|
-
text: ""
|
|
33
|
-
}
|
|
34
|
-
blocks[event.content_index][:text] += event.delta
|
|
35
|
-
when :text_delta, :text_end
|
|
36
|
-
blocks[event.content_index][:text] += event.delta
|
|
37
|
-
when :tool_start
|
|
38
|
-
blocks[event.content_index] = {
|
|
39
|
-
type: "tool_use",
|
|
40
|
-
id: event.id,
|
|
41
|
-
name: event.name,
|
|
42
|
-
input: ""
|
|
43
|
-
}
|
|
44
|
-
when :tool_delta, :tool_end
|
|
45
|
-
blocks[event.content_index][:input] += event.delta
|
|
46
|
-
when :message_start
|
|
47
|
-
message_hash.merge!(event.delta)
|
|
48
|
-
usage_hash.each_key do |key|
|
|
49
|
-
usage_hash[key] += event.usage_increment.fetch(key, 0)
|
|
50
|
-
end
|
|
51
|
-
when :reasoning_start
|
|
52
|
-
blocks[event.content_index] = {
|
|
53
|
-
type: "reasoning",
|
|
54
|
-
reasoning: "",
|
|
55
|
-
signature: ""
|
|
56
|
-
}
|
|
57
|
-
blocks[event.content_index][:reasoning] += event.delta
|
|
58
|
-
blocks[event.content_index][:signature] += event.respond_to?(:signature) ? event.signature : ""
|
|
59
|
-
when :reasoning_delta
|
|
60
|
-
blocks[event.content_index][:reasoning] += event.delta
|
|
61
|
-
blocks[event.content_index][:signature] += event.signature
|
|
62
|
-
when :reasoning_end
|
|
63
|
-
blocks[event.content_index][:reasoning] += event.delta
|
|
64
|
-
blocks[event.content_index][:signature] += event.respond_to?(:signature) ? event.signature : ""
|
|
65
|
-
when :message_delta
|
|
66
|
-
message_hash.merge!(event.delta)
|
|
67
|
-
usage_hash.each_key do |key|
|
|
68
|
-
usage_hash[key] += event.usage_increment.fetch(key, 0)
|
|
69
|
-
end
|
|
70
|
-
when :message_end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
def serialized_blocks
|
|
77
|
-
blocks.map do |block|
|
|
78
|
-
next block unless block[:type] == "tool_use"
|
|
79
|
-
|
|
80
|
-
block.merge(input: LlmGateway::Utils.deep_symbolize_keys(parse_tool_input(block[:input])))
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def parse_tool_input(input)
|
|
85
|
-
return {} if input.nil? || input.empty?
|
|
86
|
-
|
|
87
|
-
JSON.parse(input)
|
|
88
|
-
rescue JSON::ParserError
|
|
89
|
-
{}
|
|
90
|
-
end
|
|
91
|
-
end
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "time"
|
|
5
|
-
require "fileutils"
|
|
6
|
-
require 'debug'
|
|
7
|
-
require_relative "../lib/llm_gateway"
|
|
8
|
-
require_relative "../test/utils/calculator_tool_helper"
|
|
9
|
-
|
|
10
|
-
class HandoffLiveFixtureGenerator
|
|
11
|
-
include CalculatorToolHelper
|
|
12
|
-
|
|
13
|
-
PAIRS = [
|
|
14
|
-
{ provider: "openai_apikey_completions", model: "gpt-5.1" },
|
|
15
|
-
{ provider: "openai_apikey_responses", model: "gpt-5.4" },
|
|
16
|
-
{ provider: "openai_oauth_codex", model: "gpt-5.4" },
|
|
17
|
-
{ provider: "anthropic_apikey_messages", model: "claude-sonnet-4-20250514" }
|
|
18
|
-
].freeze
|
|
19
|
-
|
|
20
|
-
FIXTURE_PATH = File.expand_path("../test/fixtures/handoff_live_fixture.json", __dir__)
|
|
21
|
-
|
|
22
|
-
def run
|
|
23
|
-
flat_transcript = []
|
|
24
|
-
skipped = []
|
|
25
|
-
|
|
26
|
-
PAIRS.each do |pair|
|
|
27
|
-
begin
|
|
28
|
-
adapter = load_provider(provider: pair[:provider], model: pair[:model])
|
|
29
|
-
mini_context = generate_mini_context(adapter)
|
|
30
|
-
flat_transcript.concat(mini_context.map(&:to_h))
|
|
31
|
-
puts "[ok] #{pair[:provider]} / #{pair[:model]}"
|
|
32
|
-
rescue StandardError => e
|
|
33
|
-
skipped << pair.merge(error: e.message)
|
|
34
|
-
puts "[skip] #{pair[:provider]} / #{pair[:model]} -> #{e.message}"
|
|
35
|
-
ensure
|
|
36
|
-
LlmGateway.reset_configuration!
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
FileUtils.mkdir_p(File.dirname(FIXTURE_PATH))
|
|
41
|
-
File.write(FIXTURE_PATH, JSON.pretty_generate(flat_transcript) + "\n")
|
|
42
|
-
|
|
43
|
-
puts "\nWrote fixture: #{FIXTURE_PATH}"
|
|
44
|
-
puts "Messages: #{flat_transcript.length}"
|
|
45
|
-
puts "Skipped pairs: #{skipped.length}"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def generate_mini_context(adapter)
|
|
51
|
-
transcript = [
|
|
52
|
-
{
|
|
53
|
-
role: "user",
|
|
54
|
-
content: "Think hard about this, then use the math_operation tool to double 21 by multiplying 21 and 2. Call the tool."
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
first = adapter.stream(transcript, tools: [ math_operation_tool ], reasoning: "high")
|
|
59
|
-
raise first.error_message if first.stop_reason == "error"
|
|
60
|
-
|
|
61
|
-
transcript << first
|
|
62
|
-
|
|
63
|
-
tool_call = first.content.find { |block| block.type == "tool_use" && block.name == "math_operation" }
|
|
64
|
-
raise "model did not call math_operation" unless tool_call
|
|
65
|
-
|
|
66
|
-
tool_result = {
|
|
67
|
-
role: "developer",
|
|
68
|
-
content: [
|
|
69
|
-
{
|
|
70
|
-
type: "tool_result",
|
|
71
|
-
tool_use_id: tool_call.id,
|
|
72
|
-
content: evaluate_math_operation(tool_call.input).to_s
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
}
|
|
76
|
-
transcript << tool_result
|
|
77
|
-
|
|
78
|
-
second = adapter.stream(
|
|
79
|
-
transcript.map { |m| m.respond_to?(:to_h) ? m.to_h : m },
|
|
80
|
-
tools: [ math_operation_tool ],
|
|
81
|
-
reasoning: "high"
|
|
82
|
-
)
|
|
83
|
-
raise second.error_message if second.stop_reason == "error"
|
|
84
|
-
|
|
85
|
-
transcript << second
|
|
86
|
-
transcript
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def load_provider(provider:, model:)
|
|
90
|
-
config = {
|
|
91
|
-
"provider" => provider,
|
|
92
|
-
"model_key" => model
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
case provider
|
|
96
|
-
when "openai_apikey_completions", "openai_apikey_responses"
|
|
97
|
-
api_key = ENV["OPENAI_API_KEY"].to_s
|
|
98
|
-
raise "missing OPENAI_API_KEY" if api_key.empty?
|
|
99
|
-
|
|
100
|
-
config["api_key"] = api_key
|
|
101
|
-
when "anthropic_apikey_messages"
|
|
102
|
-
api_key = ENV["ANTHROPIC_API_KEY"].to_s
|
|
103
|
-
raise "missing ANTHROPIC_API_KEY" if api_key.empty?
|
|
104
|
-
|
|
105
|
-
config["api_key"] = api_key
|
|
106
|
-
when "openai_oauth_codex"
|
|
107
|
-
creds = load_auth_credentials("openai")
|
|
108
|
-
config["api_key"] = oauth_access_token_for("openai")
|
|
109
|
-
config["account_id"] = creds["account_id"] if creds["account_id"]
|
|
110
|
-
else
|
|
111
|
-
raise ArgumentError, "Unsupported provider: #{provider}"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
LlmGateway.build_provider(config)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def auth_file_path
|
|
118
|
-
File.expand_path(ENV.fetch("LLM_GATEWAY_AUTH_FILE", "~/.config/llm_gateway/auth.json"))
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def load_auth_credentials(provider)
|
|
122
|
-
path = auth_file_path
|
|
123
|
-
raise "missing auth file at #{path}" unless File.exist?(path)
|
|
124
|
-
|
|
125
|
-
auth = JSON.parse(File.read(path))
|
|
126
|
-
creds = auth[provider]
|
|
127
|
-
raise "missing #{provider} credentials in #{path}" unless creds
|
|
128
|
-
|
|
129
|
-
creds
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def persist_auth_credentials(provider, attributes)
|
|
133
|
-
path = auth_file_path
|
|
134
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
135
|
-
|
|
136
|
-
auth = File.exist?(path) ? JSON.parse(File.read(path)) : {}
|
|
137
|
-
auth[provider] ||= {}
|
|
138
|
-
auth[provider].merge!(attributes)
|
|
139
|
-
|
|
140
|
-
File.write(path, JSON.pretty_generate(auth) + "\n")
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def oauth_access_token_for(provider)
|
|
144
|
-
creds = load_auth_credentials(provider)
|
|
145
|
-
|
|
146
|
-
case provider
|
|
147
|
-
when "openai"
|
|
148
|
-
token = LlmGateway::Clients::OpenAi.new.get_oauth_access_token(
|
|
149
|
-
access_token: creds["access_token"],
|
|
150
|
-
refresh_token: creds["refresh_token"],
|
|
151
|
-
expires_at: creds["expires_at"],
|
|
152
|
-
account_id: creds["account_id"]
|
|
153
|
-
) do |access_token, refresh_token, expires_at|
|
|
154
|
-
persist_auth_credentials("openai", {
|
|
155
|
-
"access_token" => access_token,
|
|
156
|
-
"refresh_token" => refresh_token,
|
|
157
|
-
"expires_at" => expires_at&.iso8601
|
|
158
|
-
})
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
persist_auth_credentials("openai", { "access_token" => token }) if token != creds["access_token"]
|
|
162
|
-
token
|
|
163
|
-
else
|
|
164
|
-
raise ArgumentError, "Unsupported OAuth provider: #{provider}"
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
HandoffLiveFixtureGenerator.new.run
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "time"
|
|
5
|
-
require "fileutils"
|
|
6
|
-
require_relative "../lib/llm_gateway"
|
|
7
|
-
require_relative "../test/utils/readfile_tool_helper"
|
|
8
|
-
|
|
9
|
-
class HandoffMediaLiveFixtureGenerator
|
|
10
|
-
include ReadfileToolHelper
|
|
11
|
-
|
|
12
|
-
PAIRS = [
|
|
13
|
-
{ provider: "openai_apikey_completions", model: "gpt-5.1" },
|
|
14
|
-
{ provider: "openai_apikey_responses", model: "gpt-5.4" },
|
|
15
|
-
{ provider: "openai_oauth_codex", model: "gpt-5.4" },
|
|
16
|
-
{ provider: "anthropic_apikey_messages", model: "claude-sonnet-4-20250514" }
|
|
17
|
-
].freeze
|
|
18
|
-
|
|
19
|
-
FIXTURE_PATH = File.expand_path("../test/fixtures/handoff_media_live_fixture.json", __dir__)
|
|
20
|
-
|
|
21
|
-
def run
|
|
22
|
-
flat_transcript = []
|
|
23
|
-
skipped = []
|
|
24
|
-
|
|
25
|
-
PAIRS.each do |pair|
|
|
26
|
-
begin
|
|
27
|
-
adapter = load_provider(provider: pair[:provider], model: pair[:model])
|
|
28
|
-
mini_context = generate_mini_context(adapter)
|
|
29
|
-
flat_transcript.concat(mini_context.map(&:to_h))
|
|
30
|
-
puts "[ok] #{pair[:provider]} / #{pair[:model]}"
|
|
31
|
-
rescue StandardError => e
|
|
32
|
-
skipped << pair.merge(error: e.message)
|
|
33
|
-
puts "[skip] #{pair[:provider]} / #{pair[:model]} -> #{e.message}"
|
|
34
|
-
ensure
|
|
35
|
-
LlmGateway.reset_configuration!
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
FileUtils.mkdir_p(File.dirname(FIXTURE_PATH))
|
|
40
|
-
File.write(FIXTURE_PATH, JSON.pretty_generate(flat_transcript) + "\n")
|
|
41
|
-
|
|
42
|
-
puts "\nWrote fixture: #{FIXTURE_PATH}"
|
|
43
|
-
puts "Messages: #{flat_transcript.length}"
|
|
44
|
-
puts "Skipped pairs: #{skipped.length}"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def generate_mini_context(adapter)
|
|
50
|
-
transcript = [
|
|
51
|
-
{
|
|
52
|
-
role: "user",
|
|
53
|
-
content: "Use the readfile tool with path test/fixtures/red-circle.png and tell me what is in the image."
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
first = adapter.stream(transcript, tools: [ readfile_tool ], reasoning: "high")
|
|
58
|
-
raise first.error_message if first.stop_reason == "error"
|
|
59
|
-
|
|
60
|
-
transcript << first
|
|
61
|
-
|
|
62
|
-
tool_call = first.content.find { |block| block.type == "tool_use" && block.name == "readfile" }
|
|
63
|
-
raise "model did not call readfile" unless tool_call
|
|
64
|
-
|
|
65
|
-
transcript << {
|
|
66
|
-
role: "developer",
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: "tool_result",
|
|
70
|
-
tool_use_id: tool_call.id,
|
|
71
|
-
content: evaluate_readfile(tool_call.input)
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
second = adapter.stream(
|
|
77
|
-
transcript.map { |m| m.respond_to?(:to_h) ? m.to_h : m },
|
|
78
|
-
tools: [ readfile_tool ],
|
|
79
|
-
reasoning: "high"
|
|
80
|
-
)
|
|
81
|
-
raise second.error_message if second.stop_reason == "error"
|
|
82
|
-
|
|
83
|
-
transcript << second
|
|
84
|
-
transcript
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def load_provider(provider:, model:)
|
|
88
|
-
config = {
|
|
89
|
-
"provider" => provider,
|
|
90
|
-
"model_key" => model
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
case provider
|
|
94
|
-
when "openai_apikey_completions", "openai_apikey_responses"
|
|
95
|
-
api_key = ENV["OPENAI_API_KEY"].to_s
|
|
96
|
-
raise "missing OPENAI_API_KEY" if api_key.empty?
|
|
97
|
-
|
|
98
|
-
config["api_key"] = api_key
|
|
99
|
-
when "anthropic_apikey_messages"
|
|
100
|
-
api_key = ENV["ANTHROPIC_API_KEY"].to_s
|
|
101
|
-
raise "missing ANTHROPIC_API_KEY" if api_key.empty?
|
|
102
|
-
|
|
103
|
-
config["api_key"] = api_key
|
|
104
|
-
when "openai_oauth_codex"
|
|
105
|
-
creds = load_auth_credentials("openai")
|
|
106
|
-
config["api_key"] = oauth_access_token_for("openai")
|
|
107
|
-
config["account_id"] = creds["account_id"] if creds["account_id"]
|
|
108
|
-
else
|
|
109
|
-
raise ArgumentError, "Unsupported provider: #{provider}"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
LlmGateway.build_provider(config)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def auth_file_path
|
|
116
|
-
File.expand_path(ENV.fetch("LLM_GATEWAY_AUTH_FILE", "~/.config/llm_gateway/auth.json"))
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def load_auth_credentials(provider)
|
|
120
|
-
path = auth_file_path
|
|
121
|
-
raise "missing auth file at #{path}" unless File.exist?(path)
|
|
122
|
-
|
|
123
|
-
auth = JSON.parse(File.read(path))
|
|
124
|
-
creds = auth[provider]
|
|
125
|
-
raise "missing #{provider} credentials in #{path}" unless creds
|
|
126
|
-
|
|
127
|
-
creds
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def persist_auth_credentials(provider, attributes)
|
|
131
|
-
path = auth_file_path
|
|
132
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
133
|
-
|
|
134
|
-
auth = File.exist?(path) ? JSON.parse(File.read(path)) : {}
|
|
135
|
-
auth[provider] ||= {}
|
|
136
|
-
auth[provider].merge!(attributes)
|
|
137
|
-
|
|
138
|
-
File.write(path, JSON.pretty_generate(auth) + "\n")
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def oauth_access_token_for(provider)
|
|
142
|
-
creds = load_auth_credentials(provider)
|
|
143
|
-
|
|
144
|
-
case provider
|
|
145
|
-
when "openai"
|
|
146
|
-
token = LlmGateway::Clients::OpenAi.new.get_oauth_access_token(
|
|
147
|
-
access_token: creds["access_token"],
|
|
148
|
-
refresh_token: creds["refresh_token"],
|
|
149
|
-
expires_at: creds["expires_at"],
|
|
150
|
-
account_id: creds["account_id"]
|
|
151
|
-
) do |access_token, refresh_token, expires_at|
|
|
152
|
-
persist_auth_credentials("openai", {
|
|
153
|
-
"access_token" => access_token,
|
|
154
|
-
"refresh_token" => refresh_token,
|
|
155
|
-
"expires_at" => expires_at&.iso8601
|
|
156
|
-
})
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
persist_auth_credentials("openai", { "access_token" => token }) if token != creds["access_token"]
|
|
160
|
-
token
|
|
161
|
-
else
|
|
162
|
-
raise ArgumentError, "Unsupported OAuth provider: #{provider}"
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
HandoffMediaLiveFixtureGenerator.new.run
|