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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccproxypal",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Local proxy server that routes AI API requests through your Claude Code OAuth subscription",
5
5
  "type": "module",
6
6
  "bin": {
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
- ?.filter((t) => t.type === 'function' || t.name)
48
- .map((t) =>
49
- t.type === 'function'
50
- ? { name: t.function.name, description: t.function.description, input_schema: t.function.parameters ?? { type: 'object', properties: {} } }
51
- : t
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
- async function callAnthropic(body, reqHeaders) {
34
- const { accessToken } = await getToken();
35
-
36
- const upstreamBody = injectClaudeCodeSystem(body);
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
- // Remove fields unsupported by Claude Code
39
- delete upstreamBody.reasoning_budget;
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 res = await fetch(`${ANTHROPIC_API_BASE}/v1/messages`, {
42
- method: 'POST',
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 so next request re-loads credentials
54
- if (res.status === 401) clearTokenCache();
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
- _cache = { accessToken, refreshToken, expiresAt: Date.now() + 60 * 60 * 1000 };
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() {