lex-llm 0.7.6 → 0.8.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 +87 -0
- data/RULES.md +97 -0
- data/lib/legion/extensions/llm/auto_registration.rb +4 -13
- data/lib/legion/extensions/llm/canonical/chunk.rb +106 -92
- data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
- data/lib/legion/extensions/llm/canonical/message.rb +52 -67
- data/lib/legion/extensions/llm/canonical/params.rb +59 -33
- data/lib/legion/extensions/llm/canonical/request.rb +67 -54
- data/lib/legion/extensions/llm/canonical/response.rb +61 -76
- data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
- data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
- data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
- data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
- data/lib/legion/extensions/llm/canonical.rb +6 -5
- data/lib/legion/extensions/llm/configuration.rb +40 -10
- data/lib/legion/extensions/llm/connection.rb +8 -30
- data/lib/legion/extensions/llm/credential_sources.rb +32 -49
- data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
- data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
- data/lib/legion/extensions/llm/error.rb +0 -14
- data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
- data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
- data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
- data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -156
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
- data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
- data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
- data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
- data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
- data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
- data/lib/legion/extensions/llm/inventory/records.rb +36 -100
- data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
- data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -182
- data/lib/legion/extensions/llm/provider.rb +192 -315
- data/lib/legion/extensions/llm/provider_contract.rb +25 -8
- data/lib/legion/extensions/llm/provider_settings.rb +5 -26
- data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
- data/lib/legion/extensions/llm/responses/tool_arguments.rb +34 -0
- data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
- data/lib/legion/extensions/llm/routing/records.rb +34 -17
- data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
- data/lib/legion/extensions/llm/streaming.rb +50 -35
- data/lib/legion/extensions/llm/taxonomies.rb +14 -26
- data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
- data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
- data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
- data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
- data/lib/legion/extensions/llm/utils.rb +23 -5
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +8 -98
- data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
- data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
- data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
- data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
- data/spec/legion/extensions/llm/canonical/params_spec.rb +57 -136
- data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
- data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +68 -148
- data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
- data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
- data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
- data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
- data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
- data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
- data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
- data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
- data/spec/legion/extensions/llm/error_spec.rb +2 -12
- data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +173 -95
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
- data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
- data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
- data/spec/legion/extensions/llm/inventory/records_spec.rb +82 -47
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
- data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
- data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
- data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +7 -3
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -68
- data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
- data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
- data/spec/legion/extensions/llm/provider_spec.rb +98 -78
- data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
- data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -43
- data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
- data/spec/legion/extensions/llm/utils_spec.rb +26 -7
- data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
- data/spec/legion/extensions/llm_extension_spec.rb +5 -5
- data/spec/support/fake_llm_provider.rb +45 -39
- data/spec/support/fake_ssot_harness.rb +7 -2
- metadata +11 -54
- data/lib/legion/extensions/llm/agent.rb +0 -366
- data/lib/legion/extensions/llm/aliases.json +0 -436
- data/lib/legion/extensions/llm/aliases.rb +0 -67
- data/lib/legion/extensions/llm/attachment.rb +0 -229
- data/lib/legion/extensions/llm/chat.rb +0 -354
- data/lib/legion/extensions/llm/chunk.rb +0 -10
- data/lib/legion/extensions/llm/content.rb +0 -81
- data/lib/legion/extensions/llm/context.rb +0 -33
- data/lib/legion/extensions/llm/embedding.rb +0 -33
- data/lib/legion/extensions/llm/image.rb +0 -109
- data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -310
- data/lib/legion/extensions/llm/message.rb +0 -118
- data/lib/legion/extensions/llm/mime_type.rb +0 -75
- data/lib/legion/extensions/llm/model/info.rb +0 -286
- data/lib/legion/extensions/llm/model/modalities.rb +0 -26
- data/lib/legion/extensions/llm/model/pricing.rb +0 -52
- data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
- data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
- data/lib/legion/extensions/llm/model.rb +0 -11
- data/lib/legion/extensions/llm/models.json +0 -57313
- data/lib/legion/extensions/llm/models.rb +0 -530
- data/lib/legion/extensions/llm/models_schema.json +0 -168
- data/lib/legion/extensions/llm/moderation.rb +0 -60
- data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
- data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
- data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
- data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
- data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
- data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
- data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
- data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
- data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
- data/lib/legion/extensions/llm/thinking.rb +0 -53
- data/lib/legion/extensions/llm/tokens.rb +0 -51
- data/lib/legion/extensions/llm/tool_call.rb +0 -34
- data/lib/legion/extensions/llm/transcription.rb +0 -39
- data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
- data/spec/legion/extensions/llm/agent_spec.rb +0 -179
- data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
- data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
- data/spec/legion/extensions/llm/context_spec.rb +0 -127
- data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -340
- data/spec/legion/extensions/llm/message_spec.rb +0 -64
- data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
- data/spec/legion/extensions/llm/models_spec.rb +0 -104
- data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
- data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
- data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
- data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
- data/spec/legion/extensions/llm/routing/registry_event_spec.rb +0 -120
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
# Core embedding interface.
|
|
7
|
-
class Embedding
|
|
8
|
-
attr_reader :vectors, :model, :input_tokens
|
|
9
|
-
|
|
10
|
-
def initialize(vectors:, model:, input_tokens: 0)
|
|
11
|
-
@vectors = vectors
|
|
12
|
-
@model = model
|
|
13
|
-
@input_tokens = input_tokens
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.embed(text, # rubocop:disable Metrics/ParameterLists
|
|
17
|
-
model: nil,
|
|
18
|
-
provider: nil,
|
|
19
|
-
assume_model_exists: false,
|
|
20
|
-
context: nil,
|
|
21
|
-
dimensions: nil)
|
|
22
|
-
config = context&.config || Legion::Extensions::Llm.config
|
|
23
|
-
model ||= config.default_embedding_model
|
|
24
|
-
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
25
|
-
config: config)
|
|
26
|
-
model_id = model.id
|
|
27
|
-
|
|
28
|
-
provider_instance.embed(text:, model: model_id, dimensions:)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
# Represents a generated image from an AI model.
|
|
7
|
-
class Image
|
|
8
|
-
attr_reader :url, :data, :mime_type, :revised_prompt, :model_id, :usage
|
|
9
|
-
|
|
10
|
-
def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model_id: nil, usage: {}) # rubocop:disable Metrics/ParameterLists
|
|
11
|
-
@url = url
|
|
12
|
-
@data = data
|
|
13
|
-
@mime_type = mime_type
|
|
14
|
-
@revised_prompt = revised_prompt
|
|
15
|
-
@model_id = model_id
|
|
16
|
-
@usage = usage
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def base64?
|
|
20
|
-
!@data.nil?
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def to_blob
|
|
24
|
-
if base64?
|
|
25
|
-
Base64.decode64 @data
|
|
26
|
-
else
|
|
27
|
-
response = Connection.basic.get @url
|
|
28
|
-
response.body
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def save(path)
|
|
33
|
-
File.binwrite(File.expand_path(path), to_blob)
|
|
34
|
-
path
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.paint(prompt, # rubocop:disable Metrics/ParameterLists
|
|
38
|
-
model: nil,
|
|
39
|
-
provider: nil,
|
|
40
|
-
assume_model_exists: false,
|
|
41
|
-
size: '1024x1024',
|
|
42
|
-
context: nil,
|
|
43
|
-
with: nil,
|
|
44
|
-
mask: nil,
|
|
45
|
-
params: {})
|
|
46
|
-
config = context&.config || Legion::Extensions::Llm.config
|
|
47
|
-
model ||= config.default_image_model
|
|
48
|
-
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
49
|
-
config: config)
|
|
50
|
-
model_id = model.id
|
|
51
|
-
|
|
52
|
-
provider_instance.paint(prompt, model: model_id, size:, with:, mask:, params:)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def total_cost
|
|
56
|
-
input_cost + output_cost
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def input_cost
|
|
60
|
-
return flat_input_cost unless detailed_input_usage?
|
|
61
|
-
|
|
62
|
-
text_input_cost + image_input_cost
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def output_cost
|
|
66
|
-
tokens_for('output_tokens') * output_token_price.to_f / 1_000_000
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def model_info
|
|
70
|
-
@model_info ||= Legion::Extensions::Llm.models.find(model_id)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
def flat_input_cost
|
|
76
|
-
tokens_for('input_tokens') * model_info.input_price_per_million.to_f / 1_000_000
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def text_input_cost
|
|
80
|
-
input_detail('text_tokens') * model_info.input_price_per_million.to_f / 1_000_000
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def image_input_cost
|
|
84
|
-
input_detail('image_tokens') * image_input_token_price.to_f / 1_000_000
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def output_token_price
|
|
88
|
-
model_info.pricing.images.output || model_info.output_price_per_million
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def image_input_token_price
|
|
92
|
-
model_info.pricing.images.input || model_info.input_price_per_million
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def detailed_input_usage?
|
|
96
|
-
usage['input_tokens_details'].is_a?(Hash)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def input_detail(key)
|
|
100
|
-
usage.dig('input_tokens_details', key).to_i
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def tokens_for(key)
|
|
104
|
-
usage[key].to_i
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../capabilities'
|
|
4
|
-
|
|
5
|
-
module Legion
|
|
6
|
-
module Extensions
|
|
7
|
-
module Llm
|
|
8
|
-
module Inventory
|
|
9
|
-
# Inventory-side capability normalization. Every per-gem DiscoveryRefresh actor
|
|
10
|
-
# calls this from `lanes_from_instance` to coerce offering capabilities into a
|
|
11
|
-
# canonical symbol list before writing the lane to Inventory. Without this
|
|
12
|
-
# constant, every actor's guard `return [] unless defined?(...)` fires and the
|
|
13
|
-
# lane is written with `capabilities: []` — even when the operator declared
|
|
14
|
-
# `enable_tools: true` / `enable_thinking: true` on the instance and the
|
|
15
|
-
# provider's CapabilityPolicy correctly resolved them in the offering.
|
|
16
|
-
#
|
|
17
|
-
# Delegates to Legion::Extensions::Llm::Capabilities so the alias table
|
|
18
|
-
# (function_calling/tool_use/tool_calls/tool/functions → tools, etc.) stays
|
|
19
|
-
# in one place.
|
|
20
|
-
module Capabilities
|
|
21
|
-
ALIASES = Legion::Extensions::Llm::Capabilities::ALIASES
|
|
22
|
-
|
|
23
|
-
module_function
|
|
24
|
-
|
|
25
|
-
def normalize(caps, **)
|
|
26
|
-
Legion::Extensions::Llm::Capabilities.normalize(caps, **)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def merge(*sets, **)
|
|
30
|
-
Legion::Extensions::Llm::Capabilities.merge(*sets, **)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def include_all?(available, required, **)
|
|
34
|
-
Legion::Extensions::Llm::Capabilities.include_all?(available, required, **)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/logging'
|
|
4
|
-
require 'legion/extensions/llm/inventory/errors'
|
|
5
|
-
require 'legion/extensions/llm/inventory/identity'
|
|
6
|
-
require 'legion/extensions/llm/inventory/snapshot'
|
|
7
|
-
require 'legion/extensions/llm/taxonomies'
|
|
8
|
-
|
|
9
|
-
module Legion
|
|
10
|
-
module Extensions
|
|
11
|
-
module Llm
|
|
12
|
-
module Inventory
|
|
13
|
-
# DEPRECATED (Phase 4 removal): the only Phase 1 file permitted to hold a
|
|
14
|
-
# direct `Legion::LLM::Inventory` reverse reference. Old released provider
|
|
15
|
-
# actors keep using ScopedRefresher#tick until their Phase 2 migration; a
|
|
16
|
-
# migrated actor instead injects ScopedRefresher::LegacyCoordinatorAdapter
|
|
17
|
-
# into Inventory::Publisher and never calls #tick. Neither old lane hashes
|
|
18
|
-
# nor old health feed the new registry. Phase 4 deletes this file once
|
|
19
|
-
# every provider and coordinator floor has migrated.
|
|
20
|
-
#
|
|
21
|
-
# Mix into a Legion::Extensions::Llm::*::Actors::DiscoveryRefresh class.
|
|
22
|
-
# The host class must include Legion::Extensions::Helpers::Lex (auto-injects
|
|
23
|
-
# log / settings / handle_exception / cache_*) and define:
|
|
24
|
-
# - #scope_key — Hash like { provider: :vllm, instance: instance_id }
|
|
25
|
-
# - #compute_lanes_for_scope — Array<Hash> lane fact-sheets (no health, no
|
|
26
|
-
# lane_weight — added by Inventory.write_lane).
|
|
27
|
-
# Each lane MUST set :id via compose_id.
|
|
28
|
-
# - #credential_hash — String identifying the auth credential for this scope
|
|
29
|
-
# (used by the auth-failure cooldown circuit).
|
|
30
|
-
module ScopedRefresher
|
|
31
|
-
# Auth-failure cooldown TTL (5 minutes). Operator can fix the credential
|
|
32
|
-
# and lanes auto-recover on the next tick after expiry.
|
|
33
|
-
AUTH_COOLDOWN_TTL = 300
|
|
34
|
-
|
|
35
|
-
# G22: 5-part lane id composed here and ONLY here. All gem writers MUST call
|
|
36
|
-
# this helper; Inventory.write_lane rejects any lane with a missing or malformed :id.
|
|
37
|
-
# Accepts a Hash (or keyword splat) with keys: tier, provider_family, instance_id, type, model.
|
|
38
|
-
def self.compose_id(lane_fields)
|
|
39
|
-
t = lane_fields[:tier]
|
|
40
|
-
pf = lane_fields[:provider_family]
|
|
41
|
-
ii = lane_fields[:instance_id]
|
|
42
|
-
ty = lane_fields[:type]
|
|
43
|
-
mo = lane_fields[:model]
|
|
44
|
-
"#{t}:#{pf}:#{ii}:#{ty}:#{mo}"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# G7 write-then-delete-orphans: write new lanes FIRST (eliminates zero-results
|
|
48
|
-
# race window), then delete orphans from the previous scope snapshot.
|
|
49
|
-
def tick(**)
|
|
50
|
-
return if auth_cooldown_active?
|
|
51
|
-
|
|
52
|
-
new_lanes = safe_compute
|
|
53
|
-
log.info("[llm][scoped_refresher] action=tick provider=#{scope_key[:provider]} lanes_computed=#{new_lanes ? new_lanes.size : 0}")
|
|
54
|
-
return unless new_lanes&.any?
|
|
55
|
-
|
|
56
|
-
written = 0
|
|
57
|
-
new_lanes.each do |lane_fact|
|
|
58
|
-
written += 1 if Legion::LLM::Inventory.write_lane(lane: lane_fact)
|
|
59
|
-
end
|
|
60
|
-
log.info("[llm][scoped_refresher] action=tick_complete provider=#{scope_key[:provider]} lanes_computed=#{new_lanes.size} lanes_written=#{written}")
|
|
61
|
-
|
|
62
|
-
orphans = (@prev_scope_keys || []) - new_lanes.map { it[:id] }
|
|
63
|
-
orphans.each { |id| Legion::LLM::Inventory.delete_lane(id: id) }
|
|
64
|
-
|
|
65
|
-
@prev_scope_keys = new_lanes.map { it[:id] }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
# Wraps compute_lanes_for_scope with auth-failure cooldown logic.
|
|
71
|
-
# If a cooldown key is present from a previous auth failure, skips the
|
|
72
|
-
# compute entirely (no real call burned). On a new auth failure, writes the
|
|
73
|
-
# cooldown key with AUTH_COOLDOWN_TTL so subsequent ticks also skip.
|
|
74
|
-
def safe_compute
|
|
75
|
-
if auth_cooldown_active?
|
|
76
|
-
log.warn("[llm][scoped_refresher] action=skip reason=auth_cooldown scope=#{scope_key}")
|
|
77
|
-
return nil
|
|
78
|
-
end
|
|
79
|
-
compute_lanes_for_scope
|
|
80
|
-
rescue NotImplementedError
|
|
81
|
-
raise
|
|
82
|
-
rescue StandardError => e
|
|
83
|
-
if auth_failure?(error: e)
|
|
84
|
-
Legion::Cache::Local.set(auth_cooldown_key, 1, ttl: AUTH_COOLDOWN_TTL)
|
|
85
|
-
handle_exception(e, level: :warn, handled: true,
|
|
86
|
-
operation: 'inventory.scoped_refresher.auth_failure',
|
|
87
|
-
scope: scope_key)
|
|
88
|
-
else
|
|
89
|
-
handle_exception(e, level: :warn, handled: true,
|
|
90
|
-
operation: 'inventory.scoped_refresher.compute',
|
|
91
|
-
scope: scope_key)
|
|
92
|
-
end
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def auth_cooldown_active?
|
|
97
|
-
!Legion::Cache::Local.get(auth_cooldown_key).nil?
|
|
98
|
-
rescue StandardError
|
|
99
|
-
false
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def auth_cooldown_key
|
|
103
|
-
"llm_auth_failed:#{credential_hash}"
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Default auth-failure predicate. Matches HTTP 401/403 status codes and
|
|
107
|
-
# common auth-error message patterns. Provider gems may override this if
|
|
108
|
-
# their error shapes differ (e.g. Bedrock's AccessDeniedException).
|
|
109
|
-
def auth_failure?(error:, **)
|
|
110
|
-
return true if error.respond_to?(:status_code) && [401, 403].include?(error.status_code)
|
|
111
|
-
return true if error.respond_to?(:http_status) && [401, 403].include?(error.http_status)
|
|
112
|
-
|
|
113
|
-
error.message&.match?(/unauthorized|invalid[_ ]api[_ ]key|invalid[_ ]credentials|forbidden/i)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# One-way, post-commit projection of an already-committed new Snapshot
|
|
117
|
-
# into the old Legion::LLM::Inventory coordinator, for the mixed-version
|
|
118
|
-
# window only. It never rediscovers, selects, authorizes new state, or
|
|
119
|
-
# feeds an old fact back into the new Registry. See section 13.3.
|
|
120
|
-
class LegacyCoordinatorAdapter
|
|
121
|
-
include Legion::Logging::Helper
|
|
122
|
-
|
|
123
|
-
HEALTHY_COMPAT = { circuit_state: :closed, denied: false, available: true, adjustment: 0 }.freeze
|
|
124
|
-
LEGACY_CAPABILITIES = {
|
|
125
|
-
chat: :completion, stream_chat: :streaming, embed: :embedding, image: :image,
|
|
126
|
-
transcribe: :audio_transcription, translate: :audio_transcription, speak: :audio_speech, moderate: :moderation
|
|
127
|
-
}.freeze
|
|
128
|
-
|
|
129
|
-
def initialize(provider_family:, legacy_instance_ids_by_instance: {})
|
|
130
|
-
@provider_family = canonical_family(provider_family)
|
|
131
|
-
@legacy_instance_ids = deep_freeze_legacy_ids(legacy_instance_ids_by_instance)
|
|
132
|
-
@published_ids_by_instance = {}.freeze
|
|
133
|
-
@mutex = Mutex.new
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def sync_snapshot(snapshot:, instance_key:, mutation_reason:)
|
|
137
|
-
validate_inputs!(snapshot, instance_key, mutation_reason)
|
|
138
|
-
return :not_loaded unless legacy_inventory_available?
|
|
139
|
-
|
|
140
|
-
project(snapshot, instance_key)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
private
|
|
144
|
-
|
|
145
|
-
def project(snapshot, instance_key)
|
|
146
|
-
@mutex.synchronize { project_locked(snapshot, instance_key) }
|
|
147
|
-
:applied
|
|
148
|
-
rescue StandardError => e
|
|
149
|
-
handle_exception(
|
|
150
|
-
e, handled: true, level: :warn, operation: 'inventory.scoped_refresher.legacy_projection',
|
|
151
|
-
provider_family: @provider_family, instance_id: instance_key.instance_id,
|
|
152
|
-
source_generation: snapshot.generation
|
|
153
|
-
)
|
|
154
|
-
:failed
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def project_locked(snapshot, instance_key)
|
|
158
|
-
desired = desired_lanes(snapshot, instance_key)
|
|
159
|
-
written = write_desired(desired)
|
|
160
|
-
reconcile_deletions(instance_key, written)
|
|
161
|
-
@published_ids_by_instance = @published_ids_by_instance.merge(
|
|
162
|
-
instance_key.instance_id => written.to_a.freeze
|
|
163
|
-
).freeze
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def desired_lanes(snapshot, instance_key)
|
|
167
|
-
record = snapshot.instance(instance_key: instance_key)
|
|
168
|
-
return {} if record.nil? || record.availability.state != :available
|
|
169
|
-
|
|
170
|
-
build_desired(group_lanes(snapshot, instance_key), snapshot.generation)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def group_lanes(snapshot, instance_key)
|
|
174
|
-
groups = {}
|
|
175
|
-
snapshot.lanes_for(instance_key: instance_key).each do |lane|
|
|
176
|
-
type = Taxonomies.lane_type_for(operation: lane.operation)
|
|
177
|
-
key = [lane.tier, instance_key.provider_family, instance_key.instance_id, type, lane.model]
|
|
178
|
-
group = (groups[key] ||= new_group(instance_key, lane, type))
|
|
179
|
-
accumulate_group(group, lane)
|
|
180
|
-
end
|
|
181
|
-
groups
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def new_group(instance_key, lane, type)
|
|
185
|
-
{
|
|
186
|
-
tier: lane.tier, provider_family: instance_key.provider_family, instance_id: instance_key.instance_id,
|
|
187
|
-
type: type, model: lane.model, offering_ids: Set.new, capabilities: Set.new,
|
|
188
|
-
context_values: Set.new, max_output_values: Set.new
|
|
189
|
-
}
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def accumulate_group(group, lane)
|
|
193
|
-
group[:offering_ids] << lane.offering_id
|
|
194
|
-
mapped = LEGACY_CAPABILITIES[lane.operation]
|
|
195
|
-
group[:capabilities] << mapped if mapped
|
|
196
|
-
lane.capability_evidence.each { |capability, evidence| group[:capabilities] << capability if evidence.supported? }
|
|
197
|
-
group[:context_values] << lane.context_evidence.value if lane.context_evidence.known?
|
|
198
|
-
group[:max_output_values] << lane.max_output_evidence.value if lane.max_output_evidence.known?
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def build_desired(groups, generation)
|
|
202
|
-
by_id = {}
|
|
203
|
-
groups.each_value do |group|
|
|
204
|
-
legacy_id = ScopedRefresher.compose_id(identity_fields(group))
|
|
205
|
-
(by_id[legacy_id] ||= []) << group
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
by_id.each_with_object({}) do |(legacy_id, colliding), desired|
|
|
209
|
-
if colliding.size > 1 || colliding.first[:offering_ids].size != 1
|
|
210
|
-
log.warn("[llm][inventory][legacy_adapter] action=fail_closed reason=ambiguous_group legacy_id=#{legacy_id}")
|
|
211
|
-
next
|
|
212
|
-
end
|
|
213
|
-
desired[legacy_id] = build_lane_hash(legacy_id, colliding.first, generation)
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def identity_fields(group)
|
|
218
|
-
{
|
|
219
|
-
tier: group[:tier], provider_family: group[:provider_family],
|
|
220
|
-
instance_id: group[:instance_id], type: group[:type], model: group[:model]
|
|
221
|
-
}.freeze
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def build_lane_hash(legacy_id, group, generation)
|
|
225
|
-
{
|
|
226
|
-
id: legacy_id, tier: group[:tier], provider_family: group[:provider_family],
|
|
227
|
-
provider_instance: group[:instance_id], instance_id: group[:instance_id], type: group[:type],
|
|
228
|
-
model: group[:model], offering_id: group[:offering_ids].first,
|
|
229
|
-
capabilities: group[:capabilities].to_a.sort, limits: build_limits(group),
|
|
230
|
-
metadata: { ssot_v3_compatibility_projection: true, source_generation: generation }, enabled: true
|
|
231
|
-
}
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
def build_limits(group)
|
|
235
|
-
limits = {}
|
|
236
|
-
limits[:context_window] = group[:context_values].first if group[:context_values].size == 1
|
|
237
|
-
limits[:max_output_tokens] = group[:max_output_values].first if group[:max_output_values].size == 1
|
|
238
|
-
limits
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def write_desired(desired)
|
|
242
|
-
written = Set.new
|
|
243
|
-
desired.each do |legacy_id, lane_hash|
|
|
244
|
-
result = ::Legion::LLM::Inventory.write_lane(lane: lane_hash, health: HEALTHY_COMPAT)
|
|
245
|
-
written << legacy_id unless result.nil?
|
|
246
|
-
end
|
|
247
|
-
written
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def reconcile_deletions(instance_key, written)
|
|
251
|
-
tracked = @published_ids_by_instance.fetch(instance_key.instance_id, [])
|
|
252
|
-
candidates = (tracked.to_a + read_current_old_ids(instance_key).to_a).uniq
|
|
253
|
-
candidates.each do |old_id|
|
|
254
|
-
next if written.include?(old_id)
|
|
255
|
-
|
|
256
|
-
::Legion::LLM::Inventory.delete_lane(id: old_id)
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def read_current_old_ids(instance_key)
|
|
261
|
-
instance_ids_to_read(instance_key).each_with_object(Set.new) do |instance_id, ids|
|
|
262
|
-
lanes = ::Legion::LLM::Inventory.lanes_for(provider: @provider_family, instance: instance_id)
|
|
263
|
-
Array(lanes).each { |lane| ids << (lane.is_a?(::Hash) ? lane[:id] : lane) }
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def instance_ids_to_read(instance_key)
|
|
268
|
-
([instance_key.instance_id] + @legacy_instance_ids.fetch(instance_key.instance_id, [])).uniq
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def validate_inputs!(snapshot, instance_key, mutation_reason)
|
|
272
|
-
raise Errors::ValidationError, 'snapshot must be an Inventory::Snapshot' unless snapshot.is_a?(Snapshot)
|
|
273
|
-
unless instance_key.is_a?(Identity::InstanceKey) && instance_key.provider_family == @provider_family
|
|
274
|
-
raise Errors::ValidationError, 'instance_key must belong to the adapter provider family'
|
|
275
|
-
end
|
|
276
|
-
return if Taxonomies::MUTATION_REASONS.include?(mutation_reason)
|
|
277
|
-
|
|
278
|
-
raise Errors::ValidationError, 'mutation_reason must be a Taxonomies::MUTATION_REASONS value'
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def legacy_inventory_available?
|
|
282
|
-
defined?(::Legion::LLM::Inventory) &&
|
|
283
|
-
::Legion::LLM::Inventory.respond_to?(:lanes_for) &&
|
|
284
|
-
::Legion::LLM::Inventory.respond_to?(:write_lane) &&
|
|
285
|
-
::Legion::LLM::Inventory.respond_to?(:delete_lane)
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def canonical_family(value)
|
|
289
|
-
text = Identity.normalize_text(value: value, field: :provider_family).downcase
|
|
290
|
-
raise Errors::ValidationError, 'provider_family must be snake-case ASCII' unless text.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
291
|
-
|
|
292
|
-
text.to_sym
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def deep_freeze_legacy_ids(mapping)
|
|
296
|
-
raise Errors::ValidationError, 'legacy_instance_ids_by_instance must be a Hash' unless mapping.is_a?(::Hash)
|
|
297
|
-
|
|
298
|
-
mapping.each_with_object({}) do |(key, values), frozen|
|
|
299
|
-
raise Errors::ValidationError, 'legacy instance id keys must be Strings' unless key.is_a?(::String)
|
|
300
|
-
raise Errors::ValidationError, 'legacy superseded ids must be a nonempty Array of Strings' unless values.is_a?(::Array) && !values.empty? && values.all?(::String)
|
|
301
|
-
|
|
302
|
-
frozen[key.dup.freeze] = values.map { |value| value.dup.freeze }.freeze
|
|
303
|
-
end.freeze
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
end
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
# A single message in a chat conversation.
|
|
7
|
-
class Message
|
|
8
|
-
ROLES = %i[system user assistant tool].freeze
|
|
9
|
-
|
|
10
|
-
attr_reader :role, :model_id, :tool_calls, :tool_call_id, :raw, :thinking, :tokens, :stop_reason
|
|
11
|
-
attr_writer :content
|
|
12
|
-
|
|
13
|
-
def initialize(options = {})
|
|
14
|
-
@role = options.fetch(:role).to_sym
|
|
15
|
-
@tool_calls = options[:tool_calls]
|
|
16
|
-
@content = normalize_content(options.fetch(:content), role: @role, tool_calls: @tool_calls)
|
|
17
|
-
@model_id = options[:model_id]
|
|
18
|
-
@tool_call_id = options[:tool_call_id]
|
|
19
|
-
@tokens = options[:tokens] || Tokens.build(
|
|
20
|
-
input: options[:input_tokens],
|
|
21
|
-
output: options[:output_tokens],
|
|
22
|
-
cached: options[:cached_tokens],
|
|
23
|
-
cache_creation: options[:cache_creation_tokens],
|
|
24
|
-
thinking: options[:thinking_tokens],
|
|
25
|
-
reasoning: options[:reasoning_tokens]
|
|
26
|
-
)
|
|
27
|
-
@raw = options[:raw]
|
|
28
|
-
@thinking = options[:thinking]
|
|
29
|
-
@stop_reason = options[:stop_reason]
|
|
30
|
-
|
|
31
|
-
ensure_valid_role
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def content
|
|
35
|
-
if @content.is_a?(Content) && @content.text && @content.attachments.empty?
|
|
36
|
-
@content.text
|
|
37
|
-
else
|
|
38
|
-
@content
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def tool_call?
|
|
43
|
-
!tool_calls.nil? && !tool_calls.empty?
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def tool_result?
|
|
47
|
-
!tool_call_id.nil? && !tool_call_id.empty?
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def tool_results
|
|
51
|
-
content if tool_result?
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def input_tokens
|
|
55
|
-
tokens&.input
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def output_tokens
|
|
59
|
-
tokens&.output
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def cached_tokens
|
|
63
|
-
tokens&.cached
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def cache_creation_tokens
|
|
67
|
-
tokens&.cache_creation
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def thinking_tokens
|
|
71
|
-
tokens&.thinking
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def reasoning_tokens
|
|
75
|
-
tokens&.thinking
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def to_h
|
|
79
|
-
{
|
|
80
|
-
role: role,
|
|
81
|
-
content: content,
|
|
82
|
-
model_id: model_id,
|
|
83
|
-
tool_calls: tool_calls,
|
|
84
|
-
tool_call_id: tool_call_id
|
|
85
|
-
}.merge(tokens ? tokens.to_h : {}).compact
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def to_internal_h
|
|
89
|
-
to_h.merge(
|
|
90
|
-
thinking: thinking&.text,
|
|
91
|
-
thinking_signature: thinking&.signature,
|
|
92
|
-
raw: raw
|
|
93
|
-
).compact
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def instance_variables
|
|
97
|
-
super - [:@raw]
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
def normalize_content(content, role:, tool_calls:)
|
|
103
|
-
return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
|
|
104
|
-
|
|
105
|
-
case content
|
|
106
|
-
when String then Content.new(content)
|
|
107
|
-
when Hash then Content.new(content[:text], content)
|
|
108
|
-
else content
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def ensure_valid_role
|
|
113
|
-
raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'marcel'
|
|
4
|
-
|
|
5
|
-
module Legion
|
|
6
|
-
module Extensions
|
|
7
|
-
module Llm
|
|
8
|
-
# MimeTypes module provides methods to handle MIME types using Marcel gem
|
|
9
|
-
module MimeType
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
def for(...)
|
|
13
|
-
Marcel::MimeType.for(...)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def image?(type)
|
|
17
|
-
type.start_with?('image/')
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def video?(type)
|
|
21
|
-
type.start_with?('video/')
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def audio?(type)
|
|
25
|
-
type.start_with?('audio/')
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def pdf?(type)
|
|
29
|
-
type == 'application/pdf'
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def text?(type)
|
|
33
|
-
type.start_with?('text/') ||
|
|
34
|
-
TEXT_SUFFIXES.any? { |suffix| type.end_with?(suffix) } ||
|
|
35
|
-
NON_TEXT_PREFIX_TEXT_MIME_TYPES.include?(type)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# MIME types that have a text/ prefix but need to be handled differently
|
|
39
|
-
TEXT_SUFFIXES = ['+json', '+xml', '+html', '+yaml', '+csv', '+plain', '+javascript', '+svg'].freeze
|
|
40
|
-
|
|
41
|
-
# MIME types that don't have a text/ prefix but should be treated as text
|
|
42
|
-
NON_TEXT_PREFIX_TEXT_MIME_TYPES = [
|
|
43
|
-
'application/json', # Base type, even if specific ones end with +json
|
|
44
|
-
'application/xml', # Base type, even if specific ones end with +xml
|
|
45
|
-
'application/javascript',
|
|
46
|
-
'application/ecmascript',
|
|
47
|
-
'application/rtf',
|
|
48
|
-
'application/sql',
|
|
49
|
-
'application/x-sh',
|
|
50
|
-
'application/x-csh',
|
|
51
|
-
'application/x-httpd-php',
|
|
52
|
-
'application/sdp',
|
|
53
|
-
'application/sparql-query',
|
|
54
|
-
'application/graphql',
|
|
55
|
-
'application/yang', # Data modeling language, often serialized as XML/JSON but the type itself is distinct
|
|
56
|
-
'application/mbox', # Mailbox format
|
|
57
|
-
'application/x-tex',
|
|
58
|
-
'application/x-latex',
|
|
59
|
-
'application/x-perl',
|
|
60
|
-
'application/x-python',
|
|
61
|
-
'application/x-tcl',
|
|
62
|
-
'application/pgp-signature', # Often ASCII armored
|
|
63
|
-
'application/pgp-keys', # Often ASCII armored
|
|
64
|
-
'application/vnd.coffeescript',
|
|
65
|
-
'application/vnd.dart',
|
|
66
|
-
'application/vnd.oai.openapi', # Base for OpenAPI, often with +json or +yaml suffix
|
|
67
|
-
'application/vnd.zul', # ZK User Interface Language (can be XML-like)
|
|
68
|
-
'application/x-yaml', # Common non-standard for YAML
|
|
69
|
-
'application/yaml', # Standard for YAML
|
|
70
|
-
'application/toml' # TOML configuration files
|
|
71
|
-
].freeze
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|