llm_logs 0.4.1 → 0.5.1
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/README.md +6 -0
- data/app/jobs/llm_logs/batch/flush_job.rb +1 -0
- data/app/jobs/llm_logs/batch/poll_job.rb +1 -0
- data/app/models/llm_logs/batch/reconciler.rb +1 -0
- data/app/models/llm_logs/batch/submitter.rb +1 -0
- data/app/models/llm_logs/batch.rb +12 -1
- data/app/models/llm_logs/prompt.rb +11 -2
- data/app/views/llm_logs/spans/show.html.erb +3 -3
- data/app/views/llm_logs/traces/index.html.erb +3 -3
- data/app/views/llm_logs/traces/show.html.erb +5 -3
- data/db/migrate/001_create_llm_logs_traces.rb +1 -1
- data/db/migrate/002_create_llm_logs_spans.rb +3 -3
- data/db/migrate/004_create_llm_logs_prompt_versions.rb +3 -3
- data/db/migrate/006_add_tags_to_prompts.rb +6 -2
- data/db/migrate/007_create_llm_logs_batches.rb +1 -1
- data/db/migrate/008_create_llm_logs_batch_requests.rb +2 -2
- data/db/migrate/009_add_provider_columns_to_llm_logs_batches.rb +1 -1
- data/lib/llm_logs/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a687cddb64dc51d51cdf1bfa833833523b54194054ae32fe631354f187c9f256
|
|
4
|
+
data.tar.gz: ec0375795a8d27afa1266c1a8ebacd0984395fd8d41753c6ebd4c2658cfef026
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe87b0ed824568ba3c1104e600fdc670a43919c5b061159a7e1e94bffe59257272b74914fd83256633236fd02da3b1ae99efd240ea8a6e645c93880e07d3a7af
|
|
7
|
+
data.tar.gz: 81986848eec310e3c2360ceecfbe894a58fc5836adc1a5a6832c90a2c60abbc72898521c6a7b44cf9a1aa118c0837aea861d2e919aeb4fd33b5d83c251e6b22d
|
data/README.md
CHANGED
|
@@ -349,3 +349,9 @@ end
|
|
|
349
349
|
## License
|
|
350
350
|
|
|
351
351
|
MIT
|
|
352
|
+
|
|
353
|
+
## Database support
|
|
354
|
+
|
|
355
|
+
Prompts, prompt versions, tag filters, traces, spans, and their UI support PostgreSQL and SQLite. PostgreSQL retains JSONB and array tags; SQLite uses JSON columns and `json_each` for exact tag matching. Provider batch execution requires PostgreSQL; direct calls and jobs raise `LlmLogs::Batch::UnsupportedAdapter` on SQLite. `LlmLogs::Batch.supported_adapter?` reports the capability.
|
|
356
|
+
|
|
357
|
+
Run the shared SQLite tests with `LLM_LOGS_DATABASE=sqlite bundle exec rspec`; PostgreSQL remains the default. SQLite migrations run from the gem's install chain before tests. `script/verify_migrations.rb` verifies install, rollback, and schema reload **only on a disposable database** with `LLM_LOGS_DISPOSABLE_DATABASE=1` and an explicit `DATABASE_URL`.
|
|
@@ -4,6 +4,7 @@ module LlmLogs
|
|
|
4
4
|
queue_as :default
|
|
5
5
|
|
|
6
6
|
def perform(purpose)
|
|
7
|
+
LlmLogs::Batch.require_supported_adapter!
|
|
7
8
|
models = LlmLogs::BatchRequest.pending.where(purpose: purpose).distinct.pluck(:model)
|
|
8
9
|
models.each { |model| LlmLogs::Batch.submit_pending(purpose: purpose, model: model) }
|
|
9
10
|
end
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
module LlmLogs
|
|
2
2
|
class Batch < ApplicationRecord
|
|
3
|
+
class UnsupportedAdapter < StandardError; end
|
|
4
|
+
|
|
5
|
+
def self.supported_adapter?
|
|
6
|
+
connection.adapter_name == "PostgreSQL"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.require_supported_adapter!
|
|
10
|
+
raise UnsupportedAdapter, "Provider batch execution requires PostgreSQL" unless supported_adapter?
|
|
11
|
+
end
|
|
12
|
+
|
|
3
13
|
self.table_name = "llm_logs_batches"
|
|
4
14
|
|
|
5
15
|
has_many :requests, class_name: "LlmLogs::BatchRequest", dependent: :destroy
|
|
@@ -19,6 +29,7 @@ module LlmLogs
|
|
|
19
29
|
scope :unreconciled, -> { where.not(status: %i[reconciled failed expired]) }
|
|
20
30
|
|
|
21
31
|
def self.enqueue(purpose:, model:, input:, instructions:, schema:, routing:, temperature: nil, reasoning_effort: nil)
|
|
32
|
+
require_supported_adapter!
|
|
22
33
|
BatchRequest.create!(
|
|
23
34
|
purpose: purpose,
|
|
24
35
|
model: model,
|
|
@@ -50,7 +61,7 @@ module LlmLogs
|
|
|
50
61
|
end
|
|
51
62
|
|
|
52
63
|
def self.batchable?(model)
|
|
53
|
-
return false unless LlmLogs.batch_enabled?
|
|
64
|
+
return false unless supported_adapter? && LlmLogs.batch_enabled?
|
|
54
65
|
|
|
55
66
|
!batch_provider_for(model).nil?
|
|
56
67
|
end
|
|
@@ -5,8 +5,17 @@ module LlmLogs
|
|
|
5
5
|
validates :slug, presence: true, uniqueness: true
|
|
6
6
|
validates :name, presence: true
|
|
7
7
|
|
|
8
|
-
scope :with_tag,
|
|
9
|
-
scope :with_any_tag,
|
|
8
|
+
scope :with_tag, ->(tag) { with_any_tag([tag]) }
|
|
9
|
+
scope :with_any_tag, ->(tags) {
|
|
10
|
+
values = Array(tags)
|
|
11
|
+
if values.empty?
|
|
12
|
+
none
|
|
13
|
+
elsif connection.adapter_name == "PostgreSQL"
|
|
14
|
+
where("tags && ARRAY[?]::varchar[]", values)
|
|
15
|
+
else
|
|
16
|
+
where("EXISTS (SELECT 1 FROM json_each(llm_logs_prompts.tags) AS tag WHERE tag.value COLLATE BINARY IN (?))", values)
|
|
17
|
+
end
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
def tags_string
|
|
12
21
|
Array(tags).join(", ")
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
</div>
|
|
25
25
|
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
26
26
|
<dt class="text-xs font-medium text-gray-500 uppercase">Input Tokens</dt>
|
|
27
|
-
<dd class="text-lg font-semibold text-gray-900 mt-1"><%= number_with_delimiter(@span.input_tokens
|
|
27
|
+
<dd class="text-lg font-semibold text-gray-900 mt-1"><%= @span.input_tokens.nil? ? "Unavailable" : number_with_delimiter(@span.input_tokens) %></dd>
|
|
28
28
|
</div>
|
|
29
29
|
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
30
30
|
<dt class="text-xs font-medium text-gray-500 uppercase">Output Tokens</dt>
|
|
31
|
-
<dd class="text-lg font-semibold text-gray-900 mt-1"><%= number_with_delimiter(@span.output_tokens
|
|
31
|
+
<dd class="text-lg font-semibold text-gray-900 mt-1"><%= @span.output_tokens.nil? ? "Unavailable" : number_with_delimiter(@span.output_tokens) %></dd>
|
|
32
32
|
</div>
|
|
33
33
|
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
34
34
|
<dt class="text-xs font-medium text-gray-500 uppercase">Cached Tokens</dt>
|
|
35
|
-
<dd class="text-lg font-semibold text-green-600 mt-1"><%= number_with_delimiter(@span.cached_tokens
|
|
35
|
+
<dd class="text-lg font-semibold text-green-600 mt-1"><%= @span.cached_tokens.nil? ? "Unavailable" : number_with_delimiter(@span.cached_tokens) %></dd>
|
|
36
36
|
<% if (@span.input_tokens || 0) > 0 && (@span.cached_tokens || 0) > 0 %>
|
|
37
37
|
<dd class="text-xs text-green-600 mt-0.5"><%= (@span.cached_tokens.to_f / @span.input_tokens * 100).round %>% of input</dd>
|
|
38
38
|
<% end %>
|
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
</td>
|
|
51
51
|
<td class="px-4 py-3 text-sm text-gray-500 text-right"><%= trace.spans_count %></td>
|
|
52
52
|
<td class="px-4 py-3 text-sm text-gray-500 text-right">
|
|
53
|
-
<%= number_with_delimiter(trace.total_input_tokens) %> → <%= number_with_delimiter(trace.total_output_tokens) %>
|
|
53
|
+
<%= trace.metadata&.dig("usage_missing") ? "Usage unavailable" : number_with_delimiter(trace.total_input_tokens) %> → <%= trace.metadata&.dig("usage_missing") ? "Usage unavailable" : number_with_delimiter(trace.total_output_tokens) %>
|
|
54
54
|
<% if trace.total_cached_tokens > 0 %>
|
|
55
|
-
<br><span class="text-xs text-green-600"><%= number_with_delimiter(trace.total_cached_tokens) %> cached</span>
|
|
55
|
+
<br><span class="text-xs text-green-600"><%= trace.metadata&.dig("usage_missing") ? "Usage unavailable" : number_with_delimiter(trace.total_cached_tokens) %> cached</span>
|
|
56
56
|
<% end %>
|
|
57
57
|
</td>
|
|
58
58
|
<td class="px-4 py-3 text-sm text-gray-500 text-right">
|
|
59
|
-
<% if trace.total_cost > 0 %>$<%= sprintf('%.4f', trace.total_cost) %><% end %>
|
|
59
|
+
<% if trace.metadata&.dig('billing_mode') == 'subscription' %>Subscription · cost unavailable<% elsif trace.total_cost > 0 %>$<%= sprintf('%.4f', trace.total_cost) %><% end %>
|
|
60
60
|
</td>
|
|
61
61
|
<td class="px-4 py-3 text-sm text-gray-500 text-right">
|
|
62
62
|
<%= format_duration_ms(trace.duration_ms) %>
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
</div>
|
|
25
25
|
|
|
26
|
+
<% if @trace.metadata&.dig("billing_mode") == "subscription" %><p class="text-sm text-gray-500 mb-4">Subscription · cost unavailable</p><% end %>
|
|
27
|
+
|
|
26
28
|
<div class="grid grid-cols-6 gap-4 mb-6">
|
|
27
29
|
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
28
30
|
<dt class="text-xs font-medium text-gray-500 uppercase">Model</dt>
|
|
@@ -34,15 +36,15 @@
|
|
|
34
36
|
</div>
|
|
35
37
|
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
36
38
|
<dt class="text-xs font-medium text-gray-500 uppercase">Input Tokens</dt>
|
|
37
|
-
<dd class="text-2xl font-semibold text-gray-900 mt-1"><%= number_with_delimiter(@trace.total_input_tokens) %></dd>
|
|
39
|
+
<dd class="text-2xl font-semibold text-gray-900 mt-1"><%= @trace.metadata&.dig("usage_missing") ? "Usage unavailable" : number_with_delimiter(@trace.total_input_tokens) %></dd>
|
|
38
40
|
</div>
|
|
39
41
|
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
40
42
|
<dt class="text-xs font-medium text-gray-500 uppercase">Output Tokens</dt>
|
|
41
|
-
<dd class="text-2xl font-semibold text-gray-900 mt-1"><%= number_with_delimiter(@trace.total_output_tokens) %></dd>
|
|
43
|
+
<dd class="text-2xl font-semibold text-gray-900 mt-1"><%= @trace.metadata&.dig("usage_missing") ? "Usage unavailable" : number_with_delimiter(@trace.total_output_tokens) %></dd>
|
|
42
44
|
</div>
|
|
43
45
|
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
44
46
|
<dt class="text-xs font-medium text-gray-500 uppercase">Cached Tokens</dt>
|
|
45
|
-
<dd class="text-2xl font-semibold text-green-600 mt-1"><%= number_with_delimiter(@trace.total_cached_tokens) %></dd>
|
|
47
|
+
<dd class="text-2xl font-semibold text-green-600 mt-1"><%= @trace.metadata&.dig("usage_missing") ? "Usage unavailable" : number_with_delimiter(@trace.total_cached_tokens) %></dd>
|
|
46
48
|
<% if @trace.total_input_tokens > 0 && @trace.total_cached_tokens > 0 %>
|
|
47
49
|
<dd class="text-xs text-green-600 mt-0.5"><%= (@trace.total_cached_tokens.to_f / @trace.total_input_tokens * 100).round %>% of input</dd>
|
|
48
50
|
<% end %>
|
|
@@ -3,7 +3,7 @@ class CreateLlmLogsTraces < ActiveRecord::Migration[8.0]
|
|
|
3
3
|
create_table :llm_logs_traces do |t|
|
|
4
4
|
t.string :name, null: false
|
|
5
5
|
t.string :status, null: false, default: "running"
|
|
6
|
-
t.
|
|
6
|
+
t.column :metadata, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), default: {}
|
|
7
7
|
t.integer :total_input_tokens, default: 0
|
|
8
8
|
t.integer :total_output_tokens, default: 0
|
|
9
9
|
t.integer :total_cached_tokens, default: 0, null: false
|
|
@@ -7,8 +7,8 @@ class CreateLlmLogsSpans < ActiveRecord::Migration[8.0]
|
|
|
7
7
|
t.string :span_type, null: false
|
|
8
8
|
t.string :model
|
|
9
9
|
t.string :provider
|
|
10
|
-
t.jsonb :
|
|
11
|
-
t.jsonb :
|
|
10
|
+
t.column :input, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json)
|
|
11
|
+
t.column :output, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json)
|
|
12
12
|
t.integer :input_tokens
|
|
13
13
|
t.integer :output_tokens
|
|
14
14
|
t.integer :cached_tokens
|
|
@@ -16,7 +16,7 @@ class CreateLlmLogsSpans < ActiveRecord::Migration[8.0]
|
|
|
16
16
|
t.float :duration_ms
|
|
17
17
|
t.string :status, null: false, default: "ok"
|
|
18
18
|
t.text :error_message
|
|
19
|
-
t.
|
|
19
|
+
t.column :metadata, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), default: {}
|
|
20
20
|
t.datetime :started_at, null: false
|
|
21
21
|
t.datetime :completed_at
|
|
22
22
|
|
|
@@ -3,10 +3,10 @@ class CreateLlmLogsPromptVersions < ActiveRecord::Migration[8.0]
|
|
|
3
3
|
create_table :llm_logs_prompt_versions do |t|
|
|
4
4
|
t.references :prompt, null: false, foreign_key: { to_table: :llm_logs_prompts }
|
|
5
5
|
t.integer :version_number, null: false
|
|
6
|
-
t.
|
|
6
|
+
t.column :messages, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), null: false, default: []
|
|
7
7
|
t.string :model
|
|
8
|
-
t.
|
|
9
|
-
t.
|
|
8
|
+
t.column :model_params, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), default: {}
|
|
9
|
+
t.column :default_variables, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), default: {}
|
|
10
10
|
t.text :changelog
|
|
11
11
|
|
|
12
12
|
t.timestamps
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
class AddTagsToPrompts < ActiveRecord::Migration[8.0]
|
|
2
2
|
def change
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
if connection.adapter_name == "PostgreSQL"
|
|
4
|
+
add_column :llm_logs_prompts, :tags, :string, array: true, default: [], null: false
|
|
5
|
+
add_index :llm_logs_prompts, :tags, using: :gin, name: "index_llm_logs_prompts_on_tags"
|
|
6
|
+
else
|
|
7
|
+
add_column :llm_logs_prompts, :tags, :json, default: [], null: false
|
|
8
|
+
end
|
|
5
9
|
end
|
|
6
10
|
end
|
|
@@ -9,7 +9,7 @@ class CreateLlmLogsBatches < ActiveRecord::Migration[8.0]
|
|
|
9
9
|
t.string :openai_error_file_id
|
|
10
10
|
t.string :status, null: false, default: "pending"
|
|
11
11
|
t.integer :request_count, null: false, default: 0
|
|
12
|
-
t.
|
|
12
|
+
t.column :metadata, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), null: false, default: {}
|
|
13
13
|
t.datetime :submitted_at
|
|
14
14
|
t.datetime :completed_at
|
|
15
15
|
t.datetime :reconciled_at
|
|
@@ -6,8 +6,8 @@ class CreateLlmLogsBatchRequests < ActiveRecord::Migration[8.0]
|
|
|
6
6
|
t.string :purpose, null: false
|
|
7
7
|
t.string :status, null: false, default: "pending"
|
|
8
8
|
t.string :model, null: false
|
|
9
|
-
t.
|
|
10
|
-
t.
|
|
9
|
+
t.column :payload, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), null: false, default: {}
|
|
10
|
+
t.column :routing, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), null: false, default: {}
|
|
11
11
|
t.text :result_content
|
|
12
12
|
t.integer :input_tokens
|
|
13
13
|
t.integer :output_tokens
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class AddProviderColumnsToLlmLogsBatches < ActiveRecord::Migration[8.0]
|
|
2
2
|
def change
|
|
3
3
|
add_column :llm_logs_batches, :provider_batch_id, :string
|
|
4
|
-
add_column :llm_logs_batches, :provider_metadata, :jsonb, null: false, default: {}
|
|
4
|
+
add_column :llm_logs_batches, :provider_metadata, (connection.adapter_name == "PostgreSQL" ? :jsonb : :json), null: false, default: {}
|
|
5
5
|
add_index :llm_logs_batches, :provider_batch_id
|
|
6
6
|
|
|
7
7
|
# Backfill existing OpenAI rows so the reconciler/poll job can key off the
|
data/lib/llm_logs/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm_logs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anton
|
|
@@ -261,7 +261,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
261
261
|
- !ruby/object:Gem::Version
|
|
262
262
|
version: '0'
|
|
263
263
|
requirements: []
|
|
264
|
-
rubygems_version:
|
|
264
|
+
rubygems_version: 4.0.20
|
|
265
265
|
specification_version: 4
|
|
266
266
|
summary: Rails engine for LLM logging and prompt management
|
|
267
267
|
test_files: []
|