Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 87 additions & 8 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ const DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS = Object.freeze({
port: 8328,
provider: '',
authSource: 'provider',
targetApi: 'responses',
timeoutMs: 30000
});
const CLI_INSTALL_TARGETS = Object.freeze([
Expand Down Expand Up @@ -5488,7 +5489,9 @@ const {
HTTPS_KEEP_ALIVE_AGENT,
readConfigOrVirtualDefault,
resolveBuiltinProxyProviderName,
resolveAuthTokenFromCurrentProfile
resolveAuthTokenFromCurrentProfile,
OPENAI_BRIDGE_SETTINGS_FILE,
resolveOpenaiBridgeUpstream
});

function applyBuiltinProxyProvider(params = {}) {
Expand Down Expand Up @@ -9148,8 +9151,29 @@ function maskKey(key) {
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
}

function normalizeClaudeTargetApi(value) {
const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
return raw === 'chat_completions' || raw === 'chat-completions' || raw === 'chat/completions'
? 'chat_completions'
: 'responses';
}

function resetBuiltinClaudeProxySavedSettingsToResponses() {
const proxySettingsResult = readJsonObjectFromFile(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS);
const proxySettings = proxySettingsResult.ok && proxySettingsResult.data && typeof proxySettingsResult.data === 'object' && !Array.isArray(proxySettingsResult.data)
? proxySettingsResult.data
: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS;
writeJsonAtomic(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, {
...DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
...proxySettings,
enabled: false,
targetApi: 'responses'
});
}

// 应用到 Claude Code settings.json(跨平台)
function applyToClaudeSettings(config = {}) {
async function applyToClaudeSettings(config = {}) {
let proxyStarted = false;
try {
const apiKey = (config.apiKey || '').trim();
if (!apiKey) {
Expand All @@ -9158,8 +9182,48 @@ function applyToClaudeSettings(config = {}) {

const baseUrl = (config.baseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
const targetApi = normalizeClaudeTargetApi(config.targetApi);
let settingsBaseUrl = baseUrl;
let settingsApiKey = apiKey;
let proxyResult = null;

if (targetApi === 'chat_completions') {
await stopBuiltinClaudeProxyRuntime();
const proxyToken = crypto.randomBytes(24).toString('hex');
proxyResult = await startBuiltinClaudeProxyRuntime({
enabled: true,
host: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host,
provider: typeof config.name === 'string' ? config.name.trim() : '',
authSource: 'provider',
targetApi,
timeoutMs: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs,
upstreamProviderName: typeof config.name === 'string' ? config.name.trim() : '',
upstreamBaseUrl: baseUrl,
upstreamApiKey: apiKey
});
Comment on lines 9183 to +9203
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject chat_completions mode without an explicit upstream URL.

When config.baseUrl is empty, this path still falls back to the Anthropic Messages base URL and passes it into upstreamBaseUrl. That silently creates a chat_completions proxy against the wrong upstream instead of failing fast. Return a validation error here, or omit upstreamBaseUrl so the runtime can resolve it from config.name if that's supported.

Possible fix
-        const baseUrl = (config.baseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
+        const configuredBaseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
+        const baseUrl = (configuredBaseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
         const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
         const targetApi = normalizeClaudeTargetApi(config.targetApi);
@@
         if (targetApi === 'chat_completions') {
+            if (!configuredBaseUrl) {
+                return { success: false, mode: 'claude-proxy', error: 'chat_completions 模式需要显式的上游 Base URL' };
+            }
             await stopBuiltinClaudeProxyRuntime();
             const proxyToken = crypto.randomBytes(24).toString('hex');
             proxyResult = await startBuiltinClaudeProxyRuntime({
@@
-                upstreamBaseUrl: baseUrl,
+                upstreamBaseUrl: configuredBaseUrl,
                 upstreamApiKey: apiKey
             });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 9183 - 9203, The code allows targetApi ===
'chat_completions' to startBuiltinClaudeProxyRuntime even when config.baseUrl is
empty (falling back to the Anthropic Messages URL), which silently wires the
proxy to the wrong upstream; update the validation around
normalizeClaudeTargetApi/targetApi and baseUrl before calling
startBuiltinClaudeProxyRuntime so that if targetApi === 'chat_completions' and
config.baseUrl is missing or equal to the default Anthropic messages URL you
either throw/return a validation error or call startBuiltinClaudeProxyRuntime
without upstreamBaseUrl (omit the upstreamBaseUrl property) so the runtime
resolves the upstream from config.name; adjust the block that prepares
proxyResult (the call to startBuiltinClaudeProxyRuntime and the
upstreamBaseUrl/upstreamProviderName fields) accordingly to enforce this check.

Comment on lines +9192 to +9203
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cli.js callsite =="
rg -n -C3 'startBuiltinClaudeProxyRuntime\(' cli.js

echo
echo "== Claude proxy runtime/controller auth handling =="
rg -n -C4 'createBuiltinClaudeProxyRuntimeController|startBuiltinClaudeProxyRuntime|expected(ApiKey|Token)|proxyToken|authorization|x-api-key|ANTHROPIC_API_KEY|auth' cli/claude-proxy.js

Repository: SakuraByteCore/codexmate

Length of output: 11163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for CLAUDE_SETTINGS_FILE and proxyToken/expected token fields =="
rg -n "CLAUDE_SETTINGS_FILE|proxyToken|expectedApiKey|expected.*token|x-api-key|settings\.json|authHeader|authSource" cli/claude-proxy.js

echo
echo "== Inspect startBuiltinClaudeProxyRuntime implementation (payload merging / settings defaults) =="
rg -n "async function startBuiltinClaudeProxyRuntime|function readBuiltinClaudeProxySettings|saveBuiltinClaudeProxySettings" cli/claude-proxy.js
python3 - <<'PY'
import re,sys,subprocess,os, textwrap, json, pathlib
path="cli/claude-proxy.js"
# Print a focused window around startBuiltinClaudeProxyRuntime if found
import subprocess
out=subprocess.check_output(["rg","-n","async function startBuiltinClaudeProxyRuntime","cli/claude-proxy.js"],text=True)
m=re.search(r":(\d+):",out)
start=int(m.group(1))
print(f"start line: {start}")
# Print range start-40..start+80
import itertools
with open(path,"r",encoding="utf-8",errors="replace") as f:
    lines=f.readlines()
lo=max(0,start-40)
hi=min(len(lines),start+120)
for i in range(lo,hi):
    print(f"{i+1}:{lines[i].rstrip()}")
PY

echo
echo "== Inspect the code that handles auth for built-in proxy requests (Claude -> proxy) =="
python3 - <<'PY'
import re,subprocess
path="cli/claude-proxy.js"
# Find where request handler calls buildBuiltinClaudeProxyRequestAuthHeader and where headers are checked
patterns=["buildBuiltinClaudeProxyRequestAuthHeader","writeAnthropicProxyError","handleBuiltinClaudeProxyRequest","Unauthorized","CODEXMATE_HTTP_TOKEN"]
for pat in patterns:
    try:
        out=subprocess.check_output(["rg","-n",pat,path],text=True)
    except subprocess.CalledProcessError:
        continue
    print(f"\n-- matches for {pat} --")
    print(out)
PY

echo
echo "== Inspect around buildBuiltinClaudeProxyRequestAuthHeader + incoming request checks =="
python3 - <<'PY'
import re
path="cli/claude-proxy.js"
# Locate buildBuiltinClaudeProxyRequestAuthHeader definition line
import subprocess
out=subprocess.check_output(["rg","-n","function buildBuiltinClaudeProxyRequestAuthHeader","cli/claude-proxy.js"],text=True)
m=re.search(r":(\d+):",out)
start=int(m.group(1))
with open(path,'r',encoding='utf-8',errors='replace') as f:
    lines=f.readlines()
lo=max(0,start-30); hi=min(len(lines),start+140)
for i in range(lo,hi):
    print(f"{i+1}:{lines[i].rstrip()}")
PY

echo
echo "== Inspect how upstream auth is derived from settings (provider/authSource/profile/none) =="
python3 - <<'PY'
import re,subprocess
path="cli/claude-proxy.js"
# Locate resolveBuiltinClaudeProxyUpstream start
out=subprocess.check_output(["rg","-n","function resolveBuiltinClaudeProxyUpstream|resolveBuiltinClaudeProxyUpstream","cli/claude-proxy.js"],text=True)
# choose line with function resolveBuiltinClaudeProxyUpstream(deps?) etc.
# just print first match line number
import itertools
lines=out.strip().splitlines()
first=lines[0]
m=re.search(r":(\d+):",first)
start=int(m.group(1))
print(f"first match line: {start} ({first})")
with open(path,'r',encoding='utf-8',errors='replace') as f:
    arr=f.readlines()
lo=max(0,start-20); hi=min(len(arr),start+220)
for i in range(lo,hi):
    print(f"{i+1}:{arr[i].rstrip()}")
PY

Repository: SakuraByteCore/codexmate

Length of output: 2202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Show read/save settings helpers in cli/claude-proxy.js (around 680-760) =="
nl -ba cli/claude-proxy.js | sed -n '660,780p'

echo
echo "== Show startBuiltinClaudeProxyRuntime implementation (around 1180-1260) =="
nl -ba cli/claude-proxy.js | sed -n '1170,1265p'

echo
echo "== Find/inspect resolveAuthTokenFromCurrentProfile wiring (usage/definition) =="
rg -n "resolveAuthTokenFromCurrentProfile" cli/claude-proxy.js

echo
echo "== Inspect how BUILTIN_CLAUDE_PROXY_SETTINGS_FILE is used (usage sites) =="
rg -n "BUILTIN_CLAUDE_PROXY_SETTINGS_FILE" cli/claude-proxy.js

Repository: SakuraByteCore/codexmate

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cli.js: locate proxyToken usage and applyToClaudeSettings write paths =="
rg -n "proxyToken|applyToClaudeSettings|settingsApiKey|settingsBaseUrl|claude.*settings\.json|~\/\.claude\/settings\.json" cli.js

echo
echo "== cli.js: show region around proxyToken + startBuiltinClaudeProxyRuntime call =="
cat -n cli.js | sed -n '9165,9265p'

echo
echo "== cli/claude-proxy.js: show read/save settings helpers =="
cat -n cli/claude-proxy.js | sed -n '640,760p'

echo
echo "== cli/claude-proxy.js: show startBuiltinClaudeProxyRuntime payload merge + controller start =="
cat -n cli/claude-proxy.js | sed -n '1180,1265p'

echo
echo "== cli/claude-proxy.js: show incoming Claude request auth check (CODEXMATE_HTTP_TOKEN / x-codexmate-token) =="
cat -n cli/claude-proxy.js | sed -n '1120,1185p'

Repository: SakuraByteCore/codexmate

Length of output: 431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cli.js: find proxyToken + settingsApiKey/settingsBaseUrl usage =="
rg -n "proxyToken" cli.js
rg -n "applyToClaudeSettings" cli.js
rg -n "settingsApiKey|settingsBaseUrl" cli.js
rg -n "claude.*settings\.json|~\/\.claude\/settings\.json" cli.js || true

Repository: SakuraByteCore/codexmate

Length of output: 990


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== cli.js: show region around proxyToken + startBuiltinClaudeProxyRuntime call =="
cat -n cli.js | sed -n '9170,9250p'

Repository: SakuraByteCore/codexmate

Length of output: 3974


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== cli/claude-proxy.js: show read/save settings helpers and startBuiltinClaudeProxyRuntime payload merge =="
cat -n cli/claude-proxy.js | sed -n '620,820p'
cat -n cli/claude-proxy.js | sed -n '1160,1265p'

Repository: SakuraByteCore/codexmate

Length of output: 14937


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== cli/claude-proxy.js: show incoming Claude request auth check =="
cat -n cli/claude-proxy.js | sed -n '1120,1185p'

Repository: SakuraByteCore/codexmate

Length of output: 3657


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS host definition =="
rg -n "DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS" -S cli.js cli/claude-proxy.js

echo
echo "== Show where proxy listen url host comes from =="
rg -n "buildBuiltinClaudeProxyListenUrl|formatHostForUrl|http://\\$\\{host\\}" cli/claude-proxy.js

echo
echo "== Search CLAUDE_SETTINGS_FILE usage in cli/claude-proxy.js =="
rg -n "CLAUDE_SETTINGS_FILE" cli/claude-proxy.js || true

echo
echo "== Show DEFAULT_* settings block (closest match regions) =="
# Print small regions around each DEFAULT_* match
for f in cli.js cli/claude-proxy.js; do
  echo "--- $f ---"
  rg -n "DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS" "$f" | cut -d: -f1 | sort -u | while read -r line; do
    start=$((line-20)); end=$((line+40));
    sed -n "${start},${end}p" "$f";
  done
done

Repository: SakuraByteCore/codexmate

Length of output: 45075


Fix proxy-token/auth mismatch in chat_completions flow.

applyToClaudeSettings(...) generates proxyToken and writes it into Claude’s ANTHROPIC_API_KEY, but startBuiltinClaudeProxyRuntime(...) doesn’t pass that token anywhere. In cli/claude-proxy.js, the proxy does not use Claude settings to validate it; it only enforces access using process.env.CODEXMATE_HTTP_TOKEN for non-loopback requests (loopback requests bypass this check). With DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host = 127.0.0.1 this often won’t surface, but if Claude reaches the proxy via a non-loopback address, requests will be unauthorized unless CODEXMATE_HTTP_TOKEN matches the generated proxyToken.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 9192 - 9203, The generated proxyToken from
applyToClaudeSettings(...) is not passed into startBuiltinClaudeProxyRuntime,
causing an auth mismatch; update the call to startBuiltinClaudeProxyRuntime (the
invocation that currently sets host, provider, authSource, targetApi, timeoutMs,
upstreamProviderName, upstreamBaseUrl, upstreamApiKey) to include the generated
proxyToken (e.g., as an authToken or proxyToken parameter) so the runtime
enforces the same token that was written into Claude’s ANTHROPIC_API_KEY, and
also verify cli/claude-proxy.js checks that token (or falls back to
CODEXMATE_HTTP_TOKEN only when appropriate) for non-loopback requests when
DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host is non-loopback. Ensure parameter
naming matches startBuiltinClaudeProxyRuntime’s signature and update any
doc/comment accordingly.

if (!proxyResult || proxyResult.error || proxyResult.success === false || !proxyResult.listenUrl) {
await stopBuiltinClaudeProxyRuntime();
resetBuiltinClaudeProxySavedSettingsToResponses();
return {
success: false,
mode: 'claude-proxy',
error: (proxyResult && proxyResult.error) || '启动 Claude 兼容代理失败'
};
}
proxyStarted = true;
settingsBaseUrl = proxyResult.listenUrl;
settingsApiKey = proxyToken;
} else {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
await stopBuiltinClaudeProxyRuntime();
resetBuiltinClaudeProxySavedSettingsToResponses();
}

const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
if (!readResult.ok) {
if (proxyStarted) {
await stopBuiltinClaudeProxyRuntime();
resetBuiltinClaudeProxySavedSettingsToResponses();
}
return { success: false, mode: 'settings-file', error: readResult.error };
}

Expand All @@ -9170,8 +9234,8 @@ function applyToClaudeSettings(config = {}) {

const nextEnv = {
...currentEnv,
ANTHROPIC_API_KEY: apiKey,
ANTHROPIC_BASE_URL: baseUrl,
ANTHROPIC_API_KEY: settingsApiKey,
ANTHROPIC_BASE_URL: settingsBaseUrl,
ANTHROPIC_MODEL: model
};
delete nextEnv.ANTHROPIC_AUTH_TOKEN;
Expand All @@ -9188,19 +9252,32 @@ function applyToClaudeSettings(config = {}) {

const result = {
success: true,
mode: 'settings-file',
mode: targetApi === 'chat_completions' ? 'claude-proxy' : 'settings-file',
targetApi,
targetPath: CLAUDE_SETTINGS_FILE,
updatedKeys: [
'env.ANTHROPIC_API_KEY',
'env.ANTHROPIC_BASE_URL',
'env.ANTHROPIC_MODEL'
]
};
if (proxyResult) {
result.proxy = {
running: true,
listenUrl: proxyResult.listenUrl,
upstreamProvider: proxyResult.upstreamProvider || '',
mode: proxyResult.mode || 'anthropic-to-chat-completions'
};
}
if (backupPath) {
result.backupPath = backupPath;
}
return result;
} catch (e) {
if (proxyStarted) {
try { await stopBuiltinClaudeProxyRuntime(); } catch (_) {}
try { resetBuiltinClaudeProxySavedSettingsToResponses(); } catch (_) {}
}
return {
success: false,
mode: 'settings-file',
Expand Down Expand Up @@ -9337,7 +9414,7 @@ async function cmdClaude(args = []) {
throw new Error('BaseURL 和 API 密钥必填');
}

const result = applyToClaudeSettings({
const result = await applyToClaudeSettings({
baseUrl: normalizedBaseUrl,
apiKey: normalizedKey,
model: normalizedModel
Expand Down Expand Up @@ -10946,7 +11023,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
result = applyClaudeSettingsRaw(params || {});
break;
case 'apply-claude-config':
result = applyToClaudeSettings(params.config);
result = await applyToClaudeSettings(params.config);
if (result && !result.error) {
const cfgName = (params && params.config && typeof params.config.name === 'string') ? params.config.name : '';
const cfgFrom = (params && typeof params.previousName === 'string') ? params.previousName : '';
Expand Down Expand Up @@ -15494,7 +15571,9 @@ function createMcpTools(options = {}) {
properties: {
apiKey: { type: 'string' },
baseUrl: { type: 'string' },
model: { type: 'string' }
model: { type: 'string' },
name: { type: 'string' },
targetApi: { type: 'string' }
},
required: ['apiKey'],
additionalProperties: false
Expand Down
Loading
Loading