spectre_ai 2.1.0 → 2.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a0cd0a25dd4345c62af319d8669a3702ce2e505299c01fbf43e4a94cb3b3109
4
- data.tar.gz: 544e5bb1462d2d9477601d6a581d455a30e5b0a5f107ec1264608c0f9b713307
3
+ metadata.gz: 83d3a297e011e019679dcb12edb0c00f3bb73c6dc599378923627ef257ff6f1f
4
+ data.tar.gz: 79def4a06049ed718bf0c66ffc7f0a29016d8a0831ab0941cc9c02f0b451ab03
5
5
  SHA512:
6
- metadata.gz: cdb1062a23c35d4df2ca354eeff42ebd28b5d2ddef3a710ee032917a5a1bad811645f61dc8d5e5914b2fc16cd319768a3bd6ec84f3e9bc4ed50c22af1a073855
7
- data.tar.gz: ecca27c2bd3f7ada23f744dd93017b488fbf193652b1bbdf5c3f47110bfd7ae7c042a66398d53592bc3c59d79031c0f46987b12551ebc1b4f1923f215efa55ce
6
+ metadata.gz: 18cea546ff80840b1cb6087b35305a6efdf17fd8c6b6bea565157336958b02105822bb5725d7bb8e23a170362105506143992feb52ba6834986ced9fe87ab21d
7
+ data.tar.gz: 2397f388fbab927a41b959c92364f2cbbc50e8d6cebe1ed4a0ea33b631fa0d2e635687d349f523d5f20d88f414d176f9b297163337ed3632232cb520f04040f9
data/CHANGELOG.md CHANGED
@@ -308,8 +308,94 @@ Key Benefits:\
308
308
  - Added RSpec tests for `Spectre::Openrouter::Completions` and `Spectre::Openrouter::Embeddings` covering:
309
309
  - Success responses, error propagation, JSON parse errors.
310
310
  - Finish reasons and refusal handling.
311
+
312
+ # Changelog for Version 2.1.1
313
+
314
+ **Release Date:** [15th Dec 2025]
315
+
316
+ ### Enhancements: Extra generation options for Completions
317
+
318
+ - You can now pass additional generation options (e.g., `temperature`, `top_p`, `presence_penalty`) directly as keyword arguments to all `Completions.create` methods.
319
+ - For OpenAI, OpenRouter, Gemini, and Claude these extra kwargs are forwarded into the request body automatically.
320
+ - For Ollama, pass extra kwargs at the top level just like other providers. Spectre maps them into `body[:options]` internally (including `max_tokens`). The legacy `ollama: { options: ... }` is now ignored.
321
+
322
+ ### Notes and exclusions
323
+
324
+ - Control/network keys are not forwarded: `read_timeout`, `open_timeout`.
325
+ - `max_tokens` remains supported:
326
+ - OpenAI/OpenRouter/Gemini/Claude: stays a top‑level request body field.
327
+ - Ollama: forwarded into `:options` along with other generation kwargs.
328
+ - Claude: `tool_choice` is NOT auto‑forwarded from extra kwargs; pass it explicitly via the dedicated parameter if needed.
329
+
330
+ ### Examples
331
+
332
+ ```ruby
333
+ # OpenAI
334
+ Spectre::Openai::Completions.create(
335
+ messages: [ { role: 'user', content: 'Hi' } ],
336
+ model: 'gpt-4o-mini',
337
+ temperature: 0.1,
338
+ top_p: 0.9,
339
+ max_tokens: 512
340
+ )
341
+
342
+ # OpenRouter
343
+ Spectre::Openrouter::Completions.create(
344
+ messages: [ { role: 'user', content: 'Hi' } ],
345
+ model: 'openai/gpt-4o-mini',
346
+ temperature: 0.1,
347
+ presence_penalty: 0.2,
348
+ max_tokens: 256
349
+ )
350
+
351
+ # Gemini (OpenAI‑compatible endpoint)
352
+ Spectre::Gemini::Completions.create(
353
+ messages: [ { role: 'user', content: 'Hi' } ],
354
+ model: 'gemini-2.5-flash',
355
+ temperature: 0.1,
356
+ max_tokens: 256
357
+ )
358
+
359
+ # Claude
360
+ Spectre::Claude::Completions.create(
361
+ messages: [ { role: 'user', content: 'Hi' } ],
362
+ model: 'claude-opus-4-1',
363
+ temperature: 0.1,
364
+ max_tokens: 512,
365
+ tool_choice: { type: 'auto' } # pass explicitly when needed
366
+ )
367
+
368
+ # Ollama — pass options at top level; Spectre maps them to body[:options]
369
+ Spectre::Ollama::Completions.create(
370
+ messages: [ { role: 'user', content: 'Hi' } ],
371
+ model: 'llama3.1:8b',
372
+ temperature: 0.1,
373
+ max_tokens: 256, # forwarded into body[:options]
374
+ path: 'api/chat' # optional: override endpoint path
375
+ )
376
+ ## Note: `ollama: { options: ... }` is ignored; use top-level kwargs instead.
377
+ ```
311
378
  - Request body formation (max_tokens, tools, response_format.json_schema).
312
379
 
380
+ ### OpenRouter: Plugins support in chat completions
381
+
382
+ - Added pass-through support for OpenRouter Plugins in chat completions.
383
+ - You can pass the `plugins` array directly to `Spectre::Openrouter::Completions.create`, and it will be included in the request body.
384
+
385
+ Example:
386
+
387
+ ```ruby
388
+ Spectre::Openrouter::Completions.create(
389
+ messages: [ { role: 'user', content: 'Heal my response if needed' } ],
390
+ model: 'openai/gpt-4o-mini',
391
+ plugins: [ { id: 'response-healing' } ],
392
+ temperature: 0.2,
393
+ max_tokens: 256
394
+ )
395
+ ```
396
+
397
+ Docs: https://openrouter.ai/docs/guides/features/plugins/overview
398
+
313
399
  ### Breaking Changes
314
400
 
315
401
  - Unified `max_tokens` option across providers:
@@ -336,3 +422,4 @@ Key Benefits:\
336
422
  ```ruby
337
423
  Spectre::Openrouter::Embeddings.create('some text', model: 'text-embedding-3-small')
338
424
  ```
425
+
data/README.md CHANGED
@@ -224,6 +224,78 @@ Spectre.provider_module::Completions.create(
224
224
 
225
225
  ```
226
226
 
227
+ #### Passing extra generation options (temperature, top_p, etc.)
228
+
229
+ You can pass common generation options directly as keyword arguments to `Completions.create`.
230
+
231
+ - OpenAI/OpenRouter/Gemini/Claude: extra kwargs are forwarded into the request body (e.g., `temperature`, `top_p`, `presence_penalty`).
232
+ - Ollama: pass extra kwargs the same way (top-level). Spectre will put them into `body[:options]` internally (including `max_tokens`). The `ollama: { options: ... }` hash is no longer used.
233
+ - Excluded control keys: `read_timeout`, `open_timeout` are never forwarded.
234
+ - Provider differences for `max_tokens`:
235
+ - OpenAI/OpenRouter/Gemini/Claude: `max_tokens` is a top‑level field in the request body.
236
+ - Ollama: `max_tokens` is forwarded into `body[:options]`.
237
+ - Claude: `tool_choice` is not auto‑forwarded; provide it explicitly via the `tool_choice:` parameter when needed.
238
+
239
+ Examples:
240
+
241
+ ```ruby
242
+ # OpenAI
243
+ Spectre::Openai::Completions.create(
244
+ messages: [ { role: 'user', content: 'Hi' } ],
245
+ model: 'gpt-4o-mini',
246
+ temperature: 0.1,
247
+ top_p: 0.9,
248
+ max_tokens: 512
249
+ )
250
+
251
+ # OpenRouter
252
+ Spectre::Openrouter::Completions.create(
253
+ messages: [ { role: 'user', content: 'Hi' } ],
254
+ model: 'openai/gpt-4o-mini',
255
+ temperature: 0.1,
256
+ presence_penalty: 0.2,
257
+ max_tokens: 256
258
+ )
259
+
260
+ # OpenRouter with Plugins
261
+ # Docs: https://openrouter.ai/docs/guides/features/plugins/overview
262
+ Spectre::Openrouter::Completions.create(
263
+ messages: [ { role: 'user', content: 'Heal my response if needed' } ],
264
+ model: 'openai/gpt-4o-mini',
265
+ plugins: [ { id: 'response-healing' } ],
266
+ temperature: 0.2,
267
+ max_tokens: 256
268
+ )
269
+
270
+ # Gemini (OpenAI‑compatible endpoint)
271
+ Spectre::Gemini::Completions.create(
272
+ messages: [ { role: 'user', content: 'Hi' } ],
273
+ model: 'gemini-2.5-flash',
274
+ temperature: 0.1,
275
+ max_tokens: 256
276
+ )
277
+
278
+ # Claude
279
+ Spectre::Claude::Completions.create(
280
+ messages: [ { role: 'user', content: 'Hi' } ],
281
+ model: 'claude-opus-4-1',
282
+ temperature: 0.1,
283
+ max_tokens: 512,
284
+ tool_choice: { type: 'auto' } # pass explicitly when needed
285
+ )
286
+
287
+ # Ollama — pass options at top level (Spectre maps them to body[:options])
288
+ Spectre::Ollama::Completions.create(
289
+ messages: [ { role: 'user', content: 'Hi' } ],
290
+ model: 'llama3.1:8b',
291
+ temperature: 0.1, # forwarded into body[:options]
292
+ max_tokens: 256, # forwarded into body[:options]
293
+ path: 'api/chat' # optional: override endpoint path
294
+ )
295
+
296
+ # Note: `ollama: { options: ... }` is ignored; use top-level kwargs instead.
297
+ ```
298
+
227
299
  **Using a JSON Schema for Structured Output**
228
300
 
229
301
  For cases where you need structured output (e.g., for returning specific fields or formatted responses), you can pass a `json_schema` parameter. The schema ensures that the completion conforms to a predefined structure:
@@ -22,6 +22,7 @@ module Spectre
22
22
  # @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
23
23
  # @param tool_choice [Hash, nil] Optional tool_choice to force a specific tool use (e.g., { type: 'tool', name: 'record_summary' })
24
24
  # @param args [Hash, nil] optional arguments like read_timeout and open_timeout. Provide max_tokens at the top level only.
25
+ # Any additional kwargs (e.g., temperature:, top_p:) will be forwarded into the request body.
25
26
  # @return [Hash] The parsed response including any tool calls or content
26
27
  # @raise [APIKeyNotConfiguredError] If the API key is not set
27
28
  # @raise [RuntimeError] For general API errors or unexpected issues
@@ -44,7 +45,9 @@ module Spectre
44
45
  })
45
46
 
46
47
  max_tokens = args[:max_tokens] || 1024
47
- request.body = generate_body(messages, model, json_schema, max_tokens, tools, tool_choice).to_json
48
+ # Forward extra args (like temperature) into the body, excluding control/network keys
49
+ forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :max_tokens, :tool_choice].include?(k) }
50
+ request.body = generate_body(messages, model, json_schema, max_tokens, tools, tool_choice, forwarded).to_json
48
51
  response = http.request(request)
49
52
 
50
53
  unless response.is_a?(Net::HTTPSuccess)
@@ -83,7 +86,7 @@ module Spectre
83
86
  # @param max_tokens [Integer] The maximum number of tokens for the completion
84
87
  # @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
85
88
  # @return [Hash] The body for the API request
86
- def self.generate_body(messages, model, json_schema, max_tokens, tools, tool_choice)
89
+ def self.generate_body(messages, model, json_schema, max_tokens, tools, tool_choice, forwarded)
87
90
  system_prompts, chat_messages = partition_system_and_chat(messages)
88
91
 
89
92
  body = {
@@ -125,6 +128,11 @@ module Spectre
125
128
  body[:tools] = tools if tools && !body.key?(:tools)
126
129
  body[:tool_choice] = tool_choice if tool_choice
127
130
 
131
+ # Merge any extra forwarded options (e.g., temperature, top_p)
132
+ if forwarded && !forwarded.empty?
133
+ body.merge!(forwarded.transform_keys(&:to_sym))
134
+ end
135
+
128
136
  body
129
137
  end
130
138
 
@@ -19,6 +19,7 @@ module Spectre
19
19
  # @param json_schema [Hash, nil] An optional JSON schema to enforce structured output (OpenAI-compatible "response_format")
20
20
  # @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
21
21
  # @param args [Hash, nil] optional arguments like read_timeout and open_timeout. Provide max_tokens at the top level only.
22
+ # Any additional kwargs (e.g., temperature:, top_p:) will be forwarded into the request body.
22
23
  # @return [Hash] The parsed response including any function calls or content
23
24
  # @raise [APIKeyNotConfiguredError] If the API key is not set
24
25
  # @raise [RuntimeError] For general API errors or unexpected issues
@@ -40,7 +41,9 @@ module Spectre
40
41
  })
41
42
 
42
43
  max_tokens = args[:max_tokens]
43
- request.body = generate_body(messages, model, json_schema, max_tokens, tools).to_json
44
+ # Forward extra args (like temperature) into the body, excluding control/network keys
45
+ forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :max_tokens].include?(k) }
46
+ request.body = generate_body(messages, model, json_schema, max_tokens, tools, forwarded).to_json
44
47
  response = http.request(request)
45
48
 
46
49
  unless response.is_a?(Net::HTTPSuccess)
@@ -75,7 +78,7 @@ module Spectre
75
78
  end
76
79
 
77
80
  # Helper method to generate the request body (OpenAI-compatible)
78
- def self.generate_body(messages, model, json_schema, max_tokens, tools)
81
+ def self.generate_body(messages, model, json_schema, max_tokens, tools, forwarded)
79
82
  body = {
80
83
  model: model,
81
84
  messages: messages
@@ -85,6 +88,11 @@ module Spectre
85
88
  body[:response_format] = { type: 'json_schema', json_schema: json_schema } if json_schema
86
89
  body[:tools] = tools if tools
87
90
 
91
+ # Merge any extra forwarded options (e.g., temperature, top_p)
92
+ if forwarded && !forwarded.empty?
93
+ body.merge!(forwarded.transform_keys(&:to_sym))
94
+ end
95
+
88
96
  body
89
97
  end
90
98
 
@@ -17,9 +17,9 @@ module Spectre
17
17
  # @param model [String] The model to be used for generating completions, defaults to DEFAULT_MODEL
18
18
  # @param json_schema [Hash, nil] An optional JSON schema to enforce structured output
19
19
  # @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
20
- # @param args [Hash, nil] optional arguments like read_timeout and open_timeout. You can pass in the ollama hash to specify the path and options.
21
- # @param args.ollama.path [String, nil] The path to the Ollama API endpoint, defaults to API_PATH
22
- # @param args.ollama.options [Hash, nil] Additional model parameters listed in the documentation for the https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values such as temperature
20
+ # @param args [Hash, nil] optional arguments like read_timeout and open_timeout.
21
+ # Any additional top-level kwargs (e.g., temperature:, max_tokens:) will be forwarded into body[:options], same as other providers forward into body.
22
+ # @param path [String, nil] Top-level path override for the Ollama API endpoint, defaults to API_PATH
23
23
  # @return [Hash] The parsed response including any function calls or content
24
24
  # @raise [HostNotConfiguredError] If the API host is not set in the provider configuration.
25
25
  # @raise [APIKeyNotConfiguredError] If the API key is not set
@@ -32,7 +32,7 @@ module Spectre
32
32
 
33
33
  validate_messages!(messages)
34
34
 
35
- path = args.dig(:ollama, :path) || API_PATH
35
+ path = args[:path] || API_PATH
36
36
  uri = URI.join(api_host, path)
37
37
  http = Net::HTTP.new(uri.host, uri.port)
38
38
  http.use_ssl = true if uri.scheme == 'https'
@@ -44,7 +44,13 @@ module Spectre
44
44
  'Authorization' => "Bearer #{api_key}"
45
45
  })
46
46
 
47
- options = args.dig(:ollama, :options)
47
+ # Forward extra top-level args (like temperature, max_tokens) into body[:options],
48
+ # excluding control/network keys and the request path override.
49
+ forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :path].include?(k) }
50
+ options = nil
51
+ if forwarded && !forwarded.empty?
52
+ options = forwarded.transform_keys(&:to_sym)
53
+ end
48
54
  request.body = generate_body(messages, model, json_schema, tools, options).to_json
49
55
  response = http.request(request)
50
56
 
@@ -18,6 +18,7 @@ module Spectre
18
18
  # @param json_schema [Hash, nil] An optional JSON schema to enforce structured output
19
19
  # @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
20
20
  # @param args [Hash, nil] optional arguments like read_timeout and open_timeout. Provide max_tokens at the top level only.
21
+ # Any additional kwargs (e.g., temperature:, top_p:) will be forwarded into the request body.
21
22
  # @return [Hash] The parsed response including any function calls or content
22
23
  # @raise [APIKeyNotConfiguredError] If the API key is not set
23
24
  # @raise [RuntimeError] For general API errors or unexpected issues
@@ -39,7 +40,9 @@ module Spectre
39
40
  })
40
41
 
41
42
  max_tokens = args[:max_tokens]
42
- request.body = generate_body(messages, model, json_schema, max_tokens, tools).to_json
43
+ # Forward extra args (like temperature) into the body, excluding control/network keys
44
+ forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :max_tokens].include?(k) }
45
+ request.body = generate_body(messages, model, json_schema, max_tokens, tools, forwarded).to_json
43
46
  response = http.request(request)
44
47
 
45
48
  unless response.is_a?(Net::HTTPSuccess)
@@ -82,7 +85,7 @@ module Spectre
82
85
  # @param max_tokens [Integer, nil] The maximum number of tokens for the completion
83
86
  # @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
84
87
  # @return [Hash] The body for the API request
85
- def self.generate_body(messages, model, json_schema, max_tokens, tools)
88
+ def self.generate_body(messages, model, json_schema, max_tokens, tools, forwarded)
86
89
  body = {
87
90
  model: model,
88
91
  messages: messages
@@ -92,6 +95,11 @@ module Spectre
92
95
  body[:response_format] = { type: 'json_schema', json_schema: json_schema } if json_schema
93
96
  body[:tools] = tools if tools # Add the tools to the request body if provided
94
97
 
98
+ # Merge any extra forwarded options (e.g., temperature, top_p)
99
+ if forwarded && !forwarded.empty?
100
+ body.merge!(forwarded.transform_keys(&:to_sym))
101
+ end
102
+
95
103
  body
96
104
  end
97
105
 
@@ -18,6 +18,7 @@ module Spectre
18
18
  # @param json_schema [Hash, nil] An optional JSON schema to enforce structured output (OpenAI-compatible)
19
19
  # @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
20
20
  # @param args [Hash, nil] optional arguments like read_timeout and open_timeout. Provide max_tokens at the top level only.
21
+ # Any additional kwargs (e.g., temperature:, top_p:) will be forwarded into the request body.
21
22
  # @return [Hash] The parsed response including any tool calls or content
22
23
  # @raise [APIKeyNotConfiguredError] If the API key is not set
23
24
  # @raise [RuntimeError] For general API errors or unexpected issues
@@ -44,7 +45,9 @@ module Spectre
44
45
  request = Net::HTTP::Post.new(uri.path, headers)
45
46
 
46
47
  max_tokens = args[:max_tokens]
47
- request.body = generate_body(messages, model, json_schema, max_tokens, tools).to_json
48
+ # Forward extra args into body, excluding control/network keys
49
+ forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :max_tokens].include?(k) }
50
+ request.body = generate_body(messages, model, json_schema, max_tokens, tools, forwarded).to_json
48
51
  response = http.request(request)
49
52
 
50
53
  unless response.is_a?(Net::HTTPSuccess)
@@ -66,7 +69,7 @@ module Spectre
66
69
  raise ArgumentError, 'Messages cannot be empty.' if messages.empty?
67
70
  end
68
71
 
69
- def self.generate_body(messages, model, json_schema, max_tokens, tools)
72
+ def self.generate_body(messages, model, json_schema, max_tokens, tools, forwarded)
70
73
  body = {
71
74
  model: model,
72
75
  messages: messages
@@ -74,6 +77,9 @@ module Spectre
74
77
  body[:max_tokens] = max_tokens if max_tokens
75
78
  body[:response_format] = { type: 'json_schema', json_schema: json_schema } if json_schema
76
79
  body[:tools] = tools if tools
80
+ if forwarded && !forwarded.empty?
81
+ body.merge!(forwarded.transform_keys(&:to_sym))
82
+ end
77
83
  body
78
84
  end
79
85
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spectre # :nodoc:all
4
- VERSION = "2.1.0"
4
+ VERSION = "2.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spectre_ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ilya Klapatok
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-11-13 00:00:00.000000000 Z
12
+ date: 2025-12-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec-rails