lex-llm-anthropic 0.2.13 → 0.2.16
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/.rubocop.yml +40 -6
- data/CHANGELOG.md +17 -0
- data/README.md +194 -22
- data/lib/legion/extensions/llm/anthropic/actors/discovery_refresh.rb +1 -1
- data/lib/legion/extensions/llm/anthropic/provider.rb +67 -58
- data/lib/legion/extensions/llm/anthropic/registry_event_builder.rb +10 -10
- data/lib/legion/extensions/llm/anthropic/runners/fleet_worker.rb +5 -5
- data/lib/legion/extensions/llm/anthropic/transport/messages/registry_event.rb +1 -1
- data/lib/legion/extensions/llm/anthropic/version.rb +1 -1
- data/lib/legion/extensions/llm/anthropic.rb +34 -34
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ef129fbcb71eb5500eff0ef7047dda892d97d57b21dab9567488be60cca0401
|
|
4
|
+
data.tar.gz: 3efed0ed6d287da75ac969e7dd29121b1c4e49e11f989a7fb7fed4f8c24f1ac2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dfd4e546964d3994cb294860aae5d2f81476c0c067b76519b9c65506eb782e32df58fda6ede3df2f9c17095b1b7aaf7e940a2e5afc4ea34efb7d086c00e17c4f
|
|
7
|
+
data.tar.gz: 62d2e1ac960fd5084becf6d83beb966ad02556205f1d70b183ab48892a69cb3783004eddcbea08435986b7a8ec569a3f41f8b835a5e12aed63b8388b078471e3
|
data/.rubocop.yml
CHANGED
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
plugins:
|
|
2
2
|
- rubocop-performance
|
|
3
3
|
- rubocop-rake
|
|
4
|
-
- rubocop-rspec
|
|
5
4
|
|
|
6
5
|
AllCops:
|
|
7
|
-
NewCops: enable
|
|
8
6
|
TargetRubyVersion: 3.4
|
|
7
|
+
NewCops: enable
|
|
9
8
|
SuggestExtensions: false
|
|
10
9
|
|
|
10
|
+
Layout/LineLength:
|
|
11
|
+
Max: 195
|
|
12
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
|
13
|
+
EnforcedStyle: space
|
|
14
|
+
Layout/HashAlignment:
|
|
15
|
+
EnforcedHashRocketStyle: table
|
|
16
|
+
EnforcedColonStyle: table
|
|
17
|
+
Metrics/MethodLength:
|
|
18
|
+
Max: 150
|
|
19
|
+
Metrics/ClassLength:
|
|
20
|
+
Max: 1500
|
|
21
|
+
Metrics/ModuleLength:
|
|
22
|
+
Max: 1500
|
|
11
23
|
Metrics/BlockLength:
|
|
24
|
+
Max: 150
|
|
12
25
|
Exclude:
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
Metrics/
|
|
26
|
+
- 'spec/**/*'
|
|
27
|
+
|
|
28
|
+
Metrics/AbcSize:
|
|
29
|
+
Max: 110
|
|
30
|
+
Metrics/BlockNesting:
|
|
31
|
+
Max: 4
|
|
32
|
+
Metrics/CyclomaticComplexity:
|
|
33
|
+
Max: 50
|
|
34
|
+
|
|
35
|
+
Metrics/PerceivedComplexity:
|
|
36
|
+
Max: 50
|
|
37
|
+
Style/Documentation:
|
|
16
38
|
Enabled: false
|
|
17
|
-
|
|
39
|
+
Style/SymbolArray:
|
|
40
|
+
Enabled: true
|
|
41
|
+
Style/FrozenStringLiteralComment:
|
|
42
|
+
Enabled: true
|
|
43
|
+
EnforcedStyle: always
|
|
44
|
+
Naming/FileName:
|
|
18
45
|
Enabled: false
|
|
46
|
+
Naming/PredicateMethod:
|
|
47
|
+
Enabled: false
|
|
48
|
+
Metrics/ParameterLists:
|
|
49
|
+
Max: 9
|
|
50
|
+
Style/RedundantConstantBase:
|
|
51
|
+
Exclude:
|
|
52
|
+
- 'spec/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.16 - 2026-06-10
|
|
4
|
+
|
|
5
|
+
- **Hash-backed tool support** — `format_tools` and `tool_schema` now handle both `ToolDefinition` objects and plain Hashes from `native_dispatch` via `respond_to?` checks with symbol/string key fallbacks. Prevents `NoMethodError` when tools arrive as hash-backed definitions (provider.rb).
|
|
6
|
+
- **RuboCop configuration overhaul** — Relaxed metrics to match project scale: LineLength 195, MethodLength 150, ClassLength 1500, AbcSize 110, BlockNesting 4, CyclomaticComplexity/PerceivedComplexity 50. Added `Layout/HashAlignment` (table style), `Layout/SpaceAroundEqualsInParameterDefault`, `Naming/PredicateMethod` disable, `Style/RedundantConstantBase` spec exclusion. Removed `rubocop-rspec` plugin (no longer needed). All 28 specs passing, 0 offenses (.rubocop.yml).
|
|
7
|
+
- **Hash alignment formatting** — Applied consistent table-style hash alignment across provider.rb, anthropic.rb, registry_event_builder.rb, fleet_worker.rb, and transport messages for readability.
|
|
8
|
+
|
|
9
|
+
## 0.2.15 - 2026-06-05
|
|
10
|
+
|
|
11
|
+
- **Fix RuboCop cyclomatic complexity** — Extract `extract_hash_budget` helper to reduce `thinking_budget` cyclomatic complexity from 8 to 6, meeting the 7-line threshold.
|
|
12
|
+
- **Add budget_tokens support** — `extract_hash_budget` now checks `:budget_tokens` and `'budget_tokens'` keys (Anthropic API canonical) in addition to legacy `:budget`/`'budget'`.
|
|
13
|
+
- **Spec and RuboCop compliance** — All 28 specs passing, 0 RuboCop offenses.
|
|
14
|
+
|
|
15
|
+
## 0.2.14 - 2026-06-05
|
|
16
|
+
|
|
17
|
+
- **Fix RuboCop cyclomatic complexity** — Extract `extract_hash_budget` helper to reduce `thinking_budget` complexity from 8 to 6, meeting the 7-line threshold.
|
|
18
|
+
- **Fix Style/IfUnlessModifier** — Split conditional return in `thinking_budget` to avoid modifier form exceeding max line length.
|
|
19
|
+
|
|
3
20
|
## 0.2.13 - 2026-06-02
|
|
4
21
|
|
|
5
22
|
- **Fix invalid anthropic-version header** — Default `api_version` was `'2023-10-02'` (typo), which Anthropic rejects. Changed to `'2023-10-16'` (anthropic.rb)
|
data/README.md
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
# lex-llm-anthropic
|
|
2
2
|
|
|
3
|
-
LegionIO LLM provider extension for Anthropic.
|
|
3
|
+
LegionIO LLM provider extension for **Anthropic Claude** models.
|
|
4
4
|
|
|
5
|
-
This gem
|
|
5
|
+
This gem provides the `:anthropic` provider family for LegionIO's LLM routing layer, connecting to the [Anthropic Messages API](https://docs.anthropic.com/en/docs/api/messages). It handles chat completions, streaming, tool use, extended thinking, model discovery, and fleet request consumption.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Namespace:** `Legion::Extensions::Llm::Anthropic`
|
|
8
|
+
**Provider family:** `:anthropic`
|
|
9
|
+
**Default model:** `claude-sonnet-4-6`
|
|
10
|
+
**Dependency:** `lex-llm >= 0.4.3` (shared provider contracts, response normalization, fleet responders)
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
require 'legion/extensions/llm/anthropic'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## File Index
|
|
19
|
+
|
|
20
|
+
| File | Role |
|
|
21
|
+
|------|------|
|
|
22
|
+
| `anthropic.rb` | Entry point; namespace, `PROVIDER_FAMILY`, default settings, instance discovery |
|
|
23
|
+
| `anthropic/provider.rb` | `Provider` class — chat, stream, list_models, format/parse payloads |
|
|
24
|
+
| `anthropic/registry_event_builder.rb` | Builds registry event envelopes for model availability |
|
|
25
|
+
| `anthropic/registry_publisher.rb` | Publishes model events to `llm.registry` exchange (async, best-effort) |
|
|
26
|
+
| `anthropic/version.rb` | `VERSION` constant |
|
|
27
|
+
| `anthropic/actors/discovery_refresh.rb` | Periodic actor that refreshes Anthropic model list |
|
|
28
|
+
| `anthropic/actors/fleet_worker.rb` | Subscription actor for fleet request consumption |
|
|
29
|
+
| `anthropic/runners/fleet_worker.rb` | Runner entrypoint for fleet request execution |
|
|
30
|
+
| `anthropic/transport/exchanges/llm_registry.rb` | Topic exchange definition for `llm.registry` |
|
|
31
|
+
| `anthropic/transport/messages/registry_event.rb` | Transport message wrapper for registry events |
|
|
32
|
+
|
|
33
|
+
---
|
|
8
34
|
|
|
9
35
|
## Installation
|
|
10
36
|
|
|
@@ -12,36 +38,83 @@ Load it with `require 'legion/extensions/llm/anthropic'`.
|
|
|
12
38
|
gem 'lex-llm-anthropic', '~> 0.2'
|
|
13
39
|
```
|
|
14
40
|
|
|
15
|
-
|
|
41
|
+
---
|
|
16
42
|
|
|
17
|
-
##
|
|
43
|
+
## Architecture
|
|
18
44
|
|
|
19
|
-
|
|
45
|
+
### Provider (`Provider`)
|
|
20
46
|
|
|
21
|
-
-
|
|
22
|
-
- model discovery: `/v1/models`
|
|
23
|
-
- authentication headers: `x-api-key` and `anthropic-version`
|
|
24
|
-
- tools: Anthropic `tools` and `tool_choice` payload fields
|
|
25
|
-
- extended thinking: Anthropic `thinking` request field and returned thinking blocks
|
|
47
|
+
The `Provider` class extends the `lex-llm` base provider contract and implements:
|
|
26
48
|
|
|
27
|
-
|
|
49
|
+
| Method | Description |
|
|
50
|
+
|--------|-------------|
|
|
51
|
+
| `chat(**kwargs)` | Synchronous chat completion via `/v1/messages` |
|
|
52
|
+
| `stream_chat(**kwargs)` | Streaming chat via `/v1/messages?stream=true` |
|
|
53
|
+
| `list_models` | Fetches available models from `/v1/models` |
|
|
54
|
+
| `format_payload(**)` | Builds Anthropic Messages API request body |
|
|
55
|
+
| `parse_response(response)` | Normalizes API response to Legion envelope |
|
|
56
|
+
| `parse_stream(response)` | Parses SSE stream into chunk events |
|
|
57
|
+
| `build_chunk(event, state)` | Accumulates streaming state (content, tool calls, thinking) |
|
|
28
58
|
|
|
29
|
-
|
|
59
|
+
**Supported capabilities:** `:completion`, `:streaming`, `:vision`, `:tools`
|
|
60
|
+
|
|
61
|
+
**API endpoints:**
|
|
62
|
+
- Chat & streaming: `POST /v1/messages`
|
|
63
|
+
- Model discovery: `GET /v1/models`
|
|
64
|
+
|
|
65
|
+
**Authentication headers:** `x-api-key`, `anthropic-version`
|
|
66
|
+
|
|
67
|
+
### Instance Discovery (`discover_instances`)
|
|
68
|
+
|
|
69
|
+
The extension discovers and normalizes Anthropic credentials from four sources, in priority order:
|
|
70
|
+
|
|
71
|
+
1. **Environment** — `ANTHROPIC_API_KEY`
|
|
72
|
+
2. **Claude config** — `~/.claude/settings.json` under `anthropicApiKey`
|
|
73
|
+
3. **Extension settings** — `Legion::Settings` at `extensions.llm.anthropic`
|
|
74
|
+
4. **Identity broker** — `Legion::Identity::Broker.credential_for(:anthropic)`
|
|
75
|
+
|
|
76
|
+
Named instances are supported under `extensions.llm.anthropic.instances.<name>`. Generic keys (`api_key`, `endpoint`, `version`) are normalized to Anthropic-specific keys (`anthropic_api_key`, `anthropic_api_base`, `anthropic_version`). Duplicate credentials are deduplicated by fingerprint.
|
|
77
|
+
|
|
78
|
+
### Default Settings
|
|
30
79
|
|
|
31
80
|
```ruby
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
{
|
|
82
|
+
default_model: 'claude-sonnet-4-6',
|
|
83
|
+
endpoint: 'https://api.anthropic.com',
|
|
84
|
+
api_version: '2023-10-16',
|
|
85
|
+
default_max_tokens: 4096,
|
|
86
|
+
tier: :frontier,
|
|
87
|
+
transport: :http,
|
|
88
|
+
credentials: { api_key: 'env://ANTHROPIC_API_KEY' },
|
|
89
|
+
usage: { inference: true, embedding: false, image: false },
|
|
90
|
+
limits: { concurrency: 4 },
|
|
91
|
+
fleet: {
|
|
92
|
+
enabled: false,
|
|
93
|
+
respond_to_requests: false,
|
|
94
|
+
capabilities: %i[chat stream_chat],
|
|
95
|
+
lanes: [],
|
|
96
|
+
concurrency: 4,
|
|
97
|
+
queue_suffix: nil
|
|
98
|
+
}
|
|
99
|
+
}
|
|
36
100
|
```
|
|
37
101
|
|
|
38
|
-
|
|
102
|
+
### Registry Events (Availability Publishing)
|
|
103
|
+
|
|
104
|
+
After model discovery, discovered models are published to the `llm.registry` topic exchange so other LegionIO nodes can discover Anthropic availability.
|
|
105
|
+
|
|
106
|
+
| Class | Role |
|
|
107
|
+
|-------|------|
|
|
108
|
+
| `RegistryEventBuilder` | Builds the event envelope with model offering, health, and metadata |
|
|
109
|
+
| `RegistryPublisher` | Schedules async publish; checks transport readiness before sending |
|
|
110
|
+
| `Transport::Messages::RegistryEvent` | Message wrapper targeting the `llm.registry` exchange |
|
|
111
|
+
| `Transport::Exchanges::LlmRegistry` | Topic exchange definition (`llm.registry`) |
|
|
39
112
|
|
|
40
|
-
|
|
113
|
+
Publishing is **best-effort** and requires transport to be loaded. Failures are logged at `debug` level and silently absorbed.
|
|
41
114
|
|
|
42
|
-
|
|
115
|
+
### Fleet Responder
|
|
43
116
|
|
|
44
|
-
Provider instances can
|
|
117
|
+
Provider instances can consume Legion LLM fleet requests. The fleet actor only starts when at least one configured instance enables `respond_to_requests`.
|
|
45
118
|
|
|
46
119
|
```yaml
|
|
47
120
|
extensions:
|
|
@@ -57,4 +130,103 @@ extensions:
|
|
|
57
130
|
- stream_chat
|
|
58
131
|
```
|
|
59
132
|
|
|
60
|
-
|
|
133
|
+
| Component | Role |
|
|
134
|
+
|-----------|------|
|
|
135
|
+
| `Actor::FleetWorker` | Subscription actor; checks if any instance enables fleet responding |
|
|
136
|
+
| `Runners::FleetWorker` | Runner entrypoint; delegates to `lex-llm` `ProviderResponder` |
|
|
137
|
+
|
|
138
|
+
Fleet execution is delegated to `Legion::Extensions::Llm::Fleet::ProviderResponder` from `lex-llm`. Routing and fleet request publication are handled outside this extension.
|
|
139
|
+
|
|
140
|
+
### Model Discovery Refresh
|
|
141
|
+
|
|
142
|
+
A periodic actor (`Actor::DiscoveryRefresh`) refreshes the Anthropic model list every 30 minutes (configurable via `extensions.llm.anthropic.discovery_interval`). It calls `Legion::LLM::Discovery.refresh_discovered_models!(provider: :anthropic)`.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Configuration
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
Legion::Extensions::Llm.configure do |config|
|
|
150
|
+
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY')
|
|
151
|
+
config.anthropic_version = '2023-10-16'
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Configurable options:**
|
|
156
|
+
|
|
157
|
+
| Setting | Key | Description |
|
|
158
|
+
|---------|-----|-------------|
|
|
159
|
+
| API key | `anthropic_api_key` / `ANTHROPIC_API_KEY` | Required; API authentication |
|
|
160
|
+
| API version | `anthropic_version` | Defaults to `2023-10-16` |
|
|
161
|
+
| Endpoint | `anthropic_api_base` | Override `https://api.anthropic.com` |
|
|
162
|
+
| Max tokens | `default_max_tokens` | Defaults to `4096` |
|
|
163
|
+
| Discovery interval | `discovery_interval` (seconds) | Defaults to `1800` |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Usage Examples
|
|
168
|
+
|
|
169
|
+
### Chat (synchronous)
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
provider = Legion::Extensions::Llm::Anthropic::Provider.new(api_key: 'sk-ant-...')
|
|
173
|
+
response = provider.chat(model: 'claude-sonnet-4-6', messages: [{ role: 'user', content: 'Hello' }])
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Chat (streaming)
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
provider.stream_chat(model: 'claude-sonnet-4-6', messages: [{ role: 'user', content: 'Hello' }]) do |chunk|
|
|
180
|
+
print chunk[:content] if chunk[:content]
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### With tools
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
response = provider.chat(
|
|
188
|
+
model: 'claude-sonnet-4-6',
|
|
189
|
+
messages: [{ role: 'user', content: 'What is the weather in SF?' }],
|
|
190
|
+
tools: [{ name: 'get_weather', description: '...', input_schema: { ... } }]
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Extended thinking
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
response = provider.chat(
|
|
198
|
+
model: 'claude-sonnet-4-6',
|
|
199
|
+
messages: [{ role: 'user', content: 'Solve this math problem...' }],
|
|
200
|
+
thinking: { budget_tokens: 4096, enabled: true }
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Dependencies
|
|
207
|
+
|
|
208
|
+
| Gem | Minimum version | Role |
|
|
209
|
+
|-----|-----------------|------|
|
|
210
|
+
| `lex-llm` | `>= 0.4.3` | Base provider contract, response normalization, fleet responder, auto-registration |
|
|
211
|
+
| `legion-logging` | (via lex-llm) | Logging helper for diagnostics |
|
|
212
|
+
| `legion-settings` | (via lex-llm) | Configuration access |
|
|
213
|
+
| `legion-transport` | (via lex-llm) | Message exchange for registry events |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Testing
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
bundle exec rspec # 28 examples
|
|
221
|
+
bundle exec rubocop # 0 offenses
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Design Notes
|
|
227
|
+
|
|
228
|
+
- **No embeddings** — Anthropic embeddings are intentionally not exposed.
|
|
229
|
+
- **No `:claude` alias** — `provider_aliases` returns `[]`; only `:anthropic` is registered.
|
|
230
|
+
- **Prompt caching** — When `cache_enabled?` is true, system content and tool definitions are marked as cache breakpoints; early conversation turns are cacheable, the final message is never cached.
|
|
231
|
+
- **Thinking budget** — Supports `Integer`, `Hash` (with `:budget_tokens` or legacy `:budget`), and objects responding to `#budget`. Defaults to `1024`.
|
|
232
|
+
- **Context windows** — Static `CONTEXT_WINDOWS` map covers known Claude model families; `fetch_model_detail` and `infer_context_window` provide fallback inference.
|
|
@@ -13,7 +13,7 @@ module Legion
|
|
|
13
13
|
module Llm
|
|
14
14
|
module Anthropic
|
|
15
15
|
module Actor
|
|
16
|
-
class DiscoveryRefresh < Legion::Extensions::Actors::Every
|
|
16
|
+
class DiscoveryRefresh < Legion::Extensions::Actors::Every
|
|
17
17
|
include Legion::Logging::Helper
|
|
18
18
|
|
|
19
19
|
REFRESH_INTERVAL = 1800
|
|
@@ -8,7 +8,7 @@ module Legion
|
|
|
8
8
|
module Llm
|
|
9
9
|
module Anthropic
|
|
10
10
|
# Anthropic Messages API provider implementation for the Legion::Extensions::Llm contract.
|
|
11
|
-
class Provider < Legion::Extensions::Llm::Provider
|
|
11
|
+
class Provider < Legion::Extensions::Llm::Provider
|
|
12
12
|
include Legion::Logging::Helper
|
|
13
13
|
|
|
14
14
|
class << self
|
|
@@ -45,7 +45,7 @@ module Legion
|
|
|
45
45
|
|
|
46
46
|
def headers
|
|
47
47
|
identity_headers.merge({
|
|
48
|
-
'x-api-key'
|
|
48
|
+
'x-api-key' => config.anthropic_api_key,
|
|
49
49
|
'anthropic-version' => config.anthropic_version || settings[:api_version] || '2023-06-01'
|
|
50
50
|
}.compact)
|
|
51
51
|
end
|
|
@@ -67,18 +67,18 @@ module Legion
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
CONTEXT_WINDOWS = {
|
|
70
|
-
'claude-opus-4'
|
|
70
|
+
'claude-opus-4' => 200_000,
|
|
71
71
|
'claude-sonnet-4' => 200_000,
|
|
72
|
-
'claude-haiku-4'
|
|
73
|
-
'claude-3-5'
|
|
74
|
-
'claude-3-opus'
|
|
72
|
+
'claude-haiku-4' => 200_000,
|
|
73
|
+
'claude-3-5' => 200_000,
|
|
74
|
+
'claude-3-opus' => 200_000,
|
|
75
75
|
'claude-3-sonnet' => 200_000,
|
|
76
|
-
'claude-3-haiku'
|
|
76
|
+
'claude-3-haiku' => 200_000
|
|
77
77
|
}.freeze
|
|
78
78
|
|
|
79
79
|
private
|
|
80
80
|
|
|
81
|
-
def render_payload(messages, tools:, temperature:, model:, stream:, schema:, thinking:, tool_prefs:)
|
|
81
|
+
def render_payload(messages, tools:, temperature:, model:, stream:, schema:, thinking:, tool_prefs:)
|
|
82
82
|
log_render_payload(messages:, tools:, model:, stream:, schema:)
|
|
83
83
|
system_messages, chat_messages = messages.partition { |message| message.role == :system }
|
|
84
84
|
|
|
@@ -87,15 +87,15 @@ module Legion
|
|
|
87
87
|
cacheable_count = caching ? [chat_messages.size - exclude_count, 0].max : 0
|
|
88
88
|
|
|
89
89
|
{
|
|
90
|
-
model:
|
|
91
|
-
messages:
|
|
92
|
-
stream:
|
|
93
|
-
max_tokens:
|
|
94
|
-
system:
|
|
95
|
-
thinking:
|
|
96
|
-
temperature:
|
|
97
|
-
tools:
|
|
98
|
-
tool_choice:
|
|
90
|
+
model: model.id,
|
|
91
|
+
messages: format_messages(chat_messages, thinking: thinking_enabled?(thinking), cacheable_count:),
|
|
92
|
+
stream: stream,
|
|
93
|
+
max_tokens: model.max_tokens || settings[:default_max_tokens] || 4096,
|
|
94
|
+
system: system_content(system_messages, cache: caching),
|
|
95
|
+
thinking: thinking_payload(thinking),
|
|
96
|
+
temperature: temperature,
|
|
97
|
+
tools: format_tools(tools, cache: caching),
|
|
98
|
+
tool_choice: tool_choice(tool_prefs),
|
|
99
99
|
output_config: output_config(schema)
|
|
100
100
|
}.compact
|
|
101
101
|
end
|
|
@@ -123,7 +123,7 @@ module Legion
|
|
|
123
123
|
format_tool_result_message(message, cache:)
|
|
124
124
|
else
|
|
125
125
|
{
|
|
126
|
-
role:
|
|
126
|
+
role: anthropic_role(message.role),
|
|
127
127
|
content: content_blocks(message.content, thinking:, message:, cache:)
|
|
128
128
|
}
|
|
129
129
|
end
|
|
@@ -167,11 +167,11 @@ module Legion
|
|
|
167
167
|
next unless attachment.image?
|
|
168
168
|
|
|
169
169
|
{
|
|
170
|
-
type:
|
|
170
|
+
type: 'image',
|
|
171
171
|
source: {
|
|
172
|
-
type:
|
|
172
|
+
type: 'base64',
|
|
173
173
|
media_type: attachment.mime_type,
|
|
174
|
-
data:
|
|
174
|
+
data: attachment.encoded
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
end
|
|
@@ -192,10 +192,10 @@ module Legion
|
|
|
192
192
|
|
|
193
193
|
def tool_use_block(tool_call, cache: false)
|
|
194
194
|
{
|
|
195
|
-
type:
|
|
196
|
-
id:
|
|
197
|
-
name:
|
|
198
|
-
input:
|
|
195
|
+
type: 'tool_use',
|
|
196
|
+
id: tool_call.id,
|
|
197
|
+
name: tool_call.name,
|
|
198
|
+
input: tool_call.arguments,
|
|
199
199
|
cache_control: { type: 'ephemeral' }
|
|
200
200
|
}.tap do |block|
|
|
201
201
|
block.delete(:cache_control) unless cache
|
|
@@ -204,12 +204,12 @@ module Legion
|
|
|
204
204
|
|
|
205
205
|
def format_tool_result_message(message, cache: false)
|
|
206
206
|
{
|
|
207
|
-
role:
|
|
207
|
+
role: 'user',
|
|
208
208
|
content: [
|
|
209
209
|
{
|
|
210
|
-
type:
|
|
211
|
-
tool_use_id:
|
|
212
|
-
content:
|
|
210
|
+
type: 'tool_result',
|
|
211
|
+
tool_use_id: message.tool_call_id,
|
|
212
|
+
content: content_blocks(message.content, cache:),
|
|
213
213
|
cache_control: { type: 'ephemeral' }
|
|
214
214
|
}.tap { |block| block.delete(:cache_control) unless cache }
|
|
215
215
|
]
|
|
@@ -231,12 +231,17 @@ module Legion
|
|
|
231
231
|
|
|
232
232
|
def thinking_budget(thinking)
|
|
233
233
|
return thinking if thinking.is_a?(Integer)
|
|
234
|
-
return thinking
|
|
234
|
+
return extract_hash_budget(thinking) if thinking.is_a?(Hash)
|
|
235
235
|
return thinking.budget if thinking.respond_to?(:budget) && thinking.budget
|
|
236
236
|
|
|
237
237
|
1024
|
|
238
238
|
end
|
|
239
239
|
|
|
240
|
+
# Anthropic API uses :budget_tokens, but legacy config may use :budget
|
|
241
|
+
def extract_hash_budget(thinking)
|
|
242
|
+
thinking[:budget_tokens] || thinking['budget_tokens'] || thinking[:budget] || thinking['budget']
|
|
243
|
+
end
|
|
244
|
+
|
|
240
245
|
def thinking_block(thinking)
|
|
241
246
|
return nil unless thinking
|
|
242
247
|
|
|
@@ -251,9 +256,12 @@ module Legion
|
|
|
251
256
|
return nil if tools.empty?
|
|
252
257
|
|
|
253
258
|
tool_array = tools.values.map do |tool|
|
|
259
|
+
# Tools can be ToolDefinition objects or plain Hashes from native_dispatch.
|
|
260
|
+
tool_name = tool.respond_to?(:name) ? tool.name : (tool[:name] || tool['name'])
|
|
261
|
+
tool_desc = tool.respond_to?(:description) ? tool.description : (tool[:description] || tool['description'] || '')
|
|
254
262
|
{
|
|
255
|
-
name:
|
|
256
|
-
description:
|
|
263
|
+
name: tool_name,
|
|
264
|
+
description: tool_desc,
|
|
257
265
|
input_schema: tool_schema(tool)
|
|
258
266
|
}
|
|
259
267
|
end
|
|
@@ -266,7 +274,8 @@ module Legion
|
|
|
266
274
|
def tool_schema(tool)
|
|
267
275
|
return tool.params_schema if tool.respond_to?(:params_schema) && tool.params_schema
|
|
268
276
|
|
|
269
|
-
{ type: 'object',
|
|
277
|
+
{ type: 'object',
|
|
278
|
+
properties: tool.respond_to?(:parameters) ? tool.parameters : (tool[:parameters] || tool['parameters'] || {}), required: [] }
|
|
270
279
|
end
|
|
271
280
|
|
|
272
281
|
def tool_choice(tool_prefs)
|
|
@@ -313,17 +322,17 @@ module Legion
|
|
|
313
322
|
usage = body['usage'] || {}
|
|
314
323
|
|
|
315
324
|
Legion::Extensions::Llm::Message.new(
|
|
316
|
-
role:
|
|
317
|
-
content:
|
|
318
|
-
model_id:
|
|
319
|
-
thinking:
|
|
320
|
-
tool_calls:
|
|
321
|
-
input_tokens:
|
|
322
|
-
output_tokens:
|
|
323
|
-
cached_tokens:
|
|
325
|
+
role: :assistant,
|
|
326
|
+
content: text_from(content_blocks),
|
|
327
|
+
model_id: body['model'],
|
|
328
|
+
thinking: thinking_from(content_blocks),
|
|
329
|
+
tool_calls: parse_tool_calls(content_blocks),
|
|
330
|
+
input_tokens: usage['input_tokens'],
|
|
331
|
+
output_tokens: usage['output_tokens'],
|
|
332
|
+
cached_tokens: usage['cache_read_input_tokens'],
|
|
324
333
|
cache_creation_tokens: cache_creation_tokens(usage),
|
|
325
|
-
thinking_tokens:
|
|
326
|
-
raw:
|
|
334
|
+
thinking_tokens: thinking_tokens(usage),
|
|
335
|
+
raw: body
|
|
327
336
|
)
|
|
328
337
|
end
|
|
329
338
|
|
|
@@ -336,7 +345,7 @@ module Legion
|
|
|
336
345
|
redacted_block = blocks.find { |block| block['type'] == 'redacted_thinking' }
|
|
337
346
|
|
|
338
347
|
Legion::Extensions::Llm::Thinking.build(
|
|
339
|
-
text:
|
|
348
|
+
text: thinking_block&.dig('thinking') || thinking_block&.dig('text'),
|
|
340
349
|
signature: thinking_block&.dig('signature') || redacted_block&.dig('data')
|
|
341
350
|
)
|
|
342
351
|
end
|
|
@@ -359,16 +368,16 @@ module Legion
|
|
|
359
368
|
delta_type = data.dig('delta', 'type')
|
|
360
369
|
|
|
361
370
|
Legion::Extensions::Llm::Chunk.new(
|
|
362
|
-
role:
|
|
363
|
-
content:
|
|
364
|
-
model_id:
|
|
365
|
-
thinking:
|
|
366
|
-
text:
|
|
371
|
+
role: :assistant,
|
|
372
|
+
content: delta_type == 'text_delta' ? data.dig('delta', 'text') : nil,
|
|
373
|
+
model_id: data.dig('message', 'model'),
|
|
374
|
+
thinking: Legion::Extensions::Llm::Thinking.build(
|
|
375
|
+
text: delta_type == 'thinking_delta' ? data.dig('delta', 'thinking') : nil,
|
|
367
376
|
signature: delta_type == 'signature_delta' ? data.dig('delta', 'signature') : nil
|
|
368
377
|
),
|
|
369
|
-
input_tokens:
|
|
378
|
+
input_tokens: data.dig('message', 'usage', 'input_tokens'),
|
|
370
379
|
output_tokens: data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens'),
|
|
371
|
-
tool_calls:
|
|
380
|
+
tool_calls: extract_streaming_tool_calls(data, delta_type)
|
|
372
381
|
)
|
|
373
382
|
end
|
|
374
383
|
|
|
@@ -389,8 +398,8 @@ module Legion
|
|
|
389
398
|
[
|
|
390
399
|
block['id'],
|
|
391
400
|
Legion::Extensions::Llm::ToolCall.new(
|
|
392
|
-
id:
|
|
393
|
-
name:
|
|
401
|
+
id: block['id'],
|
|
402
|
+
name: block['name'],
|
|
394
403
|
arguments: block['input'] || {}
|
|
395
404
|
)
|
|
396
405
|
]
|
|
@@ -403,12 +412,12 @@ module Legion
|
|
|
403
412
|
detail = model_detail(model_id)
|
|
404
413
|
ctx = detail&.dig(:context_window) || infer_context_window(model_id)
|
|
405
414
|
Legion::Extensions::Llm::Model::Info.new(
|
|
406
|
-
id:
|
|
407
|
-
name:
|
|
408
|
-
provider:
|
|
409
|
-
capabilities:
|
|
415
|
+
id: model_id,
|
|
416
|
+
name: model['display_name'] || model_id,
|
|
417
|
+
provider: provider,
|
|
418
|
+
capabilities: %i[completion streaming tools],
|
|
410
419
|
context_length: ctx,
|
|
411
|
-
metadata:
|
|
420
|
+
metadata: model.merge('created_at' => model['created_at']).compact
|
|
412
421
|
)
|
|
413
422
|
end
|
|
414
423
|
end
|
|
@@ -13,8 +13,8 @@ module Legion
|
|
|
13
13
|
def model_available(model, readiness:)
|
|
14
14
|
registry_event_class.available(
|
|
15
15
|
model_offering(model),
|
|
16
|
-
runtime:
|
|
17
|
-
health:
|
|
16
|
+
runtime: runtime_metadata,
|
|
17
|
+
health: model_health(readiness),
|
|
18
18
|
metadata: model_metadata(model)
|
|
19
19
|
)
|
|
20
20
|
end
|
|
@@ -23,14 +23,14 @@ module Legion
|
|
|
23
23
|
|
|
24
24
|
def model_offering(model)
|
|
25
25
|
{
|
|
26
|
-
provider_family:
|
|
26
|
+
provider_family: :anthropic,
|
|
27
27
|
provider_instance: provider_instance,
|
|
28
|
-
transport:
|
|
29
|
-
model:
|
|
30
|
-
usage_type:
|
|
31
|
-
capabilities:
|
|
32
|
-
limits:
|
|
33
|
-
metadata:
|
|
28
|
+
transport: :http,
|
|
29
|
+
model: model.id,
|
|
30
|
+
usage_type: :inference,
|
|
31
|
+
capabilities: Array(model.capabilities).map(&:to_sym),
|
|
32
|
+
limits: model_limits(model),
|
|
33
|
+
metadata: { lex: :llm_anthropic, model_name: model.name }.compact
|
|
34
34
|
}
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -49,7 +49,7 @@ module Legion
|
|
|
49
49
|
|
|
50
50
|
def model_limits(model)
|
|
51
51
|
{
|
|
52
|
-
context_window:
|
|
52
|
+
context_window: model.context_window,
|
|
53
53
|
max_output_tokens: model.max_output_tokens
|
|
54
54
|
}.compact
|
|
55
55
|
end
|
|
@@ -14,12 +14,12 @@ module Legion
|
|
|
14
14
|
|
|
15
15
|
def handle_fleet_request(payload, delivery: nil, properties: nil)
|
|
16
16
|
Legion::Extensions::Llm::Fleet::ProviderResponder.call(
|
|
17
|
-
payload:
|
|
18
|
-
provider_family:
|
|
19
|
-
provider_class:
|
|
17
|
+
payload: payload,
|
|
18
|
+
provider_family: Anthropic::PROVIDER_FAMILY,
|
|
19
|
+
provider_class: Anthropic::Provider,
|
|
20
20
|
provider_instances: -> { Anthropic.discover_instances },
|
|
21
|
-
delivery:
|
|
22
|
-
properties:
|
|
21
|
+
delivery: delivery,
|
|
22
|
+
properties: properties
|
|
23
23
|
)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -11,7 +11,7 @@ module Legion
|
|
|
11
11
|
module Extensions
|
|
12
12
|
module Llm
|
|
13
13
|
# Anthropic provider extension namespace.
|
|
14
|
-
module Anthropic
|
|
14
|
+
module Anthropic
|
|
15
15
|
extend ::Legion::Extensions::Core if ::Legion::Extensions.const_defined?(:Core, false)
|
|
16
16
|
extend Legion::Logging::Helper
|
|
17
17
|
extend Legion::Extensions::Llm::AutoRegistration
|
|
@@ -20,24 +20,24 @@ module Legion
|
|
|
20
20
|
|
|
21
21
|
def self.default_settings
|
|
22
22
|
::Legion::Extensions::Llm.provider_settings(
|
|
23
|
-
family:
|
|
23
|
+
family: PROVIDER_FAMILY,
|
|
24
24
|
instance: {
|
|
25
|
-
default_model:
|
|
26
|
-
endpoint:
|
|
27
|
-
api_version:
|
|
25
|
+
default_model: 'claude-sonnet-4-6',
|
|
26
|
+
endpoint: 'https://api.anthropic.com',
|
|
27
|
+
api_version: '2023-10-16',
|
|
28
28
|
default_max_tokens: 4096,
|
|
29
|
-
tier:
|
|
30
|
-
transport:
|
|
31
|
-
credentials:
|
|
32
|
-
usage:
|
|
33
|
-
limits:
|
|
34
|
-
fleet:
|
|
35
|
-
enabled:
|
|
29
|
+
tier: :frontier,
|
|
30
|
+
transport: :http,
|
|
31
|
+
credentials: { api_key: 'env://ANTHROPIC_API_KEY' },
|
|
32
|
+
usage: { inference: true, embedding: false, image: false },
|
|
33
|
+
limits: { concurrency: 4 },
|
|
34
|
+
fleet: {
|
|
35
|
+
enabled: false,
|
|
36
36
|
respond_to_requests: false,
|
|
37
|
-
capabilities:
|
|
38
|
-
lanes:
|
|
39
|
-
concurrency:
|
|
40
|
-
queue_suffix:
|
|
37
|
+
capabilities: %i[chat stream_chat],
|
|
38
|
+
lanes: [],
|
|
39
|
+
concurrency: 4,
|
|
40
|
+
queue_suffix: nil
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
)
|
|
@@ -51,16 +51,16 @@ module Legion
|
|
|
51
51
|
[]
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def self.discover_instances
|
|
54
|
+
def self.discover_instances
|
|
55
55
|
candidates = {}
|
|
56
56
|
|
|
57
57
|
env_key = CredentialSources.env('ANTHROPIC_API_KEY')
|
|
58
58
|
if env_key
|
|
59
59
|
candidates[:env] = {
|
|
60
|
-
api_key:
|
|
61
|
-
anthropic_api_key:
|
|
62
|
-
tier:
|
|
63
|
-
source:
|
|
60
|
+
api_key: env_key,
|
|
61
|
+
anthropic_api_key: env_key,
|
|
62
|
+
tier: :frontier,
|
|
63
|
+
source: CredentialSources.source_tag(:env, 'ANTHROPIC_API_KEY'),
|
|
64
64
|
credential_fingerprint: CredentialSources.credential_fingerprint(env_key)
|
|
65
65
|
}
|
|
66
66
|
end
|
|
@@ -68,10 +68,10 @@ module Legion
|
|
|
68
68
|
claude_key = CredentialSources.claude_config_value(:anthropicApiKey)
|
|
69
69
|
if claude_key
|
|
70
70
|
candidates[:claude] = {
|
|
71
|
-
api_key:
|
|
72
|
-
anthropic_api_key:
|
|
73
|
-
tier:
|
|
74
|
-
source:
|
|
71
|
+
api_key: claude_key,
|
|
72
|
+
anthropic_api_key: claude_key,
|
|
73
|
+
tier: :frontier,
|
|
74
|
+
source: CredentialSources.source_tag(:file, '~/.claude/settings.json', 'anthropicApiKey'),
|
|
75
75
|
credential_fingerprint: CredentialSources.credential_fingerprint(claude_key)
|
|
76
76
|
}
|
|
77
77
|
end
|
|
@@ -81,10 +81,10 @@ module Legion
|
|
|
81
81
|
settings_key = settings_config[:api_key] || settings_config['api_key']
|
|
82
82
|
if settings_key
|
|
83
83
|
candidates[:settings] = normalize_instance_config(settings_config).merge(
|
|
84
|
-
api_key:
|
|
85
|
-
anthropic_api_key:
|
|
86
|
-
tier:
|
|
87
|
-
source:
|
|
84
|
+
api_key: settings_key,
|
|
85
|
+
anthropic_api_key: settings_key,
|
|
86
|
+
tier: :frontier,
|
|
87
|
+
source: CredentialSources.source_tag(:settings, 'extensions.llm.anthropic'),
|
|
88
88
|
credential_fingerprint: CredentialSources.credential_fingerprint(settings_key)
|
|
89
89
|
)
|
|
90
90
|
end
|
|
@@ -108,10 +108,10 @@ module Legion
|
|
|
108
108
|
broker_cred = Legion::Identity::Broker.credential_for(:anthropic)
|
|
109
109
|
if broker_cred
|
|
110
110
|
candidates[:broker] = {
|
|
111
|
-
api_key:
|
|
112
|
-
anthropic_api_key:
|
|
113
|
-
tier:
|
|
114
|
-
source:
|
|
111
|
+
api_key: broker_cred,
|
|
112
|
+
anthropic_api_key: broker_cred,
|
|
113
|
+
tier: :frontier,
|
|
114
|
+
source: CredentialSources.source_tag(:broker, 'identity', 'anthropic'),
|
|
115
115
|
credential_fingerprint: CredentialSources.credential_fingerprint(broker_cred)
|
|
116
116
|
}
|
|
117
117
|
end
|
|
@@ -130,7 +130,7 @@ module Legion
|
|
|
130
130
|
instances.is_a?(Hash) ? instances : {}
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
def self.normalize_instance_config(config)
|
|
133
|
+
def self.normalize_instance_config(config)
|
|
134
134
|
normalized = config.to_h.transform_keys { |key| key.respond_to?(:to_sym) ? key.to_sym : key }
|
|
135
135
|
normalized[:anthropic_api_key] ||= normalized.delete(:api_key)
|
|
136
136
|
normalized[:anthropic_api_base] ||= normalized.delete(:base_url)
|