llm.rb 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -8
  3. data/lib/json/schema/array.rb +22 -0
  4. data/lib/json/schema/boolean.rb +9 -0
  5. data/lib/json/schema/integer.rb +21 -0
  6. data/lib/json/schema/leaf.rb +40 -0
  7. data/lib/json/schema/null.rb +9 -0
  8. data/lib/json/schema/number.rb +21 -0
  9. data/lib/json/schema/object.rb +26 -0
  10. data/lib/json/schema/string.rb +9 -0
  11. data/lib/json/schema.rb +73 -0
  12. data/lib/llm/chat.rb +7 -3
  13. data/lib/llm/core_ext/ostruct.rb +1 -1
  14. data/lib/llm/file.rb +8 -1
  15. data/lib/llm/message.rb +7 -0
  16. data/lib/llm/model.rb +27 -2
  17. data/lib/llm/provider.rb +36 -28
  18. data/lib/llm/providers/anthropic/format.rb +19 -6
  19. data/lib/llm/providers/anthropic/models.rb +62 -0
  20. data/lib/llm/providers/anthropic.rb +22 -8
  21. data/lib/llm/providers/gemini/format.rb +6 -1
  22. data/lib/llm/providers/gemini/images.rb +3 -3
  23. data/lib/llm/providers/gemini/models.rb +69 -0
  24. data/lib/llm/providers/gemini/response_parser.rb +1 -5
  25. data/lib/llm/providers/gemini.rb +30 -5
  26. data/lib/llm/providers/ollama/format.rb +11 -3
  27. data/lib/llm/providers/ollama/models.rb +66 -0
  28. data/lib/llm/providers/ollama.rb +30 -8
  29. data/lib/llm/providers/openai/audio.rb +0 -2
  30. data/lib/llm/providers/openai/format.rb +6 -1
  31. data/lib/llm/providers/openai/images.rb +1 -1
  32. data/lib/llm/providers/openai/models.rb +62 -0
  33. data/lib/llm/providers/openai/response_parser.rb +1 -5
  34. data/lib/llm/providers/openai/responses.rb +12 -6
  35. data/lib/llm/providers/openai.rb +37 -7
  36. data/lib/llm/response/modellist.rb +18 -0
  37. data/lib/llm/response.rb +1 -0
  38. data/lib/llm/version.rb +1 -1
  39. data/lib/llm.rb +2 -1
  40. data/spec/anthropic/completion_spec.rb +36 -0
  41. data/spec/anthropic/models_spec.rb +21 -0
  42. data/spec/gemini/images_spec.rb +4 -12
  43. data/spec/gemini/models_spec.rb +21 -0
  44. data/spec/llm/conversation_spec.rb +71 -3
  45. data/spec/ollama/models_spec.rb +20 -0
  46. data/spec/openai/completion_spec.rb +19 -0
  47. data/spec/openai/images_spec.rb +2 -6
  48. data/spec/openai/models_spec.rb +21 -0
  49. metadata +20 -6
  50. data/share/llm/models/anthropic.yml +0 -35
  51. data/share/llm/models/gemini.yml +0 -35
  52. data/share/llm/models/ollama.yml +0 -155
  53. data/share/llm/models/openai.yml +0 -46
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c55653b476d2fe6fe9457c89bc430c698668312ce89660a1d69abd8adf338eb
4
- data.tar.gz: fe7d456bbb739eb091e82351839baef4c64d1d108a2c4cd7de3eb1b478982631
3
+ metadata.gz: 3d8311b461c49095ba393be6d3456bf7c3032a07e4480f4fd3e4cd9133f2b6e7
4
+ data.tar.gz: 5f9ce812a4a27e0982ea5072bca3f0494317ee0274987e57df792cc0e0d2fcfc
5
5
  SHA512:
6
- metadata.gz: 8cd55bb28eb92fea745d8b11062b2442bf4b2de88ecfb0b7dc99cfefd293bd45113088dd13ccfe7e251d2e369459da700f15725bae51c3d31d4bf68e19953138
7
- data.tar.gz: dab47021b94d00e51e7d0ca3f92e2966170b9fd8ce7138e0728d2be7fb83da03104ff93cd7c54b760acca62dd03adf16462069db9eb5c30185743c25259105aa
6
+ metadata.gz: 1d860cabe75a0718e0c06d686127d6d24c65b4dc8967aaf490b892bfad10bf88268791e9138b26e2d1c646a22e2eda7e74ad48e1fa31e9cb68c42de7ec53ac12
7
+ data.tar.gz: 8c2e14a316e87560e5d5dc3cdb416d151172ae12e2244bb813ff1bba61c582474607bd6920f82fa5f080e628b2dac319dd20d4f74eff55940f9c88ab7544b327
data/README.md CHANGED
@@ -38,7 +38,9 @@ The following example enables lazy mode for a
38
38
  object by entering into a "lazy" conversation where messages are buffered and
39
39
  sent to the provider only when necessary. Both lazy and non-lazy conversations
40
40
  maintain a message thread that can be reused as context throughout a conversation.
41
- The example uses the stateless chat completions API that all LLM providers support:
41
+ The example captures the spirit of llm.rb by demonstrating how objects cooperate
42
+ together through composition, and it uses the stateless chat completions API that
43
+ all LLM providers support:
42
44
 
43
45
  ```ruby
44
46
  #!/usr/bin/env ruby
@@ -108,6 +110,46 @@ bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
108
110
  # The answer to ((5 + 15) * 2) / 10 is 4.
109
111
  ```
110
112
 
113
+ ### Schema
114
+
115
+ #### Structured
116
+
117
+ All LLM providers except Anthropic allow a client to describe the structure
118
+ of a response that a LLM emits according to a schema that is described by JSON.
119
+ The schema lets a client describe what JSON object (or value) an LLM should emit,
120
+ and the LLM will abide by the schema. See also: [JSON Schema website](https://json-schema.org/overview/what-is-jsonschema).
121
+
122
+ True to the llm.rb spirit of doing one thing well, and solving problems through the
123
+ composition of objects, the generation of a schema is delegated to another object
124
+ who is responsible for and an expert in the generation of JSON schemas. We will use
125
+ the
126
+ [llmrb/json-schema](https://github.com/llmrb/json-schema)
127
+ library for the sake of the examples - it is an optional dependency that is loaded
128
+ on-demand. At least for the time being it is not necessary to install it separately.
129
+ The interface is designed so you could drop in any other library in its place:
130
+
131
+ ```ruby
132
+ #!/usr/bin/env ruby
133
+ require "llm"
134
+
135
+ llm = LLM.openai(ENV["KEY"])
136
+ schema = llm.schema.object({os: llm.schema.string.enum("OpenBSD", "FreeBSD", "NetBSD").required})
137
+ bot = LLM::Chat.new(llm, schema:)
138
+ bot.chat "You secretly love NetBSD", :system
139
+ bot.chat "What operating system is the best?", :user
140
+ bot.messages.find(&:assistant?).content! # => {os: "NetBSD"}
141
+
142
+ schema = llm.schema.object({answer: llm.schema.integer.required})
143
+ bot = LLM::Chat.new(llm, schema:)
144
+ bot.chat "Tell me the answer to ((5 + 5) / 2)", :user
145
+ bot.messages.find(&:assistant?).content! # => {answer: 5}
146
+
147
+ schema = llm.schema.object({probability: llm.schema.number.required})
148
+ bot = LLM::Chat.new(llm, schema:)
149
+ bot.chat "Does the earth orbit the sun?", :user
150
+ bot.messages.find(&:assistant?).content! # => {probability: 1}
151
+ ```
152
+
111
153
  ### Audio
112
154
 
113
155
  #### Speech
@@ -126,8 +168,7 @@ require "llm"
126
168
 
127
169
  llm = LLM.openai(ENV["KEY"])
128
170
  res = llm.audio.create_speech(input: "Hello world")
129
- File.binwrite File.join(Dir.home, "hello.mp3"),
130
- res.audio.string
171
+ IO.copy_stream res.audio, File.join(Dir.home, "hello.mp3")
131
172
  ```
132
173
 
133
174
  #### Transcribe
@@ -325,7 +366,7 @@ also understand URLs, and various file types (eg images, audio, video,
325
366
  etc). The llm.rb approach to multimodal prompts is to let you pass `URI`
326
367
  objects to describe links, `LLM::File` / `LLM::Response::File` objects
327
368
  to describe files, `String` objects to describe text blobs, or an array
328
- of the forementioned objects to describe multiple objects in a single
369
+ of the aforementioned objects to describe multiple objects in a single
329
370
  prompt. Each object is a first class citizen that can be passed directly
330
371
  to a prompt.
331
372
 
@@ -388,6 +429,38 @@ print res.embeddings[0].size, "\n"
388
429
  # 1536
389
430
  ```
390
431
 
432
+ ### Models
433
+
434
+ #### List
435
+
436
+ Almost all LLM providers provide a models endpoint that allows a client to
437
+ query the list of models that are available to use. The list is dynamic,
438
+ maintained by LLM providers, and it is independent of a specific llm.rb release.
439
+ True to the llm.rb spirit of small, composable objects that cooperate with
440
+ each other, a
441
+ [LLM::Model](https://0x1eef.github.io/x/llm.rb/LLM/Model.html)
442
+ object can be used instead of a string that describes a model name (although
443
+ either works). Let's take a look at an example:
444
+
445
+ ```ruby
446
+ #!/usr/bin/env ruby
447
+ require "llm"
448
+
449
+ ##
450
+ # List all models
451
+ llm = LLM.openai(ENV["KEY"])
452
+ llm.models.all.each do |model|
453
+ print "model: ", model.id, "\n"
454
+ end
455
+
456
+ ##
457
+ # Select a model
458
+ model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
459
+ bot = LLM::Chat.new(llm, model:)
460
+ bot.chat "Hello #{model.id} :)"
461
+ bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
462
+ ```
463
+
391
464
  ### Memory
392
465
 
393
466
  #### Child process
@@ -410,7 +483,7 @@ llm = LLM.gemini(ENV["KEY"])
410
483
  fork do
411
484
  %w[dog cat sheep goat capybara].each do |animal|
412
485
  res = llm.images.create(prompt: "a #{animal} on a rocket to the moon")
413
- File.binwrite "#{animal}.png", res.images[0].binary
486
+ IO.copy_stream res.images[0], "#{animal}.png"
414
487
  end
415
488
  end
416
489
  Process.wait
@@ -440,9 +513,9 @@ magic or complex metaprogramming.
440
513
 
441
514
  Every part of llm.rb is designed to be explicit, composable, memory-safe,
442
515
  and production-ready without compromise. No unnecessary abstractions,
443
- no global configuration, and no dependencies that aren't part of standard
444
- Ruby. It has been inspired in part by other languages such as Python, but
445
- it is not a port of any other library.
516
+ no global configuration, no global state, and no dependencies that aren't
517
+ part of standard Ruby. It has been inspired in part by other languages such
518
+ as Python, but it is not a port of any other library.
446
519
 
447
520
  ## License
448
521
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JSON::Schema
4
+ class Array < Leaf
5
+ def initialize(items, **rest)
6
+ @items = items
7
+ super(**rest)
8
+ end
9
+
10
+ def to_h
11
+ super.merge!({type: "array", items:})
12
+ end
13
+
14
+ def to_json(options = {})
15
+ to_h.to_json(options)
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :items
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JSON::Schema
4
+ class Booelean < Leaf
5
+ def to_h
6
+ super.merge!({type: "boolean"})
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JSON::Schema
4
+ class Integer < Leaf
5
+ def min(i)
6
+ tap { @minimum = i }
7
+ end
8
+
9
+ def max(i)
10
+ tap { @maximum = i }
11
+ end
12
+
13
+ def to_h
14
+ super.merge!({
15
+ type: "integer",
16
+ minimum: @minimum,
17
+ maximum: @maximum
18
+ }).compact
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JSON::Schema
4
+ class Leaf
5
+ def initialize
6
+ @description = nil
7
+ @default = nil
8
+ @enum = nil
9
+ @required = nil
10
+ end
11
+
12
+ def description(str)
13
+ tap { @description = str }
14
+ end
15
+
16
+ def default(value)
17
+ tap { @default = value }
18
+ end
19
+
20
+ def enum(*values)
21
+ tap { @enum = values }
22
+ end
23
+
24
+ def required
25
+ tap { @required = true }
26
+ end
27
+
28
+ def to_h
29
+ {description: @description, default: @default, enum: @enum}.compact
30
+ end
31
+
32
+ def to_json(options = {})
33
+ to_h.to_json(options)
34
+ end
35
+
36
+ def required?
37
+ @required
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JSON::Schema
4
+ class Null < Leaf
5
+ def to_h
6
+ super.merge!({type: "null"})
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JSON::Schema
4
+ class Number < Leaf
5
+ def min(i)
6
+ tap { @minimum = i }
7
+ end
8
+
9
+ def max(i)
10
+ tap { @maximum = i }
11
+ end
12
+
13
+ def to_h
14
+ super.merge!({
15
+ type: "number",
16
+ minimum: @minimum,
17
+ maximum: @maximum
18
+ }).compact
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JSON::Schema
4
+ class Object < Leaf
5
+ def initialize(properties, **rest)
6
+ @properties = properties
7
+ super(**rest)
8
+ end
9
+
10
+ def to_h
11
+ super.merge!({type: "object", properties:, required:})
12
+ end
13
+
14
+ def to_json(options = {})
15
+ to_h.to_json(options)
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :properties
21
+
22
+ def required
23
+ @properties.filter_map { _2.required? ? _1 : nil }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JSON::Schema
4
+ class String < Leaf
5
+ def to_h
6
+ super.merge!({type: "string"})
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON
4
+ end unless defined?(JSON)
5
+
6
+ class JSON::Schema
7
+ require_relative "schema/leaf"
8
+ require_relative "schema/object"
9
+ require_relative "schema/array"
10
+ require_relative "schema/string"
11
+ require_relative "schema/number"
12
+ require_relative "schema/integer"
13
+ require_relative "schema/boolean"
14
+ require_relative "schema/null"
15
+
16
+ ##
17
+ # Returns an object
18
+ # @param properties [Hash] A hash of properties
19
+ # @param rest [Hash] Any other options
20
+ # @return [JSON::Schema::Object]
21
+ def object(properties, **rest)
22
+ Object.new(properties, **rest)
23
+ end
24
+
25
+ ##
26
+ # Returns an array
27
+ # @param items [Array] An array of items
28
+ # @param rest [Hash] Any other options
29
+ # @return [JSON::Schema::Array]
30
+ def array(items, **rest)
31
+ Array.new(items, **rest)
32
+ end
33
+
34
+ ##
35
+ # Returns a string
36
+ # @param rest [Hash] Any other options
37
+ # @return [JSON::Schema::String]
38
+ def string(...)
39
+ String.new(...)
40
+ end
41
+
42
+ ##
43
+ # Returns a number
44
+ # @param rest [Hash] Any other options
45
+ # @return [JSON::Schema::Number] a number
46
+ def number(...)
47
+ Number.new(...)
48
+ end
49
+
50
+ ##
51
+ # Returns an integer
52
+ # @param rest [Hash] Any other options
53
+ # @return [JSON::Schema::Integer]
54
+ def integer(...)
55
+ Integer.new(...)
56
+ end
57
+
58
+ ##
59
+ # Returns a boolean
60
+ # @param rest [Hash] Any other options
61
+ # @return [JSON::Schema::Boolean]
62
+ def boolean(...)
63
+ Boolean.new(...)
64
+ end
65
+
66
+ ##
67
+ # Returns null
68
+ # @param rest [Hash] Any other options
69
+ # @return [JSON::Schema::Null]
70
+ def null(...)
71
+ Null.new(...)
72
+ end
73
+ end
data/lib/llm/chat.rb CHANGED
@@ -27,11 +27,15 @@ module LLM
27
27
  ##
28
28
  # @param [LLM::Provider] provider
29
29
  # A provider
30
+ # @param [to_json] schema
31
+ # The JSON schema to maintain throughout the conversation
32
+ # @param [String] model
33
+ # The model to maintain throughout the conversation
30
34
  # @param [Hash] params
31
- # The parameters to maintain throughout the conversation
32
- def initialize(provider, params = {})
35
+ # Other parameters to maintain throughout the conversation
36
+ def initialize(provider, model: provider.default_model, schema: nil, **params)
33
37
  @provider = provider
34
- @params = params
38
+ @params = params.merge!(model:, schema:)
35
39
  @lazy = false
36
40
  @messages = []
37
41
  end
@@ -18,7 +18,7 @@ class OpenStruct
18
18
  hash_obj.each do |key, value|
19
19
  visited_object[key] = walk(value)
20
20
  end
21
- OpenStruct.new(visited_object)
21
+ new(visited_object)
22
22
  end
23
23
 
24
24
  private
data/lib/llm/file.rb CHANGED
@@ -7,13 +7,20 @@
7
7
  class LLM::File
8
8
  ##
9
9
  # @return [String]
10
- # Returns the path to a file
10
+ # Returns the path to the file
11
11
  attr_reader :path
12
12
 
13
13
  def initialize(path)
14
14
  @path = path
15
15
  end
16
16
 
17
+ ##
18
+ # @return [String]
19
+ # Returns basename of the file
20
+ def basename
21
+ File.basename(path)
22
+ end
23
+
17
24
  ##
18
25
  # @return [String]
19
26
  # Returns the MIME type of the file
data/lib/llm/message.rb CHANGED
@@ -50,6 +50,13 @@ module LLM
50
50
  end
51
51
  alias_method :eql?, :==
52
52
 
53
+ ##
54
+ # Try to parse the content as JSON
55
+ # @return [Hash]
56
+ def content!
57
+ JSON.parse(content)
58
+ end
59
+
53
60
  ##
54
61
  # Returns true when the message is from the LLM
55
62
  # @return [Boolean]
data/lib/llm/model.rb CHANGED
@@ -1,7 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Model < Struct.new(:name, :parameters, :description, :to_param, keyword_init: true)
3
+ ##
4
+ # The {LLM::Model LLM::Model} class represents an LLM model that
5
+ # is available to use. Its properties are delegated to the underlying
6
+ # response body, and vary by provider.
7
+ class LLM::Model < OpenStruct
8
+ ##
9
+ # Returns a subclass of {LLM::Provider LLM::Provider}
10
+ # @return [LLM::Provider]
11
+ attr_accessor :provider
12
+
13
+ ##
14
+ # Returns the model ID
15
+ # @return [String]
16
+ def id
17
+ case @provider.class.to_s
18
+ when "LLM::Ollama"
19
+ self["name"]
20
+ when "LLM::Gemini"
21
+ self["name"].sub(%r|\Amodels/|, "")
22
+ else
23
+ self["id"]
24
+ end
25
+ end
26
+
27
+ ##
28
+ # @return [String]
4
29
  def to_json(*)
5
- to_param.to_json(*)
30
+ id.to_json(*)
6
31
  end
7
32
  end
data/lib/llm/provider.rb CHANGED
@@ -44,7 +44,7 @@ class LLM::Provider
44
44
  # @raise [NotImplementedError]
45
45
  # When the method is not implemented by a subclass
46
46
  # @return [LLM::Response::Embedding]
47
- def embed(input, model:, **params)
47
+ def embed(input, model: nil, **params)
48
48
  raise NotImplementedError
49
49
  end
50
50
 
@@ -64,12 +64,14 @@ class LLM::Provider
64
64
  # The role of the prompt (e.g. :user, :system)
65
65
  # @param [String] model
66
66
  # The model to use for the completion
67
+ # @param [#to_json, nil] schema
68
+ # The schema that describes the expected response format
67
69
  # @param [Hash] params
68
70
  # Other completion parameters
69
71
  # @raise [NotImplementedError]
70
72
  # When the method is not implemented by a subclass
71
73
  # @return [LLM::Response::Completion]
72
- def complete(prompt, role = :user, model: nil, **params)
74
+ def complete(prompt, role = :user, model: default_model, schema: nil, **params)
73
75
  raise NotImplementedError
74
76
  end
75
77
 
@@ -81,12 +83,13 @@ class LLM::Provider
81
83
  # @param prompt (see LLM::Provider#complete)
82
84
  # @param role (see LLM::Provider#complete)
83
85
  # @param model (see LLM::Provider#complete)
86
+ # @param schema (see LLM::Provider#complete)
84
87
  # @param [Hash] params
85
88
  # Other completion parameters to maintain throughout a chat
86
89
  # @raise (see LLM::Provider#complete)
87
90
  # @return [LLM::Chat]
88
- def chat(prompt, role = :user, model: nil, **params)
89
- LLM::Chat.new(self, params).lazy.chat(prompt, role)
91
+ def chat(prompt, role = :user, model: default_model, schema: nil, **params)
92
+ LLM::Chat.new(self, **params.merge(model:, schema:)).lazy.chat(prompt, role)
90
93
  end
91
94
 
92
95
  ##
@@ -97,12 +100,13 @@ class LLM::Provider
97
100
  # @param prompt (see LLM::Provider#complete)
98
101
  # @param role (see LLM::Provider#complete)
99
102
  # @param model (see LLM::Provider#complete)
103
+ # @param schema (see LLM::Provider#complete)
100
104
  # @param [Hash] params
101
105
  # Other completion parameters to maintain throughout a chat
102
106
  # @raise (see LLM::Provider#complete)
103
107
  # @return [LLM::Chat]
104
- def chat!(prompt, role = :user, model: nil, **params)
105
- LLM::Chat.new(self, params).chat(prompt, role)
108
+ def chat!(prompt, role = :user, model: default_model, schema: nil, **params)
109
+ LLM::Chat.new(self, **params.merge(model:, schema:)).chat(prompt, role)
106
110
  end
107
111
 
108
112
  ##
@@ -113,12 +117,13 @@ class LLM::Provider
113
117
  # @param prompt (see LLM::Provider#complete)
114
118
  # @param role (see LLM::Provider#complete)
115
119
  # @param model (see LLM::Provider#complete)
120
+ # @param schema (see LLM::Provider#complete)
116
121
  # @param [Hash] params
117
122
  # Other completion parameters to maintain throughout a chat
118
123
  # @raise (see LLM::Provider#complete)
119
124
  # @return [LLM::Chat]
120
- def respond(prompt, role = :user, model: nil, **params)
121
- LLM::Chat.new(self, params).lazy.respond(prompt, role)
125
+ def respond(prompt, role = :user, model: default_model, schema: nil, **params)
126
+ LLM::Chat.new(self, **params.merge(model:, schema:)).lazy.respond(prompt, role)
122
127
  end
123
128
 
124
129
  ##
@@ -129,12 +134,13 @@ class LLM::Provider
129
134
  # @param prompt (see LLM::Provider#complete)
130
135
  # @param role (see LLM::Provider#complete)
131
136
  # @param model (see LLM::Provider#complete)
137
+ # @param schema (see LLM::Provider#complete)
132
138
  # @param [Hash] params
133
139
  # Other completion parameters to maintain throughout a chat
134
140
  # @raise (see LLM::Provider#complete)
135
141
  # @return [LLM::Chat]
136
- def respond!(prompt, role = :user, model: nil, **params)
137
- LLM::Chat.new(self, params).respond(prompt, role)
142
+ def respond!(prompt, role = :user, model: default_model, schema: nil, **params)
143
+ LLM::Chat.new(self, **params.merge(model:, schema:)).respond(prompt, role)
138
144
  end
139
145
 
140
146
  ##
@@ -169,6 +175,13 @@ class LLM::Provider
169
175
  raise NotImplementedError
170
176
  end
171
177
 
178
+ ##
179
+ # @return [LLM::OpenAI::Models]
180
+ # Returns an interface to the models API
181
+ def models
182
+ raise NotImplementedError
183
+ end
184
+
172
185
  ##
173
186
  # @return [String]
174
187
  # Returns the role of the assistant in the conversation.
@@ -178,12 +191,22 @@ class LLM::Provider
178
191
  end
179
192
 
180
193
  ##
181
- # @return [Hash<String, LLM::Model>]
182
- # Returns a hash of available models
183
- def models
194
+ # @return [String]
195
+ # Returns the default model for chat completions
196
+ def default_model
184
197
  raise NotImplementedError
185
198
  end
186
199
 
200
+ ##
201
+ # Returns an object that can generate a JSON schema
202
+ # @return [JSON::Schema]
203
+ def schema
204
+ @schema ||= begin
205
+ require_relative "../json/schema"
206
+ JSON::Schema.new
207
+ end
208
+ end
209
+
187
210
  private
188
211
 
189
212
  ##
@@ -228,8 +251,6 @@ class LLM::Provider
228
251
  # When the rate limit is exceeded
229
252
  # @raise [LLM::Error::ResponseError]
230
253
  # When any other unsuccessful status code is returned
231
- # @raise [LLM::Error::PromptError]
232
- # When given an object a provider does not understand
233
254
  # @raise [SystemCallError]
234
255
  # When there is a network error at the operating system level
235
256
  def request(http, req, &b)
@@ -250,17 +271,4 @@ class LLM::Provider
250
271
  req.body_stream = io
251
272
  req["transfer-encoding"] = "chunked" unless req["content-length"]
252
273
  end
253
-
254
- ##
255
- # @param [String] provider
256
- # The name of the provider
257
- # @return [Hash<String, Hash>]
258
- def load_models!(provider)
259
- require "yaml" unless defined?(YAML)
260
- rootdir = File.realpath File.join(__dir__, "..", "..")
261
- sharedir = File.join(rootdir, "share", "llm")
262
- provider = provider.gsub(/[^a-z0-9]/i, "")
263
- yaml = File.join(sharedir, "models", "#{provider}.yml")
264
- YAML.safe_load_file(yaml).transform_values { LLM::Model.new(_1) }
265
- end
266
274
  end
@@ -26,13 +26,26 @@ class LLM::Anthropic
26
26
  # @return [String, Hash]
27
27
  # The formatted content
28
28
  def format_content(content)
29
- if URI === content
30
- [{
31
- type: :image,
32
- source: {type: :base64, media_type: LLM::File(content.to_s).mime_type, data: [content.to_s].pack("m0")}
33
- }]
29
+ case content
30
+ when Array
31
+ content.flat_map { format_content(_1) }
32
+ when URI
33
+ [{type: :image, source: {type: "url", url: content.to_s}}]
34
+ when LLM::File
35
+ if content.image?
36
+ [{type: :image, source: {type: "base64", media_type: content.mime_type, data: content.to_b64}}]
37
+ else
38
+ raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
39
+ "is not an image, and therefore not supported by the " \
40
+ "Anthropic API"
41
+ end
42
+ when String
43
+ [{type: :text, text: content}]
44
+ when LLM::Message
45
+ format_content(content.content)
34
46
  else
35
- content
47
+ raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
48
+ "is not supported by the Anthropic API"
36
49
  end
37
50
  end
38
51
  end