kward 0.79.0 → 0.80.1
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 +6 -0
- data/CHANGELOG.md +65 -1
- data/Gemfile.lock +2 -2
- data/README.md +31 -38
- data/doc/agent-tools.md +9 -10
- data/doc/api.md +3 -3
- data/doc/authentication.md +79 -110
- data/doc/code-search.md +1 -1
- data/doc/composer.md +1 -1
- data/doc/configuration.md +69 -26
- data/doc/context-budgeting.md +6 -6
- data/doc/context-tools.md +2 -2
- data/doc/editor.md +7 -7
- data/doc/extensibility.md +7 -29
- data/doc/files.md +6 -6
- data/doc/getting-started.md +2 -2
- data/doc/git.md +8 -2
- data/doc/lifecycle-hooks.md +5 -4
- data/doc/local-models.md +2 -2
- data/doc/mcp.md +5 -5
- data/doc/permissions.md +13 -4
- data/doc/plugins.md +39 -4
- data/doc/prompt-templates.md +105 -0
- data/doc/providers.md +140 -0
- data/doc/releasing.md +10 -2
- data/doc/rpc.md +54 -31
- data/doc/sandboxing.md +22 -0
- data/doc/security.md +22 -7
- data/doc/session-management.md +2 -4
- data/doc/skills.md +2 -0
- data/doc/tabs.md +37 -0
- data/doc/telegram.md +138 -0
- data/doc/transports.md +209 -0
- data/doc/usage.md +11 -3
- data/doc/web-search.md +3 -3
- data/doc/workspace-tools.md +2 -2
- data/examples/plugins/stardate_footer.rb +10 -0
- data/examples/plugins/telegram/plugin.rb +39 -0
- data/examples/plugins/telegram/telegram_api.rb +136 -0
- data/examples/plugins/telegram/telegram_transport.rb +304 -0
- data/lib/kward/agent.rb +11 -6
- data/lib/kward/ansi.rb +1 -0
- data/lib/kward/auth/api_key_store.rb +99 -0
- data/lib/kward/cli/auth_commands.rb +72 -13
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/git.rb +31 -8
- data/lib/kward/cli/memory_commands.rb +2 -2
- data/lib/kward/cli/plugins.rb +1 -0
- data/lib/kward/cli/prompt_interface.rb +44 -1
- data/lib/kward/cli/rendering.rb +6 -2
- data/lib/kward/cli/runtime_helpers.rb +5 -2
- data/lib/kward/cli/sessions.rb +1 -1
- data/lib/kward/cli/settings.rb +97 -33
- data/lib/kward/cli/slash_commands.rb +1 -1
- data/lib/kward/cli/tabs.rb +103 -37
- data/lib/kward/cli/transports.rb +67 -0
- data/lib/kward/cli/worktrees.rb +360 -0
- data/lib/kward/cli.rb +22 -0
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +51 -12
- data/lib/kward/conversation.rb +13 -7
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/hooks/audit_log.rb +5 -2
- data/lib/kward/model/azure_openai_config.rb +54 -0
- data/lib/kward/model/catalog.rb +113 -0
- data/lib/kward/model/client.rb +225 -20
- data/lib/kward/model/model_info.rb +49 -0
- data/lib/kward/model/payloads.rb +63 -4
- data/lib/kward/model/provider_catalog.rb +114 -0
- data/lib/kward/model/sources.rb +70 -0
- data/lib/kward/model/stream_parser.rb +83 -13
- data/lib/kward/permissions/policy.rb +18 -4
- data/lib/kward/plugin_chat_runtime.rb +374 -0
- data/lib/kward/plugin_registry.rb +99 -19
- data/lib/kward/private_file.rb +9 -3
- data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
- data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
- data/lib/kward/prompt_interface/editor/controller.rb +7 -7
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
- data/lib/kward/prompt_interface/editor/state.rb +3 -9
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +219 -19
- data/lib/kward/prompt_interface/file_overlay.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +7 -3
- data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
- data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
- data/lib/kward/prompt_interface/interactive/state.rb +2 -2
- data/lib/kward/prompt_interface/project_browser.rb +25 -6
- data/lib/kward/prompt_interface/runtime_state.rb +22 -6
- data/lib/kward/prompt_interface/screen.rb +3 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
- data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
- data/lib/kward/prompt_interface.rb +53 -12
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts/templates.rb +11 -6
- data/lib/kward/prompts.rb +6 -5
- data/lib/kward/rpc/auth_manager.rb +112 -152
- data/lib/kward/rpc/config_manager.rb +46 -6
- data/lib/kward/rpc/plugin_chat_manager.rb +55 -196
- data/lib/kward/rpc/prompt_bridge.rb +8 -4
- data/lib/kward/rpc/redactor.rb +1 -1
- data/lib/kward/rpc/server.rb +60 -10
- data/lib/kward/rpc/session_manager.rb +109 -30
- data/lib/kward/session_store.rb +84 -24
- data/lib/kward/skills/registry.rb +16 -11
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/telemetry/logger.rb +5 -2
- data/lib/kward/tools/git_commit.rb +39 -0
- data/lib/kward/tools/registry.rb +7 -2
- data/lib/kward/tools/tool_call.rb +1 -0
- data/lib/kward/transport/gateway.rb +253 -0
- data/lib/kward/transport/host.rb +343 -0
- data/lib/kward/transport/manager.rb +179 -0
- data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
- data/lib/kward/transport/runtime.rb +41 -0
- data/lib/kward/transport/store.rb +101 -0
- data/lib/kward/transport.rb +204 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace_factory.rb +19 -2
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +7 -2
- data/templates/default/layout/html/setup.rb +4 -0
- metadata +26 -1
data/lib/kward/model/client.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "json"
|
|
|
2
2
|
require "net/http"
|
|
3
3
|
require "uri"
|
|
4
4
|
require_relative "../auth/anthropic_oauth"
|
|
5
|
+
require_relative "../auth/api_key_store"
|
|
5
6
|
require_relative "../auth/github_oauth"
|
|
6
7
|
require_relative "../auth/openai_oauth"
|
|
7
8
|
require_relative "../cancellation"
|
|
@@ -9,8 +10,10 @@ require_relative "../config_files"
|
|
|
9
10
|
require_relative "../http"
|
|
10
11
|
require_relative "../openrouter_model_cache"
|
|
11
12
|
require_relative "context_overflow"
|
|
13
|
+
require_relative "azure_openai_config"
|
|
12
14
|
require_relative "copilot_models"
|
|
13
15
|
require_relative "model_info"
|
|
16
|
+
require_relative "sources"
|
|
14
17
|
require_relative "payloads"
|
|
15
18
|
require_relative "../telemetry/logger"
|
|
16
19
|
require_relative "stream_parser"
|
|
@@ -35,7 +38,9 @@ module Kward
|
|
|
35
38
|
"llama_cpp" => "http://127.0.0.1:8080/v1"
|
|
36
39
|
}.freeze
|
|
37
40
|
AUTH_ERROR = "No OpenAI OAuth login found. Run `ruby lib/main.rb login`, or set OPENAI_ACCESS_TOKEN/OPENROUTER_API_KEY."
|
|
38
|
-
OPENROUTER_AUTH_ERROR = "No OpenRouter API key found. Set OPENROUTER_API_KEY or
|
|
41
|
+
OPENROUTER_AUTH_ERROR = "No OpenRouter API key found. Set OPENROUTER_API_KEY or sign in with an API key."
|
|
42
|
+
OPENAI_API_AUTH_ERROR = "No OpenAI API key found. Set OPENAI_API_KEY or sign in with an API key."
|
|
43
|
+
AZURE_OPENAI_AUTH_ERROR = "No Azure OpenAI API key found. Set AZURE_OPENAI_API_KEY or sign in with an API key."
|
|
39
44
|
COPILOT_AUTH_ERROR = "No GitHub Copilot OAuth login found. Run `ruby lib/main.rb login github` or set COPILOT_GITHUB_TOKEN."
|
|
40
45
|
ANTHROPIC_AUTH_ERROR = "No Anthropic OAuth login found. Run `ruby lib/main.rb login anthropic`."
|
|
41
46
|
DEFAULT_OPENAI_MODEL = ModelInfo::DEFAULT_OPENAI_MODEL
|
|
@@ -87,7 +92,7 @@ module Kward
|
|
|
87
92
|
TRANSIENT_NETWORK_ERRORS = [IOError, EOFError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout].freeze
|
|
88
93
|
|
|
89
94
|
# Creates an object for model provider requests.
|
|
90
|
-
def initialize(api_key: ENV["OPENROUTER_API_KEY"], model: nil, openai_access_token: ENV["OPENAI_ACCESS_TOKEN"], oauth: OpenAIOAuth.new, github_oauth: GithubOAuth.new, anthropic_oauth: AnthropicOAuth.new, config_path: OpenAIOAuth.default_config_path, telemetry_logger: TelemetryLogger.new(config_path: config_path))
|
|
95
|
+
def initialize(api_key: ENV["OPENROUTER_API_KEY"], model: nil, openai_access_token: ENV["OPENAI_ACCESS_TOKEN"], oauth: OpenAIOAuth.new, github_oauth: GithubOAuth.new, anthropic_oauth: AnthropicOAuth.new, api_key_store: nil, config_path: OpenAIOAuth.default_config_path, telemetry_logger: TelemetryLogger.new(config_path: config_path))
|
|
91
96
|
@openrouter_api_key = presence(api_key)
|
|
92
97
|
@openai_access_token = presence(openai_access_token)
|
|
93
98
|
@oauth = oauth
|
|
@@ -95,6 +100,7 @@ module Kward
|
|
|
95
100
|
@anthropic_oauth = anthropic_oauth
|
|
96
101
|
@model = model
|
|
97
102
|
@config_path = File.expand_path(config_path)
|
|
103
|
+
@api_key_store = api_key_store || APIKeyStore.new(path: File.join(File.dirname(@config_path), "api_keys.json"), config_path: @config_path)
|
|
98
104
|
@config = load_config
|
|
99
105
|
@telemetry_logger = telemetry_logger
|
|
100
106
|
@copilot_models = nil
|
|
@@ -106,7 +112,7 @@ module Kward
|
|
|
106
112
|
cancellation&.raise_if_cancelled!
|
|
107
113
|
requested_provider = provider
|
|
108
114
|
url, token, resolved_provider, account_id = credentials(provider: requested_provider)
|
|
109
|
-
if token.to_s.empty? && authentication_required?(resolved_provider) && !requested_provider.to_s.empty?
|
|
115
|
+
if token.to_s.empty? && authentication_required?(resolved_provider) && !requested_provider.to_s.empty? && resolved_provider != "OpenAI"
|
|
110
116
|
url, token, resolved_provider, account_id = credentials
|
|
111
117
|
model = nil
|
|
112
118
|
reasoning = nil
|
|
@@ -214,7 +220,8 @@ module Kward
|
|
|
214
220
|
openrouter_model = model_for("OpenRouter")
|
|
215
221
|
openrouter_choices = openrouter_model_choices
|
|
216
222
|
models += openrouter_choices.map do |id|
|
|
217
|
-
|
|
223
|
+
metadata = openrouter_cached_model_entries.find { |entry| (entry["id"] || entry[:id]).to_s == id }
|
|
224
|
+
model_entry("OpenRouter", id, current: provider == "OpenRouter" && openrouter_model == id, metadata: metadata)
|
|
218
225
|
end
|
|
219
226
|
end
|
|
220
227
|
|
|
@@ -235,6 +242,19 @@ module Kward
|
|
|
235
242
|
models << model_entry("Anthropic", anthropic_model, current: provider == "Anthropic") unless ModelInfo::ANTHROPIC_MODEL_CHOICES.include?(anthropic_model)
|
|
236
243
|
end
|
|
237
244
|
|
|
245
|
+
ProviderCatalog.api_key_providers.each do |catalog_provider|
|
|
246
|
+
next unless ["openai_chat", "openai_responses", "gemini", "azure_openai"].include?(catalog_provider.protocol)
|
|
247
|
+
next if catalog_provider.id == "openrouter"
|
|
248
|
+
next unless provider_logged_in?(catalog_provider.name)
|
|
249
|
+
|
|
250
|
+
selected_model = model_for(catalog_provider.name)
|
|
251
|
+
provider_models = ModelSources.new(provider_id: catalog_provider.id, api_key: @api_key_store.fetch(catalog_provider.id)).models
|
|
252
|
+
provider_models.each do |entry|
|
|
253
|
+
models << model_entry(catalog_provider.name, entry.fetch("id"), current: provider == catalog_provider.name && selected_model == entry.fetch("id"), metadata: entry)
|
|
254
|
+
end
|
|
255
|
+
models << model_entry(catalog_provider.name, selected_model, current: provider == catalog_provider.name) unless selected_model.to_s.empty? || provider_models.any? { |entry| entry.fetch("id") == selected_model }
|
|
256
|
+
end
|
|
257
|
+
|
|
238
258
|
if provider_logged_in?("Local")
|
|
239
259
|
local_model = model_for("Local")
|
|
240
260
|
local_model_choices.each do |id|
|
|
@@ -247,6 +267,17 @@ module Kward
|
|
|
247
267
|
models.sort_by { |model| [model[:provider], model[:id]] }
|
|
248
268
|
end
|
|
249
269
|
|
|
270
|
+
# Refreshes cached choices for an API-key provider.
|
|
271
|
+
def refresh_available_models(provider: nil)
|
|
272
|
+
provider ||= current_provider
|
|
273
|
+
catalog_provider = ProviderCatalog.find_by_name(provider) || ProviderCatalog.find(provider.to_s.downcase)
|
|
274
|
+
return available_models unless catalog_provider&.api_key?
|
|
275
|
+
return available_models unless ProviderCatalog.runtime(catalog_provider.id).automatic_model_discovery?
|
|
276
|
+
|
|
277
|
+
ModelSources.new(provider_id: catalog_provider.id, api_key: @api_key_store.fetch(catalog_provider.id)).refresh
|
|
278
|
+
available_models
|
|
279
|
+
end
|
|
280
|
+
|
|
250
281
|
# Projects messages/tools into the provider-specific context shape without sending it.
|
|
251
282
|
def current_context_parts(messages, tools, provider: current_provider, model: nil)
|
|
252
283
|
build_context_parts(ModelInfo.provider_label(provider), messages, tools, model: model)
|
|
@@ -280,6 +311,34 @@ module Kward
|
|
|
280
311
|
|
|
281
312
|
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:)
|
|
282
313
|
case provider
|
|
314
|
+
when "Azure OpenAI"
|
|
315
|
+
chat_azure_openai_provider(
|
|
316
|
+
token: token,
|
|
317
|
+
request_body: request_body,
|
|
318
|
+
current_model: current_model,
|
|
319
|
+
on_assistant_delta: on_assistant_delta,
|
|
320
|
+
cancellation: cancellation
|
|
321
|
+
)
|
|
322
|
+
when "Google Gemini"
|
|
323
|
+
chat_gemini_provider(
|
|
324
|
+
url: url,
|
|
325
|
+
token: token,
|
|
326
|
+
request_body: request_body,
|
|
327
|
+
current_model: current_model,
|
|
328
|
+
on_assistant_delta: on_assistant_delta,
|
|
329
|
+
cancellation: cancellation
|
|
330
|
+
)
|
|
331
|
+
when "OpenAI"
|
|
332
|
+
chat_openai_responses_provider(
|
|
333
|
+
url: url,
|
|
334
|
+
token: token,
|
|
335
|
+
request_body: request_body,
|
|
336
|
+
current_model: current_model,
|
|
337
|
+
on_reasoning_delta: on_reasoning_delta,
|
|
338
|
+
on_reasoning_boundary: on_reasoning_boundary,
|
|
339
|
+
on_assistant_delta: on_assistant_delta,
|
|
340
|
+
cancellation: cancellation
|
|
341
|
+
)
|
|
283
342
|
when "Codex"
|
|
284
343
|
chat_codex_provider(
|
|
285
344
|
url: url,
|
|
@@ -329,6 +388,7 @@ module Kward
|
|
|
329
388
|
chat_openrouter_provider(
|
|
330
389
|
url: url,
|
|
331
390
|
token: token,
|
|
391
|
+
provider: provider,
|
|
332
392
|
request_body: request_body,
|
|
333
393
|
current_model: current_model,
|
|
334
394
|
on_assistant_delta: on_assistant_delta,
|
|
@@ -337,6 +397,95 @@ module Kward
|
|
|
337
397
|
end
|
|
338
398
|
end
|
|
339
399
|
|
|
400
|
+
def chat_azure_openai_provider(token:, request_body:, current_model:, on_assistant_delta:, cancellation:)
|
|
401
|
+
request_url = azure_openai_config(deployment: current_model).chat_completions_url
|
|
402
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(request_url))
|
|
403
|
+
request["api-key"] = token
|
|
404
|
+
request["Content-Type"] = "application/json"
|
|
405
|
+
request["Accept"] = "text/event-stream"
|
|
406
|
+
request.body = request_body
|
|
407
|
+
|
|
408
|
+
message = nil
|
|
409
|
+
Net::HTTP.start(request_url.hostname, request_url.port, use_ssl: true, read_timeout: stream_idle_timeout_seconds) do |http|
|
|
410
|
+
cancellation&.on_cancel { close_http(http) }
|
|
411
|
+
cancellation&.raise_if_cancelled!
|
|
412
|
+
http.request(request) do |response|
|
|
413
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
414
|
+
body = +""
|
|
415
|
+
response.read_body { |chunk| body << chunk }
|
|
416
|
+
raise RequestError.new(provider: "Azure OpenAI", code: response.code, body: redact(body, token))
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
message = parse_openai_chat_sse_stream(response, on_assistant_delta: on_assistant_delta, cancellation: cancellation, provider_label: "Azure OpenAI")
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
cancellation&.raise_if_cancelled!
|
|
423
|
+
attach_response_metadata(message, provider: "Azure OpenAI", model: current_model)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def chat_gemini_provider(url:, token:, request_body:, current_model:, on_assistant_delta:, cancellation:)
|
|
427
|
+
model = current_model.to_s.delete_prefix("models/")
|
|
428
|
+
request_url = URI("#{url.to_s.sub(%r{/+\z}, "")}/models/#{URI.encode_www_form_component(model)}:streamGenerateContent?alt=sse")
|
|
429
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(request_url))
|
|
430
|
+
request["x-goog-api-key"] = token
|
|
431
|
+
request["Content-Type"] = "application/json"
|
|
432
|
+
request["Accept"] = "text/event-stream"
|
|
433
|
+
request.body = request_body
|
|
434
|
+
|
|
435
|
+
message = nil
|
|
436
|
+
Net::HTTP.start(request_url.hostname, request_url.port, use_ssl: true, read_timeout: stream_idle_timeout_seconds) do |http|
|
|
437
|
+
cancellation&.on_cancel { close_http(http) }
|
|
438
|
+
cancellation&.raise_if_cancelled!
|
|
439
|
+
http.request(request) do |response|
|
|
440
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
441
|
+
body = +""
|
|
442
|
+
response.read_body { |chunk| body << chunk }
|
|
443
|
+
raise RequestError.new(provider: "Google Gemini", code: response.code, body: redact(body, token))
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
message = ModelStreamParser.parse_gemini_sse_stream(response, on_assistant_delta: on_assistant_delta, cancellation: cancellation, usage_normalizer: method(:gemini_usage))
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
cancellation&.raise_if_cancelled!
|
|
450
|
+
attach_response_metadata(message, provider: "Google Gemini", model: current_model)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def gemini_usage(usage)
|
|
454
|
+
return nil unless usage.is_a?(Hash)
|
|
455
|
+
|
|
456
|
+
normalized_usage(
|
|
457
|
+
"input_tokens" => usage["promptTokenCount"],
|
|
458
|
+
"output_tokens" => usage["candidatesTokenCount"],
|
|
459
|
+
"cache_read_tokens" => usage["cachedContentTokenCount"],
|
|
460
|
+
"total_tokens" => usage["totalTokenCount"]
|
|
461
|
+
)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def chat_openai_responses_provider(url:, token:, request_body:, current_model:, on_reasoning_delta:, on_reasoning_boundary:, on_assistant_delta:, cancellation:)
|
|
465
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(url))
|
|
466
|
+
request["Authorization"] = "Bearer #{token}"
|
|
467
|
+
request["Content-Type"] = "application/json"
|
|
468
|
+
request["Accept"] = "text/event-stream"
|
|
469
|
+
request.body = request_body
|
|
470
|
+
|
|
471
|
+
message = nil
|
|
472
|
+
Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: stream_idle_timeout_seconds) do |http|
|
|
473
|
+
cancellation&.on_cancel { close_http(http) }
|
|
474
|
+
cancellation&.raise_if_cancelled!
|
|
475
|
+
http.request(request) do |response|
|
|
476
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
477
|
+
body = +""
|
|
478
|
+
response.read_body { |chunk| body << chunk }
|
|
479
|
+
raise RequestError.new(provider: "OpenAI", code: response.code, body: redact(body, token))
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
message = parse_responses_sse_stream(response, provider: "OpenAI", on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, cancellation: cancellation)
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
cancellation&.raise_if_cancelled!
|
|
486
|
+
attach_response_metadata(message, provider: "OpenAI", model: current_model)
|
|
487
|
+
end
|
|
488
|
+
|
|
340
489
|
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:)
|
|
341
490
|
message = codex_chat(
|
|
342
491
|
url,
|
|
@@ -365,10 +514,14 @@ module Kward
|
|
|
365
514
|
|
|
366
515
|
def chat_anthropic_provider(url:, token:, request_body:, current_model:, on_reasoning_delta:, on_assistant_delta:, cancellation:)
|
|
367
516
|
request = Http.apply_user_agent(Net::HTTP::Post.new(url))
|
|
368
|
-
|
|
517
|
+
if anthropic_api_key
|
|
518
|
+
request["x-api-key"] = token
|
|
519
|
+
else
|
|
520
|
+
request["Authorization"] = "Bearer #{token}"
|
|
521
|
+
end
|
|
369
522
|
request["Content-Type"] = "application/json"
|
|
370
523
|
request["Accept"] = "text/event-stream"
|
|
371
|
-
anthropic_headers.each { |key, value| request[key] = value }
|
|
524
|
+
anthropic_headers(api_key: !anthropic_api_key.nil?).each { |key, value| request[key] = value }
|
|
372
525
|
request.body = request_body
|
|
373
526
|
|
|
374
527
|
message = nil
|
|
@@ -414,7 +567,7 @@ module Kward
|
|
|
414
567
|
attach_response_metadata(message, provider: "Local", model: current_model)
|
|
415
568
|
end
|
|
416
569
|
|
|
417
|
-
def chat_openrouter_provider(url:, token:, request_body:, current_model:, on_assistant_delta:, cancellation:)
|
|
570
|
+
def chat_openrouter_provider(url:, token:, provider: "OpenRouter", request_body:, current_model:, on_assistant_delta:, cancellation:)
|
|
418
571
|
request = Http.apply_user_agent(Net::HTTP::Post.new(url))
|
|
419
572
|
request["Authorization"] = "Bearer #{token}"
|
|
420
573
|
request["Content-Type"] = "application/json"
|
|
@@ -428,18 +581,23 @@ module Kward
|
|
|
428
581
|
cancellation&.raise_if_cancelled!
|
|
429
582
|
|
|
430
583
|
unless response.is_a?(Net::HTTPSuccess)
|
|
431
|
-
|
|
584
|
+
body = provider == "OpenRouter" ? response.body : redact(response.body, token)
|
|
585
|
+
raise RequestError.new(provider: provider, code: response.code, body: body)
|
|
432
586
|
end
|
|
433
587
|
|
|
434
588
|
body = JSON.parse(response.body)
|
|
435
589
|
message = body.fetch("choices").first.fetch("message")
|
|
436
590
|
cancellation&.raise_if_cancelled!
|
|
437
591
|
on_assistant_delta&.call(message.fetch("content", ""))
|
|
438
|
-
attach_response_metadata(message, provider:
|
|
592
|
+
attach_response_metadata(message, provider: provider, model: current_model, usage: normalized_usage(body["usage"]))
|
|
439
593
|
end
|
|
440
594
|
|
|
441
595
|
def auth_error_for(provider)
|
|
442
596
|
case provider
|
|
597
|
+
when "Azure OpenAI"
|
|
598
|
+
AZURE_OPENAI_AUTH_ERROR
|
|
599
|
+
when "OpenAI"
|
|
600
|
+
OPENAI_API_AUTH_ERROR
|
|
443
601
|
when "OpenRouter"
|
|
444
602
|
OPENROUTER_AUTH_ERROR
|
|
445
603
|
when "Copilot"
|
|
@@ -531,7 +689,13 @@ module Kward
|
|
|
531
689
|
def request_body_payload(provider, messages, tools, max_tokens: nil, model: nil, reasoning: nil)
|
|
532
690
|
reasoning = false unless ModelInfo.reasoning_supported?(provider, model)
|
|
533
691
|
|
|
534
|
-
if provider == "
|
|
692
|
+
if provider == "Azure OpenAI"
|
|
693
|
+
request_payload(provider, messages, tools, max_tokens: max_tokens, model: model, reasoning: false).merge(stream: true, stream_options: { include_usage: true })
|
|
694
|
+
elsif provider == "Google Gemini"
|
|
695
|
+
gemini_payload(messages, tools, max_tokens: max_tokens, model: model)
|
|
696
|
+
elsif provider == "OpenAI"
|
|
697
|
+
openai_responses_payload(messages, tools, max_tokens: max_tokens, model: model, reasoning: reasoning)
|
|
698
|
+
elsif provider == "Codex"
|
|
535
699
|
codex_payload(messages, tools, max_tokens: max_tokens, model: model, reasoning: reasoning)
|
|
536
700
|
elsif provider == "Anthropic"
|
|
537
701
|
anthropic_payload(messages, tools, max_tokens: max_tokens, model: model, reasoning: reasoning)
|
|
@@ -546,12 +710,15 @@ module Kward
|
|
|
546
710
|
model.to_s.match?(/\Agpt-5(?:\.|-|\z)/)
|
|
547
711
|
end
|
|
548
712
|
|
|
549
|
-
def model_entry(provider, id, current: false)
|
|
713
|
+
def model_entry(provider, id, current: false, metadata: nil)
|
|
714
|
+
metadata ||= {}
|
|
550
715
|
{
|
|
551
716
|
provider: provider,
|
|
552
717
|
id: id,
|
|
553
718
|
current: current,
|
|
554
|
-
contextWindow: context_window_for(provider, id)
|
|
719
|
+
contextWindow: metadata["contextWindow"] || metadata[:contextWindow] || context_window_for(provider, id),
|
|
720
|
+
supportedParameters: metadata["supportedParameters"] || metadata[:supportedParameters],
|
|
721
|
+
source: metadata["source"] || metadata[:source]
|
|
555
722
|
}.compact
|
|
556
723
|
end
|
|
557
724
|
|
|
@@ -707,13 +874,15 @@ module Kward
|
|
|
707
874
|
ModelStreamParser.parse_openai_chat_sse_stream(response, on_assistant_delta: on_assistant_delta, cancellation: cancellation, usage_normalizer: method(:normalized_usage), provider_label: provider_label)
|
|
708
875
|
end
|
|
709
876
|
|
|
710
|
-
def anthropic_headers
|
|
711
|
-
{
|
|
712
|
-
|
|
877
|
+
def anthropic_headers(api_key: false)
|
|
878
|
+
headers = { "anthropic-version" => "2023-06-01" }
|
|
879
|
+
return headers if api_key
|
|
880
|
+
|
|
881
|
+
headers.merge(
|
|
713
882
|
"anthropic-beta" => "claude-code-20250219,oauth-2025-04-20",
|
|
714
883
|
"anthropic-dangerous-direct-browser-access" => "true",
|
|
715
884
|
"x-app" => "cli"
|
|
716
|
-
|
|
885
|
+
)
|
|
717
886
|
end
|
|
718
887
|
|
|
719
888
|
def copilot_headers(messages)
|
|
@@ -783,7 +952,11 @@ module Kward
|
|
|
783
952
|
end
|
|
784
953
|
|
|
785
954
|
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil)
|
|
786
|
-
|
|
955
|
+
parse_responses_sse_stream(response, provider: "Codex", on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, cancellation: cancellation)
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
def parse_responses_sse_stream(response, provider:, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil)
|
|
959
|
+
ModelStreamParser.parse_codex_sse_stream(response, provider_label: provider, 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)
|
|
787
960
|
end
|
|
788
961
|
|
|
789
962
|
def parse_anthropic_sse_stream(response, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil)
|
|
@@ -869,6 +1042,25 @@ module Kward
|
|
|
869
1042
|
return [OPENROUTER_URL, openrouter_api_key, provider, nil]
|
|
870
1043
|
end
|
|
871
1044
|
|
|
1045
|
+
if provider == "OpenAI"
|
|
1046
|
+
runtime = ProviderCatalog.runtime("openai")
|
|
1047
|
+
return [URI(runtime.chat_url), @api_key_store.fetch("openai"), provider, nil]
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
if provider == "Google Gemini"
|
|
1051
|
+
runtime = ProviderCatalog.runtime("gemini")
|
|
1052
|
+
return [URI(runtime.chat_url), @api_key_store.fetch("gemini"), provider, nil]
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
if provider == "Azure OpenAI"
|
|
1056
|
+
return [azure_openai_config.chat_completions_url, @api_key_store.fetch("azure_openai"), provider, nil]
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
if (catalog_provider = ProviderCatalog.find_by_name(provider)) && catalog_provider.protocol == "openai_chat"
|
|
1060
|
+
runtime = ProviderCatalog.runtime(catalog_provider.id)
|
|
1061
|
+
return [URI(runtime.chat_url), @api_key_store.fetch(catalog_provider.id), provider, nil]
|
|
1062
|
+
end
|
|
1063
|
+
|
|
872
1064
|
openai_token = @openai_access_token || @oauth.access_token
|
|
873
1065
|
if openai_token
|
|
874
1066
|
[CODEX_URL, openai_token, "Codex", @oauth.respond_to?(:account_id) ? @oauth.account_id : nil]
|
|
@@ -904,7 +1096,8 @@ module Kward
|
|
|
904
1096
|
when "Anthropic"
|
|
905
1097
|
!anthropic_access_token.to_s.empty?
|
|
906
1098
|
else
|
|
907
|
-
|
|
1099
|
+
catalog_provider = ProviderCatalog.find_by_name(provider)
|
|
1100
|
+
catalog_provider&.api_key? && !@api_key_store.fetch(catalog_provider.id).to_s.empty?
|
|
908
1101
|
end
|
|
909
1102
|
rescue StandardError
|
|
910
1103
|
false
|
|
@@ -964,7 +1157,7 @@ module Kward
|
|
|
964
1157
|
end
|
|
965
1158
|
|
|
966
1159
|
def openrouter_api_key
|
|
967
|
-
@openrouter_api_key || config_value("openrouter_api_key")
|
|
1160
|
+
@openrouter_api_key || @api_key_store.stored_value("openrouter") || config_value("openrouter_api_key")
|
|
968
1161
|
end
|
|
969
1162
|
|
|
970
1163
|
def github_access_token
|
|
@@ -972,13 +1165,25 @@ module Kward
|
|
|
972
1165
|
end
|
|
973
1166
|
|
|
974
1167
|
def anthropic_access_token
|
|
975
|
-
@anthropic_oauth.access_token
|
|
1168
|
+
anthropic_api_key || @anthropic_oauth.access_token
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
def anthropic_api_key
|
|
1172
|
+
@api_key_store.fetch("anthropic")
|
|
976
1173
|
end
|
|
977
1174
|
|
|
978
1175
|
def copilot_chat_url
|
|
979
1176
|
URI("#{@github_oauth.base_url}/chat/completions")
|
|
980
1177
|
end
|
|
981
1178
|
|
|
1179
|
+
def azure_openai_config(deployment: nil)
|
|
1180
|
+
AzureOpenAIConfig.new(
|
|
1181
|
+
endpoint: config_value("azure_openai_endpoint"),
|
|
1182
|
+
deployment: deployment || config_value("azure_openai_model"),
|
|
1183
|
+
api_version: config_value("azure_openai_api_version")
|
|
1184
|
+
)
|
|
1185
|
+
end
|
|
1186
|
+
|
|
982
1187
|
def configured_provider
|
|
983
1188
|
value = ENV["KWARD_PROVIDER"].to_s.strip
|
|
984
1189
|
value = config_value("provider") if value.empty?
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "../config_files"
|
|
2
|
+
require_relative "provider_catalog"
|
|
2
3
|
|
|
3
4
|
# Namespace for the Kward CLI agent runtime.
|
|
4
5
|
module Kward
|
|
@@ -127,6 +128,17 @@ module Kward
|
|
|
127
128
|
def model_for(provider, config:, override_model: nil, env: ENV)
|
|
128
129
|
return override_model if override_model
|
|
129
130
|
|
|
131
|
+
if provider_label(provider) == "OpenAI"
|
|
132
|
+
runtime = ProviderCatalog.runtime("openai")
|
|
133
|
+
return env["OPENAI_API_MODEL"] || ConfigFiles.config_value(config, runtime.model_config_key) || DEFAULT_OPENAI_MODEL
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
catalog_provider = ProviderCatalog.find_by_name(provider) || ProviderCatalog.find(provider.to_s.downcase)
|
|
137
|
+
if catalog_provider && !["anthropic", "openrouter", "copilot"].include?(catalog_provider.id)
|
|
138
|
+
runtime = ProviderCatalog.runtime(catalog_provider.id)
|
|
139
|
+
return env["#{catalog_provider.id.upcase}_MODEL"] || ConfigFiles.config_value(config, runtime.model_config_key, "model")
|
|
140
|
+
end
|
|
141
|
+
|
|
130
142
|
case provider
|
|
131
143
|
when "Local"
|
|
132
144
|
env["KWARD_LOCAL_MODEL"] || ConfigFiles.config_value(config, "local_model")
|
|
@@ -176,6 +188,11 @@ module Kward
|
|
|
176
188
|
end
|
|
177
189
|
|
|
178
190
|
def reasoning_effort(config:, env: ENV, provider: nil)
|
|
191
|
+
if provider_label(provider) == "OpenAI"
|
|
192
|
+
runtime = ProviderCatalog.runtime("openai")
|
|
193
|
+
return env["OPENAI_API_REASONING_EFFORT"] || ConfigFiles.config_value(config, runtime.reasoning_config_key) || DEFAULT_REASONING_EFFORT
|
|
194
|
+
end
|
|
195
|
+
|
|
179
196
|
case provider.to_s
|
|
180
197
|
when "Local"
|
|
181
198
|
nil
|
|
@@ -191,6 +208,11 @@ module Kward
|
|
|
191
208
|
end
|
|
192
209
|
|
|
193
210
|
def provider_label(provider)
|
|
211
|
+
return "OpenAI" if provider.to_s == "OpenAI" || provider.to_s.downcase == "openai_api"
|
|
212
|
+
|
|
213
|
+
catalog_provider = ProviderCatalog.find(provider.to_s.downcase) || ProviderCatalog.find_by_name(provider)
|
|
214
|
+
return catalog_provider.name if catalog_provider && !["openai", "copilot"].include?(catalog_provider.id)
|
|
215
|
+
|
|
194
216
|
case provider.to_s.downcase
|
|
195
217
|
when "openrouter" then "OpenRouter"
|
|
196
218
|
when "copilot" then "Copilot"
|
|
@@ -202,6 +224,11 @@ module Kward
|
|
|
202
224
|
end
|
|
203
225
|
|
|
204
226
|
def provider_config_value(provider)
|
|
227
|
+
return ProviderCatalog.runtime("openai").id if provider_label(provider) == "OpenAI"
|
|
228
|
+
|
|
229
|
+
catalog_provider = ProviderCatalog.find(provider.to_s.downcase) || ProviderCatalog.find_by_name(provider)
|
|
230
|
+
return ProviderCatalog.runtime(catalog_provider.id).id if catalog_provider && catalog_provider.id != "openai"
|
|
231
|
+
|
|
205
232
|
case provider.to_s.downcase
|
|
206
233
|
when "openrouter" then "openrouter"
|
|
207
234
|
when "copilot" then "copilot"
|
|
@@ -212,6 +239,11 @@ module Kward
|
|
|
212
239
|
end
|
|
213
240
|
|
|
214
241
|
def config_key_for_provider(provider)
|
|
242
|
+
return ProviderCatalog.runtime("openai").model_config_key if provider_label(provider) == "OpenAI"
|
|
243
|
+
|
|
244
|
+
catalog_provider = ProviderCatalog.find(provider.to_s.downcase) || ProviderCatalog.find_by_name(provider)
|
|
245
|
+
return ProviderCatalog.runtime(catalog_provider.id).model_config_key if catalog_provider && catalog_provider.id != "openai"
|
|
246
|
+
|
|
215
247
|
case provider.to_s.downcase
|
|
216
248
|
when "openrouter" then "openrouter_model"
|
|
217
249
|
when "copilot" then "copilot_model"
|
|
@@ -222,6 +254,11 @@ module Kward
|
|
|
222
254
|
end
|
|
223
255
|
|
|
224
256
|
def reasoning_config_key_for_provider(provider)
|
|
257
|
+
return ProviderCatalog.runtime("openai").reasoning_config_key if provider_label(provider) == "OpenAI"
|
|
258
|
+
|
|
259
|
+
catalog_provider = ProviderCatalog.find(provider.to_s.downcase) || ProviderCatalog.find_by_name(provider)
|
|
260
|
+
return ProviderCatalog.runtime(catalog_provider.id).reasoning_config_key if catalog_provider && catalog_provider.id != "openai"
|
|
261
|
+
|
|
225
262
|
case provider.to_s.downcase
|
|
226
263
|
when "openrouter" then "openrouter_reasoning_effort"
|
|
227
264
|
when "copilot" then "copilot_reasoning_effort"
|
|
@@ -244,6 +281,16 @@ module Kward
|
|
|
244
281
|
|
|
245
282
|
def context_window(provider, id, openrouter_models: nil)
|
|
246
283
|
case provider
|
|
284
|
+
when "Azure OpenAI"
|
|
285
|
+
pattern_context_window(OPENAI_CONTEXT_WINDOWS, id) ||
|
|
286
|
+
conservative_unknown_context_window(id)
|
|
287
|
+
when "Google Gemini"
|
|
288
|
+
pattern_context_window(GEMINI_CONTEXT_WINDOWS, id.to_s.delete_prefix("models/")) ||
|
|
289
|
+
conservative_unknown_context_window(id)
|
|
290
|
+
when "OpenAI"
|
|
291
|
+
pattern_context_window(OPENAI_CONTEXT_WINDOWS, id) ||
|
|
292
|
+
conservative_context_window(id) ||
|
|
293
|
+
conservative_unknown_context_window(id)
|
|
247
294
|
when "Codex"
|
|
248
295
|
pattern_context_window(CODEX_CONTEXT_WINDOWS, id) ||
|
|
249
296
|
openrouter_inferred_context_window(provider, id, openrouter_models: openrouter_models) ||
|
|
@@ -427,6 +474,8 @@ module Kward
|
|
|
427
474
|
reasoning: reasoning,
|
|
428
475
|
reasoningEffort: reasoning_effort,
|
|
429
476
|
contextWindow: model["contextWindow"] || model["context_window"] || context_window(provider, id),
|
|
477
|
+
supportedParameters: model["supportedParameters"] || model["supported_parameters"],
|
|
478
|
+
source: model["source"],
|
|
430
479
|
current: boolean_value(model["current"], default: provider == current_provider && id == current_model)
|
|
431
480
|
}.compact
|
|
432
481
|
end
|
data/lib/kward/model/payloads.rb
CHANGED
|
@@ -114,8 +114,67 @@ module Kward
|
|
|
114
114
|
payload
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
+
def gemini_payload(messages, tools, max_tokens: nil, model: nil)
|
|
118
|
+
system_parts = messages.filter_map do |message|
|
|
119
|
+
plain_content(MessageAccess.content(message)).to_s if MessageAccess.role(message).to_s == "system"
|
|
120
|
+
end.reject(&:empty?).map { |text| { text: text } }
|
|
121
|
+
payload = {
|
|
122
|
+
contents: messages.filter_map { |message| gemini_message(message) },
|
|
123
|
+
tools: tools.empty? ? nil : [{ functionDeclarations: tools.map { |tool| gemini_tool_schema(tool) } }]
|
|
124
|
+
}.compact
|
|
125
|
+
payload[:systemInstruction] = { parts: system_parts } unless system_parts.empty?
|
|
126
|
+
payload[:generationConfig] = { maxOutputTokens: max_tokens.to_i } if max_tokens.to_i.positive?
|
|
127
|
+
payload
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def gemini_message(message)
|
|
131
|
+
role = MessageAccess.role(message).to_s
|
|
132
|
+
content = MessageAccess.content(message)
|
|
133
|
+
case role
|
|
134
|
+
when "system"
|
|
135
|
+
nil
|
|
136
|
+
when "assistant", "compactionSummary"
|
|
137
|
+
parts = []
|
|
138
|
+
text = role == "compactionSummary" ? MessageAccess.summary(message) || content.to_s : plain_content(content).to_s
|
|
139
|
+
parts << { text: text } unless text.empty?
|
|
140
|
+
MessageAccess.tool_calls(message).each do |tool_call|
|
|
141
|
+
function = ToolCall.function(tool_call)
|
|
142
|
+
parts << { functionCall: { name: ToolCall.value(function, :name), args: parse_tool_arguments(ToolCall.value(function, :arguments)) } }
|
|
143
|
+
end
|
|
144
|
+
{ role: "model", parts: parts }
|
|
145
|
+
when "tool", "toolResult"
|
|
146
|
+
name = MessageAccess.tool_name(message) || MessageAccess.tool_call_id(message) || "tool"
|
|
147
|
+
response = JSON.parse(plain_content(content).to_s)
|
|
148
|
+
response = { "result" => response } unless response.is_a?(Hash)
|
|
149
|
+
{ role: "user", parts: [{ functionResponse: { name: name, response: response } }] }
|
|
150
|
+
else
|
|
151
|
+
{ role: "user", parts: [{ text: plain_content(content).to_s }] }
|
|
152
|
+
end
|
|
153
|
+
rescue JSON::ParserError
|
|
154
|
+
{ role: "user", parts: [{ functionResponse: { name: name, response: { result: plain_content(content).to_s } } }] }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def gemini_tool_schema(tool)
|
|
158
|
+
function = ToolCall.function(tool)
|
|
159
|
+
{
|
|
160
|
+
name: ToolCall.value(function, :name),
|
|
161
|
+
description: ToolCall.value(function, :description) || "",
|
|
162
|
+
parameters: ToolCall.value(function, :parameters) || {}
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
117
166
|
def codex_payload(messages, tools, max_tokens: nil, model: nil, reasoning: nil)
|
|
118
|
-
|
|
167
|
+
responses_payload("Codex", messages, tools, model: model, reasoning: reasoning)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def openai_responses_payload(messages, tools, max_tokens: nil, model: nil, reasoning: nil)
|
|
171
|
+
payload = responses_payload("OpenAI", messages, tools, model: model, reasoning: reasoning)
|
|
172
|
+
payload[:max_output_tokens] = max_tokens.to_i if max_tokens.to_i.positive?
|
|
173
|
+
payload
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def responses_payload(provider, messages, tools, model:, reasoning:)
|
|
177
|
+
parts = build_context_parts(provider, messages, tools, model: model)
|
|
119
178
|
payload = {
|
|
120
179
|
model: parts[:model],
|
|
121
180
|
instructions: parts[:instructions],
|
|
@@ -128,10 +187,10 @@ module Kward
|
|
|
128
187
|
include: []
|
|
129
188
|
}
|
|
130
189
|
unless reasoning == false
|
|
131
|
-
effort = reasoning || reasoning_effort(
|
|
190
|
+
effort = reasoning || reasoning_effort(provider)
|
|
132
191
|
payload[:reasoning] = { effort: effort, summary: "auto" }
|
|
133
192
|
# TODO: Remove this Luna-specific Responses Lite workaround when Codex accepts Kward's own client identity.
|
|
134
|
-
payload[:reasoning][:context] = "all_turns" if parts[:model] == "gpt-5.6-luna"
|
|
193
|
+
payload[:reasoning][:context] = "all_turns" if provider == "Codex" && parts[:model] == "gpt-5.6-luna"
|
|
135
194
|
end
|
|
136
195
|
payload
|
|
137
196
|
end
|
|
@@ -161,7 +220,7 @@ module Kward
|
|
|
161
220
|
input: input,
|
|
162
221
|
tools: tools.map { |tool| codex_tool_schema(tool) }
|
|
163
222
|
}
|
|
164
|
-
elsif
|
|
223
|
+
elsif ["Codex", "OpenAI"].include?(provider)
|
|
165
224
|
instructions, input = codex_messages(messages)
|
|
166
225
|
{
|
|
167
226
|
provider: provider,
|