pocketrb 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +98 -17
  4. data/lib/pocketrb/agent/compaction.rb +235 -22
  5. data/lib/pocketrb/agent/context.rb +26 -3
  6. data/lib/pocketrb/agent/loop.rb +53 -5
  7. data/lib/pocketrb/agent/spawn_tool.rb +14 -0
  8. data/lib/pocketrb/agent/subagent_manager.rb +24 -2
  9. data/lib/pocketrb/bus/events.rb +42 -1
  10. data/lib/pocketrb/bus/message_bus.rb +37 -0
  11. data/lib/pocketrb/channels/base.rb +13 -0
  12. data/lib/pocketrb/channels/cli.rb +16 -1
  13. data/lib/pocketrb/channels/telegram.rb +70 -4
  14. data/lib/pocketrb/channels/whatsapp.rb +49 -7
  15. data/lib/pocketrb/cli/chat.rb +7 -0
  16. data/lib/pocketrb/cli/config.rb +4 -0
  17. data/lib/pocketrb/cli/cron.rb +6 -0
  18. data/lib/pocketrb/cli.rb +0 -1
  19. data/lib/pocketrb/config.rb +55 -6
  20. data/lib/pocketrb/cron/job.rb +41 -0
  21. data/lib/pocketrb/cron/service.rb +44 -15
  22. data/lib/pocketrb/heartbeat/service.rb +21 -0
  23. data/lib/pocketrb/mcp/client.rb +41 -3
  24. data/lib/pocketrb/mcp/memory_tool.rb +33 -0
  25. data/lib/pocketrb/media/processor.rb +32 -1
  26. data/lib/pocketrb/memory.rb +12 -3
  27. data/lib/pocketrb/planning/manager.rb +29 -1
  28. data/lib/pocketrb/planning/plan.rb +52 -0
  29. data/lib/pocketrb/planning/tool.rb +45 -0
  30. data/lib/pocketrb/providers/anthropic.rb +113 -9
  31. data/lib/pocketrb/providers/base.rb +39 -4
  32. data/lib/pocketrb/providers/claude_cli.rb +79 -7
  33. data/lib/pocketrb/providers/claude_max_proxy.rb +72 -6
  34. data/lib/pocketrb/providers/openai_compatible.rb +79 -0
  35. data/lib/pocketrb/providers/openrouter.rb +72 -12
  36. data/lib/pocketrb/providers/registry.rb +24 -0
  37. data/lib/pocketrb/providers/ruby_llm_provider.rb +168 -54
  38. data/lib/pocketrb/providers/types.rb +51 -1
  39. data/lib/pocketrb/session/manager.rb +34 -2
  40. data/lib/pocketrb/session/session.rb +53 -11
  41. data/lib/pocketrb/skills/builtin/tmux/SKILL.md +9 -9
  42. data/lib/pocketrb/skills/create_tool.rb +28 -0
  43. data/lib/pocketrb/skills/loader.rb +22 -1
  44. data/lib/pocketrb/skills/modify_tool.rb +22 -0
  45. data/lib/pocketrb/skills/skill.rb +19 -3
  46. data/lib/pocketrb/tools/background_job_manager.rb +29 -0
  47. data/lib/pocketrb/tools/base.rb +16 -1
  48. data/lib/pocketrb/tools/browser.rb +40 -0
  49. data/lib/pocketrb/tools/browser_advanced.rb +94 -4
  50. data/lib/pocketrb/tools/browser_session.rb +18 -0
  51. data/lib/pocketrb/tools/cron.rb +46 -0
  52. data/lib/pocketrb/tools/edit_file.rb +16 -0
  53. data/lib/pocketrb/tools/exec.rb +46 -0
  54. data/lib/pocketrb/tools/jobs.rb +26 -2
  55. data/lib/pocketrb/tools/list_dir.rb +19 -0
  56. data/lib/pocketrb/tools/memory.rb +34 -0
  57. data/lib/pocketrb/tools/message.rb +15 -0
  58. data/lib/pocketrb/tools/para_memory.rb +47 -0
  59. data/lib/pocketrb/tools/read_file.rb +11 -0
  60. data/lib/pocketrb/tools/registry.rb +12 -4
  61. data/lib/pocketrb/tools/send_file.rb +29 -0
  62. data/lib/pocketrb/tools/think.rb +9 -0
  63. data/lib/pocketrb/tools/web_fetch.rb +58 -3
  64. data/lib/pocketrb/tools/web_search.rb +25 -0
  65. data/lib/pocketrb/tools/write_file.rb +10 -0
  66. data/lib/pocketrb/version.rb +2 -1
  67. data/lib/pocketrb.rb +4 -1
  68. data/pocketrb.gemspec +6 -8
  69. metadata +24 -44
@@ -6,38 +6,56 @@ require "uri"
6
6
 
7
7
  module Pocketrb
8
8
  module Providers
9
- # Claude Max API Proxy provider
10
- # Uses the claude-max-api-proxy which provides OpenAI-compatible API
11
- # Install: npm install -g claude-max-api-proxy
12
- # Start: claude-max-api (runs on localhost:3456)
9
+ # Claude Max API Proxy provider. Uses the claude-max-api-proxy which provides OpenAI-compatible API
10
+ # - Install: npm install -g claude-max-api-proxy
11
+ # - Start: claude-max-api (runs on localhost:3456)
13
12
  class ClaudeMaxProxy < Base
13
+ # Available model aliases mapped to full model names
14
14
  MODELS = {
15
15
  "opus" => "claude-opus-4",
16
16
  "sonnet" => "claude-sonnet-4",
17
17
  "haiku" => "claude-haiku-4"
18
18
  }.freeze
19
19
 
20
+ # Default model alias
20
21
  DEFAULT_MODEL = "sonnet"
22
+ # Default base URL for claude-max-api proxy
21
23
  DEFAULT_BASE_URL = "http://localhost:3456/v1"
22
24
 
25
+ # Initialize Claude Max Proxy provider
26
+ # @param config [Hash] Configuration options (base_url, etc.)
23
27
  def initialize(config = {})
24
28
  @config = config
25
29
  @base_url = config[:base_url] || ENV["CLAUDE_MAX_PROXY_URL"] || DEFAULT_BASE_URL
26
30
  validate_config!
27
31
  end
28
32
 
33
+ # Provider name
34
+ # @return [Symbol] Provider identifier
29
35
  def name
30
36
  :claude_max_proxy
31
37
  end
32
38
 
39
+ # Default model for this provider
40
+ # @return [String] Default model alias
33
41
  def default_model
34
42
  DEFAULT_MODEL
35
43
  end
36
44
 
45
+ # List of available model aliases
46
+ # @return [Array<String>] Model alias names
37
47
  def available_models
38
48
  MODELS.keys
39
49
  end
40
50
 
51
+ # Send a chat request
52
+ # @param messages [Array<Message>] Conversation history
53
+ # @param tools [Array<Hash>, nil] Available tools for the model to use
54
+ # @param model [String, nil] Model name (defaults to DEFAULT_MODEL)
55
+ # @param temperature [Float] Temperature for response randomness (0.0-1.0)
56
+ # @param max_tokens [Integer] Maximum tokens in response
57
+ # @param thinking [Boolean] Enable extended thinking mode (not used for this provider)
58
+ # @return [LLMResponse] Model response
41
59
  def chat(messages:, tools: nil, model: nil, temperature: 0.7, max_tokens: 4096, thinking: false)
42
60
  model ||= default_model
43
61
  model_id = MODELS[model] || model
@@ -55,6 +73,14 @@ module Pocketrb
55
73
  parse_response(response, model_id)
56
74
  end
57
75
 
76
+ # Send a streaming chat request
77
+ # @param messages [Array<Message>] Conversation history
78
+ # @param tools [Array<Hash>, nil] Available tools for the model to use
79
+ # @param model [String, nil] Model name (defaults to DEFAULT_MODEL)
80
+ # @param temperature [Float] Temperature for response randomness (0.0-1.0)
81
+ # @param max_tokens [Integer] Maximum tokens in response
82
+ # @yieldparam chunk [String] Text chunk from streaming response
83
+ # @return [LLMResponse] Final complete response
58
84
  def chat_stream(messages:, tools: nil, model: nil, temperature: 0.7, max_tokens: 4096, &)
59
85
  model ||= default_model
60
86
  model_id = MODELS[model] || model
@@ -72,6 +98,7 @@ module Pocketrb
72
98
  end
73
99
 
74
100
  # Check if proxy is running
101
+ # @return [Boolean] true if the proxy health endpoint responds
75
102
  def available?
76
103
  uri = URI.parse("#{@base_url.sub("/v1", "")}/health")
77
104
  http = Net::HTTP.new(uri.host, uri.port)
@@ -85,10 +112,14 @@ module Pocketrb
85
112
 
86
113
  protected
87
114
 
115
+ # Supported provider features
116
+ # @return [Array<Symbol>]
88
117
  def supported_features
89
118
  %i[tools streaming]
90
119
  end
91
120
 
121
+ # Validate provider configuration (no API key needed for proxy)
122
+ # @return [void]
92
123
  def validate_config!
93
124
  # No API key needed - proxy uses CLI auth
94
125
  Pocketrb.logger.debug("Claude Max Proxy: #{@base_url}")
@@ -96,6 +127,14 @@ module Pocketrb
96
127
 
97
128
  private
98
129
 
130
+ # Build the OpenAI-compatible request body
131
+ # @param messages [Array<Message>] conversation messages
132
+ # @param model [String] model identifier
133
+ # @param tools [Array<Hash>, nil] tool definitions
134
+ # @param temperature [Float] sampling temperature
135
+ # @param max_tokens [Integer] maximum tokens to generate
136
+ # @param stream [Boolean] whether to enable streaming
137
+ # @return [Hash] request body
99
138
  def build_request_body(messages:, model:, tools:, temperature:, max_tokens:, stream:)
100
139
  body = {
101
140
  model: model,
@@ -113,6 +152,9 @@ module Pocketrb
113
152
  body
114
153
  end
115
154
 
155
+ # Format messages for the OpenAI-compatible proxy API
156
+ # @param messages [Array<Message>] conversation messages
157
+ # @return [Array<Hash>] formatted messages with role, content, and tool data
116
158
  def format_messages(messages)
117
159
  messages.map do |msg|
118
160
  formatted = { role: msg.role.to_s }
@@ -156,6 +198,9 @@ module Pocketrb
156
198
  end
157
199
  end
158
200
 
201
+ # Format a media object as an OpenAI image_url content block
202
+ # @param media [Media] image attachment to encode as a data URI or text fallback
203
+ # @return [Hash] image_url or text fallback content block
159
204
  def format_image_content(media)
160
205
  if media.data
161
206
  {
@@ -178,6 +223,9 @@ module Pocketrb
178
223
  end
179
224
  end
180
225
 
226
+ # Convert tool definitions to OpenAI function-calling format
227
+ # @param tools [Array<Hash>] tool definitions
228
+ # @return [Array<Hash>] OpenAI-formatted tool definitions
181
229
  def format_tools(tools)
182
230
  tools.map do |tool|
183
231
  func = tool[:function] || tool
@@ -192,6 +240,11 @@ module Pocketrb
192
240
  end
193
241
  end
194
242
 
243
+ # Make an HTTP POST request to the proxy
244
+ # @param endpoint [String] API endpoint path
245
+ # @param body [Hash] request body
246
+ # @return [Hash] parsed JSON response
247
+ # @raise [ProviderError] on non-200 response
195
248
  def make_request(endpoint, body)
196
249
  uri = URI.parse("#{@base_url}#{endpoint}")
197
250
  http = Net::HTTP.new(uri.host, uri.port)
@@ -218,6 +271,12 @@ module Pocketrb
218
271
  JSON.parse(response.body)
219
272
  end
220
273
 
274
+ # Make a streaming HTTP POST request, yielding text chunks
275
+ # @param endpoint [String] API endpoint path
276
+ # @param body [Hash] request body
277
+ # @param block [Proc] block to receive text chunks
278
+ # @return [LLMResponse] final accumulated response
279
+ # @raise [ProviderError] on non-200 response
221
280
  def stream_request(endpoint, body, &block)
222
281
  uri = URI.parse("#{@base_url}#{endpoint}")
223
282
  http = Net::HTTP.new(uri.host, uri.port)
@@ -277,7 +336,7 @@ module Pocketrb
277
336
  # Build final response
278
337
  parsed_tool_calls = tool_calls.compact.map do |tc|
279
338
  args = begin
280
- JSON.parse(tc[:arguments])
339
+ JSON.parse(tc[:arguments], allow_duplicate_key: true)
281
340
  rescue JSON::ParserError
282
341
  {}
283
342
  end
@@ -297,6 +356,10 @@ module Pocketrb
297
356
  )
298
357
  end
299
358
 
359
+ # Parse the OpenAI-compatible response into an LLMResponse
360
+ # @param response [Hash] parsed JSON response
361
+ # @param model [String] model identifier used
362
+ # @return [LLMResponse] structured response
300
363
  def parse_response(response, model)
301
364
  choice = response.dig("choices", 0)
302
365
  message = choice&.dig("message") || {}
@@ -326,12 +389,15 @@ module Pocketrb
326
389
  )
327
390
  end
328
391
 
392
+ # Parse OpenAI-format tool calls into ToolCall objects
393
+ # @param tool_calls [Array<Hash>, nil] raw tool call data
394
+ # @return [Array<ToolCall>] parsed tool calls
329
395
  def parse_tool_calls(tool_calls)
330
396
  return [] unless tool_calls
331
397
 
332
398
  tool_calls.map do |tc|
333
399
  args = begin
334
- JSON.parse(tc.dig("function", "arguments") || "{}")
400
+ JSON.parse(tc.dig("function", "arguments") || "{}", allow_duplicate_key: true)
335
401
  rescue JSON::ParserError
336
402
  {}
337
403
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Pocketrb
6
+ module Providers
7
+ # Provider for any server exposing the OpenAI chat completions API (vLLM, llama.cpp server,
8
+ # llamafile, LM Studio, Ollama, OpenAI itself)
9
+ class OpenAICompatible < OpenRouter
10
+ # Provider name
11
+ # @return [Symbol]
12
+ def name
13
+ :openai_compatible
14
+ end
15
+
16
+ # Default model, taken from the configuration
17
+ # @return [String, nil]
18
+ def default_model
19
+ @config[:model]
20
+ end
21
+
22
+ # Available models
23
+ # @return [Array<String>]
24
+ def available_models
25
+ [default_model].compact
26
+ end
27
+
28
+ protected
29
+
30
+ # Validate that an http(s) base URL is configured
31
+ # @return [void]
32
+ # @raise [ConfigurationError] if base URL is missing or invalid
33
+ def validate_config!
34
+ if base_url.to_s.empty?
35
+ raise ConfigurationError, "openai_compatible_base_url is required for #{self.class.name}"
36
+ end
37
+
38
+ return if http_url?(base_url)
39
+
40
+ raise ConfigurationError, "openai_compatible_base_url must be an http(s) URL, got #{base_url.inspect}"
41
+ end
42
+
43
+ private
44
+
45
+ # Base URL of the API, e.g. http://localhost:8000/v1
46
+ # @return [String, nil]
47
+ def base_url
48
+ @config[:openai_compatible_base_url] || ENV.fetch("OPENAI_COMPATIBLE_BASE_URL", nil)
49
+ end
50
+
51
+ # Check that a value is an http(s) URL with a host
52
+ # @param value [String] URL to check
53
+ # @return [Boolean]
54
+ def http_url?(value)
55
+ uri = URI.parse(value)
56
+ uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
57
+ rescue URI::InvalidURIError
58
+ false
59
+ end
60
+
61
+ # Provider label used in error messages
62
+ # @return [String]
63
+ def api_label
64
+ "OpenAI-compatible"
65
+ end
66
+
67
+ # Build Faraday HTTP client, sending the API key only when one is configured
68
+ # @return [Faraday::Connection]
69
+ def client
70
+ @client ||= Faraday.new(url: base_url) do |f|
71
+ f.headers["Content-Type"] = "application/json"
72
+ key = api_key(:openai_compatible_api_key)
73
+ f.headers["Authorization"] = "Bearer #{key}" if key
74
+ f.adapter Faraday.default_adapter
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -5,57 +5,81 @@ require "json"
5
5
 
6
6
  module Pocketrb
7
7
  module Providers
8
- # OpenRouter API provider for multi-model access
9
- # Supports Claude, GPT-4, Llama, and many other models
8
+ # OpenRouter API provider for multi-model access. Supports Claude, GPT-4, Llama, and many other models
10
9
  class OpenRouter < Base
10
+ # OpenRouter API base URL
11
11
  API_URL = "https://openrouter.ai/api/v1"
12
12
 
13
+ # Popular models available through OpenRouter
13
14
  POPULAR_MODELS = %w[
14
- anthropic/claude-sonnet-4
15
- anthropic/claude-3.5-haiku
15
+ anthropic/claude-sonnet-5.5
16
+ anthropic/claude-haiku-5.5
17
+ anthropic/claude-opus-5.5
16
18
  openai/gpt-4o
17
19
  openai/gpt-4o-mini
18
- google/gemini-pro-1.5
20
+ google/gemini-2.5-pro
19
21
  meta-llama/llama-3.1-70b-instruct
20
22
  mistralai/mistral-large
21
23
  ].freeze
22
24
 
25
+ # Provider name
26
+ # @return [Symbol]
23
27
  def name
24
28
  :openrouter
25
29
  end
26
30
 
31
+ # Default model
32
+ # @return [String]
27
33
  def default_model
28
- "anthropic/claude-sonnet-4"
34
+ "anthropic/claude-sonnet-5.5"
29
35
  end
30
36
 
37
+ # Available models
38
+ # @return [Array<String>]
31
39
  def available_models
32
40
  # Could fetch from API, but use popular models for now
33
41
  POPULAR_MODELS
34
42
  end
35
43
 
44
+ # Send chat completion request
45
+ # @param messages [Array<Message>] Conversation messages
46
+ # @param tools [Array<Hash>, nil] Tool definitions
47
+ # @param model [String, nil] Model name (e.g., "anthropic/claude-sonnet-5.5")
48
+ # @param temperature [Float] Sampling temperature
49
+ # @param max_tokens [Integer] Maximum tokens to generate
50
+ # @param thinking [Boolean] Enable extended thinking (model-dependent)
51
+ # @return [LLMResponse] Parsed response
36
52
  def chat(messages:, tools: nil, model: nil, temperature: 0.7, max_tokens: 4096, thinking: false)
37
53
  model ||= default_model
38
54
  body = build_request_body(messages, tools, model, temperature, max_tokens)
39
55
 
40
- response = client.post("/api/v1/chat/completions") do |req|
56
+ response = client.post("chat/completions") do |req|
41
57
  req.body = body.to_json
42
58
  end
43
59
 
44
60
  handle_response(response)
45
61
  end
46
62
 
47
- def chat_stream(messages:, tools: nil, model: nil, temperature: 0.7, max_tokens: 4096, &block)
63
+ # Send streaming chat completion request
64
+ # @param messages [Array<Message>] Conversation messages
65
+ # @param tools [Array<Hash>, nil] Tool definitions
66
+ # @param model [String, nil] Model name
67
+ # @param temperature [Float] Sampling temperature
68
+ # @param max_tokens [Integer] Maximum tokens to generate
69
+ # @yieldparam chunk [String] Streamed text chunk
70
+ # @return [LLMResponse] Final response
71
+ def chat_stream(messages:, tools: nil, model: nil, temperature: 0.7, max_tokens: 4096, &)
48
72
  model ||= default_model
49
73
  body = build_request_body(messages, tools, model, temperature, max_tokens)
50
74
  body[:stream] = true
51
75
 
52
- accumulated_content = ""
76
+ accumulated_content = +""
53
77
  accumulated_tool_calls = []
54
78
 
55
- client.post("/api/v1/chat/completions") do |req|
79
+ client.post("chat/completions") do |req|
56
80
  req.body = body.to_json
57
81
  req.options.on_data = proc do |chunk, _|
58
- process_stream_chunk(chunk, accumulated_content, accumulated_tool_calls, &block)
82
+ process_stream_chunk(chunk, accumulated_content, accumulated_tool_calls, &)
59
83
  end
60
84
  end
61
85
 
@@ -68,16 +92,29 @@ module Pocketrb
68
92
 
69
93
  protected
70
94
 
95
+ # Supported provider features
96
+ # @return [Array<Symbol>]
71
97
  def supported_features
72
98
  %i[tools streaming]
73
99
  end
74
100
 
101
+ # Validate that an OpenRouter API key is configured
102
+ # @return [void]
103
+ # @raise [ConfigurationError] if API key is missing
75
104
  def validate_config!
76
105
  require_api_key!(:openrouter_api_key)
77
106
  end
78
107
 
79
108
  private
80
109
 
110
+ # Provider label used in error messages
111
+ # @return [String]
112
+ def api_label
113
+ "OpenRouter"
114
+ end
115
+
116
+ # Build Faraday HTTP client with OpenRouter headers
117
+ # @return [Faraday::Connection]
81
118
  def client
82
119
  @client ||= Faraday.new(url: API_URL) do |f|
83
120
  f.headers["Content-Type"] = "application/json"
@@ -88,6 +125,13 @@ module Pocketrb
88
125
  end
89
126
  end
90
127
 
128
+ # Build the OpenAI-compatible request body for OpenRouter
129
+ # @param messages [Array<Message>] conversation messages
130
+ # @param tools [Array<Hash>, nil] tool definitions
131
+ # @param model [String] model identifier
132
+ # @param temperature [Float] sampling temperature
133
+ # @param max_tokens [Integer] maximum tokens to generate
134
+ # @return [Hash] request body
91
135
  def build_request_body(messages, tools, model, temperature, max_tokens)
92
136
  body = {
93
137
  model: model,
@@ -101,6 +145,9 @@ module Pocketrb
101
145
  body
102
146
  end
103
147
 
148
+ # Format a single message for the OpenAI-compatible API
149
+ # @param message [Message] conversation message with role, content, and optional tool data
150
+ # @return [Hash] formatted message with role and content
104
151
  def format_message(message)
105
152
  case message.role
106
153
  when Role::SYSTEM
@@ -130,6 +177,10 @@ module Pocketrb
130
177
  end
131
178
  end
132
179
 
180
+ # Handle HTTP response, raising on errors
181
+ # @param response [Faraday::Response] HTTP response
182
+ # @return [LLMResponse] parsed response
183
+ # @raise [ProviderError] on non-success HTTP status
133
184
  def handle_response(response)
134
185
  unless response.success?
135
186
  error_body = begin
@@ -137,13 +188,16 @@ module Pocketrb
137
188
  rescue StandardError
138
189
  { "error" => response.body }
139
190
  end
140
- raise ProviderError, "OpenRouter API error: #{error_body["error"]}"
191
+ raise ProviderError, "#{api_label} API error: #{error_body["error"]}"
141
192
  end
142
193
 
143
194
  data = JSON.parse(response.body)
144
195
  parse_response(data)
145
196
  end
146
197
 
198
+ # Parse OpenRouter API response into an LLMResponse
199
+ # @param data [Hash] parsed JSON response body
200
+ # @return [LLMResponse] structured response with content, tool calls, and usage
147
201
  def parse_response(data)
148
202
  choice = data.dig("choices", 0)
149
203
  return LLMResponse.new(content: nil) unless choice
@@ -181,6 +235,12 @@ module Pocketrb
181
235
  )
182
236
  end
183
237
 
238
+ # Process a single SSE chunk from the streaming response
239
+ # @param chunk [String] raw SSE data chunk
240
+ # @param accumulated_content [String] buffer for accumulated text content
241
+ # @param _accumulated_tool_calls [Array<ToolCall>] buffer for tool calls (unused)
242
+ # @param block [Proc] callback receiving each text delta
243
+ # @return [void]
184
244
  def process_stream_chunk(chunk, accumulated_content, _accumulated_tool_calls, &block)
185
245
  chunk.split("\n").each do |line|
186
246
  next unless line.start_with?("data: ")
@@ -5,34 +5,54 @@ module Pocketrb
5
5
  # Registry for LLM providers
6
6
  class Registry
7
7
  class << self
8
+ # Get registry singleton instance
9
+ # @return [Registry] Registry instance
8
10
  def instance
9
11
  @instance ||= new
10
12
  end
11
13
 
14
+ # Register a provider (class method)
15
+ # @param name [String, Symbol] Provider name
16
+ # @param klass [Class] Provider class
17
+ # @return [void]
12
18
  def register(name, klass)
13
19
  instance.register(name, klass)
14
20
  end
15
21
 
22
+ # Get a provider instance (class method)
23
+ # @param name [String, Symbol] Provider name
24
+ # @param config [Hash] Provider configuration (defaults to empty hash)
25
+ # @return [Base] Provider instance
16
26
  def get(name, config = {})
17
27
  instance.get(name, config)
18
28
  end
19
29
 
30
+ # Get list of available provider names (class method)
31
+ # @return [Array<Symbol>] Provider names
20
32
  def available
21
33
  instance.available
22
34
  end
23
35
  end
24
36
 
37
+ # Initialize registry with default providers
25
38
  def initialize
26
39
  @providers = {}
27
40
  register_defaults
28
41
  end
29
42
 
30
43
  # Register a provider class
44
+ # @param name [String, Symbol] Provider name
45
+ # @param klass [Class] Provider class
46
+ # @return [void]
31
47
  def register(name, klass)
32
48
  @providers[name.to_sym] = klass
33
49
  end
34
50
 
35
51
  # Get an instance of a provider
52
+ # @param name [String, Symbol] Provider name
53
+ # @param config [Hash] Provider configuration (defaults to empty hash)
54
+ # @return [Base] Provider instance
55
+ # @raise [ConfigurationError] if provider is not registered
36
56
  def get(name, config = {})
37
57
  klass = @providers[name.to_sym]
38
58
  raise ConfigurationError, "Unknown provider: #{name}" unless klass
@@ -41,15 +61,19 @@ module Pocketrb
41
61
  end
42
62
 
43
63
  # List available provider names
64
+ # @return [Array<Symbol>]
44
65
  def available
45
66
  @providers.keys
46
67
  end
47
68
 
48
69
  private
49
70
 
71
+ # Register all built-in provider classes
72
+ # @return [void]
50
73
  def register_defaults
51
74
  register(:anthropic, Anthropic)
52
75
  register(:openrouter, OpenRouter)
76
+ register(:openai_compatible, OpenAICompatible)
53
77
  register(:ruby_llm, RubyLLMProvider)
54
78
  register(:claude_cli, ClaudeCLI)
55
79
  register(:claude_max_proxy, ClaudeMaxProxy)