amlexia 1.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d25d8c8ded5e08e6a76090728c11816ba7267fdd2cc05de42ca538561f57c171
4
+ data.tar.gz: 4cbe93f9d3d9a7cb0b1f6204f218a7ae099a53f44cfa33d5404e6a7e48e5ad4c
5
+ SHA512:
6
+ metadata.gz: 55e69475ca93b746803e3d25affdade613e51b67b09debce0a75020ece7353b9318f71aa183331410b9813b60a6467b9eab93a9c2f5bc32bc1790edf24301ffc
7
+ data.tar.gz: aa4acd6b0c9d2dc8576a63f2a29375f5b855331f4c3e1de8a64918dd3c497a9a7379e29e78e5446fca63d87c0235c15ca64eeaf5fad1065580f01e05de55743b
data/LICENSE ADDED
@@ -0,0 +1,38 @@
1
+ Amlexia Software License Agreement (SDK)
2
+
3
+ Copyright (c) 2026 Amlexia. All rights reserved.
4
+
5
+ IMPORTANT: This software is proprietary and is NOT open source. You may not
6
+ use, copy, modify, merge, publish, distribute, sublicense, or sell copies of
7
+ this software except as expressly permitted below.
8
+
9
+ 1. Grant
10
+ Amlexia grants you a limited, non-exclusive, non-transferable, revocable
11
+ license to install and use this SDK solely to send telemetry to and integrate
12
+ with the Amlexia service (https://amlexia.com) in accordance with the
13
+ Amlexia Terms of Service (https://amlexia.com/terms).
14
+
15
+ 2. Restrictions
16
+ You may not:
17
+ (a) use the SDK except with an active Amlexia account and valid credentials;
18
+ (b) remove or alter proprietary notices;
19
+ (c) reverse engineer, decompile, or disassemble the SDK except where law
20
+ expressly permits and cannot be waived;
21
+ (d) redistribute, sublicense, or make the SDK available to third parties as
22
+ a standalone product or competing service;
23
+ (e) use the SDK to build a product that substitutes for Amlexia.
24
+
25
+ 3. Ownership
26
+ Amlexia and its licensors retain all rights, title, and interest in the SDK.
27
+
28
+ 4. Termination
29
+ This license terminates automatically if you breach these terms or your
30
+ Amlexia account is terminated. Upon termination, you must cease use and
31
+ destroy copies in your possession.
32
+
33
+ 5. Disclaimer
34
+ THE SDK IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. SEE
35
+ https://amlexia.com/terms FOR LIMITATION OF LIABILITY.
36
+
37
+ 6. Contact
38
+ Support: support@amlexia.com
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # Amlexia Ruby SDK
2
+
3
+ ```ruby
4
+ require "amlexia"
5
+
6
+ client = Amlexia::Client.from_env
7
+
8
+ client.track(
9
+ endpoint: "/v1/chat/completions",
10
+ method: "POST",
11
+ status_code: 200,
12
+ latency_ms: 340,
13
+ provider: "openai",
14
+ model_name: "gpt-4o-mini",
15
+ tokens_input: 120,
16
+ tokens_output: 45
17
+ )
18
+
19
+ Amlexia::OpenAI.track_openai_completion(client, model: "gpt-4o-mini", status_code: 200, latency_ms: 340, usage: { prompt_tokens: 120, completion_tokens: 45 })
20
+ Amlexia::Health.check_ingest_health
21
+ ```
22
+
23
+ CLI: `bundle exec amlexia health`
data/amlexia.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ require_relative "lib/amlexia/version"
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "amlexia"
5
+ s.version = Amlexia::VERSION
6
+ s.summary = "Official Amlexia Ruby SDK — API, AI, and infrastructure observability"
7
+ s.description = "Track HTTP APIs, LLM providers, payments, and webhooks with traces, latency, and cost fields."
8
+ s.authors = ["Amlexia"]
9
+ s.email = "support@amlexia.com"
10
+ s.files = Dir["lib/**/*.rb", "exe/amlexia", "README.md", "LICENSE"] + ["amlexia.gemspec"]
11
+ s.executables = ["amlexia"]
12
+ s.bindir = "exe"
13
+ s.homepage = "https://amlexia.com"
14
+ s.metadata = {
15
+ "source_code_uri" => "https://github.com/Amlexia/Amlexia",
16
+ "documentation_uri" => "https://docs.amlexia.com/sdk/ruby",
17
+ "changelog_uri" => "https://docs.amlexia.com/changelog"
18
+ }
19
+ s.license = "Nonstandard"
20
+ s.required_ruby_version = ">= 3.0"
21
+ end
data/exe/amlexia ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require_relative "../lib/amlexia"
6
+
7
+ ingest = ENV.fetch("AMLEXIA_INGEST_URL", Amlexia::Client::DEFAULT_INGEST_URL)
8
+ cmd = ARGV[0] || "health"
9
+
10
+ case cmd
11
+ when "version"
12
+ puts Amlexia::VERSION
13
+ when "health"
14
+ result = Amlexia::Health.check_ingest_health(ingest_url: ingest)
15
+ puts JSON.pretty_generate({ ingestUrl: ingest, **result })
16
+ exit(result[:ok] ? 0 : 1)
17
+ else
18
+ warn "Usage: amlexia [health|version]"
19
+ exit 1
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amlexia
4
+ module Anthropic
5
+ module_function
6
+
7
+ def track_anthropic_message(client, model:, status_code:, latency_ms:, usage: {}, cost_usd: nil, **kwargs)
8
+ event = Cost.enrich_event(
9
+ cost_usd: cost_usd,
10
+ model_name: model,
11
+ provider: "anthropic",
12
+ tokens_input: usage[:input_tokens],
13
+ tokens_output: usage[:output_tokens]
14
+ )
15
+ client.track(
16
+ endpoint: "/v1/messages",
17
+ method: "POST",
18
+ status_code: status_code,
19
+ latency_ms: latency_ms,
20
+ provider: "anthropic",
21
+ model_name: model,
22
+ tokens_input: usage[:input_tokens],
23
+ tokens_output: usage[:output_tokens],
24
+ cost_usd: event[:cost_usd],
25
+ **kwargs
26
+ )
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require "thread"
7
+
8
+ module Amlexia
9
+ class Client
10
+ DEFAULT_INGEST_URL = "https://ingest.amlexia.com"
11
+ DEFAULT_FLUSH_SECONDS = 5
12
+ DEFAULT_BATCH_SIZE = 50
13
+ DEFAULT_MAX_RETRIES = 5
14
+
15
+ attr_reader :diagnostic
16
+
17
+ def initialize(
18
+ sdk_key:,
19
+ ingest_url: DEFAULT_INGEST_URL,
20
+ flush_interval_seconds: DEFAULT_FLUSH_SECONDS,
21
+ max_batch_size: DEFAULT_BATCH_SIZE,
22
+ max_retries: DEFAULT_MAX_RETRIES,
23
+ environment: nil,
24
+ release_version: nil,
25
+ default_session_id: nil,
26
+ sample_rate: 1.0,
27
+ diagnostic: false
28
+ )
29
+ @sdk_key = sdk_key
30
+ @ingest_url = ingest_url.chomp("/")
31
+ @flush_interval = flush_interval_seconds
32
+ @max_batch_size = max_batch_size
33
+ @max_retries = max_retries
34
+ @environment = environment
35
+ @release_version = release_version
36
+ @default_session_id = default_session_id
37
+ @sample_rate = sample_rate
38
+ @diagnostic = DiagnosticState.new(enabled: diagnostic, events_buffered: 0)
39
+ @buffer = []
40
+ @mutex = Mutex.new
41
+ @flushing = false
42
+ @stop = false
43
+ @flush_thread = Thread.new { flush_loop }
44
+ end
45
+
46
+ def self.from_env
47
+ new(
48
+ sdk_key: ENV.fetch("AMLEXIA_SDK_KEY"),
49
+ ingest_url: ENV.fetch("AMLEXIA_INGEST_URL", DEFAULT_INGEST_URL),
50
+ environment: ENV["AMLEXIA_ENVIRONMENT"],
51
+ release_version: ENV["AMLEXIA_RELEASE"]
52
+ )
53
+ end
54
+
55
+ def track(**event)
56
+ return unless Sampling.should_sample(@sample_rate)
57
+
58
+ event = Cost.enrich_event(event)
59
+ event[:timestamp] ||= Time.now.to_i
60
+ event[:environment] ||= @environment
61
+ event[:release_version] ||= @release_version
62
+ event[:session_id] ||= @default_session_id
63
+ event[:method] = event[:method].to_s.upcase
64
+
65
+ should_flush = false
66
+ @mutex.synchronize do
67
+ @buffer << event
68
+ should_flush = @buffer.length >= @max_batch_size
69
+ end
70
+ flush if should_flush
71
+ end
72
+
73
+ def get_diagnostic_snapshot
74
+ @mutex.synchronize do
75
+ @diagnostic.events_buffered = @buffer.length
76
+ DiagnosticState.new(
77
+ enabled: @diagnostic.enabled,
78
+ events_buffered: @diagnostic.events_buffered,
79
+ last_flush_at: @diagnostic.last_flush_at,
80
+ last_error: @diagnostic.last_error
81
+ )
82
+ end
83
+ end
84
+
85
+ def flush
86
+ events = nil
87
+ @mutex.synchronize do
88
+ return if @flushing || @buffer.empty?
89
+
90
+ @flushing = true
91
+ events = @buffer.shift(@max_batch_size)
92
+ end
93
+ post_events(events)
94
+ ensure
95
+ @mutex.synchronize { @flushing = false }
96
+ end
97
+
98
+ def shutdown
99
+ @stop = true
100
+ @flush_thread.join
101
+ flush until @mutex.synchronize { @buffer.empty? }
102
+ end
103
+
104
+ private
105
+
106
+ def flush_loop
107
+ until @stop
108
+ sleep @flush_interval
109
+ flush
110
+ rescue StandardError
111
+ nil
112
+ end
113
+ end
114
+
115
+ def post_events(events)
116
+ post("/v1/events", { sdk_key: @sdk_key, events: events })
117
+ @diagnostic.last_flush_at = (Time.now.to_f * 1000).to_i
118
+ @diagnostic.last_error = nil
119
+ warn "[amlexia] flushed #{events.length} events" if @diagnostic.enabled
120
+ rescue StandardError => e
121
+ @diagnostic.last_error = e.message
122
+ warn "[amlexia] flush failed: #{e.message}" if @diagnostic.enabled
123
+ @mutex.synchronize { @buffer = events + @buffer }
124
+ raise
125
+ end
126
+
127
+ def post(path, body)
128
+ uri = URI("#{@ingest_url}#{path}")
129
+ data = JSON.generate(body)
130
+ attempt = 0
131
+ last_error = nil
132
+ while attempt < @max_retries
133
+ begin
134
+ http = Net::HTTP.new(uri.host, uri.port)
135
+ http.use_ssl = uri.scheme == "https"
136
+ req = Net::HTTP::Post.new(uri)
137
+ req["Content-Type"] = "application/json"
138
+ req.body = data
139
+ res = http.request(req)
140
+ return true if res.is_a?(Net::HTTPSuccess)
141
+ raise "Invalid SDK key" if res.code.to_i == 401
142
+ raise "Usage limit exceeded" if res.code.to_i == 402
143
+ raise "Ingest failed: #{res.code} #{res.body}" if res.code.to_i >= 400 && res.code.to_i < 500
144
+
145
+ last_error = "HTTP #{res.code}"
146
+ rescue StandardError => e
147
+ last_error = e.message
148
+ raise e if attempt >= @max_retries - 1
149
+ end
150
+ sleep([2**attempt, 30].min)
151
+ attempt += 1
152
+ end
153
+ raise last_error || "Failed to send events after retries"
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amlexia
4
+ MODEL_PRICES = {
5
+ "gpt-4o-mini" => { input: 0.15, output: 0.6 },
6
+ "gpt-4o" => { input: 2.5, output: 10 },
7
+ "claude-3-5-haiku" => { input: 0.8, output: 4 },
8
+ "claude-3-5-sonnet" => { input: 3, output: 15 },
9
+ "gemini-2.0-flash" => { input: 0.1, output: 0.4 }
10
+ }.freeze
11
+
12
+ PROVIDER_DEFAULT_MODEL = {
13
+ "openai" => "gpt-4o-mini",
14
+ "anthropic" => "claude-3-5-haiku",
15
+ "google" => "gemini-2.0-flash"
16
+ }.freeze
17
+
18
+ module Cost
19
+ module_function
20
+
21
+ def estimate_cost_usd(cost_usd:, model_name: nil, provider: nil, tokens_input: nil, tokens_output: nil, total_tokens: nil)
22
+ return [cost_usd, "reported"] if cost_usd && cost_usd.positive?
23
+
24
+ key = (model_name || "").downcase
25
+ price = MODEL_PRICES[key]
26
+ price ||= MODEL_PRICES[PROVIDER_DEFAULT_MODEL[(provider || "").downcase]] if provider
27
+ return [cost_usd, nil] unless price
28
+
29
+ inp = tokens_input || 0
30
+ out = tokens_output || 0
31
+ if inp.zero? && out.zero?
32
+ return [cost_usd, nil] unless total_tokens&.positive?
33
+
34
+ blended = (price[:input] + price[:output]) / 2.0
35
+ return [(total_tokens / 1_000_000.0) * blended, "estimated"]
36
+ end
37
+
38
+ est = (inp / 1_000_000.0) * price[:input] + (out / 1_000_000.0) * price[:output]
39
+ [est.round(8), "estimated"]
40
+ end
41
+
42
+ def enrich_event(event)
43
+ cost, = estimate_cost_usd(
44
+ cost_usd: event[:cost_usd],
45
+ model_name: event[:model_name],
46
+ provider: event[:provider] || event[:provider_name],
47
+ tokens_input: event[:tokens_input],
48
+ tokens_output: event[:tokens_output],
49
+ total_tokens: event[:total_tokens]
50
+ )
51
+ event[:cost_usd] = cost if cost
52
+ event
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amlexia
4
+ DiagnosticState = Struct.new(:enabled, :events_buffered, :last_flush_at, :last_error, keyword_init: true)
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+
6
+ module Amlexia
7
+ module Health
8
+ module_function
9
+
10
+ def check_ingest_health(ingest_url: Client::DEFAULT_INGEST_URL, timeout: 5)
11
+ uri = URI("#{ingest_url.to_s.chomp('/')}/health")
12
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
13
+ http = Net::HTTP.new(uri.host, uri.port)
14
+ http.use_ssl = uri.scheme == "https"
15
+ http.open_timeout = timeout
16
+ http.read_timeout = timeout
17
+ res = http.get(uri.path)
18
+ latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).to_i
19
+ { ok: res.is_a?(Net::HTTPSuccess), status: res.code.to_i, latency_ms: latency }
20
+ rescue StandardError
21
+ latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).to_i
22
+ { ok: false, status: 0, latency_ms: latency }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+
6
+ module Amlexia
7
+ module HttpWrap
8
+ module_function
9
+
10
+ def provider_from_url(url)
11
+ return "openai" if url.include?("openai")
12
+ return "anthropic" if url.include?("anthropic")
13
+ return "stripe" if url.include?("stripe")
14
+
15
+ nil
16
+ end
17
+
18
+ def wrap_net_http(client)
19
+ Net::HTTP.class_eval do
20
+ alias_method :amlexia_original_request, :request unless method_defined?(:amlexia_original_request)
21
+
22
+ define_method(:request) do |req, body = nil, &block|
23
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
+ status = 500
25
+ err = nil
26
+ begin
27
+ res = amlexia_original_request(req, body, &block)
28
+ status = res.code.to_i
29
+ res
30
+ rescue StandardError => e
31
+ err = e.message
32
+ raise
33
+ ensure
34
+ latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).to_i
35
+ client.track(
36
+ endpoint: req.uri.to_s,
37
+ method: req.method,
38
+ status_code: status,
39
+ latency_ms: latency,
40
+ provider: Amlexia::HttpWrap.provider_from_url(req.uri.to_s),
41
+ error_message: err
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def track_http_call(client, **event)
49
+ client.track(**event)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amlexia
4
+ module OpenAI
5
+ module_function
6
+
7
+ def track_openai_completion(client, model:, status_code:, latency_ms:, usage: {}, cost_usd: nil, endpoint: "/v1/chat/completions", **kwargs)
8
+ event = Cost.enrich_event(
9
+ cost_usd: cost_usd,
10
+ model_name: model,
11
+ provider: "openai",
12
+ tokens_input: usage[:prompt_tokens],
13
+ tokens_output: usage[:completion_tokens],
14
+ total_tokens: usage[:total_tokens]
15
+ )
16
+ client.track(
17
+ endpoint: endpoint,
18
+ method: "POST",
19
+ status_code: status_code,
20
+ latency_ms: latency_ms,
21
+ provider: "openai",
22
+ model_name: model,
23
+ tokens_input: usage[:prompt_tokens],
24
+ tokens_output: usage[:completion_tokens],
25
+ cost_usd: event[:cost_usd],
26
+ **kwargs
27
+ )
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amlexia
4
+ module Sampling
5
+ module_function
6
+
7
+ def should_sample(rate)
8
+ return true if rate >= 1.0
9
+ return false if rate <= 0.0
10
+
11
+ rand < rate
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Amlexia
6
+ TraceContext = Struct.new(
7
+ :trace_id, :span_id, :parent_span_id, :session_id, :user_id, :environment, :release_version,
8
+ keyword_init: true
9
+ )
10
+
11
+ module Tracing
12
+ module_function
13
+
14
+ def create_trace_context(**kwargs)
15
+ TraceContext.new(
16
+ trace_id: kwargs[:trace_id] || SecureRandom.hex(16),
17
+ span_id: kwargs[:span_id] || SecureRandom.hex(8),
18
+ parent_span_id: kwargs[:parent_span_id],
19
+ session_id: kwargs[:session_id],
20
+ user_id: kwargs[:user_id],
21
+ environment: kwargs[:environment],
22
+ release_version: kwargs[:release_version]
23
+ )
24
+ end
25
+
26
+ def child_span(parent)
27
+ TraceContext.new(
28
+ trace_id: parent.trace_id,
29
+ span_id: SecureRandom.hex(8),
30
+ parent_span_id: parent.span_id,
31
+ session_id: parent.session_id,
32
+ user_id: parent.user_id,
33
+ environment: parent.environment,
34
+ release_version: parent.release_version
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amlexia
4
+ VERSION = "1.0.2"
5
+ end
data/lib/amlexia.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "amlexia/version"
4
+ require_relative "amlexia/cost"
5
+ require_relative "amlexia/sampling"
6
+ require_relative "amlexia/diagnostic"
7
+ require_relative "amlexia/health"
8
+ require_relative "amlexia/tracing"
9
+ require_relative "amlexia/http_wrap"
10
+ require_relative "amlexia/openai"
11
+ require_relative "amlexia/anthropic"
12
+ require_relative "amlexia/client"
13
+
14
+ module Amlexia
15
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amlexia
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Amlexia
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-05-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Track HTTP APIs, LLM providers, payments, and webhooks with traces, latency,
14
+ and cost fields.
15
+ email: support@amlexia.com
16
+ executables:
17
+ - amlexia
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - amlexia.gemspec
24
+ - exe/amlexia
25
+ - lib/amlexia.rb
26
+ - lib/amlexia/anthropic.rb
27
+ - lib/amlexia/client.rb
28
+ - lib/amlexia/cost.rb
29
+ - lib/amlexia/diagnostic.rb
30
+ - lib/amlexia/health.rb
31
+ - lib/amlexia/http_wrap.rb
32
+ - lib/amlexia/openai.rb
33
+ - lib/amlexia/sampling.rb
34
+ - lib/amlexia/tracing.rb
35
+ - lib/amlexia/version.rb
36
+ homepage: https://amlexia.com
37
+ licenses:
38
+ - Nonstandard
39
+ metadata:
40
+ source_code_uri: https://github.com/Amlexia/Amlexia
41
+ documentation_uri: https://docs.amlexia.com/sdk/ruby
42
+ changelog_uri: https://docs.amlexia.com/changelog
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '3.0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.22
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Official Amlexia Ruby SDK — API, AI, and infrastructure observability
62
+ test_files: []