turnkit 0.2.10 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 268561a36c656098e1d23ea6de4c17616358ff931e05e1389e707a9e28fe458b
4
- data.tar.gz: 8f6731d78fed5b3e3cc94d781c4f4e26accc4f8d05842b5c56eb58a6e7448907
3
+ metadata.gz: 2b34c1c2760df56b69b055246a8afcccaec42c5493cce13ca30303c9c7e1e809
4
+ data.tar.gz: d3ff91766661eddbd6bb8eefba627ceaf13d8d2b2daa673781819cfe71b41a79
5
5
  SHA512:
6
- metadata.gz: ae0a246b5937e586c808a25d28f051bafc54c2a922a52d89160eb3f5ef3bf7360b1d637cbb0c170d41eb74cd536638b6f9a1880275bd0ccd2fc8dcb4ac44db5c
7
- data.tar.gz: 7ffebcfeadf51f193c7f2277a0842c2f56e00d9ff95d502915924f2a6d7e10744a0a710d1d2f5b1865182a9de21b2cce30edc3e94c16f49626912b93b1fc7063
6
+ metadata.gz: 02a3e913bcfa72bc5fcbd8e18faaf60def5f62b75981c7e72220e994b7874df2a67fd306dd01cde4f9c3a0d5082aa90271bf210c5c4c5876a5015812077df79f
7
+ data.tar.gz: 0d6d917eeeef41a3a624a31daccdc60f28e4c121b8959ad0ab3d57301be63427e83f2a349a920fa9707bd9f5e2fa2310721f72a8151fbbb44c26a3ec75466259
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-06-19
4
+
5
+ - Add first-class image generation with `Turn#paint`, `TurnKit.paint`, and `TurnKit::ImageTool`.
6
+ - Persist generated images as durable image messages with normalized metadata, usage, cost, and event callbacks.
7
+ - Add image output policy support and a `generate-image` CLI smoke example for Gemini 16:9 image generation.
8
+
9
+ ## 0.3.0 - 2026-06-10
10
+
11
+ - Make the task-runtime API skills-first and intentionally breaking: `max_spend` is the only spend-limit name and output validation is exposed as `output_policy` / `policy_audit`.
12
+ - Store message content as ordered typed parts, with text derived from content and tool calls/results persisted in the transcript instead of metadata.
13
+ - Add `load_skill` for progressively disclosed available skills.
14
+ - Add output-policy revision loops with `output_retries`, including skill/policy rehydration in revision prompts.
15
+ - Add deterministic `input_schema` validation before turns are created.
16
+ - Ensure terminal tools never orphan sibling tool calls; skipped siblings receive cancelled executions and tool-result messages.
17
+ - Add turn claiming, tool-runner heartbeats, persisted budget resume, and sub-agent failure details.
18
+
3
19
  ## 0.2.10 - 2026-06-10
4
20
 
5
21
  - Add output audits and file-backed output policies for validating final run output.
data/README.md CHANGED
@@ -331,8 +331,8 @@ end
331
331
 
332
332
  workflow = TurnKit::Workflow.new(
333
333
  name: "memo_writer",
334
- output_audit: [no_em_dash, numbered_lists_only],
335
- output_audit_mode: :fail
334
+ output_policy: [no_em_dash, numbered_lists_only],
335
+ output_policy_mode: :fail
336
336
  )
337
337
  ```
338
338
 
@@ -340,7 +340,7 @@ Run checks directly when you want to test a renderer or policy without calling a
340
340
  model:
341
341
 
342
342
  ```ruby
343
- audit = TurnKit.audit_output(
343
+ audit = TurnKit.check_output_policy(
344
344
  "1. Recommendation\n- unordered item — fix this\n",
345
345
  constraints: [no_em_dash, numbered_lists_only]
346
346
  )
@@ -350,8 +350,8 @@ puts audit.messages
350
350
  ```
351
351
 
352
352
  Use `output_policy` when a semantic judge is worth the extra model call. The
353
- policy can be a `.md`, `.markdown`, or `.txt` file path, a `TurnKit::OutputPolicy`,
354
- or any object that responds to `#call` or `#check`.
353
+ policy can be a `.md`, `.markdown`, or `.txt` file path, a `TurnKit::Skill`, a
354
+ `TurnKit::OutputPolicy`, or any object that responds to `#call` or `#check`.
355
355
 
356
356
  ```ruby
357
357
  workflow = TurnKit::Workflow.new(
@@ -364,8 +364,34 @@ workflow = TurnKit::Workflow.new(
364
364
  ```
365
365
 
366
366
  `output_policy_mode: :report` records violations while allowing the run to
367
- complete. `:fail` marks the run failed after recording the output and audit.
368
- Policy model usage and cost are counted on the parent run.
367
+ complete. `:fail` marks the run failed after recording the output and audit;
368
+ `:fail` is the default for contract-driven workflows. Policy model usage and
369
+ cost are counted on the parent run.
370
+
371
+ Add `output_retries:` to turn policy failures into bounded revision loops instead
372
+ of dead ends:
373
+
374
+ ```ruby
375
+ voice = TurnKit::Skill.from_file("app/ai/skills/memo_voice.md")
376
+
377
+ workflow = TurnKit::Workflow.new(
378
+ name: "memo_writer",
379
+ skills: [voice],
380
+ output_policy: [voice, no_em_dash],
381
+ output_retries: 2,
382
+ input_schema: {
383
+ "type" => "object",
384
+ "required" => ["project_id"],
385
+ "properties" => { "project_id" => { "type" => "string" } }
386
+ }
387
+ )
388
+ ```
389
+
390
+ `skills:` are always loaded into the prompt. `available_skills:` are listed in
391
+ `<skills_available>` and exposed through the `load_skill` tool, so the model can
392
+ load full instructions on demand. Every advertised tool call receives exactly one
393
+ tool result, including validation errors, budget denials, and calls skipped after
394
+ a terminal tool ends the turn.
369
395
 
370
396
  ### Prompt Preview
371
397
 
@@ -433,6 +459,61 @@ puts turn.output_text
433
459
 
434
460
  Rely on TurnKit to validate tools and model-provided arguments.
435
461
 
462
+ ### Images
463
+
464
+ Generate images inside a durable turn with `turn.paint`. The image call uses the
465
+ configured client adapter, records usage and cost on the turn, persists an image
466
+ message, and emits `image.requested` / `image.completed` events.
467
+
468
+ ```ruby
469
+ image = turn.paint(
470
+ "Create a 16:9 editorial header image for the article.",
471
+ model: "gemini-3-pro-image-preview",
472
+ provider: :gemini,
473
+ size: "1024x576",
474
+ metadata: { article_id: article.id }
475
+ )
476
+
477
+ image.url # provider-hosted URL when returned
478
+ image.to_blob # generated bytes for base64 responses, or fetched URL bytes
479
+ image.mime_type # "image/png"
480
+ ```
481
+
482
+ For reusable workflow steps, subclass `TurnKit::ImageTool`:
483
+
484
+ ```ruby
485
+ class GenerateHeaderImage < TurnKit::ImageTool
486
+ description "Generate an article header image."
487
+ parameter :title, :string, required: true
488
+
489
+ model "gemini-3-pro-image-preview"
490
+ provider :gemini
491
+ size "1024x576"
492
+
493
+ def prompt(title:)
494
+ "Create a 16:9 editorial header image for #{title}."
495
+ end
496
+ end
497
+ ```
498
+
499
+ Rails apps can attach generated images from the event stream without TurnKit
500
+ taking a dependency on Active Storage:
501
+
502
+ ```ruby
503
+ TurnKit.on_event = ->(event) do
504
+ next unless event.type == "image.completed"
505
+
506
+ image = TurnKit::ImageResult.from_h(event.payload.fetch(:image))
507
+ Article.find(event.payload.dig(:metadata, :article_id)).header_image.attach(
508
+ io: StringIO.new(image.to_blob),
509
+ filename: "header.png",
510
+ content_type: image.mime_type
511
+ )
512
+ end
513
+ ```
514
+
515
+ Require an image before completion with `TurnKit::OutputPolicy.require_image`.
516
+
436
517
  ### Structured Output
437
518
 
438
519
  Define a schema:
@@ -639,8 +720,7 @@ TurnKit.output_policy_model = "gpt-4.1-mini"
639
720
  TurnKit.timeout = 300
640
721
  ```
641
722
 
642
- `TurnKit.cost_limit` remains supported as the internal/legacy name for
643
- `max_spend`.
723
+ `max_spend` is the only spend-limit name in the public API.
644
724
 
645
725
  Set options per agent:
646
726
 
data/UPGRADE.md CHANGED
@@ -1,313 +1,51 @@
1
1
  # Upgrade Guide
2
2
 
3
- This guide covers migrating to the workflow-based task-runtime API. The
4
- recommended migration is about making the three work shapes easier to read:
3
+ ## 0.3.0 is a clean break
5
4
 
6
- - conversations for durable multi-turn threads;
7
- - runs for one non-interactive application task;
8
- - workflows for reusable task runners with tools, skills, limits, and policy.
5
+ TurnKit 0.3.0 intentionally removes the short-lived legacy names from the 0.2
6
+ series. The gem is pre-1.0 and the durable transcript schema changed, so migrate
7
+ by updating call sites and reinstalling the generated tables for new projects.
9
8
 
10
- ## Quick summary
9
+ ### Renames
11
10
 
12
- Before changing call sites, bump TurnKit to the latest version and run your
13
- test suite against the new release.
11
+ - `TurnKit.cost_limit` `TurnKit.max_spend`
12
+ - `Agent.new(cost_limit:)` `Agent.new(max_spend:)`
13
+ - `Workflow.new(cost_limit:)` / `workflow.run(cost_limit:)` → `max_spend:`
14
+ - `output_audit:` → `output_policy:`
15
+ - `output_audit_mode:` → `output_policy_mode:`
16
+ - `run.output_audit` → `run.policy_audit`
17
+ - `run.output_audit_clean?` → `run.policy_clean?`
18
+ - `TurnKit.audit_output(...)` → `TurnKit.check_output_policy(...)`
14
19
 
15
- ```ruby
16
- # Gemfile
17
- gem "turnkit", "~> 0.2.9"
18
- ```
20
+ The audit result class remains `TurnKit::OutputAudit`; only the public option and
21
+ run-accessor names changed.
19
22
 
20
- ```sh
21
- bundle update turnkit
22
- ```
23
+ ### Message schema
23
24
 
24
- Use workflows for reusable autonomous task runners.
25
+ `turnkit_messages.text` was removed. Message `content` is now the canonical
26
+ ordered array of parts:
25
27
 
26
- Recommended new forms:
28
+ - `text`
29
+ - `thinking`
30
+ - `tool_call`
31
+ - `tool_result`
32
+ - opaque provider parts
27
33
 
28
- ```ruby
29
- TurnKit.configure do |config|
30
- config.model = "gpt-5.2"
31
- config.max_spend = 0.25
32
- end
34
+ `Message#text` is derived from text parts. New Rails installs should regenerate
35
+ the install migration; there is no compatibility shim for older schemas.
33
36
 
34
- workflow = TurnKit::Workflow.new(name: "brief_writer", tools: [WebSearch, SaveBrief])
35
- run = workflow.run("Create a source-grounded brief.", input: { topic: "Rails 8" })
37
+ ### Workflows
36
38
 
37
- puts run.output
38
- ```
39
+ `TurnKit::Workflow` now forwards options directly to `Agent`. Use
40
+ `workflow.options[:name]` or `workflow.agent` for inspection instead of per-option
41
+ workflow attr readers. Workflow `instructions:` compose with the orchestrator
42
+ preamble by default; pass `preamble: false` to opt out.
39
43
 
40
- ## Configuration
44
+ ### Skills and policy loops
41
45
 
42
- ### Model name
43
-
44
- Before:
45
-
46
- ```ruby
47
- TurnKit.default_model = "gpt-5.2"
48
- ```
49
-
50
- After:
51
-
52
- ```ruby
53
- TurnKit.model = "gpt-5.2"
54
- ```
55
-
56
- `TurnKit.default_model` remains supported. `TurnKit.model` is the shorter public
57
- alias for app code and initializers.
58
-
59
- ### Global setup
60
-
61
- Before:
62
-
63
- ```ruby
64
- TurnKit.default_model = "gpt-5.2"
65
- TurnKit.cost_limit = 0.25
66
- TurnKit.max_iterations = 12
67
- ```
68
-
69
- After:
70
-
71
- ```ruby
72
- TurnKit.configure do |config|
73
- config.model = "gpt-5.2"
74
- config.max_spend = 0.25
75
- config.max_iterations = 12
76
- end
77
- ```
78
-
79
- `TurnKit.configure` simply yields the `TurnKit` module. There is no separate
80
- configuration object or DSL.
81
-
82
- ### Spend limit naming
83
-
84
- Before:
85
-
86
- ```ruby
87
- TurnKit.cost_limit = 0.25
88
- ```
89
-
90
- After:
91
-
92
- ```ruby
93
- TurnKit.max_spend = 0.25
94
- ```
95
-
96
- `cost_limit` remains supported. Prefer `max_spend` in application-facing code
97
- because it matches how developers think about autonomous runs.
98
-
99
- ## Running application tasks
100
-
101
- ### Agent tasks
102
-
103
- Before:
104
-
105
- ```ruby
106
- run = agent.run(task: "Classify this lead.", input: lead.attributes)
107
- puts run.output_text
108
- ```
109
-
110
- After:
111
-
112
- ```ruby
113
- run = agent.run("Classify this lead.", input: lead.attributes)
114
- puts run.output
115
- ```
116
-
117
- The keyword form still works. The positional string is the recommended form for
118
- the common case. `Agent#run` uses task prompt behavior by default; pass
119
- `prompt_mode: :full` if you need conversation-style prompt behavior for a run.
120
-
121
- ### Pending runs
122
-
123
- No behavior change.
124
-
125
- ```ruby
126
- run = agent.run("Classify later.", async: true)
127
- request = run.preview
128
- run.run!
129
- ```
130
-
131
- The existing keyword form remains valid:
132
-
133
- ```ruby
134
- run = agent.run(task: "Classify later.", async: true)
135
- ```
136
-
137
- ## Workflows
138
-
139
- The preferred name for reusable autonomous task runtimes is now workflow. A
140
- workflow packages:
141
-
142
- - one task-mode orchestrator
143
- - workflow skills
144
- - tools
145
- - guardrails
146
- - compaction
147
- - optional persistence/action tools
148
-
149
- ### Construction
150
-
151
- ```ruby
152
- workflow = TurnKit::Workflow.new(
153
- name: "sales_enrichment",
154
- tools: [AccountLookup, WebSearch, SaveEnrichment],
155
- skills: [sales_research_skill],
156
- max_spend: 0.25
157
- )
158
- ```
159
-
160
- ### Running
161
-
162
- ```ruby
163
- run = workflow.run(
164
- "Enrich this account for responsible outreach.",
165
- input: account.attributes
166
- )
167
- ```
168
-
169
- `task:` remains supported.
170
-
171
- ## Run inspection
172
-
173
- New convenience methods were added to `TurnKit::Run`.
174
-
175
- Before:
176
-
177
- ```ruby
178
- run.output_text
179
- run.tool_executions
180
- run.turn_records.length
181
- TurnKit.store.load_turn(run.id)["error"]
182
- ```
183
-
184
- After:
185
-
186
- ```ruby
187
- run.output
188
- run.tool_calls
189
- run.steps
190
- run.error
191
- ```
192
-
193
- Old methods remain available. Prefer the shorter methods in application code,
194
- examples, and docs.
195
-
196
- ## Save/action tools
197
-
198
- Use `terminal!` for tools that complete the run by saving an artifact or taking
199
- the final action.
200
-
201
- Before:
202
-
203
- ```ruby
204
- class SaveBrief < TurnKit::Tool
205
- def self.ends_turn? = true
206
- def self.completion_message(result) = "Saved #{result.fetch("id")}."
207
-
208
- def call(title:, body:, context:)
209
- { "id" => Brief.create!(title: title, body: body).id }
210
- end
211
- end
212
- ```
213
-
214
- After:
215
-
216
- ```ruby
217
- class SaveBrief < TurnKit::Tool
218
- terminal! { |result| "Saved #{result.fetch("id")}." }
219
-
220
- def call(title:, body:, context:)
221
- { "id" => Brief.create!(title: title, body: body).id }
222
- end
223
- end
224
- ```
225
-
226
- The old `ends_turn?` and `completion_message` methods remain supported. Prefer
227
- `terminal!` for readability.
228
-
229
- ## Tool instances
230
-
231
- If a tool needs constructor arguments, register an instance instead of a class.
232
-
233
- Before, this may have failed at runtime:
234
-
235
- ```ruby
236
- class WebSearch < TurnKit::Tool
237
- def initialize(client:)
238
- @client = client
239
- end
240
- end
241
-
242
- agent = TurnKit::Agent.new(tools: [WebSearch])
243
- ```
244
-
245
- After:
246
-
247
- ```ruby
248
- client = SearchClient.new(api_key: ENV.fetch("SEARCH_API_KEY"))
249
- agent = TurnKit::Agent.new(tools: [WebSearch.new(client: client)])
250
- ```
251
-
252
- This is the recommended pattern for API clients, test doubles, and per-tenant
253
- dependencies.
254
-
255
- ## Multi-agent workflows
256
-
257
- If you previously modeled every role as a separate agent, consider migrating the
258
- default path to one workflow with a workflow skill.
259
-
260
- Before:
261
-
262
- ```ruby
263
- researcher = TurnKit::Agent.new(name: "researcher", tools: [WebSearch])
264
- writer = TurnKit::Agent.new(name: "writer")
265
- verifier = TurnKit::Agent.new(name: "verifier")
266
-
267
- orchestrator = TurnKit::Agent.new(
268
- name: "orchestrator",
269
- sub_agents: [researcher, writer, verifier]
270
- )
271
- ```
272
-
273
- After:
274
-
275
- ```ruby
276
- workflow = TurnKit::Skill.new(
277
- key: "source_grounded_brief",
278
- name: "Source Grounded Brief",
279
- content: <<~TEXT
280
- Research first. Build an evidence pack. Draft only from evidence. Verify
281
- important claims. Revise unsupported claims before final output.
282
- TEXT
283
- )
284
-
285
- source_brief = TurnKit::Workflow.new(
286
- name: "source_brief",
287
- skills: [workflow],
288
- tools: [WebSearch, ReadWebPage, SaveBrief],
289
- max_spend: 0.25,
290
- max_tool_executions: 20
291
- )
292
- ```
293
-
294
- Keep separate agents when the isolation is worth the extra model calls:
295
-
296
- - different models
297
- - different tool permissions
298
- - adversarial review
299
- - parallel specialist research
300
- - separate durable child conversations
301
-
302
- ## Suggested migration order
303
-
304
- 1. Replace `TurnKit.default_model =` with `TurnKit.model =` in app-level config.
305
- 2. Wrap global settings in `TurnKit.configure` if you have more than one.
306
- 3. Use `TurnKit::Workflow.new(name: "...")` for reusable autonomous task runners.
307
- 4. Replace `run(task: "...")` with `run("...")` where it improves readability.
308
- 5. Replace `run.output_text` with `run.output` in application code.
309
- 6. Replace save/action tool overrides with `terminal!` when convenient.
310
- 7. Consider collapsing role-agent workflows into one workflow plus workflow skills if
311
- cost or complexity is a concern.
312
-
313
- Run your test suite after migrating call sites.
46
+ - `available_skills:` now exposes a real `load_skill` tool.
47
+ - `output_policy:` accepts `TurnKit::Skill` instances.
48
+ - `output_retries:` controls bounded revision loops. The default policy mode is
49
+ now `:fail`; use `output_policy_mode: :report` if dirty output should complete.
50
+ - `input_schema:` validates application input before any conversation or turn is
51
+ created.
@@ -41,6 +41,23 @@ module TurnKit
41
41
  normalize_response(response, model: model)
42
42
  end
43
43
 
44
+ def paint(prompt:, model:, provider: nil, size: nil, assume_model_exists: nil, input_images: nil, mask: nil, params: {}, metadata: nil, on_event: nil)
45
+ require "ruby_llm"
46
+
47
+ configure_from_environment
48
+ kwargs = paint_kwargs(
49
+ model: model,
50
+ provider: provider,
51
+ assume_model_exists: assume_model_exists || false,
52
+ size: size || "1024x1024",
53
+ with: input_images,
54
+ mask: mask,
55
+ params: params || {}
56
+ )
57
+ image = ::RubyLLM.paint(prompt, **kwargs)
58
+ normalize_image_response(image, model: model, provider: provider, params: { "size" => size || "1024x1024" }.merge(params || {}), metadata: metadata)
59
+ end
60
+
44
61
  private
45
62
  def configure_from_environment
46
63
  config = ::RubyLLM.config
@@ -182,6 +199,7 @@ module TurnKit
182
199
  )
183
200
  Result.new(
184
201
  text: response_text(response),
202
+ parts: response_parts(response, tool_calls: tool_calls),
185
203
  output_data: response_data(response),
186
204
  tool_calls: tool_calls,
187
205
  usage: usage,
@@ -189,6 +207,34 @@ module TurnKit
189
207
  )
190
208
  end
191
209
 
210
+ def response_parts(response, tool_calls:)
211
+ content = response.respond_to?(:content) ? response.content : response
212
+ parts = case content
213
+ when Array
214
+ content.map { |part| normalize_provider_part(part) }
215
+ when Hash
216
+ [ { "type" => "text", "text" => content.to_json } ]
217
+ else
218
+ text = content.to_s
219
+ text.empty? ? [] : [ { "type" => "text", "text" => text } ]
220
+ end.compact
221
+ parts + Array(tool_calls).map { |call| { "type" => "tool_call", "id" => call.id, "name" => call.name, "arguments" => call.arguments } }
222
+ end
223
+
224
+ def normalize_provider_part(part)
225
+ attrs = part.respond_to?(:to_h) ? part.to_h.transform_keys(&:to_s) : nil
226
+ return { "type" => "text", "text" => part.to_s } unless attrs
227
+
228
+ case attrs["type"].to_s
229
+ when "text", "output_text"
230
+ { "type" => "text", "text" => attrs["text"] || attrs["content"].to_s }
231
+ when "thinking", "reasoning"
232
+ { "type" => "thinking", "text" => attrs["text"] || attrs["content"].to_s, "signature" => attrs["signature"], "redacted" => attrs["redacted"] || false }.compact
233
+ else
234
+ { "type" => "provider", "kind" => attrs["type"].to_s, "data" => attrs }
235
+ end
236
+ end
237
+
192
238
  def response_text(response)
193
239
  content = response.respond_to?(:content) ? response.content : response
194
240
  content.is_a?(Hash) || content.is_a?(Array) ? content.to_json : content.to_s
@@ -217,6 +263,47 @@ module TurnKit
217
263
 
218
264
  response.cost&.total
219
265
  end
266
+
267
+ def paint_kwargs(kwargs)
268
+ parameters = ::RubyLLM::Image.method(:paint).parameters
269
+ return kwargs if parameters.any? { |kind, _| kind == :keyrest }
270
+
271
+ accepted = parameters.filter_map { |kind, name| name if %i[key keyreq].include?(kind) }
272
+ unsupported = kwargs.keys.select { |key| !accepted.include?(key) && !blank?(kwargs[key]) }
273
+ raise ArgumentError, "RubyLLM image generation does not support: #{unsupported.join(", ")}" if unsupported.any?
274
+
275
+ kwargs.slice(*accepted)
276
+ end
277
+
278
+ def blank?(value)
279
+ value.nil? || value == false || (value.respond_to?(:empty?) && value.empty?)
280
+ end
281
+
282
+ def normalize_image_response(image, model:, provider:, params:, metadata:)
283
+ usage = Usage.new(
284
+ input_tokens: image_usage_value(image, "input_tokens"),
285
+ output_tokens: image_usage_value(image, "output_tokens"),
286
+ cost: response_cost(image)
287
+ )
288
+ part = ImageResult.new(
289
+ url: image.respond_to?(:url) ? image.url : nil,
290
+ data: image.respond_to?(:data) ? image.data : nil,
291
+ mime_type: image.respond_to?(:mime_type) ? image.mime_type : nil,
292
+ revised_prompt: image.respond_to?(:revised_prompt) ? image.revised_prompt : nil,
293
+ model: image.respond_to?(:model_id) ? image.model_id : model,
294
+ provider: provider&.to_s,
295
+ usage: usage,
296
+ params: params,
297
+ metadata: metadata || {}
298
+ ).to_h.merge("type" => "image")
299
+
300
+ Result.new(parts: [ part ], usage: usage, model: part["model"], output_data: { "type" => "image", "images" => [ part ] })
301
+ end
302
+
303
+ def image_usage_value(image, key)
304
+ usage = image.respond_to?(:usage) ? image.usage || {} : {}
305
+ (usage[key] || usage[key.to_sym]).to_i
306
+ end
220
307
  end
221
308
  end
222
309
  end