kward 0.75.0 → 0.77.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/ci.yml +8 -1
- data/CHANGELOG.md +48 -0
- data/Gemfile.lock +2 -2
- data/README.md +12 -4
- data/doc/agent-tools.md +0 -3
- data/doc/api.md +4 -0
- data/doc/composer.md +219 -0
- data/doc/configuration.md +87 -27
- data/doc/editor.md +18 -2
- data/doc/files.md +1 -1
- data/doc/getting-started.md +4 -0
- data/doc/git.md +2 -2
- data/doc/lifecycle-hooks.md +2 -14
- data/doc/pan.md +167 -0
- data/doc/plugins.md +1 -0
- data/doc/rpc.md +3 -22
- data/doc/security.md +180 -0
- data/doc/usage.md +15 -32
- data/lib/kward/agent.rb +5 -0
- data/lib/kward/ansi.rb +2 -2
- data/lib/kward/cancellation.rb +2 -2
- data/lib/kward/cli/commands.rb +1 -3
- data/lib/kward/cli/interactive_turn.rb +3 -17
- data/lib/kward/cli/plugins.rb +2 -7
- data/lib/kward/cli/prompt_interface.rb +10 -17
- data/lib/kward/cli/rendering.rb +7 -3
- data/lib/kward/cli/runtime_helpers.rb +0 -38
- data/lib/kward/cli/sessions.rb +14 -5
- data/lib/kward/cli/settings.rb +22 -0
- data/lib/kward/cli/slash_commands.rb +1 -561
- data/lib/kward/cli/tabs.rb +14 -2
- data/lib/kward/cli.rb +1 -17
- data/lib/kward/cli_transcript_formatter.rb +21 -11
- data/lib/kward/compactor.rb +3 -3
- data/lib/kward/config_files.rb +21 -0
- data/lib/kward/ekwsh.rb +0 -4
- data/lib/kward/events.rb +1 -0
- data/lib/kward/hooks/catalog.rb +0 -5
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +12 -9
- data/lib/kward/model/model_info.rb +14 -5
- data/lib/kward/model/stream_parser.rb +12 -12
- data/lib/kward/pan/index.html.erb +460 -64
- data/lib/kward/pan/kward_logo.png +0 -0
- data/lib/kward/pan/server.rb +250 -23
- data/lib/kward/plugin_registry.rb +20 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +31 -23
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +72 -3
- data/lib/kward/prompt_interface/layout.rb +0 -4
- data/lib/kward/prompt_interface/project_browser.rb +40 -1
- data/lib/kward/prompt_interface/runtime_state.rb +4 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +0 -1
- data/lib/kward/prompt_interface/transcript_renderer.rb +1 -13
- data/lib/kward/prompt_interface.rb +18 -1
- data/lib/kward/prompts/commands.rb +1 -3
- data/lib/kward/prompts.rb +20 -0
- data/lib/kward/rpc/server.rb +14 -42
- data/lib/kward/rpc/session_manager.rb +77 -25
- data/lib/kward/rpc/tool_event_normalizer.rb +2 -0
- data/lib/kward/rpc/tool_metadata.rb +23 -0
- data/lib/kward/rpc/transcript_normalizer.rb +14 -10
- data/lib/kward/session_store.rb +55 -16
- data/lib/kward/skills/registry.rb +21 -1
- data/lib/kward/tools/base.rb +14 -0
- data/lib/kward/tools/registry.rb +11 -16
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/search/web.rb +1 -1
- data/lib/kward/tools/tool_call.rb +0 -4
- data/lib/kward/transcript_export.rb +1 -1
- data/lib/kward/update_check.rb +12 -7
- data/lib/kward/version.rb +1 -1
- data/templates/default/kward_navigation.rb +5 -1
- data/templates/default/layout/html/layout.erb +0 -2
- metadata +5 -12
- data/lib/kward/workers/git_guard.rb +0 -93
- data/lib/kward/workers/job.rb +0 -99
- data/lib/kward/workers/live_view.rb +0 -49
- data/lib/kward/workers/manager.rb +0 -328
- data/lib/kward/workers/queue_runner.rb +0 -166
- data/lib/kward/workers/queue_store.rb +0 -112
- data/lib/kward/workers/store.rb +0 -72
- data/lib/kward/workers/tool_policy.rb +0 -23
- data/lib/kward/workers/worker.rb +0 -82
- data/lib/kward/workers/write_lock.rb +0 -38
- data/lib/kward/workers.rb +0 -10
|
@@ -10,25 +10,31 @@ module Kward
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
def reasoning(message)
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
reasoning_blocks(message).join("\n")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reasoning_blocks(message)
|
|
17
|
+
response_blocks = response_items(message).flat_map do |item|
|
|
18
|
+
next [] unless MessageAccess.value(item, :type) == "reasoning"
|
|
19
|
+
|
|
20
|
+
summary = response_item_text_parts(MessageAccess.value(item, :summary))
|
|
21
|
+
summary.empty? ? response_item_text_parts(MessageAccess.value(item, :content)) : summary
|
|
22
|
+
end
|
|
23
|
+
return response_blocks unless response_blocks.empty?
|
|
15
24
|
|
|
16
25
|
content = MessageAccess.content(message)
|
|
17
26
|
if content.is_a?(Array)
|
|
18
|
-
|
|
27
|
+
blocks = content.filter_map do |part|
|
|
19
28
|
type = MessageAccess.value(part, :type)
|
|
20
29
|
next unless ["thinking", "reasoning"].include?(type)
|
|
21
30
|
|
|
22
31
|
MessageAccess.value(part, :thinking) || MessageAccess.value(part, :reasoning) || MessageAccess.value(part, :text)
|
|
23
|
-
end.
|
|
24
|
-
return
|
|
32
|
+
end.map(&:to_s).reject(&:empty?)
|
|
33
|
+
return blocks unless blocks.empty?
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
response_item_text(MessageAccess.value(item, :summary)).empty? ? response_item_text(MessageAccess.value(item, :content)) : response_item_text(MessageAccess.value(item, :summary))
|
|
31
|
-
end.join("\n")
|
|
36
|
+
direct = MessageAccess.value(message, :reasoning_summary).to_s
|
|
37
|
+
direct.empty? ? [] : [direct]
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def content_text(content)
|
|
@@ -113,11 +119,15 @@ module Kward
|
|
|
113
119
|
end
|
|
114
120
|
|
|
115
121
|
def response_item_text(parts)
|
|
122
|
+
response_item_text_parts(parts).join
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def response_item_text_parts(parts)
|
|
116
126
|
Array(parts).filter_map do |part|
|
|
117
127
|
next unless part.is_a?(Hash)
|
|
118
128
|
|
|
119
129
|
MessageAccess.value(part, :text) || MessageAccess.value(part, :refusal)
|
|
120
|
-
end.
|
|
130
|
+
end.map(&:to_s).reject(&:empty?)
|
|
121
131
|
end
|
|
122
132
|
|
|
123
133
|
def image_part_reference(part)
|
data/lib/kward/compactor.rb
CHANGED
|
@@ -37,7 +37,7 @@ module Kward
|
|
|
37
37
|
|
|
38
38
|
Cut = Struct.new(:first_kept_index, :messages_to_summarize, :turn_prefix_messages, :split_turn, :preserved_messages, :preserved_start_index, keyword_init: true)
|
|
39
39
|
|
|
40
|
-
#
|
|
40
|
+
# Configuration for manual and automatic conversation compaction.
|
|
41
41
|
class Settings
|
|
42
42
|
DEFAULT_ENABLED = true
|
|
43
43
|
DEFAULT_RESERVE_TOKENS = 16_384
|
|
@@ -731,7 +731,7 @@ module Kward
|
|
|
731
731
|
SummarizationFailed = Compaction::SummarizationFailed
|
|
732
732
|
|
|
733
733
|
AUTO_COMPACTION_GUARD_RATIO = 0.10
|
|
734
|
-
|
|
734
|
+
AUTO_COMPACTION_EXTRA_GUARD_FLOOR = 12_000
|
|
735
735
|
|
|
736
736
|
# Creates an object for conversation compaction.
|
|
737
737
|
def initialize(conversation:, client:, tool_result_summarizer: nil, settings: nil, summarizer: nil)
|
|
@@ -805,7 +805,7 @@ module Kward
|
|
|
805
805
|
def self.auto_compaction_reserve_tokens(context_window:, configured_reserve_tokens:)
|
|
806
806
|
context_window_i = context_window.to_i
|
|
807
807
|
dynamic_guard = (context_window_i * AUTO_COMPACTION_GUARD_RATIO).to_i
|
|
808
|
-
[configured_reserve_tokens.to_i, dynamic_guard,
|
|
808
|
+
[configured_reserve_tokens.to_i, dynamic_guard, AUTO_COMPACTION_EXTRA_GUARD_FLOOR].max
|
|
809
809
|
end
|
|
810
810
|
|
|
811
811
|
def compaction_messages(custom_instructions = nil)
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -24,7 +24,9 @@ module Kward
|
|
|
24
24
|
# Keep path decisions here. Higher-level code should ask `ConfigFiles` for
|
|
25
25
|
# config, prompt, skill, plugin, cache, memory, and session locations instead of
|
|
26
26
|
# reconstructing `~/.kward` paths independently.
|
|
27
|
+
# @api public
|
|
27
28
|
module ConfigFiles
|
|
29
|
+
# Raised when a user configuration file cannot be parsed.
|
|
28
30
|
class ConfigError < StandardError
|
|
29
31
|
attr_reader :path, :format, :detail
|
|
30
32
|
|
|
@@ -39,6 +41,7 @@ module Kward
|
|
|
39
41
|
MAX_SKILL_FILE_BYTES = 100_000
|
|
40
42
|
MAX_PROMPT_FILE_BYTES = 32 * 1024
|
|
41
43
|
DEFAULT_OVERLAY_SETTINGS = { "alignment" => "center", "width" => "maximum" }.freeze
|
|
44
|
+
PROJECT_BROWSER_ICON_THEMES = %w[off nerd-font].freeze
|
|
42
45
|
DEFAULT_PERSONAS = {
|
|
43
46
|
"characters" => [
|
|
44
47
|
{
|
|
@@ -87,10 +90,12 @@ module Kward
|
|
|
87
90
|
File.expand_path(ENV["KWARD_CONFIG_PATH"] || File.join(config_dir, "config.json"))
|
|
88
91
|
end
|
|
89
92
|
|
|
93
|
+
# @return [String] directory containing reusable local caches
|
|
90
94
|
def cache_dir
|
|
91
95
|
File.join(config_dir, "cache")
|
|
92
96
|
end
|
|
93
97
|
|
|
98
|
+
# @return [String] embedded-shell YAML config path
|
|
94
99
|
def ekwsh_config_path
|
|
95
100
|
File.join(config_dir, "ekwsh.yml")
|
|
96
101
|
end
|
|
@@ -103,6 +108,9 @@ module Kward
|
|
|
103
108
|
File.join(config_dir, "trusted_workspace_hooks.json")
|
|
104
109
|
end
|
|
105
110
|
|
|
111
|
+
# Returns a fresh default config suitable for first-run persistence.
|
|
112
|
+
#
|
|
113
|
+
# @return [Hash] deep-mutable config defaults
|
|
106
114
|
def default_config
|
|
107
115
|
{
|
|
108
116
|
"personas" => JSON.parse(JSON.generate(DEFAULT_PERSONAS)),
|
|
@@ -124,11 +132,17 @@ module Kward
|
|
|
124
132
|
"diff_view" => "auto"
|
|
125
133
|
},
|
|
126
134
|
"overlay" => DEFAULT_OVERLAY_SETTINGS.dup,
|
|
135
|
+
"project_browser" => {
|
|
136
|
+
"icons" => "off"
|
|
137
|
+
},
|
|
127
138
|
"web_search" => {
|
|
128
139
|
"enabled" => true,
|
|
129
140
|
"provider" => "auto",
|
|
130
141
|
"allow_model_providers" => false
|
|
131
142
|
},
|
|
143
|
+
"updates" => {
|
|
144
|
+
"check" => true
|
|
145
|
+
},
|
|
132
146
|
"sessions" => {
|
|
133
147
|
"auto_resume" => false
|
|
134
148
|
},
|
|
@@ -406,6 +420,13 @@ module Kward
|
|
|
406
420
|
settings
|
|
407
421
|
end
|
|
408
422
|
|
|
423
|
+
# Returns the project browser icon theme, or off when unset or invalid.
|
|
424
|
+
def project_browser_icon_theme(config = read_config)
|
|
425
|
+
browser = config["project_browser"].is_a?(Hash) ? config["project_browser"] : {}
|
|
426
|
+
theme = browser["icons"].to_s
|
|
427
|
+
PROJECT_BROWSER_ICON_THEMES.include?(theme) ? theme : "off"
|
|
428
|
+
end
|
|
429
|
+
|
|
409
430
|
# Returns whether the composer should show busy-state keyboard help.
|
|
410
431
|
def composer_busy_help?(config = read_config)
|
|
411
432
|
composer = config["composer"].is_a?(Hash) ? config["composer"] : {}
|
data/lib/kward/ekwsh.rb
CHANGED
data/lib/kward/events.rb
CHANGED
|
@@ -3,6 +3,7 @@ module Kward
|
|
|
3
3
|
# Frontend-neutral event objects emitted during agent turns.
|
|
4
4
|
module Events
|
|
5
5
|
ReasoningDelta = Struct.new(:delta, keyword_init: true)
|
|
6
|
+
ReasoningBoundary = Data.define
|
|
6
7
|
AssistantDelta = Struct.new(:delta, keyword_init: true)
|
|
7
8
|
AssistantMessage = Struct.new(:message, keyword_init: true)
|
|
8
9
|
Retry = Struct.new(:provider, :model, :attempt, :max_attempts, :delay_seconds, :error, :request_bytes, keyword_init: true)
|
data/lib/kward/hooks/catalog.rb
CHANGED
|
@@ -45,11 +45,6 @@ module Kward
|
|
|
45
45
|
EventDefinition.new(name: "git_stage_after", failure_policy: "warn"),
|
|
46
46
|
EventDefinition.new(name: "git_commit_before", failure_policy: "deny"),
|
|
47
47
|
EventDefinition.new(name: "git_commit_after", failure_policy: "warn"),
|
|
48
|
-
EventDefinition.new(name: "worker_job_create", failure_policy: "warn"),
|
|
49
|
-
EventDefinition.new(name: "worker_job_start_before", failure_policy: "warn"),
|
|
50
|
-
EventDefinition.new(name: "worker_job_start_after", failure_policy: "warn"),
|
|
51
|
-
EventDefinition.new(name: "worker_job_ready_for_review", failure_policy: "warn"),
|
|
52
|
-
EventDefinition.new(name: "worker_job_failed", failure_policy: "warn")
|
|
53
48
|
].each_with_object({}) { |definition, result| result[definition.name] = definition }.freeze
|
|
54
49
|
|
|
55
50
|
module_function
|
data/lib/kward/model/client.rb
CHANGED
|
@@ -94,7 +94,7 @@ module Kward
|
|
|
94
94
|
@openrouter_models = nil
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
def chat(messages, tools: [], on_reasoning_delta: nil, on_assistant_delta: nil, on_retry: nil, cancellation: nil, steering: nil, max_tokens: nil, provider: nil, model: nil, reasoning: nil)
|
|
97
|
+
def chat(messages, tools: [], on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, on_retry: nil, cancellation: nil, steering: nil, max_tokens: nil, provider: nil, model: nil, reasoning: nil)
|
|
98
98
|
cancellation&.raise_if_cancelled!
|
|
99
99
|
requested_provider = provider
|
|
100
100
|
url, token, resolved_provider, account_id = credentials(provider: requested_provider)
|
|
@@ -126,6 +126,7 @@ module Kward
|
|
|
126
126
|
request_body: request_body,
|
|
127
127
|
current_model: current_model,
|
|
128
128
|
on_reasoning_delta: on_reasoning_delta,
|
|
129
|
+
on_reasoning_boundary: on_reasoning_boundary,
|
|
129
130
|
on_assistant_delta: on_assistant_delta,
|
|
130
131
|
cancellation: cancellation,
|
|
131
132
|
max_tokens: max_tokens
|
|
@@ -257,7 +258,7 @@ module Kward
|
|
|
257
258
|
}
|
|
258
259
|
end
|
|
259
260
|
|
|
260
|
-
def chat_provider_request(provider:, url:, token:, account_id:, messages:, tools:, request_body:, current_model:, on_reasoning_delta:, on_assistant_delta:, cancellation:, max_tokens:)
|
|
261
|
+
def chat_provider_request(provider:, url:, token:, account_id:, messages:, tools:, request_body:, current_model:, on_reasoning_delta:, on_reasoning_boundary:, on_assistant_delta:, cancellation:, max_tokens:)
|
|
261
262
|
case provider
|
|
262
263
|
when "Codex"
|
|
263
264
|
chat_codex_provider(
|
|
@@ -269,6 +270,7 @@ module Kward
|
|
|
269
270
|
request_body: request_body,
|
|
270
271
|
current_model: current_model,
|
|
271
272
|
on_reasoning_delta: on_reasoning_delta,
|
|
273
|
+
on_reasoning_boundary: on_reasoning_boundary,
|
|
272
274
|
on_assistant_delta: on_assistant_delta,
|
|
273
275
|
cancellation: cancellation,
|
|
274
276
|
max_tokens: max_tokens
|
|
@@ -306,7 +308,7 @@ module Kward
|
|
|
306
308
|
end
|
|
307
309
|
end
|
|
308
310
|
|
|
309
|
-
def chat_codex_provider(url:, token:, account_id:, messages:, tools:, request_body:, current_model:, on_reasoning_delta:, on_assistant_delta:, cancellation:, max_tokens:)
|
|
311
|
+
def chat_codex_provider(url:, token:, account_id:, messages:, tools:, request_body:, current_model:, on_reasoning_delta:, on_reasoning_boundary:, on_assistant_delta:, cancellation:, max_tokens:)
|
|
310
312
|
message = codex_chat(
|
|
311
313
|
url,
|
|
312
314
|
token,
|
|
@@ -315,6 +317,7 @@ module Kward
|
|
|
315
317
|
tools,
|
|
316
318
|
request_body: request_body,
|
|
317
319
|
on_reasoning_delta: on_reasoning_delta,
|
|
320
|
+
on_reasoning_boundary: on_reasoning_boundary,
|
|
318
321
|
on_assistant_delta: on_assistant_delta,
|
|
319
322
|
cancellation: cancellation,
|
|
320
323
|
max_tokens: max_tokens
|
|
@@ -649,7 +652,7 @@ module Kward
|
|
|
649
652
|
role.to_s == "user" ? "user" : "agent"
|
|
650
653
|
end
|
|
651
654
|
|
|
652
|
-
def codex_chat(url, token, account_id, messages, tools, request_body: nil, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil, max_tokens: nil)
|
|
655
|
+
def codex_chat(url, token, account_id, messages, tools, request_body: nil, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil, max_tokens: nil)
|
|
653
656
|
request = Net::HTTP::Post.new(url)
|
|
654
657
|
request["Authorization"] = "Bearer #{token}"
|
|
655
658
|
request["ChatGPT-Account-Id"] = account_id if account_id
|
|
@@ -669,7 +672,7 @@ module Kward
|
|
|
669
672
|
raise RequestError.new(provider: "Codex", code: response.code, body: redact(body, token))
|
|
670
673
|
end
|
|
671
674
|
|
|
672
|
-
message = parse_codex_sse_stream(response, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, cancellation: cancellation)
|
|
675
|
+
message = parse_codex_sse_stream(response, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, cancellation: cancellation)
|
|
673
676
|
end
|
|
674
677
|
end
|
|
675
678
|
|
|
@@ -681,12 +684,12 @@ module Kward
|
|
|
681
684
|
raise e
|
|
682
685
|
end
|
|
683
686
|
|
|
684
|
-
def parse_codex_sse(body, on_reasoning_delta: nil, on_assistant_delta: nil)
|
|
685
|
-
ModelStreamParser.parse_codex_sse(body, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
|
|
687
|
+
def parse_codex_sse(body, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil)
|
|
688
|
+
ModelStreamParser.parse_codex_sse(body, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
|
|
686
689
|
end
|
|
687
690
|
|
|
688
|
-
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil)
|
|
689
|
-
ModelStreamParser.parse_codex_sse_stream(response, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, cancellation: cancellation, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
|
|
691
|
+
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil)
|
|
692
|
+
ModelStreamParser.parse_codex_sse_stream(response, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, cancellation: cancellation, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
|
|
690
693
|
end
|
|
691
694
|
|
|
692
695
|
def parse_anthropic_sse_stream(response, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil)
|
|
@@ -4,13 +4,15 @@ require_relative "../config_files"
|
|
|
4
4
|
module Kward
|
|
5
5
|
# Static and configured model metadata helpers.
|
|
6
6
|
module ModelInfo
|
|
7
|
-
DEFAULT_OPENAI_MODEL = "gpt-5.
|
|
7
|
+
DEFAULT_OPENAI_MODEL = "gpt-5.6-sol"
|
|
8
8
|
DEFAULT_COPILOT_MODEL = "gpt-5-mini"
|
|
9
|
-
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-
|
|
9
|
+
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5"
|
|
10
10
|
DEFAULT_REASONING_EFFORT = "medium"
|
|
11
|
-
OPENAI_MODEL_CHOICES = %w[gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
|
|
11
|
+
OPENAI_MODEL_CHOICES = %w[gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
|
|
12
12
|
ANTHROPIC_MODEL_CHOICES = %w[
|
|
13
|
+
claude-fable-5
|
|
13
14
|
claude-opus-4-8
|
|
15
|
+
claude-sonnet-5
|
|
14
16
|
claude-sonnet-4-6
|
|
15
17
|
claude-haiku-4-5
|
|
16
18
|
claude-opus-4-7
|
|
@@ -46,6 +48,10 @@ module Kward
|
|
|
46
48
|
["none", "None"],
|
|
47
49
|
*REASONING_EFFORT_CHOICES
|
|
48
50
|
].freeze
|
|
51
|
+
OPENAI_MAX_REASONING_EFFORT_CHOICES = [
|
|
52
|
+
*OPENAI_REASONING_EFFORT_CHOICES,
|
|
53
|
+
["max", "Max"]
|
|
54
|
+
].freeze
|
|
49
55
|
ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES = [
|
|
50
56
|
["low", "Low"],
|
|
51
57
|
["medium", "Medium"],
|
|
@@ -70,6 +76,7 @@ module Kward
|
|
|
70
76
|
].freeze
|
|
71
77
|
|
|
72
78
|
CODEX_CONTEXT_WINDOWS = [
|
|
79
|
+
[/\Agpt-5\.6/, 1_050_000],
|
|
73
80
|
[/\Agpt-5\.5/, 400_000],
|
|
74
81
|
[/\Agpt-5\.4-mini/, 400_000],
|
|
75
82
|
[/\Agpt-5\.4/, 1_050_000],
|
|
@@ -87,6 +94,7 @@ module Kward
|
|
|
87
94
|
[/\Agpt-3\.5-turbo/, 16_385]
|
|
88
95
|
].freeze
|
|
89
96
|
OPENAI_CONTEXT_WINDOWS = [
|
|
97
|
+
[/\Agpt-5\.6/, 1_050_000],
|
|
90
98
|
[/\Agpt-5\.5/, 1_050_000],
|
|
91
99
|
[/\Agpt-5\.4-mini/, 400_000],
|
|
92
100
|
[/\Agpt-5\.4/, 1_050_000],
|
|
@@ -104,7 +112,7 @@ module Kward
|
|
|
104
112
|
[/\Agpt-3\.5-turbo/, 16_385]
|
|
105
113
|
].freeze
|
|
106
114
|
ANTHROPIC_CONTEXT_WINDOWS = [
|
|
107
|
-
[/\Aclaude-(?:fable|mythos)-5(?:\z|-)/, 1_000_000],
|
|
115
|
+
[/\Aclaude-(?:fable|mythos|sonnet)-5(?:\z|-)/, 1_000_000],
|
|
108
116
|
[/\Aclaude-opus-4-(?:6|7|8)(?:\z|-)/, 1_000_000],
|
|
109
117
|
[/\Aclaude-sonnet-4-6(?:\z|-)/, 1_000_000],
|
|
110
118
|
[/\Aclaude-(?:haiku|opus|sonnet)-4-5(?:\z|-)/, 200_000],
|
|
@@ -382,13 +390,14 @@ module Kward
|
|
|
382
390
|
def openai_reasoning_effort_choices(id)
|
|
383
391
|
text = id.to_s.delete_prefix("openai/")
|
|
384
392
|
return REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.[23]-codex/)
|
|
393
|
+
return OPENAI_MAX_REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.6(?:\z|-)/)
|
|
385
394
|
|
|
386
395
|
OPENAI_REASONING_EFFORT_CHOICES
|
|
387
396
|
end
|
|
388
397
|
|
|
389
398
|
def anthropic_reasoning_effort_choices(id)
|
|
390
399
|
text = normalize_anthropic_model(id)
|
|
391
|
-
return ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:fable|mythos)-5(?:\z|-)|\Aclaude-opus-4-(?:7|8)(?:\z|-)/)
|
|
400
|
+
return ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:fable|mythos|sonnet)-5(?:\z|-)|\Aclaude-opus-4-(?:7|8)(?:\z|-)/)
|
|
392
401
|
return ANTHROPIC_STANDARD_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:opus-4-6|sonnet-4-6)(?:\z|-)/)
|
|
393
402
|
return ANTHROPIC_OPUS_4_5_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-opus-4-5(?:\z|-)/)
|
|
394
403
|
|
|
@@ -45,10 +45,10 @@ module Kward
|
|
|
45
45
|
raise "Copilot returned invalid SSE JSON: #{e.message}"
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def parse_codex_sse(body, on_reasoning_delta: nil, on_assistant_delta: nil, show_raw_reasoning: false, usage_normalizer: nil, request_error_class: nil)
|
|
48
|
+
def parse_codex_sse(body, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, show_raw_reasoning: false, usage_normalizer: nil, request_error_class: nil)
|
|
49
49
|
state = codex_sse_state(show_raw_reasoning: show_raw_reasoning)
|
|
50
50
|
body.split(/\r?\n\r?\n/).each do |block|
|
|
51
|
-
process_codex_sse_block(block, state, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class)
|
|
51
|
+
process_codex_sse_block(block, state, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class)
|
|
52
52
|
end
|
|
53
53
|
flush_codex_reasoning_delta(state, on_reasoning_delta: on_reasoning_delta)
|
|
54
54
|
codex_sse_message(state)
|
|
@@ -61,7 +61,7 @@ module Kward
|
|
|
61
61
|
# Deltas are yielded as soon as complete SSE blocks arrive so interactive
|
|
62
62
|
# frontends can render streamed assistant and reasoning text without waiting
|
|
63
63
|
# for the provider to close the response.
|
|
64
|
-
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil, show_raw_reasoning: false, usage_normalizer: nil, request_error_class: nil)
|
|
64
|
+
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil, show_raw_reasoning: false, usage_normalizer: nil, request_error_class: nil)
|
|
65
65
|
state = codex_sse_state(show_raw_reasoning: show_raw_reasoning)
|
|
66
66
|
buffer = +""
|
|
67
67
|
|
|
@@ -72,11 +72,11 @@ module Kward
|
|
|
72
72
|
delimiter = Regexp.last_match[0]
|
|
73
73
|
block = buffer[0...index]
|
|
74
74
|
buffer = buffer[(index + delimiter.length)..] || +""
|
|
75
|
-
process_codex_sse_block(block, state, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class)
|
|
75
|
+
process_codex_sse_block(block, state, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class)
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
cancellation&.raise_if_cancelled!
|
|
79
|
-
process_codex_sse_block(buffer, state, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class) unless buffer.empty?
|
|
79
|
+
process_codex_sse_block(buffer, state, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class) unless buffer.empty?
|
|
80
80
|
flush_codex_reasoning_delta(state, on_reasoning_delta: on_reasoning_delta)
|
|
81
81
|
codex_sse_message(state)
|
|
82
82
|
rescue JSON::ParserError => e
|
|
@@ -239,7 +239,7 @@ module Kward
|
|
|
239
239
|
}
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
-
def process_codex_sse_block(block, state, on_reasoning_delta: nil, on_assistant_delta: nil, usage_normalizer: nil, request_error_class: nil)
|
|
242
|
+
def process_codex_sse_block(block, state, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, usage_normalizer: nil, request_error_class: nil)
|
|
243
243
|
data = block.lines.filter_map { |line| line.start_with?("data:") ? line.delete_prefix("data:").strip : nil }.join("\n")
|
|
244
244
|
return if data.empty? || data == "[DONE]"
|
|
245
245
|
|
|
@@ -256,7 +256,7 @@ module Kward
|
|
|
256
256
|
when "response.reasoning_summary_text.delta"
|
|
257
257
|
codex_reasoning_delta(state, event["delta"], on_reasoning_delta: on_reasoning_delta)
|
|
258
258
|
when "response.reasoning_summary_part.done"
|
|
259
|
-
codex_reasoning_part_done(state, on_reasoning_delta: on_reasoning_delta)
|
|
259
|
+
codex_reasoning_part_done(state, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary)
|
|
260
260
|
when "response.reasoning_text.delta"
|
|
261
261
|
codex_raw_reasoning_delta(state, event["delta"], on_reasoning_delta: on_reasoning_delta)
|
|
262
262
|
when "response.reasoning_text.done"
|
|
@@ -391,16 +391,16 @@ module Kward
|
|
|
391
391
|
append_codex_raw_reasoning_text(state, text, on_reasoning_delta: on_reasoning_delta) unless text.empty?
|
|
392
392
|
end
|
|
393
393
|
|
|
394
|
-
def codex_reasoning_part_done(state, on_reasoning_delta: nil)
|
|
394
|
+
def codex_reasoning_part_done(state, on_reasoning_delta: nil, on_reasoning_boundary: nil)
|
|
395
395
|
item = active_codex_item(state)
|
|
396
396
|
return unless item&.fetch("type", nil) == "reasoning"
|
|
397
397
|
return if item["summary"].to_a.empty?
|
|
398
398
|
|
|
399
399
|
flush_codex_reasoning_delta(state, on_reasoning_delta: on_reasoning_delta)
|
|
400
|
-
text = "\n\n"
|
|
401
|
-
|
|
402
|
-
state[:reasoning_summary] <<
|
|
403
|
-
|
|
400
|
+
item["summary"].last["text"] = "#{item["summary"].last["text"].to_s.rstrip}\n\n"
|
|
401
|
+
state[:reasoning_summary].rstrip!
|
|
402
|
+
state[:reasoning_summary] << "\n\n"
|
|
403
|
+
on_reasoning_boundary&.call
|
|
404
404
|
end
|
|
405
405
|
|
|
406
406
|
def append_codex_reasoning_text(state, text, on_reasoning_delta: nil)
|