llm.rb 8.0.0 → 8.1.0
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/CHANGELOG.md +45 -0
- data/README.md +8 -3
- data/data/bedrock.json +2948 -0
- data/data/deepseek.json +8 -8
- data/data/openai.json +39 -2
- data/data/xai.json +35 -0
- data/data/zai.json +1 -1
- data/lib/llm/object.rb +8 -0
- data/lib/llm/provider/transport/http/execution.rb +1 -1
- data/lib/llm/provider.rb +7 -0
- data/lib/llm/providers/bedrock/error_handler.rb +80 -0
- data/lib/llm/providers/bedrock/models.rb +109 -0
- data/lib/llm/providers/bedrock/request_adapter/completion.rb +153 -0
- data/lib/llm/providers/bedrock/request_adapter.rb +95 -0
- data/lib/llm/providers/bedrock/response_adapter/completion.rb +143 -0
- data/lib/llm/providers/bedrock/response_adapter/models.rb +34 -0
- data/lib/llm/providers/bedrock/response_adapter.rb +40 -0
- data/lib/llm/providers/bedrock/signature.rb +166 -0
- data/lib/llm/providers/bedrock/stream_decoder.rb +140 -0
- data/lib/llm/providers/bedrock/stream_parser.rb +201 -0
- data/lib/llm/providers/bedrock.rb +272 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +8 -0
- metadata +13 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Bedrock::ResponseAdapter
|
|
4
|
+
##
|
|
5
|
+
# Adapts Bedrock Converse API completion responses.
|
|
6
|
+
#
|
|
7
|
+
# The Bedrock Converse response body looks like:
|
|
8
|
+
# {
|
|
9
|
+
# "output" => {"message" => {
|
|
10
|
+
# "role" => "assistant",
|
|
11
|
+
# "content" => [{"text" => "..."}, {"toolUse" => {...}}]
|
|
12
|
+
# }},
|
|
13
|
+
# "usage" => {"inputTokens" => N, "outputTokens" => N},
|
|
14
|
+
# "modelId" => "anthropic.claude-sonnet-4-20250514-v1:0",
|
|
15
|
+
# "stopReason" => "end_turn"
|
|
16
|
+
# }
|
|
17
|
+
module Completion
|
|
18
|
+
##
|
|
19
|
+
# (see LLM::Contract::Completion#messages)
|
|
20
|
+
def messages
|
|
21
|
+
source = texts.empty? && tools.any? ? [{"text" => ""}] : texts
|
|
22
|
+
source.map.with_index do |choice, index|
|
|
23
|
+
extra = {
|
|
24
|
+
index:, response: self,
|
|
25
|
+
reasoning_content:,
|
|
26
|
+
tool_calls: adapt_tool_calls(tools),
|
|
27
|
+
original_tool_calls: tools
|
|
28
|
+
}
|
|
29
|
+
LLM::Message.new(role, choice["text"], extra)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
alias_method :choices, :messages
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Returns the Bedrock request id when present.
|
|
36
|
+
# @return [String, nil]
|
|
37
|
+
def id
|
|
38
|
+
res["x-amzn-requestid"] || res["x-amzn-request-id"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# (see LLM::Contract::Completion#input_tokens)
|
|
43
|
+
def input_tokens
|
|
44
|
+
body.usage&.inputTokens || 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# (see LLM::Contract::Completion#output_tokens)
|
|
49
|
+
def output_tokens
|
|
50
|
+
body.usage&.outputTokens || 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# (see LLM::Contract::Completion#reasoning_tokens)
|
|
55
|
+
def reasoning_tokens
|
|
56
|
+
0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# (see LLM::Contract::Completion#total_tokens)
|
|
61
|
+
def total_tokens
|
|
62
|
+
input_tokens + output_tokens
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# (see LLM::Contract::Completion#usage)
|
|
67
|
+
def usage
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# (see LLM::Contract::Completion#model)
|
|
73
|
+
def model
|
|
74
|
+
body.modelId
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# (see LLM::Contract::Completion#content)
|
|
79
|
+
def content
|
|
80
|
+
super
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# (see LLM::Contract::Completion#reasoning_content)
|
|
85
|
+
def reasoning_content
|
|
86
|
+
@reasoning_content ||= begin
|
|
87
|
+
text = parts.filter_map { _1.dig("reasoningContent", "text") }.join
|
|
88
|
+
text.empty? ? nil : text
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# (see LLM::Contract::Completion#content!)
|
|
94
|
+
def content!
|
|
95
|
+
super
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def adapt_tool_calls(tools)
|
|
101
|
+
(tools || []).filter_map do |tool|
|
|
102
|
+
next unless tool["toolUse"]
|
|
103
|
+
{
|
|
104
|
+
id: tool["toolUse"]["toolUseId"],
|
|
105
|
+
name: tool["toolUse"]["name"],
|
|
106
|
+
arguments: parse_tool_input(tool["toolUse"]["input"])
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_tool_input(input)
|
|
112
|
+
case input
|
|
113
|
+
when Hash then input
|
|
114
|
+
when String
|
|
115
|
+
parsed = LLM.json.load(input)
|
|
116
|
+
Hash === parsed ? parsed : {}
|
|
117
|
+
when nil then {}
|
|
118
|
+
else input.respond_to?(:to_h) ? input.to_h : {}
|
|
119
|
+
end
|
|
120
|
+
rescue *LLM.json.parser_error
|
|
121
|
+
{}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def parts
|
|
125
|
+
raw = body.output&.message&.content || []
|
|
126
|
+
raw.is_a?(Array) ? raw : [raw].compact
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def texts
|
|
130
|
+
@texts ||= parts.select { |b| b["text"] }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def tools
|
|
134
|
+
@tools ||= parts.select { |b| b["toolUse"] }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def role
|
|
138
|
+
body.output&.message&.role || "assistant"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
include LLM::Contract::Completion
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Bedrock::ResponseAdapter
|
|
4
|
+
##
|
|
5
|
+
# Adapts Bedrock ListFoundationModels API responses to llm.rb's
|
|
6
|
+
# normalized model collection format.
|
|
7
|
+
#
|
|
8
|
+
# The Bedrock ListFoundationModels response looks like:
|
|
9
|
+
# {
|
|
10
|
+
# "modelSummaries": [{
|
|
11
|
+
# "modelId": "anthropic.claude-sonnet-4-20250514-v1:0",
|
|
12
|
+
# "modelName": "Claude Sonnet 4",
|
|
13
|
+
# "providerName": "Anthropic",
|
|
14
|
+
# "inputModalities": ["TEXT", "IMAGE"],
|
|
15
|
+
# "outputModalities": ["TEXT"],
|
|
16
|
+
# ...
|
|
17
|
+
# }]
|
|
18
|
+
# }
|
|
19
|
+
module Models
|
|
20
|
+
include LLM::Model::Collection
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def raw_models
|
|
25
|
+
(body.modelSummaries || []).map do |summary|
|
|
26
|
+
LLM::Object.from({
|
|
27
|
+
id: summary.modelId,
|
|
28
|
+
name: summary.modelName,
|
|
29
|
+
provider_name: summary.providerName
|
|
30
|
+
})
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Bedrock
|
|
4
|
+
##
|
|
5
|
+
# Adapts Bedrock Converse API responses to llm.rb's Response format.
|
|
6
|
+
#
|
|
7
|
+
# Bedrock Converse returns:
|
|
8
|
+
# {
|
|
9
|
+
# output: {message: {role: "assistant", content: [{...}, ...]}},
|
|
10
|
+
# usage: {inputTokens: N, outputTokens: N},
|
|
11
|
+
# modelId: "anthropic.claude-...",
|
|
12
|
+
# stopReason: "end_turn" | "tool_use" | "max_tokens" | ...
|
|
13
|
+
# }
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
module ResponseAdapter
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @param [LLM::Response, Net::HTTPResponse] res
|
|
21
|
+
# @param [Symbol] type
|
|
22
|
+
# @return [LLM::Response]
|
|
23
|
+
def adapt(res, type:)
|
|
24
|
+
response = (LLM::Response === res) ? res : LLM::Response.new(res)
|
|
25
|
+
response.extend(select(type))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# @api private
|
|
30
|
+
def select(type)
|
|
31
|
+
case type
|
|
32
|
+
when :completion then LLM::Bedrock::ResponseAdapter::Completion
|
|
33
|
+
when :models then LLM::Bedrock::ResponseAdapter::Models
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"Unknown response adapter type: #{type.inspect}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
class LLM::Bedrock
|
|
7
|
+
##
|
|
8
|
+
# Signs HTTP requests and headers with AWS Signature V4.
|
|
9
|
+
#
|
|
10
|
+
# Returns the signed headers as a Hash through #to_h, ready to merge
|
|
11
|
+
# into a Net::HTTPRequest or other HTTP client. Everything else is
|
|
12
|
+
# private.
|
|
13
|
+
#
|
|
14
|
+
# Uses only Ruby's stdlib (openssl, digest) with no external deps.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# signature = LLM::Bedrock::Signature.new(
|
|
18
|
+
# credentials: LLM::Object.from(
|
|
19
|
+
# access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
|
20
|
+
# secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
|
|
21
|
+
# aws_region: "us-east-1",
|
|
22
|
+
# host: "bedrock-runtime.us-east-1.amazonaws.com",
|
|
23
|
+
# session_token: nil
|
|
24
|
+
# ),
|
|
25
|
+
# method: "POST",
|
|
26
|
+
# path: "/model/anthropic.claude-3/converse",
|
|
27
|
+
# body: '{"messages":[...]}'
|
|
28
|
+
# )
|
|
29
|
+
# signature.sign!(req)
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
32
|
+
class Signature
|
|
33
|
+
SERVICE = "bedrock"
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# @param [LLM::Object] credentials AWS signing credentials and host
|
|
37
|
+
# @param [String] method HTTP method ("POST", "GET", etc.)
|
|
38
|
+
# @param [String] path Request path (e.g. "/model/.../converse")
|
|
39
|
+
# @param [String] body Raw request body
|
|
40
|
+
# @param [String, nil] query Canonical query string
|
|
41
|
+
def initialize(credentials:, method:, path:, body:, query: nil)
|
|
42
|
+
@credentials = credentials
|
|
43
|
+
@method = method
|
|
44
|
+
@path = path
|
|
45
|
+
@query = query
|
|
46
|
+
@body = body
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# Returns the signed headers as a plain Hash.
|
|
51
|
+
#
|
|
52
|
+
# Call this once per request and merge the result into your
|
|
53
|
+
# HTTP headers. Each call recomputes the signature with the
|
|
54
|
+
# current time, so call it immediately before sending.
|
|
55
|
+
#
|
|
56
|
+
# @return [Hash{String => String}]
|
|
57
|
+
def to_h
|
|
58
|
+
now = Time.now.utc
|
|
59
|
+
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
|
60
|
+
date_stamp = now.strftime("%Y%m%d")
|
|
61
|
+
payload_hash = Digest::SHA256.hexdigest(@body)
|
|
62
|
+
headers = {
|
|
63
|
+
"X-Amz-Date" => amz_date,
|
|
64
|
+
"X-Amz-Content-Sha256" => payload_hash,
|
|
65
|
+
"Content-Type" => "application/json",
|
|
66
|
+
"Host" => @credentials.host
|
|
67
|
+
}
|
|
68
|
+
headers["X-Amz-Security-Token"] = @credentials.session_token if @credentials.session_token
|
|
69
|
+
signed_headers = build_signed_headers
|
|
70
|
+
canonical_headers = build_canonical_headers(headers, signed_headers)
|
|
71
|
+
canonical_uri = build_canonical_uri
|
|
72
|
+
canonical_query = build_canonical_query
|
|
73
|
+
canonical_request = build_canonical_request(
|
|
74
|
+
canonical_uri, canonical_query, canonical_headers, signed_headers, payload_hash
|
|
75
|
+
)
|
|
76
|
+
credential_scope = "#{date_stamp}/#{@credentials.aws_region}/#{SERVICE}/aws4_request"
|
|
77
|
+
string_to_sign = build_string_to_sign(
|
|
78
|
+
amz_date, credential_scope, canonical_request
|
|
79
|
+
)
|
|
80
|
+
signing_key = derive_signing_key(date_stamp)
|
|
81
|
+
signature = OpenSSL::HMAC.hexdigest(
|
|
82
|
+
"sha256", signing_key, string_to_sign
|
|
83
|
+
)
|
|
84
|
+
headers["Authorization"] =
|
|
85
|
+
"AWS4-HMAC-SHA256 " \
|
|
86
|
+
"Credential=#{@credentials.access_key_id}/#{credential_scope}, " \
|
|
87
|
+
"SignedHeaders=#{signed_headers}, Signature=#{signature}"
|
|
88
|
+
headers
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# @param [Net::HTTPRequest] req
|
|
93
|
+
# @return [Net::HTTPRequest]
|
|
94
|
+
def sign!(req)
|
|
95
|
+
to_h.each { |k, v| req[k] = v }
|
|
96
|
+
req
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def build_signed_headers
|
|
102
|
+
%w[host x-amz-date x-amz-content-sha256].tap do |h|
|
|
103
|
+
h << "x-amz-security-token" if @credentials.session_token
|
|
104
|
+
h << "content-type"
|
|
105
|
+
end.sort.join(";")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_canonical_headers(headers, signed_headers)
|
|
109
|
+
headers = headers.transform_keys(&:downcase)
|
|
110
|
+
signed_headers.split(";").map do |key|
|
|
111
|
+
"#{key}:#{headers[key].to_s.strip}\n"
|
|
112
|
+
end.join
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def build_canonical_uri
|
|
116
|
+
path = @path
|
|
117
|
+
return "/" if path.nil? || path.empty?
|
|
118
|
+
segments = path.split("/", -1).map { |s| uri_encode(s) }
|
|
119
|
+
canonical = segments.join("/")
|
|
120
|
+
canonical.start_with?("/") ? canonical : "/#{canonical}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_canonical_query
|
|
124
|
+
return "" if @query.to_s.empty?
|
|
125
|
+
URI.decode_www_form(@query).sort.map do |key, value|
|
|
126
|
+
"#{uri_encode(key)}=#{uri_encode(value)}"
|
|
127
|
+
end.join("&")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_canonical_request(uri, query, canonical_headers,
|
|
131
|
+
signed_headers, payload_hash)
|
|
132
|
+
[
|
|
133
|
+
@method,
|
|
134
|
+
uri,
|
|
135
|
+
query,
|
|
136
|
+
canonical_headers,
|
|
137
|
+
signed_headers,
|
|
138
|
+
payload_hash
|
|
139
|
+
].join("\n")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def build_string_to_sign(amz_date, credential_scope, canonical_request)
|
|
143
|
+
[
|
|
144
|
+
"AWS4-HMAC-SHA256",
|
|
145
|
+
amz_date,
|
|
146
|
+
credential_scope,
|
|
147
|
+
Digest::SHA256.hexdigest(canonical_request)
|
|
148
|
+
].join("\n")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def derive_signing_key(date_stamp)
|
|
152
|
+
k_date = OpenSSL::HMAC.digest(
|
|
153
|
+
"sha256", "AWS4#{@credentials.secret_access_key}", date_stamp
|
|
154
|
+
)
|
|
155
|
+
k_region = OpenSSL::HMAC.digest("sha256", k_date, @credentials.aws_region)
|
|
156
|
+
k_service = OpenSSL::HMAC.digest("sha256", k_region, SERVICE)
|
|
157
|
+
OpenSSL::HMAC.digest("sha256", k_service, "aws4_request")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def uri_encode(str)
|
|
161
|
+
URI.encode_www_form_component(str.to_s)
|
|
162
|
+
.gsub("+", "%20")
|
|
163
|
+
.gsub("%7E", "~")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
class LLM::Bedrock
|
|
6
|
+
##
|
|
7
|
+
# Decodes AWS Event Stream binary frames.
|
|
8
|
+
#
|
|
9
|
+
# Bedrock Converse Stream uses the AWS Event Stream protocol,
|
|
10
|
+
# a binary framing format (not SSE). Each message has:
|
|
11
|
+
# - total length (4 bytes, big-endian)
|
|
12
|
+
# - headers length (4 bytes, big-endian)
|
|
13
|
+
# - prelude CRC (4 bytes)
|
|
14
|
+
# - headers (variable)
|
|
15
|
+
# - payload (variable, usually JSON)
|
|
16
|
+
# - message CRC (4 bytes)
|
|
17
|
+
#
|
|
18
|
+
# Implements #<< to match the interface expected by llm.rb's
|
|
19
|
+
# streaming transport, so it can replace the SSE-based
|
|
20
|
+
# StreamDecoder when streaming from Bedrock.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
class StreamDecoder
|
|
24
|
+
##
|
|
25
|
+
# @return [LLM::Bedrock::StreamParser]
|
|
26
|
+
attr_reader :parser
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# @param [LLM::Bedrock::StreamParser] parser
|
|
30
|
+
def initialize(parser)
|
|
31
|
+
@buffer = +"".b
|
|
32
|
+
@parser = parser
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# Feeds a raw binary chunk into the decoder.
|
|
37
|
+
# Accumulates data until complete frames are available,
|
|
38
|
+
# then decodes them and passes the JSON payload to the parser.
|
|
39
|
+
#
|
|
40
|
+
# @param [String] chunk Raw binary data
|
|
41
|
+
# @return [void]
|
|
42
|
+
def <<(chunk)
|
|
43
|
+
@buffer << chunk
|
|
44
|
+
decode_frames
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# @return [Hash] The fully constructed response body
|
|
49
|
+
def body
|
|
50
|
+
parser.body
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @return [void]
|
|
55
|
+
def free
|
|
56
|
+
@buffer.clear
|
|
57
|
+
parser.free if parser.respond_to?(:free)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def decode_frames
|
|
63
|
+
loop do
|
|
64
|
+
break if @buffer.bytesize < 12
|
|
65
|
+
total_length = @buffer[0, 4].unpack1("N")
|
|
66
|
+
break if @buffer.bytesize < total_length
|
|
67
|
+
# headers_length = @buffer[4, 4].unpack1("N")
|
|
68
|
+
# prelude_crc = @buffer[8, 4].unpack1("N")
|
|
69
|
+
headers = decode_headers
|
|
70
|
+
payload_start = 12 + headers[:length]
|
|
71
|
+
payload_length = total_length - payload_start - 4
|
|
72
|
+
payload = @buffer[payload_start, payload_length] if payload_length > 0
|
|
73
|
+
# message_crc from last 4 bytes, not needed for our purposes
|
|
74
|
+
json = payload ? LLM.json.load(payload) : {}
|
|
75
|
+
parser.parse!(json, event_type: headers[:event_type]) if json.is_a?(Hash)
|
|
76
|
+
@buffer = @buffer[total_length..] || +"".b
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def decode_headers
|
|
81
|
+
headers_length = @buffer[4, 4].unpack1("N")
|
|
82
|
+
offset = 12
|
|
83
|
+
end_offset = offset + headers_length
|
|
84
|
+
result = {event_type: nil, length: headers_length}
|
|
85
|
+
while offset < end_offset
|
|
86
|
+
name_len = @buffer.getbyte(offset)
|
|
87
|
+
offset += 1
|
|
88
|
+
break if offset + name_len > end_offset
|
|
89
|
+
name = @buffer[offset, name_len]
|
|
90
|
+
offset += name_len
|
|
91
|
+
break if offset >= end_offset
|
|
92
|
+
value_type = @buffer.getbyte(offset)
|
|
93
|
+
offset += 1
|
|
94
|
+
value = case value_type
|
|
95
|
+
when 7 # string
|
|
96
|
+
str_len = @buffer[offset, 2].unpack1("n")
|
|
97
|
+
offset += 2
|
|
98
|
+
str = @buffer[offset, str_len]
|
|
99
|
+
offset += str_len
|
|
100
|
+
str
|
|
101
|
+
when 8 # binary
|
|
102
|
+
bin_len = @buffer[offset, 2].unpack1("n")
|
|
103
|
+
offset += 2
|
|
104
|
+
bin = @buffer[offset, bin_len]
|
|
105
|
+
offset += bin_len
|
|
106
|
+
bin
|
|
107
|
+
when 9 # boolean true
|
|
108
|
+
true
|
|
109
|
+
when 1 # boolean false
|
|
110
|
+
false
|
|
111
|
+
when 2 # byte
|
|
112
|
+
val = @buffer.getbyte(offset)
|
|
113
|
+
offset += 1
|
|
114
|
+
val
|
|
115
|
+
when 3 # int16
|
|
116
|
+
val = @buffer[offset, 2].unpack1("s>")
|
|
117
|
+
offset += 2
|
|
118
|
+
val
|
|
119
|
+
when 4 # int32
|
|
120
|
+
val = @buffer[offset, 4].unpack1("l>")
|
|
121
|
+
offset += 4
|
|
122
|
+
val
|
|
123
|
+
when 6 # byte array (as raw string)
|
|
124
|
+
bin_len = @buffer[offset, 2].unpack1("n")
|
|
125
|
+
offset += 2
|
|
126
|
+
bin = @buffer[offset, bin_len]
|
|
127
|
+
offset += bin_len
|
|
128
|
+
bin
|
|
129
|
+
else
|
|
130
|
+
# Unknown type, skip to end of headers
|
|
131
|
+
offset = end_offset
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
result[:event_type] = value if name == ":event-type"
|
|
135
|
+
result[name] = value if name
|
|
136
|
+
end
|
|
137
|
+
result
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|