llm_mock_anthropic 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
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +116 -0
- data/lib/llm_mock_anthropic/version.rb +7 -0
- data/lib/llm_mock_anthropic.rb +140 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cad30c8947173278ddcf0167d6544e1809fd5608ce62efb574fcf3b5785de14e
|
|
4
|
+
data.tar.gz: 2fc3bad0730ff740b0766617f7f9a63dc3bf93c75fea026aaa6765b6fef38d24
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 79899f2e1ea007f85fa611774894a5c6b9c2095c78124121a8fb3034ae102ee94e2586a1d4db6f564ae2c290cd4ae6dd34bb2b2253a5e9cfd7d1a38e79c01077
|
|
7
|
+
data.tar.gz: d7860e8a06ea7a8cbdbd031bcc43216ac9df081c80bde477bdd7cab64eabf7ada8749196882c53c93a382b49a72f0d055448dd09fe26c707bbcd2e4b07b11d2c
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. This project adheres to
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-06-19
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Initial release, extracted from the `deja` gem.
|
|
10
|
+
- `LlmMock::Anthropic` response structs (`Message`, `Stream`, `TextBlock`,
|
|
11
|
+
`ToolUseBlock`) and `.text` / `.tool_use` / `.message` / `.stream` builders.
|
|
12
|
+
- `LlmMock::Anthropic::Provider` implementing the `llm_mock` contract
|
|
13
|
+
(build_client, call_real, serialize, deserialize, prompt_for).
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nate Brustein
|
|
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,116 @@
|
|
|
1
|
+
# llm_mock_anthropic
|
|
2
|
+
|
|
3
|
+
When you test Ruby code that calls the Anthropic API, you usually don't want it
|
|
4
|
+
hitting the network. One clean way to avoid that is to **stub your Anthropic
|
|
5
|
+
client and return a canned response object** — but that runs into a wall:
|
|
6
|
+
constructing a realistic Anthropic SDK response by hand is genuinely painful.
|
|
7
|
+
A real `Anthropic::Message` needs `id`, `model`, `role`, `type`, `usage`,
|
|
8
|
+
`stop_reason`, and typed content blocks; tool calls are nested; and **streaming
|
|
9
|
+
responses have no simple object to fake at all**.
|
|
10
|
+
|
|
11
|
+
`llm_mock_anthropic` gives you small, ergonomic stand-ins for exactly those
|
|
12
|
+
response shapes — messages, text blocks, tool_use blocks, and streams — so your
|
|
13
|
+
stub can return something your code happily consumes:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
allow(client.messages).to receive(:create).and_return(
|
|
17
|
+
LlmMock::Anthropic.message([
|
|
18
|
+
LlmMock::Anthropic.tool_use(id: "tu_1", name: "save_summary", input: {"text" => "…"}),
|
|
19
|
+
])
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Why object-level (and when not to)
|
|
24
|
+
|
|
25
|
+
The common community approach is to stub at the **HTTP layer** (VCR/WebMock):
|
|
26
|
+
record real HTTP and let the SDK deserialize it. That's a great fit when you can
|
|
27
|
+
make a real call once. Stub at the **object layer** — what this gem is for — when
|
|
28
|
+
that's awkward, most often because:
|
|
29
|
+
|
|
30
|
+
- **Streaming.** Replaying SSE streams through VCR is fiddly; returning a
|
|
31
|
+
`Stream` double is trivial.
|
|
32
|
+
- **You want to script the model's behavior** deterministically (e.g. "this turn
|
|
33
|
+
calls the `complete` tool") without recording anything.
|
|
34
|
+
|
|
35
|
+
If you want to *record real calls once and replay them* rather than hand-script
|
|
36
|
+
responses, see [`deja`](https://github.com/nbrustein/deja) — it builds on this
|
|
37
|
+
gem.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Gemfile
|
|
43
|
+
group :test do
|
|
44
|
+
gem "llm_mock_anthropic"
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What you get
|
|
49
|
+
|
|
50
|
+
Response value objects (duck-typed to the SDK's response surface — `.content`,
|
|
51
|
+
block fields, `.text`, `.accumulated_message`):
|
|
52
|
+
|
|
53
|
+
| Builder | Returns | Shape |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `LlmMock::Anthropic.text(str)` | `TextBlock` | `.type`, `.text` |
|
|
56
|
+
| `LlmMock::Anthropic.tool_use(id:, name:, input:)` | `ToolUseBlock` | `.type`, `.id`, `.name`, `.input` |
|
|
57
|
+
| `LlmMock::Anthropic.message(blocks)` | `Message` | `.content` |
|
|
58
|
+
| `LlmMock::Anthropic.stream(text_chunks:, message:)` | `Stream` | `.text`, `.accumulated_message` |
|
|
59
|
+
|
|
60
|
+
The structs are also available directly (`LlmMock::Anthropic::Message.new(...)`)
|
|
61
|
+
if you prefer.
|
|
62
|
+
|
|
63
|
+
### Example: a streamed tutor turn that ends by calling a tool
|
|
64
|
+
|
|
65
|
+
Say the code under test streams a reply and finishes when the model calls a
|
|
66
|
+
`complete` tool — it calls `messages.stream`, renders the incremental text, then
|
|
67
|
+
inspects the final message:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
class TutorTurn
|
|
71
|
+
def run(client, conversation)
|
|
72
|
+
stream = client.messages.stream(
|
|
73
|
+
model: "claude-sonnet-4-5",
|
|
74
|
+
max_tokens: 1024,
|
|
75
|
+
messages: conversation,
|
|
76
|
+
tools: [ complete_tool ],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
stream.text.each {|chunk| broadcast(chunk) } # render text as it arrives
|
|
80
|
+
|
|
81
|
+
stream.accumulated_message.content.each do |block|
|
|
82
|
+
finish! if block.type == :tool_use && block.name == "complete"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
In a test, return a fake stream so that code runs without the network — one text
|
|
89
|
+
chunk plus a final message that includes the `complete` tool call:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
fake = LlmMock::Anthropic.stream(
|
|
93
|
+
text_chunks: [ "Here's the core idea. " ],
|
|
94
|
+
message: LlmMock::Anthropic.message([
|
|
95
|
+
LlmMock::Anthropic.text("Here's the core idea. "),
|
|
96
|
+
LlmMock::Anthropic.tool_use(id: "tu_done", name: "complete", input: {}),
|
|
97
|
+
]),
|
|
98
|
+
)
|
|
99
|
+
allow(client.messages).to receive(:stream).and_return(fake)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`stream.text` yields the chunks and `stream.accumulated_message.content` is the
|
|
103
|
+
final block list — exactly the surface `TutorTurn#run` reads from a real stream.
|
|
104
|
+
|
|
105
|
+
## For tool authors
|
|
106
|
+
|
|
107
|
+
`LlmMock::Anthropic::Provider` implements the
|
|
108
|
+
[`llm_mock`](https://github.com/nbrustein/llm_mock) contract — it builds a stub
|
|
109
|
+
client routed through a responder, invokes the real client, and
|
|
110
|
+
serializes/deserializes responses to/from plain hashes. That's how `deja` uses
|
|
111
|
+
this gem to record and replay Anthropic calls. You don't need any of that to use
|
|
112
|
+
the builders above.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "llm_mock"
|
|
4
|
+
require "llm_mock_anthropic/version"
|
|
5
|
+
|
|
6
|
+
module LlmMock
|
|
7
|
+
# Fabricates Anthropic Ruby SDK response objects for tests, and serializes them
|
|
8
|
+
# to/from the plain hashes a cache stores.
|
|
9
|
+
#
|
|
10
|
+
# `LlmMock::Anthropic` is the namespace: the response structs and the builder
|
|
11
|
+
# helpers live here, and `LlmMock::Anthropic::Provider` is the object a
|
|
12
|
+
# consumer (e.g. deja) drives. Use `::Anthropic` for the SDK constant — bare
|
|
13
|
+
# `Anthropic` inside this module would resolve to here.
|
|
14
|
+
module Anthropic
|
|
15
|
+
# Value objects shaped like what the SDK returns. App code reads `.content`
|
|
16
|
+
# on a message, the block fields on each block, and `.text` /
|
|
17
|
+
# `.accumulated_message` on a stream — these provide exactly that surface.
|
|
18
|
+
TextBlock = Struct.new(:type, :text)
|
|
19
|
+
ToolUseBlock = Struct.new(:type, :id, :name, :input)
|
|
20
|
+
Message = Struct.new(:content)
|
|
21
|
+
Stream = Struct.new(:text, :accumulated_message)
|
|
22
|
+
|
|
23
|
+
# Ergonomic builders for canned responses — handy in specs that stub the
|
|
24
|
+
# client directly and return a scripted response.
|
|
25
|
+
#
|
|
26
|
+
# LlmMock::Anthropic.message([
|
|
27
|
+
# LlmMock::Anthropic.text("Here's the idea."),
|
|
28
|
+
# LlmMock::Anthropic.tool_use(id: "tu_1", name: "do_thing", input: {"x" => 1}),
|
|
29
|
+
# ])
|
|
30
|
+
def self.text(text)
|
|
31
|
+
TextBlock.new(:text, text)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.tool_use(id:, name:, input:)
|
|
35
|
+
ToolUseBlock.new(:tool_use, id, name, input)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.message(blocks)
|
|
39
|
+
Message.new(blocks)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.stream(text_chunks:, message:)
|
|
43
|
+
Stream.new(text_chunks, message)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Drives the Anthropic SDK shape for a consumer like deja: builds the stub
|
|
47
|
+
# client, invokes the real client, and (de)serializes responses.
|
|
48
|
+
class Provider < LlmMock::Provider
|
|
49
|
+
def build_client(&responder)
|
|
50
|
+
messages = Object.new
|
|
51
|
+
messages.define_singleton_method(:create) {|**kwargs| responder.call(:create, kwargs) }
|
|
52
|
+
messages.define_singleton_method(:stream) {|**kwargs| responder.call(:stream, kwargs) }
|
|
53
|
+
|
|
54
|
+
client = Object.new
|
|
55
|
+
client.define_singleton_method(:messages) { messages }
|
|
56
|
+
client
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def call_real(client, method, kwargs)
|
|
60
|
+
client.messages.public_send(method, **kwargs)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def default_real_client
|
|
64
|
+
-> { ::Anthropic::Client.new }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def prompt_for(kwargs)
|
|
68
|
+
kwargs[:system].to_s
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def serialize(method, response)
|
|
72
|
+
method == :stream ? serialize_stream(response) : serialize_message(response)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def deserialize(method, data)
|
|
76
|
+
method == :stream ? deserialize_stream(data) : deserialize_message(data)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def serialize_message(message)
|
|
82
|
+
build_response(message.content.map {|block| serialize_block(block) })
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def serialize_stream(stream)
|
|
86
|
+
build_response(
|
|
87
|
+
stream.accumulated_message.content.map {|block| serialize_block(block) },
|
|
88
|
+
text_chunks: stream.text.to_a,
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The recorded `response` hash. `content` is what deserialize replays; the
|
|
93
|
+
# `text_response`/`tool_uses` fields (and any consumer's summary) are
|
|
94
|
+
# readable conveniences derived from it.
|
|
95
|
+
def build_response(blocks, text_chunks: nil)
|
|
96
|
+
text_blocks = blocks.select {|b| b["type"] == "text" }
|
|
97
|
+
tool_use_blocks = blocks.select {|b| b["type"] == "tool_use" }
|
|
98
|
+
|
|
99
|
+
response = {}
|
|
100
|
+
response["text_response"] = text_blocks.map {|b| b["text"] }.join("\n") unless text_blocks.empty?
|
|
101
|
+
unless tool_use_blocks.empty?
|
|
102
|
+
response["tool_uses"] = tool_use_blocks.map do |b|
|
|
103
|
+
{"id" => b["id"], "name" => b["name"], "input" => b["input"]}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
response["content"] = blocks
|
|
107
|
+
response["text_chunks"] = text_chunks if text_chunks
|
|
108
|
+
response
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def serialize_block(block)
|
|
112
|
+
case block.type.to_s
|
|
113
|
+
when "tool_use"
|
|
114
|
+
{"type" => "tool_use", "id" => block.id, "name" => block.name, "input" => block.input}
|
|
115
|
+
else
|
|
116
|
+
{"type" => block.type.to_s, "text" => block.text}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def deserialize_message(data)
|
|
121
|
+
Message.new(data["content"].map {|b| deserialize_block(b) })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def deserialize_block(data)
|
|
125
|
+
case data["type"]
|
|
126
|
+
when "tool_use"
|
|
127
|
+
ToolUseBlock.new(:tool_use, data["id"], data["name"], data["input"])
|
|
128
|
+
else
|
|
129
|
+
TextBlock.new(data["type"].to_sym, data["text"])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def deserialize_stream(data)
|
|
134
|
+
blocks = (data["content"] || [ {"type" => "text", "text" => data["text_chunks"].join} ])
|
|
135
|
+
.map {|b| deserialize_block(b) }
|
|
136
|
+
Stream.new(data["text_chunks"].dup, Message.new(blocks))
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: llm_mock_anthropic
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nate Brustein
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: llm_mock
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
description: |
|
|
41
|
+
Fabricate Anthropic Ruby SDK response objects — messages, content blocks,
|
|
42
|
+
tool_use blocks, and streams — for tests that stub the client directly and
|
|
43
|
+
return canned responses, without hitting the network or wrestling the real
|
|
44
|
+
SDK's typed models. Also serializes those objects to/from plain hashes, which
|
|
45
|
+
is how the deja gem records and replays them. Part of the llm_mock family.
|
|
46
|
+
email:
|
|
47
|
+
- nate@bidwrangler.com
|
|
48
|
+
executables: []
|
|
49
|
+
extensions: []
|
|
50
|
+
extra_rdoc_files: []
|
|
51
|
+
files:
|
|
52
|
+
- CHANGELOG.md
|
|
53
|
+
- LICENSE
|
|
54
|
+
- README.md
|
|
55
|
+
- lib/llm_mock_anthropic.rb
|
|
56
|
+
- lib/llm_mock_anthropic/version.rb
|
|
57
|
+
homepage: https://github.com/nbrustein/llm_mock_anthropic
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
homepage_uri: https://github.com/nbrustein/llm_mock_anthropic
|
|
62
|
+
source_code_uri: https://github.com/nbrustein/llm_mock_anthropic
|
|
63
|
+
changelog_uri: https://github.com/nbrustein/llm_mock_anthropic/blob/main/CHANGELOG.md
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.2'
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.3
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Build and (de)serialize Anthropic SDK response objects in tests.
|
|
82
|
+
test_files: []
|