@token2chat/t2c 0.2.0-beta.1 → 0.2.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 +117 -144
- package/dist/cashu-store.d.ts +1 -1
- package/dist/cashu-store.js +4 -4
- package/dist/commands/audit.d.ts +65 -0
- package/dist/commands/audit.js +12 -12
- package/dist/commands/balance.js +2 -2
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/mint.js +14 -14
- package/dist/commands/monitor.d.ts +51 -0
- package/dist/commands/monitor.js +353 -0
- package/dist/commands/recover.js +4 -4
- package/dist/commands/setup.js +2 -2
- package/dist/commands/status.js +2 -3
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/connectors/cursor.js +44 -15
- package/dist/index.js +8 -1
- package/dist/proxy/auth.d.ts +20 -0
- package/dist/proxy/auth.js +28 -0
- package/dist/proxy/errors.d.ts +58 -0
- package/dist/proxy/errors.js +95 -0
- package/dist/proxy/gate-client.d.ts +34 -0
- package/dist/proxy/gate-client.js +81 -0
- package/dist/proxy/index.d.ts +10 -0
- package/dist/proxy/index.js +17 -0
- package/dist/proxy/payment-service.d.ts +65 -0
- package/dist/proxy/payment-service.js +101 -0
- package/dist/proxy/pricing.d.ts +37 -0
- package/dist/proxy/pricing.js +90 -0
- package/dist/proxy/response.d.ts +24 -0
- package/dist/proxy/response.js +48 -0
- package/dist/proxy/sse-parser.d.ts +19 -0
- package/dist/proxy/sse-parser.js +80 -0
- package/dist/proxy/types.d.ts +113 -0
- package/dist/proxy/types.js +74 -0
- package/dist/proxy.d.ts +2 -9
- package/dist/proxy.js +74 -186
- package/package.json +4 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy error classes with OpenAI-compatible JSON serialization.
|
|
3
|
+
*/
|
|
4
|
+
export interface ProxyErrorJSON {
|
|
5
|
+
error: {
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Base error class for proxy errors.
|
|
13
|
+
* Serializes to OpenAI-compatible error format.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ProxyError extends Error {
|
|
16
|
+
readonly code: string;
|
|
17
|
+
readonly httpStatus: number;
|
|
18
|
+
readonly type?: string;
|
|
19
|
+
constructor(code: string, message: string, httpStatus?: number, type?: string);
|
|
20
|
+
toJSON(): ProxyErrorJSON;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when wallet balance is insufficient for the requested model.
|
|
24
|
+
*/
|
|
25
|
+
export declare class InsufficientBalanceError extends ProxyError {
|
|
26
|
+
readonly balance: number;
|
|
27
|
+
readonly required: number;
|
|
28
|
+
readonly model: string;
|
|
29
|
+
constructor(balance: number, required: number, model: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Thrown when the Gate is unreachable.
|
|
33
|
+
*/
|
|
34
|
+
export declare class GateUnreachableError extends ProxyError {
|
|
35
|
+
readonly gateUrl: string;
|
|
36
|
+
constructor(gateUrl: string, cause?: Error);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when request body exceeds size limit.
|
|
40
|
+
*/
|
|
41
|
+
export declare class PayloadTooLargeError extends ProxyError {
|
|
42
|
+
readonly size: number;
|
|
43
|
+
readonly limit: number;
|
|
44
|
+
constructor(size: number, limit: number);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Thrown when authentication fails.
|
|
48
|
+
*/
|
|
49
|
+
export declare class UnauthorizedError extends ProxyError {
|
|
50
|
+
constructor(message?: string);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when endpoint is not found.
|
|
54
|
+
*/
|
|
55
|
+
export declare class NotFoundError extends ProxyError {
|
|
56
|
+
readonly path: string;
|
|
57
|
+
constructor(path: string);
|
|
58
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy error classes with OpenAI-compatible JSON serialization.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Base error class for proxy errors.
|
|
6
|
+
* Serializes to OpenAI-compatible error format.
|
|
7
|
+
*/
|
|
8
|
+
export class ProxyError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
httpStatus;
|
|
11
|
+
type;
|
|
12
|
+
constructor(code, message, httpStatus = 500, type) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ProxyError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.httpStatus = httpStatus;
|
|
17
|
+
this.type = type;
|
|
18
|
+
}
|
|
19
|
+
toJSON() {
|
|
20
|
+
return {
|
|
21
|
+
error: {
|
|
22
|
+
code: this.code,
|
|
23
|
+
message: this.message,
|
|
24
|
+
...(this.type && { type: this.type }),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Thrown when wallet balance is insufficient for the requested model.
|
|
31
|
+
*/
|
|
32
|
+
export class InsufficientBalanceError extends ProxyError {
|
|
33
|
+
balance;
|
|
34
|
+
required;
|
|
35
|
+
model;
|
|
36
|
+
constructor(balance, required, model) {
|
|
37
|
+
super("insufficient_balance", `Wallet balance ${balance} < ${required} required for ${model}. Run 't2c mint' to add funds.`, 402, "insufficient_funds");
|
|
38
|
+
this.name = "InsufficientBalanceError";
|
|
39
|
+
this.balance = balance;
|
|
40
|
+
this.required = required;
|
|
41
|
+
this.model = model;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Thrown when the Gate is unreachable.
|
|
46
|
+
*/
|
|
47
|
+
export class GateUnreachableError extends ProxyError {
|
|
48
|
+
gateUrl;
|
|
49
|
+
constructor(gateUrl, cause) {
|
|
50
|
+
const host = new URL(gateUrl).host;
|
|
51
|
+
super("gate_unreachable", `Gate at ${host} is unreachable`, 502);
|
|
52
|
+
this.name = "GateUnreachableError";
|
|
53
|
+
this.gateUrl = gateUrl;
|
|
54
|
+
if (cause) {
|
|
55
|
+
this.cause = cause;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function formatBytes(bytes) {
|
|
60
|
+
const mb = bytes / (1024 * 1024);
|
|
61
|
+
return `${mb.toFixed(1)} MB`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Thrown when request body exceeds size limit.
|
|
65
|
+
*/
|
|
66
|
+
export class PayloadTooLargeError extends ProxyError {
|
|
67
|
+
size;
|
|
68
|
+
limit;
|
|
69
|
+
constructor(size, limit) {
|
|
70
|
+
super("payload_too_large", `Request body ${formatBytes(size)} exceeds limit of ${formatBytes(limit)}`, 413);
|
|
71
|
+
this.name = "PayloadTooLargeError";
|
|
72
|
+
this.size = size;
|
|
73
|
+
this.limit = limit;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Thrown when authentication fails.
|
|
78
|
+
*/
|
|
79
|
+
export class UnauthorizedError extends ProxyError {
|
|
80
|
+
constructor(message = "Unauthorized. Provide a valid Bearer token.") {
|
|
81
|
+
super("unauthorized", message, 401, "authentication_error");
|
|
82
|
+
this.name = "UnauthorizedError";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Thrown when endpoint is not found.
|
|
87
|
+
*/
|
|
88
|
+
export class NotFoundError extends ProxyError {
|
|
89
|
+
path;
|
|
90
|
+
constructor(path) {
|
|
91
|
+
super("not_found", `Endpoint not found: ${path}`, 404);
|
|
92
|
+
this.name = "NotFoundError";
|
|
93
|
+
this.path = path;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GateClient - HTTP client for token2chat Gate with retry logic.
|
|
3
|
+
*/
|
|
4
|
+
import { type Logger } from "./types.js";
|
|
5
|
+
export interface GateClientOptions {
|
|
6
|
+
fetchFn?: typeof fetch;
|
|
7
|
+
logger?: Logger;
|
|
8
|
+
}
|
|
9
|
+
export interface GateRequestOptions {
|
|
10
|
+
path: string;
|
|
11
|
+
body: string;
|
|
12
|
+
token: string;
|
|
13
|
+
gateUrl?: string;
|
|
14
|
+
stream?: boolean;
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
baseDelayMs?: number;
|
|
17
|
+
maxDelayMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface GateResponse {
|
|
20
|
+
status: number;
|
|
21
|
+
body?: string;
|
|
22
|
+
stream?: ReadableStream<Uint8Array>;
|
|
23
|
+
contentType?: string;
|
|
24
|
+
changeToken?: string;
|
|
25
|
+
refundToken?: string;
|
|
26
|
+
retriesExhausted?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare class GateClient {
|
|
29
|
+
private readonly gateUrl;
|
|
30
|
+
private readonly fetchFn;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
constructor(gateUrl: string, options?: GateClientOptions);
|
|
33
|
+
request(options: GateRequestOptions): Promise<GateResponse>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GateClient - HTTP client for token2chat Gate with retry logic.
|
|
3
|
+
*/
|
|
4
|
+
import { defaultLogger, parseRetryAfter, MAX_RETRY_DELAY_MS } from "./types.js";
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
export class GateClient {
|
|
9
|
+
gateUrl;
|
|
10
|
+
fetchFn;
|
|
11
|
+
logger;
|
|
12
|
+
constructor(gateUrl, options = {}) {
|
|
13
|
+
this.gateUrl = gateUrl;
|
|
14
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
15
|
+
this.logger = options.logger ?? defaultLogger;
|
|
16
|
+
}
|
|
17
|
+
async request(options) {
|
|
18
|
+
const { path, body, token, gateUrl = this.gateUrl, stream = false, maxRetries = 0, baseDelayMs = 2000, maxDelayMs = MAX_RETRY_DELAY_MS, } = options;
|
|
19
|
+
const url = `${gateUrl}${path}`;
|
|
20
|
+
let lastResponse = null;
|
|
21
|
+
let lastBody;
|
|
22
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
23
|
+
try {
|
|
24
|
+
const res = await this.fetchFn(url, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"X-Cashu": token,
|
|
29
|
+
},
|
|
30
|
+
body,
|
|
31
|
+
});
|
|
32
|
+
// Extract change/refund tokens
|
|
33
|
+
const changeToken = res.headers.get("X-Cashu-Change") ?? undefined;
|
|
34
|
+
const refundToken = res.headers.get("X-Cashu-Refund") ?? undefined;
|
|
35
|
+
const contentType = res.headers.get("content-type") ?? undefined;
|
|
36
|
+
// If not 429, return immediately
|
|
37
|
+
if (res.status !== 429) {
|
|
38
|
+
if (stream && res.body) {
|
|
39
|
+
return {
|
|
40
|
+
status: res.status,
|
|
41
|
+
stream: res.body,
|
|
42
|
+
contentType,
|
|
43
|
+
changeToken,
|
|
44
|
+
refundToken,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
status: res.status,
|
|
49
|
+
body: await res.text(),
|
|
50
|
+
contentType,
|
|
51
|
+
changeToken,
|
|
52
|
+
refundToken,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Store for potential return after retries exhausted
|
|
56
|
+
lastResponse = res;
|
|
57
|
+
lastBody = await res.text();
|
|
58
|
+
// Log and wait before retry
|
|
59
|
+
if (attempt < maxRetries) {
|
|
60
|
+
const retryAfterMs = parseRetryAfter(res.headers.get("Retry-After"));
|
|
61
|
+
const backoffMs = Math.min(retryAfterMs ?? baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
62
|
+
this.logger.warn(`Rate limited (429), retrying in ${backoffMs}ms...`);
|
|
63
|
+
await sleep(backoffMs);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
this.logger.error("Gate request failed:", e);
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// All retries exhausted
|
|
72
|
+
return {
|
|
73
|
+
status: lastResponse.status,
|
|
74
|
+
body: lastBody,
|
|
75
|
+
contentType: lastResponse.headers.get("content-type") ?? undefined,
|
|
76
|
+
changeToken: lastResponse.headers.get("X-Cashu-Change") ?? undefined,
|
|
77
|
+
refundToken: lastResponse.headers.get("X-Cashu-Refund") ?? undefined,
|
|
78
|
+
retriesExhausted: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy module exports.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export * from "./errors.js";
|
|
6
|
+
export { PricingCache, type PricingCacheOptions } from "./pricing.js";
|
|
7
|
+
export { GateClient, type GateClientOptions, type GateRequestOptions, type GateResponse } from "./gate-client.js";
|
|
8
|
+
export { PaymentService, type PaymentServiceOptions, type Wallet, type TokenSelectionResult, type GateTokensResult } from "./payment-service.js";
|
|
9
|
+
export { createAuthChecker, type AuthChecker, type AuthRequest } from "./auth.js";
|
|
10
|
+
export { handleError, sendError, sendJsonResponse, type ResponseWriter } from "./response.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy module exports.
|
|
3
|
+
*/
|
|
4
|
+
// Types and utilities
|
|
5
|
+
export * from "./types.js";
|
|
6
|
+
// Error classes
|
|
7
|
+
export * from "./errors.js";
|
|
8
|
+
// Pricing cache
|
|
9
|
+
export { PricingCache } from "./pricing.js";
|
|
10
|
+
// Gate client
|
|
11
|
+
export { GateClient } from "./gate-client.js";
|
|
12
|
+
// Payment service
|
|
13
|
+
export { PaymentService } from "./payment-service.js";
|
|
14
|
+
// Auth
|
|
15
|
+
export { createAuthChecker } from "./auth.js";
|
|
16
|
+
// Response utilities
|
|
17
|
+
export { handleError, sendError, sendJsonResponse } from "./response.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaymentService - Handles ecash payment operations for proxy requests.
|
|
3
|
+
*/
|
|
4
|
+
import { type Logger } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Minimal wallet interface for PaymentService.
|
|
7
|
+
*/
|
|
8
|
+
export interface Wallet {
|
|
9
|
+
balance: number;
|
|
10
|
+
selectAndEncode(amount: number): Promise<string>;
|
|
11
|
+
receiveToken(token: string): Promise<number>;
|
|
12
|
+
}
|
|
13
|
+
export interface PaymentServiceOptions {
|
|
14
|
+
wallet: Wallet;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
appendFailedToken?: (token: string, type: "change" | "refund", error: string) => Promise<void>;
|
|
17
|
+
lowBalanceThreshold?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TokenSelectionResult {
|
|
20
|
+
token: string;
|
|
21
|
+
balanceBefore: number;
|
|
22
|
+
balanceAfter: number;
|
|
23
|
+
}
|
|
24
|
+
export interface GateTokensResult {
|
|
25
|
+
changeSat: number;
|
|
26
|
+
refundSat: number;
|
|
27
|
+
}
|
|
28
|
+
export declare class PaymentService {
|
|
29
|
+
private readonly wallet;
|
|
30
|
+
private readonly logger;
|
|
31
|
+
private readonly appendFailedToken;
|
|
32
|
+
private readonly lowBalanceThreshold;
|
|
33
|
+
constructor(options: PaymentServiceOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Check if wallet has sufficient balance for the request.
|
|
36
|
+
* @throws InsufficientBalanceError if balance is insufficient
|
|
37
|
+
*/
|
|
38
|
+
checkBalance(required: number, model: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Select and encode tokens for payment.
|
|
41
|
+
*/
|
|
42
|
+
selectToken(amount: number): Promise<TokenSelectionResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Receive change token from Gate.
|
|
45
|
+
* Returns amount received, or 0 if failed.
|
|
46
|
+
*/
|
|
47
|
+
receiveChange(token: string): Promise<number>;
|
|
48
|
+
/**
|
|
49
|
+
* Receive refund token from Gate.
|
|
50
|
+
* Returns amount received, or 0 if failed.
|
|
51
|
+
*/
|
|
52
|
+
receiveRefund(token: string): Promise<number>;
|
|
53
|
+
/**
|
|
54
|
+
* Process change and refund tokens from Gate response.
|
|
55
|
+
*/
|
|
56
|
+
processGateTokens(changeToken?: string, refundToken?: string): Promise<GateTokensResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Check and warn if balance is below threshold.
|
|
59
|
+
*/
|
|
60
|
+
checkLowBalance(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Get current wallet balance.
|
|
63
|
+
*/
|
|
64
|
+
getBalance(): number;
|
|
65
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaymentService - Handles ecash payment operations for proxy requests.
|
|
3
|
+
*/
|
|
4
|
+
import { defaultLogger } from "./types.js";
|
|
5
|
+
import { InsufficientBalanceError } from "./errors.js";
|
|
6
|
+
export class PaymentService {
|
|
7
|
+
wallet;
|
|
8
|
+
logger;
|
|
9
|
+
appendFailedToken;
|
|
10
|
+
lowBalanceThreshold;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.wallet = options.wallet;
|
|
13
|
+
this.logger = options.logger ?? defaultLogger;
|
|
14
|
+
this.appendFailedToken = options.appendFailedToken ?? (async () => { });
|
|
15
|
+
this.lowBalanceThreshold = options.lowBalanceThreshold ?? 100;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if wallet has sufficient balance for the request.
|
|
19
|
+
* @throws InsufficientBalanceError if balance is insufficient
|
|
20
|
+
*/
|
|
21
|
+
checkBalance(required, model) {
|
|
22
|
+
const balance = this.wallet.balance;
|
|
23
|
+
if (balance < required) {
|
|
24
|
+
this.logger.warn(`Insufficient balance: ${balance} < ${required} for ${model}`);
|
|
25
|
+
throw new InsufficientBalanceError(balance, required, model);
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Select and encode tokens for payment.
|
|
31
|
+
*/
|
|
32
|
+
async selectToken(amount) {
|
|
33
|
+
const balanceBefore = this.wallet.balance;
|
|
34
|
+
const token = await this.wallet.selectAndEncode(amount);
|
|
35
|
+
const balanceAfter = this.wallet.balance;
|
|
36
|
+
return { token, balanceBefore, balanceAfter };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Receive change token from Gate.
|
|
40
|
+
* Returns amount received, or 0 if failed.
|
|
41
|
+
*/
|
|
42
|
+
async receiveChange(token) {
|
|
43
|
+
try {
|
|
44
|
+
const amount = await this.wallet.receiveToken(token);
|
|
45
|
+
this.logger.info(`Received ${amount} change`);
|
|
46
|
+
return amount;
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
50
|
+
this.logger.warn(`Failed to store change: ${errMsg}`);
|
|
51
|
+
await this.appendFailedToken(token, "change", errMsg);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Receive refund token from Gate.
|
|
57
|
+
* Returns amount received, or 0 if failed.
|
|
58
|
+
*/
|
|
59
|
+
async receiveRefund(token) {
|
|
60
|
+
try {
|
|
61
|
+
const amount = await this.wallet.receiveToken(token);
|
|
62
|
+
this.logger.info(`Received ${amount} refund`);
|
|
63
|
+
return amount;
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
67
|
+
this.logger.warn(`Failed to store refund: ${errMsg}`);
|
|
68
|
+
await this.appendFailedToken(token, "refund", errMsg);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Process change and refund tokens from Gate response.
|
|
74
|
+
*/
|
|
75
|
+
async processGateTokens(changeToken, refundToken) {
|
|
76
|
+
let changeSat = 0;
|
|
77
|
+
let refundSat = 0;
|
|
78
|
+
if (changeToken) {
|
|
79
|
+
changeSat = await this.receiveChange(changeToken);
|
|
80
|
+
}
|
|
81
|
+
if (refundToken) {
|
|
82
|
+
refundSat = await this.receiveRefund(refundToken);
|
|
83
|
+
}
|
|
84
|
+
return { changeSat, refundSat };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check and warn if balance is below threshold.
|
|
88
|
+
*/
|
|
89
|
+
checkLowBalance() {
|
|
90
|
+
const balance = this.wallet.balance;
|
|
91
|
+
if (balance < this.lowBalanceThreshold) {
|
|
92
|
+
this.logger.warn(`⚠️ Low ecash balance: ${balance} (threshold: ${this.lowBalanceThreshold})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get current wallet balance.
|
|
97
|
+
*/
|
|
98
|
+
getBalance() {
|
|
99
|
+
return this.wallet.balance;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PricingCache - Caches model pricing from Gate with TTL.
|
|
3
|
+
*/
|
|
4
|
+
export interface PricingCacheOptions {
|
|
5
|
+
ttlMs?: number;
|
|
6
|
+
fetchFn?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
export declare class PricingCache {
|
|
9
|
+
private cache;
|
|
10
|
+
private fetchedAt;
|
|
11
|
+
private fetchPromise;
|
|
12
|
+
private readonly gateUrl;
|
|
13
|
+
private readonly ttlMs;
|
|
14
|
+
private readonly fetchFn;
|
|
15
|
+
constructor(gateUrl: string, options?: PricingCacheOptions);
|
|
16
|
+
/**
|
|
17
|
+
* Get cached pricing, refreshing if stale.
|
|
18
|
+
*/
|
|
19
|
+
get(): Promise<Record<string, number>>;
|
|
20
|
+
/**
|
|
21
|
+
* Force refresh pricing from gate.
|
|
22
|
+
*/
|
|
23
|
+
refresh(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Clear cache, forcing next get() to refetch.
|
|
26
|
+
*/
|
|
27
|
+
invalidate(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Get price for a model. Uses wildcard "*" or default if not found.
|
|
30
|
+
*/
|
|
31
|
+
getPrice(model: string, defaultPrice?: number): number;
|
|
32
|
+
/**
|
|
33
|
+
* Get list of available models (excludes wildcard).
|
|
34
|
+
*/
|
|
35
|
+
getModels(): string[];
|
|
36
|
+
private doFetch;
|
|
37
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PricingCache - Caches model pricing from Gate with TTL.
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_TTL_MS = 5 * 60_000; // 5 minutes
|
|
5
|
+
const DEFAULT_PRICE = 500; // units
|
|
6
|
+
export class PricingCache {
|
|
7
|
+
cache = null;
|
|
8
|
+
fetchedAt = 0;
|
|
9
|
+
fetchPromise = null;
|
|
10
|
+
gateUrl;
|
|
11
|
+
ttlMs;
|
|
12
|
+
fetchFn;
|
|
13
|
+
constructor(gateUrl, options = {}) {
|
|
14
|
+
this.gateUrl = gateUrl;
|
|
15
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
16
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get cached pricing, refreshing if stale.
|
|
20
|
+
*/
|
|
21
|
+
async get() {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
if (this.cache && now - this.fetchedAt < this.ttlMs) {
|
|
24
|
+
return this.cache;
|
|
25
|
+
}
|
|
26
|
+
// Deduplicate concurrent fetches
|
|
27
|
+
if (this.fetchPromise) {
|
|
28
|
+
return this.fetchPromise;
|
|
29
|
+
}
|
|
30
|
+
this.fetchPromise = this.doFetch();
|
|
31
|
+
try {
|
|
32
|
+
return await this.fetchPromise;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
this.fetchPromise = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Force refresh pricing from gate.
|
|
40
|
+
*/
|
|
41
|
+
async refresh() {
|
|
42
|
+
this.fetchPromise = null;
|
|
43
|
+
await this.doFetch();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Clear cache, forcing next get() to refetch.
|
|
47
|
+
*/
|
|
48
|
+
invalidate() {
|
|
49
|
+
this.cache = null;
|
|
50
|
+
this.fetchedAt = 0;
|
|
51
|
+
this.fetchPromise = null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get price for a model. Uses wildcard "*" or default if not found.
|
|
55
|
+
*/
|
|
56
|
+
getPrice(model, defaultPrice = DEFAULT_PRICE) {
|
|
57
|
+
if (!this.cache) {
|
|
58
|
+
return defaultPrice;
|
|
59
|
+
}
|
|
60
|
+
return this.cache[model] ?? this.cache["*"] ?? defaultPrice;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get list of available models (excludes wildcard).
|
|
64
|
+
*/
|
|
65
|
+
getModels() {
|
|
66
|
+
if (!this.cache) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return Object.keys(this.cache).filter((m) => m !== "*");
|
|
70
|
+
}
|
|
71
|
+
async doFetch() {
|
|
72
|
+
try {
|
|
73
|
+
const res = await this.fetchFn(`${this.gateUrl}/v1/pricing`);
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
return this.cache ?? {};
|
|
76
|
+
}
|
|
77
|
+
const data = (await res.json());
|
|
78
|
+
const prices = {};
|
|
79
|
+
for (const [model, rule] of Object.entries(data.models)) {
|
|
80
|
+
prices[model] = rule.per_request;
|
|
81
|
+
}
|
|
82
|
+
this.cache = prices;
|
|
83
|
+
this.fetchedAt = Date.now();
|
|
84
|
+
return prices;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return this.cache ?? {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response utilities and error handler for proxy.
|
|
3
|
+
*/
|
|
4
|
+
import { type Logger } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Minimal response writer interface.
|
|
7
|
+
*/
|
|
8
|
+
export interface ResponseWriter {
|
|
9
|
+
writeHead(status: number, headers?: Record<string, string>): void;
|
|
10
|
+
end(body?: string): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Send a JSON response.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendJsonResponse(res: ResponseWriter, status: number, body?: unknown): void;
|
|
16
|
+
/**
|
|
17
|
+
* Send an error response in OpenAI-compatible format.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sendError(res: ResponseWriter, status: number, code: string, message: string, type?: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Handle an error and send appropriate response.
|
|
22
|
+
* Supports ProxyError subclasses and standard Errors.
|
|
23
|
+
*/
|
|
24
|
+
export declare function handleError(res: ResponseWriter, error: unknown, logger?: Logger): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response utilities and error handler for proxy.
|
|
3
|
+
*/
|
|
4
|
+
import { defaultLogger } from "./types.js";
|
|
5
|
+
import { ProxyError } from "./errors.js";
|
|
6
|
+
/**
|
|
7
|
+
* Send a JSON response.
|
|
8
|
+
*/
|
|
9
|
+
export function sendJsonResponse(res, status, body) {
|
|
10
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
11
|
+
res.end(JSON.stringify(body ?? {}));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Send an error response in OpenAI-compatible format.
|
|
15
|
+
*/
|
|
16
|
+
export function sendError(res, status, code, message, type) {
|
|
17
|
+
const error = { code, message };
|
|
18
|
+
if (type)
|
|
19
|
+
error.type = type;
|
|
20
|
+
sendJsonResponse(res, status, { error });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Handle an error and send appropriate response.
|
|
24
|
+
* Supports ProxyError subclasses and standard Errors.
|
|
25
|
+
*/
|
|
26
|
+
export function handleError(res, error, logger = defaultLogger) {
|
|
27
|
+
// Handle ProxyError (and subclasses)
|
|
28
|
+
if (error instanceof ProxyError) {
|
|
29
|
+
logger.error("Proxy error:", error);
|
|
30
|
+
sendJsonResponse(res, error.httpStatus, error.toJSON());
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Handle standard Error
|
|
34
|
+
if (error instanceof Error) {
|
|
35
|
+
logger.error("Proxy error:", error);
|
|
36
|
+
// Special case for body too large
|
|
37
|
+
if (error.message === "Request body too large") {
|
|
38
|
+
sendError(res, 413, "payload_too_large", "Request body too large");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Generic internal error
|
|
42
|
+
sendError(res, 500, "proxy_error", "Internal proxy error");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Handle unknown error types
|
|
46
|
+
logger.error("Proxy error:", error);
|
|
47
|
+
sendError(res, 500, "proxy_error", "Internal proxy error");
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE stream parser that extracts cashu-change events.
|
|
3
|
+
*
|
|
4
|
+
* The Gate emits change tokens via `event: cashu-change` SSE events
|
|
5
|
+
* during streaming responses. This parser intercepts those events,
|
|
6
|
+
* extracts the token, and filters them out so they are not forwarded
|
|
7
|
+
* to the AI tool.
|
|
8
|
+
*/
|
|
9
|
+
export interface SSEFilterResult {
|
|
10
|
+
/** The filtered stream with cashu-change events removed. */
|
|
11
|
+
filtered: ReadableStream<Uint8Array>;
|
|
12
|
+
/** Returns the extracted change token (available after stream is consumed). */
|
|
13
|
+
changeToken: () => string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a TransformStream that filters out `event: cashu-change` SSE events
|
|
17
|
+
* and captures the token data.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractCashuChangeFromSSE(input: ReadableStream<Uint8Array>): SSEFilterResult;
|