active_harness 0.2.9 → 0.2.10

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: 15017e6a42df89f5372596d3b75deb192117a680ad297196eda52111a8931a96
4
- data.tar.gz: 135fdfb6b453c4c2bfaef6ff8ed7f022b2c3fb1b45923f2ed282701fd106864d
3
+ metadata.gz: 79db5ea3cf1416b2d9e8d3958d9a6e69c9ab65ea36ddf52bcdd8e41ff2e07b0c
4
+ data.tar.gz: 0fde20074cb5d215226fdac581b84220ae2d5462e14cae4bfc6e1216dff84164
5
5
  SHA512:
6
- metadata.gz: 253c875d86e0d52535ba46ef206679a8cd5920e10b0cc0bcc82fad743e4a7bed1d1a3a28c8063db72400f17f057070ae0ac8cf61e14a3fe5708a766d9fa3b00f
7
- data.tar.gz: f7f5a376c825d3ee9ace72f0512cb55f835dc7bad6b4dc419517d14b42f100b78b4bff1e6bf70cc6e7a137624d0ccf91fa71286805e3408560d3eb4fff54b904
6
+ metadata.gz: f6ccb47cc9f4c176d44503323ee642e879c66961521d69b102616ca92be8eeb8c0b38ce12e72bab1dbf67756c2d984edb8fb238d26778b4755f49370290a7861
7
+ data.tar.gz: 0c0478912520ed1a466a16dd4f453996c25f20878a6d1b10399faf89a7668fbfac58f0ef00d72c39b10db442f54366d0f886f1e70eb9908cc4bbbf6035412e0f
@@ -25,6 +25,7 @@ module ActiveHarness
25
25
 
26
26
  buffer = ""
27
27
  content = ""
28
+ usage = nil
28
29
 
29
30
  http.request(req) do |response|
30
31
  response.read_body do |chunk|
@@ -42,11 +43,12 @@ module ActiveHarness
42
43
  on_token.call(token)
43
44
  content += token
44
45
  end
46
+ usage ||= parsed["usage"] if parsed.key?("usage")
45
47
  end
46
48
  end
47
49
  end
48
50
 
49
- content
51
+ { content: content, raw_usage: usage }
50
52
  rescue Net::OpenTimeout, Net::ReadTimeout
51
53
  raise Errors::TimeoutError, "Request to #{url.host} timed out"
52
54
  rescue JSON::ParserError
@@ -45,6 +45,21 @@ module ActiveHarness
45
45
  }
46
46
  end
47
47
 
48
+ # Streaming call for OpenAI-compatible providers.
49
+ # Adds stream: true and stream_options to body, calls StreamingClient,
50
+ # and returns the same { content:, provider:, model:, usage: } shape
51
+ # as non-streaming calls so callers need no special handling.
52
+ def call_streaming(url:, headers:, body:, stream:, provider:, model:)
53
+ body = body.merge(stream: true, stream_options: { include_usage: true })
54
+ result = post_json_stream(URI(url), headers: headers, body: body, on_token: stream)
55
+ {
56
+ content: result[:content],
57
+ provider: provider,
58
+ model: model,
59
+ usage: extract_usage_openai({ "usage" => result[:raw_usage] })
60
+ }
61
+ end
62
+
48
63
  def parse!(raw)
49
64
  JSON.parse(raw)
50
65
  rescue JSON::ParserError => e
@@ -5,14 +5,13 @@ module ActiveHarness
5
5
  # DeepSeek — OpenAI-compatible API.
6
6
  # https://platform.deepseek.com/api-docs
7
7
  class DeepSeek < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.deepseek_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.deepseek_api_url, headers: headers, body: body, stream: stream, provider: :deepseek, model: model) if stream
13
+
14
+ raw = post_json(URI(config.deepseek_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
@@ -5,23 +5,17 @@ module ActiveHarness
5
5
  # Google Gemini — OpenAI-compatible endpoint (beta).
6
6
  # https://ai.google.dev/gemini-api/docs/openai
7
7
  class Gemini < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.gemini_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.gemini_api_url, headers: headers, body: body, stream: stream, provider: :gemini, model: model) if stream
13
+
14
+ raw = post_json(URI(config.gemini_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
19
- {
20
- content: data.dig("choices", 0, "message", "content").to_s.strip,
21
- provider: :gemini,
22
- model: data["model"] || model,
23
- usage: extract_usage_openai(data)
24
- }
18
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :gemini, model: data["model"] || model, usage: extract_usage_openai(data) }
25
19
  end
26
20
 
27
21
  private
@@ -13,26 +13,20 @@ module ActiveHarness
13
13
  # use provider: :gpustack, model: "Qwen/Qwen2.5-7B-Instruct-GGUF"
14
14
  # end
15
15
  class GPUStack < Base
16
- def call(model:, messages:, temperature: 0.7)
17
- url = URI("#{api_base}/v1/chat/completions")
18
-
16
+ def call(model:, messages:, temperature: 0.7, stream: nil)
17
+ url = "#{api_base}/v1/chat/completions"
19
18
  headers = { "Content-Type" => "application/json" }
20
19
  key = api_key
21
20
  headers["Authorization"] = "Bearer #{key}" if key
21
+ body = { model: model, messages: messages, temperature: temperature }
22
+
23
+ return call_streaming(url: url, headers: headers, body: body, stream: stream, provider: :gpustack, model: model) if stream
22
24
 
23
- raw = post_json(url,
24
- headers: headers,
25
- body: { model: model, messages: messages, temperature: temperature }
26
- )
25
+ raw = post_json(URI(url), headers: headers, body: body)
27
26
  data = parse!(raw)
28
27
  handle_error!(data)
29
28
 
30
- {
31
- content: data.dig("choices", 0, "message", "content").to_s.strip,
32
- provider: :gpustack,
33
- model: data["model"] || model,
34
- usage: extract_usage_openai(data)
35
- }
29
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :gpustack, model: data["model"] || model, usage: extract_usage_openai(data) }
36
30
  end
37
31
 
38
32
  private
@@ -5,23 +5,17 @@ module ActiveHarness
5
5
  # Groq — OpenAI-compatible API with fast inference.
6
6
  # https://console.groq.com/docs/openai
7
7
  class Groq < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.groq_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.groq_api_url, headers: headers, body: body, stream: stream, provider: :groq, model: model) if stream
13
+
14
+ raw = post_json(URI(config.groq_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
19
- {
20
- content: data.dig("choices", 0, "message", "content").to_s.strip,
21
- provider: :groq,
22
- model: data["model"] || model,
23
- usage: extract_usage_openai(data)
24
- }
18
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :groq, model: data["model"] || model, usage: extract_usage_openai(data) }
25
19
  end
26
20
 
27
21
  private
@@ -5,14 +5,13 @@ module ActiveHarness
5
5
  # Mistral AI — OpenAI-compatible API.
6
6
  # https://docs.mistral.ai/api
7
7
  class Mistral < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.mistral_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.mistral_api_url, headers: headers, body: body, stream: stream, provider: :mistral, model: model) if stream
13
+
14
+ raw = post_json(URI(config.mistral_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
@@ -13,26 +13,20 @@ module ActiveHarness
13
13
  # use provider: :ollama, model: "llama3.2"
14
14
  # end
15
15
  class Ollama < Base
16
- def call(model:, messages:, temperature: 0.7)
17
- url = URI("#{api_base}/v1/chat/completions")
18
-
16
+ def call(model:, messages:, temperature: 0.7, stream: nil)
17
+ url = "#{api_base}/v1/chat/completions"
19
18
  headers = { "Content-Type" => "application/json" }
20
19
  key = api_key
21
20
  headers["Authorization"] = "Bearer #{key}" if key
21
+ body = { model: model, messages: messages, temperature: temperature }
22
+
23
+ return call_streaming(url: url, headers: headers, body: body, stream: stream, provider: :ollama, model: model) if stream
22
24
 
23
- raw = post_json(url,
24
- headers: headers,
25
- body: { model: model, messages: messages, temperature: temperature }
26
- )
25
+ raw = post_json(URI(url), headers: headers, body: body)
27
26
  data = parse!(raw)
28
27
  handle_error!(data)
29
28
 
30
- {
31
- content: data.dig("choices", 0, "message", "content").to_s.strip,
32
- provider: :ollama,
33
- model: data["model"] || model,
34
- usage: extract_usage_openai(data)
35
- }
29
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :ollama, model: data["model"] || model, usage: extract_usage_openai(data) }
36
30
  end
37
31
 
38
32
  private
@@ -7,23 +7,17 @@ module ActiveHarness
7
7
  # @param messages [Array<Hash>] [{role:, content:}, ...]
8
8
  # @param temperature [Float]
9
9
  # @return [Hash] { content:, provider:, model: }
10
- def call(model:, messages:, temperature: 0.7)
11
- raw = post_json(URI(config.openai_api_url),
12
- headers: {
13
- "Content-Type" => "application/json",
14
- "Authorization" => "Bearer #{api_key}"
15
- },
16
- body: { model: model, messages: messages, temperature: temperature }
17
- )
10
+ def call(model:, messages:, temperature: 0.7, stream: nil)
11
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
12
+ body = { model: model, messages: messages, temperature: temperature }
13
+
14
+ return call_streaming(url: config.openai_api_url, headers: headers, body: body, stream: stream, provider: :openai, model: model) if stream
15
+
16
+ raw = post_json(URI(config.openai_api_url), headers: headers, body: body)
18
17
  data = parse!(raw)
19
18
  handle_error!(data)
20
19
 
21
- {
22
- content: data.dig("choices", 0, "message", "content").to_s.strip,
23
- provider: :openai,
24
- model: data["model"] || model,
25
- usage: extract_usage_openai(data)
26
- }
20
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :openai, model: data["model"] || model, usage: extract_usage_openai(data) }
27
21
  end
28
22
 
29
23
  private
@@ -17,11 +17,7 @@ module ActiveHarness
17
17
  }
18
18
  body = { model: model, messages: messages, temperature: temperature }
19
19
 
20
- if stream
21
- body[:stream] = true
22
- content = post_json_stream(URI(config.openrouter_api_url), headers: headers, body: body, on_token: stream)
23
- return { content: content, provider: :openrouter, model: model }
24
- end
20
+ return call_streaming(url: config.openrouter_api_url, headers: headers, body: body, stream: stream, provider: :openrouter, model: model) if stream
25
21
 
26
22
  raw = post_json(URI(config.openrouter_api_url), headers: headers, body: body)
27
23
  data = parse!(raw)
@@ -5,14 +5,13 @@ module ActiveHarness
5
5
  # Perplexity — OpenAI-compatible API with web-search-augmented models.
6
6
  # https://docs.perplexity.ai/api-reference/chat-completions
7
7
  class Perplexity < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.perplexity_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.perplexity_api_url, headers: headers, body: body, stream: stream, provider: :perplexity, model: model) if stream
13
+
14
+ raw = post_json(URI(config.perplexity_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
@@ -5,14 +5,13 @@ module ActiveHarness
5
5
  # xAI (Grok) — OpenAI-compatible API.
6
6
  # https://docs.x.ai/api
7
7
  class XAI < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.xai_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.xai_api_url, headers: headers, body: body, stream: stream, provider: :xai, model: model) if stream
13
+
14
+ raw = post_json(URI(config.xai_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.2.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - the-teacher