codex-copilot-dx 0.6.21 → 0.7.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/README.md +123 -326
- package/bin/cli.mjs +13 -129
- package/package.json +2 -8
- package/scripts/benchmark.mjs +0 -114
- package/src/adapter.mjs +19 -106
- package/src/auth-profile.mjs +18 -130
- package/src/cli-auth.mjs +44 -398
- package/src/cli-models.mjs +21 -49
- package/src/cli-options.mjs +45 -75
- package/src/cli-status.mjs +6 -23
- package/src/config.mjs +2 -8
- package/src/copilot.mjs +2 -2
- package/src/doctor.mjs +44 -291
- package/src/model-cache.mjs +4 -11
- package/src/models.mjs +0 -184
- package/src/observability.mjs +1 -38
- package/src/profile-model-runtime.mjs +61 -189
- package/src/profile-routing.mjs +2 -102
- package/src/profile-runtime.mjs +15 -68
- package/src/responses-handler.mjs +1 -1
- package/src/responses-proxy.mjs +1 -1
- package/src/running-adapter.mjs +2 -4
- package/src/stream-errors.mjs +3 -6
- package/src/stream-performance.mjs +1 -1
- package/src/usage.mjs +0 -31
- package/src/anthropic-handler.mjs +0 -195
- package/src/anthropic.mjs +0 -387
- package/src/claude-config.mjs +0 -57
- package/src/claude-desktop-config.mjs +0 -317
- package/src/cli-error.mjs +0 -50
- package/src/pm-studio-asar.mjs +0 -1244
- package/src/pm-studio-bundle-content.mjs +0 -165
- package/src/pm-studio-relay.mjs +0 -563
- package/src/pm-studio-setup-lock.mjs +0 -336
- package/src/pm-studio-setup.mjs +0 -1581
- package/src/pm-studio-status.mjs +0 -369
- package/src/token-counter-service.mjs +0 -211
- package/src/token-counter-worker.mjs +0 -16
package/src/profile-routing.mjs
CHANGED
|
@@ -1,103 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
isClaudeCopilotModel,
|
|
4
|
-
} from "./models.mjs";
|
|
5
|
-
import { createRoutePlan } from "./route-plan.mjs";
|
|
6
|
-
|
|
7
|
-
function catalogData(catalog) {
|
|
8
|
-
if (Array.isArray(catalog)) return catalog;
|
|
9
|
-
return Array.isArray(catalog?.data) ? catalog.data : [];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function profileRouting({ claudeMode = "inherited", claudeConfigured = false } = {}) {
|
|
13
|
-
const isolated = claudeMode === "isolated" || claudeConfigured === true;
|
|
14
|
-
return Object.freeze({ responses: "codex", messages: isolated ? "claude" : "codex" });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {}) {
|
|
18
|
-
if (typeof getCatalog !== "function") throw new Error("getCatalog is required");
|
|
19
|
-
if (typeof isClaudeEnabled !== "function") throw new Error("isClaudeEnabled is required");
|
|
20
|
-
|
|
21
|
-
const unset = Symbol("unset");
|
|
22
|
-
let cachedCatalog = unset;
|
|
23
|
-
let cachedEnabled = false;
|
|
24
|
-
let cachedAllowed = new Map();
|
|
25
|
-
let cachedKnown = new Set();
|
|
26
|
-
let rebuilds = 0;
|
|
27
|
-
|
|
28
|
-
function availability() {
|
|
29
|
-
const catalog = getCatalog();
|
|
30
|
-
const enabled = isClaudeEnabled() === true;
|
|
31
|
-
if (catalog === cachedCatalog && enabled === cachedEnabled) {
|
|
32
|
-
return { allowed: cachedAllowed, known: cachedKnown };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const allowed = new Map();
|
|
36
|
-
const known = new Set();
|
|
37
|
-
for (const model of catalogData(catalog)) {
|
|
38
|
-
const id = String(model?.id || "").trim();
|
|
39
|
-
if (!id || !isClaudeCopilotCatalogEntry(model)) continue;
|
|
40
|
-
known.add(id);
|
|
41
|
-
if (enabled && isClaudeCopilotModel(model) && !allowed.has(id)) allowed.set(id, model);
|
|
42
|
-
}
|
|
43
|
-
cachedCatalog = catalog;
|
|
44
|
-
cachedEnabled = enabled;
|
|
45
|
-
cachedAllowed = allowed;
|
|
46
|
-
cachedKnown = known;
|
|
47
|
-
rebuilds += 1;
|
|
48
|
-
return { allowed, known };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function classify(modelId) {
|
|
52
|
-
const id = String(modelId || "").trim();
|
|
53
|
-
const { allowed, known } = availability();
|
|
54
|
-
if (allowed.has(id)) return "claude";
|
|
55
|
-
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
56
|
-
return "enterprise";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function plan(modelId) {
|
|
60
|
-
const id = String(modelId || "").trim();
|
|
61
|
-
const modelType = classify(id);
|
|
62
|
-
const protocol = "openai-chat-completions";
|
|
63
|
-
if (modelType === "claude") {
|
|
64
|
-
return createRoutePlan({
|
|
65
|
-
disposition: "relay",
|
|
66
|
-
origin: "ccdx",
|
|
67
|
-
profile: "claude",
|
|
68
|
-
protocol,
|
|
69
|
-
model: id,
|
|
70
|
-
surface: "pm-studio",
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
if (modelType === "unsupported_claude") {
|
|
74
|
-
return createRoutePlan({
|
|
75
|
-
disposition: "reject",
|
|
76
|
-
origin: "ccdx",
|
|
77
|
-
profile: "claude",
|
|
78
|
-
protocol,
|
|
79
|
-
model: id || "unknown",
|
|
80
|
-
surface: "pm-studio",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return createRoutePlan({
|
|
84
|
-
disposition: "native",
|
|
85
|
-
origin: "pm-studio-native",
|
|
86
|
-
profile: "enterprise",
|
|
87
|
-
protocol,
|
|
88
|
-
model: id || "unknown",
|
|
89
|
-
surface: "pm-studio",
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return Object.freeze({
|
|
94
|
-
plan,
|
|
95
|
-
classify,
|
|
96
|
-
allowedModels() {
|
|
97
|
-
return [...availability().allowed.values()];
|
|
98
|
-
},
|
|
99
|
-
diagnostics() {
|
|
100
|
-
return { rebuilds, allowed: cachedAllowed.size, known: cachedKnown.size };
|
|
101
|
-
},
|
|
102
|
-
});
|
|
1
|
+
export function profileRouting() {
|
|
2
|
+
return Object.freeze({ responses: "codex" });
|
|
103
3
|
}
|
package/src/profile-runtime.mjs
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AUTH_PROFILE_CLAUDE,
|
|
3
2
|
AUTH_PROFILE_CODEX,
|
|
4
|
-
authProfilePaths,
|
|
5
|
-
profileReauthMessage,
|
|
6
3
|
readAuthProfileCredentials,
|
|
7
4
|
} from "./auth-profile.mjs";
|
|
8
|
-
import {
|
|
5
|
+
import { defaultCopilotClient } from "./copilot.mjs";
|
|
9
6
|
import { githubTokenFingerprint } from "./github-identity.mjs";
|
|
10
7
|
|
|
11
8
|
function safeProfileStatus(credentials) {
|
|
@@ -28,75 +25,25 @@ export function createProfileRuntime({
|
|
|
28
25
|
home,
|
|
29
26
|
codexClient = defaultCopilotClient,
|
|
30
27
|
} = {}) {
|
|
31
|
-
|
|
32
|
-
profile: AUTH_PROFILE_CLAUDE,
|
|
33
|
-
configured: true,
|
|
34
|
-
valid: false,
|
|
35
|
-
reason: "credential_read_failed",
|
|
36
|
-
token: "",
|
|
37
|
-
identity: null,
|
|
38
|
-
paths: authProfilePaths(AUTH_PROFILE_CLAUDE, { home }),
|
|
39
|
-
});
|
|
40
|
-
const readCredentials = () => {
|
|
41
|
-
try {
|
|
42
|
-
return readAuthProfileCredentials(AUTH_PROFILE_CLAUDE, { home });
|
|
43
|
-
} catch {
|
|
44
|
-
return unreadableCredentials();
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const credentials = readCredentials();
|
|
48
|
-
const claudeProfile = safeProfileStatus(credentials);
|
|
49
|
-
const claudeCredentialFingerprint = credentials.valid
|
|
50
|
-
? githubTokenFingerprint(credentials.token)
|
|
51
|
-
: "";
|
|
52
|
-
const isClaudeProfileCurrent = () => {
|
|
53
|
-
if (!claudeCredentialFingerprint) return false;
|
|
54
|
-
const current = readCredentials();
|
|
55
|
-
return current.valid === true
|
|
56
|
-
&& githubTokenFingerprint(current.token) === claudeCredentialFingerprint;
|
|
57
|
-
};
|
|
58
|
-
let codexCredentialFingerprint = "";
|
|
28
|
+
let credentials;
|
|
59
29
|
try {
|
|
60
|
-
|
|
61
|
-
if (codexCredentials.valid) {
|
|
62
|
-
codexCredentialFingerprint = githubTokenFingerprint(codexCredentials.token);
|
|
63
|
-
}
|
|
30
|
+
credentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
64
31
|
} catch {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
claudeMode: "inherited",
|
|
74
|
-
claudeProfile,
|
|
75
|
-
codexCredentialFingerprint,
|
|
76
|
-
claudeCredentialFingerprint: "",
|
|
77
|
-
isClaudeProfileCurrent,
|
|
78
|
-
});
|
|
32
|
+
credentials = {
|
|
33
|
+
profile: AUTH_PROFILE_CODEX,
|
|
34
|
+
configured: true,
|
|
35
|
+
valid: false,
|
|
36
|
+
reason: "credential_read_failed",
|
|
37
|
+
token: "",
|
|
38
|
+
identity: null,
|
|
39
|
+
};
|
|
79
40
|
}
|
|
80
41
|
|
|
81
|
-
const readRuntimeCredentials = credentials.reason === "credential_read_failed"
|
|
82
|
-
? () => credentials
|
|
83
|
-
: readCredentials;
|
|
84
|
-
const claudeClient = createCopilotClient({
|
|
85
|
-
profile: AUTH_PROFILE_CLAUDE,
|
|
86
|
-
home,
|
|
87
|
-
tokenPath: credentials.paths.tokenPath,
|
|
88
|
-
allowTokenDiscovery: false,
|
|
89
|
-
readGithubCredentials: readRuntimeCredentials,
|
|
90
|
-
reauthMessage: (reason) => `${reason}\n${profileReauthMessage(AUTH_PROFILE_CLAUDE, { home })}`,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
42
|
return Object.freeze({
|
|
94
43
|
codexClient,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
claudeCredentialFingerprint,
|
|
100
|
-
isClaudeProfileCurrent,
|
|
44
|
+
codexProfile: safeProfileStatus(credentials),
|
|
45
|
+
codexCredentialFingerprint: credentials.valid
|
|
46
|
+
? githubTokenFingerprint(credentials.token)
|
|
47
|
+
: "",
|
|
101
48
|
});
|
|
102
49
|
}
|
|
@@ -286,7 +286,7 @@ export function createResponsesHandler(options) {
|
|
|
286
286
|
sendJsonError(res, responseError, statusCode || 500);
|
|
287
287
|
return;
|
|
288
288
|
}
|
|
289
|
-
await endStreamWithError(res,
|
|
289
|
+
await endStreamWithError(res, error || new Error(errMsg), abort);
|
|
290
290
|
}, {
|
|
291
291
|
signal: abort.signal,
|
|
292
292
|
abort,
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -258,7 +258,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
258
258
|
}
|
|
259
259
|
} catch (e) {
|
|
260
260
|
logRequestFailure("Responses", e, options.abort);
|
|
261
|
-
await endStreamWithError(res,
|
|
261
|
+
await endStreamWithError(res, e, options.abort);
|
|
262
262
|
return { successful: false, compacted: false };
|
|
263
263
|
} finally {
|
|
264
264
|
await reader.cancel().catch(() => {});
|
package/src/running-adapter.mjs
CHANGED
|
@@ -3,11 +3,9 @@ import { localPackageVersion } from "./version.mjs";
|
|
|
3
3
|
|
|
4
4
|
export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
|
|
5
5
|
export const ADAPTER_STATUS_PATH = "/_ccdx/status";
|
|
6
|
-
export const ADAPTER_PROTOCOL_VERSION =
|
|
6
|
+
export const ADAPTER_PROTOCOL_VERSION = 3;
|
|
7
7
|
export const ADAPTER_VERSION = localPackageVersion();
|
|
8
|
-
export const ADAPTER_CAPABILITIES = Object.freeze([
|
|
9
|
-
"pm_studio_split_origin_v1",
|
|
10
|
-
]);
|
|
8
|
+
export const ADAPTER_CAPABILITIES = Object.freeze([]);
|
|
11
9
|
const ADAPTER_INSTANCE_ID = randomUUID();
|
|
12
10
|
|
|
13
11
|
export function adapterHealthPayload() {
|
package/src/stream-errors.mjs
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { writeOrDrain } from "./http-transport.mjs";
|
|
2
2
|
import { markStreamFailure } from "./stream-performance.mjs";
|
|
3
3
|
|
|
4
|
-
function streamErrorData(
|
|
4
|
+
function streamErrorData(error, abort) {
|
|
5
5
|
const code = abort?.reason || error?.code || "upstream_stream_error";
|
|
6
6
|
const message = `${code}: ${error?.message || "Upstream stream failed"}`;
|
|
7
|
-
if (protocol === "anthropic") {
|
|
8
|
-
return { type: "error", error: { type: "api_error", message } };
|
|
9
|
-
}
|
|
10
7
|
return { type: "error", code, message, param: null };
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
export async function endStreamWithError(res,
|
|
10
|
+
export async function endStreamWithError(res, error, abort) {
|
|
14
11
|
markStreamFailure();
|
|
15
12
|
if (res.destroyed || res.writableEnded) return;
|
|
16
|
-
const data = streamErrorData(
|
|
13
|
+
const data = streamErrorData(error, abort);
|
|
17
14
|
await writeOrDrain(res, `event: error\ndata: ${JSON.stringify(data)}\n\n`).catch(() => false);
|
|
18
15
|
if (!res.destroyed && !res.writableEnded) res.end();
|
|
19
16
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { currentRequestContext } from "./request-context.mjs";
|
|
3
3
|
|
|
4
|
-
const PERFORMANCE_ROUTES = Object.freeze(["responses", "responses_compact"
|
|
4
|
+
const PERFORMANCE_ROUTES = Object.freeze(["responses", "responses_compact"]);
|
|
5
5
|
const TTFT_EDGES_MS = Object.freeze([
|
|
6
6
|
100, 200, 300, 500, 700, 1_000, 1_400, 2_000, 2_800, 4_000,
|
|
7
7
|
5_500, 8_000, 12_000, 16_000, 24_000, 36_000, 60_000, 120_000, 300_000,
|
package/src/usage.mjs
CHANGED
|
@@ -56,20 +56,6 @@ function normalizeResponsesUsage(usage) {
|
|
|
56
56
|
return hasPositiveTokenValue(out) ? out : undefined;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function normalizeAnthropicUsage(usage) {
|
|
60
|
-
if (!usage || typeof usage !== "object") return undefined;
|
|
61
|
-
const out = compactObject({
|
|
62
|
-
input_tokens: positiveNumber(usage.input_tokens),
|
|
63
|
-
cache_read_input_tokens: positiveNumber(usage.cache_read_input_tokens),
|
|
64
|
-
cache_creation_input_tokens: positiveNumber(usage.cache_creation_input_tokens),
|
|
65
|
-
output_tokens: positiveNumber(usage.output_tokens),
|
|
66
|
-
});
|
|
67
|
-
if (out.input_tokens !== undefined || out.cache_read_input_tokens !== undefined || out.cache_creation_input_tokens !== undefined || out.output_tokens !== undefined) {
|
|
68
|
-
out.total_tokens = (out.input_tokens || 0) + (out.cache_read_input_tokens || 0) + (out.cache_creation_input_tokens || 0) + (out.output_tokens || 0);
|
|
69
|
-
}
|
|
70
|
-
return hasPositiveTokenValue(out) ? out : undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
59
|
function normalizeCopilotUsage(copilotUsage) {
|
|
74
60
|
if (!copilotUsage || typeof copilotUsage !== "object") return undefined;
|
|
75
61
|
const out = {};
|
|
@@ -112,19 +98,6 @@ export function buildResponsesUsageRecord({ surface = "responses", mode, model,
|
|
|
112
98
|
});
|
|
113
99
|
}
|
|
114
100
|
|
|
115
|
-
export function buildAnthropicUsageRecord({ surface = "messages", mode, model, responseId, usage } = {}) {
|
|
116
|
-
const normalized = normalizeAnthropicUsage(usage);
|
|
117
|
-
if (!normalized) return null;
|
|
118
|
-
return compactObject({
|
|
119
|
-
ts: new Date().toISOString(),
|
|
120
|
-
surface,
|
|
121
|
-
mode,
|
|
122
|
-
model,
|
|
123
|
-
response_id: responseId,
|
|
124
|
-
usage: normalized,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
101
|
function takePendingWrites(filePath, maxBytes) {
|
|
129
102
|
const batch = [];
|
|
130
103
|
let retained = 0;
|
|
@@ -188,10 +161,6 @@ export function recordResponsesUsage(args) {
|
|
|
188
161
|
return recordUsage(buildResponsesUsageRecord(args));
|
|
189
162
|
}
|
|
190
163
|
|
|
191
|
-
export function recordAnthropicUsage(args) {
|
|
192
|
-
return recordUsage(buildAnthropicUsageRecord(args));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
164
|
export async function flushUsageWritesForTests() {
|
|
196
165
|
while (writeDrain || pendingWrites.length > 0) {
|
|
197
166
|
if (writeDrain) await writeDrain;
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
anthropicToChat,
|
|
3
|
-
chatToAnthropic,
|
|
4
|
-
countTokens,
|
|
5
|
-
streamAnthropicFromLines,
|
|
6
|
-
} from "./anthropic.mjs";
|
|
7
|
-
import {
|
|
8
|
-
createRequestAbort,
|
|
9
|
-
logRequestFailure,
|
|
10
|
-
MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
|
|
11
|
-
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
12
|
-
readJsonBody,
|
|
13
|
-
readBoundedResponseText,
|
|
14
|
-
sendJsonError,
|
|
15
|
-
sendUpstreamError,
|
|
16
|
-
writeOrDrain,
|
|
17
|
-
} from "./http-transport.mjs";
|
|
18
|
-
import { resolveAnthropicModel } from "./models.mjs";
|
|
19
|
-
import { status } from "./status.mjs";
|
|
20
|
-
import { webStreamLines } from "./stream.mjs";
|
|
21
|
-
import { endStreamWithError } from "./stream-errors.mjs";
|
|
22
|
-
import { requireUpstreamEventStream, withChatStreamUsage } from "./stream-contract.mjs";
|
|
23
|
-
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
24
|
-
import { recordAnthropicUsage } from "./usage.mjs";
|
|
25
|
-
|
|
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");
|
|
28
|
-
|
|
29
|
-
function contextWindowErrorBody(text) {
|
|
30
|
-
let error;
|
|
31
|
-
try {
|
|
32
|
-
const parsed = JSON.parse(text);
|
|
33
|
-
error = parsed && typeof parsed === "object" ? parsed.error : null;
|
|
34
|
-
} catch {
|
|
35
|
-
error = { message: text };
|
|
36
|
-
}
|
|
37
|
-
if (!error || typeof error !== "object") return null;
|
|
38
|
-
const code = error.code;
|
|
39
|
-
const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
|
|
40
|
-
const matches = code === "context_length_exceeded"
|
|
41
|
-
|| code === "model_max_prompt_tokens_exceeded"
|
|
42
|
-
|| message.includes("exceeds the context window of this model")
|
|
43
|
-
|| message.includes("maximum context length is")
|
|
44
|
-
|| message.includes("request body is too large for model context window");
|
|
45
|
-
if (!matches) return null;
|
|
46
|
-
return JSON.stringify({
|
|
47
|
-
type: "error",
|
|
48
|
-
error: { type: "invalid_request_error", message: PROMPT_TOO_LONG_MESSAGE },
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function sendAnthropicUpstreamError(res, response, text) {
|
|
53
|
-
const rewritten = contextWindowErrorBody(text);
|
|
54
|
-
if (!rewritten) {
|
|
55
|
-
sendUpstreamError(res, response, text);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const headers = new Headers(response.headers);
|
|
59
|
-
headers.set("content-type", "application/json");
|
|
60
|
-
sendUpstreamError(res, { status: response.status, headers }, rewritten);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function createAnthropicCountTokensHandler(options) {
|
|
64
|
-
const { acquireRequest, requestBodyTimeoutMs, countTokensFn = countTokens } = options;
|
|
65
|
-
|
|
66
|
-
return async function handleAnthropicCountTokens(req, res) {
|
|
67
|
-
const abort = createRequestAbort(req, res);
|
|
68
|
-
let releaseRequest = () => {};
|
|
69
|
-
try {
|
|
70
|
-
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
71
|
-
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
72
|
-
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
73
|
-
abort.clearTimeout();
|
|
74
|
-
const result = await countTokensFn(parsed, { signal: abort.signal });
|
|
75
|
-
releaseRequest();
|
|
76
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
77
|
-
res.end(JSON.stringify(result));
|
|
78
|
-
} catch (error) {
|
|
79
|
-
sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 : 500);
|
|
80
|
-
} finally {
|
|
81
|
-
releaseRequest();
|
|
82
|
-
abort.cleanup();
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function createAnthropicMessagesHandler(options) {
|
|
88
|
-
const {
|
|
89
|
-
acquireRequest,
|
|
90
|
-
chatCompletionsFn,
|
|
91
|
-
environment,
|
|
92
|
-
modelOptions,
|
|
93
|
-
requestBodyTimeoutMs,
|
|
94
|
-
streamHandshakeTimeoutMs,
|
|
95
|
-
streamIdleTimeoutMs,
|
|
96
|
-
upstreamTimeoutMs,
|
|
97
|
-
} = options;
|
|
98
|
-
|
|
99
|
-
return async function handleAnthropicMessages(req, res) {
|
|
100
|
-
const abort = createRequestAbort(req, res);
|
|
101
|
-
let releaseRequest = () => {};
|
|
102
|
-
try {
|
|
103
|
-
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
104
|
-
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
105
|
-
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
106
|
-
abort.setTimeout(
|
|
107
|
-
parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
108
|
-
parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
|
109
|
-
);
|
|
110
|
-
const { requestedModel, upstreamModel } = resolveAnthropicModel(parsed.model || "unknown", environment, modelOptions());
|
|
111
|
-
const modelNote = upstreamModel === requestedModel ? requestedModel : `${requestedModel} -> ${upstreamModel}`;
|
|
112
|
-
console.log(status("info", `messages model=${modelNote} stream=${!!parsed.stream}`));
|
|
113
|
-
const chatReq = anthropicToChat(parsed, { upstreamModel });
|
|
114
|
-
const forceRequestedModel = upstreamModel !== requestedModel;
|
|
115
|
-
if (parsed.stream) {
|
|
116
|
-
const upstream = await chatCompletionsFn(withChatStreamUsage(chatReq), { signal: abort.signal });
|
|
117
|
-
releaseRequest();
|
|
118
|
-
if (!upstream.ok) {
|
|
119
|
-
sendAnthropicUpstreamError(res, upstream, await readBoundedResponseText(upstream, {
|
|
120
|
-
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
121
|
-
label: "Copilot Chat error body",
|
|
122
|
-
}));
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
await requireUpstreamEventStream(upstream);
|
|
126
|
-
abort.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
|
|
127
|
-
res.writeHead(200, {
|
|
128
|
-
...safeUpstreamResponseHeaders(upstream.headers, { contentType: "text/event-stream" }),
|
|
129
|
-
"Cache-Control": "no-cache",
|
|
130
|
-
Connection: "keep-alive",
|
|
131
|
-
});
|
|
132
|
-
let messageId;
|
|
133
|
-
let usage;
|
|
134
|
-
try {
|
|
135
|
-
await streamAnthropicFromLines(
|
|
136
|
-
webStreamLines(upstream, {
|
|
137
|
-
onChunk: () => abort.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout"),
|
|
138
|
-
}),
|
|
139
|
-
async (event, data) => {
|
|
140
|
-
if (event === "message_start") messageId = data.message?.id;
|
|
141
|
-
if (event === "message_delta") usage = data.usage;
|
|
142
|
-
if (!await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)) {
|
|
143
|
-
throw DOWNSTREAM_CLOSED;
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
requestedModel,
|
|
147
|
-
{ forceModel: forceRequestedModel },
|
|
148
|
-
);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
if (error === DOWNSTREAM_CLOSED) return;
|
|
151
|
-
logRequestFailure("Messages", error, abort);
|
|
152
|
-
await endStreamWithError(res, "anthropic", error, abort);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
recordAnthropicUsage({ surface: "messages", mode: "stream", model: requestedModel, responseId: messageId, usage });
|
|
156
|
-
if (!res.writableEnded) res.end();
|
|
157
|
-
} else {
|
|
158
|
-
const upstream = await chatCompletionsFn({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
159
|
-
releaseRequest();
|
|
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
|
-
});
|
|
170
|
-
if (!upstream.ok) {
|
|
171
|
-
sendAnthropicUpstreamError(res, upstream, data);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const anthropicMessage = chatToAnthropic(JSON.parse(data), requestedModel, { forceModel: forceRequestedModel });
|
|
175
|
-
recordAnthropicUsage({
|
|
176
|
-
surface: "messages",
|
|
177
|
-
mode: "json",
|
|
178
|
-
model: anthropicMessage.model,
|
|
179
|
-
responseId: anthropicMessage.id,
|
|
180
|
-
usage: anthropicMessage.usage,
|
|
181
|
-
});
|
|
182
|
-
res.writeHead(200, safeUpstreamResponseHeaders(upstream.headers, {
|
|
183
|
-
contentType: "application/json",
|
|
184
|
-
}));
|
|
185
|
-
res.end(JSON.stringify(anthropicMessage));
|
|
186
|
-
}
|
|
187
|
-
} catch (error) {
|
|
188
|
-
logRequestFailure("Messages", error, abort);
|
|
189
|
-
sendJsonError(res, error, 502);
|
|
190
|
-
} finally {
|
|
191
|
-
releaseRequest();
|
|
192
|
-
abort.cleanup();
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
}
|