ccproxypal 0.1.3 → 0.1.4
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/package.json +1 -1
- package/src/adapter.js +22 -7
- package/src/server.js +45 -19
- package/src/token.js +2 -1
package/package.json
CHANGED
package/src/adapter.js
CHANGED
|
@@ -22,6 +22,11 @@ export function openaiToAnthropic(body) {
|
|
|
22
22
|
for (const msg of convMsgs) {
|
|
23
23
|
if (msg.role === 'user') {
|
|
24
24
|
const content = convertUserContent(msg.content);
|
|
25
|
+
// Skip user messages with empty content — Anthropic rejects them
|
|
26
|
+
const isEmpty = Array.isArray(content) ? content.length === 0
|
|
27
|
+
: typeof content === 'string' ? content.trim().length === 0
|
|
28
|
+
: !content;
|
|
29
|
+
if (isEmpty) continue;
|
|
25
30
|
anthropicMessages.push({ role: 'user', content });
|
|
26
31
|
} else if (msg.role === 'assistant') {
|
|
27
32
|
const content = convertAssistantContent(msg);
|
|
@@ -42,14 +47,24 @@ export function openaiToAnthropic(body) {
|
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
// Convert tool definitions
|
|
50
|
+
// Convert tool definitions — filter out entries with null/invalid names (e.g. Cursor placeholders)
|
|
46
51
|
const anthropicTools = tools
|
|
47
|
-
?.
|
|
48
|
-
|
|
49
|
-
t.type === '
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
?.map((t) => {
|
|
53
|
+
// Claude Code format: {type:"custom", custom:{name,...}} → standard
|
|
54
|
+
if (t.type === 'custom' && t.custom) {
|
|
55
|
+
const { name, description, input_schema } = t.custom;
|
|
56
|
+
if (!name || typeof name !== 'string') return null;
|
|
57
|
+
return { name, description, input_schema };
|
|
58
|
+
}
|
|
59
|
+
// OpenAI format: {type:"function", function:{name,...}}
|
|
60
|
+
if (t.type === 'function' && t.function) {
|
|
61
|
+
return { name: t.function.name, description: t.function.description, input_schema: t.function.parameters ?? { type: 'object', properties: {} } };
|
|
62
|
+
}
|
|
63
|
+
// Already Anthropic format — drop if name is missing/null
|
|
64
|
+
if (!t.name || typeof t.name !== 'string') return null;
|
|
65
|
+
return t;
|
|
66
|
+
})
|
|
67
|
+
.filter(Boolean);
|
|
53
68
|
|
|
54
69
|
return {
|
|
55
70
|
model: model ?? 'claude-opus-4-5',
|
package/src/server.js
CHANGED
|
@@ -30,28 +30,54 @@ function jsonResponse(res, status, body) {
|
|
|
30
30
|
|
|
31
31
|
// ─── Upstream request to Anthropic API ───────────────────────────────────────
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
function sanitizeBody(body) {
|
|
34
|
+
const b = injectClaudeCodeSystem(body);
|
|
35
|
+
// Remove fields the Anthropic OAuth API doesn't support
|
|
36
|
+
delete b.reasoning_budget;
|
|
37
|
+
delete b.context_management;
|
|
38
|
+
// Filter out tools with null/empty names (Cursor sends placeholder entries)
|
|
39
|
+
if (Array.isArray(b.tools)) {
|
|
40
|
+
b.tools = b.tools
|
|
41
|
+
.map((t) => {
|
|
42
|
+
if (t.type === 'custom' && t.custom) {
|
|
43
|
+
const { name, description, input_schema } = t.custom;
|
|
44
|
+
return (name && typeof name === 'string') ? { name, description, input_schema } : null;
|
|
45
|
+
}
|
|
46
|
+
return (t.name && typeof t.name === 'string') ? t : null;
|
|
47
|
+
})
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
if (b.tools.length === 0) delete b.tools;
|
|
50
|
+
}
|
|
51
|
+
return b;
|
|
52
|
+
}
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
async function callAnthropic(body, reqHeaders) {
|
|
55
|
+
const makeRequest = async (accessToken) =>
|
|
56
|
+
fetch(`${ANTHROPIC_API_BASE}/v1/messages`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
61
|
+
'anthropic-beta': [reqHeaders['anthropic-beta'], CLAUDE_CODE_BETA].filter(Boolean).join(','),
|
|
62
|
+
'anthropic-version': reqHeaders['anthropic-version'] ?? ANTHROPIC_VERSION,
|
|
63
|
+
'User-Agent': USER_AGENT,
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify(sanitizeBody(body)),
|
|
66
|
+
});
|
|
40
67
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
headers: {
|
|
44
|
-
'Content-Type': 'application/json',
|
|
45
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
46
|
-
'anthropic-beta': [reqHeaders['anthropic-beta'], CLAUDE_CODE_BETA].filter(Boolean).join(','),
|
|
47
|
-
'anthropic-version': reqHeaders['anthropic-version'] ?? ANTHROPIC_VERSION,
|
|
48
|
-
'User-Agent': USER_AGENT,
|
|
49
|
-
},
|
|
50
|
-
body: JSON.stringify(upstreamBody),
|
|
51
|
-
});
|
|
68
|
+
const { accessToken } = await getToken();
|
|
69
|
+
let res = await makeRequest(accessToken);
|
|
52
70
|
|
|
53
|
-
// On 401 clear cache
|
|
54
|
-
if (res.status === 401)
|
|
71
|
+
// On 401: clear cache, refresh token, retry once
|
|
72
|
+
if (res.status === 401) {
|
|
73
|
+
clearTokenCache();
|
|
74
|
+
try {
|
|
75
|
+
const { accessToken: newToken } = await getToken();
|
|
76
|
+
res = await makeRequest(newToken);
|
|
77
|
+
} catch {
|
|
78
|
+
// If refresh fails, return the original 401
|
|
79
|
+
}
|
|
80
|
+
}
|
|
55
81
|
|
|
56
82
|
return res;
|
|
57
83
|
}
|
package/src/token.js
CHANGED
|
@@ -11,7 +11,8 @@ let _cache = null;
|
|
|
11
11
|
|
|
12
12
|
/** Inject tokens manually (client mode — no local credentials needed) */
|
|
13
13
|
export function setManualToken(accessToken, refreshToken) {
|
|
14
|
-
|
|
14
|
+
// 55 min — lets the 5-min buffer trigger a refresh before actual expiry
|
|
15
|
+
_cache = { accessToken, refreshToken, expiresAt: Date.now() + 55 * 60 * 1000 };
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function loadFromKeychain() {
|