converse-mcp-server 4.5.2 → 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.
@@ -1,14 +1,14 @@
1
1
  /**
2
- * System One HTTP client
2
+ * Decision API HTTP client
3
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.
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 @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.
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 System One call. `retryable` marks failures worth repeating or
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
- * 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.
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
- // Every host forwards to TypeSafe's edge, so a firewall block repeats on
188
- // retry and on failover alike.
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
- if (!parsed || typeof parsed.answers !== 'object' || parsed.answers === null) {
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 parsed;
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.baseURL - Host base, e.g. https://api.typesafe.ai
242
+ * @param {string} params.url - Full endpoint URL
214
243
  * @param {object} params.headers - Auth and attribution headers
215
- * @param {object} params.body - `{ model, state, questions }`
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>} Parsed response body
250
+ * @returns {Promise<object>} Normalized response
220
251
  */
221
- export async function callSystemOne({
222
- baseURL,
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 System One decision models (TypeSafe's Jev family). These are kept
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 provider's default, in priority order
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 name, in priority order; the
15
- * first is used and the rest are failover candidates
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
- baseURL: 'https://api.typesafe.ai',
31
- apiKeyEnv: 'TYPESAFE_API_KEY',
38
+ setup: 'set TYPESAFE_API_KEY',
32
39
  defaultModel: 'jev-latest',
33
- models: {
34
- 'jev-latest': ['jev', '~typesafe/jev-latest'],
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
- baseURL: 'https://openrouter.ai/api',
47
- apiKeyEnv: 'OPENROUTER_API_KEY',
48
- defaultModel: '~typesafe/jev-latest',
49
- models: {
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: the native host first, then OpenRouter. */
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 against one provider's catalog, then its passthrough rule.
68
- * @returns {string|null} The model ID to send to that provider
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 wanted = String(name).trim().toLowerCase();
72
- if (!wanted) return null;
73
- for (const [id, aliases] of Object.entries(provider.models)) {
74
- if (id.toLowerCase() === wanted || aliases.some((a) => a.toLowerCase() === wanted)) {
75
- return id;
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(String(name).trim()) ? String(name).trim() : null;
153
+ return provider.passthrough(trimmed) ? { model: trimmed, family: trimmed, maxImages: 0 } : null;
79
154
  }
80
155
 
81
- function setupHint(provider) {
82
- return `set ${provider.apiKeyEnv}`;
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
- const names = new Set();
87
- for (const providerName of DECISION_PROVIDER_PRIORITY) {
88
- const provider = DECISION_PROVIDERS[providerName];
89
- for (const [id, aliases] of Object.entries(provider.models)) {
90
- names.add(id);
91
- aliases.forEach((a) => names.add(a));
92
- }
93
- }
94
- return [...names];
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 = DECISION_PROVIDER_PRIORITY
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} (${setupHint(DECISION_PROVIDERS[n])})`).join('; ')}.`,
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 model = name ? findDecisionModel(provider, name) : provider.defaultModel;
140
- if (!model) {
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 (${setupHint(provider)}).`);
225
+ return fail('unavailable', `${provider.label} is not configured (${provider.setup}).`);
148
226
  }
149
- return ok([{ providerName: provider.name, provider, model }]);
227
+ return ok([candidate(provider, found)]);
150
228
  }
151
229
 
152
- const matches = DECISION_PROVIDER_PRIORITY
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} (${setupHint(m.provider)})`).join('; ')}, but none is configured.`,
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 - Fast and intelligent model with extended thinking',
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
  };
@@ -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
  /**
@@ -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: ['fable'],
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)',