lex-claude 0.1.2 → 0.3.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/.github/CODEOWNERS +7 -0
- data/.github/dependabot.yml +18 -0
- data/.github/workflows/ci.yml +19 -1
- data/.rubocop.yml +2 -50
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +1 -0
- data/LICENSE +201 -21
- data/README.md +144 -37
- data/lib/legion/extensions/claude/client.rb +1 -0
- data/lib/legion/extensions/claude/helpers/client.rb +49 -4
- data/lib/legion/extensions/claude/helpers/errors.rb +71 -0
- data/lib/legion/extensions/claude/helpers/models.rb +48 -0
- data/lib/legion/extensions/claude/helpers/response.rb +61 -0
- data/lib/legion/extensions/claude/helpers/retry.rb +41 -0
- data/lib/legion/extensions/claude/helpers/sse.rb +69 -0
- data/lib/legion/extensions/claude/helpers/tools.rb +32 -0
- data/lib/legion/extensions/claude/runners/batches.rb +8 -7
- data/lib/legion/extensions/claude/runners/messages.rb +98 -26
- data/lib/legion/extensions/claude/runners/models.rb +5 -4
- data/lib/legion/extensions/claude/version.rb +1 -1
- data/lib/legion/extensions/claude.rb +7 -1
- metadata +9 -1
|
@@ -9,19 +9,64 @@ module Legion
|
|
|
9
9
|
module Helpers
|
|
10
10
|
module Client
|
|
11
11
|
DEFAULT_HOST = 'https://api.anthropic.com'
|
|
12
|
-
API_VERSION
|
|
12
|
+
API_VERSION = '2023-06-01'
|
|
13
|
+
|
|
14
|
+
BETA_HEADERS = {
|
|
15
|
+
interleaved_thinking: 'interleaved-thinking-2025-05-14',
|
|
16
|
+
context_1m: 'context-1m-2025-08-07',
|
|
17
|
+
context_management: 'context-management-2025-06-27',
|
|
18
|
+
structured_outputs: 'structured-outputs-2025-12-15',
|
|
19
|
+
web_search: 'web-search-2025-03-05',
|
|
20
|
+
advanced_tool_use: 'advanced-tool-use-2025-11-20',
|
|
21
|
+
effort: 'effort-2025-11-24',
|
|
22
|
+
task_budgets: 'task-budgets-2026-03-13',
|
|
23
|
+
prompt_caching_scope: 'prompt-caching-scope-2026-01-05',
|
|
24
|
+
fast_mode: 'fast-mode-2026-02-01',
|
|
25
|
+
redact_thinking: 'redact-thinking-2026-02-12',
|
|
26
|
+
token_efficient_tools: 'token-efficient-tools-2026-03-28',
|
|
27
|
+
summarize_connector: 'summarize-connector-text-2026-03-13',
|
|
28
|
+
afk_mode: 'afk-mode-2026-01-31',
|
|
29
|
+
advisor: 'advisor-tool-2026-03-01',
|
|
30
|
+
files_api: 'files-api-2025-04-14',
|
|
31
|
+
claude_code: 'claude-code-20250219',
|
|
32
|
+
tool_search: 'tool-search-tool-2025-10-19'
|
|
33
|
+
}.freeze
|
|
13
34
|
|
|
14
35
|
module_function
|
|
15
36
|
|
|
16
|
-
def client(api_key:, host: DEFAULT_HOST, **_opts)
|
|
37
|
+
def client(api_key:, host: DEFAULT_HOST, betas: nil, **_opts)
|
|
38
|
+
beta_list = resolve_betas(betas)
|
|
39
|
+
|
|
17
40
|
Faraday.new(url: host) do |conn|
|
|
18
41
|
conn.request :json
|
|
19
42
|
conn.response :json, content_type: /\bjson$/
|
|
20
43
|
conn.headers['x-api-key'] = api_key
|
|
21
|
-
conn.headers['anthropic-version']
|
|
22
|
-
conn.headers['Content-Type']
|
|
44
|
+
conn.headers['anthropic-version'] = API_VERSION
|
|
45
|
+
conn.headers['Content-Type'] = 'application/json'
|
|
46
|
+
conn.headers['anthropic-beta'] = beta_list.join(',') if beta_list.any?
|
|
23
47
|
end
|
|
24
48
|
end
|
|
49
|
+
|
|
50
|
+
def streaming_client(api_key:, host: DEFAULT_HOST, betas: nil, **_opts)
|
|
51
|
+
beta_list = resolve_betas(betas)
|
|
52
|
+
|
|
53
|
+
Faraday.new(url: host) do |conn|
|
|
54
|
+
conn.headers['x-api-key'] = api_key
|
|
55
|
+
conn.headers['anthropic-version'] = API_VERSION
|
|
56
|
+
conn.headers['Content-Type'] = 'application/json'
|
|
57
|
+
conn.headers['Accept'] = 'text/event-stream'
|
|
58
|
+
conn.headers['anthropic-beta'] = beta_list.join(',') if beta_list.any?
|
|
59
|
+
conn.adapter Faraday.default_adapter
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolve_betas(betas)
|
|
64
|
+
return [] if betas.nil? || betas.empty?
|
|
65
|
+
|
|
66
|
+
betas.filter_map do |b|
|
|
67
|
+
b.is_a?(Symbol) ? BETA_HEADERS[b] : b.to_s
|
|
68
|
+
end.uniq
|
|
69
|
+
end
|
|
25
70
|
end
|
|
26
71
|
end
|
|
27
72
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Claude
|
|
6
|
+
module Helpers
|
|
7
|
+
module Errors
|
|
8
|
+
class ApiError < StandardError
|
|
9
|
+
attr_reader :status, :error_type, :body
|
|
10
|
+
|
|
11
|
+
def initialize(message = nil, status: nil, error_type: nil, body: nil)
|
|
12
|
+
super(message)
|
|
13
|
+
@status = status
|
|
14
|
+
@error_type = error_type
|
|
15
|
+
@body = body
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class AuthenticationError < ApiError; end
|
|
20
|
+
class PermissionError < ApiError; end
|
|
21
|
+
class NotFoundError < ApiError; end
|
|
22
|
+
class RateLimitError < ApiError; end
|
|
23
|
+
class OverloadedError < ApiError; end
|
|
24
|
+
class InvalidRequestError < ApiError; end
|
|
25
|
+
class ServerError < ApiError; end
|
|
26
|
+
class StreamingError < ApiError; end
|
|
27
|
+
|
|
28
|
+
STATUS_MAP = {
|
|
29
|
+
401 => AuthenticationError,
|
|
30
|
+
403 => PermissionError,
|
|
31
|
+
404 => NotFoundError,
|
|
32
|
+
429 => RateLimitError,
|
|
33
|
+
529 => OverloadedError
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
TYPE_MAP = {
|
|
37
|
+
'authentication_error' => AuthenticationError,
|
|
38
|
+
'permission_error' => PermissionError,
|
|
39
|
+
'not_found_error' => NotFoundError,
|
|
40
|
+
'rate_limit_error' => RateLimitError,
|
|
41
|
+
'overloaded_error' => OverloadedError,
|
|
42
|
+
'invalid_request_error' => InvalidRequestError,
|
|
43
|
+
'server_error' => ServerError,
|
|
44
|
+
'streaming_error' => StreamingError
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
RETRYABLE = [RateLimitError, OverloadedError].freeze
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
def from_response(status:, body:)
|
|
52
|
+
error_hash = body.is_a?(Hash) ? (body[:error] || body['error']) : nil # rubocop:disable Legion/Framework/ApiStringKeys
|
|
53
|
+
error_type = error_hash.is_a?(Hash) ? (error_hash[:type] || error_hash['type']) : nil
|
|
54
|
+
message = error_hash.is_a?(Hash) ? (error_hash[:message] || error_hash['message']) : nil
|
|
55
|
+
message ||= body.to_s
|
|
56
|
+
|
|
57
|
+
klass = TYPE_MAP[error_type] ||
|
|
58
|
+
STATUS_MAP[status] ||
|
|
59
|
+
(status >= 500 ? ServerError : InvalidRequestError)
|
|
60
|
+
|
|
61
|
+
klass.new(message, status: status, error_type: error_type, body: body)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def retryable?(error)
|
|
65
|
+
RETRYABLE.any? { |klass| error.is_a?(klass) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Claude
|
|
6
|
+
module Helpers
|
|
7
|
+
module Models
|
|
8
|
+
# rubocop:disable Naming/VariableNumber
|
|
9
|
+
MODELS = {
|
|
10
|
+
haiku_3_5: 'claude-3-5-haiku-20241022',
|
|
11
|
+
haiku_4_5: 'claude-haiku-4-5-20251001',
|
|
12
|
+
sonnet_3_5: 'claude-3-5-sonnet-20241022',
|
|
13
|
+
sonnet_3_7: 'claude-3-7-sonnet-20250219',
|
|
14
|
+
sonnet_4: 'claude-sonnet-4-20250514',
|
|
15
|
+
sonnet_4_5: 'claude-sonnet-4-5-20250929',
|
|
16
|
+
sonnet_4_6: 'claude-sonnet-4-6',
|
|
17
|
+
opus_4: 'claude-opus-4-20250514',
|
|
18
|
+
opus_4_1: 'claude-opus-4-1-20250805',
|
|
19
|
+
opus_4_5: 'claude-opus-4-5-20251101',
|
|
20
|
+
opus_4_6: 'claude-opus-4-6'
|
|
21
|
+
}.freeze
|
|
22
|
+
# rubocop:enable Naming/VariableNumber
|
|
23
|
+
|
|
24
|
+
ADAPTIVE_THINKING_MODELS = %w[
|
|
25
|
+
claude-sonnet-4-20250514
|
|
26
|
+
claude-sonnet-4-5-20250929
|
|
27
|
+
claude-sonnet-4-6
|
|
28
|
+
claude-opus-4-20250514
|
|
29
|
+
claude-opus-4-1-20250805
|
|
30
|
+
claude-opus-4-5-20251101
|
|
31
|
+
claude-opus-4-6
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def resolve(model)
|
|
37
|
+
key = model.is_a?(Symbol) ? model : model.to_s.to_sym
|
|
38
|
+
MODELS.fetch(key, model.to_s)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def adaptive_thinking?(model_id)
|
|
42
|
+
ADAPTIVE_THINKING_MODELS.include?(model_id.to_s)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/claude/helpers/errors'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Claude
|
|
8
|
+
module Helpers
|
|
9
|
+
module Response
|
|
10
|
+
RATE_LIMIT_HEADERS = {
|
|
11
|
+
'anthropic-ratelimit-unified-status' => :status,
|
|
12
|
+
'anthropic-ratelimit-unified-reset' => :reset,
|
|
13
|
+
'anthropic-ratelimit-unified-fallback' => :fallback,
|
|
14
|
+
'anthropic-ratelimit-unified-5h-utilization' => :utilization_5h,
|
|
15
|
+
'anthropic-ratelimit-unified-5h-reset' => :reset_5h,
|
|
16
|
+
'anthropic-ratelimit-unified-7d-utilization' => :utilization_7d,
|
|
17
|
+
'anthropic-ratelimit-unified-7d-reset' => :reset_7d,
|
|
18
|
+
'anthropic-ratelimit-unified-overage-status' => :overage_status,
|
|
19
|
+
'anthropic-ratelimit-unified-overage-reset' => :overage_reset
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
FLOAT_KEYS = %i[utilization_5h utilization_7d].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def handle_response(response)
|
|
27
|
+
raise Errors.from_response(status: response.status, body: response.body) unless response.status >= 200 && response.status < 300
|
|
28
|
+
|
|
29
|
+
result = { result: response.body, status: response.status }
|
|
30
|
+
rate_info = parse_rate_limit_headers(response.headers)
|
|
31
|
+
result[:rate_limit] = rate_info unless rate_info.empty?
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_rate_limit_headers(headers)
|
|
36
|
+
return {} if headers.nil? || headers.empty?
|
|
37
|
+
|
|
38
|
+
parsed = {}
|
|
39
|
+
RATE_LIMIT_HEADERS.each do |header_name, key|
|
|
40
|
+
value = headers[header_name]
|
|
41
|
+
next if value.nil?
|
|
42
|
+
|
|
43
|
+
parsed[key] = FLOAT_KEYS.include?(key) ? value.to_f : value
|
|
44
|
+
end
|
|
45
|
+
parsed
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_usage(body)
|
|
49
|
+
usage = body.is_a?(Hash) ? (body[:usage] || body['usage'] || {}) : {} # rubocop:disable Legion/Framework/ApiStringKeys
|
|
50
|
+
{
|
|
51
|
+
input_tokens: (usage[:input_tokens] || usage['input_tokens'] || 0).to_i,
|
|
52
|
+
output_tokens: (usage[:output_tokens] || usage['output_tokens'] || 0).to_i,
|
|
53
|
+
cache_read_tokens: (usage[:cache_read_input_tokens] || usage['cache_read_input_tokens'] || 0).to_i,
|
|
54
|
+
cache_write_tokens: (usage[:cache_creation_input_tokens] || usage['cache_creation_input_tokens'] || 0).to_i
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/claude/helpers/errors'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Claude
|
|
8
|
+
module Helpers
|
|
9
|
+
module Retry
|
|
10
|
+
DEFAULT_MAX_ATTEMPTS = 3
|
|
11
|
+
DEFAULT_BASE_DELAY = 1.0
|
|
12
|
+
DEFAULT_MAX_DELAY = 60.0
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def with_retry(max_attempts: DEFAULT_MAX_ATTEMPTS, base_delay: DEFAULT_BASE_DELAY,
|
|
17
|
+
max_delay: DEFAULT_MAX_DELAY)
|
|
18
|
+
attempt = 0
|
|
19
|
+
begin
|
|
20
|
+
yield
|
|
21
|
+
rescue Errors::ApiError => e
|
|
22
|
+
raise unless Errors.retryable?(e)
|
|
23
|
+
|
|
24
|
+
attempt += 1
|
|
25
|
+
raise if attempt >= max_attempts
|
|
26
|
+
|
|
27
|
+
delay = backoff_seconds(attempt: attempt - 1, base_delay: base_delay, max_delay: max_delay)
|
|
28
|
+
sleep(delay) if delay.positive?
|
|
29
|
+
retry
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def backoff_seconds(attempt:, base_delay: DEFAULT_BASE_DELAY, max_delay: DEFAULT_MAX_DELAY)
|
|
34
|
+
raw = base_delay * (2**attempt)
|
|
35
|
+
[raw, max_delay].min.to_f
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'multi_json'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Claude
|
|
8
|
+
module Helpers
|
|
9
|
+
module Sse
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def parse_stream(raw, include_pings: false)
|
|
13
|
+
events = []
|
|
14
|
+
current_event = nil
|
|
15
|
+
|
|
16
|
+
raw.each_line do |line|
|
|
17
|
+
line = line.chomp
|
|
18
|
+
if line.start_with?('event:')
|
|
19
|
+
current_event = line.sub(/^event:\s*/, '').strip
|
|
20
|
+
elsif line.start_with?('data:')
|
|
21
|
+
next if current_event == 'ping' && !include_pings
|
|
22
|
+
|
|
23
|
+
json_str = line.sub(/^data:\s*/, '').strip
|
|
24
|
+
next if json_str.empty?
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
data = MultiJson.load(json_str)
|
|
28
|
+
events << { event: current_event, data: data }
|
|
29
|
+
rescue MultiJson::ParseError => e
|
|
30
|
+
log.warn("SSE parse error: #{e.message}")
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
current_event = nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
events
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def collect_text(events)
|
|
41
|
+
events
|
|
42
|
+
.select { |e| e[:event] == 'content_block_delta' && e[:data].dig('delta', 'type') == 'text_delta' }
|
|
43
|
+
.map { |e| e[:data].dig('delta', 'text').to_s }
|
|
44
|
+
.join
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def collect_usage(events)
|
|
48
|
+
input_tokens = 0
|
|
49
|
+
output_tokens = 0
|
|
50
|
+
|
|
51
|
+
events.each do |e|
|
|
52
|
+
case e[:event]
|
|
53
|
+
when 'message_start'
|
|
54
|
+
usage = e[:data].dig('message', 'usage') || {}
|
|
55
|
+
input_tokens += usage.fetch('input_tokens', 0).to_i
|
|
56
|
+
output_tokens += usage.fetch('output_tokens', 0).to_i
|
|
57
|
+
when 'message_delta'
|
|
58
|
+
usage = e[:data].fetch('usage', {})
|
|
59
|
+
output_tokens += usage.fetch('output_tokens', 0).to_i
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
{ input_tokens: input_tokens, output_tokens: output_tokens }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Claude
|
|
6
|
+
module Helpers
|
|
7
|
+
module Tools
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def web_search(max_uses: 5, allowed_domains: nil, blocked_domains: nil)
|
|
11
|
+
tool = { type: 'web_search_20250305', max_uses: max_uses }
|
|
12
|
+
tool[:allowed_domains] = allowed_domains if allowed_domains
|
|
13
|
+
tool[:blocked_domains] = blocked_domains if blocked_domains
|
|
14
|
+
tool
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cache_control
|
|
18
|
+
{ type: 'ephemeral' }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def required_betas_for(tools)
|
|
22
|
+
return [] if tools.nil? || tools.empty?
|
|
23
|
+
|
|
24
|
+
betas = []
|
|
25
|
+
betas << :web_search if tools.any? { |t| t.is_a?(Hash) && t[:type].to_s.start_with?('web_search') }
|
|
26
|
+
betas
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/claude/helpers/client'
|
|
4
|
+
require 'legion/extensions/claude/helpers/response'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Extensions
|
|
@@ -12,7 +13,7 @@ module Legion
|
|
|
12
13
|
def create_batch(api_key:, requests:, **)
|
|
13
14
|
body = { requests: requests }
|
|
14
15
|
response = client(api_key: api_key, **).post('/v1/messages/batches', body)
|
|
15
|
-
|
|
16
|
+
Helpers::Response.handle_response(response)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def list_batches(api_key:, limit: 20, before_id: nil, after_id: nil, **)
|
|
@@ -21,26 +22,26 @@ module Legion
|
|
|
21
22
|
params[:after_id] = after_id if after_id
|
|
22
23
|
|
|
23
24
|
response = client(api_key: api_key, **).get('/v1/messages/batches', params)
|
|
24
|
-
|
|
25
|
+
Helpers::Response.handle_response(response)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def retrieve_batch(api_key:, batch_id:, **)
|
|
28
29
|
response = client(api_key: api_key, **).get("/v1/messages/batches/#{batch_id}")
|
|
29
|
-
|
|
30
|
+
Helpers::Response.handle_response(response)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def cancel_batch(api_key:, batch_id:, **)
|
|
33
34
|
response = client(api_key: api_key, **).post("/v1/messages/batches/#{batch_id}/cancel")
|
|
34
|
-
|
|
35
|
+
Helpers::Response.handle_response(response)
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def batch_results(api_key:, batch_id:, **)
|
|
38
39
|
response = client(api_key: api_key, **).get("/v1/messages/batches/#{batch_id}/results")
|
|
39
|
-
|
|
40
|
+
Helpers::Response.handle_response(response)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
43
|
-
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
43
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
44
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/claude/helpers/client'
|
|
4
|
+
require 'legion/extensions/claude/helpers/response'
|
|
5
|
+
require 'legion/extensions/claude/helpers/sse'
|
|
4
6
|
|
|
5
7
|
module Legion
|
|
6
8
|
module Extensions
|
|
@@ -9,39 +11,109 @@ module Legion
|
|
|
9
11
|
module Messages
|
|
10
12
|
extend Legion::Extensions::Claude::Helpers::Client
|
|
11
13
|
|
|
12
|
-
def create(api_key:, model:, messages:, max_tokens: 1024,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
def create(api_key:, model:, messages:, max_tokens: 1024, stream: false, betas: nil, **opts)
|
|
15
|
+
body = build_message_body(model: model, messages: messages, max_tokens: max_tokens, stream: stream, **opts)
|
|
16
|
+
resolved_betas = resolve_feature_betas(betas, opts)
|
|
17
|
+
|
|
18
|
+
response = client(api_key: api_key, betas: resolved_betas, **opts).post('/v1/messages', body)
|
|
19
|
+
result = Helpers::Response.handle_response(response)
|
|
20
|
+
result[:usage] = Helpers::Response.parse_usage(response.body) if response.body.is_a?(Hash)
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_stream(api_key:, model:, messages:, max_tokens: 1024, betas: nil, **opts, &block)
|
|
25
|
+
body = build_message_body(model: model, messages: messages, max_tokens: max_tokens, stream: true, **opts)
|
|
26
|
+
resolved_betas = resolve_feature_betas(betas, opts)
|
|
27
|
+
|
|
28
|
+
raw_body = +''
|
|
29
|
+
conn = Helpers::Client.streaming_client(api_key: api_key, betas: resolved_betas)
|
|
30
|
+
response = conn.post('/v1/messages', MultiJson.dump(body)) do |req|
|
|
31
|
+
req.options.on_data = proc { |chunk, _bytes| raw_body << chunk }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
raise Helpers::Errors.from_response(status: response.status, body: {}) unless response.status == 200
|
|
35
|
+
|
|
36
|
+
raw_body = response.body if raw_body.empty? && response.body.is_a?(String)
|
|
37
|
+
|
|
38
|
+
events = Helpers::Sse.parse_stream(raw_body)
|
|
39
|
+
events.each(&block) if block
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
result: Helpers::Sse.collect_text(events),
|
|
43
|
+
events: events,
|
|
44
|
+
usage: Helpers::Sse.collect_usage(events),
|
|
45
|
+
status: 200
|
|
20
46
|
}
|
|
21
|
-
body[:system] = system if system
|
|
22
|
-
body[:temperature] = temperature if temperature
|
|
23
|
-
body[:top_p] = top_p if top_p
|
|
24
|
-
body[:top_k] = top_k if top_k
|
|
25
|
-
body[:stop_sequences] = stop_sequences if stop_sequences
|
|
26
|
-
body[:metadata] = metadata if metadata
|
|
27
|
-
body[:tools] = tools if tools
|
|
28
|
-
body[:tool_choice] = tool_choice if tool_choice
|
|
29
|
-
|
|
30
|
-
response = client(api_key: api_key, **).post('/v1/messages', body)
|
|
31
|
-
{ result: response.body, status: response.status }
|
|
32
47
|
end
|
|
33
48
|
|
|
34
|
-
def count_tokens(api_key:, model:, messages:,
|
|
49
|
+
def count_tokens(api_key:, model:, messages:, betas: nil, **opts)
|
|
50
|
+
system = opts[:system]
|
|
51
|
+
tools = opts[:tools]
|
|
52
|
+
thinking = opts[:thinking]
|
|
53
|
+
cache_system = opts.fetch(:cache_system, false)
|
|
54
|
+
|
|
35
55
|
body = { model: model, messages: messages }
|
|
36
|
-
body[:system]
|
|
37
|
-
body[:tools]
|
|
56
|
+
body[:system] = build_system(system, cache_system) if system
|
|
57
|
+
body[:tools] = tools if tools
|
|
58
|
+
body[:thinking] = thinking if thinking
|
|
59
|
+
|
|
60
|
+
resolved_betas = Array(betas).dup
|
|
61
|
+
resolved_betas << :interleaved_thinking if thinking && !resolved_betas.include?(:interleaved_thinking)
|
|
62
|
+
|
|
63
|
+
response = client(api_key: api_key, betas: resolved_betas).post('/v1/messages/count_tokens', body)
|
|
64
|
+
Helpers::Response.handle_response(response)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def build_message_body(model:, messages:, max_tokens:, stream:, system: nil, temperature: nil, # rubocop:disable Metrics/ParameterLists
|
|
70
|
+
top_p: nil, top_k: nil, stop_sequences: nil, metadata: nil, tools: nil,
|
|
71
|
+
tool_choice: nil, cache_system: false, thinking: nil, output_config: nil,
|
|
72
|
+
fast_mode: false, context_management: nil, **)
|
|
73
|
+
body = { model: model, messages: messages, max_tokens: max_tokens, stream: stream }
|
|
74
|
+
|
|
75
|
+
body[:system] = build_system(system, cache_system) if system
|
|
76
|
+
body[:top_p] = top_p if top_p
|
|
77
|
+
body[:top_k] = top_k if top_k
|
|
78
|
+
body[:stop_sequences] = stop_sequences if stop_sequences
|
|
79
|
+
body[:metadata] = metadata if metadata
|
|
80
|
+
body[:tools] = tools if tools
|
|
81
|
+
body[:tool_choice] = tool_choice if tool_choice
|
|
82
|
+
body[:output_config] = output_config if output_config
|
|
83
|
+
body[:speed] = 'fast' if fast_mode
|
|
84
|
+
body[:context_management] = context_management if context_management
|
|
85
|
+
|
|
86
|
+
if thinking
|
|
87
|
+
body[:thinking] = thinking
|
|
88
|
+
elsif temperature
|
|
89
|
+
body[:temperature] = temperature
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
body
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def resolve_feature_betas(betas, opts)
|
|
96
|
+
resolved = Array(betas).dup
|
|
97
|
+
resolved << :prompt_caching_scope if opts[:cache_scope] == :global
|
|
98
|
+
resolved << :interleaved_thinking if opts[:thinking] && !resolved.include?(:interleaved_thinking)
|
|
99
|
+
resolved << :structured_outputs if opts[:output_config]&.key?(:format)
|
|
100
|
+
resolved << :effort if opts[:output_config]&.key?(:effort)
|
|
101
|
+
resolved << :task_budgets if opts[:output_config]&.key?(:task_budget)
|
|
102
|
+
resolved << :fast_mode if opts[:fast_mode]
|
|
103
|
+
resolved << :context_management if opts[:context_management]
|
|
104
|
+
resolved
|
|
105
|
+
end
|
|
38
106
|
|
|
39
|
-
|
|
40
|
-
|
|
107
|
+
def build_system(system, cache_system)
|
|
108
|
+
if cache_system
|
|
109
|
+
[{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }]
|
|
110
|
+
else
|
|
111
|
+
system
|
|
112
|
+
end
|
|
41
113
|
end
|
|
42
114
|
|
|
43
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
44
|
-
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
115
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
116
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
45
117
|
end
|
|
46
118
|
end
|
|
47
119
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/claude/helpers/client'
|
|
4
|
+
require 'legion/extensions/claude/helpers/response'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Extensions
|
|
@@ -15,16 +16,16 @@ module Legion
|
|
|
15
16
|
params[:after_id] = after_id if after_id
|
|
16
17
|
|
|
17
18
|
response = client(api_key: api_key, **).get('/v1/models', params)
|
|
18
|
-
|
|
19
|
+
Helpers::Response.handle_response(response)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def retrieve(api_key:, model_id:, **)
|
|
22
23
|
response = client(api_key: api_key, **).get("/v1/models/#{model_id}")
|
|
23
|
-
|
|
24
|
+
Helpers::Response.handle_response(response)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
27
|
-
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
27
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
28
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
end
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/claude/version'
|
|
4
4
|
require 'legion/extensions/claude/helpers/client'
|
|
5
|
+
require 'legion/extensions/claude/helpers/errors'
|
|
6
|
+
require 'legion/extensions/claude/helpers/retry'
|
|
7
|
+
require 'legion/extensions/claude/helpers/sse'
|
|
8
|
+
require 'legion/extensions/claude/helpers/response'
|
|
9
|
+
require 'legion/extensions/claude/helpers/tools'
|
|
10
|
+
require 'legion/extensions/claude/helpers/models'
|
|
5
11
|
require 'legion/extensions/claude/runners/messages'
|
|
6
12
|
require 'legion/extensions/claude/runners/models'
|
|
7
13
|
require 'legion/extensions/claude/runners/batches'
|
|
@@ -9,7 +15,7 @@ require 'legion/extensions/claude/runners/batches'
|
|
|
9
15
|
module Legion
|
|
10
16
|
module Extensions
|
|
11
17
|
module Claude
|
|
12
|
-
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
18
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
|
|
13
19
|
end
|
|
14
20
|
end
|
|
15
21
|
end
|