clawmoney 0.12.0 → 0.12.2

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.
@@ -4,7 +4,9 @@ import { homedir } from "node:os";
4
4
  import YAML from "yaml";
5
5
  import { RelayWsClient } from "./ws-client.js";
6
6
  import { spawnCli, buildCliArgs, parseCliOutput, ensureEmptyMcpConfig, ensureSandboxDir, } from "./executor.js";
7
- import { callClaudeApi, preflightClaudeApi } from "./upstream/claude-api.js";
7
+ import { callClaudeApi, preflightClaudeApi, getRateGuardSnapshot } from "./upstream/claude-api.js";
8
+ import { callCodexApi, preflightCodexApi } from "./upstream/codex-api.js";
9
+ import { callGeminiApi, preflightGeminiApi } from "./upstream/gemini-api.js";
8
10
  import { calculateCost } from "./pricing.js";
9
11
  import { relayLogger as logger } from "./logger.js";
10
12
  const CONFIG_DIR = join(homedir(), ".clawmoney");
@@ -106,8 +108,10 @@ async function executeRelayRequest(request, config) {
106
108
  const model = request.model ?? config.relay.model;
107
109
  const stateful = request.stateful ?? false;
108
110
  const cliSessionId = request.cli_session_id ?? undefined;
109
- // api mode is currently claude-only; everything else falls back to spawn CLI.
110
- const useApiMode = config.relay.execution_mode === "api" && cliType === "claude";
111
+ // api mode is supported for claude / codex / gemini; anything else falls
112
+ // back to spawning the local CLI subprocess.
113
+ const useApiMode = config.relay.execution_mode === "api" &&
114
+ (cliType === "claude" || cliType === "codex" || cliType === "gemini");
111
115
  // Build prompt from messages
112
116
  const prompt = request.messages
113
117
  ? messagesToPrompt(request.messages)
@@ -130,13 +134,32 @@ async function executeRelayRequest(request, config) {
130
134
  const startMs = Date.now();
131
135
  let parsed;
132
136
  if (useApiMode) {
133
- // Direct /v1/messages call — no subprocess, no sandbox needed because
134
- // the only thing the upstream sees is the prompt text we pass in.
135
- parsed = await callClaudeApi({
136
- prompt,
137
- model,
138
- maxTokens: max_budget_usd ? undefined : 4096,
139
- });
137
+ // Direct upstream HTTPS call — no subprocess, no sandbox needed because
138
+ // the only thing the upstream sees is the prompt text we pass in. The
139
+ // right handler is picked by cli_type (claude → Anthropic, codex →
140
+ // chatgpt.com, gemini → cloudcode-pa). Each handler has its own
141
+ // fingerprint file and rate-guard instance.
142
+ if (cliType === "codex") {
143
+ parsed = await callCodexApi({
144
+ prompt,
145
+ model,
146
+ maxTokens: max_budget_usd ? undefined : 4096,
147
+ });
148
+ }
149
+ else if (cliType === "gemini") {
150
+ parsed = await callGeminiApi({
151
+ prompt,
152
+ model,
153
+ maxTokens: max_budget_usd ? undefined : 8192,
154
+ });
155
+ }
156
+ else {
157
+ parsed = await callClaudeApi({
158
+ prompt,
159
+ model,
160
+ maxTokens: max_budget_usd ? undefined : 4096,
161
+ });
162
+ }
140
163
  }
141
164
  else {
142
165
  // In stateful mode, pass cli_session_id so buildCliArgs adds --resume
@@ -159,6 +182,22 @@ async function executeRelayRequest(request, config) {
159
182
  logger.info(` │ Cost: input=$${cost.inputCost.toFixed(4)} cache_w=$${cost.cacheCreationCost.toFixed(4)} cache_r=$${cost.cacheReadCost.toFixed(4)} output=$${cost.outputCost.toFixed(4)}`);
160
183
  logger.info(` │ Total: API $${cost.apiCost.toFixed(4)} → Relay $${cost.relayCost.toFixed(4)} → Earn $${cost.providerEarn.toFixed(4)}`);
161
184
  logger.info(` └─ Done`);
185
+ // When we're running in api mode, piggy-back the provider's current 5h
186
+ // session-window snapshot onto the response so the Hub can use it for
187
+ // predictive claim scheduling (avoid routing fresh work to a provider
188
+ // whose window is already 90%+ saturated). Only populated if upstream
189
+ // actually surfaced the headers this turn.
190
+ let sessionWindowTelemetry;
191
+ if (useApiMode) {
192
+ const snap = getRateGuardSnapshot();
193
+ if (snap?.sessionWindow) {
194
+ sessionWindowTelemetry = {
195
+ reset_at_ms: snap.sessionWindow.endMs,
196
+ utilization: snap.sessionWindow.utilization,
197
+ status: snap.sessionWindow.status,
198
+ };
199
+ }
200
+ }
162
201
  return {
163
202
  event: "relay_response",
164
203
  request_id,
@@ -167,6 +206,7 @@ async function executeRelayRequest(request, config) {
167
206
  usage: parsed.usage,
168
207
  model_used: parsed.model || model,
169
208
  cost_usd: parsed.costUsd || undefined,
209
+ session_window: sessionWindowTelemetry,
170
210
  };
171
211
  }
172
212
  catch (err) {
@@ -192,12 +232,23 @@ export function runRelayProvider(cliOverride) {
192
232
  ensureEmptyMcpConfig();
193
233
  ensureSandboxDir();
194
234
  // If the operator picked api mode, validate the OAuth token + fingerprint
195
- // up-front so we fail fast instead of on the first inbound request.
196
- if (config.relay.execution_mode === "api" && config.relay.cli_type === "claude") {
197
- preflightClaudeApi(config.relay.rate_guard).catch((err) => {
198
- logger.error(`Claude API preflight failed — falling back to CLI mode: ${err.message}`);
199
- config.relay.execution_mode = "cli";
200
- });
235
+ // up-front so we fail fast instead of on the first inbound request. Each
236
+ // cli_type has its own preflight path (different credential file, different
237
+ // fingerprint schema, different rate-guard instance).
238
+ if (config.relay.execution_mode === "api") {
239
+ const preflightFn = config.relay.cli_type === "codex"
240
+ ? preflightCodexApi
241
+ : config.relay.cli_type === "gemini"
242
+ ? preflightGeminiApi
243
+ : config.relay.cli_type === "claude"
244
+ ? preflightClaudeApi
245
+ : null;
246
+ if (preflightFn) {
247
+ preflightFn(config.relay.rate_guard).catch((err) => {
248
+ logger.error(`${config.relay.cli_type} API preflight failed — falling back to CLI mode: ${err.message}`);
249
+ config.relay.execution_mode = "cli";
250
+ });
251
+ }
201
252
  }
202
253
  const activeTasks = new Set();
203
254
  // Create WS client
@@ -24,6 +24,11 @@ export interface RelayErrorEvent {
24
24
  message: string;
25
25
  }
26
26
  export type RelayIncomingEvent = RelayRequest | RelayConnectedEvent | RelayErrorEvent;
27
+ export interface RelayResponseSessionWindow {
28
+ reset_at_ms: number;
29
+ utilization?: number;
30
+ status?: string;
31
+ }
27
32
  export interface RelayResponse {
28
33
  event: "relay_response";
29
34
  request_id: string;
@@ -39,6 +44,7 @@ export interface RelayResponse {
39
44
  model_used?: string;
40
45
  cost_usd?: number;
41
46
  error?: string;
47
+ session_window?: RelayResponseSessionWindow;
42
48
  }
43
49
  export type RelayOutgoingEvent = RelayResponse;
44
50
  export interface ParsedOutput {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Direct chatgpt.com upstream for Codex (ChatGPT Plus/Pro) OAuth subscriptions.
3
+ *
4
+ * Mirrors claude-api.ts structure exactly: same export shape, same error types,
5
+ * same RateGuard integration, same OAuth refresh + persist-back pattern, same
6
+ * fingerprint file loading, same 5xx retry path, same preflight function.
7
+ *
8
+ * IMPORTANT — wire format: codex-cli 0.118+ migrated from HTTP POST+SSE to a
9
+ * WebSocket-based Responses API. The endpoint is accessed as
10
+ * wss://chatgpt.com/backend-api/codex/responses
11
+ * with the handshake headers shown below, and after the upgrade the client
12
+ * sends a single `{type:"response.create", ...}` JSON frame. The server
13
+ * replies with a stream of JSON frames that mirror the old SSE event names
14
+ * (`response.created`, `response.output_text.delta`, `response.completed`,
15
+ * `response.failed`, `response.error`, etc.). We accumulate text deltas +
16
+ * the terminal event, close cleanly, and return ParsedOutput — exactly the
17
+ * same contract the caller sees for HTTP Claude.
18
+ *
19
+ * Key differences from claude-api.ts:
20
+ * - Token source: ~/.codex/auth.json (written by the Codex CLI)
21
+ * - Upstream transport: WebSocket to chatgpt.com/backend-api/codex/responses
22
+ * - Handshake header `openai-beta: responses_websockets=2026-02-06`
23
+ * - Handshake header `version: <codex cli version>`
24
+ * - Handshake header `chatgpt-account-id` from ~/.codex/auth.json tokens.account_id
25
+ * - First frame is a JSON `response.create` — request body is OpenAI Responses
26
+ * API shape (input[], instructions, model, store, stream) with `type` added
27
+ * - Session headers: session_id + conversation_id (not x-claude-code-session-id)
28
+ * - Rate-limit headers surface on the upgrade response or via `rate_limits` /
29
+ * `response.failed` frames — we parse both
30
+ */
31
+ import type { ParsedOutput, RelayRateGuardConfig } from "../types.js";
32
+ import { RateGuard, RateGuardBudgetExceededError, RateGuardCooldownError } from "./rate-guard.js";
33
+ export { RateGuardBudgetExceededError, RateGuardCooldownError };
34
+ export declare function configureRateGuard(config?: RelayRateGuardConfig): void;
35
+ export declare function getRateGuardSnapshot(): ReturnType<RateGuard["currentLoad"]> | null;
36
+ export declare function preflightCodexApi(config?: RelayRateGuardConfig): Promise<void>;
37
+ export interface CallCodexApiOptions {
38
+ prompt: string;
39
+ model: string;
40
+ maxTokens?: number;
41
+ }
42
+ export declare function callCodexApi(opts: CallCodexApiOptions): Promise<ParsedOutput>;