kward 0.78.0 → 0.80.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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -2
  3. data/.github/workflows/pages.yml +1 -1
  4. data/CHANGELOG.md +82 -1
  5. data/Gemfile.lock +2 -2
  6. data/README.md +33 -38
  7. data/doc/agent-tools.md +10 -11
  8. data/doc/api.md +3 -3
  9. data/doc/authentication.md +79 -110
  10. data/doc/code-search.md +1 -1
  11. data/doc/composer.md +1 -1
  12. data/doc/configuration.md +136 -27
  13. data/doc/context-budgeting.md +6 -6
  14. data/doc/context-tools.md +2 -2
  15. data/doc/editor.md +7 -7
  16. data/doc/extensibility.md +9 -29
  17. data/doc/files.md +6 -6
  18. data/doc/getting-started.md +3 -3
  19. data/doc/git.md +8 -2
  20. data/doc/lifecycle-hooks.md +5 -4
  21. data/doc/local-models.md +130 -0
  22. data/doc/mcp.md +5 -5
  23. data/doc/permissions.md +15 -5
  24. data/doc/plugins.md +39 -4
  25. data/doc/prompt-templates.md +105 -0
  26. data/doc/providers.md +140 -0
  27. data/doc/releasing.md +11 -3
  28. data/doc/rpc.md +65 -31
  29. data/doc/sandboxing.md +142 -0
  30. data/doc/security.md +31 -10
  31. data/doc/session-management.md +2 -4
  32. data/doc/skills.md +10 -0
  33. data/doc/tabs.md +37 -0
  34. data/doc/telegram.md +138 -0
  35. data/doc/transports.md +209 -0
  36. data/doc/usage.md +11 -3
  37. data/doc/web-search.md +3 -3
  38. data/doc/workspace-tools.md +4 -4
  39. data/examples/plugins/stardate_footer.rb +10 -0
  40. data/examples/plugins/telegram/plugin.rb +39 -0
  41. data/examples/plugins/telegram/telegram_api.rb +136 -0
  42. data/examples/plugins/telegram/telegram_transport.rb +304 -0
  43. data/kward.gemspec +1 -1
  44. data/lib/kward/ansi.rb +1 -0
  45. data/lib/kward/auth/api_key_store.rb +99 -0
  46. data/lib/kward/cli/auth_commands.rb +72 -13
  47. data/lib/kward/cli/commands.rb +7 -0
  48. data/lib/kward/cli/doctor.rb +21 -0
  49. data/lib/kward/cli/git.rb +31 -8
  50. data/lib/kward/cli/plugins.rb +1 -0
  51. data/lib/kward/cli/prompt_interface.rb +7 -0
  52. data/lib/kward/cli/rendering.rb +6 -2
  53. data/lib/kward/cli/runtime_helpers.rb +4 -2
  54. data/lib/kward/cli/sessions.rb +1 -1
  55. data/lib/kward/cli/settings.rb +162 -36
  56. data/lib/kward/cli/slash_commands.rb +109 -1
  57. data/lib/kward/cli/tabs.rb +102 -37
  58. data/lib/kward/cli/transports.rb +67 -0
  59. data/lib/kward/cli/worktrees.rb +360 -0
  60. data/lib/kward/cli.rb +22 -1
  61. data/lib/kward/config_files.rb +106 -4
  62. data/lib/kward/conversation.rb +27 -8
  63. data/lib/kward/git_worktree_manager.rb +291 -0
  64. data/lib/kward/local_command_runner.rb +2 -2
  65. data/lib/kward/model/azure_openai_config.rb +54 -0
  66. data/lib/kward/model/catalog.rb +113 -0
  67. data/lib/kward/model/client.rb +364 -23
  68. data/lib/kward/model/model_info.rb +60 -1
  69. data/lib/kward/model/payloads.rb +64 -4
  70. data/lib/kward/model/provider_catalog.rb +114 -0
  71. data/lib/kward/model/sources.rb +70 -0
  72. data/lib/kward/model/stream_parser.rb +141 -39
  73. data/lib/kward/pan/index.html.erb +50 -0
  74. data/lib/kward/pan/server.rb +49 -2
  75. data/lib/kward/permissions/policy.rb +18 -4
  76. data/lib/kward/plugin_chat_runtime.rb +374 -0
  77. data/lib/kward/plugin_registry.rb +81 -8
  78. data/lib/kward/private_file.rb +9 -3
  79. data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
  80. data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
  81. data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
  82. data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
  83. data/lib/kward/prompt_interface/editor/controller.rb +43 -10
  84. data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
  85. data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
  86. data/lib/kward/prompt_interface/editor/state.rb +3 -9
  87. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +184 -9
  88. data/lib/kward/prompt_interface/file_overlay.rb +2 -2
  89. data/lib/kward/prompt_interface/git_prompt.rb +7 -3
  90. data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
  91. data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
  92. data/lib/kward/prompt_interface/interactive/state.rb +2 -2
  93. data/lib/kward/prompt_interface/project_browser.rb +25 -6
  94. data/lib/kward/prompt_interface/runtime_state.rb +22 -6
  95. data/lib/kward/prompt_interface/screen.rb +3 -0
  96. data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
  97. data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
  98. data/lib/kward/prompt_interface.rb +71 -12
  99. data/lib/kward/prompts/commands.rb +3 -1
  100. data/lib/kward/prompts.rb +22 -10
  101. data/lib/kward/rpc/auth_manager.rb +112 -152
  102. data/lib/kward/rpc/config_manager.rb +46 -6
  103. data/lib/kward/rpc/plugin_chat_manager.rb +56 -194
  104. data/lib/kward/rpc/prompt_bridge.rb +8 -4
  105. data/lib/kward/rpc/redactor.rb +1 -1
  106. data/lib/kward/rpc/server.rb +102 -14
  107. data/lib/kward/rpc/session_manager.rb +151 -38
  108. data/lib/kward/sandbox/capabilities.rb +39 -0
  109. data/lib/kward/sandbox/command_runner.rb +28 -0
  110. data/lib/kward/sandbox/environment.rb +24 -0
  111. data/lib/kward/sandbox/linux_bubblewrap_runner.rb +71 -0
  112. data/lib/kward/sandbox/macos_seatbelt_runner.rb +96 -0
  113. data/lib/kward/sandbox/passthrough_runner.rb +13 -0
  114. data/lib/kward/sandbox/policy.rb +74 -0
  115. data/lib/kward/sandbox/runner_factory.rb +55 -0
  116. data/lib/kward/sandbox/unavailable_runner.rb +21 -0
  117. data/lib/kward/sandbox.rb +9 -0
  118. data/lib/kward/session_store.rb +110 -24
  119. data/lib/kward/skills/capture.rb +144 -0
  120. data/lib/kward/tab_driver.rb +6 -3
  121. data/lib/kward/tools/git_commit.rb +39 -0
  122. data/lib/kward/tools/registry.rb +26 -6
  123. data/lib/kward/tools/tool_call.rb +1 -0
  124. data/lib/kward/transport/gateway.rb +253 -0
  125. data/lib/kward/transport/host.rb +343 -0
  126. data/lib/kward/transport/manager.rb +179 -0
  127. data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
  128. data/lib/kward/transport/runtime.rb +41 -0
  129. data/lib/kward/transport/store.rb +101 -0
  130. data/lib/kward/transport.rb +204 -0
  131. data/lib/kward/version.rb +1 -1
  132. data/lib/kward/workspace.rb +18 -3
  133. data/lib/kward/workspace_factory.rb +34 -0
  134. data/templates/default/fulldoc/html/js/kward.js +3 -0
  135. data/templates/default/kward_navigation.rb +8 -1
  136. data/templates/default/layout/html/setup.rb +5 -0
  137. metadata +41 -2
@@ -4,6 +4,7 @@ require_relative "../auth/anthropic_oauth"
4
4
  require_relative "../auth/github_oauth"
5
5
  require_relative "../auth/openai_oauth"
6
6
  require_relative "../model/client"
7
+ require_relative "../model/provider_catalog"
7
8
  require_relative "config_manager"
8
9
 
9
10
  # Namespace for the Kward CLI agent runtime.
@@ -25,45 +26,44 @@ module Kward
25
26
  end
26
27
 
27
28
  def status
28
- oauth = @oauth_factory.call
29
- config = stored_config
29
+ provider_status = providers.fetch(:providers)
30
30
  {
31
- openaiOAuth: oauth.logged_in?,
32
- openaiAccountId: oauth.respond_to?(:account_id) ? oauth.account_id : nil,
33
- openrouterApiKey: !ENV["OPENROUTER_API_KEY"].to_s.empty? || !config["openrouter_api_key"].to_s.empty?,
31
+ providers: provider_status,
32
+ openaiOAuth: oauth_status("openai")[:configured],
33
+ openrouterApiKey: @config_manager.api_key_status("openrouter")[:configured],
34
34
  openaiAccessToken: !ENV["OPENAI_ACCESS_TOKEN"].to_s.empty?,
35
- anthropicOAuth: @anthropic_oauth_factory.call.logged_in?,
36
- githubOAuth: @github_oauth_factory.call.logged_in?
35
+ anthropicOAuth: oauth_status("anthropic")[:configured],
36
+ githubOAuth: oauth_status("copilot")[:configured]
37
37
  }
38
38
  rescue StandardError => e
39
- { openaiOAuth: false, error: e.message }
39
+ { providers: [], openaiOAuth: false, error: e.message }
40
40
  end
41
41
 
42
42
  def providers
43
- { providers: [openai_provider, anthropic_provider, openrouter_provider, github_provider] }
43
+ { providers: ProviderCatalog.all.map { |provider| provider_payload(provider) } }
44
44
  end
45
45
 
46
- def login_with_api_key(provider_id:, api_key:)
47
- provider_id = provider_id.to_s
48
- @config_manager.set_api_key(provider_id, api_key)
49
- { providerId: provider_id, message: "Saved API key for #{provider_name(provider_id)}." }
46
+ def login_with_api_key(provider_id:, api_key:, configuration: nil)
47
+ raise ArgumentError, "API key must be a non-empty string" if api_key.to_s.strip.empty?
48
+
49
+ provider = ProviderCatalog.fetch(provider_id)
50
+ @config_manager.configure_azure_openai(configuration || {}) if provider.id == "azure_openai"
51
+ @config_manager.set_api_key(provider.id, api_key)
52
+ { providerId: provider.id, authMethod: "api_key", configured: true, message: "Saved API key for #{provider.name}." }
50
53
  end
51
54
 
52
- def logout_provider(provider_id:)
53
- provider_id = provider_id.to_s
54
- case provider_id
55
- when "openai"
56
- logout_openai
57
- { providerId: provider_id, message: "Logged out of OpenAI." }
58
- when "anthropic"
59
- logout_anthropic
60
- { providerId: provider_id, message: "Logged out of Anthropic." }
61
- when "openrouter"
62
- @config_manager.delete_key("openrouter_api_key")
63
- { providerId: provider_id, message: "Logged out of OpenRouter." }
64
- else
65
- raise "Unsupported auth provider: #{provider_id}"
55
+ def logout_provider(provider_id:, auth_method: nil)
56
+ provider = ProviderCatalog.fetch(provider_id)
57
+ auth_method = auth_method.to_s unless auth_method.nil?
58
+ removed = false
59
+ if provider.api_key? && (auth_method.nil? || auth_method == "api_key")
60
+ removed = @config_manager.delete_api_key(provider.id) || removed
61
+ end
62
+ if provider.oauth? && (auth_method.nil? || auth_method == "oauth")
63
+ removed = logout_oauth_provider(provider.id) || removed
66
64
  end
65
+
66
+ { providerId: provider.id, authMethod: auth_method, removed: removed, message: "Logged out of #{provider.name}." }.compact
67
67
  end
68
68
 
69
69
  def login_with_oauth(provider_id:, timeout_seconds: 120)
@@ -73,8 +73,12 @@ module Kward
73
73
  start_oauth_login(provider_id: "openai", oauth: @oauth_factory.call, timeout_seconds: timeout_seconds)
74
74
  when "anthropic"
75
75
  start_oauth_login(provider_id: "anthropic", oauth: @anthropic_oauth_factory.call, timeout_seconds: timeout_seconds)
76
- when "github"
77
- raise "GitHub OAuth is supported in the CLI with `ruby lib/main.rb login github`, but RPC browser login is not implemented yet."
76
+ when "github", "copilot"
77
+ raise "GitHub Copilot OAuth is unavailable over RPC; use the interactive CLI login."
78
+ when "openrouter"
79
+ raise "OpenRouter OAuth is unavailable because Kward has not implemented OpenRouter's official PKCE flow."
80
+ when "xai"
81
+ raise "xAI OAuth is unavailable because no official stable third-party flow is supported."
78
82
  else
79
83
  raise "Unsupported OAuth provider: #{provider_id}"
80
84
  end
@@ -121,144 +125,100 @@ module Kward
121
125
 
122
126
  private
123
127
 
124
- def fetch_login(login_id)
125
- @mutex.synchronize { @logins[login_id.to_s] } || raise("Unknown login: #{login_id}")
126
- end
127
-
128
- def stored_config
129
- @config_manager.read(redacted: false)
130
- rescue StandardError
131
- {}
132
- end
133
-
134
- def openai_provider
135
- oauth = @oauth_factory.call
136
- env_configured = !ENV["OPENAI_ACCESS_TOKEN"].to_s.empty?
137
- stored_configured = oauth.logged_in?
138
- provider = {
139
- id: "openai",
140
- name: "OpenAI",
141
- authType: "oauth",
142
- configured: env_configured || stored_configured,
143
- storedCredentialType: "oauth",
144
- canLogout: stored_configured,
145
- usesCallbackServer: true
146
- }
147
- provider[:source] = env_configured ? "environment" : "stored" if provider[:configured]
148
- provider[:label] = provider[:configured] ? "Signed in" : "Not signed in"
149
- provider
150
- rescue StandardError
128
+ def provider_payload(provider)
129
+ methods = []
130
+ if provider.api_key?
131
+ status = @config_manager.api_key_status(provider.id)
132
+ methods << {
133
+ id: "api_key",
134
+ supported: true,
135
+ configured: status[:configured],
136
+ source: status[:source],
137
+ canLogout: status[:canLogout]
138
+ }.compact
139
+ end
140
+ methods << oauth_method_payload(provider) if provider.oauth?
141
+ configured_methods = methods.select { |method| method[:configured] }
142
+ preferred = methods.find { |method| method[:id] == "oauth" } || methods.first
151
143
  {
152
- id: "openai",
153
- name: "OpenAI",
154
- authType: "oauth",
155
- configured: !ENV["OPENAI_ACCESS_TOKEN"].to_s.empty?,
156
- source: (!ENV["OPENAI_ACCESS_TOKEN"].to_s.empty? ? "environment" : nil),
157
- label: (!ENV["OPENAI_ACCESS_TOKEN"].to_s.empty? ? "Signed in" : "Not signed in"),
158
- storedCredentialType: "oauth",
159
- canLogout: false,
160
- usesCallbackServer: true
144
+ id: provider.id,
145
+ runtimeId: ProviderCatalog.runtime(provider.id).id,
146
+ name: provider.name,
147
+ authType: preferred&.fetch(:id, nil),
148
+ authMethods: methods,
149
+ configured: !configured_methods.empty?,
150
+ source: configured_methods.find { |method| method[:source] == "environment" }&.fetch(:source, nil) || configured_methods.first&.fetch(:source, nil),
151
+ canLogout: configured_methods.any? { |method| method[:canLogout] },
152
+ usesCallbackServer: methods.any? { |method| method[:usesCallbackServer] },
153
+ label: configured_methods.empty? ? "Not configured" : "Configured"
161
154
  }.compact
162
155
  end
163
156
 
164
- def anthropic_provider
165
- oauth = @anthropic_oauth_factory.call
166
- stored_configured = oauth.logged_in?
167
- provider = {
168
- id: "anthropic",
169
- name: "Anthropic",
170
- authType: "oauth",
171
- configured: stored_configured,
172
- storedCredentialType: "oauth",
173
- canLogout: stored_configured,
174
- usesCallbackServer: true
175
- }
176
- provider[:source] = "stored" if provider[:configured]
177
- provider[:label] = provider[:configured] ? "Signed in" : "Not signed in"
178
- provider
179
- rescue StandardError
157
+ def oauth_method_payload(provider)
158
+ status = oauth_status(provider.id)
159
+ supported = ["openai", "anthropic"].include?(provider.id)
160
+ reason = unless supported
161
+ case provider.id
162
+ when "openrouter"
163
+ "Official PKCE is documented, but Kward has not implemented it."
164
+ when "xai"
165
+ "No official stable third-party OAuth flow is available."
166
+ else
167
+ "OAuth login is available only in the interactive CLI."
168
+ end
169
+ end
180
170
  {
181
- id: "anthropic",
182
- name: "Anthropic",
183
- authType: "oauth",
184
- configured: false,
185
- label: "Not signed in",
186
- storedCredentialType: "oauth",
187
- canLogout: false,
188
- usesCallbackServer: true
189
- }
190
- end
191
-
192
- def openrouter_provider
193
- config = stored_config
194
- env_configured = !ENV["OPENROUTER_API_KEY"].to_s.empty?
195
- stored_configured = !config["openrouter_api_key"].to_s.empty?
196
- provider = {
197
- id: "openrouter",
198
- name: "OpenRouter",
199
- authType: "api_key",
200
- configured: env_configured || stored_configured,
201
- canLogout: stored_configured
202
- }
203
- provider[:source] = env_configured ? "environment" : "stored" if provider[:configured]
204
- provider[:storedCredentialType] = "api_key" if stored_configured
205
- provider[:label] = provider[:configured] ? "API key configured" : "API key not configured"
206
- provider
171
+ id: "oauth",
172
+ name: provider.oauth_name,
173
+ supported: supported,
174
+ configured: status[:configured],
175
+ source: status[:source],
176
+ canLogout: status[:canLogout],
177
+ usesCallbackServer: supported,
178
+ reason: reason
179
+ }.compact
207
180
  end
208
181
 
209
- def github_provider
210
- oauth = @github_oauth_factory.call
211
- env_configured = !ENV["COPILOT_GITHUB_TOKEN"].to_s.empty?
212
- stored_configured = oauth.logged_in? && !env_configured
213
- provider = {
214
- id: "github",
215
- name: "GitHub",
216
- authType: "oauth",
217
- configured: env_configured || stored_configured,
218
- storedCredentialType: "oauth",
219
- canLogout: false,
220
- usesCallbackServer: false,
221
- supported: false,
222
- reason: "GitHub OAuth is available in the CLI for Copilot scaffolding; RPC login is not implemented yet."
223
- }
224
- provider[:source] = env_configured ? "environment" : "stored" if provider[:configured]
225
- provider[:label] = provider[:configured] ? "Signed in" : "Not signed in"
226
- provider
182
+ def oauth_status(provider_id)
183
+ case provider_id
184
+ when "openai"
185
+ oauth_credential_status(@oauth_factory.call, env_name: "OPENAI_ACCESS_TOKEN")
186
+ when "anthropic"
187
+ oauth_credential_status(@anthropic_oauth_factory.call)
188
+ when "copilot"
189
+ oauth_credential_status(@github_oauth_factory.call, env_name: "COPILOT_GITHUB_TOKEN", can_logout: false)
190
+ else
191
+ { configured: false, canLogout: false }
192
+ end
227
193
  rescue StandardError
228
- {
229
- id: "github",
230
- name: "GitHub",
231
- authType: "oauth",
232
- configured: false,
233
- label: "Not signed in",
234
- storedCredentialType: "oauth",
235
- canLogout: false,
236
- usesCallbackServer: false,
237
- supported: false,
238
- reason: "GitHub OAuth status unavailable."
239
- }
194
+ { configured: false, canLogout: false }
240
195
  end
241
196
 
242
- def provider_name(provider_id)
243
- case provider_id
244
- when "openrouter" then "OpenRouter"
245
- when "openai" then "OpenAI"
246
- when "anthropic" then "Anthropic"
247
- when "github" then "GitHub"
248
- else provider_id
249
- end
197
+ def oauth_credential_status(oauth, env_name: nil, can_logout: true)
198
+ environment = env_name && !ENV[env_name].to_s.empty?
199
+ stored = oauth.logged_in?
200
+ {
201
+ configured: environment || stored,
202
+ source: environment ? "environment" : (stored ? "stored" : nil),
203
+ canLogout: can_logout && stored
204
+ }.compact
250
205
  end
251
206
 
252
- def logout_openai
253
- oauth = @oauth_factory.call
207
+ def logout_oauth_provider(provider_id)
208
+ oauth = case provider_id
209
+ when "openai" then @oauth_factory.call
210
+ when "anthropic" then @anthropic_oauth_factory.call
211
+ else return false
212
+ end
254
213
  path = oauth.auth_path if oauth.respond_to?(:auth_path)
255
- File.delete(path) if path && File.exist?(path)
214
+ return false unless path && File.exist?(path)
215
+
216
+ File.delete(path)
217
+ true
256
218
  end
257
219
 
258
- def logout_anthropic
259
- oauth = @anthropic_oauth_factory.call
260
- path = oauth.auth_path if oauth.respond_to?(:auth_path)
261
- File.delete(path) if path && File.exist?(path)
220
+ def fetch_login(login_id)
221
+ @mutex.synchronize { @logins[login_id.to_s] } || raise("Unknown login: #{login_id}")
262
222
  end
263
223
 
264
224
  def wait_for_callback(login, timeout_seconds:)
@@ -1,6 +1,9 @@
1
1
  require_relative "../auth/openai_oauth"
2
+ require_relative "../auth/api_key_store"
2
3
  require_relative "../config_files"
3
4
  require_relative "../model/model_info"
5
+ require_relative "../model/provider_catalog"
6
+ require_relative "../model/azure_openai_config"
4
7
  require_relative "redactor"
5
8
 
6
9
  # Namespace for the Kward CLI agent runtime.
@@ -9,8 +12,9 @@ module Kward
9
12
  module RPC
10
13
  # RPC configuration manager for reading and updating user config.
11
14
  class ConfigManager
12
- def initialize(config_path: OpenAIOAuth.default_config_path)
15
+ def initialize(config_path: OpenAIOAuth.default_config_path, api_key_store: nil)
13
16
  @config_path = File.expand_path(config_path)
17
+ @api_key_store = api_key_store || APIKeyStore.new(path: File.join(File.dirname(@config_path), "api_keys.json"), config_path: @config_path)
14
18
  end
15
19
 
16
20
  attr_reader :config_path
@@ -21,6 +25,7 @@ module Kward
21
25
  end
22
26
 
23
27
  def update(values)
28
+ reject_provider_api_keys!(values)
24
29
  Redactor.redact(ConfigFiles.update_config(values, @config_path))
25
30
  end
26
31
 
@@ -39,12 +44,35 @@ module Kward
39
44
  end
40
45
 
41
46
  def set_api_key(provider_id, api_key)
42
- provider_id = provider_id.to_s
43
- api_key = api_key.to_s.strip
44
- raise "API key must be a non-empty string" if api_key.empty?
45
- raise "Unsupported API key provider: #{provider_id}" unless provider_id == "openrouter"
47
+ provider = ProviderCatalog.fetch(provider_id)
48
+ raise "#{provider.name} does not accept an API key" unless provider.api_key?
46
49
 
47
- update("openrouter_api_key" => api_key)
50
+ @api_key_store.store(provider.id, api_key)
51
+ end
52
+
53
+ def api_key_status(provider_id)
54
+ provider = ProviderCatalog.fetch(provider_id)
55
+ configured = @api_key_store.configured?(provider.id)
56
+ stored = @api_key_store.stored?(provider.id)
57
+ {
58
+ configured: configured,
59
+ stored: stored,
60
+ source: configured ? (environment_api_key?(provider) ? "environment" : "stored") : nil,
61
+ canLogout: stored
62
+ }.compact
63
+ end
64
+
65
+ def delete_api_key(provider_id)
66
+ @api_key_store.delete(provider_id)
67
+ end
68
+
69
+ def configure_azure_openai(values)
70
+ setup = AzureOpenAIConfig.new(
71
+ endpoint: values["endpoint"] || values[:endpoint],
72
+ deployment: values["deployment"] || values[:deployment],
73
+ api_version: values["apiVersion"] || values[:apiVersion] || values["api_version"] || values[:api_version]
74
+ )
75
+ update(setup.to_config)
48
76
  end
49
77
 
50
78
  def delete_key(key)
@@ -61,6 +89,18 @@ module Kward
61
89
 
62
90
  private
63
91
 
92
+ def reject_provider_api_keys!(values)
93
+ keys = values.respond_to?(:keys) ? values.keys.map(&:to_s) : []
94
+ private_keys = ProviderCatalog.api_key_providers.map { |provider| "#{provider.id}_api_key" }
95
+ return if (keys & private_keys).empty?
96
+
97
+ raise ArgumentError, "Provider API keys must be saved with auth/loginWithApiKey"
98
+ end
99
+
100
+ def environment_api_key?(provider)
101
+ provider.api_key_env.any? { |name| !ENV[name].to_s.strip.empty? }
102
+ end
103
+
64
104
  def load_config
65
105
  ConfigFiles.read_config(@config_path)
66
106
  end