rasti-ai 3.0.1 → 3.2.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/.features/api-normalization/api-design.md +184 -0
- data/.features/api-normalization/overview.md +230 -0
- data/AGENTS.md +167 -10
- data/README.md +161 -23
- data/lib/rasti/ai/anthropic/assistant.rb +1 -11
- data/lib/rasti/ai/anthropic/client.rb +6 -28
- data/lib/rasti/ai/anthropic/provider.rb +87 -0
- data/lib/rasti/ai/assistant.rb +10 -11
- data/lib/rasti/ai/client.rb +13 -10
- data/lib/rasti/ai/errors.rb +8 -0
- data/lib/rasti/ai/gemini/assistant.rb +1 -11
- data/lib/rasti/ai/gemini/client.rb +2 -24
- data/lib/rasti/ai/gemini/provider.rb +90 -0
- data/lib/rasti/ai/huawei_maas/assistant.rb +8 -0
- data/lib/rasti/ai/huawei_maas/client.rb +8 -0
- data/lib/rasti/ai/huawei_maas/provider.rb +25 -0
- data/lib/rasti/ai/huawei_maas/roles.rb +7 -0
- data/lib/rasti/ai/open_ai/assistant.rb +0 -4
- data/lib/rasti/ai/open_ai/client.rb +4 -26
- data/lib/rasti/ai/open_ai/provider.rb +74 -0
- data/lib/rasti/ai/open_router/assistant.rb +8 -0
- data/lib/rasti/ai/open_router/client.rb +8 -0
- data/lib/rasti/ai/open_router/provider.rb +25 -0
- data/lib/rasti/ai/open_router/roles.rb +7 -0
- data/lib/rasti/ai/provider.rb +197 -0
- data/lib/rasti/ai/provider_aware.rb +17 -0
- data/lib/rasti/ai/result.rb +11 -0
- data/lib/rasti/ai/roles.rb +11 -0
- data/lib/rasti/ai/version.rb +1 -1
- data/lib/rasti/ai.rb +101 -1
- data/spec/anthropic/provider_spec.rb +106 -0
- data/spec/gemini/provider_spec.rb +90 -0
- data/spec/huawei_maas/assistant_spec.rb +80 -0
- data/spec/huawei_maas/client_spec.rb +60 -0
- data/spec/minitest_helper.rb +6 -0
- data/spec/open_ai/provider_spec.rb +101 -0
- data/spec/open_router/assistant_spec.rb +80 -0
- data/spec/open_router/client_spec.rb +60 -0
- data/spec/rasti_ai_spec.rb +322 -0
- data/spec/resources/open_ai/conversation_request.json +1 -0
- data/spec/resources/open_ai/system_request.json +1 -0
- data/tasks/assistant.rake +5 -3
- metadata +39 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Rasti
|
|
2
|
+
module AI
|
|
3
|
+
module Anthropic
|
|
4
|
+
class Provider < Rasti::AI::Provider
|
|
5
|
+
|
|
6
|
+
STRUCTURED_OUTPUT_TOOL = 'structured_output'.freeze
|
|
7
|
+
|
|
8
|
+
THINKING_LEVELS = {
|
|
9
|
+
'low' => {type: 'enabled', budget_tokens: 1_024}.freeze,
|
|
10
|
+
'medium' => {type: 'enabled', budget_tokens: 8_000}.freeze,
|
|
11
|
+
'high' => {type: 'enabled', budget_tokens: 16_000}.freeze
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def name
|
|
15
|
+
:anthropic
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def default_model
|
|
19
|
+
Rasti::AI.anthropic_default_model
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def default_api_key
|
|
23
|
+
Rasti::AI.anthropic_api_key
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def base_url
|
|
27
|
+
'https://api.anthropic.com/v1'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_usage(response)
|
|
31
|
+
usage = response['usage']
|
|
32
|
+
return nil unless usage
|
|
33
|
+
|
|
34
|
+
Usage.new(
|
|
35
|
+
provider: name.to_s,
|
|
36
|
+
model: response['model'],
|
|
37
|
+
input_tokens: usage['input_tokens'],
|
|
38
|
+
output_tokens: usage['output_tokens'],
|
|
39
|
+
cached_tokens: usage['cache_read_input_tokens'] || 0,
|
|
40
|
+
reasoning_tokens: 0,
|
|
41
|
+
raw: usage
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def request(client:, messages:, system:, model:, json_schema:, thinking:)
|
|
48
|
+
tools = json_schema ? [structured_output_tool(json_schema)] : []
|
|
49
|
+
tool_choice = json_schema ? {type: 'tool', name: STRUCTURED_OUTPUT_TOOL} : nil
|
|
50
|
+
|
|
51
|
+
client.messages messages: messages,
|
|
52
|
+
model: model,
|
|
53
|
+
system: system,
|
|
54
|
+
tools: tools,
|
|
55
|
+
tool_choice: tool_choice,
|
|
56
|
+
thinking: thinking
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def encode_message(message)
|
|
60
|
+
{role: message[:role], content: message[:content]}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_content(response)
|
|
64
|
+
content = response['content'] || []
|
|
65
|
+
|
|
66
|
+
structured = content.find { |block| block['type'] == 'tool_use' && block['name'] == STRUCTURED_OUTPUT_TOOL }
|
|
67
|
+
return JSON.dump(structured['input']) if structured
|
|
68
|
+
|
|
69
|
+
text_block = content.find { |block| block['type'] == 'text' }
|
|
70
|
+
text_block['text'] if text_block
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def structured_output_tool(json_schema)
|
|
74
|
+
{
|
|
75
|
+
name: STRUCTURED_OUTPUT_TOOL,
|
|
76
|
+
description: 'Return the structured response',
|
|
77
|
+
input_schema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: json_schema
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/rasti/ai/assistant.rb
CHANGED
|
@@ -2,14 +2,17 @@ module Rasti
|
|
|
2
2
|
module AI
|
|
3
3
|
class Assistant
|
|
4
4
|
|
|
5
|
+
include ProviderAware
|
|
6
|
+
|
|
5
7
|
attr_reader :state, :model, :thinking
|
|
6
8
|
|
|
7
9
|
VALID_THINKING_LEVELS = %w[low medium high].freeze
|
|
8
10
|
|
|
9
|
-
def initialize(client:nil, json_schema:nil, state:nil, model:nil, thinking:nil, tools:[], mcp_servers:{}, logger:nil)
|
|
11
|
+
def initialize(client:nil, provider:nil, json_schema:nil, state:nil, model:nil, thinking:nil, tools:[], mcp_servers:{}, logger:nil)
|
|
10
12
|
raise ArgumentError, "Invalid thinking level '#{thinking}'. Valid: #{VALID_THINKING_LEVELS.join(', ')}" if thinking && !VALID_THINKING_LEVELS.include?(thinking)
|
|
11
13
|
|
|
12
|
-
@
|
|
14
|
+
@provider = provider
|
|
15
|
+
@client = client
|
|
13
16
|
@json_schema = json_schema
|
|
14
17
|
@state = state || AssistantState.new
|
|
15
18
|
@model = model
|
|
@@ -50,14 +53,16 @@ module Rasti
|
|
|
50
53
|
|
|
51
54
|
private
|
|
52
55
|
|
|
53
|
-
attr_reader :
|
|
56
|
+
attr_reader :json_schema, :tools, :serialized_tools, :logger
|
|
57
|
+
|
|
58
|
+
def client
|
|
59
|
+
@client ||= provider.build_client
|
|
60
|
+
end
|
|
54
61
|
|
|
55
62
|
def messages
|
|
56
63
|
state.messages
|
|
57
64
|
end
|
|
58
65
|
|
|
59
|
-
# --- Shared behavior ---
|
|
60
|
-
|
|
61
66
|
def register_tools(tools)
|
|
62
67
|
tools.each do |tool|
|
|
63
68
|
serialization = wrap_tool_serialization(ToolSerializer.serialize(tool.class))
|
|
@@ -99,12 +104,6 @@ module Rasti
|
|
|
99
104
|
"Error: #{ex.message}"
|
|
100
105
|
end
|
|
101
106
|
|
|
102
|
-
# --- Template methods ---
|
|
103
|
-
|
|
104
|
-
def build_default_client
|
|
105
|
-
raise NotImplementedError
|
|
106
|
-
end
|
|
107
|
-
|
|
108
107
|
def build_user_message(prompt)
|
|
109
108
|
raise NotImplementedError
|
|
110
109
|
end
|
data/lib/rasti/ai/client.rb
CHANGED
|
@@ -2,9 +2,12 @@ module Rasti
|
|
|
2
2
|
module AI
|
|
3
3
|
class Client
|
|
4
4
|
|
|
5
|
+
include ProviderAware
|
|
6
|
+
|
|
5
7
|
RETRYABLE_STATUS_CODES = [502, 503, 504].freeze
|
|
6
8
|
|
|
7
|
-
def initialize(api_key:nil, logger:nil, http_connect_timeout:nil, http_read_timeout:nil, http_max_retries:nil, usage_tracker:nil)
|
|
9
|
+
def initialize(provider:nil, api_key:nil, logger:nil, http_connect_timeout:nil, http_read_timeout:nil, http_max_retries:nil, usage_tracker:nil)
|
|
10
|
+
@provider = provider
|
|
8
11
|
@api_key = api_key || default_api_key
|
|
9
12
|
@logger = logger || Rasti::AI.logger
|
|
10
13
|
@http_connect_timeout = http_connect_timeout || Rasti::AI.http_connect_timeout
|
|
@@ -17,22 +20,22 @@ module Rasti
|
|
|
17
20
|
|
|
18
21
|
attr_reader :api_key, :logger, :http_connect_timeout, :http_read_timeout, :http_max_retries, :usage_tracker
|
|
19
22
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
usage = parse_usage response
|
|
23
|
-
usage_tracker.call usage if usage
|
|
23
|
+
def default_api_key
|
|
24
|
+
provider.default_api_key
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def
|
|
27
|
-
|
|
27
|
+
def default_model
|
|
28
|
+
provider.default_model
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
def
|
|
31
|
-
|
|
31
|
+
def track_usage(response)
|
|
32
|
+
return unless usage_tracker
|
|
33
|
+
usage = provider.parse_usage response
|
|
34
|
+
usage_tracker.call usage if usage
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
def base_url
|
|
35
|
-
|
|
38
|
+
provider.base_url
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
def build_url(relative_url)
|
data/lib/rasti/ai/errors.rb
CHANGED
|
@@ -34,6 +34,14 @@ module Rasti
|
|
|
34
34
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
class UnknownProvider < StandardError
|
|
38
|
+
|
|
39
|
+
def initialize(provider, valid_providers)
|
|
40
|
+
super "Unknown provider #{provider.inspect}. Valid providers: #{valid_providers.join(', ')}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
end
|
|
38
46
|
end
|
|
39
47
|
end
|
|
@@ -5,18 +5,8 @@ module Rasti
|
|
|
5
5
|
|
|
6
6
|
ALLOWED_SCHEMA_FIELDS = %w[type description properties required enum items format nullable anyOf].freeze
|
|
7
7
|
|
|
8
|
-
THINKING_LEVELS = {
|
|
9
|
-
'low' => {thinking_budget: 1_024}.freeze,
|
|
10
|
-
'medium' => {thinking_budget: 8_192}.freeze,
|
|
11
|
-
'high' => {thinking_budget: 24_576}.freeze
|
|
12
|
-
}.freeze
|
|
13
|
-
|
|
14
8
|
private
|
|
15
9
|
|
|
16
|
-
def build_default_client
|
|
17
|
-
Client.new
|
|
18
|
-
end
|
|
19
|
-
|
|
20
10
|
def build_user_message(prompt)
|
|
21
11
|
{role: Roles::USER, parts: [{text: prompt}]}
|
|
22
12
|
end
|
|
@@ -112,7 +102,7 @@ module Rasti
|
|
|
112
102
|
end
|
|
113
103
|
|
|
114
104
|
def thinking_config
|
|
115
|
-
THINKING_LEVELS[thinking]
|
|
105
|
+
Provider::THINKING_LEVELS[thinking]
|
|
116
106
|
end
|
|
117
107
|
|
|
118
108
|
def generation_config
|
|
@@ -4,7 +4,7 @@ module Rasti
|
|
|
4
4
|
class Client < Rasti::AI::Client
|
|
5
5
|
|
|
6
6
|
def generate_content(contents:, model:nil, tools:[], system_instruction:nil, generation_config:nil)
|
|
7
|
-
model_name = model ||
|
|
7
|
+
model_name = model || default_model
|
|
8
8
|
|
|
9
9
|
body = {contents: contents}
|
|
10
10
|
|
|
@@ -17,30 +17,8 @@ module Rasti
|
|
|
17
17
|
|
|
18
18
|
private
|
|
19
19
|
|
|
20
|
-
def parse_usage(response)
|
|
21
|
-
usage = response['usageMetadata']
|
|
22
|
-
return unless usage
|
|
23
|
-
Usage.new(
|
|
24
|
-
provider: 'gemini',
|
|
25
|
-
model: response['modelVersion'],
|
|
26
|
-
input_tokens: usage['promptTokenCount'],
|
|
27
|
-
output_tokens: usage['candidatesTokenCount'],
|
|
28
|
-
cached_tokens: usage['cachedContentTokenCount'] || 0,
|
|
29
|
-
reasoning_tokens: usage['thoughtsTokenCount'] || 0,
|
|
30
|
-
raw: usage
|
|
31
|
-
)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def default_api_key
|
|
35
|
-
Rasti::AI.gemini_api_key
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def base_url
|
|
39
|
-
'https://generativelanguage.googleapis.com/v1beta'
|
|
40
|
-
end
|
|
41
|
-
|
|
42
20
|
def build_url(relative_url)
|
|
43
|
-
"#{
|
|
21
|
+
"#{super}?key=#{api_key}"
|
|
44
22
|
end
|
|
45
23
|
|
|
46
24
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module Rasti
|
|
2
|
+
module AI
|
|
3
|
+
module Gemini
|
|
4
|
+
class Provider < Rasti::AI::Provider
|
|
5
|
+
|
|
6
|
+
THINKING_LEVELS = {
|
|
7
|
+
'low' => {thinking_budget: 1_024}.freeze,
|
|
8
|
+
'medium' => {thinking_budget: 8_192}.freeze,
|
|
9
|
+
'high' => {thinking_budget: 24_576}.freeze
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def name
|
|
13
|
+
:gemini
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def default_model
|
|
17
|
+
Rasti::AI.gemini_default_model
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def default_api_key
|
|
21
|
+
Rasti::AI.gemini_api_key
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def base_url
|
|
25
|
+
'https://generativelanguage.googleapis.com/v1beta'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_usage(response)
|
|
29
|
+
usage = response['usageMetadata']
|
|
30
|
+
return nil unless usage
|
|
31
|
+
|
|
32
|
+
Usage.new(
|
|
33
|
+
provider: name.to_s,
|
|
34
|
+
model: response['modelVersion'],
|
|
35
|
+
input_tokens: usage['promptTokenCount'],
|
|
36
|
+
output_tokens: usage['candidatesTokenCount'],
|
|
37
|
+
cached_tokens: usage['cachedContentTokenCount'] || 0,
|
|
38
|
+
reasoning_tokens: usage['thoughtsTokenCount'] || 0,
|
|
39
|
+
raw: usage
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def request(client:, messages:, system:, model:, json_schema:, thinking:)
|
|
46
|
+
client.generate_content contents: messages,
|
|
47
|
+
model: model,
|
|
48
|
+
system_instruction: system_instruction(system),
|
|
49
|
+
generation_config: generation_config(json_schema, thinking)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def encode_message(message)
|
|
53
|
+
{
|
|
54
|
+
role: encode_role(message[:role]),
|
|
55
|
+
parts: [{text: message[:content]}]
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def encode_role(role)
|
|
60
|
+
role == Rasti::AI::Roles::ASSISTANT ? Roles::MODEL : Roles::USER
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_content(response)
|
|
64
|
+
parts = response.dig('candidates', 0, 'content', 'parts') || []
|
|
65
|
+
text_part = parts.find { |part| part.key? 'text' }
|
|
66
|
+
text_part['text'] if text_part
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def system_instruction(system)
|
|
70
|
+
return nil if system.nil?
|
|
71
|
+
{parts: [{text: system}]}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def generation_config(json_schema, thinking)
|
|
75
|
+
config = {}
|
|
76
|
+
|
|
77
|
+
config[:thinking_config] = thinking if thinking
|
|
78
|
+
|
|
79
|
+
if json_schema
|
|
80
|
+
config[:response_mime_type] = 'application/json'
|
|
81
|
+
config[:response_schema] = json_schema
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
config.empty? ? nil : config
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Rasti
|
|
2
|
+
module AI
|
|
3
|
+
module HuaweiMaaS
|
|
4
|
+
class Provider < Rasti::AI::OpenAI::Provider
|
|
5
|
+
|
|
6
|
+
def name
|
|
7
|
+
:huawei_maas
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def default_model
|
|
11
|
+
Rasti::AI.huawei_maas_default_model
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def default_api_key
|
|
15
|
+
Rasti::AI.huawei_maas_api_key
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def base_url
|
|
19
|
+
'https://api-ap-southeast-1.modelarts-maas.com/v2'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -5,42 +5,20 @@ module Rasti
|
|
|
5
5
|
|
|
6
6
|
def chat_completions(messages:, model:nil, tools:[], response_format:nil, reasoning_effort:nil)
|
|
7
7
|
body = {
|
|
8
|
-
model: model ||
|
|
8
|
+
model: model || default_model,
|
|
9
9
|
messages: messages,
|
|
10
10
|
tools: tools,
|
|
11
11
|
tool_choice: tools.empty? ? 'none' : 'auto'
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
body[:response_format]
|
|
15
|
-
body[:reasoning_effort]
|
|
14
|
+
body[:response_format] = response_format if response_format
|
|
15
|
+
body[:reasoning_effort] = reasoning_effort if reasoning_effort
|
|
16
16
|
|
|
17
17
|
post '/chat/completions', body
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
-
def parse_usage(response)
|
|
23
|
-
usage = response['usage']
|
|
24
|
-
return unless usage
|
|
25
|
-
Usage.new(
|
|
26
|
-
provider: 'open_ai',
|
|
27
|
-
model: response['model'],
|
|
28
|
-
input_tokens: usage['prompt_tokens'],
|
|
29
|
-
output_tokens: usage['completion_tokens'],
|
|
30
|
-
cached_tokens: usage.dig('prompt_tokens_details', 'cached_tokens') || 0,
|
|
31
|
-
reasoning_tokens: usage.dig('completion_tokens_details', 'reasoning_tokens') || 0,
|
|
32
|
-
raw: usage
|
|
33
|
-
)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def default_api_key
|
|
37
|
-
Rasti::AI.openai_api_key
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def base_url
|
|
41
|
-
'https://api.openai.com/v1'
|
|
42
|
-
end
|
|
43
|
-
|
|
44
22
|
def build_request(uri)
|
|
45
23
|
request = super
|
|
46
24
|
request['Authorization'] = "Bearer #{api_key}"
|
|
@@ -50,4 +28,4 @@ module Rasti
|
|
|
50
28
|
end
|
|
51
29
|
end
|
|
52
30
|
end
|
|
53
|
-
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Rasti
|
|
2
|
+
module AI
|
|
3
|
+
module OpenAI
|
|
4
|
+
class Provider < Rasti::AI::Provider
|
|
5
|
+
|
|
6
|
+
THINKING_LEVELS = {
|
|
7
|
+
'low' => 'low'.freeze,
|
|
8
|
+
'medium' => 'medium'.freeze,
|
|
9
|
+
'high' => 'high'.freeze
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def name
|
|
13
|
+
:open_ai
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def default_model
|
|
17
|
+
Rasti::AI.openai_default_model
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def default_api_key
|
|
21
|
+
Rasti::AI.openai_api_key
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def base_url
|
|
25
|
+
'https://api.openai.com/v1'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_usage(response)
|
|
29
|
+
usage = response['usage']
|
|
30
|
+
return nil unless usage
|
|
31
|
+
|
|
32
|
+
Usage.new(
|
|
33
|
+
provider: name.to_s,
|
|
34
|
+
model: response['model'],
|
|
35
|
+
input_tokens: usage['prompt_tokens'],
|
|
36
|
+
output_tokens: usage['completion_tokens'],
|
|
37
|
+
cached_tokens: usage.dig('prompt_tokens_details', 'cached_tokens') || 0,
|
|
38
|
+
reasoning_tokens: usage.dig('completion_tokens_details', 'reasoning_tokens') || 0,
|
|
39
|
+
raw: usage
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def request(client:, messages:, system:, model:, json_schema:, thinking:)
|
|
46
|
+
all_messages = system ? [{role: Roles::SYSTEM, content: system}] + messages : messages
|
|
47
|
+
|
|
48
|
+
client.chat_completions messages: all_messages,
|
|
49
|
+
model: model,
|
|
50
|
+
response_format: response_format(json_schema),
|
|
51
|
+
reasoning_effort: thinking
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def encode_message(message)
|
|
55
|
+
{role: message[:role], content: message[:content]}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_content(response)
|
|
59
|
+
response.dig 'choices', 0, 'message', 'content'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def response_format(json_schema)
|
|
63
|
+
return nil if json_schema.nil?
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
type: 'json_schema',
|
|
67
|
+
json_schema: json_schema
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Rasti
|
|
2
|
+
module AI
|
|
3
|
+
module OpenRouter
|
|
4
|
+
class Provider < Rasti::AI::OpenAI::Provider
|
|
5
|
+
|
|
6
|
+
def name
|
|
7
|
+
:open_router
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def default_model
|
|
11
|
+
Rasti::AI.openrouter_default_model
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def default_api_key
|
|
15
|
+
Rasti::AI.openrouter_api_key
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def base_url
|
|
19
|
+
'https://openrouter.ai/api/v1'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|