active_harness 0.2.41 → 0.2.42
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 +4 -4
- data/lib/active_harness/agent/providers.rb +1 -0
- data/lib/active_harness/providers/audio/openai.rb +127 -0
- data/lib/active_harness.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64ee242555ae4c7c33965a04607e3f72c76eb83046f62aaadebf98585866b1a8
|
|
4
|
+
data.tar.gz: 0ec32b43d65efce4ed82c3a6ab997eb42f03720cd219baf63375fd9d4abb4316
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0cb81649fd8e9a5c8ee2f2836dbda95cf94e0ce2ad5a9f60ed5325bb5666baf0c11009578b449c0b8ba45551dd670d71b78329be71a196423d08137f660d86b3
|
|
7
|
+
data.tar.gz: 2b1c6257ca5e3fd16337e57a7ab5c018de1534aa98616d6cee1be19b6a767cbd7a0ec03f1bbe578400d183a1d61ae566c6eccc91587cbe276954874633a0d5e3
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
|
|
4
|
+
module ActiveHarness
|
|
5
|
+
module Providers
|
|
6
|
+
module Audio
|
|
7
|
+
class OpenAI < Base
|
|
8
|
+
ENDPOINT = "https://api.openai.com/v1/audio/transcriptions"
|
|
9
|
+
|
|
10
|
+
# OpenAI's transcription endpoint only accepts these formats — notably
|
|
11
|
+
# no flac/ogg/aac, unlike OpenRouter's version of this endpoint.
|
|
12
|
+
CONTENT_TYPES = {
|
|
13
|
+
"mp3" => "audio/mpeg",
|
|
14
|
+
"mp4" => "audio/mp4",
|
|
15
|
+
"mpeg" => "audio/mpeg",
|
|
16
|
+
"mpga" => "audio/mpeg",
|
|
17
|
+
"m4a" => "audio/mp4",
|
|
18
|
+
"wav" => "audio/wav",
|
|
19
|
+
"webm" => "audio/webm"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# @param model [String] e.g. "whisper-1", "gpt-4o-transcribe", "gpt-4o-mini-transcribe"
|
|
23
|
+
# @param audio_data [String] raw binary audio bytes
|
|
24
|
+
# @param audio_format [String] one of CONTENT_TYPES.keys
|
|
25
|
+
# @param language [String] ISO-639-1 code, e.g. "en" (optional — auto-detected if omitted)
|
|
26
|
+
#
|
|
27
|
+
# Synchronous — this endpoint has no job/polling API. Unlike OpenRouter's
|
|
28
|
+
# transcription endpoint, OpenAI's is multipart/form-data only (no
|
|
29
|
+
# base64/JSON request mode).
|
|
30
|
+
def call(model:, audio_data:, audio_format:, language: nil, **_)
|
|
31
|
+
content_type = CONTENT_TYPES[audio_format]
|
|
32
|
+
unless content_type
|
|
33
|
+
raise Errors::InvalidRequestError,
|
|
34
|
+
"openai transcription does not support .#{audio_format} — use one of: #{CONTENT_TYPES.keys.join(', ')}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
boundary = SecureRandom.hex(16)
|
|
38
|
+
fields = { "model" => model }
|
|
39
|
+
fields["language"] = language if language
|
|
40
|
+
|
|
41
|
+
body = build_multipart_body(boundary, fields, audio_data, "audio.#{audio_format}", content_type)
|
|
42
|
+
headers = {
|
|
43
|
+
"Content-Type" => "multipart/form-data; boundary=#{boundary}",
|
|
44
|
+
"Authorization" => "Bearer #{api_key}"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
raw = HTTP.post(URI(ENDPOINT), headers: headers, body: body, timeout: 90)
|
|
48
|
+
data = parse!(raw)
|
|
49
|
+
handle_error!(data)
|
|
50
|
+
|
|
51
|
+
text = data["text"]
|
|
52
|
+
raise Errors::ProviderError, "No transcription text in response: #{data.keys}" if text.nil?
|
|
53
|
+
|
|
54
|
+
{ content: text, provider: :openai, model: model, usage: extract_transcription_usage(data) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def build_multipart_body(boundary, fields, file_data, filename, content_type)
|
|
60
|
+
body = +""
|
|
61
|
+
fields.each do |name, value|
|
|
62
|
+
body << "--#{boundary}\r\n"
|
|
63
|
+
body << "Content-Disposition: form-data; name=\"#{name}\"\r\n\r\n"
|
|
64
|
+
body << "#{value}\r\n"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
body << "--#{boundary}\r\n"
|
|
68
|
+
body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
|
|
69
|
+
body << "Content-Type: #{content_type}\r\n\r\n"
|
|
70
|
+
body << file_data
|
|
71
|
+
body << "\r\n--#{boundary}--\r\n"
|
|
72
|
+
body
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# whisper-1 reports usage as { type: "duration", seconds: N } — no token
|
|
76
|
+
# counts, so there's nothing to map to input_tokens/output_tokens. Newer
|
|
77
|
+
# models (gpt-4o-transcribe, gpt-4o-mini-transcribe) report
|
|
78
|
+
# { type: "tokens", input_tokens:, output_tokens:, total_tokens:, ... }.
|
|
79
|
+
# Neither reports a direct dollar cost like OpenRouter does — cost is left
|
|
80
|
+
# to the normal per-token Pricing lookup, which returns nil for
|
|
81
|
+
# duration-billed models since it has no token counts to work with.
|
|
82
|
+
def extract_transcription_usage(data)
|
|
83
|
+
u = data["usage"]
|
|
84
|
+
return nil unless u && u["type"] == "tokens"
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
input_tokens: u["input_tokens"].to_i,
|
|
88
|
+
output_tokens: u["output_tokens"].to_i,
|
|
89
|
+
total_tokens: u["total_tokens"].to_i
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def api_key
|
|
94
|
+
key = config.openai_api_key.to_s
|
|
95
|
+
raise Errors::InvalidApiKeyError, "openai_api_key is not configured" if key.empty?
|
|
96
|
+
key
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def handle_error!(data)
|
|
100
|
+
return unless data["error"]
|
|
101
|
+
|
|
102
|
+
msg = data.dig("error", "message").to_s
|
|
103
|
+
code = data.dig("error", "code").to_s
|
|
104
|
+
type = data.dig("error", "type").to_s
|
|
105
|
+
metadata = data["error"].reject { |k, _| %w[message code type].include?(k) }
|
|
106
|
+
metadata = nil if metadata.empty?
|
|
107
|
+
|
|
108
|
+
case code
|
|
109
|
+
when "invalid_api_key", "unauthorized"
|
|
110
|
+
raise Errors::InvalidApiKeyError.new(msg, error_code: code, metadata: metadata)
|
|
111
|
+
when "rate_limit_exceeded"
|
|
112
|
+
raise Errors::RateLimitError.new(msg, error_code: code, metadata: metadata)
|
|
113
|
+
when "content_filter"
|
|
114
|
+
raise Errors::SafetyBlockedError.new(msg, error_code: code, metadata: metadata)
|
|
115
|
+
else
|
|
116
|
+
case type
|
|
117
|
+
when "server_error"
|
|
118
|
+
raise Errors::ServerError.new(msg, error_code: code, metadata: metadata)
|
|
119
|
+
else
|
|
120
|
+
raise Errors::InvalidRequestError.new(msg, error_code: code, metadata: metadata)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
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/openai"
|
|
26
27
|
require_relative "active_harness/providers/audio/openrouter"
|
|
27
28
|
require "active_harness_pricing"
|
|
28
29
|
require_relative "active_harness/memory"
|
|
@@ -33,7 +34,7 @@ require_relative "active_harness/pipeline"
|
|
|
33
34
|
require_relative "active_harness/railtie" if defined?(Rails::Railtie)
|
|
34
35
|
|
|
35
36
|
module ActiveHarness
|
|
36
|
-
VERSION = "0.2.
|
|
37
|
+
VERSION = "0.2.42"
|
|
37
38
|
|
|
38
39
|
class << self
|
|
39
40
|
# Configure ActiveHarness.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.42
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- the-teacher
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -73,6 +73,7 @@ files:
|
|
|
73
73
|
- lib/active_harness/pipeline/step.rb
|
|
74
74
|
- lib/active_harness/providers/PROVIDER_CONTRACT.md
|
|
75
75
|
- lib/active_harness/providers/anthropic.rb
|
|
76
|
+
- lib/active_harness/providers/audio/openai.rb
|
|
76
77
|
- lib/active_harness/providers/audio/openrouter.rb
|
|
77
78
|
- lib/active_harness/providers/azure.rb
|
|
78
79
|
- lib/active_harness/providers/base.rb
|