clawmoney 0.15.8 → 0.15.10
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.
|
@@ -937,41 +937,115 @@ function mergeBetas(required, clientBeta) {
|
|
|
937
937
|
}
|
|
938
938
|
return out.join(",");
|
|
939
939
|
}
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
// match the account's pinned UA (syncBillingHeaderVersion, gateway_billing_header.go).
|
|
940
|
+
// Ensure a passthrough body carries the full Claude Code fingerprint
|
|
941
|
+
// shell that Anthropic's OAuth-endpoint validator expects. Called from
|
|
942
|
+
// doCallClaudeApiPassthrough as the last body-munging step before the
|
|
943
|
+
// HTTP request goes out.
|
|
945
944
|
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
945
|
+
// The three fingerprint-sensitive fields that MUST be present on every
|
|
946
|
+
// real Claude Code /v1/messages request:
|
|
947
|
+
//
|
|
948
|
+
// 1. system[0] = {type:"text", text:"x-anthropic-billing-header: ..."}
|
|
949
|
+
// — always the FIRST block. Contains cc_version.<FP3> where FP3 is
|
|
950
|
+
// SHA256(SALT + chars_from_first_user_msg + cli_version).hex[:3].
|
|
951
|
+
// 2. system[i] = {type:"text", text:"You are a Claude agent, built on
|
|
952
|
+
// Anthropic's Claude Agent SDK..."} with cache_control ephemeral.
|
|
953
|
+
// — the template-mode "CC identity marker" that passes the dice-
|
|
954
|
+
// coefficient validator.
|
|
955
|
+
// 3. thinking: {type, budget_tokens?} — on Claude 4+ models real CLI
|
|
956
|
+
// always sends this; zero-thinking accounts stand out.
|
|
957
|
+
// 4. tools: array (empty [] is fine) — real CLI always sends the
|
|
958
|
+
// field, missing means the request shape doesn't match.
|
|
959
|
+
//
|
|
960
|
+
// For real Claude Code / anthropic SDK clients that already send a full
|
|
961
|
+
// body (via /v1/messages passthrough path), every check here no-ops —
|
|
962
|
+
// the body is already in CC shape and we don't touch it.
|
|
963
|
+
//
|
|
964
|
+
// For OpenAI-SDK-style clients going through /v1/chat/completions (the
|
|
965
|
+
// Hub's chat→anthropic converter produces a minimal body), we augment
|
|
966
|
+
// with the missing shell fields so the outbound request is
|
|
967
|
+
// indistinguishable from a real CC request that happens to have a
|
|
968
|
+
// user-provided system prompt and no local tools.
|
|
969
|
+
//
|
|
970
|
+
// All buyer content (messages, their own system text, their own tools,
|
|
971
|
+
// thinking config if they sent one) is preserved.
|
|
972
|
+
function ensureClaudeCodeShell(body, fingerprint) {
|
|
973
|
+
// ── Normalize system to an array of content blocks ──
|
|
974
|
+
if (!Array.isArray(body.system)) {
|
|
975
|
+
if (typeof body.system === "string" && body.system.length > 0) {
|
|
976
|
+
// String-shaped system (anthropic SDK convenience form) →
|
|
977
|
+
// wrap in a single text block so we can prepend.
|
|
978
|
+
body.system = [{ type: "text", text: body.system }];
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
body.system = [];
|
|
982
|
+
}
|
|
983
|
+
}
|
|
954
984
|
const system = body.system;
|
|
955
|
-
|
|
956
|
-
|
|
985
|
+
// ── Detect CC identity marker anywhere in system ──
|
|
986
|
+
const hasCcMarker = system.some((b) => b &&
|
|
987
|
+
typeof b === "object" &&
|
|
988
|
+
b.type === "text" &&
|
|
989
|
+
typeof b.text === "string" &&
|
|
990
|
+
b.text.includes(CLAUDE_CODE_SYSTEM_PROMPT_LEAD));
|
|
991
|
+
// ── Detect billing header in system[0] ──
|
|
957
992
|
const firstBlock = system[0];
|
|
958
|
-
|
|
959
|
-
typeof firstBlock
|
|
960
|
-
firstBlock.type
|
|
961
|
-
typeof firstBlock.text
|
|
962
|
-
|
|
993
|
+
const hasBillingHeaderFirst = !!firstBlock &&
|
|
994
|
+
typeof firstBlock === "object" &&
|
|
995
|
+
firstBlock.type === "text" &&
|
|
996
|
+
typeof firstBlock.text === "string" &&
|
|
997
|
+
firstBlock.text.startsWith("x-anthropic-billing-header:");
|
|
998
|
+
// ── Build the attribution header (always recompute so cc_version + FP3
|
|
999
|
+
// match OUR fingerprint and the buyer's actual first user message) ──
|
|
1000
|
+
const firstUserMsg = extractFirstUserMessageText(body.messages);
|
|
1001
|
+
const freshHeader = buildClaudeAttributionHeader(firstUserMsg, fingerprint.cc_version, fingerprint.cc_entrypoint);
|
|
1002
|
+
// ── Inject CC marker if missing ──
|
|
1003
|
+
// Position: right after the billing header slot (idx 1), or right
|
|
1004
|
+
// after any buyer-prefixed system blocks (at head) if we're also
|
|
1005
|
+
// inserting the billing header.
|
|
1006
|
+
if (!hasCcMarker) {
|
|
1007
|
+
const markerBlock = {
|
|
1008
|
+
type: "text",
|
|
1009
|
+
text: `${CLAUDE_CODE_SYSTEM_PROMPT_LEAD}\n\n${RELAY_INSTRUCTIONS}`,
|
|
1010
|
+
cache_control: { type: "ephemeral" },
|
|
1011
|
+
};
|
|
1012
|
+
// Insert at index 1 (slot after billing header), or 0 if we'll be
|
|
1013
|
+
// unshifting the billing header next.
|
|
1014
|
+
if (hasBillingHeaderFirst) {
|
|
1015
|
+
system.splice(1, 0, markerBlock);
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
system.unshift(markerBlock);
|
|
1019
|
+
}
|
|
963
1020
|
}
|
|
964
|
-
|
|
965
|
-
if (
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
|
|
970
|
-
|
|
1021
|
+
// ── Update or inject billing header at index 0 ──
|
|
1022
|
+
if (hasBillingHeaderFirst) {
|
|
1023
|
+
// Rewrite in place so cc_version reflects OUR fingerprint, not the
|
|
1024
|
+
// buyer's original (which might have been from a different CLI
|
|
1025
|
+
// version than our pinned fingerprint).
|
|
1026
|
+
firstBlock.text = freshHeader;
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
system.unshift({ type: "text", text: freshHeader });
|
|
1030
|
+
}
|
|
1031
|
+
// ── Ensure tools array exists ──
|
|
1032
|
+
if (!Array.isArray(body.tools)) {
|
|
1033
|
+
body.tools = [];
|
|
1034
|
+
}
|
|
1035
|
+
// ── Inject thinking config if missing ──
|
|
1036
|
+
// Real CLI always sends this for Claude 4+ models; zero-thinking
|
|
1037
|
+
// accounts are a relay-farm tell. pickClaudeThinkingConfig picks the
|
|
1038
|
+
// right shape (adaptive for 4-6, enabled-with-budget for 4-5/haiku).
|
|
1039
|
+
if (!body.thinking || typeof body.thinking !== "object") {
|
|
1040
|
+
const rawMaxTokens = typeof body.max_tokens === "number" && body.max_tokens > 0
|
|
1041
|
+
? body.max_tokens
|
|
1042
|
+
: 4096;
|
|
1043
|
+
const { config, adjustedMaxTokens } = pickClaudeThinkingConfig(body.model ?? "", rawMaxTokens);
|
|
1044
|
+
if (config) {
|
|
1045
|
+
body.thinking = config;
|
|
1046
|
+
body.max_tokens = adjustedMaxTokens;
|
|
1047
|
+
}
|
|
971
1048
|
}
|
|
972
|
-
const firstUserMsg = extractFirstUserMessageText(body.messages);
|
|
973
|
-
const newHeader = buildClaudeAttributionHeader(firstUserMsg, fingerprint.cc_version, fingerprint.cc_entrypoint);
|
|
974
|
-
firstBlock.text = newHeader;
|
|
975
1049
|
}
|
|
976
1050
|
// Walk system text blocks and rewrite third-party identity sentences
|
|
977
1051
|
// (OpenCode, etc.) to the Claude Code banner. sub2api does the same thing
|
|
@@ -1025,7 +1099,7 @@ async function doCallClaudeApiPassthrough(opts) {
|
|
|
1025
1099
|
// Sanitize system: replace third-party identity sentences + sync
|
|
1026
1100
|
// billing header cc_version to match our pinned CLI version.
|
|
1027
1101
|
sanitizePassthroughSystemArray(body);
|
|
1028
|
-
|
|
1102
|
+
ensureClaudeCodeShell(body, fp);
|
|
1029
1103
|
// Clamp thinking.budget_tokens to Anthropic's minimum so buyer-chosen
|
|
1030
1104
|
// small budgets don't 400. If max_tokens < budget_tokens + 1, bump
|
|
1031
1105
|
// max_tokens too so the request stays valid.
|
|
@@ -77,9 +77,17 @@ const MAX_TRANSIENT_RETRIES = 2;
|
|
|
77
77
|
// Codex responses on small prompts come back in <10s; we give a generous
|
|
78
78
|
// ceiling to tolerate slow tokens without hanging the daemon forever.
|
|
79
79
|
const WS_OVERALL_TIMEOUT_MS = 180 * 1000;
|
|
80
|
-
// Default
|
|
81
|
-
//
|
|
80
|
+
// Default instructions for Codex template mode. Template mode flattens
|
|
81
|
+
// messages into a single prompt and drops buyer's tools — the "plain
|
|
82
|
+
// text only" hint aligns model behavior with what template can actually
|
|
83
|
+
// deliver.
|
|
82
84
|
const RELAY_INSTRUCTIONS = "You are a helpful AI assistant operating in relay mode. Respond to the user's message with plain text only. Be concise.";
|
|
85
|
+
// Neutral fallback for Codex passthrough mode when the buyer did NOT
|
|
86
|
+
// supply their own instructions. Unlike the template-mode string, this
|
|
87
|
+
// one does NOT forbid tool use — if the buyer sent a tools array we
|
|
88
|
+
// want the model to use them. Kept intentionally vague so it doesn't
|
|
89
|
+
// bias the model's behavior when the buyer's intent is unspecified.
|
|
90
|
+
const CODEX_PASSTHROUGH_FALLBACK_INSTRUCTIONS = "You are a helpful coding assistant. Use the available tools when appropriate to answer the user.";
|
|
83
91
|
// ── Proxy ──
|
|
84
92
|
//
|
|
85
93
|
// We configure the global undici dispatcher for the OAuth refresh fetch()
|
|
@@ -449,6 +457,47 @@ function buildCodexRequestFrame(prompt, model, fingerprint, sessionId, turnMetad
|
|
|
449
457
|
}
|
|
450
458
|
return frame;
|
|
451
459
|
}
|
|
460
|
+
// Patch a raw ChatGPT WS frame before we forward it to the Hub as SSE.
|
|
461
|
+
// ChatGPT's internal response.completed frames come from a proprietary
|
|
462
|
+
// backend that does NOT populate usage.total_tokens — the Codex CLI Rust
|
|
463
|
+
// parser is strict about this field (stream disconnected before completion:
|
|
464
|
+
// failed to parse ResponseCompleted: missing field `total_tokens`), so we
|
|
465
|
+
// inject it here when we can compute it from input_tokens + output_tokens.
|
|
466
|
+
// Returns the possibly-rewritten frame JSON; on parse/shape error returns
|
|
467
|
+
// the original untouched so a malformed input never turns into a crash.
|
|
468
|
+
function patchCodexFrameForForwarding(raw) {
|
|
469
|
+
try {
|
|
470
|
+
const evt = JSON.parse(raw);
|
|
471
|
+
const type = evt["type"];
|
|
472
|
+
if (type !== "response.completed" && type !== "response.done") {
|
|
473
|
+
return raw;
|
|
474
|
+
}
|
|
475
|
+
const resp = evt["response"];
|
|
476
|
+
if (!resp || typeof resp !== "object")
|
|
477
|
+
return raw;
|
|
478
|
+
const usage = resp["usage"];
|
|
479
|
+
if (!usage || typeof usage !== "object")
|
|
480
|
+
return raw;
|
|
481
|
+
if (typeof usage["total_tokens"] === "number")
|
|
482
|
+
return raw;
|
|
483
|
+
const input = Number(usage["input_tokens"] ?? 0);
|
|
484
|
+
const output = Number(usage["output_tokens"] ?? 0);
|
|
485
|
+
usage["total_tokens"] = input + output;
|
|
486
|
+
// Also ensure the nested *_details objects exist — Codex CLI's
|
|
487
|
+
// schema checks for them on the response.completed frame.
|
|
488
|
+
if (!usage["input_tokens_details"] || typeof usage["input_tokens_details"] !== "object") {
|
|
489
|
+
const cached = Number(usage.cache_read_input_tokens ?? 0);
|
|
490
|
+
usage["input_tokens_details"] = { cached_tokens: cached };
|
|
491
|
+
}
|
|
492
|
+
if (!usage["output_tokens_details"] || typeof usage["output_tokens_details"] !== "object") {
|
|
493
|
+
usage["output_tokens_details"] = { reasoning_tokens: 0 };
|
|
494
|
+
}
|
|
495
|
+
return JSON.stringify(evt);
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return raw;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
452
501
|
function handleFrame(raw, acc) {
|
|
453
502
|
let evt;
|
|
454
503
|
try {
|
|
@@ -877,7 +926,10 @@ async function doCallCodexApi(opts) {
|
|
|
877
926
|
try {
|
|
878
927
|
const parsedFrame = JSON.parse(text);
|
|
879
928
|
const frameType = typeof parsedFrame.type === "string" ? parsedFrame.type : "message";
|
|
880
|
-
|
|
929
|
+
// Inject usage.total_tokens on response.completed frames so
|
|
930
|
+
// the end client's strict parser doesn't abort the stream.
|
|
931
|
+
const patched = patchCodexFrameForForwarding(text);
|
|
932
|
+
opts.onRawEvent(`event: ${frameType}\ndata: ${patched}\n\n`);
|
|
881
933
|
}
|
|
882
934
|
catch {
|
|
883
935
|
// Non-JSON frame — forward as a plain data event.
|
|
@@ -1007,10 +1059,13 @@ function buildCodexPassthroughFrame(clientBody, model, fingerprint, sessionId, t
|
|
|
1007
1059
|
if (frame.parallel_tool_calls === undefined) {
|
|
1008
1060
|
frame.parallel_tool_calls = false;
|
|
1009
1061
|
}
|
|
1010
|
-
// Instructions: if buyer didn't send one, fall back to
|
|
1011
|
-
//
|
|
1062
|
+
// Instructions: if buyer didn't send one, fall back to a neutral
|
|
1063
|
+
// tool-friendly default so the model still has guidance while not
|
|
1064
|
+
// forbidding tool use (unlike template mode's RELAY_INSTRUCTIONS,
|
|
1065
|
+
// which says "plain text only" — wrong fit for passthrough where
|
|
1066
|
+
// buyer's tools should actually be used).
|
|
1012
1067
|
if (typeof frame.instructions !== "string" || !frame.instructions) {
|
|
1013
|
-
frame.instructions =
|
|
1068
|
+
frame.instructions = CODEX_PASSTHROUGH_FALLBACK_INSTRUCTIONS;
|
|
1014
1069
|
}
|
|
1015
1070
|
if (warmup) {
|
|
1016
1071
|
// Real CLI's prewarm flow: first frame of each turn has generate:false.
|
|
@@ -1183,7 +1238,8 @@ async function doCallCodexApiPassthrough(opts) {
|
|
|
1183
1238
|
try {
|
|
1184
1239
|
const parsedFrame = JSON.parse(text);
|
|
1185
1240
|
const frameType = typeof parsedFrame.type === "string" ? parsedFrame.type : "message";
|
|
1186
|
-
|
|
1241
|
+
const patched = patchCodexFrameForForwarding(text);
|
|
1242
|
+
opts.onRawEvent(`event: ${frameType}\ndata: ${patched}\n\n`);
|
|
1187
1243
|
}
|
|
1188
1244
|
catch {
|
|
1189
1245
|
opts.onRawEvent(`event: message\ndata: ${text}\n\n`);
|