patient_llm 0.1.0 → 0.3.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 +17 -0
- data/README.md +18 -13
- data/VERSION +1 -1
- data/lib/patient_llm/callback.rb +1 -1
- data/lib/patient_llm/configuration.rb +29 -3
- data/lib/patient_llm.rb +22 -13
- data/patient_llm.gemspec +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5a3fc235def2976b4a57107239ab72e51a50a2b3abdfed579c0c0d4ea6f681d
|
|
4
|
+
data.tar.gz: 77b63683fc706598ab29e2fa710d9d6650c568712301e1ff4d87bd8a50668228
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e43ffc0679e0ab348e8462884042ba538d734101acfb5b7ecb5e4058167dc3fdd7aa50ad953a091695faae5ad1ce4c25ff1b13614c2ac553587b3568115e8f9a
|
|
7
|
+
data.tar.gz: 034554bf031c5696ddb2bf877463f9cbcf5e57e32cdc39563975de22f0ea6cde189be72cfe053ea1338144b320254196ed51589ded11ba753161e4af544de3e4
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 0.3.0
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Renamed the `completion_path` argument to `path` on both `PatientLLM.ask` and provider configuration. The `completion_path` name is still accepted as a deprecated alias and now emits a deprecation warning.
|
|
12
|
+
- The path is now joined with the base URL using `URI.join` to ensure proper handling of relative paths in the `path` argument. If the path starts with a slash, it will be treated as an absolute path and will replace the base URL's path. If it does not start with a slash, it will be treated as a relative path and will be appended to the base URL's path.
|
|
13
|
+
|
|
14
|
+
## 0.2.0
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Requires authentication headers to be set up as secrets using `PatientHttp.secret`. This is enforced and an error will be raised if plain text values are included for any of the following headers in the provider configuration: `authorization`, `x-api-key`, `x-goog-api-key`, `api-key`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Requires patient_http version 1.1 or higher.
|
|
23
|
+
|
|
7
24
|
## 0.1.0
|
|
8
25
|
|
|
9
26
|
### Added
|
data/README.md
CHANGED
|
@@ -27,25 +27,30 @@ Without a handler, `PatientLLM.ask` raises `RuntimeError: No request handler reg
|
|
|
27
27
|
|
|
28
28
|
### Configuration
|
|
29
29
|
|
|
30
|
-
Register your LLM providers with their API base URLs and authentication headers
|
|
30
|
+
Register your LLM providers with their API base URLs and authentication headers. Authentication headers must be registered using the PatientHttp secrets manager. This ensures that these values are never included in the serialized payloads in the job queue, and are only attached to the request at dispatch time.
|
|
31
31
|
|
|
32
32
|
```ruby
|
|
33
33
|
PatientLLM.configure do |config|
|
|
34
34
|
config.provider :openai,
|
|
35
35
|
url: "https://api.openai.com",
|
|
36
|
-
headers: {"
|
|
36
|
+
headers: {"authorization" => PatientHttp.secret("openai.bearer_token")}
|
|
37
37
|
|
|
38
38
|
config.provider :anthropic,
|
|
39
39
|
url: "https://api.anthropic.com",
|
|
40
|
-
headers: {"x-api-key" =>
|
|
40
|
+
headers: {"x-api-key" => PatientHttp.secret("anthropic.api_key")},
|
|
41
41
|
serializer: :messages
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
# Register the API keys as secrets with the PatientHttp secrets manager. This example
|
|
45
|
+
# is for the Sidekiq integration, but the pattern is the same for SolidQueue.
|
|
46
|
+
PatientHttp::Sidekiq.configure do |config|
|
|
47
|
+
config.register_secret("openai.bearer_token") { "Bearer #{ENV.fetch("OPENAI_API_KEY")}" }
|
|
48
|
+
config.register_secret("anthropic.api_key") { ENV.fetch("ANTHROPIC_API_KEY") }
|
|
49
|
+
end
|
|
43
50
|
```
|
|
44
51
|
|
|
45
52
|
> [!NOTE]
|
|
46
|
-
>
|
|
47
|
-
>
|
|
48
|
-
> You should set up encryption for you job payloads to prevent leaking credentials. See the documentation for [patient_http-sidekiq](https://github.com/bdurand/patient_http-sidekiq#sensitive-data-handling) or [patient_http-solid_queue](https://github.com/bdurand/patient_http-solid_queue#sensitive-data-handling) for details.
|
|
53
|
+
> You can also set up encryption for your job payloads to ensure the entire serialized payload is always encrypted in the job queue. See the documentation for [patient_http-sidekiq](https://github.com/bdurand/patient_http-sidekiq#sensitive-data-handling) or [patient_http-solid_queue](https://github.com/bdurand/patient_http-solid_queue#sensitive-data-handling) for details.
|
|
49
54
|
|
|
50
55
|
### Creating a Callback Class
|
|
51
56
|
|
|
@@ -180,9 +185,9 @@ session.max_output_tokens = 1000
|
|
|
180
185
|
PatientLLM.ask(session,
|
|
181
186
|
provider: :openai,
|
|
182
187
|
callback: LLMCallback,
|
|
183
|
-
url: "http://localhost:1234",
|
|
188
|
+
url: "http://localhost:1234", # Override the provider's base URL
|
|
184
189
|
serializer: :messages, # Override the API format
|
|
185
|
-
|
|
190
|
+
path: "/chat/completions", # Override the endpoint path
|
|
186
191
|
headers: {"X-Custom" => "value"}, # Additional HTTP headers
|
|
187
192
|
params: {max_completion_tokens: 1000} # Additional request parameters
|
|
188
193
|
)
|
|
@@ -190,24 +195,24 @@ PatientLLM.ask(session,
|
|
|
190
195
|
|
|
191
196
|
### URL composition
|
|
192
197
|
|
|
193
|
-
The full request URL is built by concatenating the base URL (from the provider registry or the `url:` option) with the `
|
|
198
|
+
The full request URL is built by concatenating the base URL (from the provider registry or the `url:` option) with the `path`. When you don't set `path`, it defaults to the path for the active serializer (`/v1/chat/completions` for `:chat_completion`, `/v1/responses` for `:open_responses`, `/v1/messages` for `:messages`, `/converse` for `:converse`, `/v1beta/models/{model}:generateContent` for `:gemini`). A `{model}` placeholder in the path is replaced with the session's model at dispatch time, which is how the Gemini default targets Google's `/v1beta/models/{model}:generateContent` endpoint. Trailing slashes on the base and leading slashes on the path are normalized, so:
|
|
194
199
|
|
|
195
200
|
```
|
|
196
|
-
url = "https://api.openai.com"
|
|
201
|
+
url = "https://api.openai.com" path = "/v1/chat/completions"
|
|
197
202
|
-> https://api.openai.com/v1/chat/completions
|
|
198
203
|
|
|
199
|
-
url = "http://localhost:1234"
|
|
204
|
+
url = "http://localhost:1234" path = "/v1/chat/completions"
|
|
200
205
|
-> http://localhost:1234/v1/chat/completions
|
|
201
206
|
```
|
|
202
207
|
|
|
203
|
-
If your base URL already includes a `/v1` prefix, override the
|
|
208
|
+
If your base URL already includes a `/v1` prefix, override the path to avoid duplication:
|
|
204
209
|
|
|
205
210
|
```ruby
|
|
206
211
|
PatientLLM.ask(session,
|
|
207
212
|
provider: :openai,
|
|
208
213
|
callback: LLMCallback,
|
|
209
214
|
url: "https://my-gateway.internal/openai/v1",
|
|
210
|
-
|
|
215
|
+
path: "chat/completions"
|
|
211
216
|
)
|
|
212
217
|
```
|
|
213
218
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
data/lib/patient_llm/callback.rb
CHANGED
|
@@ -213,7 +213,7 @@ module PatientLLM
|
|
|
213
213
|
# Restore per-request overrides
|
|
214
214
|
ask_kwargs[:url] = request_options["url"] if request_options["url"]
|
|
215
215
|
ask_kwargs[:serializer] = request_options["serializer"].to_sym if request_options["serializer"]
|
|
216
|
-
ask_kwargs[:
|
|
216
|
+
ask_kwargs[:path] = request_options["path"] if request_options["path"]
|
|
217
217
|
ask_kwargs[:headers] = request_options["headers"] if request_options["headers"]
|
|
218
218
|
ask_kwargs[:params] = request_options["params"] if request_options["params"]
|
|
219
219
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientLLM
|
|
4
|
+
# Headers that must be setup to use the secrets manager. If any of these headers
|
|
5
|
+
# are included in the provider configuration, an error will be raised unless their
|
|
6
|
+
# values are set up as secrets using `PatientHttp.secret`.
|
|
7
|
+
AUTHENTICATION_HEADERS = ["authorization", "x-api-key", "x-goog-api-key", "api-key"].freeze
|
|
8
|
+
|
|
4
9
|
# Configuration for provider registry.
|
|
5
10
|
#
|
|
6
11
|
# @example
|
|
@@ -21,20 +26,28 @@ module PatientLLM
|
|
|
21
26
|
# @param url [String] Base URL for the provider API
|
|
22
27
|
# @param headers [Hash] Default headers for requests
|
|
23
28
|
# @param serializer [Symbol] API format (:chat_completion, :open_responses, :messages, :converse, :gemini)
|
|
24
|
-
# @param
|
|
29
|
+
# @param path [String, nil] Override the default endpoint path
|
|
30
|
+
# @param completion_path [String, nil] Deprecated alias for +path+
|
|
25
31
|
# @param params [Hash] Additional parameters to merge into every request payload
|
|
26
32
|
# @return [void]
|
|
27
|
-
def provider(name, url:, headers: {}, serializer: :chat_completion, completion_path: nil, params: {})
|
|
33
|
+
def provider(name, url:, headers: {}, serializer: :chat_completion, path: nil, completion_path: nil, params: {})
|
|
34
|
+
if completion_path
|
|
35
|
+
warn "PatientLLM::Configuration#provider: the `completion_path:` argument is deprecated; use `path:` instead", uplevel: 1
|
|
36
|
+
path ||= completion_path
|
|
37
|
+
end
|
|
38
|
+
|
|
28
39
|
sym = serializer.to_sym
|
|
29
40
|
unless PatientLLM::VALID_SERIALIZERS.include?(sym)
|
|
30
41
|
raise ArgumentError, "Unknown serializer: #{sym.inspect}. Valid options: #{PatientLLM::VALID_SERIALIZERS.map(&:inspect).join(", ")}"
|
|
31
42
|
end
|
|
32
43
|
|
|
44
|
+
ensure_auth_headers_use_secrets!(headers)
|
|
45
|
+
|
|
33
46
|
@providers[name.to_s] = {
|
|
34
47
|
url: url,
|
|
35
48
|
headers: headers,
|
|
36
49
|
serializer: sym,
|
|
37
|
-
|
|
50
|
+
path: path,
|
|
38
51
|
params: params
|
|
39
52
|
}
|
|
40
53
|
end
|
|
@@ -46,5 +59,18 @@ module PatientLLM
|
|
|
46
59
|
def lookup(name)
|
|
47
60
|
@providers[name&.to_s]
|
|
48
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def ensure_auth_headers_use_secrets!(headers)
|
|
66
|
+
headers.each do |header_name, header_value|
|
|
67
|
+
normalized_header_name = header_name.to_s.downcase
|
|
68
|
+
next unless AUTHENTICATION_HEADERS.include?(normalized_header_name)
|
|
69
|
+
|
|
70
|
+
if header_value && !header_value.is_a?(PatientHttp::SecretReference)
|
|
71
|
+
raise ArgumentError, "Authentication header #{header_name} must be set up as a secret using `PatientHttp.secret`"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
49
75
|
end
|
|
50
76
|
end
|
data/lib/patient_llm.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "patient_http"
|
|
4
4
|
require "prompt_builder"
|
|
5
|
+
require "uri"
|
|
5
6
|
|
|
6
7
|
module PatientLLM
|
|
7
8
|
VERSION = File.read(File.join(__dir__, "../VERSION")).strip
|
|
@@ -16,11 +17,11 @@ module PatientLLM
|
|
|
16
17
|
# dispatch time, matching Google's `/v1beta/models/{model}:generateContent`
|
|
17
18
|
# endpoint.
|
|
18
19
|
SERIALIZER_PATHS = {
|
|
19
|
-
chat_completion: "
|
|
20
|
-
open_responses: "
|
|
21
|
-
messages: "
|
|
22
|
-
converse: "
|
|
23
|
-
gemini: "
|
|
20
|
+
chat_completion: "v1/chat/completions",
|
|
21
|
+
open_responses: "v1/responses",
|
|
22
|
+
messages: "v1/messages",
|
|
23
|
+
converse: "converse",
|
|
24
|
+
gemini: "v1beta/models/{model}:generateContent"
|
|
24
25
|
}.freeze
|
|
25
26
|
|
|
26
27
|
# Required version header for the Anthropic Messages API.
|
|
@@ -62,11 +63,17 @@ module PatientLLM
|
|
|
62
63
|
# @param callback_args [Hash] Custom arguments passed through to the callback
|
|
63
64
|
# @param url [String, nil] Override the provider's base URL for this request
|
|
64
65
|
# @param serializer [Symbol, nil] Override the provider's serializer for this request
|
|
65
|
-
# @param
|
|
66
|
+
# @param path [String, nil] Override the endpoint path for this request
|
|
67
|
+
# @param completion_path [String, nil] Deprecated alias for +path+
|
|
66
68
|
# @param headers [Hash, nil] Additional headers merged on top of provider headers
|
|
67
69
|
# @param params [Hash, nil] Additional params merged into the request payload
|
|
68
70
|
# @return [Object] Handler-specific identifier for the enqueued request
|
|
69
|
-
def ask(session, provider:, callback:, callback_args: {}, url: nil, serializer: nil, completion_path: nil, headers: nil, params: nil, tool_iteration: 0, original_request_id: nil) # :nodoc: tool_iteration and original_request_id are internal
|
|
71
|
+
def ask(session, provider:, callback:, callback_args: {}, url: nil, serializer: nil, path: nil, completion_path: nil, headers: nil, params: nil, tool_iteration: 0, original_request_id: nil) # :nodoc: tool_iteration and original_request_id are internal
|
|
72
|
+
if completion_path
|
|
73
|
+
warn "PatientLLM.ask: the `completion_path:` argument is deprecated; use `path:` instead", uplevel: 1
|
|
74
|
+
path ||= completion_path
|
|
75
|
+
end
|
|
76
|
+
|
|
70
77
|
provider_config = self.provider(provider) || {}
|
|
71
78
|
provider_name = provider.to_s
|
|
72
79
|
|
|
@@ -79,9 +86,9 @@ module PatientLLM
|
|
|
79
86
|
|
|
80
87
|
resolved_serializer = (serializer || provider_config[:serializer] || :chat_completion).to_sym
|
|
81
88
|
validate_serializer!(resolved_serializer)
|
|
82
|
-
|
|
83
|
-
if
|
|
84
|
-
|
|
89
|
+
resolved_path = path || provider_config[:path] || SERIALIZER_PATHS[resolved_serializer] || "/v1/chat/completions"
|
|
90
|
+
if resolved_path.include?("{model}")
|
|
91
|
+
resolved_path = resolved_path.gsub("{model}", session.model.to_s)
|
|
85
92
|
end
|
|
86
93
|
resolved_headers = (provider_config[:headers] || {}).merge(headers || {})
|
|
87
94
|
if resolved_serializer == :messages && !resolved_headers.key?("anthropic-version")
|
|
@@ -92,12 +99,12 @@ module PatientLLM
|
|
|
92
99
|
payload = session.request_payload(resolved_serializer)
|
|
93
100
|
payload = deep_merge(payload, deep_stringify_keys(resolved_params)) unless resolved_params.empty?
|
|
94
101
|
|
|
95
|
-
request_url = join_url(resolved_url,
|
|
102
|
+
request_url = join_url(resolved_url, resolved_path)
|
|
96
103
|
|
|
97
104
|
request_options = {}
|
|
98
105
|
request_options["url"] = url if url
|
|
99
106
|
request_options["serializer"] = serializer.to_s if serializer
|
|
100
|
-
request_options["
|
|
107
|
+
request_options["path"] = path if path
|
|
101
108
|
request_options["headers"] = headers if headers && !headers.empty?
|
|
102
109
|
request_options["params"] = params if params && !params.empty?
|
|
103
110
|
|
|
@@ -128,7 +135,9 @@ module PatientLLM
|
|
|
128
135
|
end
|
|
129
136
|
|
|
130
137
|
def join_url(base, path)
|
|
131
|
-
|
|
138
|
+
base_uri = URI.parse(base)
|
|
139
|
+
base_uri.path = "#{base_uri.path}/" unless base_uri.path&.end_with?("/")
|
|
140
|
+
URI.join(base_uri, path).to_s
|
|
132
141
|
end
|
|
133
142
|
|
|
134
143
|
def deep_merge(hash1, hash2)
|
data/patient_llm.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: patient_llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
@@ -13,16 +13,16 @@ dependencies:
|
|
|
13
13
|
name: patient_http
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '1.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '1.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: prompt_builder
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|