llm.rb 11.3.0 → 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 +270 -1
- data/LICENSE +92 -17
- data/README.md +209 -647
- 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/a2a.rb +2 -2
- data/lib/llm/active_record/acts_as_agent.rb +10 -41
- data/lib/llm/active_record/acts_as_llm.rb +25 -0
- data/lib/llm/active_record.rb +3 -3
- data/lib/llm/agent.rb +1 -1
- data/lib/llm/context.rb +9 -5
- data/lib/llm/contract/completion.rb +2 -2
- data/lib/llm/cost.rb +1 -1
- data/lib/llm/function/fiber_group.rb +2 -2
- data/lib/llm/function/task_group.rb +2 -2
- data/lib/llm/function/thread_group.rb +3 -3
- data/lib/llm/pipe.rb +1 -1
- data/lib/llm/provider.rb +2 -2
- data/lib/llm/providers/anthropic/request_adapter.rb +1 -1
- data/lib/llm/providers/bedrock/request_adapter/completion.rb +5 -5
- data/lib/llm/providers/bedrock/request_adapter.rb +3 -3
- data/lib/llm/providers/bedrock/response_adapter/completion.rb +2 -2
- data/lib/llm/providers/bedrock/response_adapter.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 +23 -6
- data/lib/llm/providers/google.rb +4 -4
- data/lib/llm/providers/ollama/request_adapter.rb +1 -1
- 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/request_adapter.rb +1 -1
- 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/registry.rb +2 -2
- data/lib/llm/response.rb +11 -1
- data/lib/llm/schema/leaf.rb +7 -1
- data/lib/llm/schema/object.rb +1 -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/stream.rb +1 -1
- data/lib/llm/tool.rb +2 -2
- 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/http.rb +2 -2
- data/lib/llm/transport/net_http_adapter.rb +1 -1
- data/lib/llm/transport/persistent_http.rb +1 -1
- data/lib/llm/transport/response/http.rb +1 -1
- data/lib/llm/uridata.rb +16 -0
- data/lib/llm/utils.rb +1 -1
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +20 -8
- data/llm.gemspec +14 -26
- data/resources/deepdive.md +966 -0
- metadata +26 -29
- data/lib/llm/tracer/langsmith.rb +0 -144
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::DeepSeek
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::DeepSeek::Images LLM::DeepSeek::Images} class
|
|
6
|
+
# provides image generation capabilities through DeepSeek.
|
|
7
|
+
#
|
|
8
|
+
# DeepSeek does not provide an image generation model however
|
|
9
|
+
# its text-to-text models can generate vector graphics (SVGS)
|
|
10
|
+
# and that's the approach that this class takes. It is somewhat
|
|
11
|
+
# experimental.
|
|
12
|
+
#
|
|
13
|
+
# An SVG document can be converted to PNG or another format
|
|
14
|
+
# with tools like rsvg-convert.
|
|
15
|
+
class Images
|
|
16
|
+
##
|
|
17
|
+
# @param [LLM::DeepSeek] provider
|
|
18
|
+
# @return [LLM::DeepSeek::Images]
|
|
19
|
+
def initialize(provider)
|
|
20
|
+
@provider = provider
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @param [String] prompt
|
|
25
|
+
# A prompt
|
|
26
|
+
# @param [String] model
|
|
27
|
+
# A text-to-image model.
|
|
28
|
+
# @param [void] size
|
|
29
|
+
# This parameter is a noop.
|
|
30
|
+
# Exists for compatibility with other providers.
|
|
31
|
+
# @param [void] n
|
|
32
|
+
# This parameter is a noop.
|
|
33
|
+
# Exists for compatibility with other providers.
|
|
34
|
+
# @param [void] response_format
|
|
35
|
+
# This parameter is a noop.
|
|
36
|
+
# Exists for compatibility with other providers.
|
|
37
|
+
# @param [void] quality
|
|
38
|
+
# This parameter is a noop.
|
|
39
|
+
# Exists for compatibility with other providers.
|
|
40
|
+
# @param [void] style
|
|
41
|
+
# This parameter is a noop.
|
|
42
|
+
# Exists for compatibility with other providers.
|
|
43
|
+
# @return [LLM::Response<LLM::DeepSeek::ResponseAdapter::Image>]
|
|
44
|
+
# Returns a response
|
|
45
|
+
def create(prompt:, model: @provider.default_model, agent: nil, size: nil, n: nil, response_format: nil, quality: nil, style: nil)
|
|
46
|
+
agent ||= LLM::Agent.new(@provider, model:, instructions: create_instructions, response_format: {type: "json_object"})
|
|
47
|
+
res = agent.talk(prompt)
|
|
48
|
+
res = LLM::DeepSeek::ResponseAdapter.adapt(res, type: :image)
|
|
49
|
+
res.define_singleton_method(:agent) { agent }
|
|
50
|
+
res
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @param [String] prompt
|
|
55
|
+
# A prompt
|
|
56
|
+
# @param [String] model
|
|
57
|
+
# A text-to-image model.
|
|
58
|
+
# @param [String, LLM::File] image
|
|
59
|
+
# The path to an SVG file
|
|
60
|
+
# @param [void] size
|
|
61
|
+
# This parameter is a noop.
|
|
62
|
+
# Exists for compatibility with other providers.
|
|
63
|
+
# @param [void] n
|
|
64
|
+
# This parameter is a noop.
|
|
65
|
+
# Exists for compatibility with other providers.
|
|
66
|
+
# @param [void] response_format
|
|
67
|
+
# This parameter is a noop.
|
|
68
|
+
# Exists for compatibility with other providers.
|
|
69
|
+
# @param [void] quality
|
|
70
|
+
# This parameter is a noop.
|
|
71
|
+
# Exists for compatibility with other providers.
|
|
72
|
+
# @param [void] style
|
|
73
|
+
# This parameter is a noop.
|
|
74
|
+
# Exists for compatibility with other providers.
|
|
75
|
+
# @return [LLM::Response<LLM::DeepSeek::ResponseAdapter::Image>]
|
|
76
|
+
# Returns a response
|
|
77
|
+
def edit(prompt:, image:, model: @provider.default_model, agent: nil, size: nil, n: nil, response_format: nil, quality: nil, style: nil)
|
|
78
|
+
file = LLM.File(image)
|
|
79
|
+
agent ||= LLM::Agent.new(@provider, model:, instructions: edit_instructions(file), response_format: {type: "json_object"})
|
|
80
|
+
res = agent.talk(prompt)
|
|
81
|
+
res = LLM::DeepSeek::ResponseAdapter.adapt(res, type: :image)
|
|
82
|
+
res.define_singleton_method(:agent) { agent }
|
|
83
|
+
res
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def create_instructions
|
|
89
|
+
"Generate a complete SVG document that satisfies the user's prompt. " \
|
|
90
|
+
"Respond with a JSON object that has exactly one key: svg. " \
|
|
91
|
+
"The value of svg must be a valid standalone SVG document as a string. " \
|
|
92
|
+
"Do not include markdown, code fences, commentary, or any keys other than svg."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def edit_instructions(file)
|
|
96
|
+
file.with_io do |io|
|
|
97
|
+
"Edit the SVG document that is provided according to the user's prompt" \
|
|
98
|
+
"Respond with a JSON object that has exactly one key: svg. " \
|
|
99
|
+
"The value of svg must be a valid standalone SVG document as a string. " \
|
|
100
|
+
"Do not include markdown, code fences, commentary, or any keys other than svg." \
|
|
101
|
+
"The SVG document follows:\n\n#{io.read}" \
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
[:path, :headers, :execute, :transport].each do |m|
|
|
106
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -5,6 +5,7 @@ class LLM::DeepSeek
|
|
|
5
5
|
# @private
|
|
6
6
|
module RequestAdapter
|
|
7
7
|
require_relative "request_adapter/completion"
|
|
8
|
+
|
|
8
9
|
##
|
|
9
10
|
# @param [Array<LLM::Message>] messages
|
|
10
11
|
# The messages to adapt
|
|
@@ -18,7 +19,38 @@ class LLM::DeepSeek
|
|
|
18
19
|
private
|
|
19
20
|
|
|
20
21
|
##
|
|
22
|
+
# Adapt a schema for the DeepSeek chat completions API.
|
|
23
|
+
#
|
|
24
|
+
# DeepSeek does not support OpenAI's `json_schema` response format,
|
|
25
|
+
# so llm.rb falls back to `json_object` and injects a system message
|
|
26
|
+
# that describes the expected shape in prompt-friendly terms.
|
|
27
|
+
#
|
|
21
28
|
# @param [Hash] params
|
|
29
|
+
# The request params
|
|
30
|
+
# @return [Hash]
|
|
31
|
+
def adapt_schema(params)
|
|
32
|
+
return {} unless params && params[:schema]
|
|
33
|
+
schema = params.delete(:schema)
|
|
34
|
+
schema = schema.respond_to?(:object) ? schema.object : schema
|
|
35
|
+
params[:messages] ||= []
|
|
36
|
+
params[:messages] << LLM::Message.new(system_role, adapt_prompt(schema))
|
|
37
|
+
{response_format: {type: "json_object"}}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Build the system prompt that describes the schema.
|
|
42
|
+
# @param [#to_s] schema
|
|
43
|
+
# The schema object
|
|
44
|
+
# @return [String]
|
|
45
|
+
def adapt_prompt(schema)
|
|
46
|
+
"Respond with a single valid JSON object. " \
|
|
47
|
+
"Do not include markdown, code fences, commentary, or any text outside the JSON object. " \
|
|
48
|
+
"The JSON object must match this schema: " \
|
|
49
|
+
"#{schema}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# @param [Array<LLM::Function>] tools
|
|
22
54
|
# @return [Hash]
|
|
23
55
|
def adapt_tools(tools)
|
|
24
56
|
(tools.nil? || tools.empty?) ? {} : {tools: tools.map { _1.adapt(self) }}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::DeepSeek
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
module ResponseAdapter
|
|
7
|
+
require_relative "response_adapter/image"
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# @param [LLM::Response, Net::HTTPResponse] res
|
|
12
|
+
# @param [Symbol] type
|
|
13
|
+
# @return [LLM::Response]
|
|
14
|
+
def adapt(res, type:)
|
|
15
|
+
response = (LLM::Response === res) ? res : LLM::Response.new(res)
|
|
16
|
+
adapter = select(type)
|
|
17
|
+
response.extend(adapter)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @api private
|
|
22
|
+
def select(type)
|
|
23
|
+
case type
|
|
24
|
+
when :image then LLM::DeepSeek::ResponseAdapter::Image
|
|
25
|
+
else LLM::OpenAI::ResponseAdapter.select(type)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -19,6 +19,8 @@ module LLM
|
|
|
19
19
|
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
20
20
|
class DeepSeek < OpenAI
|
|
21
21
|
require_relative "deepseek/request_adapter"
|
|
22
|
+
require_relative "deepseek/response_adapter"
|
|
23
|
+
require_relative "deepseek/images"
|
|
22
24
|
include DeepSeek::RequestAdapter
|
|
23
25
|
|
|
24
26
|
##
|
|
@@ -42,9 +44,9 @@ module LLM
|
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
##
|
|
45
|
-
# @raise [
|
|
47
|
+
# @raise [LLM::DeepSeek::Images]
|
|
46
48
|
def images
|
|
47
|
-
|
|
49
|
+
LLM::DeepSeek::Images.new(self)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
##
|
|
@@ -21,20 +21,37 @@ class LLM::Google
|
|
|
21
21
|
##
|
|
22
22
|
# @param [Hash] params
|
|
23
23
|
# @return [Hash]
|
|
24
|
-
def
|
|
25
|
-
return {} unless params
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
def adapt_generation_config(params)
|
|
25
|
+
return {} unless params
|
|
26
|
+
config = {}
|
|
27
|
+
if params[:schema]
|
|
28
|
+
schema = params.delete(:schema)
|
|
29
|
+
schema = schema.respond_to?(:object) ? schema.object : schema
|
|
30
|
+
config.merge!(response_mime_type: "application/json", response_schema: schema)
|
|
31
|
+
end
|
|
32
|
+
params_map.each { config[_1] = params.delete(_2) if params.key?(_2) }
|
|
33
|
+
config.merge!(params)
|
|
34
|
+
config.empty? ? {} : {generationConfig: config}
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
##
|
|
32
|
-
# @param [
|
|
38
|
+
# @param [Array<LLM::Function>] tools
|
|
33
39
|
# @return [Hash]
|
|
34
40
|
def adapt_tools(tools)
|
|
35
41
|
return {} unless tools&.any?
|
|
36
42
|
platform, functions = [tools.grep(LLM::ServerTool), tools.grep(LLM::Function)]
|
|
37
43
|
{tools: [*platform, {functionDeclarations: functions.map { _1.adapt(self) }}]}
|
|
38
44
|
end
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# @return [Hash]
|
|
48
|
+
def params_map
|
|
49
|
+
{
|
|
50
|
+
topP: :top_p,
|
|
51
|
+
topK: :top_k,
|
|
52
|
+
maxOutputTokens: :max_tokens,
|
|
53
|
+
stopSequences: :stop
|
|
54
|
+
}
|
|
55
|
+
end
|
|
39
56
|
end
|
|
40
57
|
end
|
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
|
##
|