claude-codex-proxy 1.0.0
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/LICENSE +21 -0
- package/README.md +227 -0
- package/bin/cli.js +113 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +244 -0
- package/docs/ARCHITECTURE.md +119 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +83 -0
- package/docs/OPENCLAW.md +33 -0
- package/docs/legal.md +9 -0
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +56 -0
- package/public/css/style.css +791 -0
- package/public/index.html +838 -0
- package/public/js/app.js +619 -0
- package/src/account-manager.js +526 -0
- package/src/account-rotation/index.js +93 -0
- package/src/account-rotation/rate-limits.js +293 -0
- package/src/account-rotation/strategies/base-strategy.js +48 -0
- package/src/account-rotation/strategies/index.js +31 -0
- package/src/account-rotation/strategies/round-robin-strategy.js +42 -0
- package/src/account-rotation/strategies/sticky-strategy.js +97 -0
- package/src/claude-config.js +154 -0
- package/src/cli/accounts.js +551 -0
- package/src/direct-api.js +214 -0
- package/src/format-converter.js +563 -0
- package/src/index.js +45 -0
- package/src/middleware/credentials.js +116 -0
- package/src/middleware/sse.js +130 -0
- package/src/model-api.js +189 -0
- package/src/model-mapper.js +88 -0
- package/src/oauth.js +623 -0
- package/src/response-streamer.js +469 -0
- package/src/routes/accounts-route.js +296 -0
- package/src/routes/api-routes.js +102 -0
- package/src/routes/chat-route.js +239 -0
- package/src/routes/claude-config-route.js +114 -0
- package/src/routes/logs-route.js +43 -0
- package/src/routes/messages-route.js +164 -0
- package/src/routes/models-route.js +115 -0
- package/src/routes/settings-route.js +154 -0
- package/src/server-settings.js +70 -0
- package/src/server.js +57 -0
- package/src/signature-cache.js +106 -0
- package/src/thinking-utils.js +312 -0
- package/src/utils/logger.js +170 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct API Client
|
|
3
|
+
* Makes direct HTTP calls to ChatGPT's backend API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import { convertAnthropicToResponsesAPI, convertOutputToAnthropic, generateMessageId } from './format-converter.js';
|
|
8
|
+
import { streamResponsesAPI, parseResponsesAPIResponse, createResponsesAPIError } from './response-streamer.js';
|
|
9
|
+
import { getServerSettings } from './server-settings.js';
|
|
10
|
+
import { logger } from './utils/logger.js';
|
|
11
|
+
|
|
12
|
+
const API_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
13
|
+
const APP_VERSION = '1.0.5';
|
|
14
|
+
|
|
15
|
+
function shouldLogRawProxyPayloads() {
|
|
16
|
+
return getServerSettings().logRawProxyPayloads === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildOpencodeHeaders(accessToken, accountId, context = {}) {
|
|
20
|
+
const headers = {
|
|
21
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
22
|
+
'ChatGPT-Account-Id': accountId,
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'Accept': 'text/event-stream',
|
|
25
|
+
'originator': 'opencode',
|
|
26
|
+
'User-Agent': `opencode/${APP_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (context.sessionId) {
|
|
30
|
+
headers['session-id'] = context.sessionId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return headers;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseResetTime(response, errorText) {
|
|
37
|
+
const retryAfter = response.headers?.get?.('retry-after');
|
|
38
|
+
if (retryAfter) {
|
|
39
|
+
const seconds = parseInt(retryAfter, 10);
|
|
40
|
+
if (!isNaN(seconds)) return seconds * 1000;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ratelimitReset = response.headers?.get?.('x-ratelimit-reset');
|
|
44
|
+
if (ratelimitReset) {
|
|
45
|
+
const timestamp = parseInt(ratelimitReset, 10) * 1000;
|
|
46
|
+
const wait = timestamp - Date.now();
|
|
47
|
+
if (wait > 0) return wait;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (errorText) {
|
|
51
|
+
const delayMatch = errorText.match(/quotaResetDelay[:\s"]+(\d+(?:\.\d+)?)(ms|s)/i);
|
|
52
|
+
if (delayMatch) {
|
|
53
|
+
const value = parseFloat(delayMatch[1]);
|
|
54
|
+
return delayMatch[2] === 's' ? value * 1000 : value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const secMatch = errorText.match(/retry\s+(?:after\s+)?(\d+)\s*(?:sec|s\b)/i);
|
|
58
|
+
if (secMatch) {
|
|
59
|
+
return parseInt(secMatch[1], 10) * 1000;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return 60000;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getProxyStatusForUpstream(status) {
|
|
67
|
+
if (status >= 500) return 502;
|
|
68
|
+
return status || 500;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createHTTPError(message, status, anthropicErrorType = 'api_error', upstreamStatus = null) {
|
|
72
|
+
const error = new Error(message);
|
|
73
|
+
error.status = status;
|
|
74
|
+
error.anthropicErrorType = anthropicErrorType;
|
|
75
|
+
if (upstreamStatus !== null) {
|
|
76
|
+
error.upstreamStatus = upstreamStatus;
|
|
77
|
+
error.isUpstreamError = true;
|
|
78
|
+
}
|
|
79
|
+
return error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createUpstreamHTTPError(response, errorText) {
|
|
83
|
+
const proxyStatus = getProxyStatusForUpstream(response.status);
|
|
84
|
+
return createHTTPError(`API_ERROR: ${response.status} - ${errorText}`, proxyStatus, 'api_error', response.status);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function createResponsesAPIStreamResponse(anthropicRequest, accessToken, accountId, accountRotator = null, currentEmail = null, context = {}) {
|
|
88
|
+
const modelId = anthropicRequest.model;
|
|
89
|
+
const request = convertAnthropicToResponsesAPI(anthropicRequest);
|
|
90
|
+
if (shouldLogRawProxyPayloads()) {
|
|
91
|
+
logger.info('[OpenAI Request]', request);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const response = await fetch(API_URL, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: buildOpencodeHeaders(accessToken, accountId, context),
|
|
97
|
+
body: JSON.stringify(request)
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (response.ok) return response;
|
|
101
|
+
|
|
102
|
+
const errorText = await response.text();
|
|
103
|
+
if (shouldLogRawProxyPayloads()) {
|
|
104
|
+
logger.info('[OpenAI Error]', { status: response.status, body: errorText });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (response.status === 401) {
|
|
108
|
+
if (accountRotator && currentEmail) {
|
|
109
|
+
accountRotator.markInvalid(currentEmail, 'Token expired or revoked');
|
|
110
|
+
}
|
|
111
|
+
throw createHTTPError('AUTH_EXPIRED: Token expired or revoked. Please re-authenticate.', 401, 'authentication_error', response.status);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (response.status === 429) {
|
|
115
|
+
const resetMs = parseResetTime(response, errorText);
|
|
116
|
+
if (accountRotator && currentEmail) {
|
|
117
|
+
accountRotator.markRateLimited(currentEmail, resetMs, modelId);
|
|
118
|
+
}
|
|
119
|
+
const error = createHTTPError(`RATE_LIMITED:${resetMs}:${errorText}`, 429, 'rate_limit_error', response.status);
|
|
120
|
+
error.resetMs = resetMs;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (response.status === 403) {
|
|
125
|
+
if (errorText.includes('challenge') || errorText.includes('cloudflare')) {
|
|
126
|
+
throw createHTTPError('CLOUDFLARE_BLOCKED: Request blocked by Cloudflare.', 403, 'api_error', response.status);
|
|
127
|
+
}
|
|
128
|
+
throw createHTTPError(`FORBIDDEN: ${errorText}`, 403, 'permission_error', response.status);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (response.status === 400) {
|
|
132
|
+
throw createHTTPError(`INVALID_REQUEST: ${errorText}`, 400, 'invalid_request_error', response.status);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw createUpstreamHTTPError(response, errorText);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Send a streaming request to ChatGPT API
|
|
140
|
+
*/
|
|
141
|
+
export async function* sendMessageStream(anthropicRequest, accessToken, accountId, accountRotator = null, currentEmail = null, context = {}, responseModel = anthropicRequest.model) {
|
|
142
|
+
const response = await createResponsesAPIStreamResponse(anthropicRequest, accessToken, accountId, accountRotator, currentEmail, context);
|
|
143
|
+
yield* streamResponsesAPI(response, responseModel);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function createMessageStream(anthropicRequest, accessToken, accountId, accountRotator = null, currentEmail = null, context = {}, responseModel = anthropicRequest.model) {
|
|
147
|
+
const response = await createResponsesAPIStreamResponse(anthropicRequest, accessToken, accountId, accountRotator, currentEmail, context);
|
|
148
|
+
return streamResponsesAPI(response, responseModel);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Send a non-streaming request to ChatGPT API
|
|
153
|
+
*/
|
|
154
|
+
export async function sendMessage(anthropicRequest, accessToken, accountId, context = {}) {
|
|
155
|
+
const request = convertAnthropicToResponsesAPI({
|
|
156
|
+
...anthropicRequest,
|
|
157
|
+
stream: false
|
|
158
|
+
});
|
|
159
|
+
if (shouldLogRawProxyPayloads()) {
|
|
160
|
+
logger.info('[OpenAI Request]', request);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const response = await fetch(API_URL, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: buildOpencodeHeaders(accessToken, accountId, context),
|
|
166
|
+
body: JSON.stringify(request)
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const errorText = await response.text();
|
|
171
|
+
if (shouldLogRawProxyPayloads()) {
|
|
172
|
+
logger.info('[OpenAI Error]', { status: response.status, body: errorText });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (response.status === 401) {
|
|
176
|
+
throw createHTTPError('AUTH_EXPIRED: Token expired or revoked. Please re-authenticate.', 401, 'authentication_error', response.status);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw createUpstreamHTTPError(response, errorText);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const apiResponse = await parseResponsesAPIResponse(response);
|
|
183
|
+
|
|
184
|
+
if (!apiResponse) {
|
|
185
|
+
throw createResponsesAPIError('Upstream stream ended without response.completed');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const content = convertOutputToAnthropic(apiResponse.output);
|
|
189
|
+
const stopReason = content.some(c => c.type === 'tool_use') ? 'tool_use' : 'end_turn';
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
id: generateMessageId(),
|
|
193
|
+
type: 'message',
|
|
194
|
+
role: 'assistant',
|
|
195
|
+
content: content,
|
|
196
|
+
model: anthropicRequest.model,
|
|
197
|
+
stop_reason: stopReason,
|
|
198
|
+
stop_sequence: null,
|
|
199
|
+
usage: {
|
|
200
|
+
input_tokens: apiResponse.usage?.input_tokens || 0,
|
|
201
|
+
output_tokens: apiResponse.usage?.output_tokens || 0,
|
|
202
|
+
cache_read_input_tokens: apiResponse.usage?.cache_read_input_tokens || apiResponse.usage?.input_tokens_details?.cached_tokens || 0
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { parseResetTime };
|
|
208
|
+
|
|
209
|
+
export default {
|
|
210
|
+
sendMessageStream,
|
|
211
|
+
createMessageStream,
|
|
212
|
+
sendMessage,
|
|
213
|
+
parseResetTime
|
|
214
|
+
};
|