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
- // Rewrite the first system block's x-anthropic-billing-header (if present)
941
- // so cc_version and FP3 match OUR fingerprint and the buyer's actual
942
- // first user message. Real Claude Code always emits this block; sub2api's
943
- // gateway_service.go mirrors it verbatim but rewrites the version to
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
- // Critical because Anthropic's validator expects cc_version.<FP3> where
947
- // FP3 is a deterministic hash of (message_chars + cli_version). If we
948
- // leave the buyer's FP3 in place but their UA was a different version
949
- // from our pinned UA, the FP3 no longer matches cli_version in the header
950
- // and the validator rejects the request.
951
- function syncPassthroughBillingHeader(body, fingerprint) {
952
- if (!Array.isArray(body.system))
953
- return;
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
- if (system.length === 0)
956
- return;
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
- if (!firstBlock ||
959
- typeof firstBlock !== "object" ||
960
- firstBlock.type !== "text" ||
961
- typeof firstBlock.text !== "string") {
962
- return;
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
- const currentText = firstBlock.text;
965
- if (!currentText.startsWith("x-anthropic-billing-header:")) {
966
- // Non-CC client didn't include a billing header leave system alone.
967
- // If we're strict about this we could PREPEND one, but for now we
968
- // only touch what exists so non-CC passthrough (e.g. anthropic SDK
969
- // direct) works without extra surgery.
970
- return;
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
- syncPassthroughBillingHeader(body, fp);
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 relay instructions for Codex. Upstream treats `instructions` as
81
- // the system prompt. Keep minimal so the buyer's prompt gets full focus.
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
- opts.onRawEvent(`event: ${frameType}\ndata: ${text}\n\n`);
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 the template
1011
- // mode's RELAY_INSTRUCTIONS so the model still has guidance.
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 = RELAY_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
- opts.onRawEvent(`event: ${frameType}\ndata: ${text}\n\n`);
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.8",
3
+ "version": "0.15.10",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {