diff --git a/open-sse/config/appConstants.js b/open-sse/config/appConstants.js index 7988817..3a96be5 100644 --- a/open-sse/config/appConstants.js +++ b/open-sse/config/appConstants.js @@ -65,6 +65,34 @@ export const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local // Suffix added to client tools when forwarding to Antigravity provider (anti-ban cloaking) export const AG_TOOL_SUFFIX = "_ide"; +// Suffix added to client tools when forwarding to Claude provider (anti-ban cloaking) +export const CLAUDE_TOOL_SUFFIX = "_ide"; + +// CC native default tools — these are Claude Code's own tools, kept as decoys +// Client tools matching these names are skipped (not renamed), others get _cc suffix +export const CC_DEFAULT_TOOLS = new Set([ + "Task", + "TaskOutput", + "TaskStop", + "TaskCreate", + "TaskGet", + "TaskUpdate", + "TaskList", + "Bash", + "Glob", + "Grep", + "Read", + "Edit", + "Write", + "NotebookEdit", + "WebFetch", + "WebSearch", + "AskUserQuestion", + "Skill", + "EnterPlanMode", + "ExitPlanMode", +]); + // AG native default tools — kept as decoys with neutral description/properties // These names must match exactly what AG sends in the real request log export const AG_DEFAULT_TOOLS = new Set([ @@ -115,7 +143,7 @@ export const LOAD_CODE_ASSIST_METADATA = { }; // System prompts -export const CLAUDE_SYSTEM_PROMPT = "You are a Claude agent, built on Anthropic's Claude Agent SDK."; +export const CLAUDE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude."; 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**"; // OAuth endpoints diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js index f6cd561..4dd54ab 100644 --- a/open-sse/config/providers.js +++ b/open-sse/config/providers.js @@ -36,14 +36,14 @@ export const PROVIDERS = { retry: { 429: 0 }, 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,prompt-caching-scope-2026-01-05", + "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28", "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": "claude-cli/2.1.63 (external, cli)", + "User-Agent": "claude-cli/2.1.92 (external, sdk-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.74.0", + "X-Stainless-Runtime-Version": "v24.14.0", + "X-Stainless-Package-Version": "0.80.0", "X-Stainless-Runtime": "node", "X-Stainless-Lang": "js", "X-Stainless-Arch": mapStainlessArch(), diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index 203db79..f921ac3 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -68,7 +68,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred log?.debug?.("PASSTHROUGH", `${clientTool} → ${provider} | native lossless`); translatedBody = { ...body, model }; } else { - translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger, modelCaps); + translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger, modelCaps, connectionId); if (!translatedBody) { trackPendingRequest(model, provider, connectionId, false, true); return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Failed to translate request for ${sourceFormat} → ${targetFormat}`); diff --git a/open-sse/translator/helpers/claudeHelper.js b/open-sse/translator/helpers/claudeHelper.js index e72e782..a08b6bc 100644 --- a/open-sse/translator/helpers/claudeHelper.js +++ b/open-sse/translator/helpers/claudeHelper.js @@ -2,6 +2,7 @@ import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js"; import { adjustMaxTokens } from "./maxTokensHelper.js"; import { applyCloaking } from "../../utils/claudeCloaking.js"; +import { deriveSessionId } from "../../utils/sessionManager.js"; // Check if message has valid non-empty content export function hasValidContent(msg) { @@ -81,7 +82,7 @@ export function fixToolUseOrdering(messages) { // - Add thinking block for Anthropic endpoint (provider === "claude") // - Fix tool_use/tool_result ordering // - Apply cloaking (billing header + fake user ID) for OAuth tokens -export function prepareClaudeRequest(body, provider = null, apiKey = null) { +export function prepareClaudeRequest(body, provider = null, apiKey = null, connectionId = 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) => { @@ -196,8 +197,10 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null) { } // Apply cloaking for OAuth tokens (billing header + fake user ID) + // session_id in user_id must match X-Claude-Code-Session-Id for fingerprint consistency if ((provider === "claude" || provider?.startsWith("anthropic-compatible")) && apiKey) { - body = applyCloaking(body, apiKey); + const sessionId = connectionId ? deriveSessionId(connectionId) : null; + body = applyCloaking(body, apiKey, sessionId); } return body; diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index dc6fa4b..c2238e7 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -1,6 +1,7 @@ import { FORMATS } from "./formats.js"; import { ensureToolCallIds, fixMissingToolResponses } from "./helpers/toolCallHelper.js"; import { prepareClaudeRequest } from "./helpers/claudeHelper.js"; +import { cloakClaudeTools } from "../utils/claudeCloaking.js"; import { filterToOpenAIFormat } from "./helpers/openaiHelper.js"; import { normalizeThinkingConfig } from "../services/provider.js"; import { AntigravityExecutor } from "../executors/antigravity.js"; @@ -67,7 +68,7 @@ function stripUnsupportedMultimodal(body, multimodal = {}) { } // Translate request: source -> openai -> target -export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, caps = null) { +export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, caps = null, connectionId = null) { ensureInitialized(); let result = body; @@ -122,7 +123,20 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream // Final step: prepare request for Claude format endpoints if (targetFormat === FORMATS.CLAUDE) { const apiKey = credentials?.accessToken || credentials?.apiKey || null; - result = prepareClaudeRequest(result, provider, apiKey); + result = prepareClaudeRequest(result, provider, apiKey, connectionId); + } + + // Claude cloaking: rename client tools with _cc suffix (anti-ban) + // Only for claude provider (not anthropic-compatible-*) with OAuth token + if (provider === "claude") { + const apiKey = credentials?.accessToken || credentials?.apiKey || null; + if (apiKey?.includes("sk-ant-oat")) { + const { body: cloakedBody, toolNameMap } = cloakClaudeTools(result); + result = cloakedBody; + if (toolNameMap?.size > 0) { + result._toolNameMap = toolNameMap; + } + } } // Antigravity cloaking: rename client tools + inject decoys (anti-ban) diff --git a/open-sse/utils/claudeCloaking.js b/open-sse/utils/claudeCloaking.js index 8cb490d..ae64391 100644 --- a/open-sse/utils/claudeCloaking.js +++ b/open-sse/utils/claudeCloaking.js @@ -1,36 +1,106 @@ -import { createHash, randomBytes } from "crypto"; +import { createHash, randomBytes, randomUUID } from "crypto"; +import { CLAUDE_TOOL_SUFFIX, CC_DEFAULT_TOOLS } from "../config/appConstants.js"; -const CLAUDE_VERSION = "2.1.63"; +const CLAUDE_VERSION = "2.1.92"; +const CC_ENTRYPOINT = "sdk-cli"; -// Generate billing header matching real Claude Code format: -// x-anthropic-billing-header: cc_version=.; cc_entrypoint=cli; cch=; +// Generate billing header matching real Claude Code 2.1.92+ format: +// x-anthropic-billing-header: cc_version=.; cc_entrypoint=sdk-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};`; + return `x-anthropic-billing-header: cc_version=${CLAUDE_VERSION}.${buildHash}; cc_entrypoint=${CC_ENTRYPOINT}; 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)}`; +// Generate fake user ID in Claude Code 2.1.92+ JSON format: +// {"device_id":"<64hex>","account_uuid":"","session_id":""} +function generateFakeUserID(sessionId) { + const deviceId = randomBytes(32).toString("hex"); + const accountUuid = randomUUID(); + const sessionUuid = sessionId || randomUUID(); + return `{"device_id":"${deviceId}","account_uuid":"${accountUuid}","session_id":"${sessionUuid}"}`; } +/** + * Cloak tools before sending to Claude provider (anti-ban): + * - Rename non-CC client tools with _cc suffix in tools[] and messages[] + * - Skip tools that are already CC default names (they become decoys as-is) + * - Inject CC_DECOY_TOOLS after client tools + * Returns { body, toolNameMap } where toolNameMap maps suffixed → original + * @param {object} body - Claude API request body + * @returns {{ body: object, toolNameMap: Map|null }} + */ +export function cloakClaudeTools(body) { + const tools = body.tools; + if (!tools || tools.length === 0) return { body, toolNameMap: null }; + + const toolNameMap = new Map(); + const clientDeclarations = []; + + // All client tools get renamed with suffix + for (const tool of tools) { + const suffixed = `${tool.name}${CLAUDE_TOOL_SUFFIX}`; + toolNameMap.set(suffixed, tool.name); + clientDeclarations.push({ ...tool, name: suffixed }); + } + + // Client tools first, then CC decoy tools (no overlap: client tools all have _cc suffix) + const allTools = [...clientDeclarations, ...CC_DECOY_TOOLS]; + + // Rename tool_use in message history (all client tools get suffix) + const renamedMessages = body.messages?.map(msg => { + if (!Array.isArray(msg.content)) return msg; + const renamedContent = msg.content.map(block => { + if (block.type === "tool_use") { + return { ...block, name: `${block.name}${CLAUDE_TOOL_SUFFIX}` }; + } + return block; + }); + return { ...msg, content: renamedContent }; + }); + + return { + body: { ...body, tools: allTools, messages: renamedMessages || body.messages }, + toolNameMap: toolNameMap.size > 0 ? toolNameMap : null + }; +} + +// CC decoy tools — Claude Code native tool names, marked unavailable +const CC_DECOY_TOOLS = [ + { name: "Task", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "TaskOutput", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "TaskStop", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "TaskCreate", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "TaskGet", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "TaskUpdate", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "TaskList", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "Bash", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "Glob", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "Grep", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "Read", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "Edit", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "Write", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "NotebookEdit", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "WebFetch", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "WebSearch", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "AskUserQuestion", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "Skill", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "EnterPlanMode", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, + { name: "ExitPlanMode", description: "This tool is currently unavailable.", input_schema: { type: "object", properties: {} } }, +]; + /** * Apply Claude cloaking to request body: * 1. Inject billing header as first system block - * 2. Inject fake user ID into metadata - * + * 2. Inject fake user ID into metadata (JSON format, session_id aligned with X-Claude-Code-Session-Id) * Only applies when using OAuth token (sk-ant-oat). * @param {object} body - Claude API request body * @param {string} apiKey - API key or OAuth token + * @param {string} [sessionId] - Session ID to align with X-Claude-Code-Session-Id header * @returns {object} Modified body */ -export function applyCloaking(body, apiKey) { +export function applyCloaking(body, apiKey, sessionId) { if (!apiKey || !apiKey.includes("sk-ant-oat")) return body; const result = { ...body }; @@ -50,10 +120,10 @@ export function applyCloaking(body, apiKey) { result.system = [billingBlock]; } - // Inject fake user ID into metadata + // Inject fake user ID into metadata (session_id must match X-Claude-Code-Session-Id) const existingUserId = result.metadata?.user_id; if (!existingUserId) { - result.metadata = { ...result.metadata, user_id: generateFakeUserID() }; + result.metadata = { ...result.metadata, user_id: generateFakeUserID(sessionId) }; } return result; diff --git a/open-sse/utils/claudeHeaderCache.js b/open-sse/utils/claudeHeaderCache.js index d381b1e..11b2eb8 100644 --- a/open-sse/utils/claudeHeaderCache.js +++ b/open-sse/utils/claudeHeaderCache.js @@ -19,6 +19,7 @@ const CLAUDE_IDENTITY_HEADERS = [ "x-stainless-arch", "x-stainless-os", "x-stainless-timeout", + "x-claude-code-session-id", "package-version", "runtime-version", "os",