llm_logs 0.4.1 → 0.6.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -16
  3. data/app/jobs/llm_logs/batch/flush_job.rb +1 -0
  4. data/app/jobs/llm_logs/batch/poll_job.rb +1 -0
  5. data/app/models/llm_logs/batch/reconciler.rb +1 -0
  6. data/app/models/llm_logs/batch/submitter.rb +9 -7
  7. data/app/models/llm_logs/batch/trace_recorder.rb +19 -7
  8. data/app/models/llm_logs/batch.rb +15 -28
  9. data/app/models/llm_logs/prompt.rb +11 -2
  10. data/app/models/llm_logs/span.rb +15 -3
  11. data/app/views/llm_logs/spans/show.html.erb +3 -3
  12. data/app/views/llm_logs/traces/index.html.erb +3 -3
  13. data/app/views/llm_logs/traces/show.html.erb +5 -3
  14. data/db/migrate/001_create_llm_logs_traces.rb +1 -1
  15. data/db/migrate/002_create_llm_logs_spans.rb +3 -3
  16. data/db/migrate/004_create_llm_logs_prompt_versions.rb +3 -3
  17. data/db/migrate/006_add_tags_to_prompts.rb +6 -2
  18. data/db/migrate/007_create_llm_logs_batches.rb +1 -1
  19. data/db/migrate/008_create_llm_logs_batch_requests.rb +2 -2
  20. data/db/migrate/009_add_provider_columns_to_llm_logs_batches.rb +1 -1
  21. data/lib/llm_logs/configuration.rb +1 -2
  22. data/lib/llm_logs/engine.rb +10 -1
  23. data/lib/llm_logs/instrumentation/ruby_llm_chat.rb +190 -82
  24. data/lib/llm_logs/ruby_llm_patches/bedrock_sigv4.rb +75 -0
  25. data/lib/llm_logs/ruby_llm_patches/converse_claude_adaptive_thinking.rb +70 -0
  26. data/lib/llm_logs/ruby_llm_patches/converse_foreign_reasoning.rb +80 -0
  27. data/lib/llm_logs/ruby_llm_patches/converse_reasoning_config.rb +78 -0
  28. data/lib/llm_logs/ruby_llm_patches/retry_ssl_error.rb +25 -0
  29. data/lib/llm_logs/ruby_llm_patches.rb +54 -0
  30. data/lib/llm_logs/version.rb +1 -1
  31. data/lib/llm_logs.rb +1 -5
  32. metadata +23 -13
  33. data/app/models/llm_logs/batch/adapters/openai_responses.rb +0 -63
  34. data/app/models/llm_logs/batch/schema_format.rb +0 -25
@@ -0,0 +1,54 @@
1
+ require "llm_logs/ruby_llm_patches/bedrock_sigv4"
2
+ require "llm_logs/ruby_llm_patches/converse_reasoning_config"
3
+ require "llm_logs/ruby_llm_patches/converse_foreign_reasoning"
4
+ require "llm_logs/ruby_llm_patches/converse_claude_adaptive_thinking"
5
+ require "llm_logs/ruby_llm_patches/retry_ssl_error"
6
+
7
+ module LlmLogs
8
+ # Bedrock fixes that upstream ruby_llm 2.0.0 lacks, applied with Module#prepend. Each
9
+ # backports an open upstream PR (crmne/ruby_llm#1024, #1025, #1026; see the README), so it
10
+ # can be dropped once a ruby_llm release includes that PR.
11
+ # Each was verified against 2.0.x only: on 1.x they are skipped (different internals),
12
+ # on a newer ruby_llm they still install but log a warning so we re-check whether
13
+ # upstream fixed the bug (then drop the patch) or moved the method (then the patch
14
+ # skips itself and logs why).
15
+ module RubyLLMPatches
16
+ PATCHES = [
17
+ BedrockSigV4, ConverseReasoningConfig, ConverseForeignReasoning, ConverseClaudeAdaptiveThinking, RetrySSLError
18
+ ].freeze
19
+ MINIMUM = Gem::Version.new("2.0.0")
20
+ VERIFIED = Gem::Requirement.new(">= 2.0.0", "< 2.1")
21
+
22
+ module_function
23
+
24
+ def install!
25
+ version = Gem::Version.new(RubyLLM::VERSION)
26
+ return [] if version < MINIMUM
27
+
28
+ unless VERIFIED.satisfied_by?(version)
29
+ warn_log("verified against ruby_llm #{VERIFIED}, running #{version}: check whether upstream " \
30
+ "now fixes #{PATCHES.map(&:name).join(', ')} and drop the patches it does")
31
+ end
32
+ PATCHES.select(&:install!)
33
+ end
34
+
35
+ def warn_log(message)
36
+ logger = defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
37
+ logger ? logger.warn("[llm_logs] #{message}") : Kernel.warn("[llm_logs] #{message}")
38
+ end
39
+
40
+ # Prepends +patch+ to +target+ when +target+ still defines every method in +methods+.
41
+ def prepend_checked(target, patch, methods)
42
+ return true if target <= patch
43
+
44
+ missing = methods.reject { |name| target.method_defined?(name) || target.private_method_defined?(name) }
45
+ if missing.any?
46
+ warn_log("#{patch.name} not installed: #{target} no longer defines #{missing.join(', ')}")
47
+ return false
48
+ end
49
+
50
+ target.prepend(patch)
51
+ true
52
+ end
53
+ end
54
+ end
@@ -1,3 +1,3 @@
1
1
  module LlmLogs
2
- VERSION = "0.4.1"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/llm_logs.rb CHANGED
@@ -54,10 +54,6 @@ module LlmLogs
54
54
  configuration.batch_enabled
55
55
  end
56
56
 
57
- def self.batch_provider
58
- configuration.batch_provider
59
- end
60
-
61
57
  def self.bedrock_batch
62
58
  configuration.bedrock_batch
63
59
  end
@@ -71,7 +67,7 @@ module LlmLogs
71
67
  end
72
68
 
73
69
  def self.batch_adapters
74
- @batch_adapters ||= {openai_responses: LlmLogs::Batch::Adapters::OpenaiResponses.new}
70
+ @batch_adapters ||= {}
75
71
  end
76
72
 
77
73
  def self.register_batch_adapter(provider, adapter)
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.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton
@@ -97,30 +97,36 @@ dependencies:
97
97
  name: ruby_llm
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
- - - "~>"
100
+ - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: '1.16'
103
- type: :development
102
+ version: '2.0'
103
+ - - "<"
104
+ - !ruby/object:Gem::Version
105
+ version: '3'
106
+ type: :runtime
104
107
  prerelease: false
105
108
  version_requirements: !ruby/object:Gem::Requirement
106
109
  requirements:
107
- - - "~>"
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '2.0'
113
+ - - "<"
108
114
  - !ruby/object:Gem::Version
109
- version: '1.16'
115
+ version: '3'
110
116
  - !ruby/object:Gem::Dependency
111
- name: ruby_llm-responses_api
117
+ name: aws-eventstream
112
118
  requirement: !ruby/object:Gem::Requirement
113
119
  requirements:
114
- - - "~>"
120
+ - - ">="
115
121
  - !ruby/object:Gem::Version
116
- version: '0.6'
122
+ version: '0'
117
123
  type: :development
118
124
  prerelease: false
119
125
  version_requirements: !ruby/object:Gem::Requirement
120
126
  requirements:
121
- - - "~>"
127
+ - - ">="
122
128
  - !ruby/object:Gem::Version
123
- version: '0.6'
129
+ version: '0'
124
130
  - !ruby/object:Gem::Dependency
125
131
  name: webmock
126
132
  requirement: !ruby/object:Gem::Requirement
@@ -186,10 +192,8 @@ files:
186
192
  - app/models/llm_logs/application_record.rb
187
193
  - app/models/llm_logs/batch.rb
188
194
  - app/models/llm_logs/batch/adapters/bedrock.rb
189
- - app/models/llm_logs/batch/adapters/openai_responses.rb
190
195
  - app/models/llm_logs/batch/handler_registry.rb
191
196
  - app/models/llm_logs/batch/reconciler.rb
192
- - app/models/llm_logs/batch/schema_format.rb
193
197
  - app/models/llm_logs/batch/submitter.rb
194
198
  - app/models/llm_logs/batch/trace_recorder.rb
195
199
  - app/models/llm_logs/batch_request.rb
@@ -237,6 +241,12 @@ files:
237
241
  - lib/llm_logs/engine.rb
238
242
  - lib/llm_logs/instrumentation/ruby_llm_chat.rb
239
243
  - lib/llm_logs/prompt_renderer.rb
244
+ - lib/llm_logs/ruby_llm_patches.rb
245
+ - lib/llm_logs/ruby_llm_patches/bedrock_sigv4.rb
246
+ - lib/llm_logs/ruby_llm_patches/converse_claude_adaptive_thinking.rb
247
+ - lib/llm_logs/ruby_llm_patches/converse_foreign_reasoning.rb
248
+ - lib/llm_logs/ruby_llm_patches/converse_reasoning_config.rb
249
+ - lib/llm_logs/ruby_llm_patches/retry_ssl_error.rb
240
250
  - lib/llm_logs/tracer.rb
241
251
  - lib/llm_logs/version.rb
242
252
  - lib/tasks/llm_logs.rake
@@ -1,63 +0,0 @@
1
- module LlmLogs
2
- class Batch
3
- module Adapters
4
- # Wraps ruby_llm-responses_api's OpenAI Batch API. Contains the remote-API logic
5
- # previously inline in Submitter/Reconciler; the DB/claim lifecycle stays in those.
6
- class OpenaiResponses
7
- PROVIDER = :openai_responses
8
-
9
- def submit(_batch, requests)
10
- rubyllm_batch = RubyLLM.batch(model: requests.first.model, provider: PROVIDER)
11
- requests.each do |request|
12
- payload = request.payload
13
- rubyllm_batch.add(
14
- payload["input"],
15
- id: request.custom_id,
16
- instructions: payload["instructions"],
17
- temperature: payload["temperature"],
18
- **reasoning_extra(payload["reasoning_effort"]),
19
- **schema_extra(payload["schema"])
20
- )
21
- end
22
- rubyllm_batch.create!
23
- {provider_batch_id: rubyllm_batch.id, openai_batch_id: rubyllm_batch.id, provider_metadata: {}}
24
- end
25
-
26
- def terminal_status(batch)
27
- resume(batch).status
28
- end
29
-
30
- def results(batch)
31
- resume(batch).results
32
- end
33
-
34
- def error_ids(batch)
35
- resume(batch).errors.filter_map { |e| e["custom_id"] }
36
- end
37
-
38
- private
39
-
40
- # The Responses API nests effort under `reasoning:`; `reasoning_effort` is the
41
- # Chat Completions spelling and is silently ignored here.
42
- def reasoning_extra(effort)
43
- return {} if effort.blank?
44
-
45
- {reasoning: {effort: effort}}
46
- end
47
-
48
- # No memoization: LlmLogs.batch_adapters holds one shared instance of this adapter
49
- # for the life of the process, so caching the resumed handle here would go stale
50
- # for a long-running PollJob worker (in-progress batches would never re-fetch a
51
- # later terminal status) and would grow unbounded. Build a fresh handle every call.
52
- def resume(batch)
53
- RubyLLM.batch(id: batch.provider_batch_id, provider: PROVIDER)
54
- end
55
-
56
- def schema_extra(schema)
57
- format = SchemaFormat.call(schema)
58
- format ? {text: format} : {}
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,25 +0,0 @@
1
- module LlmLogs
2
- class Batch
3
- # Translates a {name:, schema:, strict:} schema (the shape RubyLLM::Chat#with_schema
4
- # produces) into the OpenAI Responses API `text.format` block. The batch path builds
5
- # request bodies directly via RubyLLM.batch#add(**extra), bypassing with_schema, so
6
- # we must hand the json_schema block in ourselves.
7
- module SchemaFormat
8
- module_function
9
-
10
- def call(schema)
11
- return nil if schema.nil?
12
-
13
- schema = schema.symbolize_keys if schema.respond_to?(:symbolize_keys)
14
- {
15
- format: {
16
- type: "json_schema",
17
- name: schema[:name] || "response",
18
- schema: schema[:schema] || schema,
19
- strict: schema.key?(:strict) ? schema[:strict] : true
20
- }
21
- }
22
- end
23
- end
24
- end
25
- end