durable-llm 0.1.4 → 0.1.6
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/.envrc +7 -0
- data/CHANGELOG.md +5 -0
- data/CLI.md +0 -2
- data/Gemfile +7 -9
- data/README.md +564 -30
- data/Rakefile +16 -6
- data/devenv.lock +171 -0
- data/devenv.nix +12 -0
- data/devenv.yaml +8 -0
- data/durable-llm.gemspec +52 -0
- data/examples/openai_quick_complete.rb +4 -2
- data/lib/durable/llm/cli.rb +218 -22
- data/lib/durable/llm/client.rb +228 -8
- data/lib/durable/llm/configuration.rb +163 -10
- data/lib/durable/llm/convenience.rb +102 -0
- data/lib/durable/llm/errors.rb +185 -0
- data/lib/durable/llm/provider_utilities.rb +201 -0
- data/lib/durable/llm/providers/anthropic.rb +232 -24
- data/lib/durable/llm/providers/azure_openai.rb +347 -0
- data/lib/durable/llm/providers/base.rb +220 -11
- data/lib/durable/llm/providers/cohere.rb +157 -11
- data/lib/durable/llm/providers/deepseek.rb +233 -0
- data/lib/durable/llm/providers/fireworks.rb +304 -0
- data/lib/durable/llm/providers/google.rb +327 -0
- data/lib/durable/llm/providers/groq.rb +133 -25
- data/lib/durable/llm/providers/huggingface.rb +120 -17
- data/lib/durable/llm/providers/mistral.rb +431 -0
- data/lib/durable/llm/providers/openai.rb +150 -4
- data/lib/durable/llm/providers/opencode.rb +253 -0
- data/lib/durable/llm/providers/openrouter.rb +256 -0
- data/lib/durable/llm/providers/perplexity.rb +273 -0
- data/lib/durable/llm/providers/together.rb +346 -0
- data/lib/durable/llm/providers/xai.rb +355 -0
- data/lib/durable/llm/providers.rb +113 -13
- data/lib/durable/llm/response_helpers.rb +185 -0
- data/lib/durable/llm/version.rb +5 -1
- data/lib/durable/llm.rb +214 -1
- data/lib/durable.rb +29 -4
- data/sig/durable/llm.rbs +303 -1
- metadata +106 -28
- data/Gemfile.lock +0 -103
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file implements the Cohere provider for accessing Cohere's language models through their API.
|
|
4
|
+
|
|
1
5
|
require 'faraday'
|
|
2
6
|
require 'json'
|
|
3
7
|
require 'durable/llm/errors'
|
|
4
8
|
require 'durable/llm/providers/base'
|
|
9
|
+
require 'event_stream_parser'
|
|
5
10
|
|
|
6
11
|
module Durable
|
|
7
12
|
module Llm
|
|
8
13
|
module Providers
|
|
14
|
+
# Cohere provider for accessing Cohere's language models
|
|
15
|
+
#
|
|
16
|
+
# This class provides completion, embedding, and streaming capabilities
|
|
17
|
+
# for Cohere's API, including proper error handling and response normalization.
|
|
9
18
|
class Cohere < Durable::Llm::Providers::Base
|
|
10
19
|
BASE_URL = 'https://api.cohere.ai/v2'
|
|
11
20
|
|
|
@@ -16,7 +25,7 @@ module Durable
|
|
|
16
25
|
attr_accessor :api_key
|
|
17
26
|
|
|
18
27
|
def initialize(api_key: nil)
|
|
19
|
-
|
|
28
|
+
super(api_key: api_key)
|
|
20
29
|
@conn = Faraday.new(url: BASE_URL) do |faraday|
|
|
21
30
|
faraday.request :json
|
|
22
31
|
faraday.response :json
|
|
@@ -34,10 +43,37 @@ module Durable
|
|
|
34
43
|
handle_response(response)
|
|
35
44
|
end
|
|
36
45
|
|
|
46
|
+
def stream(options)
|
|
47
|
+
options[:stream] = true
|
|
48
|
+
|
|
49
|
+
response = @conn.post('chat') do |req|
|
|
50
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
|
51
|
+
req.headers['Accept'] = 'text/event-stream'
|
|
52
|
+
req.body = options
|
|
53
|
+
|
|
54
|
+
user_proc = proc do |chunk, _size, _total|
|
|
55
|
+
yield CohereStreamResponse.new(chunk)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
req.options.on_data = to_json_stream(user_proc: user_proc)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
handle_response(response)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def embedding(model:, input:, **options)
|
|
65
|
+
response = @conn.post('embed') do |req|
|
|
66
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
|
67
|
+
req.headers['Content-Type'] = 'application/json'
|
|
68
|
+
req.body = { model: model, texts: Array(input), input_type: 'search_document', **options }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
handle_response(response, CohereEmbeddingResponse)
|
|
72
|
+
end
|
|
73
|
+
|
|
37
74
|
def models
|
|
38
|
-
response = @conn.get('models') do |req|
|
|
75
|
+
response = @conn.get('../v1/models') do |req|
|
|
39
76
|
req.headers['Authorization'] = "Bearer #{@api_key}"
|
|
40
|
-
req.headers['OpenAI-Organization'] = @organization if @organization
|
|
41
77
|
end
|
|
42
78
|
|
|
43
79
|
data = handle_response(response).raw_response
|
|
@@ -45,28 +81,73 @@ module Durable
|
|
|
45
81
|
end
|
|
46
82
|
|
|
47
83
|
def self.stream?
|
|
48
|
-
|
|
84
|
+
true
|
|
49
85
|
end
|
|
50
86
|
|
|
51
87
|
private
|
|
52
88
|
|
|
53
|
-
|
|
89
|
+
# CODE-FROM: ruby-openai @ https://github.com/alexrudall/ruby-openai/blob/main/lib/openai/http.rb
|
|
90
|
+
# MIT License: https://github.com/alexrudall/ruby-openai/blob/main/LICENSE.md
|
|
91
|
+
# Given a proc, returns an outer proc that can be used to iterate over a JSON stream of chunks.
|
|
92
|
+
# For each chunk, the inner user_proc is called giving it the JSON object. The JSON object could
|
|
93
|
+
# be a data object or an error object as described in the Cohere API documentation.
|
|
94
|
+
#
|
|
95
|
+
# @param user_proc [Proc] The inner proc to call for each JSON object in the chunk.
|
|
96
|
+
# @return [Proc] An outer proc that iterates over a raw stream, converting it to JSON.
|
|
97
|
+
def to_json_stream(user_proc:)
|
|
98
|
+
parser = EventStreamParser::Parser.new
|
|
99
|
+
|
|
100
|
+
proc do |chunk, _bytes, env|
|
|
101
|
+
if env && env.status != 200
|
|
102
|
+
raise_error = Faraday::Response::RaiseError.new
|
|
103
|
+
raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
parser.feed(chunk) do |_type, data|
|
|
107
|
+
user_proc.call(JSON.parse(data)) unless data == '[DONE]'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def try_parse_json(maybe_json)
|
|
113
|
+
JSON.parse(maybe_json)
|
|
114
|
+
rescue JSON::ParserError
|
|
115
|
+
maybe_json
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# END-CODE-FROM
|
|
119
|
+
|
|
120
|
+
def handle_response(response, response_class = CohereResponse)
|
|
54
121
|
case response.status
|
|
55
122
|
when 200..299
|
|
56
|
-
|
|
123
|
+
response_class.new(response.body)
|
|
57
124
|
when 401
|
|
58
|
-
raise Durable::Llm::AuthenticationError, response
|
|
125
|
+
raise Durable::Llm::AuthenticationError, parse_error_message(response)
|
|
59
126
|
when 429
|
|
60
|
-
raise Durable::Llm::RateLimitError, response
|
|
127
|
+
raise Durable::Llm::RateLimitError, parse_error_message(response)
|
|
61
128
|
when 400..499
|
|
62
|
-
raise Durable::Llm::InvalidRequestError, response
|
|
129
|
+
raise Durable::Llm::InvalidRequestError, parse_error_message(response)
|
|
63
130
|
when 500..599
|
|
64
|
-
raise Durable::Llm::ServerError, response
|
|
131
|
+
raise Durable::Llm::ServerError, parse_error_message(response)
|
|
65
132
|
else
|
|
66
133
|
raise Durable::Llm::APIError, "Unexpected response code: #{response.status}"
|
|
67
134
|
end
|
|
68
135
|
end
|
|
69
136
|
|
|
137
|
+
def parse_error_message(response)
|
|
138
|
+
body = begin
|
|
139
|
+
JSON.parse(response.body)
|
|
140
|
+
rescue StandardError
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
message = body&.dig('message') || response.body
|
|
144
|
+
"#{response.status} Error: #{message}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Response object for Cohere chat API responses.
|
|
148
|
+
#
|
|
149
|
+
# Wraps the raw response and provides a consistent interface for accessing
|
|
150
|
+
# message content and metadata.
|
|
70
151
|
class CohereResponse
|
|
71
152
|
attr_reader :raw_response
|
|
72
153
|
|
|
@@ -75,7 +156,7 @@ module Durable
|
|
|
75
156
|
end
|
|
76
157
|
|
|
77
158
|
def choices
|
|
78
|
-
|
|
159
|
+
@raw_response.dig('message', 'content')&.map { |generation| CohereChoice.new(generation) } || []
|
|
79
160
|
end
|
|
80
161
|
|
|
81
162
|
def to_s
|
|
@@ -83,6 +164,9 @@ module Durable
|
|
|
83
164
|
end
|
|
84
165
|
end
|
|
85
166
|
|
|
167
|
+
# Represents a single choice in a Cohere response.
|
|
168
|
+
#
|
|
169
|
+
# Contains the generated text content.
|
|
86
170
|
class CohereChoice
|
|
87
171
|
attr_reader :text
|
|
88
172
|
|
|
@@ -94,7 +178,69 @@ module Durable
|
|
|
94
178
|
@text
|
|
95
179
|
end
|
|
96
180
|
end
|
|
181
|
+
|
|
182
|
+
# Response object for Cohere embedding API responses.
|
|
183
|
+
#
|
|
184
|
+
# Wraps embedding data and provides array access to the vector representation.
|
|
185
|
+
class CohereEmbeddingResponse
|
|
186
|
+
attr_reader :embedding
|
|
187
|
+
|
|
188
|
+
def initialize(data)
|
|
189
|
+
@embedding = data.dig('embeddings', 'float', 0)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def to_a
|
|
193
|
+
@embedding
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Response object for streaming Cohere chat chunks.
|
|
198
|
+
#
|
|
199
|
+
# Wraps individual chunks from the Server-Sent Events stream.
|
|
200
|
+
class CohereStreamResponse
|
|
201
|
+
attr_reader :choices
|
|
202
|
+
|
|
203
|
+
def initialize(parsed)
|
|
204
|
+
@choices = [CohereStreamChoice.new(parsed['delta'])]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def to_s
|
|
208
|
+
@choices.map(&:to_s).join(' ')
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Represents a single choice in a streaming Cohere response chunk.
|
|
213
|
+
#
|
|
214
|
+
# Contains the delta (incremental content) for the choice.
|
|
215
|
+
class CohereStreamChoice
|
|
216
|
+
attr_reader :delta
|
|
217
|
+
|
|
218
|
+
def initialize(delta)
|
|
219
|
+
@delta = CohereStreamDelta.new(delta)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def to_s
|
|
223
|
+
@delta.to_s
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Represents the incremental content delta in a streaming response.
|
|
228
|
+
#
|
|
229
|
+
# Contains the text content of the delta.
|
|
230
|
+
class CohereStreamDelta
|
|
231
|
+
attr_reader :text
|
|
232
|
+
|
|
233
|
+
def initialize(delta)
|
|
234
|
+
@text = delta['text']
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def to_s
|
|
238
|
+
@text || ''
|
|
239
|
+
end
|
|
240
|
+
end
|
|
97
241
|
end
|
|
98
242
|
end
|
|
99
243
|
end
|
|
100
244
|
end
|
|
245
|
+
|
|
246
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DeepSeek provider for language model API access with completion, embedding, and streaming support.
|
|
4
|
+
|
|
5
|
+
require 'faraday'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'event_stream_parser'
|
|
8
|
+
require 'durable/llm/errors'
|
|
9
|
+
require 'durable/llm/providers/base'
|
|
10
|
+
|
|
11
|
+
module Durable
|
|
12
|
+
module Llm
|
|
13
|
+
module Providers
|
|
14
|
+
# DeepSeek provider for language model API interactions
|
|
15
|
+
class DeepSeek < Durable::Llm::Providers::Base
|
|
16
|
+
BASE_URL = 'https://api.deepseek.com'
|
|
17
|
+
|
|
18
|
+
def default_api_key
|
|
19
|
+
Durable::Llm.configuration.deepseek&.api_key || ENV['DEEPSEEK_API_KEY']
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_accessor :api_key
|
|
23
|
+
|
|
24
|
+
def initialize(api_key: nil)
|
|
25
|
+
super()
|
|
26
|
+
@api_key = api_key || default_api_key
|
|
27
|
+
@conn = Faraday.new(url: BASE_URL) do |faraday|
|
|
28
|
+
faraday.request :json
|
|
29
|
+
faraday.response :json
|
|
30
|
+
faraday.adapter Faraday.default_adapter
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def completion(options)
|
|
35
|
+
response = @conn.post('chat/completions') do |req|
|
|
36
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
|
37
|
+
req.body = options
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
handle_response(response)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def embedding(model:, input:, **options)
|
|
44
|
+
response = @conn.post('embeddings') do |req|
|
|
45
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
|
46
|
+
req.body = { model: model, input: input, **options }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
handle_response(response, DeepSeekEmbeddingResponse)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def models
|
|
53
|
+
response = @conn.get('models') do |req|
|
|
54
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
handle_response(response).data.map { |model| model['id'] }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.stream?
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stream(options)
|
|
65
|
+
opts = options.dup
|
|
66
|
+
opts[:stream] = true
|
|
67
|
+
opts['temperature'] = opts['temperature'].to_f if opts['temperature']
|
|
68
|
+
|
|
69
|
+
@conn.post('chat/completions') do |req|
|
|
70
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
|
71
|
+
req.headers['Accept'] = 'text/event-stream'
|
|
72
|
+
req.body = opts
|
|
73
|
+
req.options.on_data = to_json_stream(user_proc: proc { |chunk| yield DeepSeekStreamResponse.new(chunk) })
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# CODE-FROM: ruby-openai @ https://github.com/alexrudall/ruby-openai/blob/main/lib/openai/http.rb
|
|
80
|
+
# MIT License: https://github.com/alexrudall/ruby-openai/blob/main/LICENSE.md
|
|
81
|
+
def to_json_stream(user_proc:)
|
|
82
|
+
parser = EventStreamParser::Parser.new
|
|
83
|
+
|
|
84
|
+
proc do |chunk, _bytes, env|
|
|
85
|
+
if env && env.status != 200
|
|
86
|
+
raise_error = Faraday::Response::RaiseError.new
|
|
87
|
+
raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
parser.feed(chunk) do |_type, data|
|
|
91
|
+
user_proc.call(JSON.parse(data)) unless data == '[DONE]'
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def try_parse_json(maybe_json)
|
|
97
|
+
JSON.parse(maybe_json)
|
|
98
|
+
rescue JSON::ParserError
|
|
99
|
+
maybe_json
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# END-CODE-FROM
|
|
103
|
+
|
|
104
|
+
def handle_response(response, response_class = DeepSeekResponse)
|
|
105
|
+
case response.status
|
|
106
|
+
when 200..299 then response_class.new(response.body)
|
|
107
|
+
when 401 then raise Durable::Llm::AuthenticationError, parse_error_message(response)
|
|
108
|
+
when 429 then raise Durable::Llm::RateLimitError, parse_error_message(response)
|
|
109
|
+
when 400..499 then raise Durable::Llm::InvalidRequestError, parse_error_message(response)
|
|
110
|
+
when 500..599 then raise Durable::Llm::ServerError, parse_error_message(response)
|
|
111
|
+
else raise Durable::Llm::APIError, "Unexpected response code: #{response.status}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_error_message(response)
|
|
116
|
+
body = begin
|
|
117
|
+
JSON.parse(response.body)
|
|
118
|
+
rescue StandardError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
message = body&.dig('error', 'message') || response.body
|
|
122
|
+
"#{response.status} Error: #{message}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Response wrapper for DeepSeek API responses
|
|
126
|
+
class DeepSeekResponse
|
|
127
|
+
attr_reader :raw_response
|
|
128
|
+
|
|
129
|
+
def initialize(response)
|
|
130
|
+
@raw_response = response
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def choices
|
|
134
|
+
@raw_response['choices'].map { |choice| DeepSeekChoice.new(choice) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def data
|
|
138
|
+
@raw_response['data']
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def to_s
|
|
142
|
+
choices.map(&:to_s).join(' ')
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Choice wrapper for DeepSeek response choices
|
|
147
|
+
class DeepSeekChoice
|
|
148
|
+
attr_reader :message, :finish_reason
|
|
149
|
+
|
|
150
|
+
def initialize(choice)
|
|
151
|
+
@message = DeepSeekMessage.new(choice['message'])
|
|
152
|
+
@finish_reason = choice['finish_reason']
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def to_s
|
|
156
|
+
@message.to_s
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Message wrapper for DeepSeek messages
|
|
161
|
+
class DeepSeekMessage
|
|
162
|
+
attr_reader :role, :content
|
|
163
|
+
|
|
164
|
+
def initialize(message)
|
|
165
|
+
@role = message['role']
|
|
166
|
+
@content = message['content']
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def to_s
|
|
170
|
+
@content
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Stream response wrapper for DeepSeek streaming
|
|
175
|
+
class DeepSeekStreamResponse
|
|
176
|
+
attr_reader :choices
|
|
177
|
+
|
|
178
|
+
def initialize(parsed)
|
|
179
|
+
@choices = DeepSeekStreamChoice.new(parsed['choices'])
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def to_s
|
|
183
|
+
@choices.to_s
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Embedding response wrapper for DeepSeek embeddings
|
|
188
|
+
class DeepSeekEmbeddingResponse
|
|
189
|
+
attr_reader :embedding
|
|
190
|
+
|
|
191
|
+
def initialize(data)
|
|
192
|
+
@embedding = data.dig('data', 0, 'embedding')
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def to_a
|
|
196
|
+
@embedding
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Stream choice wrapper for DeepSeek streaming
|
|
201
|
+
class DeepSeekStreamChoice
|
|
202
|
+
attr_reader :delta, :finish_reason
|
|
203
|
+
|
|
204
|
+
def initialize(choice)
|
|
205
|
+
@choice = [choice].flatten.first
|
|
206
|
+
@delta = DeepSeekStreamDelta.new(@choice['delta'])
|
|
207
|
+
@finish_reason = @choice['finish_reason']
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def to_s
|
|
211
|
+
@delta.to_s
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Stream delta wrapper for DeepSeek streaming
|
|
216
|
+
class DeepSeekStreamDelta
|
|
217
|
+
attr_reader :role, :content
|
|
218
|
+
|
|
219
|
+
def initialize(delta)
|
|
220
|
+
@role = delta['role']
|
|
221
|
+
@content = delta['content']
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def to_s
|
|
225
|
+
@content || ''
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|