Class: Spectre::Openrouter::Completions
- Inherits:
-
Object
- Object
- Spectre::Openrouter::Completions
- Defined in:
- lib/spectre/openrouter/completions.rb
Constant Summary collapse
- API_URL =
'https://openrouter.ai/api/v1/chat/completions'- DEFAULT_MODEL =
'openai/gpt-4o-mini'- DEFAULT_TIMEOUT =
60
Class Method Summary collapse
-
.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) ⇒ Hash
Generate a completion based on user messages and optional tools.
- .generate_body(messages, model, json_schema, max_tokens, tools, forwarded) ⇒ Object
-
.handle_response(response) ⇒ Object
Handle OpenRouter finish reasons openrouter.ai/docs/api-reference/overview#finish-reason.
- .validate_messages!(messages) ⇒ Object
Class Method Details
.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) ⇒ Hash
Generate a completion based on user messages and optional tools
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/spectre/openrouter/completions.rb', line 25 def self.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) cfg = Spectre.openrouter_configuration api_key = cfg&.api_key raise APIKeyNotConfiguredError, 'API key is not configured' unless api_key () uri = URI(API_URL) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.read_timeout = args.fetch(:read_timeout, DEFAULT_TIMEOUT) http.open_timeout = args.fetch(:open_timeout, DEFAULT_TIMEOUT) headers = { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{api_key}" } headers['HTTP-Referer'] = cfg.referer if cfg.respond_to?(:referer) && cfg.referer headers['X-Title'] = cfg.app_title if cfg.respond_to?(:app_title) && cfg.app_title request = Net::HTTP::Post.new(uri.path, headers) max_tokens = args[:max_tokens] # Forward extra args into body, excluding control/network keys forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :max_tokens].include?(k) } request.body = generate_body(, model, json_schema, max_tokens, tools, forwarded).to_json response = http.request(request) unless response.is_a?(Net::HTTPSuccess) raise "OpenRouter API Error: #{response.code} - #{response.message}: #{response.body}" end parsed_response = JSON.parse(response.body) handle_response(parsed_response) rescue JSON::ParserError => e raise "JSON Parse Error: #{e.message}" end |
.generate_body(messages, model, json_schema, max_tokens, tools, forwarded) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/spectre/openrouter/completions.rb', line 72 def self.generate_body(, model, json_schema, max_tokens, tools, forwarded) body = { model: model, messages: } body[:max_tokens] = max_tokens if max_tokens body[:response_format] = { type: 'json_schema', json_schema: json_schema } if json_schema body[:tools] = tools if tools if forwarded && !forwarded.empty? body.merge!(forwarded.transform_keys(&:to_sym)) end body end |
.handle_response(response) ⇒ Object
Handle OpenRouter finish reasons openrouter.ai/docs/api-reference/overview#finish-reason
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/spectre/openrouter/completions.rb', line 88 def self.handle_response(response) = response.dig('choices', 0, 'message') || {} finish_reason = response.dig('choices', 0, 'finish_reason') if ['refusal'] raise "Refusal: #{message['refusal']}" end case finish_reason when 'stop' return { content: ['content'] } when 'tool_calls', 'function_call' return { tool_calls: ['tool_calls'], content: ['content'] } when 'length', 'model_length' raise 'Incomplete response: The completion was cut off due to token limit.' when 'content_filter' raise "Content filtered: The model's output was blocked due to policy violations." when 'error' raise "Model returned finish_reason=error: #{response.inspect}" else raise "Unexpected finish_reason: #{finish_reason}" end end |
.validate_messages!(messages) ⇒ Object
65 66 67 68 69 70 |
# File 'lib/spectre/openrouter/completions.rb', line 65 def self.() unless .is_a?(Array) && .all? { |msg| msg.is_a?(Hash) } raise ArgumentError, 'Messages must be an array of message hashes.' end raise ArgumentError, 'Messages cannot be empty.' if .empty? end |