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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +57 -0
  4. data/CLAUDE.md +103 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +495 -0
  7. data/Rakefile +11 -0
  8. data/examples/README.md +39 -0
  9. data/examples/batch.rb +29 -0
  10. data/examples/chat_completion.rb +24 -0
  11. data/examples/chat_completion_stop.rb +19 -0
  12. data/examples/chat_completion_streaming.rb +23 -0
  13. data/examples/embedding.rb +20 -0
  14. data/examples/error_handling.rb +27 -0
  15. data/examples/file_upload.rb +23 -0
  16. data/examples/mcp_agent.rb +63 -0
  17. data/examples/mcp_chat_with_tools.rb +103 -0
  18. data/examples/mcp_resources_and_prompts.rb +89 -0
  19. data/examples/models_list.rb +16 -0
  20. data/examples/speech.rb +23 -0
  21. data/examples/transcription.rb +23 -0
  22. data/examples/translation.rb +22 -0
  23. data/lib/groq_ruby/client.rb +69 -0
  24. data/lib/groq_ruby/configuration.rb +62 -0
  25. data/lib/groq_ruby/error_mapper.rb +37 -0
  26. data/lib/groq_ruby/errors/api_connection_error.rb +8 -0
  27. data/lib/groq_ruby/errors/api_error.rb +14 -0
  28. data/lib/groq_ruby/errors/api_response_error.rb +5 -0
  29. data/lib/groq_ruby/errors/api_status_error.rb +23 -0
  30. data/lib/groq_ruby/errors/api_timeout_error.rb +8 -0
  31. data/lib/groq_ruby/errors/authentication_error.rb +4 -0
  32. data/lib/groq_ruby/errors/bad_request_error.rb +4 -0
  33. data/lib/groq_ruby/errors/configuration_error.rb +4 -0
  34. data/lib/groq_ruby/errors/conflict_error.rb +4 -0
  35. data/lib/groq_ruby/errors/error.rb +5 -0
  36. data/lib/groq_ruby/errors/internal_server_error.rb +4 -0
  37. data/lib/groq_ruby/errors/not_found_error.rb +4 -0
  38. data/lib/groq_ruby/errors/parameter_error.rb +13 -0
  39. data/lib/groq_ruby/errors/permission_denied_error.rb +4 -0
  40. data/lib/groq_ruby/errors/rate_limit_error.rb +4 -0
  41. data/lib/groq_ruby/errors/unprocessable_entity_error.rb +4 -0
  42. data/lib/groq_ruby/mcp/bridge.rb +239 -0
  43. data/lib/groq_ruby/mcp/claude_desktop_config.rb +79 -0
  44. data/lib/groq_ruby/mcp/client.rb +171 -0
  45. data/lib/groq_ruby/mcp/errors/error.rb +7 -0
  46. data/lib/groq_ruby/mcp/errors/json_rpc_error.rb +21 -0
  47. data/lib/groq_ruby/mcp/errors/protocol_error.rb +7 -0
  48. data/lib/groq_ruby/mcp/errors/timeout_error.rb +7 -0
  49. data/lib/groq_ruby/mcp/errors/transport_error.rb +6 -0
  50. data/lib/groq_ruby/mcp/errors/unknown_tool_error.rb +7 -0
  51. data/lib/groq_ruby/mcp/json_rpc.rb +51 -0
  52. data/lib/groq_ruby/mcp/prompt.rb +21 -0
  53. data/lib/groq_ruby/mcp/resource.rb +17 -0
  54. data/lib/groq_ruby/mcp/server_config.rb +22 -0
  55. data/lib/groq_ruby/mcp/tool.rb +22 -0
  56. data/lib/groq_ruby/mcp/transport.rb +32 -0
  57. data/lib/groq_ruby/mcp/transports/stdio.rb +100 -0
  58. data/lib/groq_ruby/mcp.rb +25 -0
  59. data/lib/groq_ruby/models/audio/transcription.rb +10 -0
  60. data/lib/groq_ruby/models/audio/translation.rb +8 -0
  61. data/lib/groq_ruby/models/batches/batch.rb +16 -0
  62. data/lib/groq_ruby/models/batches/batch_list.rb +10 -0
  63. data/lib/groq_ruby/models/batches/batch_request_counts.rb +8 -0
  64. data/lib/groq_ruby/models/chat/chat_completion.rb +14 -0
  65. data/lib/groq_ruby/models/chat/chat_completion_choice.rb +10 -0
  66. data/lib/groq_ruby/models/chat/chat_completion_chunk.rb +13 -0
  67. data/lib/groq_ruby/models/chat/chat_completion_chunk_choice.rb +10 -0
  68. data/lib/groq_ruby/models/chat/chat_completion_delta.rb +8 -0
  69. data/lib/groq_ruby/models/chat/chat_completion_message.rb +10 -0
  70. data/lib/groq_ruby/models/embeddings/create_embedding_response.rb +11 -0
  71. data/lib/groq_ruby/models/embeddings/embedding.rb +8 -0
  72. data/lib/groq_ruby/models/embeddings/embedding_usage.rb +8 -0
  73. data/lib/groq_ruby/models/files/file_deleted.rb +8 -0
  74. data/lib/groq_ruby/models/files/file_list.rb +10 -0
  75. data/lib/groq_ruby/models/files/file_object.rb +8 -0
  76. data/lib/groq_ruby/models/model.rb +8 -0
  77. data/lib/groq_ruby/models/model_deleted.rb +8 -0
  78. data/lib/groq_ruby/models/model_factory.rb +31 -0
  79. data/lib/groq_ruby/models/model_list.rb +10 -0
  80. data/lib/groq_ruby/models/usage.rb +11 -0
  81. data/lib/groq_ruby/multipart.rb +84 -0
  82. data/lib/groq_ruby/request.rb +13 -0
  83. data/lib/groq_ruby/resources/audio/speech.rb +32 -0
  84. data/lib/groq_ruby/resources/audio/transcriptions.rb +48 -0
  85. data/lib/groq_ruby/resources/audio/translations.rb +45 -0
  86. data/lib/groq_ruby/resources/audio.rb +26 -0
  87. data/lib/groq_ruby/resources/base.rb +33 -0
  88. data/lib/groq_ruby/resources/batches.rb +44 -0
  89. data/lib/groq_ruby/resources/chat/completions.rb +94 -0
  90. data/lib/groq_ruby/resources/chat.rb +16 -0
  91. data/lib/groq_ruby/resources/embeddings.rb +28 -0
  92. data/lib/groq_ruby/resources/files.rb +55 -0
  93. data/lib/groq_ruby/resources/models.rb +35 -0
  94. data/lib/groq_ruby/response.rb +9 -0
  95. data/lib/groq_ruby/streaming/chunk_stream.rb +58 -0
  96. data/lib/groq_ruby/streaming/event_parser.rb +23 -0
  97. data/lib/groq_ruby/transport.rb +169 -0
  98. data/lib/groq_ruby/version.rb +5 -0
  99. data/lib/groq_ruby.rb +36 -0
  100. data/lib/tasks/gem.rake +5 -0
  101. data/lib/tasks/lint/all.rake +11 -0
  102. data/lib/tasks/lint/rubocop.rake +15 -0
  103. data/lib/tasks/security.rake +11 -0
  104. data/lib/tasks/types.rake +11 -0
  105. data/sig/groq_ruby.rbs +191 -0
  106. data/sig/zeitwerk.rbs +13 -0
  107. data.tar.gz.sig +0 -0
  108. metadata +237 -0
  109. metadata.gz.sig +0 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e37b875bee74a1cd5660dd886ae8aa670b964cd6f75822abf9d66c81060e6b81
4
+ data.tar.gz: a68e3933724ff8972efef22ab22ea3e5ceb1a657e29451008770c2f63f479068
5
+ SHA512:
6
+ metadata.gz: a97a4e32dae7e9d313da6d6f4dd75e570218f55296e2daf06c071c11230e90e75832ec646f5e6e3f4cdb92e8eaa687235bd9541d20b2e96c7d3a10b6bf9bdea1
7
+ data.tar.gz: a83b33d635b1b06ff1e157a21d3c9c317ca4bbb0d0793eb575277f1cf1b6750f0fdb8e28540d8d961e123995a223e99d380d15020f3d33967ecdfa372cac9cfe
checksums.yaml.gz.sig ADDED
Binary file
data/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. The
4
+ format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0]
10
+
11
+ Initial release. Idiomatic Ruby client for the Groq API, mirroring the
12
+ surface of the [`groq-python`](https://github.com/groq/groq-python) SDK.
13
+
14
+ ### Added — Groq API
15
+
16
+ - `GroqRuby::Client` exposes `chat`, `embeddings`, `audio`, `models`,
17
+ `files`, and `batches` resources.
18
+ - Chat completions: buffered responses and Server-Sent-Events streaming
19
+ (block form or `Enumerable`-returning `ChunkStream`).
20
+ - Function (tool) calling: pass `tools:` / `tool_choice:` directly to
21
+ `chat.completions.create`.
22
+ - Embeddings: `client.embeddings.create(model:, input:)`.
23
+ - Audio: `speech` (TTS, returns raw bytes), `transcriptions`, and
24
+ `translations` (multipart upload + Whisper).
25
+ - Models: `list`, `retrieve`, `delete`.
26
+ - Files lifecycle: `create`, `list`, `info`, `content` (binary), `delete`.
27
+ - Batches: `create`, `retrieve`, `list`, `cancel`.
28
+
29
+ ### Added — MCP (Model Context Protocol)
30
+
31
+ - `GroqRuby::MCP::Client` — stdio MCP client with `initialize`
32
+ handshake, `tools/list`, `tools/call`, `resources/list`,
33
+ `resources/read`, `prompts/list`, `prompts/get`, `supports?`.
34
+ - `GroqRuby::MCP::Bridge` — wires one or more MCP servers into Groq's
35
+ `chat.completions(tools:)` parameter. Surfaces tools (namespaced
36
+ `<server>__<tool>`), resources (via a synthetic
37
+ `<server>__read_resource(uri)` tool), and prompts (`bridge.prompts`,
38
+ `bridge.get_prompt`). Optional capabilities are probed gracefully.
39
+ - `GroqRuby::MCP::ClaudeDesktopConfig` — loads the same
40
+ `claude_desktop_config.json` shape (with `${VAR}` expansion against
41
+ per-server `env` block first, then process `ENV`) and returns
42
+ `Array<ServerConfig>`.
43
+
44
+ ### Added — infrastructure
45
+
46
+ - Typed response models (`Data` definitions, frozen) with recursive
47
+ `from_hash` coercion for nested choices/messages/usage/etc.
48
+ - Per-call parameter validation via `dry-schema` raising
49
+ `ParameterError` before any HTTP request fires.
50
+ - Internal transport pipeline uses `dry-monads` `Result` + Do notation
51
+ for short-circuiting on transport failures, mapped to a typed
52
+ exception hierarchy at the public boundary
53
+ (`AuthenticationError`, `RateLimitError`, `BadRequestError`, etc.).
54
+ - RBS signatures for the public API (`sig/groq_ruby.rbs`).
55
+ - `examples/` directory with one runnable script per major endpoint
56
+ plus three MCP variants (minimal, annotated walkthrough, full
57
+ resources+prompts coverage).
data/CLAUDE.md ADDED
@@ -0,0 +1,103 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this gem is
6
+
7
+ `groq_ruby` is an idiomatic Ruby client for the [Groq](https://groq.com)
8
+ API. It mirrors the surface of the official `groq-python` SDK and is
9
+ **not** an official Groq product — its wire protocol and resource
10
+ layout come from the publicly available python SDK at
11
+ <https://github.com/groq/groq-python>.
12
+
13
+ The `README.md` is the user-facing documentation. This file documents
14
+ the *internal* conventions and constraints — read it before changing
15
+ architecture, dependencies, or naming.
16
+
17
+ > Note: the parent directory `/Users/pablo/code/posiczko/CLAUDE.md`
18
+ > documents a different project (Ougai). It does not apply here.
19
+
20
+ ## Architectural constraints
21
+
22
+ These are deliberate, non-negotiable choices. Don't change them without
23
+ explicit discussion:
24
+
25
+ - **No global state, no `GroqRuby.configure`.** Configuration is
26
+ per-`Client` instance. Multi-tenant code holds multiple clients.
27
+ Constructor reads `GROQ_API_KEY` / `GROQ_BASE_URL` from env as a
28
+ default — that's it.
29
+ - **`Net::HTTP` only.** No Faraday/HTTParty/http.rb dependency. Single
30
+ shared `GroqRuby::Transport` instance per client. Resources never
31
+ open their own sockets.
32
+ - **Internal control flow uses `dry-monads` `Result` + Do notation**
33
+ inside `Transport`. The *public* API raises typed exceptions
34
+ (`AuthenticationError`, `RateLimitError`, ...) — `Resources::Base`
35
+ unwraps the `Failure` and re-raises so callers don't see Result.
36
+ - **Per-call parameter validation via `dry-schema`** raises
37
+ `ParameterError` *before* any HTTP request fires. Schemas live next
38
+ to each resource (e.g. `Resources::Chat::Completions::SCHEMA`).
39
+ - **Response models are frozen `Data` classes** under
40
+ `GroqRuby::Models::*`, each with a `from_hash` factory via the
41
+ `ModelFactory` mixin. Coerce nested types via `coerce :field, with:`.
42
+ - **Single class per file** so Zeitwerk autoloads cleanly. The two
43
+ exceptions to "one constant per file" (the error families and the
44
+ per-domain model groups) live in collapsed sub-directories
45
+ (`lib/groq_ruby/errors/`, `lib/groq_ruby/models/<domain>/`); see the
46
+ `loader.collapse` calls in `lib/groq_ruby.rb`.
47
+ - **No `require_relative` in `lib/`.** Zeitwerk autoloads everything.
48
+ The gemspec's `require_relative "lib/groq_ruby/version"` is the only
49
+ exception (gemspec runs before zeitwerk; zeitwerk's `for_gem`
50
+ ignores `version.rb` automatically).
51
+ - **MCP integration is client-side orchestration**, not a Groq API
52
+ feature. The bridge speaks JSON-RPC 2.0 to MCP servers and converts
53
+ their tools to OpenAI-shaped function tools. Don't suggest adding a
54
+ request-side `mcp_servers:` parameter — Groq's API doesn't accept one.
55
+
56
+ ## Layout
57
+
58
+ - `lib/groq_ruby.rb` — entry point: zeitwerk setup with inflections
59
+ (acronyms: `MCP`, `APIError` family) and collapsed sub-dirs.
60
+ - `lib/groq_ruby/{client,configuration,transport,request,response,multipart,error_mapper,version}.rb` — top-level plumbing.
61
+ - `lib/groq_ruby/errors/*.rb` — error classes (collapsed → `GroqRuby::*`).
62
+ - `lib/groq_ruby/resources/{chat,audio}/*.rb` and `lib/groq_ruby/resources/*.rb` — one class per API surface.
63
+ - `lib/groq_ruby/models/{chat,audio,embeddings,files,batches}/*.rb` — collapsed → `GroqRuby::Models::*`.
64
+ - `lib/groq_ruby/streaming/*.rb` — SSE adapter (`event_stream_parser`) + `ChunkStream` enumerable.
65
+ - `lib/groq_ruby/mcp/*.rb` — MCP client, bridge, transports, error families (collapsed). `MCP::PROTOCOL_VERSION` lives in `mcp.rb`.
66
+ - `sig/groq_ruby.rbs` — public-API RBS sigs. Keep in sync when adding public API.
67
+ - `test/**/test_*.rb` — Minitest. Real network calls are forbidden — every HTTP test stubs via WebMock; every MCP test uses `test/support/fake_mcp_transport.rb`.
68
+ - `examples/*.rb` — runnable scripts. Each starts with `require "bundler/setup"; require "groq_ruby"`.
69
+
70
+ ## Commands
71
+
72
+ - `bin/setup` — `bundle install` wrapper.
73
+ - `bin/console` — IRB with the gem preloaded.
74
+ - `bundle exec rake` — default: `test` + `lint` (rubocop+standard) + `types:validate` (RBS validate). All three must pass.
75
+ - `bundle exec rake test` — Minitest suite.
76
+ - `bundle exec rake test TESTOPTS="--name=/pattern/"` — subset.
77
+ - `bundle exec rake lint` / `lint:all:autocorrect` — rubocop check / autofix.
78
+ - `bundle exec rake types:validate` — `rbs validate` against `sig/`.
79
+ - `bundle exec rake gem:build` / `install` / `release` — gem packaging (release is maintainer-only).
80
+
81
+ ## Conventions
82
+
83
+ - Ruby 3.2+ (`.rubocop.yml` inherits from Standard; the gemspec pins `>= 3.2.0`).
84
+ - Style is Standard via Rubocop. Run autocorrect before committing.
85
+ - Prefer `git mv` when reorganising files so history is preserved.
86
+ - Bump `GroqRuby::VERSION` in `lib/groq_ruby/version.rb` for releases; the gemspec reads it via `require_relative`.
87
+ - Steep is intentionally **not** part of the default rake. Steep 2.0 crashes on `Data.define do ... end` blocks (upstream). RBS validate is enough for v1.
88
+ - CI (`.github/workflows/main.yml`) currently has a placeholder branch (`.invalid`) and a non-existent Ruby version (`'4.0.3'`) in the matrix — pre-existing scaffolding issue, fix when wiring CI for real.
89
+
90
+ ## When extending the gem
91
+
92
+ - New API endpoint → new `Resources::...` class (sub-class of
93
+ `Resources::Base`) + a `Models::...` Data class for the response.
94
+ Mirror the python SDK's parameter names verbatim.
95
+ - New error condition → add a class under `lib/groq_ruby/errors/`
96
+ matching the file naming (one class per file). Update the README's
97
+ error-hierarchy table.
98
+ - New MCP capability → add a `Client` method first, then expose via
99
+ `Bridge` if the LLM should be able to use it through chat
100
+ completions. Probe optional capabilities gracefully (catch
101
+ `JsonRpcError` / `-32601`).
102
+ - New public method → add a YARD `@param`/`@return`/`@raise` and
103
+ update `sig/groq_ruby.rbs`.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pawel Osiczko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,495 @@
1
+ # groq_ruby
2
+
3
+ Idiomatic Ruby client for the [Groq](https://groq.com) API.
4
+
5
+ `groq_ruby` mirrors the surface of the official
6
+ [`groq-python`](https://github.com/groq/groq-python) SDK in a Ruby-native
7
+ shape: typed response objects, single-purpose resource classes, internal
8
+ `dry-monads` `Result` pipelines, and request validation via
9
+ `dry-schema`. Streaming chat completions are supported via Server-Sent
10
+ Events. A built-in [MCP](https://modelcontextprotocol.io) client lets you
11
+ wire one or more MCP servers into a Groq chat completion as tools.
12
+
13
+ This gem is **not** an official Groq product. The wire protocol it
14
+ implements and the API surface it mirrors come from the publicly
15
+ available `groq-python` SDK.
16
+
17
+ ## Installation
18
+
19
+ ```ruby
20
+ # Gemfile
21
+ gem "groq_ruby"
22
+ ```
23
+
24
+ ```sh
25
+ bundle install
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```ruby
31
+ require "groq_ruby"
32
+
33
+ client = GroqRuby::Client.new # reads GROQ_API_KEY from the environment
34
+
35
+ response = client.chat.completions.create(
36
+ model: "llama-3.3-70b-versatile",
37
+ messages: [
38
+ {role: "system", content: "You are a helpful assistant."},
39
+ {role: "user", content: "Explain low-latency LLMs in one sentence."}
40
+ ]
41
+ )
42
+
43
+ puts response.choices.first.message.content
44
+ puts "tokens: #{response.usage.total_tokens}"
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ Configuration is per-client and immutable — no global state, no
50
+ `GroqRuby.configure`. Build one client per tenant or set of credentials.
51
+
52
+ ```ruby
53
+ client = GroqRuby::Client.new(
54
+ api_key: ENV["GROQ_API_KEY"], # default: ENV["GROQ_API_KEY"]
55
+ base_url: "https://api.groq.com", # default: ENV["GROQ_BASE_URL"] || "https://api.groq.com"
56
+ open_timeout: 10, # connect-phase timeout, seconds
57
+ read_timeout: 60, # socket-read timeout, seconds
58
+ user_agent: "myapp/1.0" # default: "groq_ruby/<version> (ruby; net-http)"
59
+ )
60
+ ```
61
+
62
+ A `ConfigurationError` is raised at construction time if no API key can
63
+ be found.
64
+
65
+ ## Chat completions
66
+
67
+ ### Buffered
68
+
69
+ ```ruby
70
+ response = client.chat.completions.create(
71
+ model: "llama-3.3-70b-versatile",
72
+ messages: [{role: "user", content: "Hello"}],
73
+ temperature: 0.5,
74
+ max_completion_tokens: 256
75
+ )
76
+ response.choices.first.message.content
77
+ response.usage.prompt_tokens
78
+ ```
79
+
80
+ ### Streaming
81
+
82
+ Pass `stream: true`. With a block, each chunk is yielded as it arrives.
83
+ Without a block, you get a lazy `Enumerable` you can iterate later.
84
+
85
+ ```ruby
86
+ client.chat.completions.create(
87
+ model: "llama-3.3-70b-versatile",
88
+ messages: [{role: "user", content: "Write a poem about latency."}],
89
+ stream: true
90
+ ) do |chunk|
91
+ print chunk.choices.first.delta.content
92
+ end
93
+
94
+ # Or:
95
+ stream = client.chat.completions.create(model: "...", messages: [...], stream: true)
96
+ stream.each { |chunk| ... }
97
+ ```
98
+
99
+ Validation rejects out-of-range values before any HTTP request fires:
100
+
101
+ ```ruby
102
+ client.chat.completions.create(
103
+ model: "...", messages: [{role: "user", content: "x"}], temperature: 5.0
104
+ )
105
+ # => GroqRuby::ParameterError: invalid parameters: {:temperature=>["must be less than or equal to 2.0"]}
106
+ ```
107
+
108
+ ### Function (tool) calling
109
+
110
+ Pass `tools:` (and optionally `tool_choice:`) just like the OpenAI/Groq
111
+ schema — `groq_ruby` doesn't transform them. When the model decides to
112
+ call a tool, it comes back in `response.choices.first.message.tool_calls`.
113
+
114
+ ```ruby
115
+ tools = [
116
+ {
117
+ type: "function",
118
+ function: {
119
+ name: "get_weather",
120
+ description: "Look up the current weather for a city",
121
+ parameters: {
122
+ type: "object",
123
+ properties: {city: {type: "string"}},
124
+ required: ["city"]
125
+ }
126
+ }
127
+ }
128
+ ]
129
+
130
+ response = client.chat.completions.create(
131
+ model: "llama-3.3-70b-versatile",
132
+ messages: [{role: "user", content: "What's the weather in Berlin?"}],
133
+ tools: tools,
134
+ tool_choice: "auto" # "auto" | "required" | "none" | {type: "function", function: {name: "get_weather"}}
135
+ )
136
+
137
+ call = response.choices.first.message.tool_calls&.first
138
+ if call
139
+ args = JSON.parse(call["function"]["arguments"])
140
+ result = get_weather(args["city"])
141
+
142
+ # Feed the result back as a follow-up turn:
143
+ followup = client.chat.completions.create(
144
+ model: "llama-3.3-70b-versatile",
145
+ messages: [
146
+ *original_messages,
147
+ {role: "assistant", content: nil, tool_calls: response.choices.first.message.tool_calls},
148
+ {role: "tool", tool_call_id: call["id"], content: JSON.generate(result)}
149
+ ]
150
+ )
151
+ end
152
+ ```
153
+
154
+ For an MCP-driven version of this loop (where the tools come from an
155
+ MCP server instead of being hand-defined), see the [MCP section below](#mcp--model-context-protocol).
156
+
157
+ ## Embeddings
158
+
159
+ ```ruby
160
+ response = client.embeddings.create(
161
+ model: "nomic-embed-text-v1_5",
162
+ input: "Groq makes inference very fast."
163
+ )
164
+ vector = response.data.first.embedding
165
+ ```
166
+
167
+ ## Audio
168
+
169
+ ### Text → speech
170
+
171
+ ```ruby
172
+ audio_bytes = client.audio.speech.create(
173
+ input: "Hello from Groq.",
174
+ model: "playai-tts",
175
+ voice: "Aaliyah-PlayAI",
176
+ response_format: "wav"
177
+ )
178
+ File.binwrite("speech.wav", audio_bytes)
179
+ ```
180
+
181
+ ### Speech → text (transcription)
182
+
183
+ ```ruby
184
+ response = File.open("audio.mp3", "rb") do |file|
185
+ client.audio.transcriptions.create(
186
+ file: file,
187
+ filename: "audio.mp3",
188
+ model: "whisper-large-v3-turbo"
189
+ )
190
+ end
191
+ puts response.text
192
+ ```
193
+
194
+ ### Speech → English text (translation)
195
+
196
+ ```ruby
197
+ response = File.open("audio.mp3", "rb") do |file|
198
+ client.audio.translations.create(
199
+ file: file,
200
+ filename: "audio.mp3",
201
+ model: "whisper-large-v3"
202
+ )
203
+ end
204
+ ```
205
+
206
+ ## Models
207
+
208
+ ```ruby
209
+ client.models.list.data.each { |m| puts m.id }
210
+ client.models.retrieve("llama-3.3-70b-versatile")
211
+ client.models.delete("custom-model-id")
212
+ ```
213
+
214
+ ## Files
215
+
216
+ ```ruby
217
+ uploaded = File.open("requests.jsonl", "rb") do |file|
218
+ client.files.create(file: file, filename: "requests.jsonl", purpose: "batch")
219
+ end
220
+
221
+ client.files.list.data
222
+ client.files.info(uploaded.id)
223
+ client.files.content(uploaded.id) # raw bytes
224
+ client.files.delete(uploaded.id)
225
+ ```
226
+
227
+ ## Batches
228
+
229
+ ```ruby
230
+ batch = client.batches.create(
231
+ input_file_id: uploaded.id,
232
+ endpoint: "/v1/chat/completions",
233
+ completion_window: "24h"
234
+ )
235
+
236
+ client.batches.retrieve(batch.id)
237
+ client.batches.list
238
+ client.batches.cancel(batch.id)
239
+ ```
240
+
241
+ ## MCP — Model Context Protocol
242
+
243
+ `groq_ruby` ships with a stdio MCP client, so a Groq agent can use tools
244
+ exposed by any MCP-compatible server (filesystem, web, custom tooling).
245
+ Coverage matches what host applications like Claude Desktop surface:
246
+
247
+ | MCP capability | Coverage |
248
+ |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
249
+ | **Tools** | Surfaced as Groq function tools, namespaced `<server>__<tool>` |
250
+ | **Resources** | Surfaced via a synthetic `<server>__read_resource(uri)` tool the LLM can call. `bridge.resources` returns the inventory if you want to advertise specific URIs in your system prompt |
251
+ | **Prompts** | Listed via `bridge.prompts` for *your application* to surface (e.g. as a picker in your UI) and rendered via `bridge.get_prompt(name, args)` |
252
+ | Sampling, notifications | Not supported in v1 |
253
+
254
+ Optional capabilities are probed gracefully — if a server doesn't
255
+ implement `resources/list` or `prompts/list`, those entries are simply
256
+ empty for that server.
257
+
258
+ ### Configuring servers
259
+
260
+ Build `ServerConfig` directly, or load from the same JSON shape Claude
261
+ Desktop uses (`mcpServers` block). The Claude Desktop adapter expands
262
+ `${VAR}` references against each server's `env` block first, then the
263
+ process's `ENV`, and raises on unresolved references.
264
+
265
+ ```ruby
266
+ # (a) direct
267
+ config = GroqRuby::MCP::ServerConfig.new(
268
+ name: "fs",
269
+ command: "npx",
270
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/docs"]
271
+ )
272
+
273
+ # (b) load Claude Desktop JSON
274
+ configs = GroqRuby::MCP::ClaudeDesktopConfig.load(
275
+ "~/Library/Application Support/Claude/claude_desktop_config.json"
276
+ )
277
+ bridge = GroqRuby::MCP::Bridge.new(configs)
278
+
279
+ # (c) parse an in-memory hash (e.g. for a private server with a PAT)
280
+ configs = GroqRuby::MCP::ClaudeDesktopConfig.parse({
281
+ "mcpServers" => {
282
+ "spectrum-ferret-staging" => {
283
+ "command" => "npx",
284
+ "args" => [
285
+ "-y", "mcp-remote@latest", "https://mcp-staging.spectrumferret.com",
286
+ "--header", "Authorization: Bearer ${SF_PAT}"
287
+ ],
288
+ "env" => {"SF_PAT" => ENV.fetch("SF_PAT")}
289
+ }
290
+ }
291
+ })
292
+ ```
293
+
294
+ ### Direct usage
295
+
296
+ ```ruby
297
+ config = GroqRuby::MCP::ServerConfig.new(
298
+ name: "fs",
299
+ command: "npx",
300
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/docs"]
301
+ )
302
+
303
+ mcp = GroqRuby::MCP::Client.connect(config)
304
+ mcp.tools_list # [Tool(name: "read_file", ...), ...]
305
+ mcp.tools_call(name: "read_file", arguments: {path: "/Users/me/docs/foo.txt"})
306
+ mcp.resources_list # [Resource(uri: "fs://...", ...)]
307
+ mcp.resources_read("fs://docs/foo.md")
308
+ mcp.prompts_list # [Prompt(name: "summarize", ...)]
309
+ mcp.prompts_get("summarize", {path: "foo.md"})
310
+ mcp.stop
311
+ ```
312
+
313
+ ### Bridge into chat.completions
314
+
315
+ > **Important:** Groq's chat-completions API does not have a request
316
+ > parameter for "MCP server URL." MCP integration here is **client-side
317
+ > orchestration**: this gem talks to the MCP servers, exposes their tools
318
+ > as ordinary OpenAI/Groq function tools through the standard `tools:`
319
+ > parameter, then routes the model's `tool_calls` back to the right
320
+ > server. From Groq's perspective there is no MCP — just function tools.
321
+
322
+ `Bridge` does three things at construction:
323
+
324
+ 1. spawns each `ServerConfig`'s child process via stdio,
325
+ 2. runs the MCP `initialize` handshake and asks each server for its tool list,
326
+ 3. indexes those tools by namespaced name `<server>__<tool>` so collisions across servers are impossible.
327
+
328
+ `bridge.tools` then returns an array shaped exactly like Groq's
329
+ `tools:` parameter:
330
+
331
+ ```ruby
332
+ bridge.tools
333
+ # => [
334
+ # {
335
+ # type: "function",
336
+ # function: {
337
+ # name: "fs__read_file",
338
+ # description: "Read the contents of a file at the given path",
339
+ # parameters: { # JSON Schema, straight from the MCP server
340
+ # "type" => "object",
341
+ # "properties" => { "path" => { "type" => "string" } },
342
+ # "required" => ["path"]
343
+ # }
344
+ # }
345
+ # },
346
+ # ...
347
+ # ]
348
+ ```
349
+
350
+ A complete agent loop using a real MCP server (`@modelcontextprotocol/server-filesystem`):
351
+
352
+ ```ruby
353
+ require "groq_ruby"
354
+ require "json"
355
+
356
+ filesystem = GroqRuby::MCP::ServerConfig.new(
357
+ name: "fs",
358
+ command: "npx",
359
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/docs"]
360
+ )
361
+
362
+ bridge = GroqRuby::MCP::Bridge.new([filesystem])
363
+ groq = GroqRuby::Client.new
364
+
365
+ messages = [
366
+ {role: "system", content: "You can use filesystem tools. Prefer tools over speculation."},
367
+ {role: "user", content: "Summarise README.md."}
368
+ ]
369
+
370
+ begin
371
+ loop do
372
+ response = groq.chat.completions.create(
373
+ model: "llama-3.3-70b-versatile",
374
+ messages: messages,
375
+ tools: bridge.tools # <-- MCP tools surfaced as Groq function tools
376
+ )
377
+
378
+ message = response.choices.first.message
379
+ tool_calls = message.tool_calls
380
+
381
+ if tool_calls.nil? || tool_calls.empty?
382
+ puts message.content
383
+ break
384
+ end
385
+
386
+ messages << {role: "assistant", content: message.content, tool_calls: tool_calls}
387
+ tool_calls.each do |call|
388
+ fn = call["function"]
389
+ # bridge.call accepts either a Hash or the raw JSON string Groq
390
+ # returns in `function.arguments` — it routes to the owning server.
391
+ result = bridge.call(fn["name"], fn["arguments"])
392
+ messages << {role: "tool", tool_call_id: call["id"], content: JSON.generate(result)}
393
+ end
394
+ end
395
+ ensure
396
+ bridge.stop # kills child processes, closes stdio
397
+ end
398
+ ```
399
+
400
+ Runnable variants in `examples/`:
401
+ - [`examples/mcp_agent.rb`](examples/mcp_agent.rb) — minimal version of the loop above.
402
+ - [`examples/mcp_chat_with_tools.rb`](examples/mcp_chat_with_tools.rb) — same loop, heavily annotated step by step.
403
+ - [`examples/mcp_resources_and_prompts.rb`](examples/mcp_resources_and_prompts.rb) — adds resources (catalogued in the system prompt + fetched via the synthetic `read_resource` tool) and prompts.
404
+
405
+ ## Error handling
406
+
407
+ Every API failure raises a subclass of `GroqRuby::APIError`. Rescue the
408
+ base class to handle anything; rescue specific subclasses to react to
409
+ particular conditions.
410
+
411
+ ```ruby
412
+ begin
413
+ client.chat.completions.create(...)
414
+ rescue GroqRuby::AuthenticationError => e
415
+ warn "auth failed: #{e.message}"
416
+ rescue GroqRuby::RateLimitError => e
417
+ warn "rate limited; retry after backoff"
418
+ rescue GroqRuby::APIStatusError => e
419
+ warn "status #{e.status}: #{e.message}"
420
+ rescue GroqRuby::APIConnectionError => e
421
+ warn "network: #{e.message}"
422
+ end
423
+ ```
424
+
425
+ Hierarchy:
426
+
427
+ | Class | When |
428
+ |--------------------------------------|--------------------------------------------------------|
429
+ | `GroqRuby::Error` | Base for everything in the gem |
430
+ | `GroqRuby::ConfigurationError` | Missing or invalid configuration |
431
+ | `GroqRuby::ParameterError` | Request params failed validation |
432
+ | `GroqRuby::APIError` | Base for any failure talking to the API |
433
+ | `GroqRuby::APIConnectionError` | Network failure before a response |
434
+ | `GroqRuby::APITimeoutError` | Connection or read timeout |
435
+ | `GroqRuby::APIStatusError` | 4xx/5xx response (carries `status`, `headers`, `body`) |
436
+ | `GroqRuby::BadRequestError` | 400 |
437
+ | `GroqRuby::AuthenticationError` | 401 |
438
+ | `GroqRuby::PermissionDeniedError` | 403 |
439
+ | `GroqRuby::NotFoundError` | 404 |
440
+ | `GroqRuby::ConflictError` | 409 |
441
+ | `GroqRuby::UnprocessableEntityError` | 422 |
442
+ | `GroqRuby::RateLimitError` | 429 |
443
+ | `GroqRuby::InternalServerError` | 5xx |
444
+ | `GroqRuby::APIResponseError` | API returned an unexpected payload |
445
+ | `GroqRuby::MCP::Error` | Base for any MCP-layer failure |
446
+ | `GroqRuby::MCP::TransportError` | Stdio pipe broke or process exited |
447
+ | `GroqRuby::MCP::TimeoutError` | MCP request timed out |
448
+ | `GroqRuby::MCP::ProtocolError` | Server sent malformed JSON-RPC |
449
+ | `GroqRuby::MCP::JsonRpcError` | Server returned a JSON-RPC `error` |
450
+ | `GroqRuby::MCP::UnknownToolError` | `Bridge#call` couldn't find the tool |
451
+
452
+ ## Examples
453
+
454
+ The [`examples/`](examples) directory has one runnable script per major
455
+ endpoint, plus the MCP agent loop. Each reads `GROQ_API_KEY` from the
456
+ environment. See [`examples/README.md`](examples/README.md) for the full
457
+ list.
458
+
459
+ ## Compatibility
460
+
461
+ - Ruby 3.2+
462
+ - Net::HTTP (no Faraday/HTTParty dependency)
463
+
464
+ ### Not yet supported
465
+
466
+ The python SDK has a few features that aren't in `groq_ruby` v1:
467
+
468
+ - Async client (everything here is synchronous; use threads/fibers if you need concurrency).
469
+ - `with_raw_response` / `with_streaming_response` accessors (responses are always parsed into typed models).
470
+ - Built-in retries / backoff (handle in your own caller).
471
+ - MCP sampling and `notifications/list_changed` (resource and prompt inventories are snapshotted at `Bridge` construction).
472
+
473
+ ## Development
474
+
475
+ ```sh
476
+ bin/setup # install deps
477
+ bundle exec rake # tests + lint + RBS validate
478
+ bundle exec rake test # tests only
479
+ bundle exec rake test TESTOPTS="--name=/pattern/" # subset
480
+ bin/console # IRB with the gem preloaded
481
+ ```
482
+
483
+ Tests use Minitest + WebMock; no test makes real network calls.
484
+
485
+ ## Attribution
486
+
487
+ The API surface, parameter names, and resource layout follow the
488
+ official Groq Python SDK at
489
+ <https://github.com/groq/groq-python>, distributed under the Apache 2.0
490
+ licence. This gem is an independent Ruby implementation and is not
491
+ affiliated with or endorsed by Groq.
492
+
493
+ ## License
494
+
495
+ MIT — see [`LICENSE.txt`](LICENSE.txt).