llm.rb 11.3.1 → 12.0.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 +242 -1
- data/LICENSE +92 -17
- data/README.md +204 -623
- data/data/anthropic.json +433 -249
- data/data/bedrock.json +2097 -1055
- data/data/deepinfra.json +993 -0
- data/data/deepseek.json +53 -28
- data/data/google.json +389 -771
- data/data/openai.json +1053 -771
- data/data/xai.json +133 -292
- data/data/zai.json +249 -141
- data/lib/llm/active_record/acts_as_agent.rb +3 -41
- data/lib/llm/active_record/acts_as_llm.rb +18 -0
- data/lib/llm/active_record.rb +3 -3
- data/lib/llm/context.rb +9 -5
- data/lib/llm/contract/completion.rb +2 -2
- data/lib/llm/provider.rb +2 -2
- data/lib/llm/providers/deepinfra/audio.rb +66 -0
- data/lib/llm/providers/deepinfra/images.rb +90 -0
- data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
- data/lib/llm/providers/deepinfra.rb +100 -0
- data/lib/llm/providers/deepseek/images.rb +109 -0
- data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
- data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
- data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
- data/lib/llm/providers/deepseek.rb +4 -2
- data/lib/llm/providers/google/request_adapter.rb +22 -5
- data/lib/llm/providers/google.rb +4 -4
- data/lib/llm/providers/openai/audio.rb +6 -2
- data/lib/llm/providers/openai/images.rb +9 -50
- data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
- data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
- data/lib/llm/providers/openai/responses.rb +1 -0
- data/lib/llm/providers/openai/stream_parser.rb +5 -6
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/providers/xai/images.rb +49 -26
- data/lib/llm/providers/xai.rb +2 -2
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +7 -1
- data/lib/llm/schema/renderer.rb +121 -0
- data/lib/llm/schema.rb +30 -0
- data/lib/llm/sequel/agent.rb +2 -43
- data/lib/llm/sequel/plugin.rb +25 -7
- data/lib/llm/tracer/telemetry.rb +4 -6
- data/lib/llm/tracer.rb +9 -21
- data/lib/llm/transport/execution.rb +16 -1
- data/lib/llm/transport/net_http_adapter.rb +1 -1
- data/lib/llm/uridata.rb +16 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +9 -0
- data/llm.gemspec +5 -18
- data/resources/deepdive.md +798 -264
- metadata +15 -18
- data/lib/llm/tracer/langsmith.rb +0 -144
data/lib/llm/providers/google.rb
CHANGED
|
@@ -53,7 +53,7 @@ module LLM
|
|
|
53
53
|
# @param params (see LLM::Provider#embed)
|
|
54
54
|
# @raise (see LLM::Provider#request)
|
|
55
55
|
# @return [LLM::Response]
|
|
56
|
-
def embed(input, model: "gemini-embedding-
|
|
56
|
+
def embed(input, model: "gemini-embedding-2", **params)
|
|
57
57
|
model = model.respond_to?(:id) ? model.id : model
|
|
58
58
|
path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
|
|
59
59
|
req = LLM::Transport::Request.post(path, headers)
|
|
@@ -118,10 +118,10 @@ module LLM
|
|
|
118
118
|
|
|
119
119
|
##
|
|
120
120
|
# Returns the default model for chat completions
|
|
121
|
-
# @see https://ai.google.dev/gemini-api/docs/models#gemini-
|
|
121
|
+
# @see https://ai.google.dev/gemini-api/docs/models#gemini-31-flash-lite gemini-3.1-flash-lite
|
|
122
122
|
# @return [String]
|
|
123
123
|
def default_model
|
|
124
|
-
"gemini-
|
|
124
|
+
"gemini-3.1-flash-lite"
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
##
|
|
@@ -196,7 +196,7 @@ module LLM
|
|
|
196
196
|
def normalize_complete_params(params)
|
|
197
197
|
params = {role: :user, model: default_model}.merge!(params)
|
|
198
198
|
tools = resolve_tools(params.delete(:tools))
|
|
199
|
-
params = [params,
|
|
199
|
+
params = [params, adapt_generation_config(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
200
200
|
role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
|
|
201
201
|
[params, stream, tools, role, model]
|
|
202
202
|
end
|
|
@@ -22,7 +22,7 @@ class LLM::OpenAI
|
|
|
22
22
|
# @example
|
|
23
23
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
24
24
|
# res = llm.images.create_speech(input: "A dog on a rocket to the moon")
|
|
25
|
-
#
|
|
25
|
+
# IO.copy_stream res.audio.decoded, "rocket.mp3"
|
|
26
26
|
# @see https://platform.openai.com/docs/api-reference/audio/createSpeech OpenAI docs
|
|
27
27
|
# @param [String] input The text input
|
|
28
28
|
# @param [String] voice The voice to use
|
|
@@ -36,7 +36,11 @@ class LLM::OpenAI
|
|
|
36
36
|
req.body = LLM.json.dump({input:, voice:, model:, response_format:}.merge!(params))
|
|
37
37
|
io = StringIO.new("".b)
|
|
38
38
|
res, span, tracer = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
|
|
39
|
-
|
|
39
|
+
content_type = res["content-type"].to_s.split(";").first
|
|
40
|
+
content_type = content_type.empty? ? LLM::Mime[".#{response_format}"] : content_type
|
|
41
|
+
data = "data:#{content_type};base64,#{[io.string].pack("m0")}"
|
|
42
|
+
res.body = LLM::Object.from(audio: data)
|
|
43
|
+
res = ResponseAdapter.adapt(LLM::Response.new(res), type: :audio)
|
|
40
44
|
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
41
45
|
res
|
|
42
46
|
end
|
|
@@ -4,28 +4,14 @@ class LLM::OpenAI
|
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::OpenAI::Images LLM::OpenAI::Images} class provides an interface
|
|
6
6
|
# for [OpenAI's images API](https://platform.openai.com/docs/api-reference/images).
|
|
7
|
-
# OpenAI
|
|
8
|
-
# encoded in base64. The default is to return base64-encoded image data.
|
|
7
|
+
# OpenAI's GPT Image models return base64-encoded image data.
|
|
9
8
|
#
|
|
10
|
-
# @example
|
|
9
|
+
# @example
|
|
11
10
|
# #!/usr/bin/env ruby
|
|
12
11
|
# require "llm"
|
|
13
|
-
# require "open-uri"
|
|
14
|
-
# require "fileutils"
|
|
15
12
|
#
|
|
16
13
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
17
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
18
|
-
# response_format: "url"
|
|
19
|
-
# FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
|
|
20
|
-
# "rocket.png"
|
|
21
|
-
#
|
|
22
|
-
# @example Binary strings
|
|
23
|
-
# #!/usr/bin/env ruby
|
|
24
|
-
# require "llm"
|
|
25
|
-
#
|
|
26
|
-
# llm = LLM.openai(key: ENV["KEY"])
|
|
27
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon",
|
|
28
|
-
# response_format: "b64_json"
|
|
14
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
29
15
|
# IO.copy_stream res.images[0], "rocket.png"
|
|
30
16
|
class Images
|
|
31
17
|
##
|
|
@@ -45,40 +31,13 @@ class LLM::OpenAI
|
|
|
45
31
|
# @see https://platform.openai.com/docs/api-reference/images/create OpenAI docs
|
|
46
32
|
# @param [String] prompt The prompt
|
|
47
33
|
# @param [String] model The model to use
|
|
48
|
-
# @param [String]
|
|
34
|
+
# @param [String] output_format The output format ("png", "webp", or "jpeg")
|
|
49
35
|
# @param [Hash] params Other parameters (see OpenAI docs)
|
|
50
36
|
# @raise (see LLM::Provider#request)
|
|
51
37
|
# @return [LLM::Response]
|
|
52
|
-
def create(prompt:, model: "
|
|
38
|
+
def create(prompt:, model: "gpt-image-1-mini", output_format: "png", **params)
|
|
53
39
|
req = LLM::Transport::Request.post(path("/images/generations"), headers)
|
|
54
|
-
req.body = LLM.json.dump({prompt:, n: 1, model:,
|
|
55
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
56
|
-
res = ResponseAdapter.adapt(res, type: :image)
|
|
57
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
58
|
-
res
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
##
|
|
62
|
-
# Create image variations
|
|
63
|
-
# @example
|
|
64
|
-
# llm = LLM.openai(key: ENV["KEY"])
|
|
65
|
-
# res = llm.images.create_variation(image: "/images/hat.png", n: 5)
|
|
66
|
-
# res.images.each.with_index do |image, index|
|
|
67
|
-
# IO.copy_stream image, "variation#{index}.png"
|
|
68
|
-
# end
|
|
69
|
-
# @see https://platform.openai.com/docs/api-reference/images/createVariation OpenAI docs
|
|
70
|
-
# @param [File] image The image to create variations from
|
|
71
|
-
# @param [String] model The model to use
|
|
72
|
-
# @param [String] response_format The response format ("b64_json" or "url")
|
|
73
|
-
# @param [Hash] params Other parameters (see OpenAI docs)
|
|
74
|
-
# @raise (see LLM::Provider#request)
|
|
75
|
-
# @return [LLM::Response]
|
|
76
|
-
def create_variation(image:, model: "dall-e-2", response_format: "b64_json", **params)
|
|
77
|
-
image = LLM.File(image)
|
|
78
|
-
multi = LLM::Multipart.new(params.merge!(image:, model:, response_format:))
|
|
79
|
-
req = LLM::Transport::Request.post(path("/images/variations"), headers)
|
|
80
|
-
req["content-type"] = multi.content_type
|
|
81
|
-
transport.set_body_stream(req, multi.body)
|
|
40
|
+
req.body = LLM.json.dump({prompt:, n: 1, model:, output_format:}.merge!(params))
|
|
82
41
|
res, span, tracer = execute(request: req, operation: "request")
|
|
83
42
|
res = ResponseAdapter.adapt(res, type: :image)
|
|
84
43
|
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
@@ -95,13 +54,13 @@ class LLM::OpenAI
|
|
|
95
54
|
# @param [File] image The image to edit
|
|
96
55
|
# @param [String] prompt The prompt
|
|
97
56
|
# @param [String] model The model to use
|
|
98
|
-
# @param [String]
|
|
57
|
+
# @param [String] output_format The output format ("png", "webp", or "jpeg")
|
|
99
58
|
# @param [Hash] params Other parameters (see OpenAI docs)
|
|
100
59
|
# @raise (see LLM::Provider#request)
|
|
101
60
|
# @return [LLM::Response]
|
|
102
|
-
def edit(image:, prompt:, model: "
|
|
61
|
+
def edit(image:, prompt:, model: "gpt-image-1-mini", output_format: "png", **params)
|
|
103
62
|
image = LLM.File(image)
|
|
104
|
-
multi = LLM::Multipart.new(params.merge!(image:, prompt:, model:,
|
|
63
|
+
multi = LLM::Multipart.new(params.merge!(image:, prompt:, model:, output_format:))
|
|
105
64
|
req = LLM::Transport::Request.post(path("/images/edits"), headers)
|
|
106
65
|
req["content-type"] = multi.content_type
|
|
107
66
|
transport.set_body_stream(req, multi.body)
|
|
@@ -16,7 +16,7 @@ module LLM::OpenAI::RequestAdapter
|
|
|
16
16
|
if Hash === message
|
|
17
17
|
{role: message[:role], content: adapt_content(message[:content])}
|
|
18
18
|
elsif message.tool_call?
|
|
19
|
-
message.extra[:original_tool_calls]
|
|
19
|
+
adapt_tool_calls(message.extra[:original_tool_calls])
|
|
20
20
|
else
|
|
21
21
|
adapt_message
|
|
22
22
|
end
|
|
@@ -33,11 +33,12 @@ module LLM::OpenAI::RequestAdapter
|
|
|
33
33
|
when LLM::Message then adapt_content(content.content, role: content.role)
|
|
34
34
|
when LLM::Object
|
|
35
35
|
case content.kind
|
|
36
|
-
when :image_url then [{type: :
|
|
36
|
+
when :image_url then [{type: :input_image, image_url: content.value.to_s}]
|
|
37
37
|
when :remote_file then adapt_remote_file(content.value)
|
|
38
|
-
when :local_file then
|
|
38
|
+
when :local_file then adapt_local_file(content.value)
|
|
39
39
|
else prompt_error!(content)
|
|
40
40
|
end
|
|
41
|
+
when Array then content.flat_map { adapt_content(_1, role:) }
|
|
41
42
|
else
|
|
42
43
|
prompt_error!(content)
|
|
43
44
|
end
|
|
@@ -45,6 +46,8 @@ module LLM::OpenAI::RequestAdapter
|
|
|
45
46
|
|
|
46
47
|
def adapt_message
|
|
47
48
|
case content
|
|
49
|
+
when LLM::Function::Return
|
|
50
|
+
adapt_returns([content])
|
|
48
51
|
when Array
|
|
49
52
|
adapt_array
|
|
50
53
|
else
|
|
@@ -56,12 +59,35 @@ module LLM::OpenAI::RequestAdapter
|
|
|
56
59
|
if content.empty?
|
|
57
60
|
nil
|
|
58
61
|
elsif returns.any?
|
|
59
|
-
returns
|
|
62
|
+
adapt_returns(returns)
|
|
60
63
|
else
|
|
61
64
|
{role: message.role, content: content.flat_map { adapt_content(_1, role: message.role) }}
|
|
62
65
|
end
|
|
63
66
|
end
|
|
64
67
|
|
|
68
|
+
def adapt_returns(returns)
|
|
69
|
+
returns.map { {type: "function_call_output", call_id: _1.id, output: LLM.json.dump(_1.value)} }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def adapt_tool_calls(tools)
|
|
73
|
+
[*tools].map do |tool|
|
|
74
|
+
h = LLM::Object.from(tool.to_h)
|
|
75
|
+
# Backward compatibility for conversations that
|
|
76
|
+
# started under the chat completions API and are
|
|
77
|
+
# later continued through Responses.
|
|
78
|
+
if h.type.to_s == "function"
|
|
79
|
+
{
|
|
80
|
+
type: "function_call",
|
|
81
|
+
call_id: h.id,
|
|
82
|
+
name: h.function.name,
|
|
83
|
+
arguments: h.function.arguments || "{}"
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
tool
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
65
91
|
def adapt_remote_file(content)
|
|
66
92
|
prompt_error!(content) unless content.file?
|
|
67
93
|
file = LLM::File(content.filename)
|
|
@@ -72,6 +98,14 @@ module LLM::OpenAI::RequestAdapter
|
|
|
72
98
|
end
|
|
73
99
|
end
|
|
74
100
|
|
|
101
|
+
def adapt_local_file(file)
|
|
102
|
+
if file.image?
|
|
103
|
+
[{type: :input_image, image_url: file.to_data_uri}]
|
|
104
|
+
else
|
|
105
|
+
[{type: :input_file, filename: file.basename, file_data: file.to_data_uri}]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
75
109
|
def prompt_error!(content)
|
|
76
110
|
if LLM::Object === content
|
|
77
111
|
raise LLM::PromptError, "The given LLM::Object with kind '#{content.kind}' is not " \
|
|
@@ -5,7 +5,7 @@ module LLM::OpenAI::ResponseAdapter
|
|
|
5
5
|
##
|
|
6
6
|
# (see LLM::Contract::Completion#messages)
|
|
7
7
|
def messages
|
|
8
|
-
body.choices.map.with_index do |choice, index|
|
|
8
|
+
[*body.choices].map.with_index do |choice, index|
|
|
9
9
|
message = choice.message
|
|
10
10
|
extra = {
|
|
11
11
|
index:, response: self,
|
|
@@ -100,6 +100,7 @@ class LLM::OpenAI
|
|
|
100
100
|
def adapt_schema(params)
|
|
101
101
|
return {} unless params && params[:schema]
|
|
102
102
|
schema = params.delete(:schema)
|
|
103
|
+
schema = schema.respond_to?(:object) ? schema.object : schema
|
|
103
104
|
schema = schema.to_h.merge(additionalProperties: false)
|
|
104
105
|
name = "JSONSchema"
|
|
105
106
|
{text: {format: {type: "json_schema", name:, schema:}}}
|
|
@@ -66,26 +66,25 @@ class LLM::OpenAI
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def merge_delta!(target_message, delta)
|
|
69
|
-
if delta.
|
|
70
|
-
merge_single_delta!(target_message, delta)
|
|
71
|
-
elsif content = delta["content"]
|
|
69
|
+
if delta.key?("content") and (content = delta["content"])
|
|
72
70
|
if target_content = target_message["content"]
|
|
73
71
|
target_content << content
|
|
74
72
|
else
|
|
75
73
|
target_message["content"] = content
|
|
76
74
|
end
|
|
77
75
|
emit_content(content)
|
|
78
|
-
|
|
76
|
+
end
|
|
77
|
+
if delta.key?("reasoning_content") and (reasoning = delta["reasoning_content"])
|
|
79
78
|
if target_reasoning = target_message["reasoning_content"]
|
|
80
79
|
target_reasoning << reasoning
|
|
81
80
|
else
|
|
82
81
|
target_message["reasoning_content"] = reasoning
|
|
83
82
|
end
|
|
84
83
|
emit_reasoning_content(reasoning)
|
|
85
|
-
|
|
84
|
+
end
|
|
85
|
+
if delta.key?("tool_calls") and (tool_calls = delta["tool_calls"])
|
|
86
86
|
merge_tools!(target_message, tool_calls)
|
|
87
87
|
end
|
|
88
|
-
return if delta.length <= 1
|
|
89
88
|
delta.each do |key, value|
|
|
90
89
|
next if value.nil? || key == "content" || key == "reasoning_content" || key == "tool_calls"
|
|
91
90
|
target_message[key] = value
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -146,10 +146,10 @@ module LLM
|
|
|
146
146
|
|
|
147
147
|
##
|
|
148
148
|
# Returns the default model for chat completions
|
|
149
|
-
# @see https://platform.openai.com/docs/models/gpt-4
|
|
149
|
+
# @see https://platform.openai.com/docs/models/gpt-5.4-mini gpt-5.4-mini
|
|
150
150
|
# @return [String]
|
|
151
151
|
def default_model
|
|
152
|
-
"gpt-4
|
|
152
|
+
"gpt-5.4-mini"
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
##
|
|
@@ -4,30 +4,21 @@ class LLM::XAI
|
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::XAI::Images LLM::XAI::Images} class provides an interface
|
|
6
6
|
# for [xAI's images API](https://docs.x.ai/docs/guides/image-generations).
|
|
7
|
-
# xAI
|
|
8
|
-
# encoded in base64. The default is to return base64-encoded image data.
|
|
7
|
+
# xAI returns base64-encoded image data.
|
|
9
8
|
#
|
|
10
|
-
# @example
|
|
9
|
+
# @example
|
|
11
10
|
# #!/usr/bin/env ruby
|
|
12
11
|
# require "llm"
|
|
13
|
-
# require "open-uri"
|
|
14
|
-
# require "fileutils"
|
|
15
12
|
#
|
|
16
13
|
# llm = LLM.xai(key: ENV["KEY"])
|
|
17
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
18
|
-
# response_format: "url"
|
|
19
|
-
# FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
|
|
20
|
-
# "rocket.png"
|
|
21
|
-
#
|
|
22
|
-
# @example Binary strings
|
|
23
|
-
# #!/usr/bin/env ruby
|
|
24
|
-
# require "llm"
|
|
25
|
-
#
|
|
26
|
-
# llm = LLM.xai(key: ENV["KEY"])
|
|
27
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon",
|
|
28
|
-
# response_format: "b64_json"
|
|
14
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
29
15
|
# IO.copy_stream res.images[0], "rocket.png"
|
|
30
16
|
class Images < LLM::OpenAI::Images
|
|
17
|
+
##
|
|
18
|
+
# @api private
|
|
19
|
+
PATTERN = %r{\A(?:https?://|data:)}
|
|
20
|
+
private_constant :PATTERN
|
|
21
|
+
|
|
31
22
|
##
|
|
32
23
|
# Create an image
|
|
33
24
|
# @example
|
|
@@ -40,20 +31,52 @@ class LLM::XAI
|
|
|
40
31
|
# @param [Hash] params Other parameters (see xAI docs)
|
|
41
32
|
# @raise (see LLM::Provider#request)
|
|
42
33
|
# @return [LLM::Response]
|
|
43
|
-
def create(prompt:, model: "grok-imagine-image", **params)
|
|
44
|
-
|
|
34
|
+
def create(prompt:, model: "grok-imagine-image-quality", **params)
|
|
35
|
+
req = LLM::Transport::Request.post(path("/images/generations"), headers)
|
|
36
|
+
req.body = LLM.json.dump({prompt:, n: 1, model:, response_format: "b64_json"}.merge!(params))
|
|
37
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
38
|
+
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
39
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
40
|
+
res
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
##
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
# Edit an image
|
|
45
|
+
# @example
|
|
46
|
+
# llm = LLM.xai(key: ENV["KEY"])
|
|
47
|
+
# res = llm.images.edit(image: "/images/book.png", prompt: "The book is floating in the clouds")
|
|
48
|
+
# IO.copy_stream res.images[0], "floating-book.png"
|
|
49
|
+
# @see https://docs.x.ai/docs/guides/image-generations xAI docs
|
|
50
|
+
# @param [String, LLM::File, File] image The image to edit
|
|
51
|
+
# @param [String] prompt The prompt
|
|
52
|
+
# @param [String] model The model to use
|
|
53
|
+
# @param [Hash] params Other parameters (see xAI docs)
|
|
54
|
+
# @raise (see LLM::Provider#request)
|
|
55
|
+
# @return [LLM::Response]
|
|
56
|
+
def edit(image:, prompt:, model: "grok-imagine-image-quality", **params)
|
|
57
|
+
req = LLM::Transport::Request.post(path("/images/edits"), headers)
|
|
58
|
+
req.body = LLM.json.dump({
|
|
59
|
+
prompt:,
|
|
60
|
+
model:,
|
|
61
|
+
image: image_url(image),
|
|
62
|
+
response_format: "b64_json"
|
|
63
|
+
}.merge!(params))
|
|
64
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
65
|
+
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
66
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
67
|
+
res
|
|
51
68
|
end
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def
|
|
56
|
-
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def image_url(image)
|
|
73
|
+
case image
|
|
74
|
+
when String
|
|
75
|
+
url = image.match?(PATTERN) ? image : LLM.File(image).to_data_uri
|
|
76
|
+
else
|
|
77
|
+
url = LLM.File(image).to_data_uri
|
|
78
|
+
end
|
|
79
|
+
{url:, type: "image_url"}
|
|
57
80
|
end
|
|
58
81
|
end
|
|
59
82
|
end
|
data/lib/llm/providers/xai.rb
CHANGED
|
@@ -70,10 +70,10 @@ module LLM
|
|
|
70
70
|
|
|
71
71
|
##
|
|
72
72
|
# Returns the default model for chat completions
|
|
73
|
-
# #see https://docs.x.ai/docs/models grok-4
|
|
73
|
+
# #see https://docs.x.ai/docs/models grok-4.3
|
|
74
74
|
# @return [String]
|
|
75
75
|
def default_model
|
|
76
|
-
"grok-4
|
|
76
|
+
"grok-4.3"
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|
data/lib/llm/response.rb
CHANGED
|
@@ -56,6 +56,16 @@ module LLM
|
|
|
56
56
|
@res.success?
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
##
|
|
60
|
+
# Returns the provider response id when present.
|
|
61
|
+
# @return [String, nil]
|
|
62
|
+
def id
|
|
63
|
+
return nil unless LLM::Object === body
|
|
64
|
+
body.id ||
|
|
65
|
+
body.responseId || body.response_id ||
|
|
66
|
+
body.requestId || body.request_id
|
|
67
|
+
end
|
|
68
|
+
|
|
59
69
|
##
|
|
60
70
|
# Returns true if the response is from the Files API
|
|
61
71
|
# @return [Boolean]
|
data/lib/llm/schema/leaf.rb
CHANGED
|
@@ -95,7 +95,7 @@ class LLM::Schema
|
|
|
95
95
|
##
|
|
96
96
|
# @return [Hash]
|
|
97
97
|
def to_h
|
|
98
|
-
{description: @description, default: @default, enum: @enum}.compact
|
|
98
|
+
{description: @description, default: @default, enum: @enum, const: @const}.compact
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
##
|
|
@@ -104,6 +104,12 @@ class LLM::Schema
|
|
|
104
104
|
to_h.to_json(options)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
##
|
|
108
|
+
# @return [String]
|
|
109
|
+
def to_s
|
|
110
|
+
LLM::Schema::Renderer.render(self)
|
|
111
|
+
end
|
|
112
|
+
|
|
107
113
|
##
|
|
108
114
|
# @param [LLM::Schema::Leaf] other
|
|
109
115
|
# An object to compare
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Schema
|
|
4
|
+
##
|
|
5
|
+
# Internal renderer for prompt-friendly schema output.
|
|
6
|
+
# @api private
|
|
7
|
+
module Renderer
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# Render a schema node as a human-readable string.
|
|
12
|
+
# @param [LLM::Schema::Leaf] node
|
|
13
|
+
# The schema node to render
|
|
14
|
+
# @param [Integer] indent
|
|
15
|
+
# The indentation level
|
|
16
|
+
# @param [String, Symbol, nil] name
|
|
17
|
+
# The property name for nested nodes
|
|
18
|
+
# @param [Boolean] root
|
|
19
|
+
# Whether the node is the root schema object
|
|
20
|
+
# @return [String]
|
|
21
|
+
def render(node, indent: 0, name: nil, root: false)
|
|
22
|
+
line = (" " * indent).to_s
|
|
23
|
+
if name
|
|
24
|
+
line << name.to_s
|
|
25
|
+
line << "?" unless node.required?
|
|
26
|
+
line << ": "
|
|
27
|
+
end
|
|
28
|
+
line << type_name(node)
|
|
29
|
+
metadata = metadata_for(node, include_required: !root)
|
|
30
|
+
line << " (#{metadata.join(", ")})" unless metadata.empty?
|
|
31
|
+
line << " - #{node.description}" if node.respond_to?(:description) && node.description
|
|
32
|
+
nested = nested_lines(node, indent: indent + 2)
|
|
33
|
+
([line] + nested).join("\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Render nested lines for compound schema nodes.
|
|
40
|
+
# @param [LLM::Schema::Leaf] node
|
|
41
|
+
# The schema node
|
|
42
|
+
# @param [Integer] indent
|
|
43
|
+
# The indentation level
|
|
44
|
+
# @return [Array<String>]
|
|
45
|
+
def nested_lines(node, indent:)
|
|
46
|
+
case node
|
|
47
|
+
when LLM::Schema::Object
|
|
48
|
+
node.properties.map { |key, val| render(val, indent:, name: key) }
|
|
49
|
+
when LLM::Schema::Array
|
|
50
|
+
items = node.to_h[:items]
|
|
51
|
+
items.is_a?(LLM::Schema::Object) ? [render(items, indent:, name: "items")] : []
|
|
52
|
+
else
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Return the printable type name for a schema node.
|
|
59
|
+
# @param [LLM::Schema::Leaf] node
|
|
60
|
+
# The schema node
|
|
61
|
+
# @return [String]
|
|
62
|
+
def type_name(node)
|
|
63
|
+
h = node.to_h
|
|
64
|
+
return "array<#{inline_type(h[:items])}>" if node.is_a?(LLM::Schema::Array)
|
|
65
|
+
return "anyOf<#{inline_types(h[:anyOf])}>" if node.is_a?(LLM::Schema::AnyOf)
|
|
66
|
+
return "oneOf<#{inline_types(h[:oneOf])}>" if node.is_a?(LLM::Schema::OneOf)
|
|
67
|
+
return "allOf<#{inline_types(h[:allOf])}>" if node.is_a?(LLM::Schema::AllOf)
|
|
68
|
+
h[:type] || "unknown"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Return the inline type description for a nested node.
|
|
73
|
+
# @param [LLM::Schema::Leaf, Object] node
|
|
74
|
+
# The nested schema node
|
|
75
|
+
# @return [String]
|
|
76
|
+
def inline_type(node)
|
|
77
|
+
return type_name(node) if node.is_a?(LLM::Schema::Leaf)
|
|
78
|
+
node.inspect
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Return the inline type description for a list of nodes.
|
|
83
|
+
# @param [Array<LLM::Schema::Leaf>] values
|
|
84
|
+
# The union members
|
|
85
|
+
# @return [String]
|
|
86
|
+
def inline_types(values)
|
|
87
|
+
values.map { inline_type(_1) }.join(", ")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Extract printable metadata for a schema node.
|
|
92
|
+
# @param [LLM::Schema::Leaf] node
|
|
93
|
+
# The schema node
|
|
94
|
+
# @param [Boolean] include_required
|
|
95
|
+
# Whether to include the required marker
|
|
96
|
+
# @return [Array<String>]
|
|
97
|
+
def metadata_for(node, include_required:)
|
|
98
|
+
h = node.to_h.dup
|
|
99
|
+
details = []
|
|
100
|
+
details << "required" if include_required && node.required?
|
|
101
|
+
details << "default: #{value(node.default)}" if node.default
|
|
102
|
+
details << "enum: #{node.enum.map { value(_1) }.join(" | ")}" if node.enum
|
|
103
|
+
details << "const: #{value(node.const)}" if node.const
|
|
104
|
+
h.except(:type, :description, :default, :enum, :const, :required, :properties, :items, :anyOf, :oneOf, :allOf)
|
|
105
|
+
.each { |key, val| details << "#{key}: #{value(val)}" }
|
|
106
|
+
details
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Convert a scalar value into its printable representation.
|
|
111
|
+
# @param [Object] val
|
|
112
|
+
# The value to render
|
|
113
|
+
# @return [String]
|
|
114
|
+
def value(val)
|
|
115
|
+
case val
|
|
116
|
+
when ::String then val.inspect
|
|
117
|
+
else val.to_s
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/llm/schema.rb
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
class LLM::Schema
|
|
35
35
|
require_relative "schema/version"
|
|
36
36
|
require_relative "schema/parser"
|
|
37
|
+
require_relative "schema/renderer"
|
|
37
38
|
require_relative "schema/leaf"
|
|
38
39
|
require_relative "schema/object"
|
|
39
40
|
require_relative "schema/array"
|
|
@@ -121,6 +122,19 @@ class LLM::Schema
|
|
|
121
122
|
end
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
##
|
|
126
|
+
# @param [Hash] defaults
|
|
127
|
+
# @return [LLM::Schema::Object]
|
|
128
|
+
def self.defaults(defaults)
|
|
129
|
+
lock do
|
|
130
|
+
object.tap do |schema|
|
|
131
|
+
defaults.each do |name, val|
|
|
132
|
+
Utils.fetch(schema.properties, name).default(val)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
124
138
|
##
|
|
125
139
|
# @api private
|
|
126
140
|
# @return [LLM::Schema]
|
|
@@ -139,6 +153,14 @@ class LLM::Schema
|
|
|
139
153
|
end
|
|
140
154
|
end
|
|
141
155
|
|
|
156
|
+
##
|
|
157
|
+
# Render the schema as a prompt-friendly string.
|
|
158
|
+
# @return [String]
|
|
159
|
+
def self.to_s
|
|
160
|
+
Renderer.render(object, root: true)
|
|
161
|
+
end
|
|
162
|
+
(class << self; self; end).alias_method(:inspect, :to_s)
|
|
163
|
+
|
|
142
164
|
##
|
|
143
165
|
# @api private
|
|
144
166
|
def self.lock(&)
|
|
@@ -220,4 +242,12 @@ class LLM::Schema
|
|
|
220
242
|
def null
|
|
221
243
|
Null.new
|
|
222
244
|
end
|
|
245
|
+
|
|
246
|
+
##
|
|
247
|
+
# Render a schema leaf as a prompt-friendly string.
|
|
248
|
+
# @return [String]
|
|
249
|
+
def to_s
|
|
250
|
+
self.class.to_s
|
|
251
|
+
end
|
|
252
|
+
alias_method :inspect, :to_s
|
|
223
253
|
end
|