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.
- package/.env.example +22 -3
- package/README.md +86 -49
- package/docs/API.md +160 -18
- package/docs/EXAMPLES.md +38 -0
- package/docs/PROVIDERS.md +92 -52
- package/package.json +1 -1
- package/src/config.js +36 -6
- package/src/decisionProviders/index.js +174 -0
- package/src/decisionProviders/systemOne.js +199 -0
- package/src/prompts/helpPrompt.js +50 -3
- package/src/providers/anthropic.js +5 -1
- package/src/providers/claude.js +39 -88
- package/src/providers/codex.js +73 -144
- package/src/providers/copilot.js +42 -149
- package/src/providers/deepseek.js +1 -0
- package/src/providers/gemini-cli.js +64 -97
- package/src/providers/google.js +5 -1
- package/src/providers/mistral.js +5 -1
- package/src/providers/openai-compatible.js +4 -1
- package/src/providers/openai.js +5 -1
- package/src/providers/openrouter.js +2 -1
- package/src/providers/xai.js +5 -1
- package/src/services/summarizationService.js +45 -49
- package/src/tools/chat.js +56 -52
- package/src/tools/decide.js +312 -0
- package/src/tools/index.js +2 -0
- package/src/tools/modes/roundtable.js +60 -49
- package/src/utils/localProviderAuth.js +63 -0
- package/src/utils/modelCatalog.js +38 -0
- package/src/utils/modelRouting.js +580 -343
|
@@ -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'}, ${
|
|
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 =
|
|
603
|
+
model = DEFAULT_MODEL,
|
|
600
604
|
maxTokens = null,
|
|
601
605
|
stream = false,
|
|
602
606
|
reasoning_effort = 'medium',
|
package/src/providers/claude.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
'
|
|
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: ['
|
|
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
|
-
'
|
|
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: ['
|
|
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 (!
|
|
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
|
-
*
|
|
147
|
-
*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
142
|
+
return DEFAULT_MODEL;
|
|
186
143
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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 ||
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
601
|
+
return findCatalogEntry(SUPPORTED_MODELS, modelName);
|
|
651
602
|
},
|
|
652
603
|
};
|