converse-mcp-server 3.7.1 → 4.1.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.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * System One HTTP client
3
+ *
4
+ * One client for every host of the System One decision API. TypeSafe serves it
5
+ * natively at `/v1/systemone`; OpenRouter serves the same request/response
6
+ * schema at the same path under its own base URL, so providers differ only in
7
+ * base URL, key, and headers.
8
+ *
9
+ * Plain fetch rather than @typesafe-ai/sdk: the schema is small, the tool
10
+ * needs its own abort signal and error mapping, and the SDK's model listing
11
+ * breaks against OpenRouter.
12
+ */
13
+
14
+ const DEFAULT_TIMEOUT_MS = 60_000;
15
+ const DEFAULT_MAX_RETRIES = 2;
16
+ const BACKOFF_INITIAL_MS = 500;
17
+ const BACKOFF_MAX_MS = 5_000;
18
+ // A longer server-requested wait is better spent failing over to another host.
19
+ const MAX_RETRY_AFTER_MS = 30_000;
20
+
21
+ /**
22
+ * Error from a System One call. `retryable` marks failures worth repeating or
23
+ * failing over (network, timeout, 408/429/5xx, auth); `terminal` marks request
24
+ * faults that every host would reject the same way.
25
+ */
26
+ export class DecisionError extends Error {
27
+ constructor(message, { status = null, retryable = false, terminal = false, requestId = null, retryAfterMs = null } = {}) {
28
+ super(message);
29
+ this.name = 'DecisionError';
30
+ this.status = status;
31
+ this.retryable = retryable;
32
+ this.terminal = terminal;
33
+ this.requestId = requestId;
34
+ this.retryAfterMs = retryAfterMs;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Render a validation issue list (zod-style `[{path, message}]`) as one line
40
+ * per issue; anything else is returned as-is.
41
+ */
42
+ function formatIssues(text) {
43
+ let issues;
44
+ try {
45
+ issues = JSON.parse(text);
46
+ } catch {
47
+ return text;
48
+ }
49
+ if (!Array.isArray(issues) || !issues.every((i) => i && typeof i.message === 'string')) {
50
+ return text;
51
+ }
52
+ return issues
53
+ .map((i) => (Array.isArray(i.path) && i.path.length ? `${i.path.join('.')}: ${i.message}` : i.message))
54
+ .join('; ');
55
+ }
56
+
57
+ /**
58
+ * Extract a readable message from an error body. OpenRouter wraps errors as
59
+ * `{ error: { message } }`; TypeSafe answers `{ detail: { error_type, message } }`,
60
+ * or `{ detail }` as a string or a list of validation issues.
61
+ */
62
+ export function extractErrorMessage(body, rawText) {
63
+ if (typeof body?.detail?.message === 'string') {
64
+ const type = body.detail.error_type ? `${body.detail.error_type}: ` : '';
65
+ return `${type}${body.detail.message}`;
66
+ }
67
+ const nested = body?.error?.message ?? body?.error ?? body?.detail ?? body?.message;
68
+ if (typeof nested === 'string') {
69
+ const inner = nested.match(/^HTTP \d+: (\{.*\})$/s);
70
+ if (inner) {
71
+ try {
72
+ return extractErrorMessage(JSON.parse(inner[1]), inner[1]);
73
+ } catch {
74
+ return nested;
75
+ }
76
+ }
77
+ return formatIssues(nested);
78
+ }
79
+ if (Array.isArray(nested)) {
80
+ return nested
81
+ .map((d) => (d?.loc ? `${d.loc.join('.')}: ${d.msg}` : d?.msg || JSON.stringify(d)))
82
+ .join('; ');
83
+ }
84
+ return rawText?.trim() || 'No error details returned';
85
+ }
86
+
87
+ function parseRetryAfter(headers) {
88
+ const ms = Number(headers.get('retry-after-ms'));
89
+ if (Number.isFinite(ms) && ms > 0) return ms;
90
+ const value = headers.get('retry-after');
91
+ if (!value) return null;
92
+ const seconds = Number(value);
93
+ if (Number.isFinite(seconds)) return seconds * 1000;
94
+ const date = Date.parse(value);
95
+ return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
96
+ }
97
+
98
+ function sleep(ms, signal) {
99
+ return new Promise((resolve, reject) => {
100
+ if (signal?.aborted) {
101
+ reject(signal.reason);
102
+ return;
103
+ }
104
+ const timer = setTimeout(() => {
105
+ signal?.removeEventListener('abort', onAbort);
106
+ resolve();
107
+ }, ms);
108
+ function onAbort() {
109
+ clearTimeout(timer);
110
+ reject(signal.reason);
111
+ }
112
+ signal?.addEventListener('abort', onAbort, { once: true });
113
+ });
114
+ }
115
+
116
+ async function sendOnce({ url, headers, body, signal, timeoutMs }) {
117
+ const attemptSignal = signal
118
+ ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
119
+ : AbortSignal.timeout(timeoutMs);
120
+
121
+ let response;
122
+ try {
123
+ response = await fetch(url, {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json', ...headers },
126
+ body: JSON.stringify(body),
127
+ signal: attemptSignal,
128
+ });
129
+ } catch (error) {
130
+ if (signal?.aborted) throw error;
131
+ const timedOut = error?.name === 'TimeoutError';
132
+ throw new DecisionError(
133
+ timedOut ? `Request timed out after ${timeoutMs / 1000}s` : `Connection failed: ${error.message}`,
134
+ { retryable: true },
135
+ );
136
+ }
137
+
138
+ const rawText = await response.text();
139
+ let parsed = null;
140
+ try {
141
+ parsed = rawText ? JSON.parse(rawText) : null;
142
+ } catch {
143
+ parsed = null;
144
+ }
145
+
146
+ if (!response.ok) {
147
+ const status = response.status;
148
+ throw new DecisionError(`HTTP ${status}: ${extractErrorMessage(parsed, rawText)}`, {
149
+ status,
150
+ retryable: status === 408 || status === 429 || status >= 500 || status === 401 || status === 403,
151
+ terminal: status === 400 || status === 422,
152
+ requestId: response.headers.get('x-typesafe-request-id') || response.headers.get('x-generation-id'),
153
+ retryAfterMs: parseRetryAfter(response.headers),
154
+ });
155
+ }
156
+
157
+ if (!parsed || typeof parsed.answers !== 'object' || parsed.answers === null) {
158
+ throw new DecisionError('Malformed response: missing "answers" object', { status: response.status });
159
+ }
160
+ return parsed;
161
+ }
162
+
163
+ /**
164
+ * POST a decision request, retrying transient failures (except auth, which
165
+ * retrying cannot fix) with exponential backoff that honors Retry-After.
166
+ * @param {object} params
167
+ * @param {string} params.baseURL - Host base, e.g. https://api.typesafe.ai
168
+ * @param {object} params.headers - Auth and attribution headers
169
+ * @param {object} params.body - `{ model, state, questions }`
170
+ * @param {AbortSignal} [params.signal] - Caller cancellation
171
+ * @param {number} [params.timeoutMs] - Per-attempt timeout
172
+ * @param {number} [params.maxRetries] - Retries after the first attempt
173
+ * @returns {Promise<object>} Parsed response body
174
+ */
175
+ export async function callSystemOne({
176
+ baseURL,
177
+ headers,
178
+ body,
179
+ signal,
180
+ timeoutMs = DEFAULT_TIMEOUT_MS,
181
+ maxRetries = DEFAULT_MAX_RETRIES,
182
+ }) {
183
+ const url = `${baseURL.replace(/\/+$/, '')}/v1/systemone`;
184
+ for (let attempt = 0; ; attempt++) {
185
+ try {
186
+ return await sendOnce({ url, headers, body, signal, timeoutMs });
187
+ } catch (error) {
188
+ const authFailure = error.status === 401 || error.status === 403;
189
+ if (!(error instanceof DecisionError) || !error.retryable || authFailure || attempt >= maxRetries) {
190
+ throw error;
191
+ }
192
+ if (error.retryAfterMs !== null && error.retryAfterMs > MAX_RETRY_AFTER_MS) {
193
+ throw error;
194
+ }
195
+ const backoff = Math.min(BACKOFF_INITIAL_MS * 2 ** attempt, BACKOFF_MAX_MS);
196
+ await sleep(error.retryAfterMs ?? backoff, signal);
197
+ }
198
+ }
199
+ }
@@ -8,6 +8,11 @@
8
8
  import { getProviders } from '../providers/index.js';
9
9
  import { getTools } from '../tools/index.js';
10
10
  import { CONFIG_SCHEMA } from '../config.js';
11
+ import {
12
+ BARE_NAME_PRIORITY,
13
+ PROVIDER_NAMESPACES,
14
+ PROVIDER_PRIORITY,
15
+ } from '../utils/modelRouting.js';
11
16
 
12
17
  /**
13
18
  * Sample values for generating realistic tool examples.
@@ -147,6 +152,27 @@ function generateToolExamplesFromSchema(toolName, inputSchema) {
147
152
  ].join('\n');
148
153
  }
149
154
 
155
+ if (toolName === 'decide') {
156
+ const decideExample = {
157
+ state: { message: 'I was charged twice for order A-104. Please fix this ASAP.' },
158
+ questions: {
159
+ urgent: { type: 'noul', instructions: 'Does the message convey urgency?' },
160
+ team: {
161
+ type: 'choice',
162
+ instructions: 'Which team should handle this?',
163
+ criteria: { billing: 'Payments, invoicing, refunds', technical: 'Bugs, outages', sales: null },
164
+ },
165
+ frustration: {
166
+ type: 'score',
167
+ instructions: 'How frustrated is the customer?',
168
+ criteria: ['Calm', 'Frustrated', 'Very angry'],
169
+ },
170
+ },
171
+ model: 'auto',
172
+ };
173
+ return `\`\`\`json\n${JSON.stringify(decideExample, null, 2)}\n\`\`\``;
174
+ }
175
+
150
176
  if (toolName === 'check_status' || toolName === 'cancel_job') {
151
177
  if (properties.continuation_id)
152
178
  example.continuation_id = SAMPLE_VALUES.continuation_id;
@@ -352,6 +378,7 @@ export function generateHelpContent(config = null) {
352
378
  codex: safeGetModels(providers.codex, 'codex'),
353
379
  claude: safeGetModels(providers.claude, 'claude'),
354
380
  'gemini-cli': safeGetModels(providers['gemini-cli'], 'gemini-cli'),
381
+ copilot: safeGetModels(providers.copilot, 'copilot'),
355
382
  };
356
383
 
357
384
  // Limit OpenRouter models if dynamic models enabled (could have hundreds)
@@ -406,8 +433,9 @@ export function generateHelpContent(config = null) {
406
433
  prop.default !== undefined
407
434
  ? ` (default: ${JSON.stringify(prop.default)})`
408
435
  : '';
436
+ const type = prop.type ?? prop.anyOf?.map((s) => s.type).join(' | ');
409
437
  params.push(
410
- `- **${name}** (${isRequired ? 'required' : 'optional'}, ${prop.type}): ${prop.description}${defaultValue}`,
438
+ `- **${name}** (${isRequired ? 'required' : 'optional'}, ${type}): ${prop.description}${defaultValue}`,
411
439
  );
412
440
  }
413
441
 
@@ -436,6 +464,21 @@ Welcome to the Converse MCP Server! This guide provides detailed information abo
436
464
 
437
465
  ${toolsSection}
438
466
 
467
+ ## Model Names
468
+
469
+ Every entry in \`models\` takes one of these forms:
470
+
471
+ - **\`provider\`** — that provider's default model (e.g. \`codex\`, \`claude\`, \`gemini\`, \`openai\`). Override a default with \`<PROVIDER>_DEFAULT_MODEL\` (see Environment Variables).
472
+ - **\`provider:model\`** — that model on that provider only (e.g. \`codex:astra\`, \`openai:gpt-6-astra\`, \`gemini:pro\`, \`copilot:sonnet\`). \`model\` is a model ID or alias from the provider's list below.
473
+ - **\`model\`** — a bare model ID or alias (e.g. \`gpt-6-astra\`, \`opus\`). It goes to the first configured provider that offers it, in this order: ${BARE_NAME_PRIORITY.join(', ')}. When that provider fails with an auth or availability error, the next provider offering the same model takes over. Copilot is reachable only as \`copilot:model\`.
474
+ - **\`auto\`** — the default model of the first available provider (${PROVIDER_PRIORITY.join(', ')}).
475
+
476
+ Provider namespaces: ${Object.entries(PROVIDER_NAMESPACES)
477
+ .map(([name, tokens]) => (tokens.length > 1 ? `${tokens[0]} (${name}; also ${tokens.slice(1).join(', ')})` : tokens[0]))
478
+ .join(', ')}. OpenRouter also accepts any \`vendor/model\` slug, bare or as \`openrouter:vendor/model\`, checked against OpenRouter's live catalog.
479
+
480
+ Names that match nothing are rejected with "did you mean" suggestions rather than guessed.
481
+
439
482
  ## Provider Models
440
483
  ${formatProviderModels('OpenAI', allModels.openai)}
441
484
  ${formatProviderModels('Google Gemini', allModels.google)}
@@ -447,6 +490,7 @@ ${formatProviderModels('OpenRouter', allModels.openrouter)}
447
490
  ${formatProviderModels('Codex', allModels.codex)}
448
491
  ${formatProviderModels('Claude CLI', allModels.claude)}
449
492
  ${formatProviderModels('Gemini (Antigravity CLI)', allModels['gemini-cli'])}
493
+ ${formatProviderModels('GitHub Copilot', allModels.copilot)}
450
494
 
451
495
  ${generateModelCategories(allModels)}
452
496
 
@@ -482,9 +526,12 @@ ${generateEnvironmentVariablesSection()}
482
526
 
483
527
  These providers use local CLI tools and don't require API keys:
484
528
 
485
- - **codex**: Requires ChatGPT login or CODEX_API_KEY environment variable
486
- - **claude**: Requires \`claude login\` command (Claude Code CLI authentication)
529
+ - **codex**: Requires ChatGPT login (\`codex login\`) or CODEX_API_KEY environment variable
530
+ - **claude**: Requires \`claude login\` command (Claude Code CLI authentication) or CLAUDE_CODE_OAUTH_TOKEN
487
531
  - **gemini-cli**: Requires the Antigravity CLI (\`agy\`) installed and authenticated via Google OAuth (run \`agy\` once interactively to log in)
532
+ - **copilot**: Requires @github/copilot-sdk and a GitHub Copilot subscription
533
+
534
+ Codex and Claude count as available only when their login file or token is present; an expired login is detected at call time and routing fails over to the next provider.
488
535
 
489
536
  ## Need More Help?
490
537
 
@@ -587,7 +587,11 @@ async function getAnthropicSDK() {
587
587
  /**
588
588
  * Main Anthropic provider implementation
589
589
  */
590
+ const DEFAULT_MODEL = 'claude-opus-5-5';
591
+
590
592
  export const anthropicProvider = {
593
+ defaultModel: DEFAULT_MODEL,
594
+
591
595
  /**
592
596
  * Unified provider interface: invoke messages with options
593
597
  * @param {Array} messages - Array of message objects with role and content
@@ -596,7 +600,7 @@ export const anthropicProvider = {
596
600
  */
597
601
  async invoke(messages, options = {}) {
598
602
  const {
599
- model = 'claude-opus-5-5',
603
+ model = DEFAULT_MODEL,
600
604
  maxTokens = null,
601
605
  stream = false,
602
606
  reasoning_effort = 'medium',
@@ -16,14 +16,20 @@
16
16
  import { debugLog, debugError } from '../utils/console.js';
17
17
  import { ProviderError, ErrorCodes, StopReasons } from './interface.js';
18
18
  import { clampReasoningEffort } from '../utils/reasoningEffort.js';
19
+ import { findCatalogEntry, findCatalogId } from '../utils/modelCatalog.js';
20
+ import {
21
+ hasClaudeCredentials,
22
+ isPackageResolvable,
23
+ } from '../utils/localProviderAuth.js';
19
24
 
20
- // Default underlying model when the request is just "claude" (or "claude:opus")
21
- const DEFAULT_SDK_MODEL = 'claude-opus-5-5';
25
+ const DEFAULT_MODEL = 'claude-opus-5-5';
22
26
  const SDK_EFFORT_TIERS = ['low', 'medium', 'high', 'xhigh', 'max'];
27
+ const CLAUDE_SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk';
23
28
 
24
- // Supported Claude SDK models with their configurations
29
+ // Keyed by the model ID the Agent SDK accepts, which is also the canonical ID
30
+ // the router resolves to.
25
31
  const SUPPORTED_MODELS = {
26
- opus: {
32
+ 'claude-opus-5-5': {
27
33
  modelName: 'claude-opus-5-5',
28
34
  friendlyName: 'Claude Opus 5.5 (via Agent SDK)',
29
35
  contextWindow: 1000000,
@@ -35,18 +41,14 @@ const SUPPORTED_MODELS = {
35
41
  description:
36
42
  'Claude Opus 5.5 via Agent SDK (default) - requires claude login authentication',
37
43
  aliases: [
38
- 'claude',
39
- 'claude-sdk',
40
- 'claude-code',
41
- 'claude:opus',
44
+ 'opus',
42
45
  'claude-opus',
43
- 'claude-opus-5-5',
44
46
  'claude-opus-5.5',
45
47
  'opus-5-5',
46
48
  'opus-5.5',
47
49
  ],
48
50
  },
49
- 'opus-5': {
51
+ 'claude-opus-5': {
50
52
  modelName: 'claude-opus-5',
51
53
  friendlyName: 'Claude Opus 5 (via Agent SDK)',
52
54
  contextWindow: 1000000,
@@ -57,9 +59,9 @@ const SUPPORTED_MODELS = {
57
59
  timeout: 1800000,
58
60
  description:
59
61
  'Claude Opus 5 via Agent SDK - requires claude login authentication',
60
- aliases: ['claude-opus-5'],
62
+ aliases: ['opus-5', 'opus5'],
61
63
  },
62
- fable: {
64
+ 'claude-fable-5-1': {
63
65
  modelName: 'claude-fable-5-1',
64
66
  friendlyName: 'Claude Fable 5.1 (via Agent SDK)',
65
67
  contextWindow: 1000000,
@@ -71,15 +73,14 @@ const SUPPORTED_MODELS = {
71
73
  description:
72
74
  'Claude Fable 5.1 via Agent SDK - requires claude login authentication',
73
75
  aliases: [
74
- 'claude:fable',
76
+ 'fable',
75
77
  'claude-fable',
76
- 'claude-fable-5-1',
77
78
  'claude-fable-5.1',
78
79
  'fable-5-1',
79
80
  'fable-5.1',
80
81
  ],
81
82
  },
82
- 'fable-5': {
83
+ 'claude-fable-5': {
83
84
  modelName: 'claude-fable-5',
84
85
  friendlyName: 'Claude Fable 5 (via Agent SDK)',
85
86
  contextWindow: 1000000,
@@ -90,7 +91,7 @@ const SUPPORTED_MODELS = {
90
91
  timeout: 1800000,
91
92
  description:
92
93
  'Claude Fable 5 via Agent SDK - requires claude login authentication',
93
- aliases: ['claude-fable-5'],
94
+ aliases: ['fable-5', 'fable5'],
94
95
  },
95
96
  };
96
97
 
@@ -104,25 +105,12 @@ class ClaudeProviderError extends ProviderError {
104
105
  }
105
106
  }
106
107
 
107
- /**
108
- * Check if Claude SDK is available (optional dependency)
109
- */
110
- function isClaudeSDKAvailable() {
111
- try {
112
- // Simple presence check that works in ES modules
113
- // If SDK not available, the actual import() will fail later with clear error
114
- return true;
115
- } catch {
116
- return false;
117
- }
118
- }
119
-
120
108
  /**
121
109
  * Dynamically import Claude SDK (lazy loading)
122
110
  * This keeps the SDK as an optional dependency
123
111
  */
124
112
  async function getClaudeSDK() {
125
- if (!isClaudeSDKAvailable()) {
113
+ if (!isPackageResolvable(CLAUDE_SDK_PACKAGE)) {
126
114
  throw new ClaudeProviderError(
127
115
  'Claude SDK not installed. Install with: npm install @anthropic-ai/claude-agent-sdk',
128
116
  'CLAUDE_SDK_NOT_INSTALLED',
@@ -143,58 +131,24 @@ async function getClaudeSDK() {
143
131
  }
144
132
 
145
133
  /**
146
- * Look up model config from SUPPORTED_MODELS by name or alias.
147
- * Strips the claude: prefix first (e.g. "claude:opus" -> "opus").
148
- */
149
- function findModelConfig(modelName) {
150
- if (typeof modelName !== 'string') return null;
151
-
152
- let name = modelName.trim();
153
- if (name.toLowerCase().startsWith('claude:')) {
154
- name = name.slice('claude:'.length).trim();
155
- }
156
- if (!name) return SUPPORTED_MODELS.opus;
157
-
158
- const nameLower = name.toLowerCase();
159
-
160
- if (SUPPORTED_MODELS[nameLower]) {
161
- return SUPPORTED_MODELS[nameLower];
162
- }
163
-
164
- for (const config of Object.values(SUPPORTED_MODELS)) {
165
- if (
166
- config.aliases &&
167
- config.aliases.some((alias) => alias.toLowerCase() === nameLower)
168
- ) {
169
- return config;
170
- }
171
- }
172
-
173
- return null;
174
- }
175
-
176
- /**
177
- * Resolve the requested model to the underlying SDK model ID.
178
- * - "claude" (and bare "claude:") defaults to Claude Opus 5.5
179
- * - "claude:opus" / "claude:opus-5" / "claude:fable" / "claude:fable-5" select the specific model
180
- * - Unknown names are passed through (after prefix stripping) so users can
181
- * target any model ID the Agent SDK accepts (e.g. "claude:claude-sonnet-4-6")
134
+ * Resolve a model name to the Agent SDK model ID. The router hands over
135
+ * canonical catalog IDs; aliases are accepted for direct callers.
136
+ * @param {string} [modelName] - Catalog ID or alias; defaults to DEFAULT_MODEL
137
+ * @returns {string}
138
+ * @throws {ClaudeProviderError} When the name is not in the catalog
182
139
  */
183
140
  function resolveSdkModel(modelName) {
184
141
  if (typeof modelName !== 'string' || !modelName.trim()) {
185
- return DEFAULT_SDK_MODEL;
142
+ return DEFAULT_MODEL;
186
143
  }
187
-
188
- const config = findModelConfig(modelName);
189
- if (config) {
190
- return config.modelName;
191
- }
192
-
193
- let name = modelName.trim();
194
- if (name.toLowerCase().startsWith('claude:')) {
195
- name = name.slice('claude:'.length).trim();
144
+ const id = findCatalogId(SUPPORTED_MODELS, modelName);
145
+ if (!id) {
146
+ throw new ClaudeProviderError(
147
+ `Unknown Claude model "${modelName}"`,
148
+ ErrorCodes.MODEL_NOT_FOUND,
149
+ );
196
150
  }
197
- return name || DEFAULT_SDK_MODEL;
151
+ return SUPPORTED_MODELS[id].modelName;
198
152
  }
199
153
 
200
154
  /**
@@ -338,7 +292,7 @@ async function* createStreamingGenerator(
338
292
  try {
339
293
  const queryOptions = {
340
294
  ...options,
341
- model: options.model || DEFAULT_SDK_MODEL,
295
+ model: options.model || DEFAULT_MODEL,
342
296
  maxTurns: 100,
343
297
  permissionMode: 'bypassPermissions', // Don't prompt for permissions
344
298
  };
@@ -475,7 +429,7 @@ export const claudeProvider = {
475
429
  */
476
430
  async invoke(messages, options = {}) {
477
431
  const {
478
- model = 'claude',
432
+ model = DEFAULT_MODEL,
479
433
  config,
480
434
  stream = false,
481
435
  signal,
@@ -616,16 +570,14 @@ export const claudeProvider = {
616
570
  }
617
571
  },
618
572
 
573
+ defaultModel: DEFAULT_MODEL,
574
+
619
575
  /**
620
- * Validate Claude SDK configuration
621
- * Claude SDK uses CLI authentication (NOT API keys)
622
- * Returns true optimistically - authentication errors handled at runtime
576
+ * Validate Claude SDK configuration: the SDK is installed and a Claude Code
577
+ * credential is present. Expired logins surface at invoke time.
623
578
  */
624
579
  validateConfig(_config) {
625
- // Claude SDK uses CLI authentication, not API keys
626
- // We can't reliably check auth status, so return true optimistically
627
- // and let the SDK handle authentication errors during execution
628
- return isClaudeSDKAvailable();
580
+ return isPackageResolvable(CLAUDE_SDK_PACKAGE) && hasClaudeCredentials();
629
581
  },
630
582
 
631
583
  /**
@@ -644,9 +596,8 @@ export const claudeProvider = {
644
596
 
645
597
  /**
646
598
  * Get model configuration for specific model
647
- * Handles claude: prefixed names (e.g. "claude:opus", "claude:fable", "claude:opus-5")
648
599
  */
649
600
  getModelConfig(modelName) {
650
- return findModelConfig(modelName);
601
+ return findCatalogEntry(SUPPORTED_MODELS, modelName);
651
602
  },
652
603
  };