ruby_llm 0.1.0.pre29 → 0.1.0.pre31
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/workflows/{gem-push.yml → cicd.yml} +32 -4
- data/.rspec_status +27 -0
- data/lib/ruby_llm/active_record/acts_as.rb +5 -5
- data/lib/ruby_llm/chat.rb +2 -2
- data/lib/ruby_llm/configuration.rb +3 -1
- data/lib/ruby_llm/content.rb +79 -0
- data/lib/ruby_llm/embedding.rb +9 -3
- data/lib/ruby_llm/message.rb +9 -1
- data/lib/ruby_llm/models.json +22 -22
- data/lib/ruby_llm/provider.rb +39 -14
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +81 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +86 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +37 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +97 -0
- data/lib/ruby_llm/providers/anthropic.rb +8 -234
- data/lib/ruby_llm/providers/deepseek/capabilites.rb +101 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -2
- data/lib/ruby_llm/providers/gemini/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/gemini/models.rb +20 -0
- data/lib/ruby_llm/providers/gemini.rb +5 -10
- data/lib/ruby_llm/providers/openai/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/openai/chat.rb +68 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +39 -0
- data/lib/ruby_llm/providers/openai/models.rb +40 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +31 -0
- data/lib/ruby_llm/providers/openai/tools.rb +69 -0
- data/lib/ruby_llm/providers/openai.rb +15 -197
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +4 -2
- data/ruby_llm.gemspec +2 -0
- metadata +48 -8
- data/.github/workflows/test.yml +0 -35
- data/lib/ruby_llm/model_capabilities/anthropic.rb +0 -75
- data/lib/ruby_llm/model_capabilities/deepseek.rb +0 -132
- data/lib/ruby_llm/model_capabilities/gemini.rb +0 -190
- data/lib/ruby_llm/model_capabilities/openai.rb +0 -189
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Chat methods of the OpenAI API integration
|
7
|
+
module Chat
|
8
|
+
private
|
9
|
+
|
10
|
+
def completion_url
|
11
|
+
'/v1/messages'
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false)
|
15
|
+
{
|
16
|
+
model: model,
|
17
|
+
messages: messages.map { |msg| format_message(msg) },
|
18
|
+
temperature: temperature,
|
19
|
+
stream: stream,
|
20
|
+
max_tokens: RubyLLM.models.find(model).max_tokens
|
21
|
+
}.tap do |payload|
|
22
|
+
payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_completion_response(response)
|
27
|
+
data = response.body
|
28
|
+
content_blocks = data['content'] || []
|
29
|
+
|
30
|
+
text_content = extract_text_content(content_blocks)
|
31
|
+
tool_use = find_tool_use(content_blocks)
|
32
|
+
|
33
|
+
build_message(data, text_content, tool_use)
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_text_content(blocks)
|
37
|
+
text_blocks = blocks.select { |c| c['type'] == 'text' }
|
38
|
+
text_blocks.map { |c| c['text'] }.join('')
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_message(data, content, tool_use)
|
42
|
+
Message.new(
|
43
|
+
role: :assistant,
|
44
|
+
content: content,
|
45
|
+
tool_calls: parse_tool_calls(tool_use),
|
46
|
+
input_tokens: data.dig('usage', 'input_tokens'),
|
47
|
+
output_tokens: data.dig('usage', 'output_tokens'),
|
48
|
+
model_id: data['model']
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def format_message(msg)
|
53
|
+
if msg.tool_call?
|
54
|
+
format_tool_call(msg)
|
55
|
+
elsif msg.tool_result?
|
56
|
+
format_tool_result(msg)
|
57
|
+
else
|
58
|
+
format_basic_message(msg)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_basic_message(msg)
|
63
|
+
{
|
64
|
+
role: convert_role(msg.role),
|
65
|
+
content: msg.content
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def convert_role(role)
|
70
|
+
case role
|
71
|
+
when :tool then 'user'
|
72
|
+
when :user then 'user'
|
73
|
+
else 'assistant'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def format_text_block(content)
|
78
|
+
{
|
79
|
+
type: 'text',
|
80
|
+
text: content
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Embeddings methods of the Anthropic API integration
|
7
|
+
module Embeddings
|
8
|
+
private
|
9
|
+
|
10
|
+
def embed
|
11
|
+
raise Error "Anthropic doesn't support embeddings"
|
12
|
+
end
|
13
|
+
|
14
|
+
alias render_embedding_payload embed
|
15
|
+
alias embedding_url embed
|
16
|
+
alias parse_embedding_response embed
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Models methods of the Anthropic API integration
|
7
|
+
module Models
|
8
|
+
private
|
9
|
+
|
10
|
+
def models_url
|
11
|
+
'/v1/models'
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse_list_models_response(response) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
15
|
+
(response.body['data'] || []).map do |model|
|
16
|
+
ModelInfo.new(
|
17
|
+
id: model['id'],
|
18
|
+
created_at: Time.parse(model['created_at']),
|
19
|
+
display_name: model['display_name'],
|
20
|
+
provider: slug,
|
21
|
+
type: capabilities.model_type(model['id']),
|
22
|
+
family: capabilities.model_family(model['id']),
|
23
|
+
context_window: capabilities.determine_context_window(model['id']),
|
24
|
+
max_tokens: capabilities.determine_max_tokens(model['id']),
|
25
|
+
supports_vision: capabilities.supports_vision?(model['id']),
|
26
|
+
supports_functions: capabilities.supports_functions?(model['id']),
|
27
|
+
supports_json_mode: capabilities.supports_json_mode?(model['id']),
|
28
|
+
input_price_per_million: capabilities.get_input_price(model['id']),
|
29
|
+
output_price_per_million: capabilities.get_output_price(model['id'])
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract_model_id(data)
|
35
|
+
data.dig('message', 'model')
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_input_tokens(data)
|
39
|
+
data.dig('message', 'usage', 'input_tokens')
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_output_tokens(data)
|
43
|
+
data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Streaming methods of the Anthropic API integration
|
7
|
+
module Streaming
|
8
|
+
private
|
9
|
+
|
10
|
+
def stream_url
|
11
|
+
completion_url
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_stream(&block)
|
15
|
+
to_json_stream do |data|
|
16
|
+
block.call(build_chunk(data))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_chunk(data)
|
21
|
+
Chunk.new(
|
22
|
+
role: :assistant,
|
23
|
+
model_id: extract_model_id(data),
|
24
|
+
content: data.dig('delta', 'text'),
|
25
|
+
input_tokens: extract_input_tokens(data),
|
26
|
+
output_tokens: extract_output_tokens(data),
|
27
|
+
tool_calls: extract_tool_calls(data)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def json_delta?(data)
|
32
|
+
data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Tools methods of the Anthropic API integration
|
7
|
+
module Tools
|
8
|
+
private
|
9
|
+
|
10
|
+
def find_tool_use(blocks)
|
11
|
+
blocks.find { |c| c['type'] == 'tool_use' }
|
12
|
+
end
|
13
|
+
|
14
|
+
def format_tool_call(msg)
|
15
|
+
tool_call = msg.tool_calls.values.first
|
16
|
+
|
17
|
+
{
|
18
|
+
role: 'assistant',
|
19
|
+
content: [
|
20
|
+
format_text_block(msg.content),
|
21
|
+
format_tool_use_block(tool_call)
|
22
|
+
]
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_tool_result(msg)
|
27
|
+
{
|
28
|
+
role: 'user',
|
29
|
+
content: [format_tool_result_block(msg)]
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def format_tool_use_block(tool_call)
|
34
|
+
{
|
35
|
+
type: 'tool_use',
|
36
|
+
id: tool_call.id,
|
37
|
+
name: tool_call.name,
|
38
|
+
input: tool_call.arguments
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_tool_result_block(msg)
|
43
|
+
{
|
44
|
+
type: 'tool_result',
|
45
|
+
tool_use_id: msg.tool_call_id,
|
46
|
+
content: msg.content
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def function_for(tool)
|
51
|
+
{
|
52
|
+
name: tool.name,
|
53
|
+
description: tool.description,
|
54
|
+
input_schema: {
|
55
|
+
type: 'object',
|
56
|
+
properties: clean_parameters(tool.parameters),
|
57
|
+
required: required_parameters(tool.parameters)
|
58
|
+
}
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_tool_calls(data)
|
63
|
+
if json_delta?(data)
|
64
|
+
{ nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
|
65
|
+
else
|
66
|
+
parse_tool_calls(data['content_block'])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_tool_calls(content_block)
|
71
|
+
return nil unless content_block && content_block['type'] == 'tool_use'
|
72
|
+
|
73
|
+
{
|
74
|
+
content_block['id'] => ToolCall.new(
|
75
|
+
id: content_block['id'],
|
76
|
+
name: content_block['name'],
|
77
|
+
arguments: content_block['input']
|
78
|
+
)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def clean_parameters(parameters)
|
83
|
+
parameters.transform_values do |param|
|
84
|
+
{
|
85
|
+
type: param.type,
|
86
|
+
description: param.description
|
87
|
+
}.compact
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def required_parameters(parameters)
|
92
|
+
parameters.select { |_, param| param.required }.keys
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -4,17 +4,15 @@ module RubyLLM
|
|
4
4
|
module Providers
|
5
5
|
# Anthropic Claude API integration. Handles the complexities of
|
6
6
|
# Claude's unique message format and tool calling conventions.
|
7
|
-
|
8
|
-
|
7
|
+
module Anthropic
|
8
|
+
extend Provider
|
9
|
+
extend Anthropic::Chat
|
10
|
+
extend Anthropic::Embeddings
|
11
|
+
extend Anthropic::Models
|
12
|
+
extend Anthropic::Streaming
|
13
|
+
extend Anthropic::Tools
|
9
14
|
|
10
|
-
|
11
|
-
return if response.body.empty?
|
12
|
-
|
13
|
-
body = try_parse_json(response.body)
|
14
|
-
body.is_a?(Hash) ? body.dig('error', 'message') : body
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
15
|
+
module_function
|
18
16
|
|
19
17
|
def api_base
|
20
18
|
'https://api.anthropic.com'
|
@@ -26,230 +24,6 @@ module RubyLLM
|
|
26
24
|
'anthropic-version' => '2023-06-01'
|
27
25
|
}
|
28
26
|
end
|
29
|
-
|
30
|
-
def completion_url
|
31
|
-
'/v1/messages'
|
32
|
-
end
|
33
|
-
|
34
|
-
def stream_url
|
35
|
-
completion_url
|
36
|
-
end
|
37
|
-
|
38
|
-
def models_url
|
39
|
-
'/v1/models'
|
40
|
-
end
|
41
|
-
|
42
|
-
def build_payload(messages, tools:, temperature:, model:, stream: false)
|
43
|
-
{
|
44
|
-
model: model,
|
45
|
-
messages: format_messages(messages),
|
46
|
-
temperature: temperature,
|
47
|
-
stream: stream,
|
48
|
-
max_tokens: RubyLLM.models.find(model).max_tokens
|
49
|
-
}.tap do |payload|
|
50
|
-
payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def parse_completion_response(response)
|
55
|
-
data = response.body
|
56
|
-
content_blocks = data['content'] || []
|
57
|
-
|
58
|
-
text_content = extract_text_content(content_blocks)
|
59
|
-
tool_use = find_tool_use(content_blocks)
|
60
|
-
|
61
|
-
build_message(data, text_content, tool_use)
|
62
|
-
end
|
63
|
-
|
64
|
-
def extract_text_content(blocks)
|
65
|
-
text_blocks = blocks.select { |c| c['type'] == 'text' }
|
66
|
-
text_blocks.map { |c| c['text'] }.join('')
|
67
|
-
end
|
68
|
-
|
69
|
-
def find_tool_use(blocks)
|
70
|
-
blocks.find { |c| c['type'] == 'tool_use' }
|
71
|
-
end
|
72
|
-
|
73
|
-
def build_message(data, content, tool_use)
|
74
|
-
Message.new(
|
75
|
-
role: :assistant,
|
76
|
-
content: content,
|
77
|
-
tool_calls: parse_tool_calls(tool_use),
|
78
|
-
input_tokens: data.dig('usage', 'input_tokens'),
|
79
|
-
output_tokens: data.dig('usage', 'output_tokens'),
|
80
|
-
model_id: data['model']
|
81
|
-
)
|
82
|
-
end
|
83
|
-
|
84
|
-
def parse_list_models_response(response) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
85
|
-
(response.body['data'] || []).map do |model|
|
86
|
-
ModelInfo.new(
|
87
|
-
id: model['id'],
|
88
|
-
created_at: Time.parse(model['created_at']),
|
89
|
-
display_name: model['display_name'],
|
90
|
-
provider: slug,
|
91
|
-
type: model['type'],
|
92
|
-
family: capabilities.model_family(model['id']),
|
93
|
-
context_window: capabilities.determine_context_window(model['id']),
|
94
|
-
max_tokens: capabilities.determine_max_tokens(model['id']),
|
95
|
-
supports_vision: capabilities.supports_vision?(model['id']),
|
96
|
-
supports_functions: capabilities.supports_functions?(model['id']),
|
97
|
-
supports_json_mode: capabilities.supports_json_mode?(model['id']),
|
98
|
-
input_price_per_million: capabilities.get_input_price(model['id']),
|
99
|
-
output_price_per_million: capabilities.get_output_price(model['id'])
|
100
|
-
)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def handle_stream(&block)
|
105
|
-
to_json_stream do |data|
|
106
|
-
block.call(build_chunk(data))
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def build_chunk(data)
|
111
|
-
Chunk.new(
|
112
|
-
role: :assistant,
|
113
|
-
model_id: extract_model_id(data),
|
114
|
-
content: data.dig('delta', 'text'),
|
115
|
-
input_tokens: extract_input_tokens(data),
|
116
|
-
output_tokens: extract_output_tokens(data),
|
117
|
-
tool_calls: extract_tool_calls(data)
|
118
|
-
)
|
119
|
-
end
|
120
|
-
|
121
|
-
def extract_model_id(data)
|
122
|
-
data.dig('message', 'model')
|
123
|
-
end
|
124
|
-
|
125
|
-
def extract_input_tokens(data)
|
126
|
-
data.dig('message', 'usage', 'input_tokens')
|
127
|
-
end
|
128
|
-
|
129
|
-
def extract_output_tokens(data)
|
130
|
-
data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
|
131
|
-
end
|
132
|
-
|
133
|
-
def extract_tool_calls(data)
|
134
|
-
if json_delta?(data)
|
135
|
-
{ nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
|
136
|
-
else
|
137
|
-
parse_tool_calls(data['content_block'])
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def json_delta?(data)
|
142
|
-
data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
|
143
|
-
end
|
144
|
-
|
145
|
-
def parse_tool_calls(content_block)
|
146
|
-
return nil unless content_block && content_block['type'] == 'tool_use'
|
147
|
-
|
148
|
-
{
|
149
|
-
content_block['id'] => ToolCall.new(
|
150
|
-
id: content_block['id'],
|
151
|
-
name: content_block['name'],
|
152
|
-
arguments: content_block['input']
|
153
|
-
)
|
154
|
-
}
|
155
|
-
end
|
156
|
-
|
157
|
-
def function_for(tool)
|
158
|
-
{
|
159
|
-
name: tool.name,
|
160
|
-
description: tool.description,
|
161
|
-
input_schema: {
|
162
|
-
type: 'object',
|
163
|
-
properties: clean_parameters(tool.parameters),
|
164
|
-
required: required_parameters(tool.parameters)
|
165
|
-
}
|
166
|
-
}
|
167
|
-
end
|
168
|
-
|
169
|
-
def format_messages(messages)
|
170
|
-
messages.map { |msg| format_message(msg) }
|
171
|
-
end
|
172
|
-
|
173
|
-
def format_message(msg)
|
174
|
-
if msg.tool_call?
|
175
|
-
format_tool_call(msg)
|
176
|
-
elsif msg.tool_result?
|
177
|
-
format_tool_result(msg)
|
178
|
-
else
|
179
|
-
format_basic_message(msg)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def format_tool_call(msg)
|
184
|
-
tool_call = msg.tool_calls.values.first
|
185
|
-
|
186
|
-
{
|
187
|
-
role: 'assistant',
|
188
|
-
content: [
|
189
|
-
format_text_block(msg.content),
|
190
|
-
format_tool_use_block(tool_call)
|
191
|
-
]
|
192
|
-
}
|
193
|
-
end
|
194
|
-
|
195
|
-
def format_tool_result(msg)
|
196
|
-
{
|
197
|
-
role: 'user',
|
198
|
-
content: [format_tool_result_block(msg)]
|
199
|
-
}
|
200
|
-
end
|
201
|
-
|
202
|
-
def format_basic_message(msg)
|
203
|
-
{
|
204
|
-
role: convert_role(msg.role),
|
205
|
-
content: msg.content
|
206
|
-
}
|
207
|
-
end
|
208
|
-
|
209
|
-
def format_text_block(content)
|
210
|
-
{
|
211
|
-
type: 'text',
|
212
|
-
text: content
|
213
|
-
}
|
214
|
-
end
|
215
|
-
|
216
|
-
def format_tool_use_block(tool_call)
|
217
|
-
{
|
218
|
-
type: 'tool_use',
|
219
|
-
id: tool_call.id,
|
220
|
-
name: tool_call.name,
|
221
|
-
input: tool_call.arguments
|
222
|
-
}
|
223
|
-
end
|
224
|
-
|
225
|
-
def format_tool_result_block(msg)
|
226
|
-
{
|
227
|
-
type: 'tool_result',
|
228
|
-
tool_use_id: msg.tool_call_id,
|
229
|
-
content: msg.content
|
230
|
-
}
|
231
|
-
end
|
232
|
-
|
233
|
-
def convert_role(role)
|
234
|
-
case role
|
235
|
-
when :tool then 'user'
|
236
|
-
when :user then 'user'
|
237
|
-
else 'assistant'
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def clean_parameters(parameters)
|
242
|
-
parameters.transform_values do |param|
|
243
|
-
{
|
244
|
-
type: param.type,
|
245
|
-
description: param.description
|
246
|
-
}.compact
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
def required_parameters(parameters)
|
251
|
-
parameters.select { |_, param| param.required }.keys
|
252
|
-
end
|
253
27
|
end
|
254
28
|
end
|
255
29
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module DeepSeek
|
6
|
+
# Determines capabilities and pricing for DeepSeek models
|
7
|
+
module Capabilities
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def context_window_for(model_id)
|
11
|
+
case model_id
|
12
|
+
when /deepseek-(?:chat|reasoner)/ then 64_000
|
13
|
+
else 32_768 # Sensible default
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def max_tokens_for(model_id)
|
18
|
+
case model_id
|
19
|
+
when /deepseek-(?:chat|reasoner)/ then 8_192
|
20
|
+
else 4_096 # Default if max_tokens not specified
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def input_price_for(model_id)
|
25
|
+
PRICES.dig(model_family(model_id), :input_miss) || default_input_price
|
26
|
+
end
|
27
|
+
|
28
|
+
def output_price_for(model_id)
|
29
|
+
PRICES.dig(model_family(model_id), :output) || default_output_price
|
30
|
+
end
|
31
|
+
|
32
|
+
def cache_hit_price_for(model_id)
|
33
|
+
PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
|
34
|
+
end
|
35
|
+
|
36
|
+
def supports_vision?(_model_id)
|
37
|
+
false # DeepSeek models don't currently support vision
|
38
|
+
end
|
39
|
+
|
40
|
+
def supports_functions?(model_id)
|
41
|
+
model_id.match?(/deepseek-chat/) # Only deepseek-chat supports function calling
|
42
|
+
end
|
43
|
+
|
44
|
+
def supports_json_mode?(model_id)
|
45
|
+
model_id.match?(/deepseek-chat/) # Only deepseek-chat supports JSON mode
|
46
|
+
end
|
47
|
+
|
48
|
+
def format_display_name(model_id)
|
49
|
+
case model_id
|
50
|
+
when 'deepseek-chat' then 'DeepSeek V3'
|
51
|
+
when 'deepseek-reasoner' then 'DeepSeek R1'
|
52
|
+
else
|
53
|
+
model_id.split('-')
|
54
|
+
.map(&:capitalize)
|
55
|
+
.join(' ')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def model_type(_model_id)
|
60
|
+
'chat' # All DeepSeek models are chat models
|
61
|
+
end
|
62
|
+
|
63
|
+
def model_family(model_id)
|
64
|
+
case model_id
|
65
|
+
when /deepseek-chat/ then :chat
|
66
|
+
when /deepseek-reasoner/ then :reasoner
|
67
|
+
else :chat # Default to chat family
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Pricing information for DeepSeek models (USD per 1M tokens)
|
72
|
+
PRICES = {
|
73
|
+
chat: {
|
74
|
+
input_hit: 0.07, # $0.07 per million tokens on cache hit
|
75
|
+
input_miss: 0.27, # $0.27 per million tokens on cache miss
|
76
|
+
output: 1.10 # $1.10 per million tokens output
|
77
|
+
},
|
78
|
+
reasoner: {
|
79
|
+
input_hit: 0.14, # $0.14 per million tokens on cache hit
|
80
|
+
input_miss: 0.55, # $0.55 per million tokens on cache miss
|
81
|
+
output: 2.19 # $2.19 per million tokens output
|
82
|
+
}
|
83
|
+
}.freeze
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def default_input_price
|
88
|
+
0.27 # Default to chat cache miss price
|
89
|
+
end
|
90
|
+
|
91
|
+
def default_output_price
|
92
|
+
1.10 # Default to chat output price
|
93
|
+
end
|
94
|
+
|
95
|
+
def default_cache_hit_price
|
96
|
+
0.07 # Default to chat cache hit price
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|