Fix : Claude OAuth

This commit is contained in:
decolua
2026-03-03 14:46:05 +07:00
parent 38ded5c62f
commit 07d4cdfa7e
7 changed files with 104 additions and 15 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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=<ver>.<build>; cc_entrypoint=cli; cch=<hash>;
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;
}

View File

@@ -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",
};

View File

@@ -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, {