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: 4246d31550942e31c26ec9a708f49aae5b2e0f385a32aeaa06d6d6a75f2849ee
4
- data.tar.gz: d7f12a75e7f72e3d004bfd3b64a094e99a082992fd0077e69253bddf97b919f0
3
+ metadata.gz: e575886969a297ab3688f08a326e0290b1f7c433322c7a05aab8f183178e441e
4
+ data.tar.gz: 8b73cfb142335117fd7bde7c6da6ebca88f7218906450a1897b2cd42ea9b37d5
5
5
  SHA512:
6
- metadata.gz: 81cf0f7cb7f5f54c2a996097e34461f724bd66da920db0cee57cc84f558f8430bc83ad1e914c62a56f3493e890c993c9f2dd9408c161563ae8c161f5ac03e4d5
7
- data.tar.gz: e144ed04675d274af6d66157a2f733e1cb65342adc7312b10428e9afc3c135b64af616218d6bb9ddbfb36948efd0e83b817f36654523f910c51d4797e07674e6
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
@@ -213,4 +213,5 @@ require_relative "agent/output_parser"
213
213
  require_relative "agent/custom_llm_backend"
214
214
  require_relative "agent/cost"
215
215
  require_relative "agent/image"
216
+ require_relative "agent/transcription"
216
217
 
@@ -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
@@ -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.40"
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.40
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