llmshim 0.3.3 → 0.3.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 373860a58de51c825431e5190b6799d910233273012847b371855bbbce04c432
4
- data.tar.gz: 4a6289b522e14e4cdc94d55666e8bb379ef04924f48dccd4e3f1d6c0ae27aa07
3
+ metadata.gz: dc7d1f66fec1e50c72a93b0470ef6df88d0b61be7fbc3828972280cda625caa1
4
+ data.tar.gz: 614251f70adf8b39377307c04698fa09db451266006e590628e18b8c8a4fad9c
5
5
  SHA512:
6
- metadata.gz: e2a5c5ce9b3b27ca8d95511a9acf903c2c00cacc329e5bb765d1479f27f907fa421c7b8f31cd3e355f7e7e3a69708b6b7c88ecf778cca5cfa9e2d8c340fa4f75
7
- data.tar.gz: 8ed01b6ffc12eaf18059a0264c0943bb0959721bf1fdb5793cb8fe3473de88d077b453329170684e0aacef7f0e5a4f4ec3c24bc4c1ccf51eb9aab6d6525aaa9d
6
+ metadata.gz: 68b3fcc22b3371f110a6c5dcd123bbedb4e673345fd9ec7ff4b919eb374dffbec070cc55913fa82cf8ab9d4afbe7d445f99a926700465147c521057f5689039d
7
+ data.tar.gz: e0bf20f098bf17496665d105abd69d79bff92513dc11141c64fadef4a753e4f9a57fc9ac41e9347a81490eeaa48b54bfe520160ead394c4559969b5104b0def1
data/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  A thin, dependency-free Ruby client for the [llmshim](https://github.com/sanjay920/llmshim)
4
4
  multi-provider LLM proxy. Send OpenAI-style chat requests to a running llmshim
5
- proxy and let it translate to OpenAI, Anthropic, Google Gemini, or xAI.
5
+ proxy and let it translate to OpenAI, Anthropic, Google Gemini, xAI, OpenRouter,
6
+ vLLM, or SGLang.
6
7
 
7
8
  This gem talks to a proxy over plain HTTP — it does **not** spawn the Rust
8
9
  binary. Start the proxy separately (`llmshim proxy`, default `http://localhost:3000`).
@@ -38,10 +39,22 @@ gem install ./llmshim-*.gem
38
39
  llmshim proxy # listens on 0.0.0.0:3000 by default
39
40
  ```
40
41
 
41
- Configure provider API keys for the proxy via `llmshim configure` or the
42
- `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` / `XAI_API_KEY`
42
+ Configure provider credentials for the proxy via `llmshim configure` or
43
43
  environment variables. The Ruby client never sees your keys — the proxy holds them.
44
44
 
45
+ | Provider | Model string form | Env vars |
46
+ | ---------- | --------------------------------------- | ------------------------------------- |
47
+ | OpenAI | `openai/gpt-5.6-sol` | `OPENAI_API_KEY` |
48
+ | Anthropic | `anthropic/claude-sonnet-5` | `ANTHROPIC_API_KEY` |
49
+ | Gemini | `gemini/gemini-3.5-flash` | `GEMINI_API_KEY` |
50
+ | xAI | `xai/grok-4.5` | `XAI_API_KEY` |
51
+ | OpenRouter | `openrouter/anthropic/claude-sonnet-4.5` | `OPENROUTER_API_KEY` |
52
+ | vLLM | `vllm/<served-model>` | `VLLM_BASE_URL` (+ optional `VLLM_API_KEY`) |
53
+ | SGLang | `sglang/<served-model>` | `SGLANG_BASE_URL` (+ optional `SGLANG_API_KEY`) |
54
+
55
+ vLLM and SGLang are self-hosted (local or remote) OpenAI-compatible servers;
56
+ point the proxy at them with `VLLM_BASE_URL` / `SGLANG_BASE_URL`.
57
+
45
58
  ## Quickstart
46
59
 
47
60
  ```ruby
@@ -49,10 +62,12 @@ require "llmshim"
49
62
 
50
63
  client = Llmshim::Client.new(base_url: "http://localhost:3000")
51
64
 
52
- resp = client.chat(model: "claude-sonnet-4-6", messages: "What is Rust?")
65
+ resp = client.chat(model: "anthropic/claude-sonnet-5", messages: "What is Rust?")
53
66
  puts resp.content # => "Rust is a systems programming language..."
54
67
  puts resp.provider # => "anthropic"
68
+ puts resp.latency_ms # => 1234 (round-trip latency in ms)
55
69
  puts resp.usage.total_tokens
70
+ puts resp.usage.reasoning_tokens # thinking tokens billed (nil if none)
56
71
  ```
57
72
 
58
73
  `messages:` accepts a single string (treated as one user message) or an array
@@ -71,6 +86,12 @@ resp = client.chat(
71
86
  )
72
87
  ```
73
88
 
89
+ ### Streaming vs. non-streaming
90
+
91
+ `chat` is non-streaming and returns a single `Llmshim::ChatResponse`. Passing
92
+ `stream: true` to `chat` raises `ArgumentError` — the proxy would emit SSE that
93
+ `chat` cannot parse. Use `stream` (below) for token-by-token output.
94
+
74
95
  ### Module-level convenience
75
96
 
76
97
  A shared default client (base URL from `LLMSHIM_BASE_URL`, else `http://localhost:3000`):
@@ -89,7 +110,7 @@ puts resp.content
89
110
  `done`, `error`.
90
111
 
91
112
  ```ruby
92
- client.stream(model: "claude-sonnet-4-6", messages: "Write a haiku") do |event|
113
+ client.stream(model: "anthropic/claude-sonnet-5", messages: "Write a haiku") do |event|
93
114
  case event.type
94
115
  when "reasoning" then print event.text # thinking tokens
95
116
  when "content" then print event.text # answer tokens
@@ -114,7 +135,7 @@ text = events.select(&:content?).map(&:text).join
114
135
 
115
136
  ```ruby
116
137
  resp = client.chat(
117
- model: "anthropic/claude-sonnet-4-6",
138
+ model: "anthropic/claude-sonnet-5",
118
139
  messages: "What's the weather in SF?",
119
140
  tools: [
120
141
  { type: "function",
@@ -123,10 +144,10 @@ resp = client.chat(
123
144
  properties: { city: { type: "string" } } } } }
124
145
  ],
125
146
  tool_choice: "auto",
126
- # Raw provider-specific JSON merged into the underlying request:
127
- provider_config: { thinking: { type: "adaptive" } },
147
+ # Provider-specific controls, namespaced under x-<provider> (see below):
148
+ provider_config: { "x-anthropic" => { thinking: { type: "enabled", budget_tokens: 4000 } } },
128
149
  # Try these models if the primary fails with a retryable error:
129
- fallback: ["openai/gpt-5.5", "gemini/gemini-3-flash-preview"]
150
+ fallback: ["openai/gpt-5.6-sol", "gemini/gemini-3.5-flash"]
130
151
  )
131
152
 
132
153
  resp.message.tool_calls.each do |tc|
@@ -136,7 +157,63 @@ end
136
157
 
137
158
  `tools` and `tool_choice` are folded into `provider_config` (passed straight
138
159
  through to the provider). `max_tokens`, `temperature`, `top_p`, `top_k`,
139
- `stop`, and `reasoning_effort` are folded into the request `config`.
160
+ `stop`, `reasoning_effort`, and `reasoning_mode` are folded into the request
161
+ `config`.
162
+
163
+ ### Provider passthrough (`provider_config`)
164
+
165
+ `provider_config` merges at the **request root**, so native provider controls
166
+ must be namespaced under an `x-<provider>` key — a bare `thinking:` at the top
167
+ level is ignored. Use the namespace matching the target provider:
168
+
169
+ | Provider | Namespace |
170
+ | ---------- | --------------- |
171
+ | OpenAI | `x-openai` |
172
+ | Anthropic | `x-anthropic` |
173
+ | Gemini | `x-gemini` |
174
+ | OpenRouter | `x-openrouter` |
175
+ | vLLM | `x-vllm` |
176
+ | SGLang | `x-sglang` |
177
+
178
+ ```ruby
179
+ # Anthropic extended thinking:
180
+ provider_config: { "x-anthropic" => { thinking: { type: "enabled", budget_tokens: 4000 } } }
181
+
182
+ # OpenRouter provider routing (also accepts `models`, `transforms`):
183
+ provider_config: { "x-openrouter" => { provider: { sort: "throughput" } } }
184
+ ```
185
+
186
+ `provider_config` also carries a few root-level keys the proxy understands
187
+ directly (not namespaced):
188
+
189
+ - `tools` / `tool_choice` — folded in for you from the `tools:`/`tool_choice:` kwargs.
190
+ - `response_format` — e.g. `{ type: "json_object" }` for structured output.
191
+ - `reasoning_summary` — request a summary of the model's reasoning.
192
+
193
+ ```ruby
194
+ provider_config: {
195
+ response_format: { type: "json_object" },
196
+ reasoning_summary: "auto"
197
+ }
198
+ ```
199
+
200
+ ### Reasoning controls
201
+
202
+ Two provider-agnostic knobs live in `config` (pass them as top-level kwargs):
203
+
204
+ - `reasoning_effort` — one of `none`, `low`, `medium`, `high`, `xhigh`, `max`.
205
+ Clamped to the nearest tier the target model supports.
206
+ - `reasoning_mode` — `standard` or `pro`. `pro` is native on OpenAI gpt-5.6/-pro
207
+ models and emulated as a one-tier effort bump elsewhere.
208
+
209
+ ```ruby
210
+ resp = client.chat(
211
+ model: "openai/gpt-5.6-sol",
212
+ messages: "Prove that sqrt(2) is irrational.",
213
+ reasoning_effort: "high",
214
+ reasoning_mode: "pro"
215
+ )
216
+ ```
140
217
 
141
218
  ## Models and health
142
219
 
@@ -43,7 +43,15 @@ module Llmshim
43
43
  # @param opts [Hash] see #build_body (max_tokens, temperature, top_p, top_k,
44
44
  # stop, reasoning_effort, config, provider_config, tools, tool_choice, fallback)
45
45
  # @return [Llmshim::ChatResponse]
46
+ # @raise [ArgumentError] if +stream: true+ is passed — /v1/chat would emit
47
+ # SSE, which this method cannot parse. Use #stream for streaming.
46
48
  def chat(model:, messages:, **opts)
49
+ if opts[:stream]
50
+ raise ArgumentError,
51
+ "chat() returns a single ChatResponse and cannot stream. " \
52
+ "Use #stream (or Llmshim.stream) for streaming responses."
53
+ end
54
+
47
55
  body = build_body(model, messages, opts)
48
56
  hash = post_json("/v1/chat", body)
49
57
  ChatResponse.from_hash(hash)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Llmshim
4
- VERSION = "0.3.3"
4
+ VERSION = "0.3.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llmshim
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sanjay Nadhavajhala
@@ -55,8 +55,8 @@ dependencies:
55
55
  description: |
56
56
  A thin, dependency-free HTTP client for a running llmshim proxy. Send
57
57
  OpenAI-style chat requests and let llmshim translate to OpenAI, Anthropic,
58
- Google Gemini, or xAI. Supports non-streaming and SSE streaming, model
59
- listing, and health checks. Standard library only.
58
+ Google Gemini, xAI, OpenRouter, vLLM, or SGLang. Supports non-streaming and
59
+ SSE streaming, model listing, and health checks. Standard library only.
60
60
  email:
61
61
  - sanjay@f2.ai
62
62
  executables: []