ruby_llm 0.1.0.pre35 → 0.1.0.pre36
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/lib/ruby_llm/models.json +27 -27
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +56 -18
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +39 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +69 -7
- data/lib/ruby_llm/providers/openai/capabilities.rb +74 -21
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/models.rake +25 -3
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed17bc0b342342484bd1e92b57f3b88a74d77cd43daeb1a569b132188025bafd
|
4
|
+
data.tar.gz: '0970e337a393e85cff88a449e723aa7c3e3e189b1923e16cf619f95d1bf65b03'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e8b35980c57cd61e10c50b3eb97dbedbe138e2720b11940646c74807ada4c8b260aff3cbee8ed84abfa4b998d3d2747ff5f2421731fcf73ea038306774a2dc9
|
7
|
+
data.tar.gz: f0211b49713b10e00f1070fa7abb00a151ee7084e3f5e14b047be2c567afe4fcd5ade1ce662697a776470ab4e6c21e98ab1c453526e36b4ec29d6f7f6b9f6c0c
|
data/lib/ruby_llm/models.json
CHANGED
@@ -11,8 +11,8 @@
|
|
11
11
|
"supports_vision": false,
|
12
12
|
"supports_functions": false,
|
13
13
|
"supports_json_mode": false,
|
14
|
-
"input_price_per_million": 0.
|
15
|
-
"output_price_per_million": 0.
|
14
|
+
"input_price_per_million": 0.0,
|
15
|
+
"output_price_per_million": 0.0,
|
16
16
|
"metadata": {
|
17
17
|
"object": "model",
|
18
18
|
"owned_by": "google"
|
@@ -23,8 +23,8 @@
|
|
23
23
|
"created_at": "2023-08-21T18:16:55+02:00",
|
24
24
|
"display_name": "Babbage 002",
|
25
25
|
"provider": "openai",
|
26
|
-
"context_window":
|
27
|
-
"max_tokens":
|
26
|
+
"context_window": 16384,
|
27
|
+
"max_tokens": 16384,
|
28
28
|
"type": "chat",
|
29
29
|
"family": "babbage",
|
30
30
|
"supports_vision": false,
|
@@ -80,7 +80,7 @@
|
|
80
80
|
"created_at": "2023-07-11T00:00:00Z",
|
81
81
|
"display_name": "Claude 2.0",
|
82
82
|
"provider": "anthropic",
|
83
|
-
"context_window":
|
83
|
+
"context_window": 200000,
|
84
84
|
"max_tokens": 4096,
|
85
85
|
"type": "chat",
|
86
86
|
"family": "claude2",
|
@@ -96,7 +96,7 @@
|
|
96
96
|
"created_at": "2023-11-21T00:00:00Z",
|
97
97
|
"display_name": "Claude 2.1",
|
98
98
|
"provider": "anthropic",
|
99
|
-
"context_window":
|
99
|
+
"context_window": 200000,
|
100
100
|
"max_tokens": 4096,
|
101
101
|
"type": "chat",
|
102
102
|
"family": "claude2",
|
@@ -116,7 +116,7 @@
|
|
116
116
|
"max_tokens": 8192,
|
117
117
|
"type": "chat",
|
118
118
|
"family": "claude35_haiku",
|
119
|
-
"supports_vision":
|
119
|
+
"supports_vision": true,
|
120
120
|
"supports_functions": true,
|
121
121
|
"supports_json_mode": true,
|
122
122
|
"input_price_per_million": 0.8,
|
@@ -161,9 +161,9 @@
|
|
161
161
|
"display_name": "Claude 3.7 Sonnet",
|
162
162
|
"provider": "anthropic",
|
163
163
|
"context_window": 200000,
|
164
|
-
"max_tokens":
|
164
|
+
"max_tokens": 8192,
|
165
165
|
"type": "chat",
|
166
|
-
"family": "
|
166
|
+
"family": "claude37_sonnet",
|
167
167
|
"supports_vision": true,
|
168
168
|
"supports_functions": true,
|
169
169
|
"supports_json_mode": true,
|
@@ -262,8 +262,8 @@
|
|
262
262
|
"created_at": "2023-08-21T18:11:41+02:00",
|
263
263
|
"display_name": "Davinci 002",
|
264
264
|
"provider": "openai",
|
265
|
-
"context_window":
|
266
|
-
"max_tokens":
|
265
|
+
"context_window": 16384,
|
266
|
+
"max_tokens": 16384,
|
267
267
|
"type": "chat",
|
268
268
|
"family": "davinci",
|
269
269
|
"supports_vision": false,
|
@@ -857,7 +857,7 @@
|
|
857
857
|
"family": "gemini20_flash_lite",
|
858
858
|
"supports_vision": true,
|
859
859
|
"supports_functions": false,
|
860
|
-
"supports_json_mode":
|
860
|
+
"supports_json_mode": false,
|
861
861
|
"input_price_per_million": 0.075,
|
862
862
|
"output_price_per_million": 0.3,
|
863
863
|
"metadata": {
|
@@ -876,7 +876,7 @@
|
|
876
876
|
"family": "gemini20_flash_lite",
|
877
877
|
"supports_vision": true,
|
878
878
|
"supports_functions": false,
|
879
|
-
"supports_json_mode":
|
879
|
+
"supports_json_mode": false,
|
880
880
|
"input_price_per_million": 0.075,
|
881
881
|
"output_price_per_million": 0.3,
|
882
882
|
"metadata": {
|
@@ -895,7 +895,7 @@
|
|
895
895
|
"family": "gemini20_flash_lite",
|
896
896
|
"supports_vision": true,
|
897
897
|
"supports_functions": false,
|
898
|
-
"supports_json_mode":
|
898
|
+
"supports_json_mode": false,
|
899
899
|
"input_price_per_million": 0.075,
|
900
900
|
"output_price_per_million": 0.3,
|
901
901
|
"metadata": {
|
@@ -914,7 +914,7 @@
|
|
914
914
|
"family": "gemini20_flash_lite",
|
915
915
|
"supports_vision": true,
|
916
916
|
"supports_functions": false,
|
917
|
-
"supports_json_mode":
|
917
|
+
"supports_json_mode": false,
|
918
918
|
"input_price_per_million": 0.075,
|
919
919
|
"output_price_per_million": 0.3,
|
920
920
|
"metadata": {
|
@@ -1650,7 +1650,7 @@
|
|
1650
1650
|
"display_name": "GPT-4o-Mini Realtime Preview",
|
1651
1651
|
"provider": "openai",
|
1652
1652
|
"context_window": 128000,
|
1653
|
-
"max_tokens":
|
1653
|
+
"max_tokens": 4096,
|
1654
1654
|
"type": "chat",
|
1655
1655
|
"family": "gpt4o_mini_realtime",
|
1656
1656
|
"supports_vision": true,
|
@@ -1669,7 +1669,7 @@
|
|
1669
1669
|
"display_name": "GPT-4o-Mini Realtime Preview 20241217",
|
1670
1670
|
"provider": "openai",
|
1671
1671
|
"context_window": 128000,
|
1672
|
-
"max_tokens":
|
1672
|
+
"max_tokens": 4096,
|
1673
1673
|
"type": "chat",
|
1674
1674
|
"family": "gpt4o_mini_realtime",
|
1675
1675
|
"supports_vision": true,
|
@@ -1685,10 +1685,10 @@
|
|
1685
1685
|
{
|
1686
1686
|
"id": "gpt-4o-realtime-preview",
|
1687
1687
|
"created_at": "2024-09-30T03:33:18+02:00",
|
1688
|
-
"display_name": "GPT-4o
|
1688
|
+
"display_name": "GPT-4o-Realtime Preview",
|
1689
1689
|
"provider": "openai",
|
1690
1690
|
"context_window": 128000,
|
1691
|
-
"max_tokens":
|
1691
|
+
"max_tokens": 4096,
|
1692
1692
|
"type": "chat",
|
1693
1693
|
"family": "gpt4o_realtime",
|
1694
1694
|
"supports_vision": true,
|
@@ -1704,10 +1704,10 @@
|
|
1704
1704
|
{
|
1705
1705
|
"id": "gpt-4o-realtime-preview-2024-10-01",
|
1706
1706
|
"created_at": "2024-09-24T00:49:26+02:00",
|
1707
|
-
"display_name": "GPT-4o
|
1707
|
+
"display_name": "GPT-4o-Realtime Preview 20241001",
|
1708
1708
|
"provider": "openai",
|
1709
1709
|
"context_window": 128000,
|
1710
|
-
"max_tokens":
|
1710
|
+
"max_tokens": 4096,
|
1711
1711
|
"type": "chat",
|
1712
1712
|
"family": "gpt4o_realtime",
|
1713
1713
|
"supports_vision": true,
|
@@ -1723,10 +1723,10 @@
|
|
1723
1723
|
{
|
1724
1724
|
"id": "gpt-4o-realtime-preview-2024-12-17",
|
1725
1725
|
"created_at": "2024-12-11T20:30:30+01:00",
|
1726
|
-
"display_name": "GPT-4o
|
1726
|
+
"display_name": "GPT-4o-Realtime Preview 20241217",
|
1727
1727
|
"provider": "openai",
|
1728
1728
|
"context_window": 128000,
|
1729
|
-
"max_tokens":
|
1729
|
+
"max_tokens": 4096,
|
1730
1730
|
"type": "chat",
|
1731
1731
|
"family": "gpt4o_realtime",
|
1732
1732
|
"supports_vision": true,
|
@@ -1820,7 +1820,7 @@
|
|
1820
1820
|
"created_at": "2024-09-06T20:56:48+02:00",
|
1821
1821
|
"display_name": "O1-Mini",
|
1822
1822
|
"provider": "openai",
|
1823
|
-
"context_window":
|
1823
|
+
"context_window": 128000,
|
1824
1824
|
"max_tokens": 4096,
|
1825
1825
|
"type": "chat",
|
1826
1826
|
"family": "o1_mini",
|
@@ -1839,7 +1839,7 @@
|
|
1839
1839
|
"created_at": "2024-09-06T20:56:19+02:00",
|
1840
1840
|
"display_name": "O1-Mini 20240912",
|
1841
1841
|
"provider": "openai",
|
1842
|
-
"context_window":
|
1842
|
+
"context_window": 128000,
|
1843
1843
|
"max_tokens": 65536,
|
1844
1844
|
"type": "chat",
|
1845
1845
|
"family": "o1_mini",
|
@@ -1894,7 +1894,7 @@
|
|
1894
1894
|
{
|
1895
1895
|
"id": "omni-moderation-2024-09-26",
|
1896
1896
|
"created_at": "2024-11-27T20:07:46+01:00",
|
1897
|
-
"display_name": "Omni
|
1897
|
+
"display_name": "Omni-Moderation 20240926",
|
1898
1898
|
"provider": "openai",
|
1899
1899
|
"context_window": 4096,
|
1900
1900
|
"max_tokens": 4096,
|
@@ -1913,7 +1913,7 @@
|
|
1913
1913
|
{
|
1914
1914
|
"id": "omni-moderation-latest",
|
1915
1915
|
"created_at": "2024-11-15T17:47:45+01:00",
|
1916
|
-
"display_name": "Omni
|
1916
|
+
"display_name": "Omni-Moderation Latest",
|
1917
1917
|
"provider": "openai",
|
1918
1918
|
"context_window": 4096,
|
1919
1919
|
"max_tokens": 4096,
|
@@ -7,45 +7,74 @@ module RubyLLM
|
|
7
7
|
module Capabilities
|
8
8
|
module_function
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
# Determines the context window size for a given model
|
11
|
+
# @param model_id [String] the model identifier
|
12
|
+
# @return [Integer] the context window size in tokens
|
13
|
+
def determine_context_window(_model_id)
|
14
|
+
# All Claude 3 and 3.5 and 3.7 models have 200K token context windows
|
15
|
+
200_000
|
15
16
|
end
|
16
17
|
|
18
|
+
# Determines the maximum output tokens for a given model
|
19
|
+
# @param model_id [String] the model identifier
|
20
|
+
# @return [Integer] the maximum output tokens
|
17
21
|
def determine_max_tokens(model_id)
|
18
22
|
case model_id
|
23
|
+
when /claude-3-7-sonnet/ then 8_192 # Can be increased to 64K with extended thinking
|
19
24
|
when /claude-3-5/ then 8_192
|
20
|
-
else 4_096
|
25
|
+
else 4_096 # Claude 3 Opus and Haiku
|
21
26
|
end
|
22
27
|
end
|
23
28
|
|
29
|
+
# Gets the input price per million tokens for a given model
|
30
|
+
# @param model_id [String] the model identifier
|
31
|
+
# @return [Float] the price per million tokens for input
|
24
32
|
def get_input_price(model_id)
|
25
33
|
PRICES.dig(model_family(model_id), :input) || default_input_price
|
26
34
|
end
|
27
35
|
|
36
|
+
# Gets the output price per million tokens for a given model
|
37
|
+
# @param model_id [String] the model identifier
|
38
|
+
# @return [Float] the price per million tokens for output
|
28
39
|
def get_output_price(model_id)
|
29
40
|
PRICES.dig(model_family(model_id), :output) || default_output_price
|
30
41
|
end
|
31
42
|
|
43
|
+
# Determines if a model supports vision capabilities
|
44
|
+
# @param model_id [String] the model identifier
|
45
|
+
# @return [Boolean] true if the model supports vision
|
32
46
|
def supports_vision?(model_id)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
true
|
47
|
+
# All Claude 3, 3.5, and 3.7 models support vision
|
48
|
+
!model_id.match?(/claude-[12]/)
|
37
49
|
end
|
38
50
|
|
51
|
+
# Determines if a model supports function calling
|
52
|
+
# @param model_id [String] the model identifier
|
53
|
+
# @return [Boolean] true if the model supports functions
|
39
54
|
def supports_functions?(model_id)
|
40
|
-
model_id.
|
55
|
+
model_id.match?(/claude-3/)
|
41
56
|
end
|
42
57
|
|
58
|
+
# Determines if a model supports JSON mode
|
59
|
+
# @param model_id [String] the model identifier
|
60
|
+
# @return [Boolean] true if the model supports JSON mode
|
43
61
|
def supports_json_mode?(model_id)
|
44
|
-
model_id.
|
62
|
+
model_id.match?(/claude-3/)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Determines if a model supports extended thinking
|
66
|
+
# @param model_id [String] the model identifier
|
67
|
+
# @return [Boolean] true if the model supports extended thinking
|
68
|
+
def supports_extended_thinking?(model_id)
|
69
|
+
model_id.match?(/claude-3-7-sonnet/)
|
45
70
|
end
|
46
71
|
|
72
|
+
# Determines the model family for a given model ID
|
73
|
+
# @param model_id [String] the model identifier
|
74
|
+
# @return [Symbol] the model family identifier
|
47
75
|
def model_family(model_id)
|
48
76
|
case model_id
|
77
|
+
when /claude-3-7-sonnet/ then :claude37_sonnet
|
49
78
|
when /claude-3-5-sonnet/ then :claude35_sonnet
|
50
79
|
when /claude-3-5-haiku/ then :claude35_haiku
|
51
80
|
when /claude-3-opus/ then :claude3_opus
|
@@ -55,23 +84,32 @@ module RubyLLM
|
|
55
84
|
end
|
56
85
|
end
|
57
86
|
|
87
|
+
# Returns the model type
|
88
|
+
# @param model_id [String] the model identifier (unused but kept for API consistency)
|
89
|
+
# @return [String] the model type, always 'chat' for Anthropic models
|
58
90
|
def model_type(_)
|
59
91
|
'chat'
|
60
92
|
end
|
61
93
|
|
94
|
+
# Pricing information for Anthropic models (per million tokens)
|
62
95
|
PRICES = {
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
96
|
+
claude37_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
|
97
|
+
claude35_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
|
98
|
+
claude35_haiku: { input: 0.80, output: 4.0 }, # $0.80/$4.00 per million tokens
|
99
|
+
claude3_opus: { input: 15.0, output: 75.0 }, # $15.00/$75.00 per million tokens
|
100
|
+
claude3_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
|
101
|
+
claude3_haiku: { input: 0.25, output: 1.25 }, # $0.25/$1.25 per million tokens
|
102
|
+
claude2: { input: 3.0, output: 15.0 } # Default pricing for Claude 2.x models
|
69
103
|
}.freeze
|
70
104
|
|
105
|
+
# Default input price if model not found in PRICES
|
106
|
+
# @return [Float] default price per million tokens for input
|
71
107
|
def default_input_price
|
72
108
|
3.0
|
73
109
|
end
|
74
110
|
|
111
|
+
# Default output price if model not found in PRICES
|
112
|
+
# @return [Float] default price per million tokens for output
|
75
113
|
def default_output_price
|
76
114
|
15.0
|
77
115
|
end
|
@@ -7,6 +7,9 @@ module RubyLLM
|
|
7
7
|
module Capabilities
|
8
8
|
module_function
|
9
9
|
|
10
|
+
# Returns the context window size for the given model
|
11
|
+
# @param model_id [String] the model identifier
|
12
|
+
# @return [Integer] the context window size in tokens
|
10
13
|
def context_window_for(model_id)
|
11
14
|
case model_id
|
12
15
|
when /deepseek-(?:chat|reasoner)/ then 64_000
|
@@ -14,6 +17,9 @@ module RubyLLM
|
|
14
17
|
end
|
15
18
|
end
|
16
19
|
|
20
|
+
# Returns the maximum number of tokens that can be generated
|
21
|
+
# @param model_id [String] the model identifier
|
22
|
+
# @return [Integer] the maximum number of tokens
|
17
23
|
def max_tokens_for(model_id)
|
18
24
|
case model_id
|
19
25
|
when /deepseek-(?:chat|reasoner)/ then 8_192
|
@@ -21,30 +27,51 @@ module RubyLLM
|
|
21
27
|
end
|
22
28
|
end
|
23
29
|
|
30
|
+
# Returns the price per million tokens for input (cache miss)
|
31
|
+
# @param model_id [String] the model identifier
|
32
|
+
# @return [Float] the price per million tokens in USD
|
24
33
|
def input_price_for(model_id)
|
25
34
|
PRICES.dig(model_family(model_id), :input_miss) || default_input_price
|
26
35
|
end
|
27
36
|
|
37
|
+
# Returns the price per million tokens for output
|
38
|
+
# @param model_id [String] the model identifier
|
39
|
+
# @return [Float] the price per million tokens in USD
|
28
40
|
def output_price_for(model_id)
|
29
41
|
PRICES.dig(model_family(model_id), :output) || default_output_price
|
30
42
|
end
|
31
43
|
|
44
|
+
# Returns the price per million tokens for input with cache hit
|
45
|
+
# @param model_id [String] the model identifier
|
46
|
+
# @return [Float] the price per million tokens in USD
|
32
47
|
def cache_hit_price_for(model_id)
|
33
48
|
PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
|
34
49
|
end
|
35
50
|
|
51
|
+
# Determines if the model supports vision capabilities
|
52
|
+
# @param model_id [String] the model identifier
|
53
|
+
# @return [Boolean] true if the model supports vision
|
36
54
|
def supports_vision?(_model_id)
|
37
55
|
false # DeepSeek models don't currently support vision
|
38
56
|
end
|
39
57
|
|
58
|
+
# Determines if the model supports function calling
|
59
|
+
# @param model_id [String] the model identifier
|
60
|
+
# @return [Boolean] true if the model supports function calling
|
40
61
|
def supports_functions?(model_id)
|
41
62
|
model_id.match?(/deepseek-chat/) # Only deepseek-chat supports function calling
|
42
63
|
end
|
43
64
|
|
65
|
+
# Determines if the model supports JSON mode
|
66
|
+
# @param model_id [String] the model identifier
|
67
|
+
# @return [Boolean] true if the model supports JSON mode
|
44
68
|
def supports_json_mode?(model_id)
|
45
69
|
model_id.match?(/deepseek-chat/) # Only deepseek-chat supports JSON mode
|
46
70
|
end
|
47
71
|
|
72
|
+
# Returns a formatted display name for the model
|
73
|
+
# @param model_id [String] the model identifier
|
74
|
+
# @return [String] the formatted display name
|
48
75
|
def format_display_name(model_id)
|
49
76
|
case model_id
|
50
77
|
when 'deepseek-chat' then 'DeepSeek V3'
|
@@ -56,10 +83,16 @@ module RubyLLM
|
|
56
83
|
end
|
57
84
|
end
|
58
85
|
|
86
|
+
# Returns the model type
|
87
|
+
# @param model_id [String] the model identifier
|
88
|
+
# @return [String] the model type (e.g., 'chat')
|
59
89
|
def model_type(_model_id)
|
60
90
|
'chat' # All DeepSeek models are chat models
|
61
91
|
end
|
62
92
|
|
93
|
+
# Returns the model family
|
94
|
+
# @param model_id [String] the model identifier
|
95
|
+
# @return [Symbol] the model family
|
63
96
|
def model_family(model_id)
|
64
97
|
case model_id
|
65
98
|
when /deepseek-chat/ then :chat
|
@@ -84,14 +117,20 @@ module RubyLLM
|
|
84
117
|
|
85
118
|
private
|
86
119
|
|
120
|
+
# Default input price when model family can't be determined
|
121
|
+
# @return [Float] the default input price
|
87
122
|
def default_input_price
|
88
123
|
0.27 # Default to chat cache miss price
|
89
124
|
end
|
90
125
|
|
126
|
+
# Default output price when model family can't be determined
|
127
|
+
# @return [Float] the default output price
|
91
128
|
def default_output_price
|
92
129
|
1.10 # Default to chat output price
|
93
130
|
end
|
94
131
|
|
132
|
+
# Default cache hit price when model family can't be determined
|
133
|
+
# @return [Float] the default cache hit price
|
95
134
|
def default_cache_hit_price
|
96
135
|
0.07 # Default to chat cache hit price
|
97
136
|
end
|
@@ -7,25 +7,34 @@ module RubyLLM
|
|
7
7
|
module Capabilities # rubocop:disable Metrics/ModuleLength
|
8
8
|
module_function
|
9
9
|
|
10
|
+
# Returns the context window size (input token limit) for the given model
|
11
|
+
# @param model_id [String] the model identifier
|
12
|
+
# @return [Integer] the context window size in tokens
|
10
13
|
def context_window_for(model_id)
|
11
14
|
case model_id
|
12
15
|
when /gemini-2\.0-flash/, /gemini-1\.5-flash/ then 1_048_576
|
13
16
|
when /gemini-1\.5-pro/ then 2_097_152
|
14
|
-
when /text-embedding/, /embedding-001/ then 2_048
|
17
|
+
when /text-embedding-004/, /embedding-001/ then 2_048
|
15
18
|
when /aqa/ then 7_168
|
16
19
|
else 32_768 # Sensible default for unknown models
|
17
20
|
end
|
18
21
|
end
|
19
22
|
|
23
|
+
# Returns the maximum output tokens for the given model
|
24
|
+
# @param model_id [String] the model identifier
|
25
|
+
# @return [Integer] the maximum output tokens
|
20
26
|
def max_tokens_for(model_id)
|
21
27
|
case model_id
|
22
28
|
when /gemini-2\.0-flash/, /gemini-1\.5/ then 8_192
|
23
|
-
when /text-embedding/, /embedding-001/ then 768 # Output dimension size for embeddings
|
29
|
+
when /text-embedding-004/, /embedding-001/ then 768 # Output dimension size for embeddings
|
24
30
|
when /aqa/ then 1_024
|
25
31
|
else 4_096 # Sensible default
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
35
|
+
# Returns the input price per million tokens for the given model
|
36
|
+
# @param model_id [String] the model identifier
|
37
|
+
# @return [Float] the price per million tokens in USD
|
29
38
|
def input_price_for(model_id)
|
30
39
|
base_price = PRICES.dig(pricing_family(model_id), :input) || default_input_price
|
31
40
|
return base_price unless long_context_model?(model_id)
|
@@ -34,6 +43,9 @@ module RubyLLM
|
|
34
43
|
context_length(model_id) > 128_000 ? base_price * 2 : base_price
|
35
44
|
end
|
36
45
|
|
46
|
+
# Returns the output price per million tokens for the given model
|
47
|
+
# @param model_id [String] the model identifier
|
48
|
+
# @return [Float] the price per million tokens in USD
|
37
49
|
def output_price_for(model_id)
|
38
50
|
base_price = PRICES.dig(pricing_family(model_id), :output) || default_output_price
|
39
51
|
return base_price unless long_context_model?(model_id)
|
@@ -42,6 +54,9 @@ module RubyLLM
|
|
42
54
|
context_length(model_id) > 128_000 ? base_price * 2 : base_price
|
43
55
|
end
|
44
56
|
|
57
|
+
# Determines if the model supports vision (image/video) inputs
|
58
|
+
# @param model_id [String] the model identifier
|
59
|
+
# @return [Boolean] true if the model supports vision inputs
|
45
60
|
def supports_vision?(model_id)
|
46
61
|
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
|
47
62
|
return false if model_id.match?(/gemini-1\.0/)
|
@@ -49,6 +64,9 @@ module RubyLLM
|
|
49
64
|
model_id.match?(/gemini-[12]\.[05]/)
|
50
65
|
end
|
51
66
|
|
67
|
+
# Determines if the model supports function calling
|
68
|
+
# @param model_id [String] the model identifier
|
69
|
+
# @return [Boolean] true if the model supports function calling
|
52
70
|
def supports_functions?(model_id)
|
53
71
|
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
|
54
72
|
return false if model_id.match?(/flash-lite/)
|
@@ -57,13 +75,20 @@ module RubyLLM
|
|
57
75
|
model_id.match?(/gemini-[12]\.[05]-(?:pro|flash)(?!-lite)/)
|
58
76
|
end
|
59
77
|
|
78
|
+
# Determines if the model supports JSON mode
|
79
|
+
# @param model_id [String] the model identifier
|
80
|
+
# @return [Boolean] true if the model supports JSON mode
|
60
81
|
def supports_json_mode?(model_id)
|
61
82
|
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
|
62
83
|
return false if model_id.match?(/gemini-1\.0/)
|
84
|
+
return false if model_id.match?(/gemini-2\.0-flash-lite/)
|
63
85
|
|
64
86
|
model_id.match?(/gemini-\d/)
|
65
87
|
end
|
66
88
|
|
89
|
+
# Formats the model ID into a human-readable display name
|
90
|
+
# @param model_id [String] the model identifier
|
91
|
+
# @return [String] the formatted display name
|
67
92
|
def format_display_name(model_id)
|
68
93
|
model_id
|
69
94
|
.delete_prefix('models/')
|
@@ -76,20 +101,32 @@ module RubyLLM
|
|
76
101
|
.strip
|
77
102
|
end
|
78
103
|
|
104
|
+
# Determines if the model supports context caching
|
105
|
+
# @param model_id [String] the model identifier
|
106
|
+
# @return [Boolean] true if the model supports caching
|
79
107
|
def supports_caching?(model_id)
|
80
108
|
return false if model_id.match?(/flash-lite|gemini-1\.0/)
|
81
109
|
|
82
110
|
model_id.match?(/gemini-[12]\.[05]/)
|
83
111
|
end
|
84
112
|
|
113
|
+
# Determines if the model supports tuning
|
114
|
+
# @param model_id [String] the model identifier
|
115
|
+
# @return [Boolean] true if the model supports tuning
|
85
116
|
def supports_tuning?(model_id)
|
86
117
|
model_id.match?(/gemini-1\.5-flash/)
|
87
118
|
end
|
88
119
|
|
120
|
+
# Determines if the model supports audio inputs
|
121
|
+
# @param model_id [String] the model identifier
|
122
|
+
# @return [Boolean] true if the model supports audio inputs
|
89
123
|
def supports_audio?(model_id)
|
90
124
|
model_id.match?(/gemini-[12]\.[05]/)
|
91
125
|
end
|
92
126
|
|
127
|
+
# Returns the type of model (chat, embedding, image)
|
128
|
+
# @param model_id [String] the model identifier
|
129
|
+
# @return [String] the model type
|
93
130
|
def model_type(model_id)
|
94
131
|
case model_id
|
95
132
|
when /text-embedding|embedding/ then 'embedding'
|
@@ -98,6 +135,9 @@ module RubyLLM
|
|
98
135
|
end
|
99
136
|
end
|
100
137
|
|
138
|
+
# Returns the model family identifier
|
139
|
+
# @param model_id [String] the model identifier
|
140
|
+
# @return [String] the model family identifier
|
101
141
|
def model_family(model_id) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
|
102
142
|
case model_id
|
103
143
|
when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
|
@@ -113,7 +153,10 @@ module RubyLLM
|
|
113
153
|
end
|
114
154
|
end
|
115
155
|
|
116
|
-
|
156
|
+
# Returns the pricing family identifier for the model
|
157
|
+
# @param model_id [String] the model identifier
|
158
|
+
# @return [Symbol] the pricing family identifier
|
159
|
+
def pricing_family(model_id) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
|
117
160
|
case model_id
|
118
161
|
when /gemini-2\.0-flash-lite/ then :flash_lite_2 # rubocop:disable Naming/VariableNumber
|
119
162
|
when /gemini-2\.0-flash/ then :flash_2 # rubocop:disable Naming/VariableNumber
|
@@ -122,18 +165,26 @@ module RubyLLM
|
|
122
165
|
when /gemini-1\.5-pro/ then :pro
|
123
166
|
when /gemini-1\.0-pro/ then :pro_1_0 # rubocop:disable Naming/VariableNumber
|
124
167
|
when /text-embedding|embedding/ then :embedding
|
168
|
+
when /aqa/ then :aqa
|
125
169
|
else :base
|
126
170
|
end
|
127
171
|
end
|
128
172
|
|
173
|
+
# Determines if the model supports long context
|
174
|
+
# @param model_id [String] the model identifier
|
175
|
+
# @return [Boolean] true if the model supports long context
|
129
176
|
def long_context_model?(model_id)
|
130
177
|
model_id.match?(/gemini-1\.5-(?:pro|flash)/)
|
131
178
|
end
|
132
179
|
|
180
|
+
# Returns the context length for the model
|
181
|
+
# @param model_id [String] the model identifier
|
182
|
+
# @return [Integer] the context length in tokens
|
133
183
|
def context_length(model_id)
|
134
184
|
context_window_for(model_id)
|
135
185
|
end
|
136
186
|
|
187
|
+
# Pricing information for Gemini models (per 1M tokens in USD)
|
137
188
|
PRICES = {
|
138
189
|
flash_2: { # Gemini 2.0 Flash # rubocop:disable Naming/VariableNumber
|
139
190
|
input: 0.10,
|
@@ -152,19 +203,22 @@ module RubyLLM
|
|
152
203
|
input: 0.075,
|
153
204
|
output: 0.30,
|
154
205
|
cache: 0.01875,
|
155
|
-
cache_storage: 1.00
|
206
|
+
cache_storage: 1.00,
|
207
|
+
grounding_search: 35.00 # per 1K requests
|
156
208
|
},
|
157
209
|
flash_8b: { # Gemini 1.5 Flash 8B
|
158
210
|
input: 0.0375,
|
159
211
|
output: 0.15,
|
160
212
|
cache: 0.01,
|
161
|
-
cache_storage: 0.25
|
213
|
+
cache_storage: 0.25,
|
214
|
+
grounding_search: 35.00 # per 1K requests
|
162
215
|
},
|
163
216
|
pro: { # Gemini 1.5 Pro
|
164
217
|
input: 1.25,
|
165
218
|
output: 5.0,
|
166
219
|
cache: 0.3125,
|
167
|
-
cache_storage: 4.50
|
220
|
+
cache_storage: 4.50,
|
221
|
+
grounding_search: 35.00 # per 1K requests
|
168
222
|
},
|
169
223
|
pro_1_0: { # Gemini 1.0 Pro # rubocop:disable Naming/VariableNumber
|
170
224
|
input: 0.50,
|
@@ -173,15 +227,23 @@ module RubyLLM
|
|
173
227
|
embedding: { # Text Embedding models
|
174
228
|
input: 0.00,
|
175
229
|
output: 0.00
|
230
|
+
},
|
231
|
+
aqa: { # AQA model
|
232
|
+
input: 0.00,
|
233
|
+
output: 0.00
|
176
234
|
}
|
177
235
|
}.freeze
|
178
236
|
|
237
|
+
# Default input price for unknown models
|
238
|
+
# @return [Float] the default input price per million tokens
|
179
239
|
def default_input_price
|
180
240
|
0.075 # Default to Flash pricing
|
181
241
|
end
|
182
242
|
|
243
|
+
# Default output price for unknown models
|
244
|
+
# @return [Float] the default output price per million tokens
|
183
245
|
def default_output_price
|
184
|
-
0.30
|
246
|
+
0.30 # Default to Flash pricing
|
185
247
|
end
|
186
248
|
end
|
187
249
|
end
|
@@ -7,76 +7,113 @@ module RubyLLM
|
|
7
7
|
module Capabilities # rubocop:disable Metrics/ModuleLength
|
8
8
|
module_function
|
9
9
|
|
10
|
+
# Returns the context window size for the given model ID
|
11
|
+
# @param model_id [String] the model identifier
|
12
|
+
# @return [Integer] the context window size in tokens
|
10
13
|
def context_window_for(model_id)
|
11
14
|
case model_id
|
12
|
-
when /
|
13
|
-
when /o1-
|
14
|
-
when /gpt-
|
15
|
-
when /gpt-
|
16
|
-
when /gpt-3.5
|
17
|
-
when /
|
15
|
+
when /o1-2024/, /o3-mini/, /o3-mini-2025/ then 200_000
|
16
|
+
when /gpt-4o/, /gpt-4o-mini/, /gpt-4-turbo/, /o1-mini/ then 128_000
|
17
|
+
when /gpt-4-0[0-9]{3}/ then 8_192
|
18
|
+
when /gpt-3.5-turbo-instruct/ then 4_096
|
19
|
+
when /gpt-3.5/ then 16_385
|
20
|
+
when /babbage-002/, /davinci-002/ then 16_384
|
18
21
|
else 4_096
|
19
22
|
end
|
20
23
|
end
|
21
24
|
|
22
|
-
|
25
|
+
# Returns the maximum output tokens for the given model ID
|
26
|
+
# @param model_id [String] the model identifier
|
27
|
+
# @return [Integer] the maximum output tokens
|
28
|
+
def max_tokens_for(model_id) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
|
23
29
|
case model_id
|
24
|
-
when /o1-2024/, /o3-mini/
|
25
|
-
when /o1-mini-2024/
|
26
|
-
when /gpt-4o-2024-05-13/
|
27
|
-
when /gpt-4o/, /gpt-4o-mini/
|
28
|
-
when /gpt-4o-
|
29
|
-
when /gpt-4-0[0-9]{3}/
|
30
|
-
when /gpt-3.5-turbo/
|
30
|
+
when /o1-2024/, /o3-mini/, /o3-mini-2025/ then 100_000
|
31
|
+
when /o1-mini-2024/ then 65_536
|
32
|
+
when /gpt-4o-2024-05-13/ then 4_096
|
33
|
+
when /gpt-4o-realtime/, /gpt-4o-mini-realtime/ then 4_096
|
34
|
+
when /gpt-4o/, /gpt-4o-mini/, /gpt-4o-audio/, /gpt-4o-mini-audio/ then 16_384
|
35
|
+
when /gpt-4-0[0-9]{3}/ then 8_192
|
36
|
+
when /gpt-4-turbo/, /gpt-3.5-turbo/ then 4_096
|
37
|
+
when /babbage-002/, /davinci-002/ then 16_384
|
31
38
|
else 4_096
|
32
39
|
end
|
33
40
|
end
|
34
41
|
|
42
|
+
# Returns the input price per million tokens for the given model ID
|
43
|
+
# @param model_id [String] the model identifier
|
44
|
+
# @return [Float] the price per million tokens for input
|
35
45
|
def input_price_for(model_id)
|
36
46
|
PRICES.dig(model_family(model_id), :input) || default_input_price
|
37
47
|
end
|
38
48
|
|
49
|
+
# Returns the output price per million tokens for the given model ID
|
50
|
+
# @param model_id [String] the model identifier
|
51
|
+
# @return [Float] the price per million tokens for output
|
39
52
|
def output_price_for(model_id)
|
40
53
|
PRICES.dig(model_family(model_id), :output) || default_output_price
|
41
54
|
end
|
42
55
|
|
56
|
+
# Determines if the model supports vision capabilities
|
57
|
+
# @param model_id [String] the model identifier
|
58
|
+
# @return [Boolean] true if the model supports vision
|
43
59
|
def supports_vision?(model_id)
|
44
60
|
model_id.match?(/gpt-4o|o1/) || model_id.match?(/gpt-4-(?!0314|0613)/)
|
45
61
|
end
|
46
62
|
|
63
|
+
# Determines if the model supports function calling
|
64
|
+
# @param model_id [String] the model identifier
|
65
|
+
# @return [Boolean] true if the model supports functions
|
47
66
|
def supports_functions?(model_id)
|
48
67
|
!model_id.include?('instruct')
|
49
68
|
end
|
50
69
|
|
70
|
+
# Determines if the model supports audio input/output
|
71
|
+
# @param model_id [String] the model identifier
|
72
|
+
# @return [Boolean] true if the model supports audio
|
51
73
|
def supports_audio?(model_id)
|
52
74
|
model_id.match?(/audio-preview|realtime-preview|whisper|tts/)
|
53
75
|
end
|
54
76
|
|
77
|
+
# Determines if the model supports JSON mode
|
78
|
+
# @param model_id [String] the model identifier
|
79
|
+
# @return [Boolean] true if the model supports JSON mode
|
55
80
|
def supports_json_mode?(model_id)
|
56
81
|
model_id.match?(/gpt-4-\d{4}-preview/) ||
|
57
82
|
model_id.include?('turbo') ||
|
58
83
|
model_id.match?(/gpt-3.5-turbo-(?!0301|0613)/)
|
59
84
|
end
|
60
85
|
|
86
|
+
# Formats the model ID into a human-readable display name
|
87
|
+
# @param model_id [String] the model identifier
|
88
|
+
# @return [String] the formatted display name
|
61
89
|
def format_display_name(model_id)
|
62
90
|
model_id.then { |id| humanize(id) }
|
63
91
|
.then { |name| apply_special_formatting(name) }
|
64
92
|
end
|
65
93
|
|
94
|
+
# Determines the type of model
|
95
|
+
# @param model_id [String] the model identifier
|
96
|
+
# @return [String] the model type (chat, embedding, image, audio, moderation)
|
66
97
|
def model_type(model_id)
|
67
98
|
case model_id
|
68
99
|
when /text-embedding|embedding/ then 'embedding'
|
69
100
|
when /dall-e/ then 'image'
|
70
101
|
when /tts|whisper/ then 'audio'
|
71
|
-
when /omni-moderation/ then 'moderation'
|
102
|
+
when /omni-moderation|text-moderation/ then 'moderation'
|
72
103
|
else 'chat'
|
73
104
|
end
|
74
105
|
end
|
75
106
|
|
107
|
+
# Determines if the model supports structured output
|
108
|
+
# @param model_id [String] the model identifier
|
109
|
+
# @return [Boolean] true if the model supports structured output
|
76
110
|
def supports_structured_output?(model_id)
|
77
|
-
model_id.match?(/gpt-4o|o[13]-mini|o1/)
|
111
|
+
model_id.match?(/gpt-4o|o[13]-mini|o1|o3-mini/)
|
78
112
|
end
|
79
113
|
|
114
|
+
# Determines the model family for pricing and capability lookup
|
115
|
+
# @param model_id [String] the model identifier
|
116
|
+
# @return [Symbol] the model family identifier
|
80
117
|
def model_family(model_id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
|
81
118
|
case model_id
|
82
119
|
when /o3-mini/ then 'o3_mini'
|
@@ -100,13 +137,14 @@ module RubyLLM
|
|
100
137
|
when /tts-1-hd/ then 'tts1_hd'
|
101
138
|
when /tts-1/ then 'tts1'
|
102
139
|
when /whisper/ then 'whisper1'
|
103
|
-
when /omni-moderation/ then 'moderation'
|
140
|
+
when /omni-moderation|text-moderation/ then 'moderation'
|
104
141
|
when /babbage/ then 'babbage'
|
105
142
|
when /davinci/ then 'davinci'
|
106
143
|
else 'other'
|
107
144
|
end
|
108
145
|
end
|
109
146
|
|
147
|
+
# Pricing information for OpenAI models (per million tokens unless otherwise specified)
|
110
148
|
PRICES = {
|
111
149
|
o1: { input: 15.0, cached_input: 7.5, output: 60.0 },
|
112
150
|
o1_mini: { input: 1.10, cached_input: 0.55, output: 4.40 },
|
@@ -150,19 +188,27 @@ module RubyLLM
|
|
150
188
|
embedding2: { price: 0.10 },
|
151
189
|
davinci: { input: 2.0, output: 2.0 },
|
152
190
|
babbage: { input: 0.40, output: 0.40 },
|
153
|
-
tts1: { price: 15.0 },
|
154
|
-
tts1_hd: { price: 30.0 },
|
155
|
-
whisper1: { price: 0.006 }
|
191
|
+
tts1: { price: 15.0 }, # per million characters
|
192
|
+
tts1_hd: { price: 30.0 }, # per million characters
|
193
|
+
whisper1: { price: 0.006 }, # per minute
|
194
|
+
moderation: { price: 0.0 } # free
|
156
195
|
}.freeze
|
157
196
|
|
197
|
+
# Default input price when model-specific pricing is not available
|
198
|
+
# @return [Float] the default price per million tokens
|
158
199
|
def default_input_price
|
159
200
|
0.50
|
160
201
|
end
|
161
202
|
|
203
|
+
# Default output price when model-specific pricing is not available
|
204
|
+
# @return [Float] the default price per million tokens
|
162
205
|
def default_output_price
|
163
206
|
1.50
|
164
207
|
end
|
165
208
|
|
209
|
+
# Converts a model ID to a human-readable format
|
210
|
+
# @param id [String] the model identifier
|
211
|
+
# @return [String] the humanized model name
|
166
212
|
def humanize(id)
|
167
213
|
id.tr('-', ' ')
|
168
214
|
.split(' ')
|
@@ -170,18 +216,25 @@ module RubyLLM
|
|
170
216
|
.join(' ')
|
171
217
|
end
|
172
218
|
|
219
|
+
# Applies special formatting rules to model names
|
220
|
+
# @param name [String] the humanized model name
|
221
|
+
# @return [String] the specially formatted model name
|
173
222
|
def apply_special_formatting(name) # rubocop:disable Metrics/MethodLength
|
174
223
|
name
|
175
224
|
.gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1\2\3')
|
176
225
|
.gsub(/^Gpt /, 'GPT-')
|
177
226
|
.gsub(/^O([13]) /, 'O\1-')
|
227
|
+
.gsub(/^O3 Mini/, 'O3-Mini')
|
228
|
+
.gsub(/^O1 Mini/, 'O1-Mini')
|
178
229
|
.gsub(/^Chatgpt /, 'ChatGPT-')
|
179
230
|
.gsub(/^Tts /, 'TTS-')
|
180
231
|
.gsub(/^Dall E /, 'DALL-E-')
|
181
232
|
.gsub(/3\.5 /, '3.5-')
|
182
233
|
.gsub(/4 /, '4-')
|
183
|
-
.gsub(/4o (?=Mini|Preview|Turbo|Audio)/, '4o-')
|
234
|
+
.gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime)/, '4o-')
|
184
235
|
.gsub(/\bHd\b/, 'HD')
|
236
|
+
.gsub(/Omni Moderation/, 'Omni-Moderation')
|
237
|
+
.gsub(/Text Moderation/, 'Text-Moderation')
|
185
238
|
end
|
186
239
|
end
|
187
240
|
end
|
data/lib/ruby_llm/version.rb
CHANGED
data/lib/tasks/models.rake
CHANGED
@@ -16,6 +16,9 @@ PROVIDER_DOCS = {
|
|
16
16
|
},
|
17
17
|
deepseek: {
|
18
18
|
models: 'https://api-docs.deepseek.com/quick_start/pricing/'
|
19
|
+
},
|
20
|
+
anthropic: {
|
21
|
+
models: 'https://docs.anthropic.com/en/docs/about-claude/models/all-models'
|
19
22
|
}
|
20
23
|
}.freeze
|
21
24
|
|
@@ -85,8 +88,10 @@ namespace :models do # rubocop:disable Metrics/BlockLength
|
|
85
88
|
end
|
86
89
|
end
|
87
90
|
|
88
|
-
desc 'Update model capabilities modules by scraping provider documentation'
|
91
|
+
desc 'Update model capabilities modules by scraping provider documentation (use PROVIDER=name to update only one)'
|
89
92
|
task :update_capabilities do # rubocop:disable Metrics/BlockLength
|
93
|
+
# Check if a specific provider was requested
|
94
|
+
target_provider = ENV['PROVIDER']&.to_sym
|
90
95
|
require 'ruby_llm'
|
91
96
|
require 'fileutils'
|
92
97
|
|
@@ -97,8 +102,15 @@ namespace :models do # rubocop:disable Metrics/BlockLength
|
|
97
102
|
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY')
|
98
103
|
end
|
99
104
|
|
105
|
+
# Filter providers if a specific one was requested
|
106
|
+
providers_to_process = if target_provider && PROVIDER_DOCS.key?(target_provider)
|
107
|
+
{ target_provider => PROVIDER_DOCS[target_provider] }
|
108
|
+
else
|
109
|
+
PROVIDER_DOCS
|
110
|
+
end
|
111
|
+
|
100
112
|
# Process each provider
|
101
|
-
|
113
|
+
providers_to_process.each do |provider, urls| # rubocop:disable Metrics/BlockLength
|
102
114
|
puts "Processing #{provider}..."
|
103
115
|
|
104
116
|
# Initialize our AI assistants
|
@@ -175,12 +187,22 @@ namespace :models do # rubocop:disable Metrics/BlockLength
|
|
175
187
|
|
176
188
|
response = claude.ask(code_prompt)
|
177
189
|
|
190
|
+
# Extract Ruby code from Claude's response
|
191
|
+
puts " Extracting Ruby code from Claude's response..."
|
192
|
+
ruby_code = nil
|
193
|
+
|
194
|
+
# Look for Ruby code block
|
195
|
+
ruby_code = Regexp.last_match(1).strip if response.content =~ /```ruby\s*(.*?)```/m
|
196
|
+
|
197
|
+
# Verify we found Ruby code
|
198
|
+
raise "No Ruby code block found in Claude's response" if ruby_code.nil? || ruby_code.empty?
|
199
|
+
|
178
200
|
# Save the file
|
179
201
|
file_path = "lib/ruby_llm/providers/#{provider}/capabilities.rb"
|
180
202
|
puts " Writing #{file_path}..."
|
181
203
|
|
182
204
|
FileUtils.mkdir_p(File.dirname(file_path))
|
183
|
-
File.write(file_path,
|
205
|
+
File.write(file_path, ruby_code)
|
184
206
|
rescue StandardError => e
|
185
207
|
raise "Failed to process #{provider}: #{e.message}"
|
186
208
|
end
|