converse-mcp-server 4.5.3 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +5 -3
- package/docs/API.md +44 -20
- package/docs/PROVIDERS.md +6 -4
- package/package.json +10 -10
- package/src/config.js +16 -1
- package/src/decisionProviders/formats.js +230 -0
- package/src/decisionProviders/{systemOne.js → http.js} +60 -26
- package/src/decisionProviders/index.js +167 -73
- package/src/providers/anthropic.js +32 -3
- package/src/providers/claude.js +19 -0
- package/src/providers/copilot.js +25 -1
- package/src/tools/decide.js +145 -34
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Decision API HTTP client
|
|
3
3
|
*
|
|
4
|
-
* One
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* One transport for every decision host: retries, timeouts, cancellation, and
|
|
5
|
+
* error mapping. Hosts differ in URL, auth, and wire format; each provider's
|
|
6
|
+
* format (see formats.js) builds the request body and normalizes the response,
|
|
7
|
+
* so this module only moves JSON.
|
|
8
8
|
*
|
|
9
|
-
* Plain fetch rather than
|
|
10
|
-
*
|
|
11
|
-
* breaks against OpenRouter.
|
|
9
|
+
* Plain fetch rather than vendor SDKs: the schemas are small, the tool needs
|
|
10
|
+
* its own abort signal and error mapping across hosts, and @typesafe-ai/sdk's
|
|
11
|
+
* model listing breaks against OpenRouter.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
@@ -19,17 +19,20 @@ const BACKOFF_MAX_MS = 5_000;
|
|
|
19
19
|
const MAX_RETRY_AFTER_MS = 30_000;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Error from a
|
|
22
|
+
* Error from a decision call. `retryable` marks failures worth repeating or
|
|
23
23
|
* failing over (network, timeout, 408/429/5xx, auth); `terminal` marks request
|
|
24
|
-
* faults that every host would reject the same way
|
|
24
|
+
* faults that every host of the same model would reject the same way;
|
|
25
|
+
* `notSent` marks a request refused by a local limit check before any network
|
|
26
|
+
* call, so it is not reported as a failed request.
|
|
25
27
|
*/
|
|
26
28
|
export class DecisionError extends Error {
|
|
27
|
-
constructor(message, { status = null, retryable = false, terminal = false, requestId = null, retryAfterMs = null } = {}) {
|
|
29
|
+
constructor(message, { status = null, retryable = false, terminal = false, notSent = false, requestId = null, retryAfterMs = null } = {}) {
|
|
28
30
|
super(message);
|
|
29
31
|
this.name = 'DecisionError';
|
|
30
32
|
this.status = status;
|
|
31
33
|
this.retryable = retryable;
|
|
32
34
|
this.terminal = terminal;
|
|
35
|
+
this.notSent = notSent;
|
|
33
36
|
this.requestId = requestId;
|
|
34
37
|
this.retryAfterMs = retryAfterMs;
|
|
35
38
|
}
|
|
@@ -55,11 +58,38 @@ function formatIssues(text) {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/**
|
|
58
|
-
*
|
|
59
|
-
* `
|
|
60
|
-
*
|
|
61
|
+
* Workers AI embeds the model's own error as JSON inside its message
|
|
62
|
+
* (`AiError: AiError: {"error":{...,"details":{"fieldErrors":...}}} (id)`);
|
|
63
|
+
* surface the validation details instead of the raw dump.
|
|
64
|
+
*/
|
|
65
|
+
function describeWorkersAiError(message) {
|
|
66
|
+
const embedded = message.match(/\{.*\}/s)?.[0];
|
|
67
|
+
let inner;
|
|
68
|
+
try {
|
|
69
|
+
inner = JSON.parse(embedded).error;
|
|
70
|
+
} catch {
|
|
71
|
+
return formatIssues(message);
|
|
72
|
+
}
|
|
73
|
+
if (typeof inner?.message !== 'string') return message;
|
|
74
|
+
const fields = Object.entries(inner.details?.fieldErrors ?? {})
|
|
75
|
+
.map(([field, problems]) => `${field}: ${[].concat(problems).join(', ')}`);
|
|
76
|
+
const forms = [].concat(inner.details?.formErrors ?? []);
|
|
77
|
+
const details = [...fields, ...forms];
|
|
78
|
+
return details.length ? `${inner.message} (${details.join('; ')})` : inner.message;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract a readable message from an error body. OpenRouter and OpenAI wrap
|
|
83
|
+
* errors as `{ error: { message } }`; TypeSafe answers
|
|
84
|
+
* `{ detail: { error_type, message } }`, or `{ detail }` as a string or a list
|
|
85
|
+
* of validation issues; Cloudflare answers `{ errors: [{ code, message }] }`.
|
|
61
86
|
*/
|
|
62
87
|
export function extractErrorMessage(body, rawText) {
|
|
88
|
+
if (Array.isArray(body?.errors) && body.errors.length > 0) {
|
|
89
|
+
return body.errors
|
|
90
|
+
.map((e) => (typeof e?.message === 'string' ? describeWorkersAiError(e.message) : JSON.stringify(e)))
|
|
91
|
+
.join('; ');
|
|
92
|
+
}
|
|
63
93
|
if (typeof body?.detail?.message === 'string') {
|
|
64
94
|
const type = body.detail.error_type ? `${body.detail.error_type}: ` : '';
|
|
65
95
|
return `${type}${body.detail.message}`;
|
|
@@ -150,7 +180,7 @@ function sleep(ms, signal) {
|
|
|
150
180
|
});
|
|
151
181
|
}
|
|
152
182
|
|
|
153
|
-
async function sendOnce({ url, headers, body, signal, timeoutMs }) {
|
|
183
|
+
async function sendOnce({ url, headers, body, parse, signal, timeoutMs }) {
|
|
154
184
|
const attemptSignal = signal
|
|
155
185
|
? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
156
186
|
: AbortSignal.timeout(timeoutMs);
|
|
@@ -184,8 +214,8 @@ async function sendOnce({ url, headers, body, signal, timeoutMs }) {
|
|
|
184
214
|
const status = response.status;
|
|
185
215
|
const detail = extractErrorMessage(parsed, rawText);
|
|
186
216
|
const html = describeHtmlError(detail);
|
|
187
|
-
//
|
|
188
|
-
//
|
|
217
|
+
// A firewall block repeats on retry, and on failover to any host that
|
|
218
|
+
// forwards to the same upstream.
|
|
189
219
|
const firewall = html?.firewall === true;
|
|
190
220
|
throw new DecisionError(`HTTP ${status}: ${html ? html.message : truncate(detail)}`, {
|
|
191
221
|
status,
|
|
@@ -194,42 +224,46 @@ async function sendOnce({ url, headers, body, signal, timeoutMs }) {
|
|
|
194
224
|
requestId:
|
|
195
225
|
response.headers.get('x-typesafe-request-id') ||
|
|
196
226
|
response.headers.get('x-generation-id') ||
|
|
227
|
+
response.headers.get('x-request-id') ||
|
|
197
228
|
html?.rayId ||
|
|
198
229
|
response.headers.get('cf-ray'),
|
|
199
230
|
retryAfterMs: parseRetryAfter(response.headers),
|
|
200
231
|
});
|
|
201
232
|
}
|
|
202
233
|
|
|
203
|
-
|
|
234
|
+
const result = parse(parsed);
|
|
235
|
+
if (!result || typeof result.answers !== 'object' || result.answers === null) {
|
|
204
236
|
throw new DecisionError('Malformed response: missing "answers" object', { status: response.status });
|
|
205
237
|
}
|
|
206
|
-
return
|
|
238
|
+
return result;
|
|
207
239
|
}
|
|
208
240
|
|
|
209
241
|
/**
|
|
210
242
|
* POST a decision request, retrying transient failures (except auth, which
|
|
211
243
|
* retrying cannot fix) with exponential backoff that honors Retry-After.
|
|
212
244
|
* @param {object} params
|
|
213
|
-
* @param {string} params.
|
|
245
|
+
* @param {string} params.url - Full endpoint URL
|
|
214
246
|
* @param {object} params.headers - Auth and attribution headers
|
|
215
|
-
* @param {object} params.body -
|
|
247
|
+
* @param {object} params.body - Request body in the host's wire format
|
|
248
|
+
* @param {(body: object|null) => object} [params.parse] - Maps the response
|
|
249
|
+
* body to `{ model, answers, usage, id }` in System One answer shapes
|
|
216
250
|
* @param {AbortSignal} [params.signal] - Caller cancellation
|
|
217
251
|
* @param {number} [params.timeoutMs] - Per-attempt timeout
|
|
218
252
|
* @param {number} [params.maxRetries] - Retries after the first attempt
|
|
219
|
-
* @returns {Promise<object>}
|
|
253
|
+
* @returns {Promise<object>} Normalized response
|
|
220
254
|
*/
|
|
221
|
-
export async function
|
|
222
|
-
|
|
255
|
+
export async function callDecisionApi({
|
|
256
|
+
url,
|
|
223
257
|
headers,
|
|
224
258
|
body,
|
|
259
|
+
parse = (parsed) => parsed,
|
|
225
260
|
signal,
|
|
226
261
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
227
262
|
maxRetries = DEFAULT_MAX_RETRIES,
|
|
228
263
|
}) {
|
|
229
|
-
const url = `${baseURL.replace(/\/+$/, '')}/v1/systemone`;
|
|
230
264
|
for (let attempt = 0; ; attempt++) {
|
|
231
265
|
try {
|
|
232
|
-
return await sendOnce({ url, headers, body, signal, timeoutMs });
|
|
266
|
+
return await sendOnce({ url, headers, body, parse, signal, timeoutMs });
|
|
233
267
|
} catch (error) {
|
|
234
268
|
const authFailure = error.status === 401 || error.status === 403;
|
|
235
269
|
if (!(error instanceof DecisionError) || !error.retryable || authFailure || attempt >= maxRetries) {
|
|
@@ -1,97 +1,181 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Decision Providers
|
|
3
3
|
*
|
|
4
|
-
* Hosts of
|
|
4
|
+
* Hosts of decision models: OpenAI's Decisions API, TypeSafe's Jev,
|
|
5
|
+
* Cloudflare's Clef, and OpenRouter, which re-serves all three. These are kept
|
|
5
6
|
* apart from the chat provider registry on purpose: a decision model takes a
|
|
6
7
|
* state plus typed questions and returns probabilities, never text, so it must
|
|
7
8
|
* never be reachable from `chat` routing ("auto", bare names, failover), and
|
|
8
9
|
* the `decide` tool must never reach a chat model.
|
|
9
10
|
*
|
|
10
11
|
* Spec grammar mirrors chat routing:
|
|
11
|
-
* - `auto` (or empty) — every configured
|
|
12
|
+
* - `auto` (or empty) — each AUTO_MODELS entry on every configured host, in order
|
|
12
13
|
* - `provider` / `provider:` — that provider's default model
|
|
13
14
|
* - `provider:model` — that provider only
|
|
14
|
-
* - `model` — every provider that serves the
|
|
15
|
-
*
|
|
15
|
+
* - `model` — every provider that serves the model, natives first; the first
|
|
16
|
+
* is used and the rest are failover candidates
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { getCustomHeaders as getOpenRouterAttributionHeaders } from '../providers/openrouter.js';
|
|
20
|
+
import { callDecisionApi } from './http.js';
|
|
21
|
+
import { cloudflareFormat, openaiFormat, systemOneFormat } from './formats.js';
|
|
19
22
|
|
|
20
|
-
/**
|
|
21
|
-
* Model catalogs map each provider's own model ID to the names that route to
|
|
22
|
-
* it. OpenRouter only knows `~typesafe/jev-latest` and `typesafe/jev-1.13`
|
|
23
|
-
* (it rejects `jev-1.13.0` and `jev-preview`), so native IDs are translated
|
|
24
|
-
* per provider rather than by a blanket prefix rewrite.
|
|
25
|
-
*/
|
|
26
23
|
export const DECISION_PROVIDERS = {
|
|
24
|
+
openai: {
|
|
25
|
+
name: 'openai',
|
|
26
|
+
label: 'OpenAI',
|
|
27
|
+
setup: 'set OPENAI_API_KEY',
|
|
28
|
+
defaultModel: 'gpt-6-luna',
|
|
29
|
+
format: openaiFormat,
|
|
30
|
+
endpoint: () => 'https://api.openai.com/v1/decisions',
|
|
31
|
+
headers: (config) => ({ Authorization: `Bearer ${config.apiKeys.openai}` }),
|
|
32
|
+
isAvailable: (config) => Boolean(config?.apiKeys?.openai),
|
|
33
|
+
passthrough: () => false,
|
|
34
|
+
},
|
|
27
35
|
typesafe: {
|
|
28
36
|
name: 'typesafe',
|
|
29
37
|
label: 'TypeSafe',
|
|
30
|
-
|
|
31
|
-
apiKeyEnv: 'TYPESAFE_API_KEY',
|
|
38
|
+
setup: 'set TYPESAFE_API_KEY',
|
|
32
39
|
defaultModel: 'jev-latest',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'jev-1.13.0': ['jev-1.13', 'typesafe/jev-1.13'],
|
|
36
|
-
'jev-preview': [],
|
|
37
|
-
},
|
|
38
|
-
// TypeSafe accepts every published versioned ID, listed or not.
|
|
39
|
-
passthrough: (name) => /^jev-\d+\.\d+(\.\d+)?$/i.test(name),
|
|
40
|
+
format: systemOneFormat,
|
|
41
|
+
endpoint: () => 'https://api.typesafe.ai/v1/systemone',
|
|
40
42
|
headers: (config) => ({ Authorization: `Bearer ${config.apiKeys.typesafe}` }),
|
|
41
43
|
isAvailable: (config) => Boolean(config?.apiKeys?.typesafe),
|
|
44
|
+
// TypeSafe accepts every published versioned ID, listed or not.
|
|
45
|
+
passthrough: (name) => /^jev-\d+\.\d+(\.\d+)?$/i.test(name),
|
|
46
|
+
},
|
|
47
|
+
cloudflare: {
|
|
48
|
+
name: 'cloudflare',
|
|
49
|
+
label: 'Cloudflare',
|
|
50
|
+
setup: 'set CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN',
|
|
51
|
+
defaultModel: 'clef',
|
|
52
|
+
format: cloudflareFormat,
|
|
53
|
+
// Workers AI addresses the model in the path and checks that the body's
|
|
54
|
+
// bare `model` matches it.
|
|
55
|
+
endpoint: (config, model) =>
|
|
56
|
+
`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(config.providers.cloudflareaccountid)}/ai/run/@cf/cloudflare/${model}`,
|
|
57
|
+
headers: (config) => ({ Authorization: `Bearer ${config.providers.cloudflareapitoken}` }),
|
|
58
|
+
isAvailable: (config) =>
|
|
59
|
+
Boolean(config?.providers?.cloudflareaccountid && config?.providers?.cloudflareapitoken),
|
|
60
|
+
passthrough: () => false,
|
|
42
61
|
},
|
|
43
62
|
openrouter: {
|
|
44
63
|
name: 'openrouter',
|
|
45
64
|
label: 'OpenRouter',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'~typesafe/jev-latest': ['jev-latest', 'jev'],
|
|
51
|
-
'typesafe/jev-1.13': ['jev-1.13', 'jev-1.13.0'],
|
|
52
|
-
},
|
|
53
|
-
// Decision models OpenRouter adds later are reachable by full slug.
|
|
54
|
-
passthrough: (name) => name.includes('/'),
|
|
65
|
+
setup: 'set OPENROUTER_API_KEY',
|
|
66
|
+
defaultModel: 'gpt-6-luna',
|
|
67
|
+
format: systemOneFormat,
|
|
68
|
+
endpoint: () => 'https://openrouter.ai/api/v1/systemone',
|
|
55
69
|
headers: (config) => ({
|
|
56
70
|
Authorization: `Bearer ${config.apiKeys.openrouter}`,
|
|
57
71
|
...getOpenRouterAttributionHeaders(config),
|
|
58
72
|
}),
|
|
59
73
|
isAvailable: (config) => Boolean(config?.apiKeys?.openrouter),
|
|
74
|
+
// Decision models OpenRouter adds later are reachable by full slug.
|
|
75
|
+
passthrough: (name) => name.includes('/'),
|
|
60
76
|
},
|
|
61
77
|
};
|
|
62
78
|
|
|
63
|
-
/** Failover order:
|
|
64
|
-
export const DECISION_PROVIDER_PRIORITY = ['typesafe', 'openrouter'];
|
|
79
|
+
/** Failover order: each model's native host first, then OpenRouter. */
|
|
80
|
+
export const DECISION_PROVIDER_PRIORITY = ['openai', 'typesafe', 'cloudflare', 'openrouter'];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Decision models by canonical name. `hosts` maps each provider that serves
|
|
84
|
+
* the model to that provider's own ID: OpenRouter only knows
|
|
85
|
+
* `~typesafe/jev-latest` and `typesafe/jev-1.13` (it rejects `jev-1.13.0` and
|
|
86
|
+
* `jev-preview`), so IDs are translated per host rather than by a blanket
|
|
87
|
+
* prefix rewrite. Every host ID also routes to its model. `maxImages` is how
|
|
88
|
+
* many images the model takes per call; a host also needs a format that can
|
|
89
|
+
* send them.
|
|
90
|
+
*/
|
|
91
|
+
export const DECISION_MODELS = {
|
|
92
|
+
'gpt-6-luna': {
|
|
93
|
+
aliases: ['luna', 'gpt-6-luna-decisions'],
|
|
94
|
+
hosts: { openai: 'gpt-6-luna', openrouter: 'openai/gpt-6-luna-decisions' },
|
|
95
|
+
// OpenAI documents no image count limit; the API enforces its own.
|
|
96
|
+
maxImages: Infinity,
|
|
97
|
+
},
|
|
98
|
+
'jev-latest': {
|
|
99
|
+
aliases: ['jev'],
|
|
100
|
+
hosts: { typesafe: 'jev-latest', openrouter: '~typesafe/jev-latest' },
|
|
101
|
+
maxImages: 0,
|
|
102
|
+
},
|
|
103
|
+
'jev-1.13': {
|
|
104
|
+
aliases: [],
|
|
105
|
+
hosts: { typesafe: 'jev-1.13.0', openrouter: 'typesafe/jev-1.13' },
|
|
106
|
+
maxImages: 0,
|
|
107
|
+
},
|
|
108
|
+
'jev-preview': {
|
|
109
|
+
aliases: [],
|
|
110
|
+
hosts: { typesafe: 'jev-preview' },
|
|
111
|
+
maxImages: 0,
|
|
112
|
+
},
|
|
113
|
+
clef: {
|
|
114
|
+
aliases: ['@cf/cloudflare/clef'],
|
|
115
|
+
hosts: { cloudflare: 'clef', openrouter: 'cloudflare/clef' },
|
|
116
|
+
maxImages: 4,
|
|
117
|
+
},
|
|
118
|
+
'clef-flash': {
|
|
119
|
+
aliases: ['@cf/cloudflare/clef-flash'],
|
|
120
|
+
hosts: { cloudflare: 'clef-flash', openrouter: 'cloudflare/clef-flash' },
|
|
121
|
+
maxImages: 4,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** What `auto` tries, in order: OpenAI's model first, then Jev, then Clef. */
|
|
126
|
+
export const AUTO_MODELS = ['gpt-6-luna', 'jev-latest', 'clef'];
|
|
127
|
+
|
|
128
|
+
function findCanonicalModel(name) {
|
|
129
|
+
const wanted = name.toLowerCase();
|
|
130
|
+
for (const [canonical, entry] of Object.entries(DECISION_MODELS)) {
|
|
131
|
+
const names = [canonical, ...entry.aliases, ...Object.values(entry.hosts)];
|
|
132
|
+
if (names.some((n) => n.toLowerCase() === wanted)) return canonical;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
65
136
|
|
|
66
137
|
/**
|
|
67
|
-
* Resolve a name
|
|
68
|
-
*
|
|
138
|
+
* Resolve a name on one provider: a catalog model it hosts, then its
|
|
139
|
+
* passthrough rule.
|
|
140
|
+
* @returns {{ model: string, family: string, maxImages: number }|null} The ID
|
|
141
|
+
* to send to that provider, the model it names (hosts of one family share
|
|
142
|
+
* failures), and how many images this host can send that model
|
|
69
143
|
*/
|
|
70
144
|
export function findDecisionModel(provider, name) {
|
|
71
|
-
const
|
|
72
|
-
if (!
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
145
|
+
const trimmed = String(name).trim();
|
|
146
|
+
if (!trimmed) return null;
|
|
147
|
+
const canonical = findCanonicalModel(trimmed);
|
|
148
|
+
const hosted = canonical ? DECISION_MODELS[canonical].hosts[provider.name] : undefined;
|
|
149
|
+
if (hosted) {
|
|
150
|
+
const maxImages = provider.format.images ? DECISION_MODELS[canonical].maxImages : 0;
|
|
151
|
+
return { model: hosted, family: canonical, maxImages };
|
|
77
152
|
}
|
|
78
|
-
return provider.passthrough(
|
|
153
|
+
return provider.passthrough(trimmed) ? { model: trimmed, family: trimmed, maxImages: 0 } : null;
|
|
79
154
|
}
|
|
80
155
|
|
|
81
|
-
function
|
|
82
|
-
return
|
|
156
|
+
function servedBy(providerName) {
|
|
157
|
+
return Object.entries(DECISION_MODELS)
|
|
158
|
+
.filter(([, entry]) => entry.hosts[providerName])
|
|
159
|
+
.map(([canonical]) => canonical);
|
|
83
160
|
}
|
|
84
161
|
|
|
85
162
|
function knownModels() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
163
|
+
return Object.entries(DECISION_MODELS).flatMap(([canonical, entry]) => [canonical, ...entry.aliases]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function candidate(provider, found) {
|
|
167
|
+
return { providerName: provider.name, provider, ...found };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Every provider that serves a name, in priority order. */
|
|
171
|
+
function hostsOf(name) {
|
|
172
|
+
return DECISION_PROVIDER_PRIORITY
|
|
173
|
+
.map((providerName) => {
|
|
174
|
+
const provider = DECISION_PROVIDERS[providerName];
|
|
175
|
+
const found = findDecisionModel(provider, name);
|
|
176
|
+
return found ? candidate(provider, found) : null;
|
|
177
|
+
})
|
|
178
|
+
.filter(Boolean);
|
|
95
179
|
}
|
|
96
180
|
|
|
97
181
|
function ok(candidates) {
|
|
@@ -106,21 +190,18 @@ function fail(status, error) {
|
|
|
106
190
|
* Resolve a decision model spec into an ordered candidate list.
|
|
107
191
|
* @param {string} spec - Model spec (see module grammar)
|
|
108
192
|
* @param {object} config - Server configuration (for API keys)
|
|
109
|
-
* @returns {{ status: 'ok'|'unknown'|'unavailable', candidates: Array<{ providerName: string, provider: object, model: string }>, error: string|null }}
|
|
193
|
+
* @returns {{ status: 'ok'|'unknown'|'unavailable', candidates: Array<{ providerName: string, provider: object, model: string, family: string }>, error: string|null }}
|
|
110
194
|
*/
|
|
111
195
|
export function resolveDecisionModel(spec, config) {
|
|
112
196
|
const raw = String(spec ?? '').trim();
|
|
113
197
|
const namespaces = DECISION_PROVIDER_PRIORITY.join(', ');
|
|
114
198
|
|
|
115
199
|
if (!raw || raw.toLowerCase() === 'auto') {
|
|
116
|
-
const candidates =
|
|
117
|
-
.map((name) => DECISION_PROVIDERS[name])
|
|
118
|
-
.filter((provider) => provider.isAvailable(config))
|
|
119
|
-
.map((provider) => ({ providerName: provider.name, provider, model: provider.defaultModel }));
|
|
200
|
+
const candidates = AUTO_MODELS.flatMap(hostsOf).filter((c) => c.provider.isAvailable(config));
|
|
120
201
|
if (candidates.length === 0) {
|
|
121
202
|
return fail(
|
|
122
203
|
'unavailable',
|
|
123
|
-
`No decision provider is configured: ${DECISION_PROVIDER_PRIORITY.map((n) => `${n} (${
|
|
204
|
+
`No decision provider is configured: ${DECISION_PROVIDER_PRIORITY.map((n) => `${n} (${DECISION_PROVIDERS[n].setup})`).join('; ')}.`,
|
|
124
205
|
);
|
|
125
206
|
}
|
|
126
207
|
return ok(candidates);
|
|
@@ -136,27 +217,17 @@ export function resolveDecisionModel(spec, config) {
|
|
|
136
217
|
return fail('unknown', `Unknown decision provider "${raw.slice(0, colon)}". Providers: ${namespaces}.`);
|
|
137
218
|
}
|
|
138
219
|
const name = hasNamespace ? raw.slice(colon + 1).trim() : '';
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
return fail(
|
|
142
|
-
'unknown',
|
|
143
|
-
`${provider.label} does not serve "${name}". Models: ${Object.keys(provider.models).join(', ')}.`,
|
|
144
|
-
);
|
|
220
|
+
const found = findDecisionModel(provider, name || provider.defaultModel);
|
|
221
|
+
if (!found) {
|
|
222
|
+
return fail('unknown', `${provider.label} does not serve "${name}". Models: ${servedBy(provider.name).join(', ')}.`);
|
|
145
223
|
}
|
|
146
224
|
if (!provider.isAvailable(config)) {
|
|
147
|
-
return fail('unavailable', `${provider.label} is not configured (${
|
|
225
|
+
return fail('unavailable', `${provider.label} is not configured (${provider.setup}).`);
|
|
148
226
|
}
|
|
149
|
-
return ok([
|
|
227
|
+
return ok([candidate(provider, found)]);
|
|
150
228
|
}
|
|
151
229
|
|
|
152
|
-
const matches =
|
|
153
|
-
.map((providerName) => {
|
|
154
|
-
const provider = DECISION_PROVIDERS[providerName];
|
|
155
|
-
const model = findDecisionModel(provider, raw);
|
|
156
|
-
return model ? { providerName, provider, model } : null;
|
|
157
|
-
})
|
|
158
|
-
.filter(Boolean);
|
|
159
|
-
|
|
230
|
+
const matches = hostsOf(raw);
|
|
160
231
|
if (matches.length === 0) {
|
|
161
232
|
return fail(
|
|
162
233
|
'unknown',
|
|
@@ -167,8 +238,31 @@ export function resolveDecisionModel(spec, config) {
|
|
|
167
238
|
if (available.length === 0) {
|
|
168
239
|
return fail(
|
|
169
240
|
'unavailable',
|
|
170
|
-
`"${raw}" is served by ${matches.map((m) => `${m.providerName} (${
|
|
241
|
+
`"${raw}" is served by ${matches.map((m) => `${m.providerName} (${m.provider.setup})`).join('; ')}, but none is configured.`,
|
|
171
242
|
);
|
|
172
243
|
}
|
|
173
244
|
return ok(available);
|
|
174
245
|
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Ask one candidate: translate the System One request into the host's wire
|
|
249
|
+
* format, send it, and map the answers back.
|
|
250
|
+
* @param {object} candidate - From resolveDecisionModel
|
|
251
|
+
* @param {object} params
|
|
252
|
+
* @param {string|object|Array} params.state
|
|
253
|
+
* @param {object} params.questions - System One question map
|
|
254
|
+
* @param {Array<{ mimeType: string, base64: string }>} [params.images]
|
|
255
|
+
* @param {object} params.config - Server configuration
|
|
256
|
+
* @param {AbortSignal} [params.signal]
|
|
257
|
+
* @returns {Promise<{ model?: string, answers: object, usage?: object, id?: string }>}
|
|
258
|
+
*/
|
|
259
|
+
export async function askDecisionModel(candidate, { state, questions, images = [], config, signal }) {
|
|
260
|
+
const { provider, model } = candidate;
|
|
261
|
+
return callDecisionApi({
|
|
262
|
+
url: provider.endpoint(config, model),
|
|
263
|
+
headers: provider.headers(config),
|
|
264
|
+
body: provider.format.request(model, state, questions, images),
|
|
265
|
+
parse: provider.format.parse,
|
|
266
|
+
signal,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
@@ -339,6 +339,37 @@ const SUPPORTED_MODELS = {
|
|
|
339
339
|
'claude-sonnet-4-5',
|
|
340
340
|
],
|
|
341
341
|
},
|
|
342
|
+
'claude-haiku-5-5': {
|
|
343
|
+
modelName: 'claude-haiku-5-5',
|
|
344
|
+
friendlyName: 'Claude Haiku 5.5',
|
|
345
|
+
contextWindow: 1000000, // 1M context by default - no beta header required
|
|
346
|
+
maxOutputTokens: 128000,
|
|
347
|
+
supportsStreaming: true,
|
|
348
|
+
supportsImages: true,
|
|
349
|
+
supportsWebSearch: false,
|
|
350
|
+
supportsThinking: true,
|
|
351
|
+
supportsAdaptiveThinking: true, // budget_tokens is rejected; adaptive is the only on-mode
|
|
352
|
+
timeout: 1800000,
|
|
353
|
+
supportsEffort: true,
|
|
354
|
+
effortGA: true,
|
|
355
|
+
effortTiers: EFFORT_TIERS_FULL,
|
|
356
|
+
supportsCompaction: true,
|
|
357
|
+
// No server-side refusal fallback, so `fallbacks` must not be sent
|
|
358
|
+
description:
|
|
359
|
+
'Claude Haiku 5.5 - Current Haiku: fastest and cheapest Claude for high-volume and latency-sensitive work',
|
|
360
|
+
aliases: [
|
|
361
|
+
'claude-haiku-5-5',
|
|
362
|
+
'claude-haiku-5.5',
|
|
363
|
+
'claude-5.5-haiku',
|
|
364
|
+
'claude-5-5-haiku',
|
|
365
|
+
'haiku-5.5',
|
|
366
|
+
'haiku-5-5',
|
|
367
|
+
'haiku5.5',
|
|
368
|
+
'haiku5-5',
|
|
369
|
+
'haiku',
|
|
370
|
+
'claude-haiku',
|
|
371
|
+
],
|
|
372
|
+
},
|
|
342
373
|
'claude-haiku-4-5-20251001': {
|
|
343
374
|
modelName: 'claude-haiku-4-5-20251001',
|
|
344
375
|
friendlyName: 'Claude Haiku 4.5',
|
|
@@ -352,7 +383,7 @@ const SUPPORTED_MODELS = {
|
|
|
352
383
|
maxThinkingTokens: 64000,
|
|
353
384
|
timeout: 900000,
|
|
354
385
|
description:
|
|
355
|
-
'Claude Haiku 4.5 -
|
|
386
|
+
'Claude Haiku 4.5 - Previous Haiku generation with extended thinking',
|
|
356
387
|
aliases: [
|
|
357
388
|
'claude-haiku-4-5',
|
|
358
389
|
'claude-4.5-haiku',
|
|
@@ -362,8 +393,6 @@ const SUPPORTED_MODELS = {
|
|
|
362
393
|
'claude-haiku-4.5',
|
|
363
394
|
'haiku4.5',
|
|
364
395
|
'claude-haiku-4',
|
|
365
|
-
'haiku',
|
|
366
|
-
'claude-haiku',
|
|
367
396
|
],
|
|
368
397
|
},
|
|
369
398
|
};
|
package/src/providers/claude.js
CHANGED
|
@@ -112,6 +112,25 @@ const SUPPORTED_MODELS = {
|
|
|
112
112
|
'sonnet-5.5',
|
|
113
113
|
],
|
|
114
114
|
},
|
|
115
|
+
'claude-haiku-5-5': {
|
|
116
|
+
modelName: 'claude-haiku-5-5',
|
|
117
|
+
friendlyName: 'Claude Haiku 5.5 (via Agent SDK)',
|
|
118
|
+
contextWindow: 1000000,
|
|
119
|
+
maxOutputTokens: 128000,
|
|
120
|
+
supportsStreaming: true,
|
|
121
|
+
supportsImages: true,
|
|
122
|
+
supportsWebSearch: false,
|
|
123
|
+
timeout: 1800000,
|
|
124
|
+
description:
|
|
125
|
+
'Claude Haiku 5.5 via Agent SDK - requires claude login authentication',
|
|
126
|
+
aliases: [
|
|
127
|
+
'haiku',
|
|
128
|
+
'claude-haiku',
|
|
129
|
+
'claude-haiku-5.5',
|
|
130
|
+
'haiku-5-5',
|
|
131
|
+
'haiku-5.5',
|
|
132
|
+
],
|
|
133
|
+
},
|
|
115
134
|
};
|
|
116
135
|
|
|
117
136
|
/**
|
package/src/providers/copilot.js
CHANGED
|
@@ -125,6 +125,18 @@ const SUPPORTED_MODELS = {
|
|
|
125
125
|
description: 'Anthropic Claude Opus 5.5 via Copilot subscription',
|
|
126
126
|
aliases: ['opus', 'claude', 'claude-opus-5-5'],
|
|
127
127
|
},
|
|
128
|
+
'claude-fable-5.1': {
|
|
129
|
+
modelName: 'claude-fable-5.1',
|
|
130
|
+
friendlyName: 'Claude Fable 5.1 (via Copilot)',
|
|
131
|
+
contextWindow: 200000,
|
|
132
|
+
maxOutputTokens: 32768,
|
|
133
|
+
supportsStreaming: true,
|
|
134
|
+
supportsImages: false,
|
|
135
|
+
supportsWebSearch: false,
|
|
136
|
+
timeout: 1800000,
|
|
137
|
+
description: 'Anthropic Claude Fable 5.1 via Copilot subscription',
|
|
138
|
+
aliases: ['fable', 'claude-fable-5-1'],
|
|
139
|
+
},
|
|
128
140
|
'claude-fable-5': {
|
|
129
141
|
modelName: 'claude-fable-5',
|
|
130
142
|
friendlyName: 'Claude Fable 5 (via Copilot)',
|
|
@@ -135,7 +147,7 @@ const SUPPORTED_MODELS = {
|
|
|
135
147
|
supportsWebSearch: false,
|
|
136
148
|
timeout: 1800000,
|
|
137
149
|
description: 'Anthropic Claude Fable 5 via Copilot subscription',
|
|
138
|
-
aliases: [
|
|
150
|
+
aliases: [],
|
|
139
151
|
},
|
|
140
152
|
'claude-sonnet-5.5': {
|
|
141
153
|
modelName: 'claude-sonnet-5.5',
|
|
@@ -149,6 +161,18 @@ const SUPPORTED_MODELS = {
|
|
|
149
161
|
description: 'Anthropic Claude Sonnet 5.5 via Copilot subscription',
|
|
150
162
|
aliases: ['sonnet', 'claude-sonnet-5-5'],
|
|
151
163
|
},
|
|
164
|
+
'claude-haiku-5.5': {
|
|
165
|
+
modelName: 'claude-haiku-5.5',
|
|
166
|
+
friendlyName: 'Claude Haiku 5.5 (via Copilot)',
|
|
167
|
+
contextWindow: 200000,
|
|
168
|
+
maxOutputTokens: 32768,
|
|
169
|
+
supportsStreaming: true,
|
|
170
|
+
supportsImages: false,
|
|
171
|
+
supportsWebSearch: false,
|
|
172
|
+
timeout: 1800000,
|
|
173
|
+
description: 'Anthropic Claude Haiku 5.5 via Copilot subscription',
|
|
174
|
+
aliases: ['haiku', 'claude-haiku-5-5'],
|
|
175
|
+
},
|
|
152
176
|
'claude-sonnet-5': {
|
|
153
177
|
modelName: 'claude-sonnet-5',
|
|
154
178
|
friendlyName: 'Claude Sonnet 5 (via Copilot)',
|