-
Notifications
You must be signed in to change notification settings - Fork 8
feat: add Claude chat completions proxy mode #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4b24338
9c40b1a
3501aca
7b49467
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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([ | ||
|
|
@@ -5488,7 +5489,9 @@ const { | |
| HTTPS_KEEP_ALIVE_AGENT, | ||
| readConfigOrVirtualDefault, | ||
| resolveBuiltinProxyProviderName, | ||
| resolveAuthTokenFromCurrentProfile | ||
| resolveAuthTokenFromCurrentProfile, | ||
| OPENAI_BRIDGE_SETTINGS_FILE, | ||
| resolveOpenaiBridgeUpstream | ||
| }); | ||
|
|
||
| function applyBuiltinProxyProvider(params = {}) { | ||
|
|
@@ -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) { | ||
|
|
@@ -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
+9192
to
+9203
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.jsRepository: 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()}")
PYRepository: 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.jsRepository: 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 || trueRepository: 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
doneRepository: SakuraByteCore/codexmate Length of output: 45075 Fix proxy-token/auth mismatch in
🤖 Prompt for AI Agents |
||
| 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 { | ||
|
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 }; | ||
| } | ||
|
|
||
|
|
@@ -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; | ||
|
|
@@ -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', | ||
|
|
@@ -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 | ||
|
|
@@ -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 : ''; | ||
|
|
@@ -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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reject
chat_completionsmode without an explicit upstream URL.When
config.baseUrlis empty, this path still falls back to the Anthropic Messages base URL and passes it intoupstreamBaseUrl. That silently creates achat_completionsproxy against the wrong upstream instead of failing fast. Return a validation error here, or omitupstreamBaseUrlso the runtime can resolve it fromconfig.nameif that's supported.Possible fix
🤖 Prompt for AI Agents