diff --git a/open-sse/config/constants.js b/open-sse/config/constants.js index e22c1b7..738ab0e 100644 --- a/open-sse/config/constants.js +++ b/open-sse/config/constants.js @@ -1,5 +1,24 @@ import { platform, arch } from "os"; +function mapStainlessOs() { + switch (platform()) { + case "darwin": return "MacOS"; + case "win32": return "Windows"; + case "linux": return "Linux"; + case "freebsd": return "FreeBSD"; + default: return `Other::${platform()}`; + } +} + +function mapStainlessArch() { + switch (arch()) { + case "x64": return "x64"; + case "arm64": return "arm64"; + case "ia32": return "x86"; + default: return `other::${arch()}`; + } +} + // === GitHub Copilot Version Constants === export const GITHUB_COPILOT = { VSCODE_VERSION: "1.110.0", @@ -96,23 +115,23 @@ export const PROVIDERS = { format: "claude", headers: { "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27", + "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05", "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": "claude-cli/1.0.83 (external, cli)", + "User-Agent": "claude-cli/2.1.63 (external, cli)", "X-App": "cli", "X-Stainless-Helper-Method": "stream", "X-Stainless-Retry-Count": "0", "X-Stainless-Runtime-Version": "v24.3.0", - "X-Stainless-Package-Version": "0.55.1", + "X-Stainless-Package-Version": "0.74.0", "X-Stainless-Runtime": "node", "X-Stainless-Lang": "js", - "X-Stainless-Arch": "arm64", - "X-Stainless-Os": "MacOS", - "X-Stainless-Timeout": "60" + "X-Stainless-Arch": mapStainlessArch(), + "X-Stainless-Os": mapStainlessOs(), + "X-Stainless-Timeout": "600" }, // Claude OAuth configuration clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", - tokenUrl: "https://console.anthropic.com/v1/oauth/token" + tokenUrl: "https://api.anthropic.com/v1/oauth/token" }, gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/models", @@ -376,7 +395,7 @@ export const PROVIDERS = { }; // Claude system prompt -export const CLAUDE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude."; +export const CLAUDE_SYSTEM_PROMPT = "You are a Claude agent, built on Anthropic's Claude Agent SDK."; // Antigravity default system prompt (required for API to work) export const ANTIGRAVITY_DEFAULT_SYSTEM = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**"; @@ -392,8 +411,8 @@ export const OAUTH_ENDPOINTS = { auth: "https://auth.openai.com/oauth/authorize" }, anthropic: { - token: "https://console.anthropic.com/v1/oauth/token", - auth: "https://console.anthropic.com/v1/oauth/authorize" + token: "https://api.anthropic.com/v1/oauth/token", + auth: "https://api.anthropic.com/v1/oauth/authorize" }, qwen: { token: "https://chat.qwen.ai/api/v1/oauth2/token", // From CLIProxyAPI diff --git a/open-sse/translator/helpers/claudeHelper.js b/open-sse/translator/helpers/claudeHelper.js index efd8bcc..a06bab0 100644 --- a/open-sse/translator/helpers/claudeHelper.js +++ b/open-sse/translator/helpers/claudeHelper.js @@ -1,6 +1,7 @@ // Claude helper functions for translator import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js"; import { adjustMaxTokens } from "./maxTokensHelper.js"; +import { applyCloaking } from "../../utils/claudeCloaking.js"; // Check if message has valid non-empty content export function hasValidContent(msg) { @@ -79,7 +80,8 @@ export function fixToolUseOrdering(messages) { // - Filter empty messages // - Add thinking block for Anthropic endpoint (provider === "claude") // - Fix tool_use/tool_result ordering -export function prepareClaudeRequest(body, provider = null) { +// - Apply cloaking (billing header + fake user ID) for OAuth tokens +export function prepareClaudeRequest(body, provider = null, apiKey = null) { // 1. System: remove all cache_control, add only to last block with ttl 1h if (body.system && Array.isArray(body.system)) { body.system = body.system.map((block, i) => { @@ -186,6 +188,11 @@ export function prepareClaudeRequest(body, provider = null) { } } + // Apply cloaking for OAuth tokens (billing header + fake user ID) + if (provider === "claude" && apiKey) { + body = applyCloaking(body, apiKey); + } + return body; } diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index 2bf8969..b1309e3 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -90,7 +90,8 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream // Final step: prepare request for Claude format endpoints if (targetFormat === FORMATS.CLAUDE) { - result = prepareClaudeRequest(result, provider); + const apiKey = credentials?.accessToken || credentials?.apiKey || null; + result = prepareClaudeRequest(result, provider, apiKey); } return result; diff --git a/open-sse/translator/request/openai-to-claude.js b/open-sse/translator/request/openai-to-claude.js index 05ab08f..e2e9663 100644 --- a/open-sse/translator/request/openai-to-claude.js +++ b/open-sse/translator/request/openai-to-claude.js @@ -3,8 +3,9 @@ import { FORMATS } from "../formats.js"; import { CLAUDE_SYSTEM_PROMPT } from "../../config/constants.js"; import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; -// Prefix for Claude OAuth tool names to avoid conflicts -const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; +// Empty prefix matches real Claude Code behavior (no tool name prefix). +// Previously "proxy_" was used but this is a detectable fingerprint difference. +const CLAUDE_OAUTH_TOOL_PREFIX = ""; // Convert OpenAI request to Claude format export function openaiToClaudeRequest(model, body, stream) { diff --git a/open-sse/utils/claudeCloaking.js b/open-sse/utils/claudeCloaking.js new file mode 100644 index 0000000..8cb490d --- /dev/null +++ b/open-sse/utils/claudeCloaking.js @@ -0,0 +1,60 @@ +import { createHash, randomBytes } from "crypto"; + +const CLAUDE_VERSION = "2.1.63"; + +// Generate billing header matching real Claude Code format: +// x-anthropic-billing-header: cc_version=.; cc_entrypoint=cli; cch=; +function generateBillingHeader(payload) { + const content = JSON.stringify(payload); + const cch = createHash("sha256").update(content).digest("hex").slice(0, 5); + const buildHash = randomBytes(2).toString("hex").slice(0, 3); + return `x-anthropic-billing-header: cc_version=${CLAUDE_VERSION}.${buildHash}; cc_entrypoint=cli; cch=${cch};`; +} + +// Generate a random UUID v4 +function generateFakeUserID() { + const bytes = randomBytes(16); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = bytes.toString("hex"); + return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`; +} + +/** + * Apply Claude cloaking to request body: + * 1. Inject billing header as first system block + * 2. Inject fake user ID into metadata + * + * Only applies when using OAuth token (sk-ant-oat). + * @param {object} body - Claude API request body + * @param {string} apiKey - API key or OAuth token + * @returns {object} Modified body + */ +export function applyCloaking(body, apiKey) { + if (!apiKey || !apiKey.includes("sk-ant-oat")) return body; + + const result = { ...body }; + + // Inject billing header as system[0], preserve existing system blocks + const billingText = generateBillingHeader(body); + const billingBlock = { type: "text", text: billingText }; + + if (Array.isArray(result.system)) { + // Skip if already injected + if (!result.system[0]?.text?.startsWith("x-anthropic-billing-header:")) { + result.system = [billingBlock, ...result.system]; + } + } else if (typeof result.system === "string") { + result.system = [billingBlock, { type: "text", text: result.system }]; + } else { + result.system = [billingBlock]; + } + + // Inject fake user ID into metadata + const existingUserId = result.metadata?.user_id; + if (!existingUserId) { + result.metadata = { ...result.metadata, user_id: generateFakeUserID() }; + } + + return result; +} diff --git a/src/lib/oauth/constants/oauth.js b/src/lib/oauth/constants/oauth.js index 3e52c5d..ee20dd1 100644 --- a/src/lib/oauth/constants/oauth.js +++ b/src/lib/oauth/constants/oauth.js @@ -20,7 +20,7 @@ function getOAuthPlatformEnum() { export const CLAUDE_CONFIG = { clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", authorizeUrl: "https://claude.ai/oauth/authorize", - tokenUrl: "https://console.anthropic.com/v1/oauth/token", + tokenUrl: "https://api.anthropic.com/v1/oauth/token", scopes: ["org:create_api_key", "user:profile", "user:inference"], codeChallengeMethod: "S256", }; diff --git a/src/mitm/server.js b/src/mitm/server.js index 7f9d52a..e2f774f 100644 --- a/src/mitm/server.js +++ b/src/mitm/server.js @@ -140,6 +140,7 @@ async function passthrough(req, res, bodyBuffer) { async function intercept(req, res, bodyBuffer, mappedModel) { try { const body = JSON.parse(bodyBuffer.toString()); + console.log("🚀 ~ intercept ~ body:", body) body.model = mappedModel; const response = await fetch(ROUTER_URL, {