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 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmMock
4
+ module Anthropic
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -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: []