openclacky 1.5.12 → 1.5.13
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 +4 -4
- data/CHANGELOG.md +48 -0
- data/lib/clacky/access_key.rb +59 -0
- data/lib/clacky/agent/message_compressor_helper.rb +52 -1
- data/lib/clacky/agent/session_serializer.rb +35 -2
- data/lib/clacky/agent/time_machine.rb +9 -1
- data/lib/clacky/agent.rb +11 -7
- data/lib/clacky/agent_config.rb +40 -0
- data/lib/clacky/agent_profile.rb +5 -5
- data/lib/clacky/billing/platform_billing.rb +84 -1
- data/lib/clacky/brand_config.rb +4 -3
- data/lib/clacky/cli.rb +4 -2
- data/lib/clacky/client.rb +107 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +3 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +40 -21
- data/lib/clacky/default_extensions/git/ext.yml +1 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +483 -77
- data/lib/clacky/default_extensions/meeting/ext.yml +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +1 -1
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +186 -208
- data/lib/clacky/extension/packager.rb +4 -2
- data/lib/clacky/extension/verifier.rb +14 -1
- data/lib/clacky/message_format/open_ai.rb +5 -1
- data/lib/clacky/message_format/open_ai_responses.rb +409 -0
- data/lib/clacky/message_history.rb +6 -2
- data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
- data/lib/clacky/providers.rb +18 -6
- data/lib/clacky/server/dir_picker.rb +154 -0
- data/lib/clacky/server/git_panel.rb +61 -9
- data/lib/clacky/server/http_server.rb +147 -103
- data/lib/clacky/tools/terminal.rb +27 -3
- data/lib/clacky/ui2/markdown_renderer.rb +15 -12
- data/lib/clacky/ui2/strings_cjk_patch.rb +132 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
- data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
- data/lib/clacky/utils/model_pricing.rb +70 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +412 -108
- data/lib/clacky/web/components/composer.js +45 -7
- data/lib/clacky/web/components/custom-select.js +136 -0
- data/lib/clacky/web/components/mentions.js +58 -2
- data/lib/clacky/web/components/model-picker.js +527 -0
- data/lib/clacky/web/components/onboard.js +29 -55
- data/lib/clacky/web/core/ext.js +38 -0
- data/lib/clacky/web/features/billing/view.js +20 -6
- data/lib/clacky/web/features/extensions/view.js +22 -10
- data/lib/clacky/web/features/new-session/store.js +15 -1
- data/lib/clacky/web/features/new-session/view.js +31 -22
- data/lib/clacky/web/features/trash/view.js +31 -19
- data/lib/clacky/web/features/workspace/store.js +1 -0
- data/lib/clacky/web/features/workspace/view.js +54 -3
- data/lib/clacky/web/i18n.js +73 -11
- data/lib/clacky/web/index.html +92 -37
- data/lib/clacky/web/sessions.js +550 -592
- data/lib/clacky/web/settings.js +163 -190
- data/lib/clacky.rb +3 -0
- metadata +8 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
module MessageFormat
|
|
5
|
+
# Static helpers for the OpenAI Responses API (/v1/responses).
|
|
6
|
+
#
|
|
7
|
+
# The Responses API uses a different request/response shape than the
|
|
8
|
+
# classic Chat Completions API:
|
|
9
|
+
#
|
|
10
|
+
# - Request: `input` array of typed items (not `messages`)
|
|
11
|
+
# - Response: `output` array of typed items (not `choices[].message`)
|
|
12
|
+
# - Token limit field: `max_output_tokens` (not `max_tokens`)
|
|
13
|
+
# - Tool calls are standalone `function_call` output items
|
|
14
|
+
# - Tool results are `function_call_output` input items
|
|
15
|
+
# - Usage: `input_tokens` / `output_tokens` (not `prompt_tokens` / `completion_tokens`)
|
|
16
|
+
#
|
|
17
|
+
# This module converts between the canonical internal format (which is
|
|
18
|
+
# Chat Completions-shaped) and the Responses API shape.
|
|
19
|
+
module OpenAIResponses
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# ── Request building ──────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
# Build a Responses API request body from canonical messages.
|
|
25
|
+
#
|
|
26
|
+
# @param messages [Array<Hash>] canonical messages (OpenAI chat format)
|
|
27
|
+
# @param model [String]
|
|
28
|
+
# @param tools [Array<Hash>] OpenAI-style tool definitions
|
|
29
|
+
# @param max_tokens [Integer]
|
|
30
|
+
# @param caching_enabled [Boolean] kept for signature compatibility; the
|
|
31
|
+
# Responses API has no content-level cache_control field, so Clacky's
|
|
32
|
+
# cache markers are intentionally not applied on this path
|
|
33
|
+
# @param vision_supported [Boolean] whether the target model accepts images
|
|
34
|
+
# @param reasoning_effort [String, nil] reasoning effort level
|
|
35
|
+
# @return [Hash] Responses API request body
|
|
36
|
+
def build_request_body(messages, model, tools, max_tokens, _caching_enabled, vision_supported: true, reasoning_effort: nil)
|
|
37
|
+
input_items = messages.flat_map { |msg| convert_message_to_input_items(msg, vision_supported: vision_supported) }
|
|
38
|
+
|
|
39
|
+
body = {
|
|
40
|
+
model: model,
|
|
41
|
+
input: input_items,
|
|
42
|
+
max_output_tokens: max_tokens
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if tools&.any?
|
|
46
|
+
# No cache_control markers here: the Responses API has no
|
|
47
|
+
# content-level cache_control field (that is Anthropic syntax).
|
|
48
|
+
# OpenAI's Responses prompt caching is automatic server-side;
|
|
49
|
+
# the request-level prompt_cache_key is a routing hint that does
|
|
50
|
+
# not map to Clacky's breakpoint convention. caching_enabled is
|
|
51
|
+
# kept in the signature for compatibility but intentionally
|
|
52
|
+
# unused on this path.
|
|
53
|
+
body[:tools] = convert_tools_to_responses_format(tools)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
OpenAI.apply_reasoning_params(body, model, reasoning_effort)
|
|
57
|
+
|
|
58
|
+
body
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ── Canonical message -> Responses API input items ────────────────────────
|
|
62
|
+
|
|
63
|
+
# Convert a single canonical message into one or more Responses API
|
|
64
|
+
# input items.
|
|
65
|
+
#
|
|
66
|
+
# System messages become {type:"message", role:"developer"} items.
|
|
67
|
+
# Assistant messages with tool_calls are split into a message item
|
|
68
|
+
# plus separate function_call items.
|
|
69
|
+
# Tool result messages become function_call_output items.
|
|
70
|
+
#
|
|
71
|
+
# @param msg [Hash] canonical message
|
|
72
|
+
# @param vision_supported [Boolean]
|
|
73
|
+
# @return [Array<Hash>] Responses API input items
|
|
74
|
+
def convert_message_to_input_items(msg, vision_supported:)
|
|
75
|
+
role = msg[:role].to_s
|
|
76
|
+
|
|
77
|
+
# Tool result message -> function_call_output item
|
|
78
|
+
if role == "tool" && msg[:tool_call_id]
|
|
79
|
+
content = msg[:content]
|
|
80
|
+
content = JSON.generate(content) if content.is_a?(Array) || content.is_a?(Hash)
|
|
81
|
+
return [{
|
|
82
|
+
type: "function_call_output",
|
|
83
|
+
call_id: msg[:tool_call_id],
|
|
84
|
+
output: content.to_s
|
|
85
|
+
}]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
items = []
|
|
89
|
+
|
|
90
|
+
# Map system -> developer (Responses API convention; "system" also works
|
|
91
|
+
# but "developer" is the documented role).
|
|
92
|
+
api_role = role == "system" ? "developer" : role
|
|
93
|
+
|
|
94
|
+
# Build the message item (skip when assistant message has no text
|
|
95
|
+
# content but only tool_calls - the Responses API rejects null content)
|
|
96
|
+
content = msg[:content]
|
|
97
|
+
has_content = content && !(content.is_a?(String) && content.empty?) &&
|
|
98
|
+
!(content.is_a?(Array) && content.empty?)
|
|
99
|
+
|
|
100
|
+
if has_content || role != "assistant"
|
|
101
|
+
items << {
|
|
102
|
+
type: "message",
|
|
103
|
+
role: api_role,
|
|
104
|
+
content: normalize_content(content, vision_supported: vision_supported)
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# If the assistant message has tool_calls, emit separate function_call items
|
|
109
|
+
if role == "assistant" && msg[:tool_calls]
|
|
110
|
+
msg[:tool_calls].each do |tc|
|
|
111
|
+
func = tc[:function] || tc # Handle both nested and flat formats
|
|
112
|
+
items << {
|
|
113
|
+
type: "function_call",
|
|
114
|
+
call_id: tc[:id],
|
|
115
|
+
name: func[:name],
|
|
116
|
+
arguments: serialize_arguments(func[:arguments])
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
items
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Normalize canonical content to Responses API content format.
|
|
125
|
+
#
|
|
126
|
+
# String content passes through as-is.
|
|
127
|
+
# Array content: text blocks -> input_text, image_url -> input_image
|
|
128
|
+
# (non-vision models get a text placeholder for images).
|
|
129
|
+
#
|
|
130
|
+
# @param content [String, Array, nil]
|
|
131
|
+
# @param vision_supported [Boolean]
|
|
132
|
+
# @return [String, Array]
|
|
133
|
+
def normalize_content(content, vision_supported:)
|
|
134
|
+
return content.to_s unless content.is_a?(Array)
|
|
135
|
+
|
|
136
|
+
blocks = content.map { |b| normalize_block(b, vision_supported: vision_supported) }.compact
|
|
137
|
+
blocks = [{ type: "input_text", text: "..." }] if blocks.empty?
|
|
138
|
+
blocks
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Normalize a single content block for the Responses API input side.
|
|
142
|
+
#
|
|
143
|
+
# @param block [Hash] canonical content block
|
|
144
|
+
# @param vision_supported [Boolean]
|
|
145
|
+
# @return [Hash, nil]
|
|
146
|
+
def normalize_block(block, vision_supported:)
|
|
147
|
+
return block unless block.is_a?(Hash)
|
|
148
|
+
|
|
149
|
+
case block[:type]
|
|
150
|
+
when "text"
|
|
151
|
+
text = block[:text]
|
|
152
|
+
return nil if text.nil? || text.empty?
|
|
153
|
+
|
|
154
|
+
# Any cache_control marker (Anthropic syntax) is dropped here:
|
|
155
|
+
# the Responses API does not recognize content-level cache_control.
|
|
156
|
+
{ type: "input_text", text: text }
|
|
157
|
+
when "image_url"
|
|
158
|
+
if vision_supported
|
|
159
|
+
# Responses API uses input_image with image_url sub-field
|
|
160
|
+
{ type: "input_image", image_url: block[:image_url] }
|
|
161
|
+
else
|
|
162
|
+
{ type: "input_text", text: "[Image content removed - current model does not support vision input]" }
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
block
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# ── Response parsing ──────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
# Parse a Responses API response into canonical internal format.
|
|
172
|
+
#
|
|
173
|
+
# @param data [Hash] parsed JSON response body
|
|
174
|
+
# @return [Hash] canonical response: { content, tool_calls, finish_reason, usage, raw_api_usage }
|
|
175
|
+
def parse_response(data)
|
|
176
|
+
output = data["output"] || []
|
|
177
|
+
|
|
178
|
+
# Extract text content from message-type output items
|
|
179
|
+
text_content = extract_output_text(output)
|
|
180
|
+
|
|
181
|
+
# Extract function calls from function_call-type output items
|
|
182
|
+
tool_calls = extract_function_calls(output)
|
|
183
|
+
|
|
184
|
+
# Parse usage (field names differ from Chat Completions)
|
|
185
|
+
usage = data["usage"] || {}
|
|
186
|
+
raw_api_usage = usage.dup
|
|
187
|
+
|
|
188
|
+
usage_data = {
|
|
189
|
+
prompt_tokens: usage["input_tokens"],
|
|
190
|
+
completion_tokens: usage["output_tokens"],
|
|
191
|
+
total_tokens: usage["total_tokens"] || (usage["input_tokens"].to_i + usage["output_tokens"].to_i)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Responses API stores cache info under input_tokens_details
|
|
195
|
+
if (details = usage["input_tokens_details"])
|
|
196
|
+
usage_data[:cache_read_input_tokens] = details["cached_tokens"] if details["cached_tokens"].to_i > 0
|
|
197
|
+
usage_data[:cache_creation_input_tokens] = details["cache_write_tokens"] if details["cache_write_tokens"].to_i > 0
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# OpenRouter may also send output_tokens_details
|
|
201
|
+
if (out_details = usage["output_tokens_details"])
|
|
202
|
+
usage_data[:reasoning_tokens] = out_details["reasoning_tokens"] if out_details["reasoning_tokens"].to_i > 0
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
usage_data[:api_cost] = usage["cost"] if usage["cost"]
|
|
206
|
+
|
|
207
|
+
# Determine finish_reason from status + output content
|
|
208
|
+
finish_reason = determine_finish_reason(data, tool_calls)
|
|
209
|
+
|
|
210
|
+
result = {
|
|
211
|
+
content: text_content,
|
|
212
|
+
tool_calls: tool_calls.empty? ? nil : tool_calls,
|
|
213
|
+
finish_reason: finish_reason,
|
|
214
|
+
usage: usage_data,
|
|
215
|
+
raw_api_usage: raw_api_usage
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Preserve reasoning content if present in the output
|
|
219
|
+
reasoning = extract_reasoning(output)
|
|
220
|
+
result[:reasoning_content] = reasoning if reasoning
|
|
221
|
+
|
|
222
|
+
result
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# ── Tool result formatting ────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
# Format tool results into canonical messages to append to @messages.
|
|
228
|
+
# Returns canonical format (role: "tool") - conversion to Responses API
|
|
229
|
+
# function_call_output items happens inside convert_message_to_input_items
|
|
230
|
+
# on the next request.
|
|
231
|
+
#
|
|
232
|
+
# @param response [Hash] canonical response from parse_response
|
|
233
|
+
# @param tool_results [Array<Hash>] tool execution results
|
|
234
|
+
# @return [Array<Hash>] canonical tool messages
|
|
235
|
+
def format_tool_results(response, tool_results)
|
|
236
|
+
results_map = tool_results.each_with_object({}) { |r, h| h[r[:id]] = r }
|
|
237
|
+
|
|
238
|
+
response[:tool_calls].map do |tc|
|
|
239
|
+
result = results_map[tc[:id]]
|
|
240
|
+
raw_content = result ? result[:content] : { error: "Tool result missing" }.to_json
|
|
241
|
+
|
|
242
|
+
content = raw_content.is_a?(Array) ? JSON.generate(raw_content) : raw_content
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
role: "tool",
|
|
246
|
+
tool_call_id: tc[:id],
|
|
247
|
+
content: content
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# ── Private helpers ───────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
# The Responses API expects function_call arguments as a JSON *string*.
|
|
255
|
+
# Canonical tool_calls normally carry a JSON string already, but a Hash
|
|
256
|
+
# or Array value must be JSON-encoded — #to_s would emit a Ruby literal
|
|
257
|
+
# (e.g. {:city=>"Tokyo"}) that the tool side cannot parse.
|
|
258
|
+
private_class_method def self.serialize_arguments(arguments)
|
|
259
|
+
return JSON.generate(arguments) if arguments.is_a?(Hash) || arguments.is_a?(Array)
|
|
260
|
+
|
|
261
|
+
arguments.to_s
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Extract text content from output items.
|
|
265
|
+
# Looks for message-type items with output_text content blocks.
|
|
266
|
+
#
|
|
267
|
+
# @param output [Array<Hash>] response output items
|
|
268
|
+
# @return [String, nil]
|
|
269
|
+
private_class_method def self.extract_output_text(output)
|
|
270
|
+
texts = []
|
|
271
|
+
|
|
272
|
+
output.each do |item|
|
|
273
|
+
next unless item["type"] == "message"
|
|
274
|
+
|
|
275
|
+
content = item["content"]
|
|
276
|
+
next unless content.is_a?(Array)
|
|
277
|
+
|
|
278
|
+
content.each do |block|
|
|
279
|
+
next unless block["type"] == "output_text"
|
|
280
|
+
texts << block["text"].to_s
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
texts.empty? ? nil : texts.join
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Extract reasoning content from output items.
|
|
288
|
+
# Supports both shapes seen in the wild:
|
|
289
|
+
# 1. OpenAI official — a "reasoning" block inside a message item,
|
|
290
|
+
# with summary[] and/or content[] of reasoning_text blocks.
|
|
291
|
+
# 2. DeepSeek — a top-level "reasoning" item whose content array
|
|
292
|
+
# carries {"type":"reasoning_text","text":...} blocks.
|
|
293
|
+
# Prefers full reasoning text (reasoning_text blocks); falls back to
|
|
294
|
+
# summary text when only a summary is present.
|
|
295
|
+
#
|
|
296
|
+
# @param output [Array<Hash>] response output items
|
|
297
|
+
# @return [String, nil]
|
|
298
|
+
private_class_method def self.extract_reasoning(output)
|
|
299
|
+
texts = []
|
|
300
|
+
|
|
301
|
+
output.each do |item|
|
|
302
|
+
content = item["content"]
|
|
303
|
+
|
|
304
|
+
case item["type"]
|
|
305
|
+
when "reasoning"
|
|
306
|
+
next unless content.is_a?(Array)
|
|
307
|
+
before = texts.length
|
|
308
|
+
collect_reasoning_texts(content, texts)
|
|
309
|
+
collect_summary_texts(content, texts) if texts.length == before
|
|
310
|
+
when "message"
|
|
311
|
+
next unless content.is_a?(Array)
|
|
312
|
+
content.each do |block|
|
|
313
|
+
case block["type"]
|
|
314
|
+
when "reasoning"
|
|
315
|
+
before = texts.length
|
|
316
|
+
collect_reasoning_texts(block["content"], texts)
|
|
317
|
+
collect_summary_texts(block["summary"], texts) if texts.length == before
|
|
318
|
+
when "output_text"
|
|
319
|
+
# Some providers surface reasoning as output_text blocks
|
|
320
|
+
# carrying a summary attribute.
|
|
321
|
+
collect_summary_texts(block["summary"], texts)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
texts.empty? ? nil : texts.join
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
private_class_method def self.collect_reasoning_texts(blocks, texts)
|
|
331
|
+
return unless blocks.is_a?(Array)
|
|
332
|
+
blocks.each do |b|
|
|
333
|
+
texts << b["text"].to_s if b.is_a?(Hash) && b["type"] == "reasoning_text" && b["text"]
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
private_class_method def self.collect_summary_texts(summary, texts)
|
|
338
|
+
return unless summary.is_a?(Array)
|
|
339
|
+
summary.each do |b|
|
|
340
|
+
texts << b["text"].to_s if b.is_a?(Hash) && b["type"] == "summary_text" && b["text"]
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Extract function calls from output items.
|
|
345
|
+
# Converts Responses API function_call items to canonical tool_calls format.
|
|
346
|
+
#
|
|
347
|
+
# @param output [Array<Hash>] response output items
|
|
348
|
+
# @return [Array<Hash>] canonical tool_calls
|
|
349
|
+
private_class_method def self.extract_function_calls(output)
|
|
350
|
+
# each_with_object instead of filter_map to stay Ruby 2.6 compatible.
|
|
351
|
+
output.each_with_object([]) do |item, calls|
|
|
352
|
+
next unless item["type"] == "function_call"
|
|
353
|
+
|
|
354
|
+
calls << {
|
|
355
|
+
id: item["call_id"],
|
|
356
|
+
type: "function",
|
|
357
|
+
name: item["name"],
|
|
358
|
+
arguments: item["arguments"].to_s
|
|
359
|
+
}
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Determine the canonical finish_reason from Responses API status + output.
|
|
364
|
+
#
|
|
365
|
+
# @param data [Hash] full response body
|
|
366
|
+
# @param tool_calls [Array<Hash>] extracted tool calls
|
|
367
|
+
# @return [String]
|
|
368
|
+
private_class_method def self.determine_finish_reason(data, tool_calls)
|
|
369
|
+
# If there are tool calls, the model wants to call tools
|
|
370
|
+
return "tool_calls" unless tool_calls.empty?
|
|
371
|
+
|
|
372
|
+
status = data["status"]
|
|
373
|
+
case status
|
|
374
|
+
when "completed" then "stop"
|
|
375
|
+
when "incomplete" then incomplete_finish_reason(data)
|
|
376
|
+
else "stop"
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Map an "incomplete" response to a canonical finish_reason via
|
|
381
|
+
# incomplete_details.reason: "content_filter" means the response was
|
|
382
|
+
# blocked by safety filtering (typically empty) and must not be
|
|
383
|
+
# mislabelled as a mere token limit ("length").
|
|
384
|
+
#
|
|
385
|
+
# @param data [Hash] full response body
|
|
386
|
+
# @return [String]
|
|
387
|
+
private_class_method def self.incomplete_finish_reason(data)
|
|
388
|
+
reason = data.dig("incomplete_details", "reason")
|
|
389
|
+
reason == "content_filter" ? "content_filter" : "length"
|
|
390
|
+
end
|
|
391
|
+
# Convert Chat Completions tool definitions to Responses API format.
|
|
392
|
+
# Chat Completions: {type: "function", function: {name:, description:, parameters:}}
|
|
393
|
+
# Responses API: {type: "function", name:, description:, parameters:}
|
|
394
|
+
# Tools already in flat format (no "function" key) pass through unchanged.
|
|
395
|
+
private_class_method def self.convert_tools_to_responses_format(tools)
|
|
396
|
+
tools.map do |tool|
|
|
397
|
+
func = tool[:function] || tool["function"]
|
|
398
|
+
next tool unless func.is_a?(Hash)
|
|
399
|
+
{
|
|
400
|
+
type: tool[:type] || tool["type"] || "function",
|
|
401
|
+
name: func[:name] || func["name"],
|
|
402
|
+
description: func[:description] || func["description"],
|
|
403
|
+
parameters: func[:parameters] || func["parameters"]
|
|
404
|
+
}
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
@@ -15,9 +15,11 @@ module Clacky
|
|
|
15
15
|
subagent_instructions subagent_result subagent_transcript token_usage
|
|
16
16
|
compressed_summary chunk_path truncated transient
|
|
17
17
|
chunk_index chunk_count ext_events skill_command skill_command_display
|
|
18
|
-
display_references
|
|
18
|
+
display_files display_references
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
|
+
INTERNAL_CONTENT_BLOCK_FIELDS = %i[image_path image_name].freeze
|
|
22
|
+
|
|
21
23
|
# Cap on persisted ext_events per message. These are milestone events
|
|
22
24
|
# (progress chatter is transient), so a handful per message is the norm —
|
|
23
25
|
# the cap only exists to stop a runaway extension. Oldest are dropped first.
|
|
@@ -358,7 +360,9 @@ module Clacky
|
|
|
358
360
|
next nil
|
|
359
361
|
end
|
|
360
362
|
|
|
361
|
-
block
|
|
363
|
+
next block if (block.keys & INTERNAL_CONTENT_BLOCK_FIELDS).empty?
|
|
364
|
+
|
|
365
|
+
block.reject { |key, _| INTERNAL_CONTENT_BLOCK_FIELDS.include?(key) }
|
|
362
366
|
end
|
|
363
367
|
|
|
364
368
|
return msg if cleaned == content
|