kward 0.67.1 → 0.69.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 +4 -4
- data/.github/workflows/pages.yml +48 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +54 -0
- data/Gemfile.lock +8 -2
- data/README.md +37 -30
- data/Rakefile +14 -1
- data/doc/authentication.md +84 -43
- data/doc/code-search.md +55 -28
- data/doc/configuration.md +27 -2
- data/doc/extensibility.md +90 -129
- data/doc/getting-started.md +53 -57
- data/doc/memory.md +51 -118
- data/doc/personas.md +417 -0
- data/doc/plugins.md +55 -99
- data/doc/releasing.md +10 -9
- data/doc/rpc.md +7 -7
- data/doc/usage.md +125 -141
- data/doc/web-search.md +80 -14
- data/exe/kward +2 -0
- data/kward.gemspec +4 -0
- data/lib/kward/agent.rb +30 -3
- data/lib/kward/ansi.rb +3 -0
- data/lib/kward/auth/anthropic_oauth.rb +291 -0
- data/lib/kward/auth/file.rb +2 -0
- data/lib/kward/auth/github_oauth.rb +3 -0
- data/lib/kward/auth/openai_oauth.rb +4 -0
- data/lib/kward/auth/openrouter_api_key.rb +2 -0
- data/lib/kward/cancellation.rb +3 -0
- data/lib/kward/cli/auth_commands.rb +82 -0
- data/lib/kward/cli/commands.rb +229 -0
- data/lib/kward/cli/compaction.rb +25 -0
- data/lib/kward/cli/doctor.rb +121 -0
- data/lib/kward/cli/interactive_turn.rb +227 -0
- data/lib/kward/cli/memory_commands.rb +133 -0
- data/lib/kward/cli/plugins.rb +112 -0
- data/lib/kward/cli/prompt_interface.rb +134 -0
- data/lib/kward/cli/rendering.rb +378 -0
- data/lib/kward/cli/runtime_helpers.rb +170 -0
- data/lib/kward/cli/sessions.rb +376 -0
- data/lib/kward/cli/settings.rb +669 -0
- data/lib/kward/cli/slash_commands.rb +114 -0
- data/lib/kward/cli/stats.rb +64 -0
- data/lib/kward/cli/sysprompt.rb +57 -0
- data/lib/kward/cli/tool_summaries.rb +157 -0
- data/lib/kward/cli.rb +52 -2792
- data/lib/kward/cli_transcript_formatter.rb +40 -12
- data/lib/kward/clipboard.rb +1 -0
- data/lib/kward/compaction/file_operation_tracker.rb +3 -0
- data/lib/kward/compactor.rb +31 -9
- data/lib/kward/config_files.rb +78 -34
- data/lib/kward/conversation.rb +110 -13
- data/lib/kward/events.rb +2 -0
- data/lib/kward/export_path.rb +2 -0
- data/lib/kward/image_attachments.rb +2 -0
- data/lib/kward/markdown_transcript.rb +2 -0
- data/lib/kward/memory/manager.rb +144 -14
- data/lib/kward/message_access.rb +29 -2
- data/lib/kward/message_text.rb +45 -0
- data/lib/kward/model/chat_invocation.rb +2 -0
- data/lib/kward/model/client.rb +295 -77
- data/lib/kward/model/context_overflow.rb +2 -0
- data/lib/kward/model/context_usage.rb +14 -10
- data/lib/kward/model/model_info.rb +160 -4
- data/lib/kward/model/payloads.rb +254 -22
- data/lib/kward/model/retry_message.rb +2 -0
- data/lib/kward/model/stream_parser.rb +387 -25
- data/lib/kward/pan/server.rb +3 -1
- data/lib/kward/plugin_registry.rb +12 -0
- data/lib/kward/private_file.rb +2 -0
- data/lib/kward/prompt_interface/banner.rb +3 -0
- data/lib/kward/prompt_interface/composer_controller.rb +262 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
- data/lib/kward/prompt_interface/composer_state.rb +221 -0
- data/lib/kward/prompt_interface/key_handler.rb +365 -0
- data/lib/kward/prompt_interface/layout.rb +31 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
- data/lib/kward/prompt_interface/question_prompt.rb +328 -0
- data/lib/kward/prompt_interface/runtime_state.rb +59 -0
- data/lib/kward/prompt_interface/screen.rb +186 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
- data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
- data/lib/kward/prompt_interface/stream_state.rb +65 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
- data/lib/kward/prompt_interface/transcript_renderer.rb +151 -0
- data/lib/kward/prompt_interface.rb +69 -1832
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +3 -0
- data/lib/kward/prompts.rb +63 -7
- data/lib/kward/question_contract.rb +66 -0
- data/lib/kward/resources/avatar_kward_logo.rb +2 -0
- data/lib/kward/resources/pixel_logo.rb +2 -0
- data/lib/kward/rpc/attachment_normalizer.rb +60 -0
- data/lib/kward/rpc/auth_manager.rb +65 -11
- data/lib/kward/rpc/config_manager.rb +11 -0
- data/lib/kward/rpc/prompt_bridge.rb +5 -26
- data/lib/kward/rpc/redactor.rb +3 -0
- data/lib/kward/rpc/runtime_payloads.rb +4 -1
- data/lib/kward/rpc/server.rb +43 -11
- data/lib/kward/rpc/session_manager.rb +139 -347
- data/lib/kward/rpc/session_metrics.rb +68 -0
- data/lib/kward/rpc/session_tree.rb +48 -0
- data/lib/kward/rpc/session_tree_rows.rb +208 -0
- data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
- data/lib/kward/rpc/tool_metadata.rb +3 -0
- data/lib/kward/rpc/transcript_normalizer.rb +50 -0
- data/lib/kward/rpc/transport.rb +3 -0
- data/lib/kward/session_diff.rb +2 -0
- data/lib/kward/session_store.rb +154 -25
- data/lib/kward/session_trash.rb +1 -0
- data/lib/kward/session_tree_renderer.rb +8 -41
- data/lib/kward/session_tree_tool_display.rb +56 -0
- data/lib/kward/skills/registry.rb +3 -0
- data/lib/kward/starter_pack_installer.rb +3 -2
- data/lib/kward/steering.rb +2 -0
- data/lib/kward/telemetry/logger.rb +3 -0
- data/lib/kward/telemetry/stats.rb +3 -0
- data/lib/kward/tools/ask_user_question.rb +20 -32
- data/lib/kward/tools/base.rb +8 -0
- data/lib/kward/tools/code_search.rb +5 -0
- data/lib/kward/tools/edit_file.rb +5 -0
- data/lib/kward/tools/fetch_content.rb +41 -0
- data/lib/kward/tools/fetch_raw.rb +40 -0
- data/lib/kward/tools/list_directory.rb +5 -0
- data/lib/kward/tools/read_file.rb +5 -0
- data/lib/kward/tools/read_skill.rb +5 -0
- data/lib/kward/tools/registry.rb +42 -4
- data/lib/kward/tools/run_shell_command.rb +5 -0
- data/lib/kward/tools/search/code.rb +7 -0
- data/lib/kward/tools/search/web.rb +20 -17
- data/lib/kward/tools/search/web_fetch.rb +202 -0
- data/lib/kward/tools/tool_call.rb +27 -5
- data/lib/kward/tools/web_search.rb +7 -1
- data/lib/kward/tools/write_file.rb +5 -0
- data/lib/kward/transcript_export.rb +2 -0
- data/lib/kward/version.rb +2 -1
- data/lib/kward/workspace.rb +45 -5
- data/templates/default/fulldoc/html/css/kward.css +1501 -0
- data/templates/default/fulldoc/html/images/kward_logo.png +0 -0
- data/templates/default/fulldoc/html/js/kward.js +296 -0
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/layout/html/breadcrumb.erb +11 -0
- data/templates/default/layout/html/layout.erb +141 -0
- data/templates/default/layout/html/setup.rb +139 -0
- metadata +56 -1
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
require_relative "../config_files"
|
|
2
2
|
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
3
4
|
module Kward
|
|
5
|
+
# Static and configured model metadata helpers.
|
|
4
6
|
module ModelInfo
|
|
5
7
|
DEFAULT_OPENAI_MODEL = "gpt-5.5"
|
|
6
8
|
DEFAULT_OPENROUTER_MODEL = "openai/gpt-5.5"
|
|
7
9
|
DEFAULT_COPILOT_MODEL = "gpt-5-mini"
|
|
10
|
+
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6"
|
|
8
11
|
DEFAULT_REASONING_EFFORT = "medium"
|
|
9
12
|
OPENAI_MODEL_CHOICES = %w[gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
|
|
10
13
|
OPENROUTER_MODEL_CHOICES = OPENAI_MODEL_CHOICES.map { |model| "openai/#{model}" }.freeze
|
|
14
|
+
ANTHROPIC_MODEL_CHOICES = %w[
|
|
15
|
+
claude-opus-4-8
|
|
16
|
+
claude-sonnet-4-6
|
|
17
|
+
claude-haiku-4-5
|
|
18
|
+
claude-opus-4-7
|
|
19
|
+
claude-opus-4-6
|
|
20
|
+
claude-opus-4-5
|
|
21
|
+
claude-sonnet-4-5
|
|
22
|
+
].freeze
|
|
11
23
|
COPILOT_MODEL_CHOICES = %w[
|
|
12
24
|
gpt-5-mini
|
|
13
25
|
gpt-5.3-codex
|
|
@@ -32,13 +44,38 @@ module Kward
|
|
|
32
44
|
["high", "High"],
|
|
33
45
|
["xhigh", "Extra High"]
|
|
34
46
|
].freeze
|
|
47
|
+
OPENAI_REASONING_EFFORT_CHOICES = [
|
|
48
|
+
["none", "None"],
|
|
49
|
+
*REASONING_EFFORT_CHOICES
|
|
50
|
+
].freeze
|
|
51
|
+
ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES = [
|
|
52
|
+
["low", "Low"],
|
|
53
|
+
["medium", "Medium"],
|
|
54
|
+
["high", "High"],
|
|
55
|
+
["xhigh", "Extra High"],
|
|
56
|
+
["max", "Max"]
|
|
57
|
+
].freeze
|
|
58
|
+
ANTHROPIC_STANDARD_REASONING_EFFORT_CHOICES = [
|
|
59
|
+
["low", "Low"],
|
|
60
|
+
["medium", "Medium"],
|
|
61
|
+
["high", "High"],
|
|
62
|
+
["max", "Max"]
|
|
63
|
+
].freeze
|
|
64
|
+
ANTHROPIC_OPUS_4_5_REASONING_EFFORT_CHOICES = [
|
|
65
|
+
["low", "Low"],
|
|
66
|
+
["medium", "Medium"],
|
|
67
|
+
["high", "High"]
|
|
68
|
+
].freeze
|
|
35
69
|
|
|
36
70
|
IMAGE_UNSUPPORTED_MODELS = [
|
|
37
71
|
/(?:\A|\/)gpt-5\.3-codex-spark\z/
|
|
38
72
|
].freeze
|
|
39
73
|
|
|
40
|
-
|
|
74
|
+
CODEX_CONTEXT_WINDOWS = [
|
|
41
75
|
[/\Agpt-5\.5/, 400_000],
|
|
76
|
+
[/\Agpt-5\.4-mini/, 400_000],
|
|
77
|
+
[/\Agpt-5\.4/, 1_050_000],
|
|
78
|
+
[/\Agpt-5-mini/, 400_000],
|
|
42
79
|
[/\Agpt-5-codex/, 400_000],
|
|
43
80
|
[/\Agpt-5\.3-codex-spark/, 128_000],
|
|
44
81
|
[/\Agpt-5\.3-codex/, 400_000],
|
|
@@ -51,6 +88,33 @@ module Kward
|
|
|
51
88
|
[/\Agpt-4/, 128_000],
|
|
52
89
|
[/\Agpt-3\.5-turbo/, 16_385]
|
|
53
90
|
].freeze
|
|
91
|
+
OPENAI_CONTEXT_WINDOWS = [
|
|
92
|
+
[/\Agpt-5\.5/, 1_050_000],
|
|
93
|
+
[/\Agpt-5\.4-mini/, 400_000],
|
|
94
|
+
[/\Agpt-5\.4/, 1_050_000],
|
|
95
|
+
[/\Agpt-5-mini/, 400_000],
|
|
96
|
+
[/\Agpt-5-codex/, 400_000],
|
|
97
|
+
[/\Agpt-5\.3-codex-spark/, 128_000],
|
|
98
|
+
[/\Agpt-5\.3-codex/, 400_000],
|
|
99
|
+
[/\Agpt-5\.2-codex/, 400_000],
|
|
100
|
+
[/\Agpt-5/, 400_000],
|
|
101
|
+
[/\Agpt-4\.1/, 1_047_576],
|
|
102
|
+
[/\Agpt-4o/, 128_000],
|
|
103
|
+
[/\Ao3/, 200_000],
|
|
104
|
+
[/\Ao4/, 200_000],
|
|
105
|
+
[/\Agpt-4/, 128_000],
|
|
106
|
+
[/\Agpt-3\.5-turbo/, 16_385]
|
|
107
|
+
].freeze
|
|
108
|
+
ANTHROPIC_CONTEXT_WINDOWS = [
|
|
109
|
+
[/\Aclaude-(?:fable|mythos)-5(?:\z|-)/, 1_000_000],
|
|
110
|
+
[/\Aclaude-opus-4-(?:6|7|8)(?:\z|-)/, 1_000_000],
|
|
111
|
+
[/\Aclaude-sonnet-4-6(?:\z|-)/, 1_000_000],
|
|
112
|
+
[/\Aclaude-(?:haiku|opus|sonnet)-4-5(?:\z|-)/, 200_000],
|
|
113
|
+
[/\Aclaude-(?:haiku|opus|sonnet)-4(?:\z|-)/, 200_000]
|
|
114
|
+
].freeze
|
|
115
|
+
GEMINI_CONTEXT_WINDOWS = [
|
|
116
|
+
[/\Agemini-(?:2\.5-pro|3(?:\.1)?-pro|3(?:\.5)?-flash)/, 1_048_576]
|
|
117
|
+
].freeze
|
|
54
118
|
|
|
55
119
|
module_function
|
|
56
120
|
|
|
@@ -62,11 +126,28 @@ module Kward
|
|
|
62
126
|
env["OPENROUTER_MODEL"] || ConfigFiles.config_value(config, "openrouter_model", "model") || DEFAULT_OPENROUTER_MODEL
|
|
63
127
|
when "Copilot"
|
|
64
128
|
normalize_copilot_model(env["COPILOT_MODEL"] || ConfigFiles.config_value(config, "copilot_model", "model") || DEFAULT_COPILOT_MODEL)
|
|
129
|
+
when "Anthropic"
|
|
130
|
+
normalize_anthropic_model(env["ANTHROPIC_MODEL"] || ConfigFiles.config_value(config, "anthropic_model", "model") || DEFAULT_ANTHROPIC_MODEL)
|
|
65
131
|
else
|
|
66
132
|
env["OPENAI_MODEL"] || ConfigFiles.config_value(config, "openai_model", "model") || DEFAULT_OPENAI_MODEL
|
|
67
133
|
end
|
|
68
134
|
end
|
|
69
135
|
|
|
136
|
+
def normalize_anthropic_model(id)
|
|
137
|
+
text = id.to_s.strip
|
|
138
|
+
return DEFAULT_ANTHROPIC_MODEL if text.empty?
|
|
139
|
+
text = text.delete_prefix("anthropic/").delete_prefix("claude/")
|
|
140
|
+
{
|
|
141
|
+
"claude-sonnet-4.6" => "claude-sonnet-4-6",
|
|
142
|
+
"claude-sonnet-4.5" => "claude-sonnet-4-5",
|
|
143
|
+
"claude-opus-4.8" => "claude-opus-4-8",
|
|
144
|
+
"claude-opus-4.7" => "claude-opus-4-7",
|
|
145
|
+
"claude-opus-4.6" => "claude-opus-4-6",
|
|
146
|
+
"claude-opus-4.5" => "claude-opus-4-5",
|
|
147
|
+
"claude-haiku-4.5" => "claude-haiku-4-5"
|
|
148
|
+
}.fetch(text, text)
|
|
149
|
+
end
|
|
150
|
+
|
|
70
151
|
def normalize_copilot_model(id)
|
|
71
152
|
text = id.to_s.strip
|
|
72
153
|
return DEFAULT_COPILOT_MODEL if text.empty?
|
|
@@ -92,6 +173,8 @@ module Kward
|
|
|
92
173
|
env["OPENROUTER_REASONING_EFFORT"] || ConfigFiles.config_value(config, "openrouter_reasoning_effort", "reasoning_effort", "thinking_level") || DEFAULT_REASONING_EFFORT
|
|
93
174
|
when "Copilot"
|
|
94
175
|
env["COPILOT_REASONING_EFFORT"] || ConfigFiles.config_value(config, "copilot_reasoning_effort", "reasoning_effort", "thinking_level") || DEFAULT_REASONING_EFFORT
|
|
176
|
+
when "Anthropic"
|
|
177
|
+
env["ANTHROPIC_REASONING_EFFORT"] || ConfigFiles.config_value(config, "anthropic_reasoning_effort", "reasoning_effort", "thinking_level") || DEFAULT_REASONING_EFFORT
|
|
95
178
|
else
|
|
96
179
|
env["OPENAI_REASONING_EFFORT"] || ConfigFiles.config_value(config, "openai_reasoning_effort", "reasoning_effort", "thinking_level") || DEFAULT_REASONING_EFFORT
|
|
97
180
|
end
|
|
@@ -101,6 +184,7 @@ module Kward
|
|
|
101
184
|
case provider.to_s.downcase
|
|
102
185
|
when "openrouter" then "OpenRouter"
|
|
103
186
|
when "copilot" then "Copilot"
|
|
187
|
+
when "anthropic", "claude" then "Anthropic"
|
|
104
188
|
when "codex", "openai" then "Codex"
|
|
105
189
|
else provider.to_s
|
|
106
190
|
end
|
|
@@ -110,6 +194,7 @@ module Kward
|
|
|
110
194
|
case provider.to_s.downcase
|
|
111
195
|
when "openrouter" then "openrouter"
|
|
112
196
|
when "copilot" then "copilot"
|
|
197
|
+
when "anthropic", "claude" then "anthropic"
|
|
113
198
|
else "codex"
|
|
114
199
|
end
|
|
115
200
|
end
|
|
@@ -118,6 +203,7 @@ module Kward
|
|
|
118
203
|
case provider.to_s.downcase
|
|
119
204
|
when "openrouter" then "openrouter_model"
|
|
120
205
|
when "copilot" then "copilot_model"
|
|
206
|
+
when "anthropic", "claude" then "anthropic_model"
|
|
121
207
|
else "openai_model"
|
|
122
208
|
end
|
|
123
209
|
end
|
|
@@ -126,6 +212,7 @@ module Kward
|
|
|
126
212
|
case provider.to_s.downcase
|
|
127
213
|
when "openrouter" then "openrouter_reasoning_effort"
|
|
128
214
|
when "copilot" then "copilot_reasoning_effort"
|
|
215
|
+
when "anthropic", "claude" then "anthropic_reasoning_effort"
|
|
129
216
|
else "openai_reasoning_effort"
|
|
130
217
|
end
|
|
131
218
|
end
|
|
@@ -142,18 +229,87 @@ module Kward
|
|
|
142
229
|
end
|
|
143
230
|
|
|
144
231
|
def context_window(provider, id)
|
|
145
|
-
|
|
232
|
+
case provider
|
|
233
|
+
when "Codex"
|
|
234
|
+
pattern_context_window(CODEX_CONTEXT_WINDOWS, id)
|
|
235
|
+
when "OpenRouter"
|
|
236
|
+
openrouter_context_window(id)
|
|
237
|
+
when "Copilot"
|
|
238
|
+
copilot_context_window(id)
|
|
239
|
+
when "Anthropic"
|
|
240
|
+
anthropic_context_window(id)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
146
243
|
|
|
147
|
-
|
|
244
|
+
def pattern_context_window(patterns, id)
|
|
245
|
+
match = patterns.find { |pattern, _window| id.to_s.match?(pattern) }
|
|
148
246
|
match&.last
|
|
149
247
|
end
|
|
150
248
|
|
|
249
|
+
def openrouter_context_window(id)
|
|
250
|
+
text = id.to_s
|
|
251
|
+
return pattern_context_window(OPENAI_CONTEXT_WINDOWS, text.delete_prefix("openai/")) if text.start_with?("openai/")
|
|
252
|
+
return anthropic_context_window(text.delete_prefix("anthropic/")) if text.start_with?("anthropic/")
|
|
253
|
+
return pattern_context_window(GEMINI_CONTEXT_WINDOWS, text.delete_prefix("google/")) if text.start_with?("google/")
|
|
254
|
+
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def copilot_context_window(id)
|
|
259
|
+
text = id.to_s
|
|
260
|
+
pattern_context_window(OPENAI_CONTEXT_WINDOWS, text) ||
|
|
261
|
+
anthropic_context_window(normalize_anthropic_model(text)) ||
|
|
262
|
+
pattern_context_window(GEMINI_CONTEXT_WINDOWS, text)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def anthropic_context_window(id)
|
|
266
|
+
pattern_context_window(ANTHROPIC_CONTEXT_WINDOWS, normalize_anthropic_model(id))
|
|
267
|
+
end
|
|
268
|
+
|
|
151
269
|
def supports_images?(_provider, id)
|
|
152
270
|
IMAGE_UNSUPPORTED_MODELS.none? { |pattern| id.to_s.match?(pattern) }
|
|
153
271
|
end
|
|
154
272
|
|
|
155
273
|
def reasoning_supported?(provider, id)
|
|
156
|
-
|
|
274
|
+
case provider
|
|
275
|
+
when "Codex", "OpenRouter"
|
|
276
|
+
true
|
|
277
|
+
when "Anthropic"
|
|
278
|
+
!reasoning_effort_choices(provider, id).empty?
|
|
279
|
+
when "Copilot"
|
|
280
|
+
id.to_s.match?(/\Agpt-5(?:\.|-|\z)/)
|
|
281
|
+
else
|
|
282
|
+
false
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def reasoning_effort_choices(provider, id)
|
|
287
|
+
case provider
|
|
288
|
+
when "Codex", "OpenRouter"
|
|
289
|
+
openai_reasoning_effort_choices(id)
|
|
290
|
+
when "Anthropic"
|
|
291
|
+
anthropic_reasoning_effort_choices(id)
|
|
292
|
+
when "Copilot"
|
|
293
|
+
id.to_s.match?(/\Agpt-5(?:\.|-|\z)/) ? openai_reasoning_effort_choices(id) : []
|
|
294
|
+
else
|
|
295
|
+
[]
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def openai_reasoning_effort_choices(id)
|
|
300
|
+
text = id.to_s.delete_prefix("openai/")
|
|
301
|
+
return REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.[23]-codex/)
|
|
302
|
+
|
|
303
|
+
OPENAI_REASONING_EFFORT_CHOICES
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def anthropic_reasoning_effort_choices(id)
|
|
307
|
+
text = normalize_anthropic_model(id)
|
|
308
|
+
return ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:fable|mythos)-5(?:\z|-)|\Aclaude-opus-4-(?:7|8)(?:\z|-)/)
|
|
309
|
+
return ANTHROPIC_STANDARD_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:opus-4-6|sonnet-4-6)(?:\z|-)/)
|
|
310
|
+
return ANTHROPIC_OPUS_4_5_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-opus-4-5(?:\z|-)/)
|
|
311
|
+
|
|
312
|
+
[]
|
|
157
313
|
end
|
|
158
314
|
|
|
159
315
|
def normalize(model, current_provider: nil, current_model: nil, current_reasoning_effort: nil)
|
data/lib/kward/model/payloads.rb
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
+
require "json"
|
|
1
2
|
require_relative "../image_attachments"
|
|
3
|
+
require_relative "../message_access"
|
|
2
4
|
require_relative "model_info"
|
|
3
5
|
|
|
6
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
7
|
module Kward
|
|
8
|
+
# Converts Kward conversation/tool data into provider-specific request shapes.
|
|
9
|
+
#
|
|
10
|
+
# This module is mixed into `Client` because it needs provider configuration
|
|
11
|
+
# helpers such as `model_for` and `reasoning_effort`. Keep pure transcript and
|
|
12
|
+
# schema transformations here; keep network transport, credentials, retries,
|
|
13
|
+
# and telemetry in `Client`.
|
|
14
|
+
#
|
|
15
|
+
# Kward stores one internal transcript shape, then projects it into either
|
|
16
|
+
# Chat Completions-style messages or Responses/Codex input items. Preserve both
|
|
17
|
+
# symbol and string key handling through `MessageAccess` so restored sessions,
|
|
18
|
+
# tests, and RPC-normalized messages keep working.
|
|
5
19
|
module ModelPayloads
|
|
6
20
|
private
|
|
7
21
|
|
|
8
22
|
def request_payload(provider, messages, tools, max_tokens: nil, model: nil, reasoning: nil)
|
|
9
23
|
parts = build_context_parts(provider, messages, tools, model: model)
|
|
10
24
|
payload = { model: parts[:model], messages: parts[:messages], tools: parts[:tools] }
|
|
11
|
-
payload[:reasoning] = { effort: reasoning_effort("OpenRouter") } if provider == "OpenRouter" && reasoning != false
|
|
25
|
+
payload[:reasoning] = { effort: reasoning || reasoning_effort("OpenRouter") } if provider == "OpenRouter" && reasoning != false
|
|
12
26
|
payload[:max_tokens] = max_tokens.to_i if max_tokens.to_i.positive?
|
|
13
27
|
payload
|
|
14
28
|
end
|
|
@@ -22,18 +36,18 @@ module Kward
|
|
|
22
36
|
|
|
23
37
|
def messages_include_images?(messages)
|
|
24
38
|
messages.any? do |message|
|
|
25
|
-
content =
|
|
39
|
+
content = MessageAccess.content(message)
|
|
26
40
|
content.is_a?(Array) && content.any? { |part| (part[:type] || part["type"]).to_s == "image" }
|
|
27
41
|
end
|
|
28
42
|
end
|
|
29
43
|
|
|
30
44
|
def chat_messages(messages)
|
|
31
45
|
messages.map do |message|
|
|
32
|
-
role =
|
|
33
|
-
content =
|
|
46
|
+
role = MessageAccess.role(message)
|
|
47
|
+
content = MessageAccess.content(message)
|
|
34
48
|
case role.to_s
|
|
35
49
|
when "compactionSummary"
|
|
36
|
-
{ role: "assistant", content:
|
|
50
|
+
{ role: "assistant", content: MessageAccess.summary(message) || content.to_s }
|
|
37
51
|
when "assistant"
|
|
38
52
|
api_message(message, role: "assistant", content: content.is_a?(Array) ? plain_content(content) : content, keys: ["tool_calls", :tool_calls, "name", :name])
|
|
39
53
|
when "toolResult"
|
|
@@ -51,7 +65,7 @@ module Kward
|
|
|
51
65
|
def api_message(message, role:, content:, keys: [])
|
|
52
66
|
result = { role: role, content: content }
|
|
53
67
|
keys.each_slice(2) do |string_key, symbol_key|
|
|
54
|
-
value = message
|
|
68
|
+
value = MessageAccess.value(message, string_key) || MessageAccess.value(message, symbol_key)
|
|
55
69
|
next if value.nil?
|
|
56
70
|
|
|
57
71
|
target_key = case string_key.to_s
|
|
@@ -75,6 +89,27 @@ module Kward
|
|
|
75
89
|
end
|
|
76
90
|
end
|
|
77
91
|
|
|
92
|
+
def anthropic_payload(messages, tools, max_tokens: nil, model: nil, reasoning: nil)
|
|
93
|
+
parts = build_context_parts("Anthropic", messages, tools, model: model)
|
|
94
|
+
system = [{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }]
|
|
95
|
+
system << { type: "text", text: parts[:system] } unless parts[:system].to_s.empty?
|
|
96
|
+
payload = {
|
|
97
|
+
model: parts[:model],
|
|
98
|
+
system: system,
|
|
99
|
+
messages: parts[:messages],
|
|
100
|
+
max_tokens: max_tokens.to_i.positive? ? max_tokens.to_i : 16_384,
|
|
101
|
+
stream: true
|
|
102
|
+
}
|
|
103
|
+
payload[:tools] = parts[:tools] unless parts[:tools].empty?
|
|
104
|
+
if reasoning != false
|
|
105
|
+
payload[:thinking] = { type: "adaptive", display: "summarized" }
|
|
106
|
+
payload[:output_config] = { effort: reasoning || reasoning_effort("Anthropic") }
|
|
107
|
+
else
|
|
108
|
+
payload[:thinking] = { type: "disabled" }
|
|
109
|
+
end
|
|
110
|
+
payload
|
|
111
|
+
end
|
|
112
|
+
|
|
78
113
|
def codex_payload(messages, tools, max_tokens: nil, model: nil, reasoning: nil)
|
|
79
114
|
parts = build_context_parts("Codex", messages, tools, model: model)
|
|
80
115
|
payload = {
|
|
@@ -88,12 +123,27 @@ module Kward
|
|
|
88
123
|
store: false,
|
|
89
124
|
include: []
|
|
90
125
|
}
|
|
91
|
-
payload[:reasoning] = { effort: reasoning_effort("Codex"), summary: "auto" } unless reasoning == false
|
|
126
|
+
payload[:reasoning] = { effort: reasoning || reasoning_effort("Codex"), summary: "auto" } unless reasoning == false
|
|
92
127
|
payload
|
|
93
128
|
end
|
|
94
129
|
|
|
130
|
+
# Builds provider-neutral context parts before final JSON serialization.
|
|
131
|
+
#
|
|
132
|
+
# Codex and Copilot Responses use `instructions` plus typed `input` items;
|
|
133
|
+
# OpenRouter and Copilot chat use OpenAI-compatible `messages`. Callers should
|
|
134
|
+
# prefer this method when showing context usage or debugging provider payloads
|
|
135
|
+
# so every frontend sees the same conversion rules.
|
|
95
136
|
def build_context_parts(provider, messages, tools, model: nil)
|
|
96
|
-
if provider == "
|
|
137
|
+
if provider == "Anthropic"
|
|
138
|
+
system, anthropic_messages = anthropic_messages(messages)
|
|
139
|
+
{
|
|
140
|
+
provider: provider,
|
|
141
|
+
model: model_for(provider, override_model: model),
|
|
142
|
+
system: system,
|
|
143
|
+
messages: anthropic_messages,
|
|
144
|
+
tools: tools.map { |tool| anthropic_tool_schema(tool) }
|
|
145
|
+
}
|
|
146
|
+
elsif provider == "CopilotResponses"
|
|
97
147
|
instructions, input = codex_messages(messages)
|
|
98
148
|
{
|
|
99
149
|
provider: provider,
|
|
@@ -121,36 +171,145 @@ module Kward
|
|
|
121
171
|
end
|
|
122
172
|
end
|
|
123
173
|
|
|
174
|
+
def anthropic_messages(messages)
|
|
175
|
+
system = []
|
|
176
|
+
output = []
|
|
177
|
+
messages.each do |message|
|
|
178
|
+
role = MessageAccess.role(message)
|
|
179
|
+
content = MessageAccess.content(message) || ""
|
|
180
|
+
case role.to_s
|
|
181
|
+
when "system"
|
|
182
|
+
system << plain_content(content).to_s
|
|
183
|
+
when "assistant"
|
|
184
|
+
blocks = []
|
|
185
|
+
text = plain_content(content).to_s
|
|
186
|
+
blocks << { type: "text", text: text } unless text.empty?
|
|
187
|
+
MessageAccess.tool_calls(message).each do |tool_call|
|
|
188
|
+
function = tool_call[:function] || tool_call["function"] || {}
|
|
189
|
+
name = function[:name] || function["name"]
|
|
190
|
+
arguments = function[:arguments] || function["arguments"] || "{}"
|
|
191
|
+
blocks << {
|
|
192
|
+
type: "tool_use",
|
|
193
|
+
id: normalize_anthropic_tool_call_id(tool_call[:id] || tool_call["id"] || "call_#{name}"),
|
|
194
|
+
name: claude_code_tool_name(name),
|
|
195
|
+
input: parse_tool_arguments(arguments)
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
output << { role: "assistant", content: blocks } unless blocks.empty?
|
|
199
|
+
when "tool", "toolResult"
|
|
200
|
+
output << {
|
|
201
|
+
role: "user",
|
|
202
|
+
content: [{
|
|
203
|
+
type: "tool_result",
|
|
204
|
+
tool_use_id: normalize_anthropic_tool_call_id(MessageAccess.tool_call_id(message) || MessageAccess.tool_name(message) || "tool-call"),
|
|
205
|
+
content: plain_content(content).to_s
|
|
206
|
+
}]
|
|
207
|
+
}
|
|
208
|
+
when "compactionSummary"
|
|
209
|
+
summary = MessageAccess.summary(message) || content
|
|
210
|
+
output << { role: "assistant", content: [{ type: "text", text: summary.to_s }] } unless summary.to_s.empty?
|
|
211
|
+
else
|
|
212
|
+
output << { role: "user", content: anthropic_user_content(content) }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
[system.join("\n\n"), output]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def anthropic_user_content(content)
|
|
219
|
+
return content.to_s unless content.is_a?(Array)
|
|
220
|
+
|
|
221
|
+
content.filter_map do |part|
|
|
222
|
+
type = part[:type] || part["type"]
|
|
223
|
+
if type == "text"
|
|
224
|
+
{ type: "text", text: part[:text] || part["text"] || "" }
|
|
225
|
+
elsif type == "image"
|
|
226
|
+
mime_type, data = ImageAttachments.data_url(part).split(",", 2)
|
|
227
|
+
media_type = mime_type.to_s[/data:([^;]+)/, 1] || "image/png"
|
|
228
|
+
{ type: "image", source: { type: "base64", media_type: media_type, data: data.to_s } }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
CLAUDE_CODE_TOOL_NAMES = %w[Read Write Edit Bash Grep Glob AskUserQuestion EnterPlanMode ExitPlanMode KillShell NotebookEdit Skill Task TaskOutput TodoWrite WebFetch WebSearch].freeze
|
|
234
|
+
|
|
235
|
+
def claude_code_tool_name(name)
|
|
236
|
+
mapped = {
|
|
237
|
+
"read_file" => "Read",
|
|
238
|
+
"write_file" => "Write",
|
|
239
|
+
"edit_file" => "Edit",
|
|
240
|
+
"run_shell_command" => "Bash",
|
|
241
|
+
"web_search" => "WebSearch",
|
|
242
|
+
"fetch_content" => "WebFetch",
|
|
243
|
+
"ask_user_question" => "AskUserQuestion"
|
|
244
|
+
}[name.to_s]
|
|
245
|
+
return mapped if mapped
|
|
246
|
+
|
|
247
|
+
lookup = CLAUDE_CODE_TOOL_NAMES.find { |tool_name| tool_name.downcase == name.to_s.downcase }
|
|
248
|
+
lookup || name.to_s
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def normalize_anthropic_tool_call_id(id)
|
|
252
|
+
id.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")[0, 64]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def parse_tool_arguments(arguments)
|
|
256
|
+
return arguments if arguments.is_a?(Hash)
|
|
257
|
+
|
|
258
|
+
JSON.parse(arguments.to_s.empty? ? "{}" : arguments.to_s)
|
|
259
|
+
rescue JSON::ParserError
|
|
260
|
+
{}
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def anthropic_tool_schema(tool)
|
|
264
|
+
function = tool[:function] || tool["function"] || {}
|
|
265
|
+
schema = function[:parameters] || function["parameters"] || {}
|
|
266
|
+
{
|
|
267
|
+
name: claude_code_tool_name(function[:name] || function["name"]),
|
|
268
|
+
description: function[:description] || function["description"] || "",
|
|
269
|
+
input_schema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: schema[:properties] || schema["properties"] || {},
|
|
272
|
+
required: schema[:required] || schema["required"] || []
|
|
273
|
+
},
|
|
274
|
+
eager_input_streaming: true
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
|
|
124
278
|
def codex_messages(messages)
|
|
125
279
|
instructions = []
|
|
126
280
|
input = []
|
|
127
281
|
|
|
128
282
|
messages.each do |message|
|
|
129
|
-
role =
|
|
130
|
-
content =
|
|
283
|
+
role = MessageAccess.role(message)
|
|
284
|
+
content = MessageAccess.content(message) || ""
|
|
131
285
|
case role.to_s
|
|
132
286
|
when "system"
|
|
133
287
|
instructions << plain_content(content).to_s
|
|
134
288
|
when "tool", "toolResult"
|
|
135
289
|
input << {
|
|
136
290
|
type: "function_call_output",
|
|
137
|
-
call_id:
|
|
291
|
+
call_id: MessageAccess.tool_call_id(message) || MessageAccess.tool_name(message) || "tool-call",
|
|
138
292
|
output: plain_content(content).to_s
|
|
139
293
|
}
|
|
140
294
|
when "assistant"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
295
|
+
response_items = codex_replay_response_items(message)
|
|
296
|
+
if response_items.empty?
|
|
297
|
+
content = plain_content(content)
|
|
298
|
+
input << codex_message("assistant", content.to_s) unless content.to_s.empty?
|
|
299
|
+
MessageAccess.tool_calls(message).each do |tool_call|
|
|
300
|
+
function = tool_call[:function] || tool_call["function"] || {}
|
|
301
|
+
input << {
|
|
302
|
+
type: "function_call",
|
|
303
|
+
call_id: tool_call[:id] || tool_call["id"] || function[:name] || function["name"] || "tool-call",
|
|
304
|
+
name: function[:name] || function["name"],
|
|
305
|
+
arguments: function[:arguments] || function["arguments"] || "{}"
|
|
306
|
+
}
|
|
307
|
+
end
|
|
308
|
+
else
|
|
309
|
+
input.concat(response_items)
|
|
151
310
|
end
|
|
152
311
|
when "compactionSummary"
|
|
153
|
-
summary =
|
|
312
|
+
summary = MessageAccess.summary(message) || content
|
|
154
313
|
input << codex_message("assistant", summary.to_s) unless summary.to_s.empty?
|
|
155
314
|
else
|
|
156
315
|
input << codex_user_message(content)
|
|
@@ -179,6 +338,79 @@ module Kward
|
|
|
179
338
|
{ type: "message", role: role, content: [{ type: type, text: text }] }
|
|
180
339
|
end
|
|
181
340
|
|
|
341
|
+
def codex_replay_response_items(message)
|
|
342
|
+
items = MessageAccess.response_items(message)
|
|
343
|
+
return [] if items.empty?
|
|
344
|
+
|
|
345
|
+
items.filter_map { |item| codex_replay_response_item(item) }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def codex_replay_response_item(item)
|
|
349
|
+
return nil unless item.is_a?(Hash)
|
|
350
|
+
|
|
351
|
+
case item[:type] || item["type"]
|
|
352
|
+
when "reasoning"
|
|
353
|
+
codex_replay_reasoning_item(item)
|
|
354
|
+
when "message"
|
|
355
|
+
codex_replay_message_item(item)
|
|
356
|
+
when "function_call", "custom_tool_call"
|
|
357
|
+
codex_replay_tool_call_item(item)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def codex_replay_reasoning_item(item)
|
|
362
|
+
result = { type: "reasoning" }
|
|
363
|
+
summary = item[:summary] || item["summary"]
|
|
364
|
+
content = item[:content] || item["content"]
|
|
365
|
+
encrypted_content = item[:encrypted_content] || item["encrypted_content"]
|
|
366
|
+
result[:summary] = summary if summary.is_a?(Array)
|
|
367
|
+
result[:content] = content if content.is_a?(Array)
|
|
368
|
+
result[:encrypted_content] = encrypted_content if encrypted_content
|
|
369
|
+
result
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def codex_replay_message_item(item)
|
|
373
|
+
content = item[:content] || item["content"]
|
|
374
|
+
return nil unless content.is_a?(Array)
|
|
375
|
+
|
|
376
|
+
result = { type: "message", role: item[:role] || item["role"] || "assistant", content: codex_replay_message_content(content) }
|
|
377
|
+
phase = item[:phase] || item["phase"]
|
|
378
|
+
result[:phase] = phase if phase
|
|
379
|
+
result
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def codex_replay_message_content(content)
|
|
383
|
+
content.filter_map do |part|
|
|
384
|
+
next unless part.is_a?(Hash)
|
|
385
|
+
|
|
386
|
+
type = part[:type] || part["type"]
|
|
387
|
+
next unless ["output_text", "text", "refusal"].include?(type)
|
|
388
|
+
|
|
389
|
+
replay_part = { type: type }
|
|
390
|
+
text = part[:text] || part["text"]
|
|
391
|
+
refusal = part[:refusal] || part["refusal"]
|
|
392
|
+
replay_part[:text] = text.to_s if text || type != "refusal"
|
|
393
|
+
replay_part[:refusal] = refusal.to_s if refusal
|
|
394
|
+
annotations = part[:annotations] || part["annotations"]
|
|
395
|
+
replay_part[:annotations] = annotations if annotations.is_a?(Array)
|
|
396
|
+
replay_part
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def codex_replay_tool_call_item(item)
|
|
401
|
+
type = item[:type] || item["type"]
|
|
402
|
+
result = { type: type }
|
|
403
|
+
call_id = item[:call_id] || item["call_id"]
|
|
404
|
+
name = item[:name] || item["name"]
|
|
405
|
+
arguments = item[:arguments] || item["arguments"]
|
|
406
|
+
input = item[:input] || item["input"]
|
|
407
|
+
result[:call_id] = call_id if call_id
|
|
408
|
+
result[:name] = name if name
|
|
409
|
+
result[:arguments] = arguments if arguments
|
|
410
|
+
result[:input] = input if input
|
|
411
|
+
result
|
|
412
|
+
end
|
|
413
|
+
|
|
182
414
|
def plain_content(content)
|
|
183
415
|
return content unless content.is_a?(Array)
|
|
184
416
|
|