rubik 0.3.4 → 0.4.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/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +105 -2
- data/lib/rubik/adapter/process.rb +11 -8
- data/lib/rubik/ast/go_extractor.rb +9 -2
- data/lib/rubik/ast/graph.rb +20 -1
- data/lib/rubik/ast/graph_extensions.rb +89 -1
- data/lib/rubik/ast/python_extractor.rb +5 -5
- data/lib/rubik/ast/rust_extractor.rb +9 -2
- data/lib/rubik/ast/typescript_extractor.rb +13 -7
- data/lib/rubik/buffer/manager.rb +78 -66
- data/lib/rubik/config/defaults.rb +1 -0
- data/lib/rubik/context/git_lister.rb +3 -1
- data/lib/rubik/diagnostics/parser.rb +53 -1
- data/lib/rubik/llm/anthropic.rb +5 -2
- data/lib/rubik/llm/openai_compatible.rb +83 -68
- data/lib/rubik/llm/registry.rb +81 -74
- data/lib/rubik/lsp/client.rb +320 -96
- data/lib/rubik/lsp/manager.rb +83 -12
- data/lib/rubik/lsp/server_command.rb +102 -0
- data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
- data/lib/rubik/orchestrator/step_executor.rb +138 -154
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
- data/lib/rubik/prompt/token_estimator.rb +16 -12
- data/lib/rubik/repl/commands/agent_context.rb +126 -104
- data/lib/rubik/repl/commands/agent_execution.rb +83 -68
- data/lib/rubik/repl/commands/agent_models.rb +112 -87
- data/lib/rubik/repl/commands/buffer.rb +122 -88
- data/lib/rubik/repl/commands/mcp.rb +120 -92
- data/lib/rubik/repl/commands/session.rb +90 -59
- data/lib/rubik/repl/commands/workspace.rb +110 -94
- data/lib/rubik/repl/shell.rb +10 -1
- data/lib/rubik/runtime/discovery.rb +12 -6
- data/lib/rubik/security/classifier/classifier.rb +3 -1
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +6 -2
- data/lib/rubik/storage/vakanuvis/gate.rb +37 -16
- data/lib/rubik/storage/vakanuvis/harness.rb +36 -21
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +12 -6
- data/lib/rubik/tools/builtins/lifecycle.rb +34 -7
- data/lib/rubik/tools/builtins/lsp.rb +36 -8
- data/lib/rubik/tools/truncator.rb +21 -8
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
|
@@ -21,8 +21,12 @@ module Rubik
|
|
|
21
21
|
parse_go(text)
|
|
22
22
|
when :rust
|
|
23
23
|
parse_rust(text)
|
|
24
|
-
when :zig, :c
|
|
24
|
+
when :zig, :c, :cpp, :"c++", :cxx
|
|
25
25
|
parse_zig_c(text)
|
|
26
|
+
when :python, :py
|
|
27
|
+
parse_python(text)
|
|
28
|
+
when :typescript, :javascript, :ts, :js
|
|
29
|
+
parse_ts_js(text)
|
|
26
30
|
else
|
|
27
31
|
parse_generic(text)
|
|
28
32
|
end
|
|
@@ -93,6 +97,54 @@ module Rubik
|
|
|
93
97
|
entries
|
|
94
98
|
end
|
|
95
99
|
|
|
100
|
+
def parse_python(text)
|
|
101
|
+
entries = []
|
|
102
|
+
current_file = nil
|
|
103
|
+
current_line = nil
|
|
104
|
+
|
|
105
|
+
text.each_line do |line|
|
|
106
|
+
if (m = line.match(/File "([^"]+)", line (\d+)(?:, in .*)?/))
|
|
107
|
+
current_file = m[1]
|
|
108
|
+
current_line = m[2].to_i
|
|
109
|
+
elsif (m = line.match(/^([A-Za-z0-9_]+Error|Exception):\s*(.*)$/))
|
|
110
|
+
cat = m[1] =~ /\A(?:Syntax|Indentation|Tab)Error\z/ ? :syntax : :type
|
|
111
|
+
entries << Entry.new(
|
|
112
|
+
file: current_file,
|
|
113
|
+
line: current_line,
|
|
114
|
+
message: "#{m[1]}: #{m[2]}",
|
|
115
|
+
category: cat,
|
|
116
|
+
raw_text: line
|
|
117
|
+
)
|
|
118
|
+
current_file = nil
|
|
119
|
+
current_line = nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
entries.empty? ? parse_generic(text) : entries
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_ts_js(text)
|
|
126
|
+
entries = []
|
|
127
|
+
text.each_line do |line|
|
|
128
|
+
next unless (m = line.match(%r{^((?:[a-zA-Z]:[\\/])?[^(:\s]+)(?:\((\d+),(\d+)\)|:(\d+):(\d+)):?\s*-?\s*error\s*(TS\d+)?:?\s*(.*)$}i))
|
|
129
|
+
|
|
130
|
+
file = m[1]
|
|
131
|
+
line_num = (m[2] || m[4]).to_i
|
|
132
|
+
col_num = (m[3] || m[5]).to_i
|
|
133
|
+
code = m[6]
|
|
134
|
+
msg = m[7]
|
|
135
|
+
entries << Entry.new(
|
|
136
|
+
file: file,
|
|
137
|
+
line: line_num,
|
|
138
|
+
col: col_num,
|
|
139
|
+
code: code,
|
|
140
|
+
message: msg,
|
|
141
|
+
category: :syntax,
|
|
142
|
+
raw_text: line
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
entries.empty? ? parse_generic(text) : entries
|
|
146
|
+
end
|
|
147
|
+
|
|
96
148
|
def parse_generic(text)
|
|
97
149
|
entries = []
|
|
98
150
|
text.each_line do |line|
|
data/lib/rubik/llm/anthropic.rb
CHANGED
|
@@ -56,8 +56,11 @@ module Rubik
|
|
|
56
56
|
return opts[:api_key] if opts.key?(:api_key) && !opts[:api_key].nil?
|
|
57
57
|
return @api_key if @api_key && !@api_key.to_s.empty?
|
|
58
58
|
|
|
59
|
-
env_name = opts[:api_key_env]
|
|
60
|
-
|
|
59
|
+
env_name = opts[:api_key_env]
|
|
60
|
+
env_name = @api_key_env if env_name.nil? || env_name.to_s.empty?
|
|
61
|
+
env_name = "ANTHROPIC_API_KEY" if env_name.nil? || env_name.to_s.empty?
|
|
62
|
+
|
|
63
|
+
ENV.fetch(env_name.to_s, nil)
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
def complete(messages, opts = {})
|
|
@@ -50,20 +50,11 @@ module Rubik
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def complete(messages, opts = {})
|
|
53
|
-
resolved_key = opts[:api_key] || @api_key || (ENV.fetch(@api_key_env, nil) if @api_key_env)
|
|
54
53
|
target_model = opts[:model] || @model
|
|
55
54
|
target_endpoint = self.class.normalize_endpoint(opts[:endpoint] || @endpoint)
|
|
56
55
|
messages = Rubik::LLM::MessageSanitizer.normalize(messages)
|
|
57
56
|
|
|
58
|
-
req_headers =
|
|
59
|
-
req_headers["Authorization"] = "Bearer #{resolved_key}" if resolved_key && !resolved_key.empty?
|
|
60
|
-
|
|
61
|
-
# Merge custom headers (e.g. OpenRouter HTTP-Referer, X-Title)
|
|
62
|
-
@headers.each do |k, v|
|
|
63
|
-
req_headers[k.to_s] = v.to_s
|
|
64
|
-
end
|
|
65
|
-
req_headers.merge!(opts[:headers]) if opts[:headers].is_a?(Hash)
|
|
66
|
-
|
|
57
|
+
req_headers = build_headers(opts)
|
|
67
58
|
formatted_msgs = format_messages(messages)
|
|
68
59
|
payload = build_payload(target_model, formatted_msgs, opts, stream: false)
|
|
69
60
|
|
|
@@ -79,73 +70,17 @@ module Rubik
|
|
|
79
70
|
on_retry: opts[:on_retry]
|
|
80
71
|
)
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
msg = root_json.dig("choices", 0, "message") || {}
|
|
84
|
-
text = msg["content"] || ""
|
|
85
|
-
thought = msg["reasoning_content"] || msg["reasoning"] || msg["thought"]
|
|
86
|
-
thought = nil if thought.to_s.empty?
|
|
87
|
-
usage = root_json["usage"] || res_json["usage"] || {}
|
|
88
|
-
|
|
89
|
-
raw_tool_calls = msg["tool_calls"] || []
|
|
90
|
-
parsed_tool_calls = raw_tool_calls.map do |tc|
|
|
91
|
-
fn = tc["function"] || {}
|
|
92
|
-
args = fn["arguments"]
|
|
93
|
-
malformed = nil
|
|
94
|
-
if args.is_a?(String)
|
|
95
|
-
begin
|
|
96
|
-
parsed = JSON.parse(args)
|
|
97
|
-
if parsed.is_a?(Hash)
|
|
98
|
-
args = parsed
|
|
99
|
-
else
|
|
100
|
-
malformed = args
|
|
101
|
-
args = {}
|
|
102
|
-
end
|
|
103
|
-
rescue JSON::ParserError
|
|
104
|
-
# Issue #55 3.4: the degrade carries the rejected payload so
|
|
105
|
-
# the executor can echo it back to the model in the error row.
|
|
106
|
-
malformed = args
|
|
107
|
-
args = {}
|
|
108
|
-
end
|
|
109
|
-
elsif !args.is_a?(Hash) && !args.nil?
|
|
110
|
-
malformed = args.to_s
|
|
111
|
-
args = {}
|
|
112
|
-
end
|
|
113
|
-
row = {
|
|
114
|
-
"id" => tc["id"] || "call_#{rand(1000..9999)}",
|
|
115
|
-
"tool" => fn["name"],
|
|
116
|
-
"args" => args || {}
|
|
117
|
-
}
|
|
118
|
-
row["malformed_args"] = malformed if malformed
|
|
119
|
-
row
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
in_tokens = usage["prompt_tokens"] || usage["input_tokens"] || estimate_tokens(messages.map { |m| m[:content] || m["content"] }.join)
|
|
123
|
-
out_tokens = usage["completion_tokens"] || usage["output_tokens"] || estimate_tokens(text)
|
|
124
|
-
|
|
125
|
-
Response.new(
|
|
126
|
-
text: text,
|
|
127
|
-
usage: {
|
|
128
|
-
input_tokens: in_tokens,
|
|
129
|
-
output_tokens: out_tokens
|
|
130
|
-
},
|
|
131
|
-
raw: res_json,
|
|
132
|
-
tool_calls: parsed_tool_calls,
|
|
133
|
-
thought: thought
|
|
134
|
-
)
|
|
73
|
+
build_response(res_json, messages)
|
|
135
74
|
end
|
|
136
75
|
|
|
137
76
|
# Streaming variant. Yields Chunks. Without a block, returns an
|
|
138
77
|
# Enumerator (via `to_enum`) so the caller can chain `.each`.
|
|
139
78
|
def stream_chat(messages, opts = {}, &block)
|
|
140
|
-
resolved_key = opts[:api_key] || @api_key || (ENV.fetch(@api_key_env, nil) if @api_key_env)
|
|
141
79
|
target_model = opts[:model] || @model
|
|
142
80
|
target_endpoint = self.class.normalize_endpoint(opts[:endpoint] || @endpoint)
|
|
143
81
|
messages = Rubik::LLM::MessageSanitizer.normalize(messages)
|
|
144
82
|
|
|
145
|
-
req_headers =
|
|
146
|
-
req_headers["Authorization"] = "Bearer #{resolved_key}" if resolved_key && !resolved_key.empty?
|
|
147
|
-
@headers.each { |k, v| req_headers[k.to_s] = v.to_s }
|
|
148
|
-
req_headers.merge!(opts[:headers]) if opts[:headers].is_a?(Hash)
|
|
83
|
+
req_headers = build_headers(opts)
|
|
149
84
|
|
|
150
85
|
formatted_msgs = format_messages(messages)
|
|
151
86
|
payload = build_payload(target_model, formatted_msgs, opts, stream: true)
|
|
@@ -172,6 +107,86 @@ module Rubik
|
|
|
172
107
|
|
|
173
108
|
private
|
|
174
109
|
|
|
110
|
+
def build_headers(opts = {})
|
|
111
|
+
resolved_key = opts[:api_key] || @api_key || (ENV.fetch(@api_key_env, nil) if @api_key_env)
|
|
112
|
+
req_headers = { "Content-Type" => "application/json" }
|
|
113
|
+
req_headers["Authorization"] = "Bearer #{resolved_key}" if resolved_key && !resolved_key.empty?
|
|
114
|
+
|
|
115
|
+
# Merge custom headers (e.g. OpenRouter HTTP-Referer, X-Title)
|
|
116
|
+
@headers.each do |k, v|
|
|
117
|
+
req_headers[k.to_s] = v.to_s
|
|
118
|
+
end
|
|
119
|
+
req_headers.merge!(opts[:headers]) if opts[:headers].is_a?(Hash)
|
|
120
|
+
req_headers
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_response(res_json, messages)
|
|
124
|
+
root_json = res_json["data"].is_a?(Hash) && res_json["data"]["choices"] ? res_json["data"] : res_json
|
|
125
|
+
msg = root_json.dig("choices", 0, "message") || {}
|
|
126
|
+
text = msg["content"] || ""
|
|
127
|
+
thought = msg["reasoning_content"] || msg["reasoning"] || msg["thought"]
|
|
128
|
+
thought = nil if thought.to_s.empty?
|
|
129
|
+
usage = root_json["usage"] || res_json["usage"] || {}
|
|
130
|
+
|
|
131
|
+
parsed_tool_calls = parse_tool_calls(msg["tool_calls"] || [])
|
|
132
|
+
in_tokens, out_tokens = calculate_usage(usage, messages, text)
|
|
133
|
+
|
|
134
|
+
Response.new(
|
|
135
|
+
text: text,
|
|
136
|
+
usage: {
|
|
137
|
+
input_tokens: in_tokens,
|
|
138
|
+
output_tokens: out_tokens
|
|
139
|
+
},
|
|
140
|
+
raw: res_json,
|
|
141
|
+
tool_calls: parsed_tool_calls,
|
|
142
|
+
thought: thought
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def parse_tool_calls(raw_tool_calls)
|
|
147
|
+
raw_tool_calls.map do |tc|
|
|
148
|
+
fn = tc["function"] || {}
|
|
149
|
+
args, malformed = parse_tool_args(fn["arguments"])
|
|
150
|
+
row = {
|
|
151
|
+
"id" => tc["id"] || "call_#{rand(1000..9999)}",
|
|
152
|
+
"tool" => fn["name"],
|
|
153
|
+
"args" => args || {}
|
|
154
|
+
}
|
|
155
|
+
row["malformed_args"] = malformed if malformed
|
|
156
|
+
row
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def parse_tool_args(args)
|
|
161
|
+
malformed = nil
|
|
162
|
+
if args.is_a?(String)
|
|
163
|
+
begin
|
|
164
|
+
parsed = JSON.parse(args)
|
|
165
|
+
if parsed.is_a?(Hash)
|
|
166
|
+
args = parsed
|
|
167
|
+
else
|
|
168
|
+
malformed = args
|
|
169
|
+
args = {}
|
|
170
|
+
end
|
|
171
|
+
rescue JSON::ParserError
|
|
172
|
+
# Issue #55 3.4: the degrade carries the rejected payload so
|
|
173
|
+
# the executor can echo it back to the model in the error row.
|
|
174
|
+
malformed = args
|
|
175
|
+
args = {}
|
|
176
|
+
end
|
|
177
|
+
elsif !args.is_a?(Hash) && !args.nil?
|
|
178
|
+
malformed = args.to_s
|
|
179
|
+
args = {}
|
|
180
|
+
end
|
|
181
|
+
[args, malformed]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def calculate_usage(usage, messages, text)
|
|
185
|
+
in_tokens = usage["prompt_tokens"] || usage["input_tokens"] || estimate_tokens(messages.map { |m| m[:content] || m["content"] }.join)
|
|
186
|
+
out_tokens = usage["completion_tokens"] || usage["output_tokens"] || estimate_tokens(text)
|
|
187
|
+
[in_tokens, out_tokens]
|
|
188
|
+
end
|
|
189
|
+
|
|
175
190
|
def reasoning_model?(model_name, opts)
|
|
176
191
|
return true if opts.key?(:reasoning_effort)
|
|
177
192
|
return true if model_name.to_s.match?(/\A(o[134](-[a-z0-9]+)?|o1|o3|o4)\b/i)
|
data/lib/rubik/llm/registry.rb
CHANGED
|
@@ -98,87 +98,15 @@ module Rubik
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def register_provider_spec(p_name, p_spec)
|
|
101
|
-
type = p_spec["type"].to_s.downcase
|
|
102
101
|
endpoint = p_spec["endpoint"] || p_spec["host"] || p_spec["url"]
|
|
103
|
-
api_key_env = p_spec["api_key_env"]
|
|
104
|
-
api_key = p_spec["api_key"]
|
|
105
102
|
global_headers = extract_global_headers(@config)
|
|
106
103
|
spec_headers = p_spec["headers"] || {}
|
|
107
104
|
headers = global_headers.merge(spec_headers)
|
|
108
105
|
models = p_spec["models"] || []
|
|
109
106
|
default_model = p_spec["default_model"] || (models.is_a?(Array) && models.first.is_a?(Hash) ? models.first["name"] : models.first)
|
|
110
107
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
OpenAICompatibleProvider.new(
|
|
114
|
-
name: p_name,
|
|
115
|
-
endpoint: endpoint || "https://api.openai.com/v1/chat/completions",
|
|
116
|
-
api_key: api_key,
|
|
117
|
-
api_key_env: api_key_env,
|
|
118
|
-
headers: headers,
|
|
119
|
-
model: default_model || "gpt-4o",
|
|
120
|
-
default_context_limit: spec_limit(p_spec, :context),
|
|
121
|
-
default_max_output_tokens: spec_limit(p_spec, :output),
|
|
122
|
-
tool_native: p_spec.fetch("tool_native", true)
|
|
123
|
-
)
|
|
124
|
-
when "ollama"
|
|
125
|
-
OllamaProvider.new(
|
|
126
|
-
name: p_name,
|
|
127
|
-
endpoint: endpoint || "http://localhost:11434",
|
|
128
|
-
model: default_model || "qwen2.5-coder:7b",
|
|
129
|
-
num_ctx: p_spec["num_ctx"],
|
|
130
|
-
headers: headers
|
|
131
|
-
)
|
|
132
|
-
when "anthropic"
|
|
133
|
-
AnthropicProvider.new(
|
|
134
|
-
name: p_name,
|
|
135
|
-
model: default_model || "claude-sonnet-5-5",
|
|
136
|
-
# Honor the same api_key / api_key_env that
|
|
137
|
-
# openai-compatible specs already forward;
|
|
138
|
-
# otherwise Anthropic auth fails silently.
|
|
139
|
-
api_key: api_key,
|
|
140
|
-
api_key_env: api_key_env,
|
|
141
|
-
headers: headers
|
|
142
|
-
)
|
|
143
|
-
else
|
|
144
|
-
# Default fallback to openai_compatible
|
|
145
|
-
OpenAICompatibleProvider.new(
|
|
146
|
-
name: p_name,
|
|
147
|
-
endpoint: endpoint || "http://localhost:8000/v1/chat/completions",
|
|
148
|
-
api_key: api_key,
|
|
149
|
-
api_key_env: api_key_env,
|
|
150
|
-
headers: headers,
|
|
151
|
-
model: default_model || "default",
|
|
152
|
-
default_context_limit: spec_limit(p_spec, :context),
|
|
153
|
-
default_max_output_tokens: spec_limit(p_spec, :output),
|
|
154
|
-
tool_native: p_spec.fetch("tool_native", true)
|
|
155
|
-
)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
@providers[p_name] = provider_instance
|
|
159
|
-
|
|
160
|
-
# Register models in catalog
|
|
161
|
-
return unless models.is_a?(Array)
|
|
162
|
-
|
|
163
|
-
models.each do |m_entry|
|
|
164
|
-
if m_entry.is_a?(Hash)
|
|
165
|
-
m_name = m_entry["name"]
|
|
166
|
-
c_limit = m_entry["context_limit"] || m_entry["context_window"] || 32_768
|
|
167
|
-
@models_catalog["#{p_name}/#{m_name}"] = {
|
|
168
|
-
provider: p_name,
|
|
169
|
-
name: m_name,
|
|
170
|
-
context_limit: c_limit.to_i,
|
|
171
|
-
endpoint: endpoint
|
|
172
|
-
}
|
|
173
|
-
elsif m_entry.is_a?(String)
|
|
174
|
-
@models_catalog["#{p_name}/#{m_entry}"] = {
|
|
175
|
-
provider: p_name,
|
|
176
|
-
name: m_entry,
|
|
177
|
-
context_limit: 32_768,
|
|
178
|
-
endpoint: endpoint
|
|
179
|
-
}
|
|
180
|
-
end
|
|
181
|
-
end
|
|
108
|
+
@providers[p_name] = build_provider_instance(p_name, p_spec, headers, default_model, endpoint)
|
|
109
|
+
register_models_catalog(p_name, models, endpoint)
|
|
182
110
|
end
|
|
183
111
|
|
|
184
112
|
def register(provider_name, provider_instance)
|
|
@@ -331,6 +259,85 @@ module Rubik
|
|
|
331
259
|
end
|
|
332
260
|
result
|
|
333
261
|
end
|
|
262
|
+
|
|
263
|
+
private
|
|
264
|
+
|
|
265
|
+
def build_provider_instance(p_name, p_spec, headers, default_model, endpoint)
|
|
266
|
+
type = p_spec["type"].to_s.downcase
|
|
267
|
+
api_key_env = p_spec["api_key_env"]
|
|
268
|
+
api_key = p_spec["api_key"]
|
|
269
|
+
|
|
270
|
+
case type
|
|
271
|
+
when "openai_compatible", "openai"
|
|
272
|
+
OpenAICompatibleProvider.new(
|
|
273
|
+
name: p_name,
|
|
274
|
+
endpoint: endpoint || "https://api.openai.com/v1/chat/completions",
|
|
275
|
+
api_key: api_key,
|
|
276
|
+
api_key_env: api_key_env,
|
|
277
|
+
headers: headers,
|
|
278
|
+
model: default_model || "gpt-4o",
|
|
279
|
+
default_context_limit: spec_limit(p_spec, :context),
|
|
280
|
+
default_max_output_tokens: spec_limit(p_spec, :output),
|
|
281
|
+
tool_native: p_spec.fetch("tool_native", true)
|
|
282
|
+
)
|
|
283
|
+
when "ollama"
|
|
284
|
+
OllamaProvider.new(
|
|
285
|
+
name: p_name,
|
|
286
|
+
endpoint: endpoint || "http://localhost:11434",
|
|
287
|
+
model: default_model || "qwen2.5-coder:7b",
|
|
288
|
+
num_ctx: p_spec["num_ctx"],
|
|
289
|
+
headers: headers
|
|
290
|
+
)
|
|
291
|
+
when "anthropic"
|
|
292
|
+
AnthropicProvider.new(
|
|
293
|
+
name: p_name,
|
|
294
|
+
model: default_model || "claude-sonnet-5-5",
|
|
295
|
+
# Honor the same api_key / api_key_env that
|
|
296
|
+
# openai-compatible specs already forward;
|
|
297
|
+
# otherwise Anthropic auth fails silently.
|
|
298
|
+
api_key: api_key,
|
|
299
|
+
api_key_env: api_key_env,
|
|
300
|
+
headers: headers
|
|
301
|
+
)
|
|
302
|
+
else
|
|
303
|
+
# Default fallback to openai_compatible
|
|
304
|
+
OpenAICompatibleProvider.new(
|
|
305
|
+
name: p_name,
|
|
306
|
+
endpoint: endpoint || "http://localhost:8000/v1/chat/completions",
|
|
307
|
+
api_key: api_key,
|
|
308
|
+
api_key_env: api_key_env,
|
|
309
|
+
headers: headers,
|
|
310
|
+
model: default_model || "default",
|
|
311
|
+
default_context_limit: spec_limit(p_spec, :context),
|
|
312
|
+
default_max_output_tokens: spec_limit(p_spec, :output),
|
|
313
|
+
tool_native: p_spec.fetch("tool_native", true)
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def register_models_catalog(p_name, models, endpoint)
|
|
319
|
+
return unless models.is_a?(Array)
|
|
320
|
+
|
|
321
|
+
models.each do |m_entry|
|
|
322
|
+
if m_entry.is_a?(Hash)
|
|
323
|
+
m_name = m_entry["name"]
|
|
324
|
+
c_limit = m_entry["context_limit"] || m_entry["context_window"] || 32_768
|
|
325
|
+
@models_catalog["#{p_name}/#{m_name}"] = {
|
|
326
|
+
provider: p_name,
|
|
327
|
+
name: m_name,
|
|
328
|
+
context_limit: c_limit.to_i,
|
|
329
|
+
endpoint: endpoint
|
|
330
|
+
}
|
|
331
|
+
elsif m_entry.is_a?(String)
|
|
332
|
+
@models_catalog["#{p_name}/#{m_entry}"] = {
|
|
333
|
+
provider: p_name,
|
|
334
|
+
name: m_entry,
|
|
335
|
+
context_limit: 32_768,
|
|
336
|
+
endpoint: endpoint
|
|
337
|
+
}
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
334
341
|
end
|
|
335
342
|
end
|
|
336
343
|
end
|