@calliopelabs/cli 2.3.0 → 2.5.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 (186) hide show
  1. package/README.md +17 -0
  2. package/dist/agents/agent-config-loader.js +1 -1
  3. package/dist/agents/agent-config-presets.js +13 -13
  4. package/dist/agents/agent-config-presets.js.map +1 -1
  5. package/dist/agents/agent-config-types.d.ts +1 -1
  6. package/dist/agents/agent-config-types.d.ts.map +1 -1
  7. package/dist/agents/dynamic-tools.d.ts.map +1 -1
  8. package/dist/agents/dynamic-tools.js +39 -10
  9. package/dist/agents/dynamic-tools.js.map +1 -1
  10. package/dist/agents/sdk-backend.js +1 -1
  11. package/dist/agents/sdk-backend.js.map +1 -1
  12. package/dist/api-server.d.ts +9 -0
  13. package/dist/api-server.d.ts.map +1 -1
  14. package/dist/api-server.js +74 -3
  15. package/dist/api-server.js.map +1 -1
  16. package/dist/auto-checkpoint.d.ts.map +1 -1
  17. package/dist/auto-checkpoint.js +50 -17
  18. package/dist/auto-checkpoint.js.map +1 -1
  19. package/dist/auto-compressor.d.ts.map +1 -1
  20. package/dist/auto-compressor.js +9 -5
  21. package/dist/auto-compressor.js.map +1 -1
  22. package/dist/bin.d.ts +8 -0
  23. package/dist/bin.d.ts.map +1 -1
  24. package/dist/bin.js +59 -4
  25. package/dist/bin.js.map +1 -1
  26. package/dist/branching.d.ts.map +1 -1
  27. package/dist/branching.js +14 -1
  28. package/dist/branching.js.map +1 -1
  29. package/dist/checkpoint.d.ts.map +1 -1
  30. package/dist/checkpoint.js +13 -1
  31. package/dist/checkpoint.js.map +1 -1
  32. package/dist/cli/agent.d.ts.map +1 -1
  33. package/dist/cli/agent.js +19 -3
  34. package/dist/cli/agent.js.map +1 -1
  35. package/dist/cli/commands.d.ts.map +1 -1
  36. package/dist/cli/commands.js +99 -0
  37. package/dist/cli/commands.js.map +1 -1
  38. package/dist/cli/index.d.ts.map +1 -1
  39. package/dist/cli/index.js +32 -1
  40. package/dist/cli/index.js.map +1 -1
  41. package/dist/cli/types.js +1 -1
  42. package/dist/cli/types.js.map +1 -1
  43. package/dist/config.js +2 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/diff.d.ts.map +1 -1
  46. package/dist/diff.js +42 -4
  47. package/dist/diff.js.map +1 -1
  48. package/dist/errors.d.ts.map +1 -1
  49. package/dist/errors.js +30 -3
  50. package/dist/errors.js.map +1 -1
  51. package/dist/headless.d.ts.map +1 -1
  52. package/dist/headless.js +56 -2
  53. package/dist/headless.js.map +1 -1
  54. package/dist/hooks.d.ts +8 -2
  55. package/dist/hooks.d.ts.map +1 -1
  56. package/dist/hooks.js +97 -11
  57. package/dist/hooks.js.map +1 -1
  58. package/dist/idle-eviction.d.ts.map +1 -1
  59. package/dist/idle-eviction.js +8 -1
  60. package/dist/idle-eviction.js.map +1 -1
  61. package/dist/markdown.d.ts.map +1 -1
  62. package/dist/markdown.js +32 -10
  63. package/dist/markdown.js.map +1 -1
  64. package/dist/mcp.d.ts +35 -5
  65. package/dist/mcp.d.ts.map +1 -1
  66. package/dist/mcp.js +186 -12
  67. package/dist/mcp.js.map +1 -1
  68. package/dist/model-detection.d.ts +14 -1
  69. package/dist/model-detection.d.ts.map +1 -1
  70. package/dist/model-detection.js +307 -114
  71. package/dist/model-detection.js.map +1 -1
  72. package/dist/model-router.js +7 -7
  73. package/dist/model-router.js.map +1 -1
  74. package/dist/parallel-tools.d.ts +9 -1
  75. package/dist/parallel-tools.d.ts.map +1 -1
  76. package/dist/parallel-tools.js +6 -5
  77. package/dist/parallel-tools.js.map +1 -1
  78. package/dist/plugins.d.ts +37 -0
  79. package/dist/plugins.d.ts.map +1 -1
  80. package/dist/plugins.js +87 -0
  81. package/dist/plugins.js.map +1 -1
  82. package/dist/providers/anthropic.d.ts.map +1 -1
  83. package/dist/providers/anthropic.js +36 -2
  84. package/dist/providers/anthropic.js.map +1 -1
  85. package/dist/providers/bedrock.d.ts.map +1 -1
  86. package/dist/providers/bedrock.js +81 -17
  87. package/dist/providers/bedrock.js.map +1 -1
  88. package/dist/providers/index.d.ts.map +1 -1
  89. package/dist/providers/index.js +2 -0
  90. package/dist/providers/index.js.map +1 -1
  91. package/dist/providers/types.d.ts.map +1 -1
  92. package/dist/providers/types.js +19 -10
  93. package/dist/providers/types.js.map +1 -1
  94. package/dist/risk.d.ts.map +1 -1
  95. package/dist/risk.js +15 -5
  96. package/dist/risk.js.map +1 -1
  97. package/dist/sandbox-native.d.ts +1 -0
  98. package/dist/sandbox-native.d.ts.map +1 -1
  99. package/dist/sandbox-native.js +37 -5
  100. package/dist/sandbox-native.js.map +1 -1
  101. package/dist/scope.d.ts +10 -0
  102. package/dist/scope.d.ts.map +1 -1
  103. package/dist/scope.js +75 -15
  104. package/dist/scope.js.map +1 -1
  105. package/dist/scuttlebot/client.d.ts +83 -0
  106. package/dist/scuttlebot/client.d.ts.map +1 -0
  107. package/dist/scuttlebot/client.js +350 -0
  108. package/dist/scuttlebot/client.js.map +1 -0
  109. package/dist/scuttlebot/config.d.ts +28 -0
  110. package/dist/scuttlebot/config.d.ts.map +1 -0
  111. package/dist/scuttlebot/config.js +91 -0
  112. package/dist/scuttlebot/config.js.map +1 -0
  113. package/dist/scuttlebot/http-client.d.ts +63 -0
  114. package/dist/scuttlebot/http-client.d.ts.map +1 -0
  115. package/dist/scuttlebot/http-client.js +124 -0
  116. package/dist/scuttlebot/http-client.js.map +1 -0
  117. package/dist/scuttlebot/index.d.ts +13 -0
  118. package/dist/scuttlebot/index.d.ts.map +1 -0
  119. package/dist/scuttlebot/index.js +10 -0
  120. package/dist/scuttlebot/index.js.map +1 -0
  121. package/dist/scuttlebot/irc-client.d.ts +124 -0
  122. package/dist/scuttlebot/irc-client.d.ts.map +1 -0
  123. package/dist/scuttlebot/irc-client.js +599 -0
  124. package/dist/scuttlebot/irc-client.js.map +1 -0
  125. package/dist/skills.d.ts +19 -0
  126. package/dist/skills.d.ts.map +1 -1
  127. package/dist/skills.js +98 -10
  128. package/dist/skills.js.map +1 -1
  129. package/dist/smart-router.js +4 -4
  130. package/dist/smart-router.js.map +1 -1
  131. package/dist/storage.d.ts +0 -4
  132. package/dist/storage.d.ts.map +1 -1
  133. package/dist/storage.js +81 -5
  134. package/dist/storage.js.map +1 -1
  135. package/dist/tools.d.ts.map +1 -1
  136. package/dist/tools.js +232 -38
  137. package/dist/tools.js.map +1 -1
  138. package/dist/trust.d.ts +16 -3
  139. package/dist/trust.d.ts.map +1 -1
  140. package/dist/trust.js +23 -4
  141. package/dist/trust.js.map +1 -1
  142. package/dist/types.d.ts.map +1 -1
  143. package/dist/types.js +13 -4
  144. package/dist/types.js.map +1 -1
  145. package/dist/ui/agent.d.ts +1 -1
  146. package/dist/ui/agent.d.ts.map +1 -1
  147. package/dist/ui/agent.js +35 -44
  148. package/dist/ui/agent.js.map +1 -1
  149. package/dist/ui/chat-input.d.ts +3 -1
  150. package/dist/ui/chat-input.d.ts.map +1 -1
  151. package/dist/ui/chat-input.js +82 -17
  152. package/dist/ui/chat-input.js.map +1 -1
  153. package/dist/ui/commands.d.ts +2 -0
  154. package/dist/ui/commands.d.ts.map +1 -1
  155. package/dist/ui/commands.js +318 -10
  156. package/dist/ui/commands.js.map +1 -1
  157. package/dist/ui/index.d.ts.map +1 -1
  158. package/dist/ui/index.js +236 -46
  159. package/dist/ui/index.js.map +1 -1
  160. package/dist/ui/input-utils.d.ts +20 -0
  161. package/dist/ui/input-utils.d.ts.map +1 -0
  162. package/dist/ui/input-utils.js +35 -0
  163. package/dist/ui/input-utils.js.map +1 -0
  164. package/dist/ui/messages.d.ts +6 -2
  165. package/dist/ui/messages.d.ts.map +1 -1
  166. package/dist/ui/messages.js +42 -11
  167. package/dist/ui/messages.js.map +1 -1
  168. package/dist/ui/modals.d.ts +21 -1
  169. package/dist/ui/modals.d.ts.map +1 -1
  170. package/dist/ui/modals.js +67 -5
  171. package/dist/ui/modals.js.map +1 -1
  172. package/dist/ui/status-bar.d.ts +4 -1
  173. package/dist/ui/status-bar.d.ts.map +1 -1
  174. package/dist/ui/status-bar.js +12 -1
  175. package/dist/ui/status-bar.js.map +1 -1
  176. package/dist/ui/types.d.ts +3 -0
  177. package/dist/ui/types.d.ts.map +1 -1
  178. package/package.json +4 -7
  179. package/dist/completion.d.ts +0 -75
  180. package/dist/completion.d.ts.map +0 -1
  181. package/dist/completion.js +0 -234
  182. package/dist/completion.js.map +0 -1
  183. package/dist/keyboard.d.ts +0 -57
  184. package/dist/keyboard.d.ts.map +0 -1
  185. package/dist/keyboard.js +0 -265
  186. package/dist/keyboard.js.map +0 -1
@@ -6,6 +6,17 @@
6
6
  import OpenAI from 'openai';
7
7
  import { select } from '@inquirer/prompts';
8
8
  import * as config from './config.js';
9
+ const DEBUG = process.env.CALLIOPE_DEBUG === '1';
10
+ function logModelDetectionWarning(message, error, options = {}) {
11
+ if (options.quiet || !DEBUG) {
12
+ return;
13
+ }
14
+ if (error !== undefined) {
15
+ console.warn(message, error);
16
+ return;
17
+ }
18
+ console.warn(message);
19
+ }
9
20
  // API base URLs for OpenAI-compatible providers
10
21
  const PROVIDER_BASE_URLS = {
11
22
  openrouter: 'https://openrouter.ai/api/v1',
@@ -172,7 +183,7 @@ function formatContextLength(tokens) {
172
183
  /**
173
184
  * Get available models for a provider
174
185
  */
175
- export async function getAvailableModels(provider) {
186
+ export async function getAvailableModels(provider, options = {}) {
176
187
  // Check cache first
177
188
  const cached = modelCache.get(provider);
178
189
  if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
@@ -182,10 +193,10 @@ export async function getAvailableModels(provider) {
182
193
  try {
183
194
  switch (provider) {
184
195
  case 'anthropic':
185
- models = await getAnthropicModels();
196
+ models = await getAnthropicModels(options);
186
197
  break;
187
198
  case 'google':
188
- models = await getGoogleModels();
199
+ models = await getGoogleModels(options);
189
200
  break;
190
201
  case 'openai':
191
202
  models = await getOpenAIModels();
@@ -226,14 +237,16 @@ export async function getAvailableModels(provider) {
226
237
  modelCache.set(provider, { models, timestamp: Date.now() });
227
238
  }
228
239
  catch (error) {
229
- console.warn(`Failed to fetch models for ${provider}:`, error);
240
+ logModelDetectionWarning(`Failed to fetch models for ${provider}:`, error, options);
241
+ if (options.throwOnError)
242
+ throw error;
230
243
  }
231
244
  return models;
232
245
  }
233
246
  /**
234
247
  * Get Anthropic models dynamically from API
235
248
  */
236
- async function getAnthropicModels() {
249
+ async function getAnthropicModels(options = {}) {
237
250
  const apiKey = config.getApiKey('anthropic');
238
251
  if (!apiKey)
239
252
  throw new Error('Anthropic API key not configured');
@@ -254,18 +267,20 @@ async function getAnthropicModels() {
254
267
  id: model.id,
255
268
  name: model.display_name || formatModelName(model.id),
256
269
  description: getAnthropicModelDescription(model.id),
257
- contextLength: 200000,
270
+ // The /v1/models list endpoint does not return the context window;
271
+ // derive it per model family rather than hardcoding a single value.
272
+ contextLength: getModelContextLimit('anthropic', model.id),
258
273
  }))
259
274
  .sort((a, b) => b.id.localeCompare(a.id)); // Newest first
260
275
  }
261
276
  catch (error) {
262
- // Fallback to known models if API fails
263
- console.warn('Failed to fetch Anthropic models, using fallback list');
277
+ // Emergency fallback when the API is unreachable. Keep these as the current
278
+ // shipping models discovery is the source of truth; this is the offline net.
279
+ logModelDetectionWarning('Failed to fetch Anthropic models, using fallback list', error, options);
264
280
  return [
265
- { id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5', description: 'Most capable model', contextLength: 200000 },
266
- { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', description: 'Balanced intelligence and speed', contextLength: 200000 },
267
- { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', description: 'Previous gen flagship', contextLength: 200000 },
268
- { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', description: 'Fast and affordable', contextLength: 200000 },
281
+ { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', description: 'Most capable model', contextLength: 1000000 },
282
+ { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', description: 'Balanced intelligence and speed', contextLength: 1000000 },
283
+ { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', description: 'Fast and affordable', contextLength: 200000 },
269
284
  ];
270
285
  }
271
286
  }
@@ -290,7 +305,7 @@ function getAnthropicModelDescription(modelId) {
290
305
  /**
291
306
  * Get Google models dynamically from API
292
307
  */
293
- async function getGoogleModels() {
308
+ async function getGoogleModels(options = {}) {
294
309
  const apiKey = config.getApiKey('google');
295
310
  if (!apiKey)
296
311
  throw new Error('Google API key not configured');
@@ -316,7 +331,7 @@ async function getGoogleModels() {
316
331
  }
317
332
  catch (error) {
318
333
  // Fallback to known models if API fails
319
- console.warn('Failed to fetch Google models, using fallback list');
334
+ logModelDetectionWarning('Failed to fetch Google models, using fallback list', error, options);
320
335
  return [
321
336
  { id: 'gemini-2.5-pro-preview-06-05', name: 'Gemini 2.5 Pro', description: 'Most capable', contextLength: 1048576 },
322
337
  { id: 'gemini-2.5-flash-preview-05-20', name: 'Gemini 2.5 Flash', description: 'Fast next-gen', contextLength: 1048576 },
@@ -594,45 +609,67 @@ async function getBedrockModels() {
594
609
  const apiKey = config.getApiKey('bedrock');
595
610
  // 1. Try gateway/proxy model listing (OpenAI-compatible)
596
611
  if (baseUrl) {
597
- try {
598
- const modelsUrl = baseUrl.endsWith('/v1') ? `${baseUrl}/models` : `${baseUrl}/v1/models`;
599
- const headers = {};
600
- if (apiKey) {
601
- headers['Authorization'] = `Bearer ${apiKey}`;
602
- }
603
- const response = await fetch(modelsUrl, { headers });
604
- if (response.ok) {
605
- const data = await response.json();
606
- return data.data
607
- .filter(model => isCompatibleModel(model.id, 'bedrock'))
608
- .map(model => ({
609
- id: model.id,
610
- name: model.id,
611
- description: getBedrockModelDescription(model.id),
612
- contextLength: getBedrockContextLength(model.id),
613
- }));
614
- }
612
+ const modelsUrl = baseUrl.endsWith('/v1') ? `${baseUrl}/models` : `${baseUrl}/v1/models`;
613
+ const headers = {};
614
+ if (apiKey) {
615
+ headers['Authorization'] = `Bearer ${apiKey}`;
615
616
  }
616
- catch {
617
- // Fall through to native discovery
617
+ const response = await fetch(modelsUrl, { headers });
618
+ if (response.ok) {
619
+ const data = await response.json();
620
+ return data.data
621
+ .filter(model => isCompatibleModel(model.id, 'bedrock'))
622
+ .map(model => ({
623
+ id: model.id,
624
+ name: model.id,
625
+ description: getBedrockModelDescription(model.id),
626
+ contextLength: getBedrockContextLength(model.id),
627
+ }));
618
628
  }
629
+ throw new Error(`Bedrock gateway ${baseUrl} returned ${response.status}. Check BEDROCK_BASE_URL / BEDROCK_API_KEY.`);
619
630
  }
620
- // 2. Try native AWS ListFoundationModels API
631
+ // 2. Native AWS path — let errors bubble up so the user sees the real reason.
632
+ return discoverBedrockModelsNative();
633
+ }
634
+ /**
635
+ * Resolve AWS credentials via the `aws` CLI. Handles SSO profiles,
636
+ * role-assumption profiles, and anything else `aws` knows about.
637
+ * Returns null if the CLI isn't installed or the profile resolution fails.
638
+ */
639
+ async function resolveAwsCredentialsViaCli(profile) {
621
640
  try {
622
- const hasNativeCreds = !!((process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) ||
623
- process.env.AWS_PROFILE ||
624
- (await import('fs')).existsSync((await import('path')).join((await import('os')).homedir(), '.aws', 'credentials')));
625
- if (hasNativeCreds) {
626
- const nativeModels = await discoverBedrockModelsNative();
627
- if (nativeModels.length > 0)
628
- return nativeModels;
641
+ const { execFileSync } = await import('child_process');
642
+ let output = '';
643
+ try {
644
+ output = execFileSync('aws', ['configure', 'export-credentials', '--profile', profile, '--format', 'env-no-export'], { encoding: 'utf-8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] });
645
+ }
646
+ catch {
647
+ output = execFileSync('aws', ['configure', 'export-credentials', '--profile', profile, '--format', 'env'], { encoding: 'utf-8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] });
629
648
  }
649
+ const envs = {};
650
+ for (const rawLine of output.split(/\r?\n/)) {
651
+ const line = rawLine.trim();
652
+ const match = line.match(/^(?:export\s+)?([A-Z_]+)\s*=\s*(.+)$/);
653
+ if (!match)
654
+ continue;
655
+ let val = match[2].trim();
656
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
657
+ val = val.slice(1, -1);
658
+ }
659
+ envs[match[1]] = val;
660
+ }
661
+ if (envs.AWS_ACCESS_KEY_ID && envs.AWS_SECRET_ACCESS_KEY) {
662
+ return {
663
+ accessKeyId: envs.AWS_ACCESS_KEY_ID,
664
+ secretAccessKey: envs.AWS_SECRET_ACCESS_KEY,
665
+ sessionToken: envs.AWS_SESSION_TOKEN,
666
+ };
667
+ }
668
+ return null;
630
669
  }
631
670
  catch {
632
- // Fall through to minimal fallback
671
+ return null;
633
672
  }
634
- // 3. No hardcoded fallback — the default model from types.ts is used when list is empty
635
- return [];
636
673
  }
637
674
  /**
638
675
  * Discover Bedrock models using the native AWS ListFoundationModels API.
@@ -647,85 +684,184 @@ async function discoverBedrockModelsNative() {
647
684
  let accessKeyId = process.env.AWS_ACCESS_KEY_ID || '';
648
685
  let secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY || '';
649
686
  let sessionToken = process.env.AWS_SESSION_TOKEN;
650
- if (!accessKeyId || !secretAccessKey) {
651
- const profile = process.env.AWS_PROFILE || config.get('awsProfile') || 'default';
652
- const credPath = join(homedir(), '.aws', 'credentials');
653
- if (existsSync(credPath)) {
654
- const content = readFileSync(credPath, 'utf-8');
655
- const sections = {};
656
- let section = '';
657
- for (const line of content.split('\n')) {
658
- const trimmed = line.trim();
659
- const secMatch = trimmed.match(/^\[(.+)\]$/);
660
- if (secMatch) {
661
- section = secMatch[1];
662
- sections[section] = {};
663
- continue;
664
- }
665
- const kvMatch = trimmed.match(/^([^=]+?)\s*=\s*(.+)$/);
666
- if (kvMatch && section)
667
- sections[section][kvMatch[1].trim()] = kvMatch[2].trim();
668
- }
669
- const cred = sections[profile];
670
- if (cred?.aws_access_key_id) {
671
- accessKeyId = cred.aws_access_key_id;
672
- secretAccessKey = cred.aws_secret_access_key || '';
673
- sessionToken = cred.aws_session_token;
687
+ const profile = process.env.AWS_PROFILE || config.get('awsProfile') || 'default';
688
+ // Parse an INI-style AWS file. Handles both ~/.aws/credentials sections
689
+ // ([name]) and ~/.aws/config sections ([profile name]).
690
+ const readIni = (path) => {
691
+ if (!existsSync(path))
692
+ return {};
693
+ const content = readFileSync(path, 'utf-8');
694
+ const sections = {};
695
+ let section = '';
696
+ for (const line of content.split('\n')) {
697
+ const trimmed = line.trim();
698
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';'))
699
+ continue;
700
+ const secMatch = trimmed.match(/^\[(.+)\]$/);
701
+ if (secMatch) {
702
+ section = secMatch[1].replace(/^profile\s+/, '');
703
+ sections[section] = sections[section] || {};
704
+ continue;
674
705
  }
706
+ const kvMatch = trimmed.match(/^([^=]+?)\s*=\s*(.+)$/);
707
+ if (kvMatch && section)
708
+ sections[section][kvMatch[1].trim()] = kvMatch[2].trim();
709
+ }
710
+ return sections;
711
+ };
712
+ if (!accessKeyId || !secretAccessKey) {
713
+ // Try ~/.aws/credentials (static keys) first, then ~/.aws/config (also
714
+ // used by some setups that put static keys alongside SSO config).
715
+ const credSections = readIni(join(homedir(), '.aws', 'credentials'));
716
+ const configSections = readIni(join(homedir(), '.aws', 'config'));
717
+ const cred = credSections[profile] || configSections[profile];
718
+ if (cred?.aws_access_key_id) {
719
+ accessKeyId = cred.aws_access_key_id;
720
+ secretAccessKey = cred.aws_secret_access_key || '';
721
+ sessionToken = cred.aws_session_token;
675
722
  }
676
723
  }
677
- if (!accessKeyId || !secretAccessKey)
678
- return [];
724
+ // Last resort: shell out to the AWS CLI. This resolves SSO / role-assumption
725
+ // profiles that can't be parsed from the INI files alone.
726
+ if (!accessKeyId || !secretAccessKey) {
727
+ const cliCreds = await resolveAwsCredentialsViaCli(profile);
728
+ if (cliCreds) {
729
+ accessKeyId = cliCreds.accessKeyId;
730
+ secretAccessKey = cliCreds.secretAccessKey;
731
+ sessionToken = cliCreds.sessionToken;
732
+ }
733
+ }
734
+ if (!accessKeyId || !secretAccessKey) {
735
+ throw new Error(`No AWS credentials found for profile "${profile}". ` +
736
+ `Try: aws sso login --profile ${profile} (for SSO), or set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY.`);
737
+ }
679
738
  const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || config.get('awsRegion') || 'us-east-1';
680
739
  const host = `bedrock.${region}.amazonaws.com`;
681
- const url = `https://${host}/foundation-models?byOutputModality=TEXT&byInferenceType=ON_DEMAND`;
682
- // SigV4 sign
683
- const now = new Date();
684
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
685
- const dateStamp = amzDate.slice(0, 8);
686
- const sha256Fn = (d) => createHash('sha256').update(d).digest('hex');
687
- const hmacFn = (k, d) => createHmac('sha256', k).update(d).digest();
688
- const headers = {
689
- 'host': host,
690
- 'x-amz-date': amzDate,
740
+ const signedGet = async (path, query) => {
741
+ const url = `https://${host}${path}${query ? '?' + query : ''}`;
742
+ const now = new Date();
743
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
744
+ const dateStamp = amzDate.slice(0, 8);
745
+ const sha256Fn = (d) => createHash('sha256').update(d).digest('hex');
746
+ const hmacFn = (k, d) => createHmac('sha256', k).update(d).digest();
747
+ const headers = { host, 'x-amz-date': amzDate };
748
+ if (sessionToken)
749
+ headers['x-amz-security-token'] = sessionToken;
750
+ const signedHeaderKeys = Object.keys(headers).map(k => k.toLowerCase()).sort();
751
+ const signedHeaders = signedHeaderKeys.join(';');
752
+ const canonicalHeaders = signedHeaderKeys.map(k => `${k}:${headers[k].trim()}`).join('\n') + '\n';
753
+ const payloadHash = sha256Fn('');
754
+ // AWS SigV4: non-S3 services require the canonical URI to be URI-encoded
755
+ // TWICE. Paths here don't currently contain special chars but we normalise
756
+ // for consistency with the chat signing path.
757
+ const canonicalPath = path.split('/').map(s => encodeURIComponent(s)).join('/');
758
+ const canonicalRequest = ['GET', canonicalPath, query, canonicalHeaders, signedHeaders, payloadHash].join('\n');
759
+ const credentialScope = `${dateStamp}/${region}/bedrock/aws4_request`;
760
+ const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, sha256Fn(canonicalRequest)].join('\n');
761
+ const kDate = hmacFn('AWS4' + secretAccessKey, dateStamp);
762
+ const kRegion = hmacFn(kDate, region);
763
+ const kService = hmacFn(kRegion, 'bedrock');
764
+ const signingKey = hmacFn(kService, 'aws4_request');
765
+ const signature = createHmac('sha256', signingKey).update(stringToSign).digest('hex');
766
+ headers['Authorization'] = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
767
+ return fetch(url, { headers });
691
768
  };
692
- if (sessionToken)
693
- headers['x-amz-security-token'] = sessionToken;
694
- const parsedUrl = new URL(url);
695
- const signedHeaderKeys = Object.keys(headers).map(k => k.toLowerCase()).sort();
696
- const signedHeaders = signedHeaderKeys.join(';');
697
- const canonicalHeaders = signedHeaderKeys.map(k => `${k}:${headers[k].trim()}`).join('\n') + '\n';
698
- const payloadHash = sha256Fn('');
699
- const canonicalRequest = ['GET', parsedUrl.pathname, parsedUrl.search.slice(1), canonicalHeaders, signedHeaders, payloadHash].join('\n');
700
- const credentialScope = `${dateStamp}/${region}/bedrock/aws4_request`;
701
- const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, sha256Fn(canonicalRequest)].join('\n');
702
- const kDate = hmacFn('AWS4' + secretAccessKey, dateStamp);
703
- const kRegion = hmacFn(kDate, region);
704
- const kService = hmacFn(kRegion, 'bedrock');
705
- const signingKey = hmacFn(kService, 'aws4_request');
706
- const signature = createHmac('sha256', signingKey).update(stringToSign).digest('hex');
707
- headers['Authorization'] = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
708
- const response = await fetch(url, { headers });
709
- if (!response.ok)
710
- return [];
711
- const data = await response.json();
712
- if (!data.modelSummaries)
713
- return [];
714
- return data.modelSummaries
715
- .filter(m => {
716
- // Only text-in/text-out models that support streaming
717
- if (!m.outputModalities?.includes('TEXT'))
718
- return false;
719
- if (!m.inputModalities?.includes('TEXT'))
720
- return false;
721
- return isCompatibleModel(m.modelId, 'bedrock');
722
- })
769
+ // 1. ListFoundationModels (direct on-demand access).
770
+ // Dropped the byInferenceType=ON_DEMAND filter — newer Claude models are only
771
+ // accessible via cross-region inference profiles and don't have ON_DEMAND flag.
772
+ const foundationResp = await signedGet('/foundation-models', 'byOutputModality=TEXT');
773
+ if (!foundationResp.ok) {
774
+ let body = '';
775
+ try {
776
+ body = (await foundationResp.text()).slice(0, 400);
777
+ }
778
+ catch { /* ignore */ }
779
+ throw new Error(`AWS Bedrock ListFoundationModels returned ${foundationResp.status} in region ${region}. ` +
780
+ (body || 'Common causes: (1) no Bedrock access in this region — try us-east-1 or us-west-2; ' +
781
+ '(2) IAM role missing bedrock:ListFoundationModels; (3) SSO token expired — run `aws sso login`.'));
782
+ }
783
+ const foundationData = await foundationResp.json();
784
+ const foundationModels = (foundationData.modelSummaries || [])
785
+ .filter(m => m.inputModalities?.includes('TEXT') && m.outputModalities?.includes('TEXT'))
786
+ .filter(m => bedrockSupportsConverseTools(m.modelId))
723
787
  .map(m => ({
724
788
  id: m.modelId,
725
789
  name: m.modelName || m.modelId,
726
790
  description: `${m.providerName || 'Unknown'} — ${getBedrockModelDescription(m.modelId)}`,
727
791
  contextLength: getBedrockContextLength(m.modelId),
728
792
  }));
793
+ // 2. ListInferenceProfiles — cross-region profile IDs (e.g. us.anthropic.claude-sonnet-4-5-*).
794
+ // Many modern models are ONLY reachable via these, not direct foundation-model IDs.
795
+ // Failures here are non-fatal (older accounts / regions may not support it).
796
+ let profileModels = [];
797
+ try {
798
+ const profileResp = await signedGet('/inference-profiles', '');
799
+ if (profileResp.ok) {
800
+ const profileData = await profileResp.json();
801
+ profileModels = (profileData.inferenceProfileSummaries || [])
802
+ .filter(p => p.status !== 'INACTIVE')
803
+ .filter(p => bedrockSupportsConverseTools(p.inferenceProfileId))
804
+ .map(p => ({
805
+ id: p.inferenceProfileId,
806
+ name: p.inferenceProfileName || p.inferenceProfileId,
807
+ description: `Inference profile — ${getBedrockModelDescription(p.inferenceProfileId)}`,
808
+ contextLength: getBedrockContextLength(p.inferenceProfileId),
809
+ }));
810
+ }
811
+ }
812
+ catch {
813
+ // Non-fatal — foundation models alone is still useful.
814
+ }
815
+ // Merge. For every inference profile, strip the region prefix (e.g. `us.`,
816
+ // `eu.`, `apac.`, `jp.`) to get the base foundation-model ID it wraps, and
817
+ // drop that base from the foundation list — because newer Claude 4.x / Haiku
818
+ // 4.5 models can ONLY be invoked via their inference profile on on-demand
819
+ // throughput. Showing both would let users pick the invokable-broken raw ID.
820
+ const coveredBaseIds = new Set();
821
+ for (const p of profileModels) {
822
+ const base = p.id.replace(/^[a-z]{2,5}\./, '');
823
+ if (base !== p.id)
824
+ coveredBaseIds.add(base);
825
+ }
826
+ const filteredFoundation = foundationModels.filter(m => !coveredBaseIds.has(m.id));
827
+ const merged = new Map();
828
+ for (const m of filteredFoundation)
829
+ merged.set(m.id, m);
830
+ for (const m of profileModels)
831
+ merged.set(m.id, m);
832
+ return Array.from(merged.values()).sort((a, b) => a.id.localeCompare(b.id));
833
+ }
834
+ /**
835
+ * Bedrock Converse API tool-calling support. Maintained as a local allowlist
836
+ * because AWS doesn't expose per-model tool capability via the list APIs.
837
+ * See: https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html
838
+ * Matches both raw foundation model IDs (e.g. anthropic.claude-3-5-sonnet-*)
839
+ * and cross-region inference profile IDs (e.g. us.anthropic.claude-sonnet-4-5-*).
840
+ */
841
+ function bedrockSupportsConverseTools(modelId) {
842
+ const id = modelId.toLowerCase();
843
+ // Anthropic Claude 3, 3.5, 3.7, 4, 4.5 (all support tools). Excludes Claude 2.x / Instant.
844
+ if (/anthropic\.claude-(3|opus-4|sonnet-4|haiku-4|3-5|3-7)/.test(id))
845
+ return true;
846
+ // Amazon Nova (Pro / Lite / Micro support Converse tools; Nova Canvas/Reel are image models — excluded)
847
+ if (/amazon\.nova-(pro|lite|micro|premier)/.test(id))
848
+ return true;
849
+ // Cohere Command R / R+ support tools (older Command models do not)
850
+ if (/cohere\.command-r/.test(id))
851
+ return true;
852
+ // Mistral Large (2402, 2407), Pixtral Large, Mistral Small, Nemo
853
+ if (/mistral\.(mistral-large|pixtral|mistral-small|mistral-nemo)/.test(id))
854
+ return true;
855
+ // Meta Llama 3.1+ supports tools via Converse (3.0 and earlier do not)
856
+ if (/meta\.llama(3-1|3-2|3-3|4)/.test(id))
857
+ return true;
858
+ // AI21 Jamba 1.5 supports tools
859
+ if (/ai21\.jamba-1-5/.test(id))
860
+ return true;
861
+ // DeepSeek R1 supports tools
862
+ if (/deepseek\.r1/.test(id))
863
+ return true;
864
+ return false;
729
865
  }
730
866
  function getBedrockModelDescription(modelId) {
731
867
  if (modelId.includes('claude') && modelId.includes('opus'))
@@ -865,7 +1001,7 @@ export function clearModelCache(provider) {
865
1001
  export async function preWarmModelCache() {
866
1002
  const configuredProviders = config.getConfiguredProviders();
867
1003
  // Fetch models for all configured providers in parallel
868
- await Promise.allSettled(configuredProviders.map(provider => getAvailableModels(provider)));
1004
+ await Promise.allSettled(configuredProviders.map(provider => getAvailableModels(provider, { quiet: true })));
869
1005
  }
870
1006
  /**
871
1007
  * Get model info from cache by ID
@@ -874,12 +1010,29 @@ export function getModelInfo(provider, modelId) {
874
1010
  const cached = modelCache.get(provider);
875
1011
  if (!cached)
876
1012
  return undefined;
877
- return cached.models.find(m => m.id === modelId || m.id.includes(modelId) || modelId.includes(m.id));
1013
+ // Exact match first.
1014
+ const exact = cached.models.find(m => m.id === modelId);
1015
+ if (exact)
1016
+ return exact;
1017
+ // Otherwise only accept an UNAMBIGUOUS prefix relationship. Loose substring
1018
+ // matching wrongly resolved e.g. `gpt-4` -> `gpt-4o` or `claude-opus-4` ->
1019
+ // `claude-opus-4-8`, returning a different model's context/pricing.
1020
+ const related = cached.models.filter(m => m.id.startsWith(modelId) || modelId.startsWith(m.id));
1021
+ return related.length === 1 ? related[0] : undefined;
878
1022
  }
879
1023
  /**
880
1024
  * Default context limits by model family (fallback when API doesn't provide it)
881
1025
  */
882
1026
  const DEFAULT_CONTEXT_LIMITS = {
1027
+ // Anthropic — current 1M-context models matched first (longest key wins).
1028
+ // Everything else (Haiku 4.5, Claude 3.x, and the older -20250514 IDs) falls
1029
+ // through to the generic `claude` 200K entry below.
1030
+ 'claude-fable-5': 1000000,
1031
+ 'claude-opus-4-8': 1000000,
1032
+ 'claude-opus-4-7': 1000000,
1033
+ 'claude-opus-4-6': 1000000,
1034
+ 'claude-sonnet-4-6': 1000000,
1035
+ 'claude-haiku-4-5': 200000,
883
1036
  'claude': 200000,
884
1037
  'gpt-4o': 128000,
885
1038
  'gpt-4-turbo': 128000,
@@ -941,4 +1094,44 @@ export function getModelContextLimit(provider, modelId) {
941
1094
  // Ultimate fallback
942
1095
  return 32000;
943
1096
  }
1097
+ /**
1098
+ * Default max OUTPUT tokens by model family (fallback when the API doesn't
1099
+ * report it). Replaces the old global 8192 cap so modern models can use their
1100
+ * real output ceiling. Unknown models fall through to a conservative 8192.
1101
+ */
1102
+ const DEFAULT_MAX_OUTPUT = {
1103
+ 'claude-fable-5': 128000,
1104
+ 'claude-opus-4-8': 128000,
1105
+ 'claude-opus-4-7': 128000,
1106
+ 'claude-opus-4-6': 128000,
1107
+ 'claude-sonnet-4-6': 64000,
1108
+ 'claude-haiku-4-5': 64000,
1109
+ 'claude': 8192,
1110
+ 'gpt-5': 128000,
1111
+ 'o1': 100000,
1112
+ 'o3': 100000,
1113
+ 'gpt-4o': 16384,
1114
+ 'gpt-4': 8192,
1115
+ 'gemini-2': 8192,
1116
+ 'gemini-1.5': 8192,
1117
+ };
1118
+ /**
1119
+ * Get the max output-token ceiling for a model - cached API info first, then
1120
+ * family fallback. Conservative 8192 default keeps unknown/local models safe.
1121
+ */
1122
+ export function getModelMaxOutput(provider, modelId) {
1123
+ const modelInfo = getModelInfo(provider, modelId);
1124
+ if (modelInfo?.maxOutputTokens) {
1125
+ return modelInfo.maxOutputTokens;
1126
+ }
1127
+ const lowerModel = modelId.toLowerCase();
1128
+ const sortedEntries = Object.entries(DEFAULT_MAX_OUTPUT)
1129
+ .sort((a, b) => b[0].length - a[0].length);
1130
+ for (const [key, limit] of sortedEntries) {
1131
+ if (lowerModel.includes(key.toLowerCase())) {
1132
+ return limit;
1133
+ }
1134
+ }
1135
+ return 8192;
1136
+ }
944
1137
  //# sourceMappingURL=model-detection.js.map