llm-switcher 1.2.9 → 1.2.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog — LLM Switcher
2
2
 
3
+ ## Release 1.2.10
4
+
5
+ - **Bifrost in the log:** The log line and the request inspector showed `anthropic -> openai-chat` for a Bifrost request, because the gateway logged the profile's `outFormat` before it checked Bifrost. Now both show `bifrost`.
6
+ - **Bifrost and the contract lab:** A sampled Bifrost exchange went to intact labeled with the profile's `outFormat`. intact then compared an Anthropic request with an OpenAI conversion that never ran. Now the label is `anthropic`.
7
+
3
8
  ## Release 1.2.9
4
9
 
5
10
  - **Bifrost:** Claude Code that reaches a Claude Code account on intact now goes through unchanged. Only the key changes. Before, a `convert` profile, or a `hybrid` profile with a `claude/...` model, changed the request to OpenAI Chat. The `direct` route also ran the healer and `thinkingMode`. Now the gateway asks intact for `bifrost_ua` of the mapped model. If the `User-Agent` of the client starts with this value, the gateway sends every client header, the body bytes, and the query string. There is no setting. intact 0.1.10 or newer gives `bifrost_ua`.
package/README.md CHANGED
@@ -134,6 +134,7 @@ LLM Switcher acts as a transparent man-in-the-middle without ever touching clien
134
134
  - **Bi-Directional Protocol Conversion:**
135
135
  - **4 Client Inbound Formats:** Anthropic Messages, OpenAI Chat Completions, Codex Responses API, Vertex `generateContent`.
136
136
  - **3 Upstream Outbound Formats:** OpenAI Chat, Anthropic Native, Vertex Native.
137
+ - **Bifrost:** Claude Code on a Claude Code account of intact goes through unchanged. Only the key changes. No conversion, no healer, no filter. The gateway turns it on by itself. [Bifrost](#bifrost-claude-code-to-a-claude-code-account-on-intact)
137
138
  - **Multi-Active CLI Routing:** Run Claude Code on Profile A, Codex on Profile B, and Cursor on Profile C simultaneously on a single gateway instance.
138
139
  - **Deep Thinking & Reasoning Extraction:** Tested on 48 live response combinations. Accurately extracts `reasoning_content`, `<think>` tags, Vertex `thought` parts, and signatures into native `thinking_delta` blocks.
139
140
  - **Edge Healer Engine (Anti-Collision for Token Optimizers):**
package/README.vi.md CHANGED
@@ -134,6 +134,7 @@ LLM Switcher hoạt động như một lớp trung gian mạng trong suốt (tra
134
134
  - **Chuyển đổi Giao thức 2 Chiều Đối xứng:**
135
135
  - **4 Định dạng đầu vào (Client):** Anthropic Messages, OpenAI Chat Completions, Codex Responses API, Vertex `generateContent`.
136
136
  - **3 Định dạng đầu ra (Upstream):** OpenAI Chat, Anthropic Native, Vertex Native.
137
+ - **Bifrost:** Claude Code tới tài khoản Claude Code trên intact đi nguyên vẹn. Chỉ key thay đổi. Không chuyển định dạng, không healer, không lọc. Gateway tự bật. [Bifrost](#bifrost-claude-code-tới-tài-khoản-claude-code-trên-intact)
137
138
  - **Multi-Active CLI Routing:** Kích hoạt cùng lúc Claude Code dùng Profile A, Codex dùng Profile B, Cursor dùng Profile C trên cùng 1 gateway mà không tranh chấp cấu hình.
138
139
  - **Trích xuất Thinking & Reasoning Chuyên sâu:** Kiểm chứng thực tế qua 48 tổ hợp mẫu response live. Tự động bóc tách `reasoning_content`, thẻ `<think>`, các block `thought` của Vertex và thought signature thành các `thinking_delta` chuẩn của Anthropic.
139
140
  - **Edge Healer Engine (Tự chữa lành tin nhắn):**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-switcher",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Zero-dependency multi-protocol edge gateway & provider switcher for Claude Code and Codex, with OpenAI-compatible, Anthropic and Vertex upstreams",
5
5
  "keywords": [
6
6
  "llm",
package/proxy.mjs CHANGED
@@ -775,10 +775,12 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
775
775
  const requestPreview = previewOf(ir);
776
776
  const requestedModel = ir.model || payload.model || '';
777
777
  const mappedModel = mapModel(requestedModel, profile, clientFormat);
778
+ const bifrost = clientFormat === 'anthropic' && await crossesBifrost(req, profile, mappedModel);
778
779
  const outFormat = resolveOutFormat(profile, mappedModel);
779
- console.log(`[llm-switcher] ${clientFormat} -> ${outFormat} "${requestedModel}" -> "${mappedModel}" [${profile.name || profileKey}]`);
780
+ const route = bifrost ? 'bifrost' : outFormat;
781
+ console.log(`[llm-switcher] ${clientFormat} -> ${route} "${requestedModel}" -> "${mappedModel}" [${profile.name || profileKey}]`);
780
782
 
781
- const logBase = { clientFormat, outFormat, profile: profileKey, model: mappedModel, stream: ir.stream, requestPreview };
783
+ const logBase = { clientFormat, outFormat: route, profile: profileKey, model: mappedModel, stream: ir.stream, requestPreview };
782
784
  const log = (extra) => logInspection({ ...logBase, duration: Date.now() - reqStartTime, tokens: { prompt: 0, completion: 0 }, ...extra });
783
785
 
784
786
  // Contract lab: a sampled request carries a trace id to intact, and the bytes this gateway
@@ -801,14 +803,14 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
801
803
  try {
802
804
  // Bifrost: the provider's own client reaching its own account through intact. Bytes and headers
803
805
  // go as sent, only the key changes: no healer, no thinkingMode, no conversion.
804
- if (clientFormat === 'anthropic' && await crossesBifrost(req, profile, mappedModel)) {
806
+ if (bifrost) {
805
807
  const { url } = upstreamEndpoint(profile, 'anthropic', mappedModel, ir.stream, req);
806
808
  const headers = bifrostHeaders(req, profile);
807
809
  if (traceId) headers['x-intact-trace'] = traceId;
808
810
  try {
809
811
  const r = await forwardAnthropicDirect(res, payload, bodyBuffer, withClientQuery(url, req), headers, mappedModel, ac.signal, profile, true);
810
812
  answered = !r.error && r.status >= 200 && r.status < 300;
811
- log({ outFormat: 'anthropic', status: r.status, tokens: r.tokens, responsePreview: '(bifrost)', error: r.error || undefined });
813
+ log({ status: r.status, tokens: r.tokens, responsePreview: '(bifrost)', error: r.error || undefined });
812
814
  } catch (err) {
813
815
  if (ac.signal.aborted) return log({ status: 499, error: 'client disconnected' });
814
816
  console.error(`[${profileKey}] Bifrost forward error:`, err.message);
@@ -936,7 +938,7 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
936
938
  toolResponse: halfTap?.text() || '',
937
939
  toolVersion: toolVersionFromUA(req.headers['user-agent']),
938
940
  inFormat: clientFormat,
939
- outFormat,
941
+ outFormat: bifrost ? 'anthropic' : outFormat, // Bifrost converts nothing
940
942
  openerKey: profile.apiKey || ''
941
943
  });
942
944
  }
@@ -611,6 +611,9 @@ test('Bifrost: Claude Code to a Claude Code account on intact changes only the k
611
611
  assert.equal(up.headers['anthropic-dangerous-direct-browser-access'], 'true');
612
612
  assert.equal(up.headers['x-app'], 'cli');
613
613
  assert.equal(up.raw, JSON.stringify({ ...body, model: 'claude/claude-opus-5' }));
614
+ // The inspector and the log name the route that ran, not the profile's outFormat.
615
+ const logs = (await (await fetch(url('/api/logs'), { headers: withToken('/api/logs', {}) })).json()).logs;
616
+ assert.equal(logs[0].outFormat, 'bifrost');
614
617
  });
615
618
 
616
619
  test('Bifrost stays off for a model that is not a Claude Code account', async () => {