active_harness 0.2.40 → 0.2.41
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e575886969a297ab3688f08a326e0290b1f7c433322c7a05aab8f183178e441e
|
|
4
|
+
data.tar.gz: 8b73cfb142335117fd7bde7c6da6ebca88f7218906450a1897b2cd42ea9b37d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51060b54a300332514f5993d83b5666416da70dc51493d06259106909c17a96262d28991290cbcc51b4018f324e82d06f274cd292d81ad65ad9b99a33a82060d
|
|
7
|
+
data.tar.gz: f4041988b036d9b252c0081e3e664febabaf72cbc69995629f21b6b6711c7a4283a1d9e30fb388f0bbde0a127c940e54cd3a4b325978798a4d090e93ecf639dc
|
|
@@ -43,11 +43,16 @@ module ActiveHarness
|
|
|
43
43
|
openrouter: -> { Providers::Images::OpenRouter.new }
|
|
44
44
|
}.freeze
|
|
45
45
|
|
|
46
|
+
TRANSCRIPTION_PROVIDERS = {
|
|
47
|
+
openrouter: -> { Providers::Audio::OpenRouter.new }
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
46
50
|
private
|
|
47
51
|
|
|
48
52
|
def attempt_model(entry, system_prompt)
|
|
49
53
|
return attempt_via_custom_llm(entry, system_prompt) if @config[:custom_llm_backend]
|
|
50
54
|
return attempt_image_model(entry, system_prompt) if @config[:image]
|
|
55
|
+
return attempt_transcription_model(entry) if @config[:transcribe]
|
|
51
56
|
|
|
52
57
|
provider = resolve_provider(entry[:provider])
|
|
53
58
|
messages = build_messages(system_prompt, @input)
|
|
@@ -78,6 +83,24 @@ module ActiveHarness
|
|
|
78
83
|
"#{system_prompt}\n\n#{input}"
|
|
79
84
|
end
|
|
80
85
|
|
|
86
|
+
def attempt_transcription_model(entry)
|
|
87
|
+
factory = TRANSCRIPTION_PROVIDERS[entry[:provider].to_sym]
|
|
88
|
+
raise ArgumentError, "Provider #{entry[:provider].inspect} does not support audio transcription. " \
|
|
89
|
+
"Supported transcription providers: #{TRANSCRIPTION_PROVIDERS.keys.join(', ')}" unless factory
|
|
90
|
+
|
|
91
|
+
path = @input.to_s
|
|
92
|
+
raise ArgumentError, "#{self.class.name}: audio file not found: #{path.inspect}" unless File.exist?(path)
|
|
93
|
+
|
|
94
|
+
format = File.extname(path).delete_prefix(".").downcase
|
|
95
|
+
raise ArgumentError, "#{self.class.name}: cannot determine audio format from #{path.inspect} " \
|
|
96
|
+
"— expected a file extension like .mp3/.wav/.flac/.m4a/.ogg/.webm/.aac" if format.empty?
|
|
97
|
+
|
|
98
|
+
opts = { model: entry[:model], audio_data: File.binread(path), audio_format: format }
|
|
99
|
+
language = entry[:language] || @config[:transcribe_language]
|
|
100
|
+
opts[:language] = language if language
|
|
101
|
+
factory.call.call(**opts)
|
|
102
|
+
end
|
|
103
|
+
|
|
81
104
|
def resolve_provider(name)
|
|
82
105
|
factory = PROVIDERS[name.to_sym]
|
|
83
106
|
raise ArgumentError, "Unknown provider: #{name.inspect}. Supported: #{PROVIDERS.keys.join(', ')}" unless factory
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module ActiveHarness
|
|
2
|
+
class Agent
|
|
3
|
+
class << self
|
|
4
|
+
# Mark this agent as an audio transcription agent.
|
|
5
|
+
#
|
|
6
|
+
# class TranscriptionAgent < ActiveHarness::Agent
|
|
7
|
+
# transcribe true
|
|
8
|
+
#
|
|
9
|
+
# model do
|
|
10
|
+
# use provider: :openrouter, model: "openai/whisper-1"
|
|
11
|
+
# fallback provider: :openrouter, model: "deepgram/nova-3"
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @input — path to a local audio file (String), not free text
|
|
16
|
+
# result.output — transcribed text (String)
|
|
17
|
+
# result.processed — same as output (format :text default)
|
|
18
|
+
def transcribe(value = true)
|
|
19
|
+
agent_config[:transcribe] = value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Default ISO-639-1 language hint for all models in this agent's chain.
|
|
23
|
+
# Can be overridden per-model via: use provider: :openrouter, model: "...", language: "ja"
|
|
24
|
+
def language(default_language)
|
|
25
|
+
agent_config[:transcribe_language] = default_language
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
alias_method :_model_list_before_transcribe, :model_list
|
|
30
|
+
|
|
31
|
+
def model_list
|
|
32
|
+
list = _model_list_before_transcribe
|
|
33
|
+
validate_transcription_models!(list) if @config[:transcribe]
|
|
34
|
+
list
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
alias_method :_normalize_input_before_transcribe, :normalize_input!
|
|
40
|
+
|
|
41
|
+
# @input is a file path for transcription agents, not free text —
|
|
42
|
+
# stripping/collapsing whitespace could corrupt a path. Skip it.
|
|
43
|
+
def normalize_input!
|
|
44
|
+
return if @config[:transcribe]
|
|
45
|
+
_normalize_input_before_transcribe
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Models absent from the Pricing registry are silently skipped — unknown
|
|
49
|
+
# models are assumed valid to avoid false negatives on new/private models.
|
|
50
|
+
def validate_transcription_models!(list)
|
|
51
|
+
list.each do |entry|
|
|
52
|
+
info = Pricing.find(entry[:model].to_s)
|
|
53
|
+
next unless info
|
|
54
|
+
|
|
55
|
+
unless info.categories.include?("transcription")
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"#{self.class.name}: model #{entry[:model].inspect} (provider: #{entry[:provider]}) " \
|
|
58
|
+
"does not support audio transcription " \
|
|
59
|
+
"(output_modalities: #{info.output_modalities.inspect}). " \
|
|
60
|
+
"Use a model that has 'transcription' in output_modalities."
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/active_harness/agent.rb
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
require "base64"
|
|
3
|
+
|
|
4
|
+
module ActiveHarness
|
|
5
|
+
module Providers
|
|
6
|
+
module Audio
|
|
7
|
+
class OpenRouter < Base
|
|
8
|
+
# Dedicated transcription endpoint — distinct from the chat completions
|
|
9
|
+
# endpoint used by text/image generation on OpenRouter.
|
|
10
|
+
ENDPOINT = "https://openrouter.ai/api/v1/audio/transcriptions"
|
|
11
|
+
|
|
12
|
+
# @param model [String] e.g. "openai/whisper-1", "deepgram/nova-3"
|
|
13
|
+
# @param audio_data [String] raw binary audio bytes
|
|
14
|
+
# @param audio_format [String] "wav", "mp3", "flac", "m4a", "ogg", "webm", "aac"
|
|
15
|
+
# @param language [String] ISO-639-1 code, e.g. "en" (optional — auto-detected if omitted)
|
|
16
|
+
#
|
|
17
|
+
# Synchronous call — OpenRouter's transcription endpoint has no job/polling
|
|
18
|
+
# API. Upstream providers time out after ~60s per request, so long audio
|
|
19
|
+
# should be split into shorter chunks by the caller before transcribing.
|
|
20
|
+
def call(model:, audio_data:, audio_format:, language: nil, **_)
|
|
21
|
+
headers = {
|
|
22
|
+
"Content-Type" => "application/json",
|
|
23
|
+
"Authorization" => "Bearer #{api_key}"
|
|
24
|
+
}
|
|
25
|
+
referer = config.openrouter_http_referer.to_s
|
|
26
|
+
headers["HTTP-Referer"] = referer unless referer.empty?
|
|
27
|
+
|
|
28
|
+
body = {
|
|
29
|
+
model: model,
|
|
30
|
+
input_audio: { data: Base64.strict_encode64(audio_data), format: audio_format }
|
|
31
|
+
}
|
|
32
|
+
body[:language] = language if language
|
|
33
|
+
|
|
34
|
+
raw = post_json(URI(ENDPOINT), headers: headers, body: body, timeout: 90)
|
|
35
|
+
data = parse!(raw)
|
|
36
|
+
handle_error!(data)
|
|
37
|
+
|
|
38
|
+
text = data["text"]
|
|
39
|
+
raise Errors::ProviderError, "No transcription text in response: #{data.keys}" if text.nil?
|
|
40
|
+
|
|
41
|
+
{ content: text, provider: :openrouter, model: model, usage: extract_transcription_usage(data) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# OpenRouter's transcription usage shape differs from its chat/image usage
|
|
47
|
+
# shape (input_tokens/output_tokens directly, not prompt_tokens/completion_tokens).
|
|
48
|
+
def extract_transcription_usage(data)
|
|
49
|
+
u = data["usage"]
|
|
50
|
+
return nil unless u
|
|
51
|
+
|
|
52
|
+
result = {
|
|
53
|
+
input_tokens: u["input_tokens"].to_i,
|
|
54
|
+
output_tokens: u["output_tokens"].to_i,
|
|
55
|
+
total_tokens: u["total_tokens"].to_i
|
|
56
|
+
}
|
|
57
|
+
result[:provider_cost] = u["cost"].to_f if u.key?("cost")
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def api_key
|
|
62
|
+
key = config.openrouter_api_key.to_s
|
|
63
|
+
raise Errors::InvalidApiKeyError, "openrouter_api_key is not configured" if key.empty?
|
|
64
|
+
key
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_error!(data)
|
|
68
|
+
return unless data["error"]
|
|
69
|
+
|
|
70
|
+
msg = data.dig("error", "message").to_s
|
|
71
|
+
code = data.dig("error", "code").to_s
|
|
72
|
+
metadata = data.dig("error", "metadata")
|
|
73
|
+
|
|
74
|
+
case code
|
|
75
|
+
when "401" then raise Errors::InvalidApiKeyError.new(msg, error_code: code, metadata: metadata)
|
|
76
|
+
when "402", "429" then raise Errors::RateLimitError.new(msg, error_code: code, metadata: metadata)
|
|
77
|
+
when "500", "502",
|
|
78
|
+
"503", "504" then raise Errors::ProviderUnavailableError.new(msg, error_code: code, metadata: metadata)
|
|
79
|
+
else raise Errors::InvalidRequestError.new(msg, error_code: code, metadata: metadata)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/active_harness.rb
CHANGED
|
@@ -23,6 +23,7 @@ require_relative "active_harness/providers/vertexai"
|
|
|
23
23
|
require_relative "active_harness/providers/custom"
|
|
24
24
|
require_relative "active_harness/providers/images/openai"
|
|
25
25
|
require_relative "active_harness/providers/images/openrouter"
|
|
26
|
+
require_relative "active_harness/providers/audio/openrouter"
|
|
26
27
|
require "active_harness_pricing"
|
|
27
28
|
require_relative "active_harness/memory"
|
|
28
29
|
require_relative "active_harness/agent"
|
|
@@ -32,7 +33,7 @@ require_relative "active_harness/pipeline"
|
|
|
32
33
|
require_relative "active_harness/railtie" if defined?(Rails::Railtie)
|
|
33
34
|
|
|
34
35
|
module ActiveHarness
|
|
35
|
-
VERSION = "0.2.
|
|
36
|
+
VERSION = "0.2.41"
|
|
36
37
|
|
|
37
38
|
class << self
|
|
38
39
|
# Configure ActiveHarness.
|
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.
|
|
4
|
+
version: 0.2.41
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- the-teacher
|
|
@@ -55,6 +55,7 @@ files:
|
|
|
55
55
|
- lib/active_harness/agent/output_parser.rb
|
|
56
56
|
- lib/active_harness/agent/prompt.rb
|
|
57
57
|
- lib/active_harness/agent/providers.rb
|
|
58
|
+
- lib/active_harness/agent/transcription.rb
|
|
58
59
|
- lib/active_harness/configuration.rb
|
|
59
60
|
- lib/active_harness/core/errors.rb
|
|
60
61
|
- lib/active_harness/core/hooks.rb
|
|
@@ -72,6 +73,7 @@ files:
|
|
|
72
73
|
- lib/active_harness/pipeline/step.rb
|
|
73
74
|
- lib/active_harness/providers/PROVIDER_CONTRACT.md
|
|
74
75
|
- lib/active_harness/providers/anthropic.rb
|
|
76
|
+
- lib/active_harness/providers/audio/openrouter.rb
|
|
75
77
|
- lib/active_harness/providers/azure.rb
|
|
76
78
|
- lib/active_harness/providers/base.rb
|
|
77
79
|
- lib/active_harness/providers/bedrock.rb
|