codex-copilot-dx 0.6.14 → 0.6.18
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 +30 -16
- package/bin/cli.mjs +5 -0
- package/package.json +1 -1
- package/src/adapter.mjs +34 -9
- package/src/anthropic-handler.mjs +54 -4
- package/src/anthropic.mjs +64 -26
- 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 +28 -71
- package/src/cli-options.mjs +8 -5
- package/src/copilot-client.mjs +5 -1
- package/src/copilot-responses-compat.mjs +80 -0
- package/src/copilot-responses-policy.mjs +78 -0
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +91 -0
- package/src/lock.mjs +179 -17
- package/src/model-cache.mjs +36 -6
- package/src/models.mjs +17 -0
- package/src/observability.mjs +13 -1
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-relay.mjs +6 -3
- package/src/pm-studio-setup.mjs +319 -14
- package/src/pm-studio-status.mjs +5 -1
- package/src/profile-model-runtime.mjs +87 -13
- package/src/profile-routing.mjs +45 -7
- package/src/response-history.mjs +142 -16
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +154 -39
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +10 -2
- package/src/responses-request.mjs +109 -103
- package/src/route-plan.mjs +51 -0
- package/src/startup.mjs +8 -2
- package/src/usage.mjs +25 -7
package/README.md
CHANGED
|
@@ -91,7 +91,10 @@ Once enabled, routing is fixed by API surface rather than by model name:
|
|
|
91
91
|
|
|
92
92
|
The two profiles keep separate service tokens, API bases, refresh/backoff state,
|
|
93
93
|
model endpoint metadata, and credential-bound model caches. Switching either
|
|
94
|
-
GitHub account invalidates that profile's old cache instead of borrowing it.
|
|
94
|
+
GitHub account invalidates that profile's old cache instead of borrowing it.
|
|
95
|
+
Cached catalogs are fresh for two hours; older catalogs remain immediately
|
|
96
|
+
available for up to seven days while CCDX refreshes them in the background.
|
|
97
|
+
If the isolated Claude profile later
|
|
95
98
|
becomes invalid, Claude requests fail with a reauthentication instruction while
|
|
96
99
|
Codex remains available; CCDX never silently falls back to the enterprise
|
|
97
100
|
account. Reauthorize it explicitly with:
|
|
@@ -140,7 +143,8 @@ integrity, isolated Claude profile, and live relay routing without changing file
|
|
|
140
143
|
ccdx pms status
|
|
141
144
|
```
|
|
142
145
|
|
|
143
|
-
`ccdx pm-studio status
|
|
146
|
+
`ccdx pm-studio status`, `ccdx pm-studio setup`, and `ccdx pm-studio restore`
|
|
147
|
+
are equivalent long-form aliases.
|
|
144
148
|
|
|
145
149
|
`ccdx pms setup` performs local preflight, structural verification, backup,
|
|
146
150
|
staging, integrity, signing, and replacement work. It does not access a PM
|
|
@@ -180,37 +184,47 @@ require reinstalling PM Studio. Setup prints the verified backup and
|
|
|
180
184
|
manifest paths plus the version-matched restore procedure. Never restore a
|
|
181
185
|
backup over a different PM Studio version or build.
|
|
182
186
|
|
|
183
|
-
To restore, first quit PM Studio and its updater
|
|
184
|
-
|
|
185
|
-
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:
|
|
186
189
|
|
|
187
190
|
```bash
|
|
188
|
-
|
|
189
|
-
/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
|
|
190
|
-
/usr/bin/plutil -extract CFBundleVersion raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
|
|
191
|
-
/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
192
|
-
/usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
191
|
+
ccdx pms restore
|
|
193
192
|
```
|
|
194
193
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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:
|
|
198
212
|
|
|
199
213
|
```bash
|
|
214
|
+
BACKUP_APP="<exact-verified-backup-path>"
|
|
200
215
|
RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
|
|
201
216
|
PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
|
|
202
217
|
mv "/Applications/PM Studio.app" "$PATCHED_APP"
|
|
203
218
|
/usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
|
|
204
|
-
/usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
|
|
205
219
|
ccdx pms status
|
|
206
220
|
```
|
|
207
221
|
|
|
208
222
|
The App-patch row from `ccdx pms status` must classify the restored bundle as
|
|
209
223
|
the recorded clean local content rather than integrity drift. If the version
|
|
210
|
-
check, copy, or
|
|
224
|
+
check, copy, or content verification fails,
|
|
211
225
|
do not launch the restored App. Keep the moved patched bundle at `$PATCHED_APP`
|
|
212
226
|
and reinstall PM Studio instead of applying a version-mismatched
|
|
213
|
-
backup. CCDX never
|
|
227
|
+
backup. CCDX never uses `sudo` automatically.
|
|
214
228
|
|
|
215
229
|
To change the model used by Codex Auto-review, run:
|
|
216
230
|
|
package/bin/cli.mjs
CHANGED
|
@@ -143,6 +143,11 @@ if (CLI.command === "pms") {
|
|
|
143
143
|
});
|
|
144
144
|
process.exit(result.ok ? 0 : 1);
|
|
145
145
|
}
|
|
146
|
+
if (CLI.action === "restore") {
|
|
147
|
+
const { runPmStudioRestore } = await import("../src/pm-studio-setup.mjs");
|
|
148
|
+
await runPmStudioRestore({ commandName: CLI_NAME });
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
146
151
|
const { runPmStudioSetup } = await import("../src/pm-studio-setup.mjs");
|
|
147
152
|
await runPmStudioSetup({ commandName: CLI_NAME });
|
|
148
153
|
process.exit(0);
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -41,14 +41,18 @@ export {
|
|
|
41
41
|
responseHistoryStats,
|
|
42
42
|
} from "./response-history.mjs";
|
|
43
43
|
export { forwardToChat, responsesToChat } from "./responses-bridge.mjs";
|
|
44
|
+
export {
|
|
45
|
+
openCopilotResponse,
|
|
46
|
+
} from "./copilot-responses-compat.mjs";
|
|
44
47
|
export {
|
|
45
48
|
isEncryptedContentVerificationError,
|
|
46
49
|
isImageNamespaceCollisionError,
|
|
47
|
-
|
|
50
|
+
sanitizeImageNamespaceCollisionRequest,
|
|
51
|
+
} from "./copilot-responses-policy.mjs";
|
|
52
|
+
export {
|
|
48
53
|
prepareResponsesRequest,
|
|
49
54
|
rememberResponseHistory,
|
|
50
55
|
sanitizeEncryptedReasoningRequest,
|
|
51
|
-
sanitizeImageNamespaceCollisionRequest,
|
|
52
56
|
stripInternalResponsesInputFields,
|
|
53
57
|
} from "./responses-request.mjs";
|
|
54
58
|
|
|
@@ -151,6 +155,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
151
155
|
chatCompletionsFn: codexChatCompletionsFn,
|
|
152
156
|
getCachedModelEndpointsFn,
|
|
153
157
|
imagePressure,
|
|
158
|
+
modelRegistry: codexModelRegistry,
|
|
154
159
|
now: options.now,
|
|
155
160
|
openAIModelEnv,
|
|
156
161
|
responsesPayloadOptions: options.responsesPayloadOptions,
|
|
@@ -164,6 +169,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
164
169
|
acquireRequest,
|
|
165
170
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
166
171
|
imagePressure,
|
|
172
|
+
modelRegistry: codexModelRegistry,
|
|
167
173
|
now: options.now,
|
|
168
174
|
openAIModelEnv,
|
|
169
175
|
requestBodyTimeoutMs,
|
|
@@ -290,6 +296,9 @@ export function createAdapterHandler(options = {}) {
|
|
|
290
296
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
291
297
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
292
298
|
};
|
|
299
|
+
const dispatchRequest = typeof options.dispatchRequestForTests === "function"
|
|
300
|
+
? options.dispatchRequestForTests
|
|
301
|
+
: dispatch;
|
|
293
302
|
|
|
294
303
|
return (req, res) => {
|
|
295
304
|
const requestId = createRequestId();
|
|
@@ -322,10 +331,28 @@ export function createAdapterHandler(options = {}) {
|
|
|
322
331
|
const finishRequest = ({ statusCode = 0, aborted = false } = {}) => {
|
|
323
332
|
if (requestFinished) return;
|
|
324
333
|
requestFinished = true;
|
|
325
|
-
complete({ statusCode, aborted });
|
|
326
|
-
streamPerformance?.finish({ failed: aborted || statusCode >= 400 });
|
|
334
|
+
try { complete({ statusCode, aborted }); } catch {}
|
|
335
|
+
try { streamPerformance?.finish({ failed: aborted || statusCode >= 400 }); } catch {}
|
|
327
336
|
try { finishTerminalActivity(); } catch {}
|
|
328
337
|
};
|
|
338
|
+
const containUnexpectedError = (error) => {
|
|
339
|
+
try { finishRequest({ statusCode: res.statusCode >= 400 ? res.statusCode : 500 }); } catch {}
|
|
340
|
+
const safeCause = error instanceof Error ? error : new Error("Unexpected adapter failure");
|
|
341
|
+
try { logRequestFailure("Adapter", safeCause); } catch {}
|
|
342
|
+
try {
|
|
343
|
+
const safeError = httpError("Internal adapter error", 500);
|
|
344
|
+
safeError.jsonBody = {
|
|
345
|
+
error: {
|
|
346
|
+
message: safeError.message,
|
|
347
|
+
type: "server_error",
|
|
348
|
+
code: "ccdx_internal_error",
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
sendJsonError(res, safeError, 500);
|
|
352
|
+
} catch {
|
|
353
|
+
try { res.destroy?.(); } catch {}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
329
356
|
if (trackRequest && typeof res.once === "function") {
|
|
330
357
|
res.once("finish", () => finishRequest({ statusCode: res.statusCode }));
|
|
331
358
|
res.once("close", () => finishRequest({
|
|
@@ -340,17 +367,15 @@ export function createAdapterHandler(options = {}) {
|
|
|
340
367
|
pathname,
|
|
341
368
|
showRequestId: options.showRequestId === true,
|
|
342
369
|
streamPerformance,
|
|
343
|
-
}, () =>
|
|
370
|
+
}, () => dispatchRequest(req, res, pathname));
|
|
344
371
|
if (result && typeof result.then === "function") {
|
|
345
372
|
return result.catch((error) => {
|
|
346
|
-
|
|
347
|
-
throw error;
|
|
373
|
+
containUnexpectedError(error);
|
|
348
374
|
});
|
|
349
375
|
}
|
|
350
376
|
return result;
|
|
351
377
|
} catch (error) {
|
|
352
|
-
|
|
353
|
-
throw error;
|
|
378
|
+
containUnexpectedError(error);
|
|
354
379
|
}
|
|
355
380
|
};
|
|
356
381
|
}
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
createRequestAbort,
|
|
9
9
|
logRequestFailure,
|
|
10
|
+
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
10
11
|
readJsonBody,
|
|
12
|
+
readBoundedResponseText,
|
|
11
13
|
sendJsonError,
|
|
12
14
|
sendUpstreamError,
|
|
13
15
|
writeOrDrain,
|
|
@@ -20,6 +22,43 @@ import { requireUpstreamEventStream, withChatStreamUsage } from "./stream-contra
|
|
|
20
22
|
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
21
23
|
import { recordAnthropicUsage } from "./usage.mjs";
|
|
22
24
|
|
|
25
|
+
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.";
|
|
26
|
+
const DOWNSTREAM_CLOSED = Symbol("downstream-closed");
|
|
27
|
+
|
|
28
|
+
function contextWindowErrorBody(text) {
|
|
29
|
+
let error;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(text);
|
|
32
|
+
error = parsed && typeof parsed === "object" ? parsed.error : null;
|
|
33
|
+
} catch {
|
|
34
|
+
error = { message: text };
|
|
35
|
+
}
|
|
36
|
+
if (!error || typeof error !== "object") return null;
|
|
37
|
+
const code = error.code;
|
|
38
|
+
const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
|
|
39
|
+
const matches = code === "context_length_exceeded"
|
|
40
|
+
|| code === "model_max_prompt_tokens_exceeded"
|
|
41
|
+
|| message.includes("exceeds the context window of this model")
|
|
42
|
+
|| message.includes("maximum context length is")
|
|
43
|
+
|| message.includes("request body is too large for model context window");
|
|
44
|
+
if (!matches) return null;
|
|
45
|
+
return JSON.stringify({
|
|
46
|
+
type: "error",
|
|
47
|
+
error: { type: "invalid_request_error", message: PROMPT_TOO_LONG_MESSAGE },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sendAnthropicUpstreamError(res, response, text) {
|
|
52
|
+
const rewritten = contextWindowErrorBody(text);
|
|
53
|
+
if (!rewritten) {
|
|
54
|
+
sendUpstreamError(res, response, text);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const headers = new Headers(response.headers);
|
|
58
|
+
headers.set("content-type", "application/json");
|
|
59
|
+
sendUpstreamError(res, { status: response.status, headers }, rewritten);
|
|
60
|
+
}
|
|
61
|
+
|
|
23
62
|
export function createAnthropicCountTokensHandler(options) {
|
|
24
63
|
const { acquireRequest, requestBodyTimeoutMs } = options;
|
|
25
64
|
|
|
@@ -76,7 +115,10 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
76
115
|
const upstream = await chatCompletionsFn(withChatStreamUsage(chatReq), { signal: abort.signal });
|
|
77
116
|
releaseRequest();
|
|
78
117
|
if (!upstream.ok) {
|
|
79
|
-
|
|
118
|
+
sendAnthropicUpstreamError(res, upstream, await readBoundedResponseText(upstream, {
|
|
119
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
120
|
+
label: "Copilot Chat error body",
|
|
121
|
+
}));
|
|
80
122
|
return;
|
|
81
123
|
}
|
|
82
124
|
await requireUpstreamEventStream(upstream);
|
|
@@ -96,12 +138,15 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
96
138
|
async (event, data) => {
|
|
97
139
|
if (event === "message_start") messageId = data.message?.id;
|
|
98
140
|
if (event === "message_delta") usage = data.usage;
|
|
99
|
-
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
141
|
+
if (!await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)) {
|
|
142
|
+
throw DOWNSTREAM_CLOSED;
|
|
143
|
+
}
|
|
100
144
|
},
|
|
101
145
|
requestedModel,
|
|
102
146
|
{ forceModel: forceRequestedModel },
|
|
103
147
|
);
|
|
104
148
|
} catch (error) {
|
|
149
|
+
if (error === DOWNSTREAM_CLOSED) return;
|
|
105
150
|
logRequestFailure("Messages", error, abort);
|
|
106
151
|
await endStreamWithError(res, "anthropic", error, abort);
|
|
107
152
|
return;
|
|
@@ -111,9 +156,14 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
111
156
|
} else {
|
|
112
157
|
const upstream = await chatCompletionsFn({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
113
158
|
releaseRequest();
|
|
114
|
-
const data =
|
|
159
|
+
const data = upstream.ok
|
|
160
|
+
? await upstream.text()
|
|
161
|
+
: await readBoundedResponseText(upstream, {
|
|
162
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
163
|
+
label: "Copilot Chat error body",
|
|
164
|
+
});
|
|
115
165
|
if (!upstream.ok) {
|
|
116
|
-
|
|
166
|
+
sendAnthropicUpstreamError(res, upstream, data);
|
|
117
167
|
return;
|
|
118
168
|
}
|
|
119
169
|
const anthropicMessage = chatToAnthropic(JSON.parse(data), requestedModel, { forceModel: forceRequestedModel });
|
package/src/anthropic.mjs
CHANGED
|
@@ -44,26 +44,10 @@ function toolResultContent(content) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Convert one Anthropic message into zero or more OpenAI messages.
|
|
47
|
-
function
|
|
48
|
-
const out = [];
|
|
49
|
-
const role = msg.role; // "user" | "assistant"
|
|
50
|
-
|
|
51
|
-
if (typeof msg.content === "string") {
|
|
52
|
-
out.push({ role, content: msg.content });
|
|
53
|
-
return out;
|
|
54
|
-
}
|
|
55
|
-
if (!Array.isArray(msg.content)) return out;
|
|
56
|
-
|
|
57
|
-
// tool_result blocks must become independent role:"tool" messages.
|
|
58
|
-
const toolResults = msg.content.filter((b) => b.type === "tool_result");
|
|
59
|
-
for (const tr of toolResults) {
|
|
60
|
-
out.push({ role: "tool", tool_call_id: tr.tool_use_id, content: toolResultContent(tr.content) });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Convert remaining text, image, and tool_use blocks.
|
|
47
|
+
function convertContentBlocks(role, blocks) {
|
|
64
48
|
const textImageParts = [];
|
|
65
49
|
const toolCalls = [];
|
|
66
|
-
for (const b of
|
|
50
|
+
for (const b of blocks) {
|
|
67
51
|
if (b.type === "text") {
|
|
68
52
|
textImageParts.push({ type: "text", text: b.text });
|
|
69
53
|
} else if (b.type === "image") {
|
|
@@ -80,17 +64,66 @@ function convertMessage(msg) {
|
|
|
80
64
|
const m = { role, content: null, tool_calls: toolCalls };
|
|
81
65
|
if (textImageParts.length === 1 && textImageParts[0].type === "text") m.content = textImageParts[0].text;
|
|
82
66
|
else if (textImageParts.length > 0) m.content = textImageParts;
|
|
83
|
-
|
|
67
|
+
return m;
|
|
84
68
|
} else if (textImageParts.length > 0) {
|
|
85
69
|
if (textImageParts.length === 1 && textImageParts[0].type === "text") {
|
|
86
|
-
|
|
70
|
+
return { role, content: textImageParts[0].text };
|
|
87
71
|
} else {
|
|
88
|
-
|
|
72
|
+
return { role, content: textImageParts };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function convertMessage(msg) {
|
|
79
|
+
const out = [];
|
|
80
|
+
const role = msg.role; // "user" | "assistant"
|
|
81
|
+
|
|
82
|
+
if (typeof msg.content === "string") {
|
|
83
|
+
out.push({ role, content: msg.content });
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(msg.content)) return out;
|
|
87
|
+
|
|
88
|
+
if (!msg.content.some((block) => block.type === "tool_result")) {
|
|
89
|
+
const converted = convertContentBlocks(role, msg.content);
|
|
90
|
+
if (converted) out.push(converted);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let contentBlocks = [];
|
|
95
|
+
const flushContent = () => {
|
|
96
|
+
const converted = convertContentBlocks(role, contentBlocks);
|
|
97
|
+
if (converted) out.push(converted);
|
|
98
|
+
contentBlocks = [];
|
|
99
|
+
};
|
|
100
|
+
for (const block of msg.content) {
|
|
101
|
+
if (block.type !== "tool_result") {
|
|
102
|
+
contentBlocks.push(block);
|
|
103
|
+
continue;
|
|
89
104
|
}
|
|
105
|
+
flushContent();
|
|
106
|
+
out.push({
|
|
107
|
+
role: "tool",
|
|
108
|
+
tool_call_id: block.tool_use_id,
|
|
109
|
+
content: toolResultContent(block.content),
|
|
110
|
+
});
|
|
90
111
|
}
|
|
112
|
+
flushContent();
|
|
91
113
|
return out;
|
|
92
114
|
}
|
|
93
115
|
|
|
116
|
+
function toolInputSchema(inputSchema) {
|
|
117
|
+
if (inputSchema
|
|
118
|
+
&& typeof inputSchema === "object"
|
|
119
|
+
&& !Array.isArray(inputSchema)
|
|
120
|
+
&& inputSchema.type === "object"
|
|
121
|
+
&& Object.keys(inputSchema).length === 1) {
|
|
122
|
+
return { type: "object", properties: {} };
|
|
123
|
+
}
|
|
124
|
+
return inputSchema;
|
|
125
|
+
}
|
|
126
|
+
|
|
94
127
|
function mapToolChoice(tc) {
|
|
95
128
|
if (!tc) return undefined;
|
|
96
129
|
switch (tc.type) {
|
|
@@ -119,7 +152,7 @@ export function anthropicToChat(body, options = {}) {
|
|
|
119
152
|
if (Array.isArray(body.tools) && body.tools.length) {
|
|
120
153
|
chatReq.tools = body.tools.map((t) => ({
|
|
121
154
|
type: "function",
|
|
122
|
-
function: { name: t.name, description: t.description, parameters: t.input_schema },
|
|
155
|
+
function: { name: t.name, description: t.description, parameters: toolInputSchema(t.input_schema) },
|
|
123
156
|
}));
|
|
124
157
|
}
|
|
125
158
|
const tc = mapToolChoice(body.tool_choice);
|
|
@@ -130,6 +163,10 @@ export function anthropicToChat(body, options = {}) {
|
|
|
130
163
|
|
|
131
164
|
function uid() { return randomUUID().replace(/-/g, ""); }
|
|
132
165
|
|
|
166
|
+
function nonNegativeTokenCount(value) {
|
|
167
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
133
170
|
function invalidToolArguments(toolName) {
|
|
134
171
|
const name = String(toolName || "unknown").slice(0, 100);
|
|
135
172
|
return invalidUpstreamStream(
|
|
@@ -165,10 +202,11 @@ export function chatToAnthropic(openaiResp, model, options = {}) {
|
|
|
165
202
|
}
|
|
166
203
|
|
|
167
204
|
const u = openaiResp.usage || {};
|
|
168
|
-
const
|
|
205
|
+
const prompt = nonNegativeTokenCount(u.prompt_tokens);
|
|
206
|
+
const cached = Math.min(prompt, nonNegativeTokenCount(u.prompt_tokens_details?.cached_tokens));
|
|
169
207
|
const usage = {
|
|
170
|
-
input_tokens:
|
|
171
|
-
output_tokens: u.completion_tokens
|
|
208
|
+
input_tokens: prompt - cached,
|
|
209
|
+
output_tokens: nonNegativeTokenCount(u.completion_tokens),
|
|
172
210
|
};
|
|
173
211
|
if (cached > 0) usage.cache_read_input_tokens = cached;
|
|
174
212
|
|
|
@@ -248,7 +286,7 @@ export async function streamAnthropicFromLines(lineIterator, emit, model, option
|
|
|
248
286
|
if (upstreamError) throw upstreamError;
|
|
249
287
|
if (!options.forceModel && parsed.model) actualModel = parsed.model;
|
|
250
288
|
if (parsed.usage?.completion_tokens != null) {
|
|
251
|
-
outputTokens = parsed.usage.completion_tokens;
|
|
289
|
+
outputTokens = nonNegativeTokenCount(parsed.usage.completion_tokens);
|
|
252
290
|
markOutputTokens(outputTokens);
|
|
253
291
|
}
|
|
254
292
|
const choice = parsed.choices?.[0];
|
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,
|