codex-copilot-dx 0.6.15 → 0.6.19
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/README.md +28 -15
- package/bin/cli.mjs +7 -1
- package/package.json +1 -1
- package/src/adapter.mjs +47 -10
- package/src/anthropic-handler.mjs +25 -6
- package/src/anthropic.mjs +19 -7
- package/src/atomic-file.mjs +76 -2
- package/src/auth-profile.mjs +11 -34
- package/src/auth.mjs +265 -52
- package/src/cli-auth.mjs +124 -94
- package/src/cli-error.mjs +44 -0
- package/src/cli-options.mjs +8 -5
- package/src/copilot-client.mjs +5 -1
- package/src/copilot-responses-compat.mjs +9 -2
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +138 -0
- package/src/lock.mjs +199 -17
- package/src/models.mjs +17 -0
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-relay.mjs +26 -5
- package/src/pm-studio-setup.mjs +351 -16
- package/src/pm-studio-status.mjs +5 -1
- package/src/response-history.mjs +122 -13
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +123 -36
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +13 -1
- package/src/responses-request.mjs +17 -5
- package/src/runtime-config.mjs +10 -0
- package/src/token-counter-service.mjs +188 -0
- package/src/token-counter-worker.mjs +10 -0
- package/src/usage.mjs +25 -7
package/README.md
CHANGED
|
@@ -143,7 +143,8 @@ integrity, isolated Claude profile, and live relay routing without changing file
|
|
|
143
143
|
ccdx pms status
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
`ccdx pm-studio status
|
|
146
|
+
`ccdx pm-studio status`, `ccdx pm-studio setup`, and `ccdx pm-studio restore`
|
|
147
|
+
are equivalent long-form aliases.
|
|
147
148
|
|
|
148
149
|
`ccdx pms setup` performs local preflight, structural verification, backup,
|
|
149
150
|
staging, integrity, signing, and replacement work. It does not access a PM
|
|
@@ -183,37 +184,47 @@ require reinstalling PM Studio. Setup prints the verified backup and
|
|
|
183
184
|
manifest paths plus the version-matched restore procedure. Never restore a
|
|
184
185
|
backup over a different PM Studio version or build.
|
|
185
186
|
|
|
186
|
-
To restore, first quit PM Studio and its updater
|
|
187
|
-
|
|
188
|
-
and backup still report the same version/build printed by setup:
|
|
187
|
+
To restore the original local bundle, first quit PM Studio and its updater,
|
|
188
|
+
then run:
|
|
189
189
|
|
|
190
190
|
```bash
|
|
191
|
-
|
|
192
|
-
/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
|
|
193
|
-
/usr/bin/plutil -extract CFBundleVersion raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
|
|
194
|
-
/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
195
|
-
/usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
191
|
+
ccdx pms restore
|
|
196
192
|
```
|
|
197
193
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
The explicit command authorizes the local App replacement and does not show an
|
|
195
|
+
additional confirmation prompt. Restore neither needs the Claude profile nor
|
|
196
|
+
starts the adapter, Device Flow, Codex, or PM Studio. It selects the unique
|
|
197
|
+
source-bound backup itself, verifies the installed patch record and the exact
|
|
198
|
+
clean backup, stages and verifies the complete bundle, confirms that the
|
|
199
|
+
installed App did not change during staging, and then performs an atomic
|
|
200
|
+
replacement. It never restores a missing, ambiguous, drifted, or
|
|
201
|
+
version/build-mismatched record. The original local signature is inspected and
|
|
202
|
+
reported, but is not an admission gate: the exact source-bound content recorded
|
|
203
|
+
when setup created the backup is authoritative. The verified backup and manifest
|
|
204
|
+
remain available after a successful restore. Repeating restore on the same
|
|
205
|
+
recorded clean App is safe and does not replace it again.
|
|
206
|
+
|
|
207
|
+
If automatic restore reports that the replacement state cannot be verified, do
|
|
208
|
+
not launch PM Studio. Keep the recovery path printed by the command and reinstall
|
|
209
|
+
PM Studio. The following manual procedure is only an exceptional recovery path;
|
|
210
|
+
use the exact backup and manifest paths printed by setup or restore, and first
|
|
211
|
+
confirm that the installed App and backup still match the manifest version/build:
|
|
201
212
|
|
|
202
213
|
```bash
|
|
214
|
+
BACKUP_APP="<exact-verified-backup-path>"
|
|
203
215
|
RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
|
|
204
216
|
PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
|
|
205
217
|
mv "/Applications/PM Studio.app" "$PATCHED_APP"
|
|
206
218
|
/usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
|
|
207
|
-
/usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
|
|
208
219
|
ccdx pms status
|
|
209
220
|
```
|
|
210
221
|
|
|
211
222
|
The App-patch row from `ccdx pms status` must classify the restored bundle as
|
|
212
223
|
the recorded clean local content rather than integrity drift. If the version
|
|
213
|
-
check, copy, or
|
|
224
|
+
check, copy, or content verification fails,
|
|
214
225
|
do not launch the restored App. Keep the moved patched bundle at `$PATCHED_APP`
|
|
215
226
|
and reinstall PM Studio instead of applying a version-mismatched
|
|
216
|
-
backup. CCDX never
|
|
227
|
+
backup. CCDX never uses `sudo` automatically.
|
|
217
228
|
|
|
218
229
|
To change the model used by Codex Auto-review, run:
|
|
219
230
|
|
|
@@ -367,6 +378,8 @@ Environment variables:
|
|
|
367
378
|
| `CCDX_MAX_BODY_BYTES` | `67108864` | Maximum compressed/raw request body size |
|
|
368
379
|
| `CCDX_MAX_DECODED_BODY_BYTES` | `134217728` | Maximum decoded request body size after decompression |
|
|
369
380
|
| `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Per-pool byte budget for raw bodies, decoded bodies, and materialized response history; a larger item runs exclusively in its pool |
|
|
381
|
+
| `CCDX_MAX_UPSTREAM_CHAT_RESPONSE_BYTES` | `67108864` | Maximum buffered successful Chat Completions response used by Responses, Anthropic, and PM Studio conversion paths |
|
|
382
|
+
| `CCDX_MAX_UPSTREAM_RESPONSES_RESPONSE_BYTES` | `268435456` | Maximum buffered successful native Responses or compact response; streaming Responses are forwarded with bounded backpressure instead |
|
|
370
383
|
| `CCDX_MAX_QUEUED_REQUESTS` | `16` | Maximum body requests waiting for the shared byte budget |
|
|
371
384
|
| `CCDX_REQUEST_QUEUE_TIMEOUT_MS` | `120000` | Maximum wait for request-body admission before returning `503` |
|
|
372
385
|
| `CCDX_REQUEST_BODY_TIMEOUT_MS` | `120000` | Maximum time to receive and decode one request body before returning `408` |
|
package/bin/cli.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
|
|
|
17
17
|
import { runInBackground } from "../src/startup.mjs";
|
|
18
18
|
import { cliCommandName, cliHelp, parseAdapterProbeOptions, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
|
|
19
19
|
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
20
|
+
import { formatCliError } from "../src/cli-error.mjs";
|
|
20
21
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
21
22
|
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
22
23
|
import { autoReviewModelPreference } from "../src/user-settings.mjs";
|
|
@@ -143,11 +144,16 @@ if (CLI.command === "pms") {
|
|
|
143
144
|
});
|
|
144
145
|
process.exit(result.ok ? 0 : 1);
|
|
145
146
|
}
|
|
147
|
+
if (CLI.action === "restore") {
|
|
148
|
+
const { runPmStudioRestore } = await import("../src/pm-studio-setup.mjs");
|
|
149
|
+
await runPmStudioRestore({ commandName: CLI_NAME });
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
146
152
|
const { runPmStudioSetup } = await import("../src/pm-studio-setup.mjs");
|
|
147
153
|
await runPmStudioSetup({ commandName: CLI_NAME });
|
|
148
154
|
process.exit(0);
|
|
149
155
|
} catch (e) {
|
|
150
|
-
console.error(
|
|
156
|
+
console.error(formatCliError(e));
|
|
151
157
|
process.exit(1);
|
|
152
158
|
}
|
|
153
159
|
}
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { createPmStudioRelayHandler, PM_STUDIO_RELAY_PREFIX } from "./pm-studio-
|
|
|
15
15
|
import { createStreamPerformanceMetrics } from "./stream-performance.mjs";
|
|
16
16
|
import { status } from "./status.mjs";
|
|
17
17
|
import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
|
|
18
|
+
import { createTokenCounterService } from "./token-counter-service.mjs";
|
|
18
19
|
import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
19
20
|
import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
|
|
20
21
|
import { createResponsesImagePressureController } from "./responses-image-pressure.mjs";
|
|
@@ -145,6 +146,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
145
146
|
const requestMetrics = options.requestMetrics || createRequestMetrics();
|
|
146
147
|
const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
|
|
147
148
|
const imagePressure = options.imagePressure || createResponsesImagePressureController();
|
|
149
|
+
const ownsTokenCounterService = !options.tokenCounterService;
|
|
150
|
+
const tokenCounterService = options.tokenCounterService || createTokenCounterService({
|
|
151
|
+
timeoutMs: options.countTokensTimeoutMs,
|
|
152
|
+
maxQueued: options.countTokensMaxQueued,
|
|
153
|
+
});
|
|
148
154
|
const claudeDesktopModelOptions = () => {
|
|
149
155
|
const modelDefs = claudeModelRegistry?.modelDefs || options.claudeDesktopModelDefs;
|
|
150
156
|
return Array.isArray(modelDefs) ? { modelDefs } : {};
|
|
@@ -155,6 +161,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
155
161
|
chatCompletionsFn: codexChatCompletionsFn,
|
|
156
162
|
getCachedModelEndpointsFn,
|
|
157
163
|
imagePressure,
|
|
164
|
+
modelRegistry: codexModelRegistry,
|
|
158
165
|
now: options.now,
|
|
159
166
|
openAIModelEnv,
|
|
160
167
|
responsesPayloadOptions: options.responsesPayloadOptions,
|
|
@@ -168,6 +175,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
168
175
|
acquireRequest,
|
|
169
176
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
170
177
|
imagePressure,
|
|
178
|
+
modelRegistry: codexModelRegistry,
|
|
171
179
|
now: options.now,
|
|
172
180
|
openAIModelEnv,
|
|
173
181
|
requestBodyTimeoutMs,
|
|
@@ -176,7 +184,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
176
184
|
streamIdleTimeoutMs,
|
|
177
185
|
upstreamTimeoutMs,
|
|
178
186
|
});
|
|
179
|
-
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({
|
|
187
|
+
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({
|
|
188
|
+
acquireRequest,
|
|
189
|
+
requestBodyTimeoutMs,
|
|
190
|
+
countTokensFn: (body, countOptions) => tokenCounterService.count(body, countOptions),
|
|
191
|
+
});
|
|
180
192
|
const anthropicMessagesHandler = createAnthropicMessagesHandler({
|
|
181
193
|
acquireRequest,
|
|
182
194
|
chatCompletionsFn: claudeChatCompletionsFn,
|
|
@@ -294,8 +306,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
294
306
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
295
307
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
296
308
|
};
|
|
309
|
+
const dispatchRequest = typeof options.dispatchRequestForTests === "function"
|
|
310
|
+
? options.dispatchRequestForTests
|
|
311
|
+
: dispatch;
|
|
297
312
|
|
|
298
|
-
|
|
313
|
+
const handler = (req, res) => {
|
|
299
314
|
const requestId = createRequestId();
|
|
300
315
|
if (typeof res.setHeader === "function") {
|
|
301
316
|
res.setHeader("X-Request-Id", requestId);
|
|
@@ -326,10 +341,28 @@ export function createAdapterHandler(options = {}) {
|
|
|
326
341
|
const finishRequest = ({ statusCode = 0, aborted = false } = {}) => {
|
|
327
342
|
if (requestFinished) return;
|
|
328
343
|
requestFinished = true;
|
|
329
|
-
complete({ statusCode, aborted });
|
|
330
|
-
streamPerformance?.finish({ failed: aborted || statusCode >= 400 });
|
|
344
|
+
try { complete({ statusCode, aborted }); } catch {}
|
|
345
|
+
try { streamPerformance?.finish({ failed: aborted || statusCode >= 400 }); } catch {}
|
|
331
346
|
try { finishTerminalActivity(); } catch {}
|
|
332
347
|
};
|
|
348
|
+
const containUnexpectedError = (error) => {
|
|
349
|
+
try { finishRequest({ statusCode: res.statusCode >= 400 ? res.statusCode : 500 }); } catch {}
|
|
350
|
+
const safeCause = error instanceof Error ? error : new Error("Unexpected adapter failure");
|
|
351
|
+
try { logRequestFailure("Adapter", safeCause); } catch {}
|
|
352
|
+
try {
|
|
353
|
+
const safeError = httpError("Internal adapter error", 500);
|
|
354
|
+
safeError.jsonBody = {
|
|
355
|
+
error: {
|
|
356
|
+
message: safeError.message,
|
|
357
|
+
type: "server_error",
|
|
358
|
+
code: "ccdx_internal_error",
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
sendJsonError(res, safeError, 500);
|
|
362
|
+
} catch {
|
|
363
|
+
try { res.destroy?.(); } catch {}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
333
366
|
if (trackRequest && typeof res.once === "function") {
|
|
334
367
|
res.once("finish", () => finishRequest({ statusCode: res.statusCode }));
|
|
335
368
|
res.once("close", () => finishRequest({
|
|
@@ -344,19 +377,21 @@ export function createAdapterHandler(options = {}) {
|
|
|
344
377
|
pathname,
|
|
345
378
|
showRequestId: options.showRequestId === true,
|
|
346
379
|
streamPerformance,
|
|
347
|
-
}, () =>
|
|
380
|
+
}, () => dispatchRequest(req, res, pathname));
|
|
348
381
|
if (result && typeof result.then === "function") {
|
|
349
382
|
return result.catch((error) => {
|
|
350
|
-
|
|
351
|
-
throw error;
|
|
383
|
+
containUnexpectedError(error);
|
|
352
384
|
});
|
|
353
385
|
}
|
|
354
386
|
return result;
|
|
355
387
|
} catch (error) {
|
|
356
|
-
|
|
357
|
-
throw error;
|
|
388
|
+
containUnexpectedError(error);
|
|
358
389
|
}
|
|
359
390
|
};
|
|
391
|
+
handler.cleanup = () => {
|
|
392
|
+
if (ownsTokenCounterService) tokenCounterService.close();
|
|
393
|
+
};
|
|
394
|
+
return handler;
|
|
360
395
|
}
|
|
361
396
|
|
|
362
397
|
// Public server entry point.
|
|
@@ -364,9 +399,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
364
399
|
export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
|
|
365
400
|
const ownsTerminalActivity = !Object.hasOwn(options, "terminalActivity");
|
|
366
401
|
const terminalActivity = ownsTerminalActivity ? createTerminalActivityIndicator() : options.terminalActivity;
|
|
367
|
-
const
|
|
402
|
+
const handler = createAdapterHandler({ ...options, terminalActivity });
|
|
403
|
+
const server = http.createServer(handler);
|
|
368
404
|
const cleanupTerminalActivity = () => {
|
|
369
405
|
if (ownsTerminalActivity) terminalActivity?.cleanup?.();
|
|
406
|
+
handler.cleanup?.();
|
|
370
407
|
};
|
|
371
408
|
server.once("close", cleanupTerminalActivity);
|
|
372
409
|
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
createRequestAbort,
|
|
9
9
|
logRequestFailure,
|
|
10
|
+
MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
|
|
11
|
+
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
10
12
|
readJsonBody,
|
|
13
|
+
readBoundedResponseText,
|
|
11
14
|
sendJsonError,
|
|
12
15
|
sendUpstreamError,
|
|
13
16
|
writeOrDrain,
|
|
@@ -21,6 +24,7 @@ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
|
21
24
|
import { recordAnthropicUsage } from "./usage.mjs";
|
|
22
25
|
|
|
23
26
|
const PROMPT_TOO_LONG_MESSAGE = "prompt is too long: your prompt is too long. Please reduce the number of messages or use a model with a larger context window.";
|
|
27
|
+
const DOWNSTREAM_CLOSED = Symbol("downstream-closed");
|
|
24
28
|
|
|
25
29
|
function contextWindowErrorBody(text) {
|
|
26
30
|
let error;
|
|
@@ -57,7 +61,7 @@ function sendAnthropicUpstreamError(res, response, text) {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
export function createAnthropicCountTokensHandler(options) {
|
|
60
|
-
const { acquireRequest, requestBodyTimeoutMs } = options;
|
|
64
|
+
const { acquireRequest, requestBodyTimeoutMs, countTokensFn = countTokens } = options;
|
|
61
65
|
|
|
62
66
|
return async function handleAnthropicCountTokens(req, res) {
|
|
63
67
|
const abort = createRequestAbort(req, res);
|
|
@@ -67,12 +71,12 @@ export function createAnthropicCountTokensHandler(options) {
|
|
|
67
71
|
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
68
72
|
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
69
73
|
abort.clearTimeout();
|
|
70
|
-
const result = await
|
|
74
|
+
const result = await countTokensFn(parsed, { signal: abort.signal });
|
|
71
75
|
releaseRequest();
|
|
72
76
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
73
77
|
res.end(JSON.stringify(result));
|
|
74
78
|
} catch (error) {
|
|
75
|
-
sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 :
|
|
79
|
+
sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 : 500);
|
|
76
80
|
} finally {
|
|
77
81
|
releaseRequest();
|
|
78
82
|
abort.cleanup();
|
|
@@ -112,7 +116,10 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
112
116
|
const upstream = await chatCompletionsFn(withChatStreamUsage(chatReq), { signal: abort.signal });
|
|
113
117
|
releaseRequest();
|
|
114
118
|
if (!upstream.ok) {
|
|
115
|
-
sendAnthropicUpstreamError(res, upstream, await upstream
|
|
119
|
+
sendAnthropicUpstreamError(res, upstream, await readBoundedResponseText(upstream, {
|
|
120
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
121
|
+
label: "Copilot Chat error body",
|
|
122
|
+
}));
|
|
116
123
|
return;
|
|
117
124
|
}
|
|
118
125
|
await requireUpstreamEventStream(upstream);
|
|
@@ -132,12 +139,15 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
132
139
|
async (event, data) => {
|
|
133
140
|
if (event === "message_start") messageId = data.message?.id;
|
|
134
141
|
if (event === "message_delta") usage = data.usage;
|
|
135
|
-
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
142
|
+
if (!await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)) {
|
|
143
|
+
throw DOWNSTREAM_CLOSED;
|
|
144
|
+
}
|
|
136
145
|
},
|
|
137
146
|
requestedModel,
|
|
138
147
|
{ forceModel: forceRequestedModel },
|
|
139
148
|
);
|
|
140
149
|
} catch (error) {
|
|
150
|
+
if (error === DOWNSTREAM_CLOSED) return;
|
|
141
151
|
logRequestFailure("Messages", error, abort);
|
|
142
152
|
await endStreamWithError(res, "anthropic", error, abort);
|
|
143
153
|
return;
|
|
@@ -147,7 +157,16 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
147
157
|
} else {
|
|
148
158
|
const upstream = await chatCompletionsFn({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
149
159
|
releaseRequest();
|
|
150
|
-
const data =
|
|
160
|
+
const data = upstream.ok
|
|
161
|
+
? await readBoundedResponseText(upstream, {
|
|
162
|
+
maxBytes: MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
|
|
163
|
+
label: "Copilot Chat success body",
|
|
164
|
+
signal: abort.signal,
|
|
165
|
+
})
|
|
166
|
+
: await readBoundedResponseText(upstream, {
|
|
167
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
168
|
+
label: "Copilot Chat error body",
|
|
169
|
+
});
|
|
151
170
|
if (!upstream.ok) {
|
|
152
171
|
sendAnthropicUpstreamError(res, upstream, data);
|
|
153
172
|
return;
|
package/src/anthropic.mjs
CHANGED
|
@@ -163,6 +163,10 @@ export function anthropicToChat(body, options = {}) {
|
|
|
163
163
|
|
|
164
164
|
function uid() { return randomUUID().replace(/-/g, ""); }
|
|
165
165
|
|
|
166
|
+
function nonNegativeTokenCount(value) {
|
|
167
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
166
170
|
function invalidToolArguments(toolName) {
|
|
167
171
|
const name = String(toolName || "unknown").slice(0, 100);
|
|
168
172
|
return invalidUpstreamStream(
|
|
@@ -198,10 +202,11 @@ export function chatToAnthropic(openaiResp, model, options = {}) {
|
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
const u = openaiResp.usage || {};
|
|
201
|
-
const
|
|
205
|
+
const prompt = nonNegativeTokenCount(u.prompt_tokens);
|
|
206
|
+
const cached = Math.min(prompt, nonNegativeTokenCount(u.prompt_tokens_details?.cached_tokens));
|
|
202
207
|
const usage = {
|
|
203
|
-
input_tokens:
|
|
204
|
-
output_tokens: u.completion_tokens
|
|
208
|
+
input_tokens: prompt - cached,
|
|
209
|
+
output_tokens: nonNegativeTokenCount(u.completion_tokens),
|
|
205
210
|
};
|
|
206
211
|
if (cached > 0) usage.cache_read_input_tokens = cached;
|
|
207
212
|
|
|
@@ -281,7 +286,7 @@ export async function streamAnthropicFromLines(lineIterator, emit, model, option
|
|
|
281
286
|
if (upstreamError) throw upstreamError;
|
|
282
287
|
if (!options.forceModel && parsed.model) actualModel = parsed.model;
|
|
283
288
|
if (parsed.usage?.completion_tokens != null) {
|
|
284
|
-
outputTokens = parsed.usage.completion_tokens;
|
|
289
|
+
outputTokens = nonNegativeTokenCount(parsed.usage.completion_tokens);
|
|
285
290
|
markOutputTokens(outputTokens);
|
|
286
291
|
}
|
|
287
292
|
const choice = parsed.choices?.[0];
|
|
@@ -345,8 +350,7 @@ export async function streamAnthropicFromLines(lineIterator, emit, model, option
|
|
|
345
350
|
await emit("message_stop", { type: "message_stop" });
|
|
346
351
|
}
|
|
347
352
|
|
|
348
|
-
|
|
349
|
-
export async function countTokens(body) {
|
|
353
|
+
export function anthropicTokenText(body) {
|
|
350
354
|
const parts = [];
|
|
351
355
|
const sys = systemToText(body.system);
|
|
352
356
|
if (sys) parts.push(sys);
|
|
@@ -369,7 +373,15 @@ export async function countTokens(body) {
|
|
|
369
373
|
}
|
|
370
374
|
}
|
|
371
375
|
|
|
372
|
-
|
|
376
|
+
return parts.join("\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function countTokenText(text) {
|
|
373
380
|
const count = await tokenCounter();
|
|
374
381
|
return { input_tokens: count(text) };
|
|
375
382
|
}
|
|
383
|
+
|
|
384
|
+
// Copilot has no count_tokens endpoint; estimate locally with gpt-tokenizer.
|
|
385
|
+
export async function countTokens(body) {
|
|
386
|
+
return countTokenText(anthropicTokenText(body));
|
|
387
|
+
}
|
package/src/atomic-file.mjs
CHANGED
|
@@ -24,9 +24,12 @@ function syncDirectory(directory) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function atomicWriteFileSync(filePath, data, {
|
|
27
|
+
export function atomicWriteFileSync(filePath, data, {
|
|
28
|
+
mode = 0o600,
|
|
29
|
+
preserveMode = true,
|
|
30
|
+
} = {}) {
|
|
28
31
|
const directory = path.dirname(filePath);
|
|
29
|
-
const finalMode = existingMode(filePath, mode);
|
|
32
|
+
const finalMode = preserveMode ? existingMode(filePath, mode) : mode;
|
|
30
33
|
const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
31
34
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
32
35
|
|
|
@@ -50,6 +53,77 @@ export function atomicWriteFileSync(filePath, data, { mode = 0o600 } = {}) {
|
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
function fileSnapshot(filePath) {
|
|
57
|
+
try {
|
|
58
|
+
return {
|
|
59
|
+
exists: true,
|
|
60
|
+
data: fs.readFileSync(filePath),
|
|
61
|
+
mode: fs.statSync(filePath).mode & 0o777,
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error?.code === "ENOENT") return { exists: false, data: null, mode: 0o600 };
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function removeFile(filePath, unlinkFile) {
|
|
70
|
+
try {
|
|
71
|
+
unlinkFile(filePath);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error?.code !== "ENOENT") throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeExactMode(filePath, data, mode, writeFile) {
|
|
78
|
+
writeFile(filePath, data, { mode, preserveMode: false });
|
|
79
|
+
fs.chmodSync(filePath, mode);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function restoreFile(filePath, snapshot, { writeFile, unlinkFile }) {
|
|
83
|
+
if (snapshot.exists) {
|
|
84
|
+
writeExactMode(filePath, snapshot.data, snapshot.mode, writeFile);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
removeFile(filePath, unlinkFile);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Commit a primary file first and its metadata/activation marker last. A null
|
|
91
|
+
// secondary value removes stale metadata as the final transaction step.
|
|
92
|
+
export function atomicWriteFilePairSync(
|
|
93
|
+
primaryPath,
|
|
94
|
+
primaryData,
|
|
95
|
+
secondaryPath,
|
|
96
|
+
secondaryData,
|
|
97
|
+
{
|
|
98
|
+
mode = 0o600,
|
|
99
|
+
writeFile = atomicWriteFileSync,
|
|
100
|
+
unlinkFile = fs.unlinkSync,
|
|
101
|
+
} = {},
|
|
102
|
+
) {
|
|
103
|
+
const primarySnapshot = fileSnapshot(primaryPath);
|
|
104
|
+
const secondarySnapshot = fileSnapshot(secondaryPath);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
writeExactMode(primaryPath, primaryData, mode, writeFile);
|
|
108
|
+
if (secondaryData === null) removeFile(secondaryPath, unlinkFile);
|
|
109
|
+
else writeExactMode(secondaryPath, secondaryData, mode, writeFile);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
let rollbackError;
|
|
112
|
+
try {
|
|
113
|
+
restoreFile(primaryPath, primarySnapshot, { writeFile, unlinkFile });
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
rollbackError = cause;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
restoreFile(secondaryPath, secondarySnapshot, { writeFile, unlinkFile });
|
|
119
|
+
} catch (cause) {
|
|
120
|
+
rollbackError ||= cause;
|
|
121
|
+
}
|
|
122
|
+
if (rollbackError) error.rollbackError = rollbackError;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
53
127
|
export function atomicWriteFileIfChangedSync(filePath, data, options = {}) {
|
|
54
128
|
try {
|
|
55
129
|
const current = fs.readFileSync(filePath);
|
package/src/auth-profile.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { atomicWriteFileSync } from "./atomic-file.mjs";
|
|
4
|
+
import { atomicWriteFilePairSync, atomicWriteFileSync } from "./atomic-file.mjs";
|
|
5
5
|
import {
|
|
6
6
|
githubTokenLockPath,
|
|
7
7
|
githubTokenMetadataPath,
|
|
@@ -131,27 +131,11 @@ export function withAuthProfileLock(profile, fn, {
|
|
|
131
131
|
return withFileLock(lockPath, fn, { ...lockOptions(env), ...options });
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
function writeSecret(filePath, data, writeFile) {
|
|
135
|
-
writeFile(filePath, data, { mode: 0o600 });
|
|
136
|
-
fs.chmodSync(filePath, 0o600);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function restoreSnapshot(filePath, snapshot, writeFile) {
|
|
140
|
-
if (snapshot.exists) {
|
|
141
|
-
writeSecret(filePath, snapshot.data, writeFile);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
try {
|
|
145
|
-
fs.unlinkSync(filePath);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
if (error?.code !== "ENOENT") throw error;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
134
|
export function writeClaudeAuthProfile(token, identity, {
|
|
152
135
|
home = os.homedir(),
|
|
153
136
|
now = () => new Date(),
|
|
154
137
|
writeFile = atomicWriteFileSync,
|
|
138
|
+
unlinkFile = fs.unlinkSync,
|
|
155
139
|
} = {}) {
|
|
156
140
|
const cleanToken = String(token || "").trim();
|
|
157
141
|
const normalizedIdentity = normalizeGithubIdentity(identity);
|
|
@@ -163,8 +147,6 @@ export function writeClaudeAuthProfile(token, identity, {
|
|
|
163
147
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
164
148
|
fs.chmodSync(directory, 0o700);
|
|
165
149
|
|
|
166
|
-
const previousToken = readOptionalFile(paths.tokenPath);
|
|
167
|
-
const previousMetadata = readOptionalFile(paths.metadataPath);
|
|
168
150
|
const timestamp = now();
|
|
169
151
|
const updatedAt = timestamp instanceof Date ? timestamp.toISOString() : new Date(timestamp).toISOString();
|
|
170
152
|
const metadata = `${JSON.stringify({
|
|
@@ -174,20 +156,15 @@ export function writeClaudeAuthProfile(token, identity, {
|
|
|
174
156
|
updated_at: updatedAt,
|
|
175
157
|
}, null, 2)}\n`;
|
|
176
158
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
} catch (rollbackError) {
|
|
187
|
-
error.rollbackError = rollbackError;
|
|
188
|
-
}
|
|
189
|
-
throw error;
|
|
190
|
-
}
|
|
159
|
+
// Commit the activation marker last. Readers never observe a configured
|
|
160
|
+
// Claude profile unless the token and its fingerprint agree.
|
|
161
|
+
atomicWriteFilePairSync(
|
|
162
|
+
paths.tokenPath,
|
|
163
|
+
cleanToken,
|
|
164
|
+
paths.metadataPath,
|
|
165
|
+
metadata,
|
|
166
|
+
{ mode: 0o600, writeFile, unlinkFile },
|
|
167
|
+
);
|
|
191
168
|
|
|
192
169
|
return {
|
|
193
170
|
profile: AUTH_PROFILE_CLAUDE,
|