converse-mcp-server 4.5.3 → 4.6.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 +7 -0
- package/README.md +5 -3
- package/docs/API.md +43 -19
- package/docs/PROVIDERS.md +6 -4
- package/package.json +10 -10
- package/src/config.js +16 -1
- package/src/decisionProviders/formats.js +229 -0
- package/src/decisionProviders/{systemOne.js → http.js} +56 -25
- 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 +138 -29
|
@@ -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,9 +19,9 @@ 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
25
|
*/
|
|
26
26
|
export class DecisionError extends Error {
|
|
27
27
|
constructor(message, { status = null, retryable = false, terminal = false, requestId = null, retryAfterMs = null } = {}) {
|
|
@@ -55,11 +55,38 @@ function formatIssues(text) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
* `
|
|
60
|
-
*
|
|
58
|
+
* Workers AI embeds the model's own error as JSON inside its message
|
|
59
|
+
* (`AiError: AiError: {"error":{...,"details":{"fieldErrors":...}}} (id)`);
|
|
60
|
+
* surface the validation details instead of the raw dump.
|
|
61
|
+
*/
|
|
62
|
+
function describeWorkersAiError(message) {
|
|
63
|
+
const embedded = message.match(/\{.*\}/s)?.[0];
|
|
64
|
+
let inner;
|
|
65
|
+
try {
|
|
66
|
+
inner = JSON.parse(embedded).error;
|
|
67
|
+
} catch {
|
|
68
|
+
return formatIssues(message);
|
|
69
|
+
}
|
|
70
|
+
if (typeof inner?.message !== 'string') return message;
|
|
71
|
+
const fields = Object.entries(inner.details?.fieldErrors ?? {})
|
|
72
|
+
.map(([field, problems]) => `${field}: ${[].concat(problems).join(', ')}`);
|
|
73
|
+
const forms = [].concat(inner.details?.formErrors ?? []);
|
|
74
|
+
const details = [...fields, ...forms];
|
|
75
|
+
return details.length ? `${inner.message} (${details.join('; ')})` : inner.message;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extract a readable message from an error body. OpenRouter and OpenAI wrap
|
|
80
|
+
* errors as `{ error: { message } }`; TypeSafe answers
|
|
81
|
+
* `{ detail: { error_type, message } }`, or `{ detail }` as a string or a list
|
|
82
|
+
* of validation issues; Cloudflare answers `{ errors: [{ code, message }] }`.
|
|
61
83
|
*/
|
|
62
84
|
export function extractErrorMessage(body, rawText) {
|
|
85
|
+
if (Array.isArray(body?.errors) && body.errors.length > 0) {
|
|
86
|
+
return body.errors
|
|
87
|
+
.map((e) => (typeof e?.message === 'string' ? describeWorkersAiError(e.message) : JSON.stringify(e)))
|
|
88
|
+
.join('; ');
|
|
89
|
+
}
|
|
63
90
|
if (typeof body?.detail?.message === 'string') {
|
|
64
91
|
const type = body.detail.error_type ? `${body.detail.error_type}: ` : '';
|
|
65
92
|
return `${type}${body.detail.message}`;
|
|
@@ -150,7 +177,7 @@ function sleep(ms, signal) {
|
|
|
150
177
|
});
|
|
151
178
|
}
|
|
152
179
|
|
|
153
|
-
async function sendOnce({ url, headers, body, signal, timeoutMs }) {
|
|
180
|
+
async function sendOnce({ url, headers, body, parse, signal, timeoutMs }) {
|
|
154
181
|
const attemptSignal = signal
|
|
155
182
|
? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
156
183
|
: AbortSignal.timeout(timeoutMs);
|
|
@@ -184,8 +211,8 @@ async function sendOnce({ url, headers, body, signal, timeoutMs }) {
|
|
|
184
211
|
const status = response.status;
|
|
185
212
|
const detail = extractErrorMessage(parsed, rawText);
|
|
186
213
|
const html = describeHtmlError(detail);
|
|
187
|
-
//
|
|
188
|
-
//
|
|
214
|
+
// A firewall block repeats on retry, and on failover to any host that
|
|
215
|
+
// forwards to the same upstream.
|
|
189
216
|
const firewall = html?.firewall === true;
|
|
190
217
|
throw new DecisionError(`HTTP ${status}: ${html ? html.message : truncate(detail)}`, {
|
|
191
218
|
status,
|
|
@@ -194,42 +221,46 @@ async function sendOnce({ url, headers, body, signal, timeoutMs }) {
|
|
|
194
221
|
requestId:
|
|
195
222
|
response.headers.get('x-typesafe-request-id') ||
|
|
196
223
|
response.headers.get('x-generation-id') ||
|
|
224
|
+
response.headers.get('x-request-id') ||
|
|
197
225
|
html?.rayId ||
|
|
198
226
|
response.headers.get('cf-ray'),
|
|
199
227
|
retryAfterMs: parseRetryAfter(response.headers),
|
|
200
228
|
});
|
|
201
229
|
}
|
|
202
230
|
|
|
203
|
-
|
|
231
|
+
const result = parse(parsed);
|
|
232
|
+
if (!result || typeof result.answers !== 'object' || result.answers === null) {
|
|
204
233
|
throw new DecisionError('Malformed response: missing "answers" object', { status: response.status });
|
|
205
234
|
}
|
|
206
|
-
return
|
|
235
|
+
return result;
|
|
207
236
|
}
|
|
208
237
|
|
|
209
238
|
/**
|
|
210
239
|
* POST a decision request, retrying transient failures (except auth, which
|
|
211
240
|
* retrying cannot fix) with exponential backoff that honors Retry-After.
|
|
212
241
|
* @param {object} params
|
|
213
|
-
* @param {string} params.
|
|
242
|
+
* @param {string} params.url - Full endpoint URL
|
|
214
243
|
* @param {object} params.headers - Auth and attribution headers
|
|
215
|
-
* @param {object} params.body -
|
|
244
|
+
* @param {object} params.body - Request body in the host's wire format
|
|
245
|
+
* @param {(body: object|null) => object} [params.parse] - Maps the response
|
|
246
|
+
* body to `{ model, answers, usage, id }` in System One answer shapes
|
|
216
247
|
* @param {AbortSignal} [params.signal] - Caller cancellation
|
|
217
248
|
* @param {number} [params.timeoutMs] - Per-attempt timeout
|
|
218
249
|
* @param {number} [params.maxRetries] - Retries after the first attempt
|
|
219
|
-
* @returns {Promise<object>}
|
|
250
|
+
* @returns {Promise<object>} Normalized response
|
|
220
251
|
*/
|
|
221
|
-
export async function
|
|
222
|
-
|
|
252
|
+
export async function callDecisionApi({
|
|
253
|
+
url,
|
|
223
254
|
headers,
|
|
224
255
|
body,
|
|
256
|
+
parse = (parsed) => parsed,
|
|
225
257
|
signal,
|
|
226
258
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
227
259
|
maxRetries = DEFAULT_MAX_RETRIES,
|
|
228
260
|
}) {
|
|
229
|
-
const url = `${baseURL.replace(/\/+$/, '')}/v1/systemone`;
|
|
230
261
|
for (let attempt = 0; ; attempt++) {
|
|
231
262
|
try {
|
|
232
|
-
return await sendOnce({ url, headers, body, signal, timeoutMs });
|
|
263
|
+
return await sendOnce({ url, headers, body, parse, signal, timeoutMs });
|
|
233
264
|
} catch (error) {
|
|
234
265
|
const authFailure = error.status === 401 || error.status === 403;
|
|
235
266
|
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)',
|