@bubblebrain-ai/bubble 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,544 @@
1
+ import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
2
+ const ANTHROPIC_VERSION = "2023-06-01";
3
+ const DEFAULT_MAX_TOKENS = 8192;
4
+ export function createAnthropicMessagesProvider(options) {
5
+ async function* streamChat(messages, chatOptions) {
6
+ const body = buildAnthropicRequest(options, messages, {
7
+ model: chatOptions.model,
8
+ tools: chatOptions.tools,
9
+ temperature: chatOptions.temperature,
10
+ thinkingLevel: chatOptions.thinkingLevel,
11
+ stream: true,
12
+ });
13
+ const response = await fetchAnthropicResponseWithRetry(options, {
14
+ url: resolveAnthropicMessagesUrl(options.baseURL),
15
+ stream: true,
16
+ method: "POST",
17
+ body: JSON.stringify(body),
18
+ signal: chatOptions.abortSignal,
19
+ });
20
+ yield* translateAnthropicStream(readSseEvents(response));
21
+ yield { type: "done" };
22
+ }
23
+ async function complete(messages, chatOptions) {
24
+ const body = buildAnthropicRequest(options, messages, {
25
+ model: chatOptions?.model ?? "claude-sonnet-4-6",
26
+ temperature: chatOptions?.temperature,
27
+ thinkingLevel: chatOptions?.thinkingLevel,
28
+ stream: false,
29
+ });
30
+ const response = await fetchAnthropicResponseWithRetry(options, {
31
+ url: resolveAnthropicMessagesUrl(options.baseURL),
32
+ stream: false,
33
+ method: "POST",
34
+ body: JSON.stringify(body),
35
+ signal: chatOptions?.abortSignal,
36
+ });
37
+ const data = await response.json();
38
+ return extractAnthropicText(data.content).join("");
39
+ }
40
+ return { streamChat, complete };
41
+ }
42
+ export function buildAnthropicRequest(options, messages, chatOptions) {
43
+ const { system, messages: anthropicMessages } = toAnthropicMessages(messages, shouldEchoThinking(options.providerId));
44
+ const tools = chatOptions.tools?.map((tool) => ({
45
+ name: tool.name,
46
+ description: tool.description,
47
+ input_schema: tool.parameters,
48
+ }));
49
+ const body = {
50
+ model: chatOptions.model,
51
+ max_tokens: DEFAULT_MAX_TOKENS,
52
+ system: system || undefined,
53
+ messages: anthropicMessages,
54
+ tools: tools && tools.length > 0 ? tools : undefined,
55
+ tool_choice: tools && tools.length > 0 ? { type: "auto" } : undefined,
56
+ stream: chatOptions.stream || undefined,
57
+ };
58
+ if (typeof chatOptions.temperature === "number") {
59
+ body.temperature = chatOptions.temperature;
60
+ }
61
+ const effectiveThinkingLevel = normalizeThinkingLevel(chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off", getAvailableThinkingLevels(options.providerId || "", chatOptions.model));
62
+ if (effectiveThinkingLevel !== "off") {
63
+ body.thinking = { type: "adaptive" };
64
+ }
65
+ return body;
66
+ }
67
+ export function toAnthropicMessages(messages, echoThinking = false) {
68
+ const system = [];
69
+ const out = [];
70
+ const thinkingReplayIndexes = getThinkingReplayIndexes(messages, echoThinking);
71
+ for (let index = 0; index < messages.length; index++) {
72
+ const message = messages[index];
73
+ if (message.role === "system") {
74
+ system.push(message.content);
75
+ continue;
76
+ }
77
+ if (message.role === "tool") {
78
+ pushAnthropicMessage(out, {
79
+ role: "user",
80
+ content: [{
81
+ type: "tool_result",
82
+ tool_use_id: message.toolCallId,
83
+ content: message.content,
84
+ ...(message.isError ? { is_error: true } : {}),
85
+ }],
86
+ });
87
+ continue;
88
+ }
89
+ if (message.role === "assistant") {
90
+ const content = buildAssistantAnthropicBlocks(message, thinkingReplayIndexes.has(index));
91
+ if (content.length > 0) {
92
+ pushAnthropicMessage(out, { role: "assistant", content });
93
+ }
94
+ continue;
95
+ }
96
+ pushAnthropicMessage(out, {
97
+ role: "user",
98
+ content: typeof message.content === "string"
99
+ ? message.content
100
+ : contentPartsToAnthropicBlocks(message.content),
101
+ });
102
+ }
103
+ return { system: system.join("\n\n"), messages: out };
104
+ }
105
+ function buildAssistantAnthropicBlocks(message, includeThinking) {
106
+ const rawBlocks = message.providerMetadata?.anthropic?.contentBlocks;
107
+ if (rawBlocks && rawBlocks.length > 0) {
108
+ const blocks = rawBlocks
109
+ .filter(isReplayableAssistantContentBlock)
110
+ .filter((block) => includeThinking || !isThinkingContentBlock(block))
111
+ .map((block) => cloneAnthropicContentBlock(block));
112
+ if (blocks.length > 0) {
113
+ return blocks;
114
+ }
115
+ }
116
+ const content = [];
117
+ if (includeThinking && message.reasoning?.trim()) {
118
+ content.push({ type: "thinking", thinking: message.reasoning });
119
+ }
120
+ if (message.content.trim()) {
121
+ content.push({ type: "text", text: message.content });
122
+ }
123
+ for (const toolCall of message.toolCalls ?? []) {
124
+ content.push({
125
+ type: "tool_use",
126
+ id: toolCall.id,
127
+ name: toolCall.name,
128
+ input: parseToolInput(toolCall.arguments),
129
+ });
130
+ }
131
+ return content;
132
+ }
133
+ function getThinkingReplayIndexes(messages, echoThinking) {
134
+ const indexes = new Set();
135
+ if (!echoThinking)
136
+ return indexes;
137
+ let lastUserIndex = -1;
138
+ for (let index = 0; index < messages.length; index++) {
139
+ if (messages[index].role === "user") {
140
+ lastUserIndex = index;
141
+ }
142
+ }
143
+ for (let index = Math.max(0, lastUserIndex + 1); index < messages.length; index++) {
144
+ const message = messages[index];
145
+ if (message.role === "assistant" && assistantHasToolUse(message)) {
146
+ indexes.add(index);
147
+ }
148
+ }
149
+ return indexes;
150
+ }
151
+ function assistantHasToolUse(message) {
152
+ if (message.toolCalls && message.toolCalls.length > 0)
153
+ return true;
154
+ return message.providerMetadata?.anthropic?.contentBlocks?.some((block) => block.type === "tool_use") ?? false;
155
+ }
156
+ function isThinkingContentBlock(block) {
157
+ return block.type === "thinking" || block.type === "redacted_thinking";
158
+ }
159
+ function isReplayableAssistantContentBlock(block) {
160
+ switch (block.type) {
161
+ case "text":
162
+ return typeof block.text === "string";
163
+ case "thinking":
164
+ return typeof block.thinking === "string";
165
+ case "redacted_thinking":
166
+ return typeof block.data === "string";
167
+ case "tool_use":
168
+ return typeof block.id === "string" && typeof block.name === "string" && isObjectRecord(block.input);
169
+ default:
170
+ return false;
171
+ }
172
+ }
173
+ function cloneAnthropicContentBlock(block) {
174
+ return JSON.parse(JSON.stringify(block));
175
+ }
176
+ export async function* translateAnthropicStream(events) {
177
+ const blocks = new Map();
178
+ let usage;
179
+ for await (const event of events) {
180
+ const type = typeof event.type === "string" ? event.type : "";
181
+ if (type === "message_start") {
182
+ usage = mergeAnthropicUsage(usage, event.message?.usage);
183
+ if (usage)
184
+ yield { type: "usage", usage };
185
+ continue;
186
+ }
187
+ if (type === "message_delta") {
188
+ usage = mergeAnthropicUsage(usage, event.usage);
189
+ if (usage)
190
+ yield { type: "usage", usage };
191
+ continue;
192
+ }
193
+ if (type === "error") {
194
+ const err = event.error;
195
+ throw new Error(`Anthropic stream error: ${String(err?.message || err?.type || "unknown error")}`);
196
+ }
197
+ if (type === "content_block_start") {
198
+ const index = typeof event.index === "number" ? event.index : 0;
199
+ const block = event.content_block;
200
+ const blockType = typeof block?.type === "string" ? block.type : "";
201
+ const raw = cloneProviderBlock(block, blockType);
202
+ const state = {
203
+ type: blockType,
204
+ id: typeof block?.id === "string" ? block.id : undefined,
205
+ name: typeof block?.name === "string" ? block.name : undefined,
206
+ args: "",
207
+ started: false,
208
+ input: isObjectRecord(block?.input) ? block.input : undefined,
209
+ raw,
210
+ text: typeof block?.text === "string" ? block.text : "",
211
+ thinking: typeof block?.thinking === "string" ? block.thinking : "",
212
+ signature: typeof block?.signature === "string" ? block.signature : "",
213
+ };
214
+ blocks.set(index, state);
215
+ if (blockType === "text" && typeof block?.text === "string" && block.text) {
216
+ yield { type: "text", content: block.text };
217
+ }
218
+ if (blockType === "thinking" && typeof block?.thinking === "string" && block.thinking) {
219
+ yield { type: "reasoning_delta", content: block.thinking };
220
+ }
221
+ if (blockType === "tool_use" && state.id && state.name) {
222
+ state.started = true;
223
+ yield { type: "tool_call", id: state.id, name: state.name, arguments: "", isStart: true, isEnd: false };
224
+ }
225
+ continue;
226
+ }
227
+ if (type === "content_block_delta") {
228
+ const index = typeof event.index === "number" ? event.index : 0;
229
+ const state = blocks.get(index);
230
+ const delta = event.delta;
231
+ const deltaType = typeof delta?.type === "string" ? delta.type : "";
232
+ if (deltaType === "text_delta" && typeof delta?.text === "string" && delta.text) {
233
+ if (state) {
234
+ state.text += delta.text;
235
+ state.raw.text = state.text;
236
+ }
237
+ yield { type: "text", content: delta.text };
238
+ }
239
+ else if (deltaType === "thinking_delta" && typeof delta?.thinking === "string" && delta.thinking) {
240
+ if (state) {
241
+ state.thinking += delta.thinking;
242
+ state.raw.thinking = state.thinking;
243
+ }
244
+ yield { type: "reasoning_delta", content: delta.thinking };
245
+ }
246
+ else if (deltaType === "signature_delta" && typeof delta?.signature === "string" && state) {
247
+ state.signature += delta.signature;
248
+ state.raw.signature = state.signature;
249
+ }
250
+ else if (deltaType === "input_json_delta" && state?.id && state.name && typeof delta?.partial_json === "string") {
251
+ state.args += delta.partial_json;
252
+ if (!state.started) {
253
+ state.started = true;
254
+ yield { type: "tool_call", id: state.id, name: state.name, arguments: "", isStart: true, isEnd: false };
255
+ }
256
+ if (delta.partial_json) {
257
+ yield { type: "tool_call", id: state.id, name: state.name, arguments: delta.partial_json, isStart: false, isEnd: false };
258
+ }
259
+ }
260
+ continue;
261
+ }
262
+ if (type === "content_block_stop") {
263
+ const index = typeof event.index === "number" ? event.index : 0;
264
+ const state = blocks.get(index);
265
+ blocks.delete(index);
266
+ if (state?.type === "tool_use" && state.id && state.name) {
267
+ const finalArgs = state.args || JSON.stringify(state.input ?? {});
268
+ state.raw.input = parseToolInput(normalizeToolArgs(finalArgs));
269
+ yield { type: "provider_content_block", provider: "anthropic", block: state.raw };
270
+ yield {
271
+ type: "tool_call",
272
+ id: state.id,
273
+ name: state.name,
274
+ arguments: "",
275
+ argumentsFull: normalizeToolArgs(finalArgs),
276
+ isStart: false,
277
+ isEnd: true,
278
+ };
279
+ }
280
+ else if (state && isReplayableAssistantContentBlock(state.raw)) {
281
+ finalizeRawContentBlock(state);
282
+ yield { type: "provider_content_block", provider: "anthropic", block: state.raw };
283
+ }
284
+ }
285
+ }
286
+ }
287
+ export async function* readSseEvents(response) {
288
+ if (!response.body) {
289
+ throw new Error("Anthropic Messages API returned an empty stream body.");
290
+ }
291
+ const reader = response.body.getReader();
292
+ const decoder = new TextDecoder();
293
+ let buffer = "";
294
+ try {
295
+ while (true) {
296
+ const { done, value } = await reader.read();
297
+ if (done)
298
+ break;
299
+ buffer += decoder.decode(value, { stream: true });
300
+ let separator = buffer.indexOf("\n\n");
301
+ while (separator >= 0) {
302
+ const raw = buffer.slice(0, separator);
303
+ buffer = buffer.slice(separator + 2);
304
+ const event = parseSseEvent(raw);
305
+ if (event)
306
+ yield event;
307
+ separator = buffer.indexOf("\n\n");
308
+ }
309
+ }
310
+ buffer += decoder.decode();
311
+ const event = parseSseEvent(buffer);
312
+ if (event)
313
+ yield event;
314
+ }
315
+ finally {
316
+ reader.releaseLock();
317
+ }
318
+ }
319
+ async function fetchAnthropicResponseWithRetry(options, request) {
320
+ const maxAttempts = shouldRetryMiniMaxAnthropic(options) ? 2 : 1;
321
+ let lastError;
322
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
323
+ const response = await fetch(request.url, {
324
+ method: request.method,
325
+ headers: buildAnthropicHeaders(options, request.stream),
326
+ body: request.body,
327
+ signal: request.signal,
328
+ });
329
+ if (response.ok)
330
+ return response;
331
+ const detail = await readAnthropicErrorDetail(response);
332
+ const error = new Error(`Anthropic Messages API error ${response.status}: ${detail || response.statusText}`);
333
+ lastError = error;
334
+ if (attempt >= maxAttempts || !isRetryableMiniMaxAnthropicError(response.status, detail)) {
335
+ throw error;
336
+ }
337
+ await sleepBeforeRetry(getAnthropicRetryDelayMs(), request.signal);
338
+ }
339
+ throw lastError ?? new Error("Anthropic Messages API request failed");
340
+ }
341
+ function resolveAnthropicMessagesUrl(baseURL) {
342
+ const normalized = baseURL.trim().replace(/\/+$/, "");
343
+ if (normalized.endsWith("/v1/messages"))
344
+ return normalized;
345
+ if (normalized.endsWith("/v1"))
346
+ return `${normalized}/messages`;
347
+ return `${normalized || "https://api.anthropic.com"}/v1/messages`;
348
+ }
349
+ function buildAnthropicHeaders(options, stream) {
350
+ const headers = {
351
+ "content-type": "application/json",
352
+ "x-api-key": options.apiKey,
353
+ "anthropic-version": ANTHROPIC_VERSION,
354
+ };
355
+ if (shouldSendBearerAuth(options)) {
356
+ headers.authorization = `Bearer ${options.apiKey}`;
357
+ }
358
+ if (stream)
359
+ headers.accept = "text/event-stream";
360
+ return headers;
361
+ }
362
+ async function readAnthropicErrorDetail(response) {
363
+ try {
364
+ return await response.text();
365
+ }
366
+ catch {
367
+ return response.statusText;
368
+ }
369
+ }
370
+ function shouldRetryMiniMaxAnthropic(options) {
371
+ const providerId = (options.providerId || "").toLowerCase();
372
+ const baseURL = options.baseURL.toLowerCase();
373
+ return providerId.startsWith("minimax") || baseURL.includes("api.minimaxi.com") || baseURL.includes("api.minimax.io");
374
+ }
375
+ function isRetryableMiniMaxAnthropicError(status, detail) {
376
+ return status === 500
377
+ || status === 502
378
+ || status === 503
379
+ || status === 504
380
+ || detail.includes("714 (1000)");
381
+ }
382
+ function getAnthropicRetryDelayMs() {
383
+ if (process.env.NODE_ENV === "test")
384
+ return 0;
385
+ return 800 + Math.floor(Math.random() * 700);
386
+ }
387
+ function sleepBeforeRetry(ms, signal) {
388
+ if (signal?.aborted) {
389
+ return Promise.reject(toAbortError(signal));
390
+ }
391
+ return new Promise((resolve, reject) => {
392
+ const timeout = setTimeout(resolve, ms);
393
+ signal?.addEventListener("abort", () => {
394
+ clearTimeout(timeout);
395
+ reject(toAbortError(signal));
396
+ }, { once: true });
397
+ });
398
+ }
399
+ function toAbortError(signal) {
400
+ const reason = signal?.reason;
401
+ if (reason instanceof Error)
402
+ return reason;
403
+ const error = new Error("Anthropic request retry aborted.");
404
+ error.name = "AbortError";
405
+ return error;
406
+ }
407
+ function parseSseEvent(raw) {
408
+ const dataLines = [];
409
+ for (const line of raw.split(/\r?\n/)) {
410
+ if (!line || line.startsWith(":"))
411
+ continue;
412
+ if (line.startsWith("data:")) {
413
+ dataLines.push(line.slice(5).trimStart());
414
+ }
415
+ }
416
+ if (dataLines.length === 0)
417
+ return undefined;
418
+ const data = dataLines.join("\n");
419
+ if (!data || data === "[DONE]")
420
+ return undefined;
421
+ return JSON.parse(data);
422
+ }
423
+ function cloneProviderBlock(block, fallbackType) {
424
+ const type = typeof block?.type === "string" && block.type ? block.type : fallbackType || "unknown";
425
+ const clone = block ? JSON.parse(JSON.stringify(block)) : {};
426
+ clone.type = type;
427
+ return clone;
428
+ }
429
+ function finalizeRawContentBlock(state) {
430
+ if (state.type === "text") {
431
+ state.raw.text = state.text;
432
+ }
433
+ else if (state.type === "thinking") {
434
+ state.raw.thinking = state.thinking;
435
+ if (state.signature) {
436
+ state.raw.signature = state.signature;
437
+ }
438
+ }
439
+ }
440
+ function contentPartsToAnthropicBlocks(parts) {
441
+ const blocks = [];
442
+ for (const part of parts) {
443
+ if (part.type === "text") {
444
+ blocks.push({ type: "text", text: part.text });
445
+ continue;
446
+ }
447
+ const image = part.image_url.url;
448
+ const dataUrlMatch = image.match(/^data:([^;,]+);base64,(.+)$/);
449
+ if (dataUrlMatch) {
450
+ blocks.push({
451
+ type: "image",
452
+ source: {
453
+ type: "base64",
454
+ media_type: dataUrlMatch[1],
455
+ data: dataUrlMatch[2],
456
+ },
457
+ });
458
+ }
459
+ else {
460
+ blocks.push({ type: "image", source: { type: "url", url: image } });
461
+ }
462
+ }
463
+ return blocks;
464
+ }
465
+ function pushAnthropicMessage(messages, next) {
466
+ const last = messages.at(-1);
467
+ if (!last || last.role !== next.role) {
468
+ messages.push(next);
469
+ return;
470
+ }
471
+ last.content = mergeAnthropicContent(last.content, next.content);
472
+ }
473
+ function mergeAnthropicContent(current, next) {
474
+ const currentBlocks = typeof current === "string" ? [{ type: "text", text: current }] : current;
475
+ const nextBlocks = typeof next === "string" ? [{ type: "text", text: next }] : next;
476
+ return [...currentBlocks, ...nextBlocks];
477
+ }
478
+ function parseToolInput(raw) {
479
+ try {
480
+ const parsed = JSON.parse(raw || "{}");
481
+ return isObjectRecord(parsed) ? parsed : {};
482
+ }
483
+ catch {
484
+ return {};
485
+ }
486
+ }
487
+ function normalizeToolArgs(raw) {
488
+ try {
489
+ JSON.parse(raw);
490
+ return raw;
491
+ }
492
+ catch {
493
+ return "{}";
494
+ }
495
+ }
496
+ function isObjectRecord(value) {
497
+ return !!value && typeof value === "object" && !Array.isArray(value);
498
+ }
499
+ function extractAnthropicText(content) {
500
+ if (!content)
501
+ return [];
502
+ return content.flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
503
+ }
504
+ function mergeAnthropicUsage(current, raw) {
505
+ if (!isObjectRecord(raw))
506
+ return current;
507
+ const rawInput = typeof raw.input_tokens === "number" ? raw.input_tokens : undefined;
508
+ const rawCacheRead = typeof raw.cache_read_input_tokens === "number" ? raw.cache_read_input_tokens : undefined;
509
+ const rawCacheCreation = typeof raw.cache_creation_input_tokens === "number" ? raw.cache_creation_input_tokens : undefined;
510
+ const outputTokens = typeof raw.output_tokens === "number" ? raw.output_tokens : current?.completionTokens ?? 0;
511
+ const hasPromptUsage = rawInput !== undefined || rawCacheRead !== undefined || rawCacheCreation !== undefined;
512
+ let promptTokens = current?.promptTokens ?? 0;
513
+ let promptCacheHitTokens = current?.promptCacheHitTokens;
514
+ let promptCacheMissTokens = current?.promptCacheMissTokens;
515
+ if (hasPromptUsage) {
516
+ const inputTokens = rawInput ?? promptCacheMissTokens ?? promptTokens;
517
+ const cacheRead = rawCacheRead ?? promptCacheHitTokens ?? 0;
518
+ const cacheCreation = rawCacheCreation ?? 0;
519
+ promptTokens = inputTokens + cacheRead + cacheCreation;
520
+ promptCacheHitTokens = cacheRead;
521
+ promptCacheMissTokens = inputTokens + cacheCreation;
522
+ }
523
+ return {
524
+ promptTokens,
525
+ completionTokens: outputTokens,
526
+ promptCacheHitTokens,
527
+ promptCacheMissTokens,
528
+ totalTokens: promptTokens + outputTokens,
529
+ };
530
+ }
531
+ function shouldEchoThinking(providerId) {
532
+ return providerId?.startsWith("minimax") ?? false;
533
+ }
534
+ function shouldSendBearerAuth(options) {
535
+ return !isOfficialAnthropicBaseUrl(options.baseURL) || options.providerId?.startsWith("minimax") === true;
536
+ }
537
+ function isOfficialAnthropicBaseUrl(baseURL) {
538
+ try {
539
+ return new URL(baseURL).hostname === "api.anthropic.com";
540
+ }
541
+ catch {
542
+ return false;
543
+ }
544
+ }
@@ -5,6 +5,7 @@
5
5
  * Reads provider configuration from models.json first, then falls back to config.json.
6
6
  */
7
7
  import type { UserConfig } from "./config.js";
8
+ import { type ProviderProtocol } from "./model-catalog.js";
8
9
  import { ModelConfig } from "./model-config.js";
9
10
  import { AuthStorage } from "./oauth/index.js";
10
11
  import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
@@ -15,6 +16,7 @@ export interface ProviderProfile {
15
16
  apiKey: string;
16
17
  enabled: boolean;
17
18
  authType?: "api" | "oauth";
19
+ protocol?: ProviderProtocol;
18
20
  }
19
21
  export interface ModelInfo {
20
22
  id: string;
@@ -11,7 +11,7 @@ import { fetchOpenAICodexModels } from "./provider-openai-codex.js";
11
11
  import { refreshOpenAICodex } from "./oauth/openai-codex.js";
12
12
  export const BUILTIN_PROVIDERS = CATALOG_PROVIDERS;
13
13
  export const USER_VISIBLE_PROVIDER_IDS = BUILTIN_PROVIDERS
14
- .filter((provider) => provider.id !== "openrouter" && provider.id !== "openai-codex")
14
+ .filter((provider) => !provider.hidden && provider.id !== "openrouter" && provider.id !== "openai-codex")
15
15
  .map((provider) => provider.id);
16
16
  export function isUserVisibleProvider(providerId) {
17
17
  return USER_VISIBLE_PROVIDER_IDS.includes(providerId);
@@ -123,19 +123,27 @@ export class ProviderRegistry {
123
123
  providers = keys.map((id) => {
124
124
  const builtin = getBuiltinProvider(id);
125
125
  const cfg = modelsJsonProviders[id];
126
+ const baseURL = cfg.baseURL || builtin?.baseURL || "";
126
127
  return {
127
128
  id,
128
129
  name: builtin?.name || id,
129
- baseURL: cfg.baseURL || builtin?.baseURL || "",
130
+ baseURL,
130
131
  apiKey: cfg.apiKey || "",
131
132
  enabled: true,
132
133
  authType: "api",
134
+ protocol: resolveConfiguredProtocol(id, baseURL, cfg.protocol),
133
135
  };
134
136
  });
135
137
  }
136
138
  else {
137
139
  // 2. Fall back to config.json providers (interactive TUI style)
138
- providers = this.config.getProviders();
140
+ providers = this.config.getProviders().map((provider) => {
141
+ const builtin = getBuiltinProvider(provider.id);
142
+ return {
143
+ ...provider,
144
+ protocol: resolveConfiguredProtocol(provider.id, provider.baseURL, provider.protocol),
145
+ };
146
+ });
139
147
  }
140
148
  // 3. Inject OAuth access tokens
141
149
  for (const p of providers) {
@@ -283,6 +291,24 @@ export class ProviderRegistry {
283
291
  }));
284
292
  }
285
293
  }
294
+ function resolveConfiguredProtocol(providerId, baseURL, explicitProtocol) {
295
+ if (explicitProtocol)
296
+ return explicitProtocol;
297
+ const builtin = getBuiltinProvider(providerId);
298
+ if (!builtin?.protocol)
299
+ return undefined;
300
+ const normalizedBaseURL = normalizeBaseURL(baseURL);
301
+ if (!normalizedBaseURL || normalizedBaseURL === normalizeBaseURL(builtin.baseURL)) {
302
+ return builtin.protocol;
303
+ }
304
+ if (normalizedBaseURL.includes("/anthropic")) {
305
+ return "anthropic-messages";
306
+ }
307
+ return undefined;
308
+ }
309
+ function normalizeBaseURL(baseURL) {
310
+ return baseURL.trim().replace(/\/+$/, "").toLowerCase();
311
+ }
286
312
  /** Encode a model selection as "providerId:modelId". */
287
313
  export function encodeModel(providerId, modelId) {
288
314
  return `${providerId}:${modelId}`;
@@ -3,7 +3,7 @@ export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingL
3
3
  export interface ProviderRequestConfig {
4
4
  effectiveThinkingLevel: ThinkingLevel;
5
5
  reasoningEffort?: ThinkingLevel;
6
- reasoningContentEcho?: "tool_calls" | "all" | "none";
6
+ reasoningContentEcho?: "tool_calls" | "all" | "none" | "minimax";
7
7
  parallelToolCalls?: boolean;
8
8
  maxTokens?: number;
9
9
  extraBody?: Record<string, unknown>;
@@ -4,6 +4,7 @@ const MOONSHOT_PROVIDER_IDS = new Set(["moonshot-cn", "moonshot-intl", "kimi-for
4
4
  const KIMI_K25_FAMILY = new Set(["kimi-k2.5", "k2.6-code-preview", "kimi-k2.6"]);
5
5
  const KIMI_THINKING_FAMILY = new Set(["kimi-k2-thinking", "kimi-k2-thinking-turbo"]);
6
6
  const KIMI_K26_DEFAULT_MAX_TOKENS = 32768;
7
+ const MINIMAX_M3_FAMILY = new Set(["MiniMax-M3"]);
7
8
  function isFireworksKimi(providerId, modelId) {
8
9
  const model = modelId.toLowerCase();
9
10
  return providerId === "fireworks" && (model.includes("kimi")
@@ -45,6 +46,19 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
45
46
  : { reasoning_effort: effectiveThinkingLevel },
46
47
  };
47
48
  }
49
+ if (providerId === "minimax" || providerId === "minimax-openai") {
50
+ const extraBody = { reasoning_split: true };
51
+ if (MINIMAX_M3_FAMILY.has(modelId)) {
52
+ extraBody.thinking = {
53
+ type: effectiveThinkingLevel === "off" ? "disabled" : "adaptive",
54
+ };
55
+ }
56
+ return {
57
+ effectiveThinkingLevel,
58
+ reasoningContentEcho: "minimax",
59
+ extraBody,
60
+ };
61
+ }
48
62
  // Zhipu/Z.AI OpenAI-compatible endpoints expose reasoning via a provider-specific
49
63
  // `thinking` block rather than OpenAI's `reasoning_effort` shape.
50
64
  if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(providerId)) {
@@ -4,12 +4,14 @@
4
4
  * Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
5
5
  */
6
6
  import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
7
+ import type { ProviderProtocol } from "./model-catalog.js";
7
8
  import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
8
- type ReasoningContentEcho = "tool_calls" | "all" | "none";
9
+ type ReasoningContentEcho = "tool_calls" | "all" | "none" | "minimax";
9
10
  export type ToolArgsMergeMode = "delta" | "snapshot";
10
11
  export interface TranslateOpenAIStreamOptions {
11
12
  toolArgsMergeMode?: ToolArgsMergeMode;
12
13
  reasoningMergeMode?: ToolArgsMergeMode;
14
+ textMergeMode?: ToolArgsMergeMode;
13
15
  debugProviderId?: string;
14
16
  debugModelId?: string;
15
17
  }
@@ -24,6 +26,7 @@ export interface ProviderInstanceOptions {
24
26
  thinkingLevel?: ThinkingLevel;
25
27
  /** Stable per-session seed for provider prompt caches. */
26
28
  promptCacheKey?: string;
29
+ protocol?: ProviderProtocol;
27
30
  /** Dynamic OAuth access-token loader/refresh hook for ChatGPT Codex requests. */
28
31
  openAICodexAuth?: OpenAICodexAuthAdapter;
29
32
  }