Skip to content

Commit 1ded0d4

Browse files
committed
fix(proxy): downstream abort propagation, Anthropic stream completion, history filtering
- Cancel upstream requests when client disconnects via abort signal - Stop reading Anthropic SSE after message_stop event - Skip inference history for non-LLM requests and empty records - Use Intl.NumberFormat for full USDC precision (up to 12 decimals) - Guard stream res.end() against double-close - Default claude model changed to stepfun/step-3.5-flash
1 parent f81df79 commit 1ded0d4

6 files changed

Lines changed: 253 additions & 113 deletions

File tree

packages/x402-proxy/CHANGELOG.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.10.6] - 2026-04-01
11+
12+
### Fixed
13+
- Client disconnect now cancels upstream requests via abort signal propagation (prevents resource leaks on dropped connections)
14+
- Anthropic SSE streaming stops reading after `message_stop` event instead of waiting for connection close
15+
- Non-LLM endpoint requests (tool discovery, resource listing) no longer write empty records to inference history
16+
- Empty inference history records (no amount, model, tokens, or tx) filtered out when reading history
17+
- `formatUsdcValue()` uses `Intl.NumberFormat` for full precision up to 12 decimals instead of truncating to fixed tiers
18+
- Stream responses guarded against double `res.end()` calls
19+
20+
### Changed
21+
- Default model for `claude` command changed from `minimax/minimax-m2.7` to `stepfun/step-3.5-flash`
22+
1023
## [0.10.5] - 2026-04-01
1124

1225
### Fixed
@@ -374,7 +387,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
374387
- `appendHistory` / `readHistory` / `calcSpend` - JSONL transaction history
375388
- Re-exports from `@x402/fetch`, `@x402/svm`, `@x402/evm`
376389

377-
[Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.5...HEAD
390+
[Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.6...HEAD
391+
[0.10.6]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.5...v0.10.6
378392
[0.10.5]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.4...v0.10.5
379393
[0.10.4]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.3...v0.10.4
380394
[0.10.3]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.2...v0.10.3

packages/x402-proxy/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "x402-proxy",
3-
"version": "0.10.5",
3+
"version": "0.10.6",
44
"description": "curl for x402 paid APIs. Auto-pays any endpoint on Base, Solana, and Tempo. Also works as an OpenClaw plugin.",
55
"type": "module",
66
"sideEffects": false,

packages/x402-proxy/src/commands/claude.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { dim, error } from "../lib/output.js";
44
import { DEFAULT_SURF_UPSTREAM_URL } from "../openclaw/defaults.js";
55
import { startServeServer } from "./serve.js";
66

7-
const DEFAULT_MODEL = "minimax/minimax-m2.7";
7+
const DEFAULT_MODEL = "stepfun/step-3.5-flash";
88

99
type ClaudeFlags = {
1010
model: string;
@@ -34,7 +34,7 @@ Examples:
3434
flags: {
3535
model: {
3636
kind: "parsed",
37-
brief: "Model to use (default: minimax/minimax-m2.7)",
37+
brief: "Model to use (default: stepfun/step-3.5-flash)",
3838
parse: String,
3939
default: DEFAULT_MODEL,
4040
},

packages/x402-proxy/src/history.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ export type TxRecord = {
4545
meta?: Record<string, string | number>;
4646
};
4747

48+
function isMeaningfulInferenceRecord(record: TxRecord): boolean {
49+
if (record.kind !== "x402_inference") return true;
50+
if (!record.ok) return true;
51+
return (
52+
record.amount != null ||
53+
record.model != null ||
54+
record.inputTokens != null ||
55+
record.outputTokens != null ||
56+
record.tx != null
57+
);
58+
}
59+
4860
// --- File operations ---
4961

5062
export function appendHistory(historyPath: string, record: TxRecord): void {
@@ -74,7 +86,8 @@ export function readHistory(historyPath: string): TxRecord[] {
7486
try {
7587
const parsed = JSON.parse(line);
7688
if (typeof parsed.t !== "number" || typeof parsed.kind !== "string") return [];
77-
return [parsed as TxRecord];
89+
const record = parsed as TxRecord;
90+
return isMeaningfulInferenceRecord(record) ? [record] : [];
7891
} catch {
7992
return [];
8093
}
@@ -109,12 +122,12 @@ export function calcSpend(records: TxRecord[]): {
109122

110123
// --- Formatting ---
111124

112-
/** Format a USDC value with adaptive precision (no token suffix). */
113125
export function formatUsdcValue(amount: number): string {
114-
if (amount >= 0.01) return amount.toFixed(2);
115-
if (amount >= 0.001) return amount.toFixed(3);
116-
if (amount >= 0.0001) return amount.toFixed(4);
117-
return amount.toFixed(6);
126+
return new Intl.NumberFormat("en-US", {
127+
useGrouping: false,
128+
minimumFractionDigits: 0,
129+
maximumFractionDigits: 12,
130+
}).format(amount);
118131
}
119132

120133
export function formatAmount(amount: number, token: string): string {

packages/x402-proxy/src/openclaw/route.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ServerResponse } from "node:http";
22
import { describe, expect, it } from "vitest";
3-
import { createSseTracker, writeErrorResponse } from "./route.js";
3+
import { createSseTracker, shouldAppendInferenceHistory, writeErrorResponse } from "./route.js";
44

55
// ── createSseTracker ────────────────────────────────────────────────
66

@@ -125,6 +125,7 @@ describe("createSseTracker", () => {
125125
cacheRead: undefined,
126126
cacheWrite: undefined,
127127
});
128+
expect(tracker.sawAnthropicMessageStop).toBe(true);
128129
});
129130

130131
it("extracts cache fields from message_start", () => {
@@ -189,6 +190,7 @@ describe("createSseTracker", () => {
189190
cacheRead: undefined,
190191
cacheWrite: undefined,
191192
});
193+
expect(tracker.sawAnthropicMessageStop).toBe(true);
192194
});
193195
});
194196

@@ -293,6 +295,23 @@ describe("createSseTracker", () => {
293295
});
294296
});
295297

298+
describe("shouldAppendInferenceHistory", () => {
299+
it("skips non-LLM traffic and empty successful placeholders", () => {
300+
expect(shouldAppendInferenceHistory({ isLlmEndpoint: false })).toBe(false);
301+
expect(shouldAppendInferenceHistory({ isLlmEndpoint: true })).toBe(false);
302+
});
303+
304+
it("keeps usage-bearing and priced LLM requests", () => {
305+
expect(shouldAppendInferenceHistory({ isLlmEndpoint: true, amount: 0.0133 })).toBe(true);
306+
expect(
307+
shouldAppendInferenceHistory({
308+
isLlmEndpoint: true,
309+
usage: { model: "stepfun/step-3.5-flash", inputTokens: 10, outputTokens: 2 },
310+
}),
311+
).toBe(true);
312+
});
313+
});
314+
296315
// ── writeErrorResponse ──────────────────────────────────────────────
297316

298317
describe("writeErrorResponse", () => {

0 commit comments

Comments
 (0)