groq_ruby 0.1.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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +57 -0
- data/CLAUDE.md +103 -0
- data/LICENSE.txt +21 -0
- data/README.md +495 -0
- data/Rakefile +11 -0
- data/examples/README.md +39 -0
- data/examples/batch.rb +29 -0
- data/examples/chat_completion.rb +24 -0
- data/examples/chat_completion_stop.rb +19 -0
- data/examples/chat_completion_streaming.rb +23 -0
- data/examples/embedding.rb +20 -0
- data/examples/error_handling.rb +27 -0
- data/examples/file_upload.rb +23 -0
- data/examples/mcp_agent.rb +63 -0
- data/examples/mcp_chat_with_tools.rb +103 -0
- data/examples/mcp_resources_and_prompts.rb +89 -0
- data/examples/models_list.rb +16 -0
- data/examples/speech.rb +23 -0
- data/examples/transcription.rb +23 -0
- data/examples/translation.rb +22 -0
- data/lib/groq_ruby/client.rb +69 -0
- data/lib/groq_ruby/configuration.rb +62 -0
- data/lib/groq_ruby/error_mapper.rb +37 -0
- data/lib/groq_ruby/errors/api_connection_error.rb +8 -0
- data/lib/groq_ruby/errors/api_error.rb +14 -0
- data/lib/groq_ruby/errors/api_response_error.rb +5 -0
- data/lib/groq_ruby/errors/api_status_error.rb +23 -0
- data/lib/groq_ruby/errors/api_timeout_error.rb +8 -0
- data/lib/groq_ruby/errors/authentication_error.rb +4 -0
- data/lib/groq_ruby/errors/bad_request_error.rb +4 -0
- data/lib/groq_ruby/errors/configuration_error.rb +4 -0
- data/lib/groq_ruby/errors/conflict_error.rb +4 -0
- data/lib/groq_ruby/errors/error.rb +5 -0
- data/lib/groq_ruby/errors/internal_server_error.rb +4 -0
- data/lib/groq_ruby/errors/not_found_error.rb +4 -0
- data/lib/groq_ruby/errors/parameter_error.rb +13 -0
- data/lib/groq_ruby/errors/permission_denied_error.rb +4 -0
- data/lib/groq_ruby/errors/rate_limit_error.rb +4 -0
- data/lib/groq_ruby/errors/unprocessable_entity_error.rb +4 -0
- data/lib/groq_ruby/mcp/bridge.rb +239 -0
- data/lib/groq_ruby/mcp/claude_desktop_config.rb +79 -0
- data/lib/groq_ruby/mcp/client.rb +171 -0
- data/lib/groq_ruby/mcp/errors/error.rb +7 -0
- data/lib/groq_ruby/mcp/errors/json_rpc_error.rb +21 -0
- data/lib/groq_ruby/mcp/errors/protocol_error.rb +7 -0
- data/lib/groq_ruby/mcp/errors/timeout_error.rb +7 -0
- data/lib/groq_ruby/mcp/errors/transport_error.rb +6 -0
- data/lib/groq_ruby/mcp/errors/unknown_tool_error.rb +7 -0
- data/lib/groq_ruby/mcp/json_rpc.rb +51 -0
- data/lib/groq_ruby/mcp/prompt.rb +21 -0
- data/lib/groq_ruby/mcp/resource.rb +17 -0
- data/lib/groq_ruby/mcp/server_config.rb +22 -0
- data/lib/groq_ruby/mcp/tool.rb +22 -0
- data/lib/groq_ruby/mcp/transport.rb +32 -0
- data/lib/groq_ruby/mcp/transports/stdio.rb +100 -0
- data/lib/groq_ruby/mcp.rb +25 -0
- data/lib/groq_ruby/models/audio/transcription.rb +10 -0
- data/lib/groq_ruby/models/audio/translation.rb +8 -0
- data/lib/groq_ruby/models/batches/batch.rb +16 -0
- data/lib/groq_ruby/models/batches/batch_list.rb +10 -0
- data/lib/groq_ruby/models/batches/batch_request_counts.rb +8 -0
- data/lib/groq_ruby/models/chat/chat_completion.rb +14 -0
- data/lib/groq_ruby/models/chat/chat_completion_choice.rb +10 -0
- data/lib/groq_ruby/models/chat/chat_completion_chunk.rb +13 -0
- data/lib/groq_ruby/models/chat/chat_completion_chunk_choice.rb +10 -0
- data/lib/groq_ruby/models/chat/chat_completion_delta.rb +8 -0
- data/lib/groq_ruby/models/chat/chat_completion_message.rb +10 -0
- data/lib/groq_ruby/models/embeddings/create_embedding_response.rb +11 -0
- data/lib/groq_ruby/models/embeddings/embedding.rb +8 -0
- data/lib/groq_ruby/models/embeddings/embedding_usage.rb +8 -0
- data/lib/groq_ruby/models/files/file_deleted.rb +8 -0
- data/lib/groq_ruby/models/files/file_list.rb +10 -0
- data/lib/groq_ruby/models/files/file_object.rb +8 -0
- data/lib/groq_ruby/models/model.rb +8 -0
- data/lib/groq_ruby/models/model_deleted.rb +8 -0
- data/lib/groq_ruby/models/model_factory.rb +31 -0
- data/lib/groq_ruby/models/model_list.rb +10 -0
- data/lib/groq_ruby/models/usage.rb +11 -0
- data/lib/groq_ruby/multipart.rb +84 -0
- data/lib/groq_ruby/request.rb +13 -0
- data/lib/groq_ruby/resources/audio/speech.rb +32 -0
- data/lib/groq_ruby/resources/audio/transcriptions.rb +48 -0
- data/lib/groq_ruby/resources/audio/translations.rb +45 -0
- data/lib/groq_ruby/resources/audio.rb +26 -0
- data/lib/groq_ruby/resources/base.rb +33 -0
- data/lib/groq_ruby/resources/batches.rb +44 -0
- data/lib/groq_ruby/resources/chat/completions.rb +94 -0
- data/lib/groq_ruby/resources/chat.rb +16 -0
- data/lib/groq_ruby/resources/embeddings.rb +28 -0
- data/lib/groq_ruby/resources/files.rb +55 -0
- data/lib/groq_ruby/resources/models.rb +35 -0
- data/lib/groq_ruby/response.rb +9 -0
- data/lib/groq_ruby/streaming/chunk_stream.rb +58 -0
- data/lib/groq_ruby/streaming/event_parser.rb +23 -0
- data/lib/groq_ruby/transport.rb +169 -0
- data/lib/groq_ruby/version.rb +5 -0
- data/lib/groq_ruby.rb +36 -0
- data/lib/tasks/gem.rake +5 -0
- data/lib/tasks/lint/all.rake +11 -0
- data/lib/tasks/lint/rubocop.rake +15 -0
- data/lib/tasks/security.rake +11 -0
- data/lib/tasks/types.rake +11 -0
- data/sig/groq_ruby.rbs +191 -0
- data/sig/zeitwerk.rbs +13 -0
- data.tar.gz.sig +0 -0
- metadata +237 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module GroqRuby
|
|
2
|
+
module Models
|
|
3
|
+
# Full response from `client.embeddings.create`.
|
|
4
|
+
class CreateEmbeddingResponse < Data.define(:data, :model, :object, :usage)
|
|
5
|
+
extend ModelFactory
|
|
6
|
+
|
|
7
|
+
coerce :data, with: ->(arr) { Array(arr).map { |h| Embedding.from_hash(h) } }
|
|
8
|
+
coerce :usage, with: ->(h) { EmbeddingUsage.from_hash(h) }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module GroqRuby
|
|
2
|
+
module Models
|
|
3
|
+
# Tiny mixin that gives a Data subclass a `from_hash` constructor.
|
|
4
|
+
# Walks the Data class's `members` and pulls each key out of the
|
|
5
|
+
# incoming Hash (string-keyed). Unknown keys are ignored; missing keys
|
|
6
|
+
# default to `nil`. Per-attribute coercions are declared on the class:
|
|
7
|
+
#
|
|
8
|
+
# class MyModel < Data.define(:choices)
|
|
9
|
+
# extend ModelFactory
|
|
10
|
+
# coerce :choices, with: ->(arr) { arr.map { |h| Choice.from_hash(h) } }
|
|
11
|
+
# end
|
|
12
|
+
module ModelFactory
|
|
13
|
+
def coerce(attr, with:)
|
|
14
|
+
coercions[attr] = with
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def coercions
|
|
18
|
+
@coercions ||= {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def from_hash(hash)
|
|
22
|
+
return nil if hash.nil?
|
|
23
|
+
attrs = members.each_with_object({}) do |name, acc|
|
|
24
|
+
raw = hash[name.to_s] || hash[name]
|
|
25
|
+
acc[name] = coercions.key?(name) ? coercions[name].call(raw) : raw
|
|
26
|
+
end
|
|
27
|
+
new(**attrs)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module GroqRuby
|
|
2
|
+
module Models
|
|
3
|
+
# Token + timing usage breakdown returned with chat completions.
|
|
4
|
+
class Usage < Data.define(
|
|
5
|
+
:prompt_tokens, :completion_tokens, :total_tokens,
|
|
6
|
+
:prompt_time, :completion_time, :total_time, :queue_time
|
|
7
|
+
)
|
|
8
|
+
extend ModelFactory
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module GroqRuby
|
|
4
|
+
# Encodes a list of fields into a multipart/form-data body. Used by the
|
|
5
|
+
# file upload endpoints (audio transcription/translation, files.create).
|
|
6
|
+
# Single-purpose — builds the body and content-type header. Does no IO.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# parts = [
|
|
10
|
+
# Multipart::Part.new("model", "whisper-large-v3"),
|
|
11
|
+
# Multipart::Part.file("file", io: file_io, filename: "audio.mp3", content_type: "audio/mpeg")
|
|
12
|
+
# ]
|
|
13
|
+
# multipart = Multipart.new(parts)
|
|
14
|
+
# request_body = multipart.body
|
|
15
|
+
# header_value = multipart.content_type
|
|
16
|
+
class Multipart
|
|
17
|
+
# A single field inside a multipart payload.
|
|
18
|
+
Part = Data.define(:name, :value, :filename, :content_type) do
|
|
19
|
+
# Build a plain text field.
|
|
20
|
+
# @param name [String]
|
|
21
|
+
# @param value [#to_s]
|
|
22
|
+
# @return [Part]
|
|
23
|
+
def self.text(name, value)
|
|
24
|
+
new(name: name, value: value.to_s, filename: nil, content_type: nil)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Build a file upload field.
|
|
28
|
+
# @param name [String]
|
|
29
|
+
# @param io [#read] readable IO whose `#read` returns the bytes.
|
|
30
|
+
# @param filename [String]
|
|
31
|
+
# @param content_type [String]
|
|
32
|
+
# @return [Part]
|
|
33
|
+
def self.file(name, io:, filename:, content_type: "application/octet-stream")
|
|
34
|
+
new(name: name, value: io, filename: filename, content_type: content_type)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Boolean] true if this part is a file upload (has a filename)
|
|
38
|
+
def file?
|
|
39
|
+
!filename.nil?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [String] the random boundary string used to separate parts
|
|
44
|
+
attr_reader :boundary
|
|
45
|
+
|
|
46
|
+
# @param parts [Array<Part>] the fields to encode
|
|
47
|
+
# @param boundary [String] override the auto-generated boundary
|
|
48
|
+
def initialize(parts, boundary: SecureRandom.hex(16))
|
|
49
|
+
@parts = parts
|
|
50
|
+
@boundary = boundary
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [String] the encoded multipart body, suitable for `Net::HTTP::Post#body=`
|
|
54
|
+
def body
|
|
55
|
+
buffer = +""
|
|
56
|
+
@parts.each { |p| buffer << encode_part(p) }
|
|
57
|
+
buffer << "--#{@boundary}--\r\n"
|
|
58
|
+
buffer
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [String] the value to use for the `Content-Type` header
|
|
62
|
+
def content_type
|
|
63
|
+
"multipart/form-data; boundary=#{@boundary}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def encode_part(part)
|
|
69
|
+
header = "--#{@boundary}\r\n"
|
|
70
|
+
header << if part.file?
|
|
71
|
+
%(Content-Disposition: form-data; name="#{part.name}"; filename="#{part.filename}"\r\n) +
|
|
72
|
+
"Content-Type: #{part.content_type}\r\n\r\n"
|
|
73
|
+
else
|
|
74
|
+
%(Content-Disposition: form-data; name="#{part.name}"\r\n\r\n)
|
|
75
|
+
end
|
|
76
|
+
header << read_value(part.value)
|
|
77
|
+
header << "\r\n"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def read_value(value)
|
|
81
|
+
value.respond_to?(:read) ? value.read : value.to_s
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module GroqRuby
|
|
2
|
+
# Value object describing a single HTTP request. Built by resources,
|
|
3
|
+
# consumed by {Transport}. Immutable.
|
|
4
|
+
Request = Data.define(:method, :path, :query, :body, :headers, :content_type) do
|
|
5
|
+
def initialize(method:, path:, query: nil, body: nil, headers: {}, content_type: nil)
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def with_headers(extra)
|
|
10
|
+
with(headers: headers.merge(extra))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module GroqRuby
|
|
4
|
+
module Resources
|
|
5
|
+
class Audio
|
|
6
|
+
# `client.audio.speech.create(...)` — text-to-speech. Returns the
|
|
7
|
+
# raw audio bytes (caller decides where to write them).
|
|
8
|
+
class Speech < Base
|
|
9
|
+
PATH = "/openai/v1/audio/speech".freeze
|
|
10
|
+
|
|
11
|
+
SCHEMA = Dry::Schema.define do
|
|
12
|
+
required(:input).filled(:string)
|
|
13
|
+
required(:model).filled(:string)
|
|
14
|
+
required(:voice).filled(:string)
|
|
15
|
+
optional(:response_format).filled(:string, included_in?: %w[flac mp3 mulaw ogg wav])
|
|
16
|
+
optional(:sample_rate).filled(:integer, included_in?: [8000, 16000, 22050, 24000, 32000, 44100, 48000])
|
|
17
|
+
optional(:speed).filled(:float)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param params [Hash] required: `:input` (text), `:model`, `:voice`.
|
|
21
|
+
# Optional: `:response_format` (`"flac"` | `"mp3"` | `"mulaw"` |
|
|
22
|
+
# `"ogg"` | `"wav"`), `:sample_rate`, `:speed`.
|
|
23
|
+
# @return [String] raw audio bytes — write with `File.binwrite`.
|
|
24
|
+
# @raise [ParameterError] on invalid params.
|
|
25
|
+
def create(**params)
|
|
26
|
+
body = validate!(SCHEMA, params)
|
|
27
|
+
perform(Request.new(method: :post, path: PATH, body: body), accept: :binary)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module GroqRuby
|
|
4
|
+
module Resources
|
|
5
|
+
class Audio
|
|
6
|
+
# `client.audio.transcriptions.create(...)` — speech to text. Either
|
|
7
|
+
# an in-memory `file:` IO or a remote `url:` must be supplied.
|
|
8
|
+
class Transcriptions < Base
|
|
9
|
+
PATH = "/openai/v1/audio/transcriptions".freeze
|
|
10
|
+
|
|
11
|
+
SCHEMA = Dry::Schema.define do
|
|
12
|
+
required(:model).filled(:string)
|
|
13
|
+
optional(:language).filled(:string)
|
|
14
|
+
optional(:prompt).filled(:string)
|
|
15
|
+
optional(:response_format).filled(:string, included_in?: %w[json text srt verbose_json vtt])
|
|
16
|
+
optional(:temperature).filled(:float, gteq?: 0.0, lteq?: 1.0)
|
|
17
|
+
optional(:timestamp_granularities).value(:array)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param file [IO, nil] readable IO or nil if `url:` is given.
|
|
21
|
+
# @param filename [String, nil] filename for the multipart upload.
|
|
22
|
+
# @param url [String, nil] URL the API can fetch instead of an upload.
|
|
23
|
+
# @param params [Hash] required: `:model`. Optional: `:language`,
|
|
24
|
+
# `:prompt`, `:response_format`, `:temperature`,
|
|
25
|
+
# `:timestamp_granularities`.
|
|
26
|
+
# @return [GroqRuby::Models::Transcription]
|
|
27
|
+
# @raise [ParameterError] when neither `file:` nor `url:` is given,
|
|
28
|
+
# or when other params fail validation.
|
|
29
|
+
def create(file: nil, filename: nil, url: nil, **params)
|
|
30
|
+
raise ParameterError, {file: ["either file or url is required"]} if file.nil? && url.nil?
|
|
31
|
+
body = validate!(SCHEMA, params)
|
|
32
|
+
multipart = build_multipart(file: file, filename: filename, url: url, body: body)
|
|
33
|
+
request = Request.new(method: :post, path: PATH, body: multipart.body, content_type: multipart.content_type)
|
|
34
|
+
GroqRuby::Models::Transcription.from_hash(perform(request))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def build_multipart(file:, filename:, url:, body:)
|
|
40
|
+
parts = body.map { |k, v| Multipart::Part.text(k.to_s, v) }
|
|
41
|
+
parts << Multipart::Part.text("url", url) if url
|
|
42
|
+
parts << Multipart::Part.file("file", io: file, filename: filename || "audio", content_type: "application/octet-stream") if file
|
|
43
|
+
Multipart.new(parts)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module GroqRuby
|
|
4
|
+
module Resources
|
|
5
|
+
class Audio
|
|
6
|
+
# `client.audio.translations.create(...)` — translates speech in any
|
|
7
|
+
# supported language into English text.
|
|
8
|
+
class Translations < Base
|
|
9
|
+
PATH = "/openai/v1/audio/translations".freeze
|
|
10
|
+
|
|
11
|
+
SCHEMA = Dry::Schema.define do
|
|
12
|
+
required(:model).filled(:string)
|
|
13
|
+
optional(:prompt).filled(:string)
|
|
14
|
+
optional(:response_format).filled(:string, included_in?: %w[json text srt verbose_json vtt])
|
|
15
|
+
optional(:temperature).filled(:float, gteq?: 0.0, lteq?: 1.0)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param file [IO, nil] readable IO or nil if `url:` is given.
|
|
19
|
+
# @param filename [String, nil] filename for the multipart upload.
|
|
20
|
+
# @param url [String, nil] URL the API can fetch instead of an upload.
|
|
21
|
+
# @param params [Hash] required: `:model`. Optional: `:prompt`,
|
|
22
|
+
# `:response_format`, `:temperature`.
|
|
23
|
+
# @return [GroqRuby::Models::Translation]
|
|
24
|
+
# @raise [ParameterError] when neither `file:` nor `url:` is given,
|
|
25
|
+
# or when other params fail validation.
|
|
26
|
+
def create(file: nil, filename: nil, url: nil, **params)
|
|
27
|
+
raise ParameterError, {file: ["either file or url is required"]} if file.nil? && url.nil?
|
|
28
|
+
body = validate!(SCHEMA, params)
|
|
29
|
+
multipart = build_multipart(file: file, filename: filename, url: url, body: body)
|
|
30
|
+
request = Request.new(method: :post, path: PATH, body: multipart.body, content_type: multipart.content_type)
|
|
31
|
+
GroqRuby::Models::Translation.from_hash(perform(request))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def build_multipart(file:, filename:, url:, body:)
|
|
37
|
+
parts = body.map { |k, v| Multipart::Part.text(k.to_s, v) }
|
|
38
|
+
parts << Multipart::Part.text("url", url) if url
|
|
39
|
+
parts << Multipart::Part.file("file", io: file, filename: filename || "audio", content_type: "application/octet-stream") if file
|
|
40
|
+
Multipart.new(parts)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module GroqRuby
|
|
2
|
+
module Resources
|
|
3
|
+
# Namespace for the three audio sub-resources: speech, transcriptions,
|
|
4
|
+
# and translations.
|
|
5
|
+
class Audio
|
|
6
|
+
def initialize(transport)
|
|
7
|
+
@transport = transport
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# @return [Audio::Speech]
|
|
11
|
+
def speech
|
|
12
|
+
@speech ||= Audio::Speech.new(@transport)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Audio::Transcriptions]
|
|
16
|
+
def transcriptions
|
|
17
|
+
@transcriptions ||= Audio::Transcriptions.new(@transport)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Audio::Translations]
|
|
21
|
+
def translations
|
|
22
|
+
@translations ||= Audio::Translations.new(@transport)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module GroqRuby
|
|
2
|
+
module Resources
|
|
3
|
+
# Common scaffolding shared by every resource: holds the transport and
|
|
4
|
+
# exposes a small set of helpers for building requests and surfacing
|
|
5
|
+
# validation/transport errors.
|
|
6
|
+
class Base
|
|
7
|
+
def initialize(transport)
|
|
8
|
+
@transport = transport
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
attr_reader :transport
|
|
14
|
+
|
|
15
|
+
# Run a dry-schema against caller params; raise {ParameterError} on
|
|
16
|
+
# failure so callers get a single, well-typed error to rescue.
|
|
17
|
+
def validate!(schema, params)
|
|
18
|
+
result = schema.call(params)
|
|
19
|
+
raise ParameterError, result.errors.to_h if result.failure?
|
|
20
|
+
result.to_h
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Send a buffered request and surface either the parsed body Hash or
|
|
24
|
+
# the raw String (for binary endpoints). Re-raises the {APIError}
|
|
25
|
+
# carried by a Failure result so resources stay raise-on-error.
|
|
26
|
+
def perform(request, accept: :json)
|
|
27
|
+
result = @transport.call(request, accept: accept)
|
|
28
|
+
raise result.failure if result.failure?
|
|
29
|
+
result.value!.body
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module GroqRuby
|
|
4
|
+
module Resources
|
|
5
|
+
# `client.batches.*` — async batch processing of chat completions or
|
|
6
|
+
# embeddings against an uploaded JSONL file.
|
|
7
|
+
class Batches < Base
|
|
8
|
+
BASE_PATH = "/openai/v1/batches".freeze
|
|
9
|
+
|
|
10
|
+
CREATE_SCHEMA = Dry::Schema.define do
|
|
11
|
+
required(:input_file_id).filled(:string)
|
|
12
|
+
required(:endpoint).filled(:string)
|
|
13
|
+
required(:completion_window).filled(:string)
|
|
14
|
+
optional(:metadata).value(:hash)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param params [Hash] required: `:input_file_id`, `:endpoint`,
|
|
18
|
+
# `:completion_window` (e.g. `"24h"`). Optional: `:metadata`.
|
|
19
|
+
# @return [GroqRuby::Models::Batch]
|
|
20
|
+
# @raise [ParameterError] on invalid params.
|
|
21
|
+
def create(**params)
|
|
22
|
+
body = validate!(CREATE_SCHEMA, params)
|
|
23
|
+
GroqRuby::Models::Batch.from_hash(perform(Request.new(method: :post, path: BASE_PATH, body: body)))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param batch_id [String]
|
|
27
|
+
# @return [GroqRuby::Models::Batch]
|
|
28
|
+
def retrieve(batch_id)
|
|
29
|
+
GroqRuby::Models::Batch.from_hash(perform(Request.new(method: :get, path: "#{BASE_PATH}/#{batch_id}")))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [GroqRuby::Models::BatchList]
|
|
33
|
+
def list
|
|
34
|
+
GroqRuby::Models::BatchList.from_hash(perform(Request.new(method: :get, path: BASE_PATH)))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param batch_id [String]
|
|
38
|
+
# @return [GroqRuby::Models::Batch] with `status` advanced toward `cancelled`
|
|
39
|
+
def cancel(batch_id)
|
|
40
|
+
GroqRuby::Models::Batch.from_hash(perform(Request.new(method: :post, path: "#{BASE_PATH}/#{batch_id}/cancel")))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module GroqRuby
|
|
4
|
+
module Resources
|
|
5
|
+
class Chat
|
|
6
|
+
# `client.chat.completions.create(...)` — the workhorse of the API.
|
|
7
|
+
# Supports both buffered and Server-Sent-Events streaming responses.
|
|
8
|
+
#
|
|
9
|
+
# @example Buffered call
|
|
10
|
+
# resp = client.chat.completions.create(
|
|
11
|
+
# model: "llama-3.3-70b-versatile",
|
|
12
|
+
# messages: [{role: "user", content: "Hello"}]
|
|
13
|
+
# )
|
|
14
|
+
# resp.choices.first.message.content
|
|
15
|
+
#
|
|
16
|
+
# @example Streaming call (block form)
|
|
17
|
+
# client.chat.completions.create(
|
|
18
|
+
# model: "llama-3.3-70b-versatile",
|
|
19
|
+
# messages: [{role: "user", content: "Hello"}],
|
|
20
|
+
# stream: true
|
|
21
|
+
# ) { |chunk| print chunk.choices.first.delta.content }
|
|
22
|
+
class Completions < Base
|
|
23
|
+
PATH = "/openai/v1/chat/completions".freeze
|
|
24
|
+
|
|
25
|
+
SCHEMA = Dry::Schema.define do
|
|
26
|
+
required(:model).filled(:string)
|
|
27
|
+
required(:messages).value(:array, min_size?: 1)
|
|
28
|
+
optional(:temperature).filled(:float, gteq?: 0.0, lteq?: 2.0)
|
|
29
|
+
optional(:top_p).filled(:float, gteq?: 0.0, lteq?: 1.0)
|
|
30
|
+
optional(:n).filled(:integer, gt?: 0)
|
|
31
|
+
optional(:max_tokens).filled(:integer, gt?: 0)
|
|
32
|
+
optional(:max_completion_tokens).filled(:integer, gt?: 0)
|
|
33
|
+
optional(:presence_penalty).filled(:float, gteq?: -2.0, lteq?: 2.0)
|
|
34
|
+
optional(:frequency_penalty).filled(:float, gteq?: -2.0, lteq?: 2.0)
|
|
35
|
+
optional(:seed).filled(:integer)
|
|
36
|
+
optional(:stop)
|
|
37
|
+
optional(:stream).filled(:bool)
|
|
38
|
+
optional(:user).maybe(:string)
|
|
39
|
+
optional(:tools).value(:array)
|
|
40
|
+
optional(:tool_choice)
|
|
41
|
+
optional(:response_format).value(:hash)
|
|
42
|
+
optional(:logprobs).filled(:bool)
|
|
43
|
+
optional(:top_logprobs).filled(:integer, gteq?: 0, lteq?: 20)
|
|
44
|
+
optional(:reasoning_effort).filled(:string, included_in?: %w[none default low medium high])
|
|
45
|
+
optional(:reasoning_format).filled(:string, included_in?: %w[hidden raw parsed])
|
|
46
|
+
optional(:service_tier).filled(:string, included_in?: %w[auto on_demand flex performance])
|
|
47
|
+
optional(:parallel_tool_calls).filled(:bool)
|
|
48
|
+
optional(:logit_bias).value(:hash)
|
|
49
|
+
optional(:metadata).value(:hash)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param stream [Boolean] if true, response is streamed via SSE.
|
|
53
|
+
# @param params [Hash] all chat completion parameters supported by
|
|
54
|
+
# the Groq API. Required: `:model`, `:messages`. Many optional:
|
|
55
|
+
# `:temperature`, `:top_p`, `:n`, `:max_completion_tokens`, `:stop`,
|
|
56
|
+
# `:tools`, `:tool_choice`, `:response_format`, `:reasoning_effort`,
|
|
57
|
+
# `:reasoning_format`, `:service_tier`, `:seed`, `:logit_bias`,
|
|
58
|
+
# `:metadata`, etc.
|
|
59
|
+
# @yieldparam chunk [GroqRuby::Models::ChatCompletionChunk]
|
|
60
|
+
# yielded for each event when `stream: true`.
|
|
61
|
+
# @return [GroqRuby::Models::ChatCompletion] when buffered.
|
|
62
|
+
# @return [GroqRuby::Streaming::ChunkStream] when `stream: true`
|
|
63
|
+
# and no block given. Returns nil when streaming with a block.
|
|
64
|
+
# @raise [ParameterError] when params fail dry-schema validation.
|
|
65
|
+
# @raise [APIStatusError] subclass on 4xx/5xx response.
|
|
66
|
+
# @raise [APIConnectionError] on network failure.
|
|
67
|
+
def create(stream: false, **params, &block)
|
|
68
|
+
body = validate!(SCHEMA, params.merge(stream: stream))
|
|
69
|
+
if stream
|
|
70
|
+
stream_response(body, &block)
|
|
71
|
+
else
|
|
72
|
+
buffered_response(body)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def buffered_response(body)
|
|
79
|
+
payload = perform(Request.new(method: :post, path: PATH, body: body))
|
|
80
|
+
GroqRuby::Models::ChatCompletion.from_hash(payload)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def stream_response(body, &block)
|
|
84
|
+
chunks = Streaming::ChunkStream.new(
|
|
85
|
+
transport: transport,
|
|
86
|
+
request: Request.new(method: :post, path: PATH, body: body, headers: {"Accept" => "text/event-stream"}),
|
|
87
|
+
chunk_model: GroqRuby::Models::ChatCompletionChunk
|
|
88
|
+
)
|
|
89
|
+
block ? chunks.each(&block) : chunks
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module GroqRuby
|
|
2
|
+
module Resources
|
|
3
|
+
# Namespace for the chat sub-resources (only `completions` for now).
|
|
4
|
+
# Mirrors `client.chat.completions.create(...)` from the python SDK.
|
|
5
|
+
class Chat
|
|
6
|
+
def initialize(transport)
|
|
7
|
+
@transport = transport
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# @return [Chat::Completions]
|
|
11
|
+
def completions
|
|
12
|
+
@completions ||= Chat::Completions.new(@transport)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module GroqRuby
|
|
4
|
+
module Resources
|
|
5
|
+
# `client.embeddings.create(...)` — turns text into vectors.
|
|
6
|
+
class Embeddings < Base
|
|
7
|
+
PATH = "/openai/v1/embeddings".freeze
|
|
8
|
+
|
|
9
|
+
SCHEMA = Dry::Schema.define do
|
|
10
|
+
required(:model).filled(:string)
|
|
11
|
+
required(:input)
|
|
12
|
+
optional(:encoding_format).filled(:string, included_in?: %w[float base64])
|
|
13
|
+
optional(:user).maybe(:string)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param params [Hash] required: `:model`, `:input` (String or Array of Strings).
|
|
17
|
+
# Optional: `:encoding_format` (`"float"` | `"base64"`), `:user`.
|
|
18
|
+
# @return [GroqRuby::Models::CreateEmbeddingResponse]
|
|
19
|
+
# @raise [ParameterError] when params fail validation.
|
|
20
|
+
# @raise [APIStatusError] subclass on 4xx/5xx response.
|
|
21
|
+
def create(**params)
|
|
22
|
+
body = validate!(SCHEMA, params)
|
|
23
|
+
payload = perform(Request.new(method: :post, path: PATH, body: body))
|
|
24
|
+
GroqRuby::Models::CreateEmbeddingResponse.from_hash(payload)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module GroqRuby
|
|
4
|
+
module Resources
|
|
5
|
+
# `client.files.*` — upload, list, fetch, and delete files used by the
|
|
6
|
+
# batch endpoints.
|
|
7
|
+
class Files < Base
|
|
8
|
+
BASE_PATH = "/openai/v1/files".freeze
|
|
9
|
+
|
|
10
|
+
CREATE_SCHEMA = Dry::Schema.define do
|
|
11
|
+
required(:purpose).filled(:string, included_in?: %w[batch])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Upload a JSONL file to be referenced from a batch job.
|
|
15
|
+
#
|
|
16
|
+
# @param file [IO, File] readable IO whose `#read` returns the bytes.
|
|
17
|
+
# @param filename [String] name to send with the multipart upload.
|
|
18
|
+
# @param purpose [String] currently always `"batch"`.
|
|
19
|
+
# @return [GroqRuby::Models::FileObject]
|
|
20
|
+
# @raise [ParameterError] when `purpose` is not `"batch"`.
|
|
21
|
+
def create(file:, filename:, purpose:)
|
|
22
|
+
validate!(CREATE_SCHEMA, {purpose: purpose})
|
|
23
|
+
multipart = Multipart.new([
|
|
24
|
+
Multipart::Part.text("purpose", purpose),
|
|
25
|
+
Multipart::Part.file("file", io: file, filename: filename, content_type: "application/jsonl")
|
|
26
|
+
])
|
|
27
|
+
request = Request.new(method: :post, path: BASE_PATH, body: multipart.body, content_type: multipart.content_type)
|
|
28
|
+
GroqRuby::Models::FileObject.from_hash(perform(request))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [GroqRuby::Models::FileList]
|
|
32
|
+
def list
|
|
33
|
+
GroqRuby::Models::FileList.from_hash(perform(Request.new(method: :get, path: BASE_PATH)))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param file_id [String]
|
|
37
|
+
# @return [GroqRuby::Models::FileObject]
|
|
38
|
+
def info(file_id)
|
|
39
|
+
GroqRuby::Models::FileObject.from_hash(perform(Request.new(method: :get, path: "#{BASE_PATH}/#{file_id}")))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param file_id [String]
|
|
43
|
+
# @return [String] the raw bytes of the file
|
|
44
|
+
def content(file_id)
|
|
45
|
+
perform(Request.new(method: :get, path: "#{BASE_PATH}/#{file_id}/content"), accept: :binary)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param file_id [String]
|
|
49
|
+
# @return [GroqRuby::Models::FileDeleted]
|
|
50
|
+
def delete(file_id)
|
|
51
|
+
GroqRuby::Models::FileDeleted.from_hash(perform(Request.new(method: :delete, path: "#{BASE_PATH}/#{file_id}")))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|