clawmoney 0.17.3 → 0.17.4

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.
@@ -12,6 +12,8 @@ import { API_PRICES, PLATFORM_FEE } from "../relay/pricing.js";
12
12
  import { hasClaudeFingerprint, bootstrapClaudeFingerprint, } from "../relay/upstream/claude-bootstrap.js";
13
13
  import { hasGeminiFingerprint, bootstrapGeminiFingerprint, } from "../relay/upstream/gemini-bootstrap.js";
14
14
  import { hasCodexFingerprint, bootstrapCodexFingerprint, } from "../relay/upstream/codex-bootstrap.js";
15
+ import { listOpenclawOAuthProviders, listOpenclawApiKeyProviders, } from "../relay/upstream/openclaw-creds.js";
16
+ import { hubCliTypeFor } from "../relay/upstream/passthrough-specs.js";
15
17
  // ── Per-cli_type model catalogs ──
16
18
  //
17
19
  // `RECOMMENDED_MODELS` is what gets registered when the user picks "all
@@ -74,6 +76,23 @@ const RECOMMENDED_MODELS = {
74
76
  "antigravity-gemini-3-flash",
75
77
  "antigravity-gemini-2.5-pro",
76
78
  ],
79
+ // ── Z.AI / GLM ──
80
+ // One cli_type per openclaw onboarding choice. Coding-plan variants share
81
+ // the same recommended catalog — the cli_type distinguishes the upstream
82
+ // baseUrl at call time, not the model id.
83
+ "zai-coding": ["glm-5", "glm-4.7", "glm-4.7-flash", "glm-4.5-air"],
84
+ zai: ["glm-5", "glm-4.7", "glm-4.7-flash", "glm-4.5-air"],
85
+ // ── Moonshot / Kimi ──
86
+ moonshot: ["kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo"],
87
+ "kimi-coding": ["kimi-code"],
88
+ // ── Qwen Coding Plan ──
89
+ "qwen-coding": ["qwen3.6-plus", "qwen-coder-plus", "qwen3-coder"],
90
+ // ── MiniMax ──
91
+ minimax: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"],
92
+ // ── OpenAI API-key (distinct from "codex" subscription adapter) ──
93
+ // Uses the buyer's own API key; same model catalog as codex Coding CLI
94
+ // plus the o-series reasoning models that codex can't serve.
95
+ openai: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "o4-mini"],
77
96
  };
78
97
  function modelsForCli(cli) {
79
98
  const all = Object.keys(API_PRICES);
@@ -95,6 +114,25 @@ function modelsForCli(cli) {
95
114
  // the antigravity cli_type, not the standalone gemini cli_type.
96
115
  return all.filter((m) => m.startsWith("gemini-") && !m.startsWith("antigravity-"));
97
116
  }
117
+ if (cli === "zai-coding" || cli === "zai") {
118
+ return all.filter((m) => m.startsWith("glm-"));
119
+ }
120
+ if (cli === "moonshot") {
121
+ return all.filter((m) => m.startsWith("kimi-k2"));
122
+ }
123
+ if (cli === "kimi-coding") {
124
+ return ["kimi-code"].filter((m) => m in API_PRICES);
125
+ }
126
+ if (cli === "qwen-coding") {
127
+ return all.filter((m) => m.startsWith("qwen"));
128
+ }
129
+ if (cli === "minimax") {
130
+ return all.filter((m) => m.startsWith("MiniMax-"));
131
+ }
132
+ if (cli === "openai") {
133
+ // OpenAI API-key passthrough — gpt-5.x + o-series reasoning models.
134
+ return all.filter((m) => m.startsWith("gpt-") || m === "o3" || m === "o4-mini");
135
+ }
98
136
  return [];
99
137
  }
100
138
  function detectInstalledClis() {
@@ -103,6 +141,17 @@ function detectInstalledClis() {
103
141
  // validate OAuth state here — the daemon's preflight does that on
104
142
  // first start, and probing OAuth from a sync setup wizard would be
105
143
  // brittle (keychain prompts, refresh-token races, etc).
144
+ // Map clawmoney cli_type → openclaw provider id. Used so a machine with
145
+ // only openclaw installed still surfaces the relevant subscriptions via
146
+ // ~/.openclaw/agents/*/agent/auth-profiles.json instead of requiring the
147
+ // underlying official CLI binary. The adapters (claude-api / codex-api /
148
+ // gemini-api) transparently fall back to those profiles at runtime.
149
+ const openclawProviders = new Set(listOpenclawOAuthProviders());
150
+ const openclawProviderFor = {
151
+ claude: "anthropic",
152
+ codex: "openai-codex",
153
+ gemini: "google",
154
+ };
106
155
  const binaries = [
107
156
  { cli: "claude", bin: "claude" },
108
157
  { cli: "codex", bin: "codex" },
@@ -117,14 +166,58 @@ function detectInstalledClis() {
117
166
  catch {
118
167
  installed = false;
119
168
  }
120
- results.push({
121
- cli,
122
- available: installed,
123
- hint: installed
124
- ? "binary in PATH (login state will be validated when daemon starts)"
125
- : `${bin} not found in PATH`,
126
- });
169
+ const hasOpenclawProfile = openclawProviders.has(openclawProviderFor[cli]);
170
+ const available = installed || hasOpenclawProfile;
171
+ let hint;
172
+ if (installed) {
173
+ hint = "binary in PATH (login state will be validated when daemon starts)";
174
+ }
175
+ else if (hasOpenclawProfile) {
176
+ hint = "OpenClaw OAuth profile detected (no official CLI binary needed)";
177
+ }
178
+ else {
179
+ hint = `${bin} not found in PATH`;
180
+ }
181
+ results.push({ cli, available, hint });
127
182
  }
183
+ // ── Static-key passthrough providers ──
184
+ // No binary to probe — each maps to an openclaw api_key profile or an
185
+ // env var. Pair of (provider-id-in-openclaw, env-var-name, cli_type).
186
+ const passthroughDetection = [
187
+ { cli: "zai-coding", openclawProvider: "zai", env: "ZAI_API_KEY" },
188
+ { cli: "zai", openclawProvider: "zai", env: "ZAI_API_KEY" },
189
+ { cli: "moonshot", openclawProvider: "moonshot", env: "MOONSHOT_API_KEY" },
190
+ { cli: "kimi-coding", openclawProvider: "kimi", env: "KIMI_API_KEY" },
191
+ { cli: "qwen-coding", openclawProvider: "qwen", env: "BAILIAN_CODING_PLAN_API_KEY" },
192
+ { cli: "openai", openclawProvider: "openai", env: "OPENAI_API_KEY" },
193
+ ];
194
+ const openclawApiKeyProviders = new Set(listOpenclawApiKeyProviders());
195
+ for (const { cli, openclawProvider, env } of passthroughDetection) {
196
+ const hasOpenclawKey = openclawApiKeyProviders.has(openclawProvider);
197
+ const hasEnv = !!process.env[env];
198
+ const available = hasOpenclawKey || hasEnv;
199
+ let hint;
200
+ if (hasOpenclawKey)
201
+ hint = `OpenClaw api_key profile (${openclawProvider})`;
202
+ else if (hasEnv)
203
+ hint = `${env} env var set`;
204
+ else
205
+ hint = `no key found (openclaw ${openclawProvider} profile or ${env})`;
206
+ results.push({ cli, available, hint });
207
+ }
208
+ // MiniMax: OAuth Coding Plan OR api_key fallback. List separately so the
209
+ // hint can explain which path was detected.
210
+ const hasMinimaxOauth = openclawProviders.has("minimax-portal");
211
+ const hasMinimaxKey = openclawApiKeyProviders.has("minimax") || !!process.env.MINIMAX_API_KEY;
212
+ results.push({
213
+ cli: "minimax",
214
+ available: hasMinimaxOauth || hasMinimaxKey,
215
+ hint: hasMinimaxOauth
216
+ ? "OpenClaw minimax-portal OAuth profile"
217
+ : hasMinimaxKey
218
+ ? "MiniMax api_key (openclaw or MINIMAX_API_KEY)"
219
+ : "no MiniMax credential (run `openclaw onboard --auth-choice minimax-global-oauth` or export MINIMAX_API_KEY)",
220
+ });
128
221
  // Antigravity is OAuth-file based — there's no `antigravity` binary
129
222
  // installed locally. We check for the OAuth credentials file that
130
223
  // `clawmoney antigravity login` writes.
@@ -431,9 +524,15 @@ export async function relaySetupCommand() {
431
524
  const failures = [];
432
525
  const regSpin = spinner();
433
526
  regSpin.start(`Registering ${registrations.length} providers...`);
527
+ // Hub only recognizes a closed set of cli_types (claude / codex / gemini /
528
+ // antigravity / api-key / session-token). Our fine-grained internal names
529
+ // (zai-coding / moonshot / qwen-coding / minimax / …) all fold to api-key
530
+ // on the wire — the daemon does the actual upstream routing by model
531
+ // prefix at request time. Preserve the fine-grained label only in the
532
+ // wizard UI for operator readability.
434
533
  const batchBody = {
435
534
  providers: registrations.map((r) => ({
436
- cli_type: r.cli,
535
+ cli_type: hubCliTypeFor(r.cli),
437
536
  model: r.model,
438
537
  mode: "chat",
439
538
  concurrency,
@@ -500,7 +599,7 @@ export async function relaySetupCommand() {
500
599
  try {
501
600
  const pruneResp = await apiPost("/api/v1/relay/providers/prune", {
502
601
  keep: registrations.map((r) => ({
503
- cli_type: r.cli,
602
+ cli_type: hubCliTypeFor(r.cli),
504
603
  model: r.model,
505
604
  })),
506
605
  }, config.api_key);
@@ -18,4 +18,7 @@ export declare function relayLogsCommand(options: {
18
18
  }): Promise<void>;
19
19
  export declare function relayStatusCommand(): Promise<void>;
20
20
  export declare function relayModelsCommand(): Promise<void>;
21
+ export declare function relayPreflightCommand(options: {
22
+ cli?: string;
23
+ }): Promise<void>;
21
24
  export {};
@@ -387,3 +387,81 @@ export async function relayModelsCommand() {
387
387
  throw err;
388
388
  }
389
389
  }
390
+ // ── relay preflight ──
391
+ //
392
+ // Standalone credential validation. Runs each upstream adapter's preflight
393
+ // function against the provider's local auth state (native CLI file,
394
+ // keychain, OpenClaw profile, or env var) WITHOUT starting the WebSocket
395
+ // daemon or contacting the Hub. Useful for operators who want to verify
396
+ // "my openclaw profile is being picked up" before committing to a real
397
+ // `relay start`.
398
+ //
399
+ // With --cli <type> we preflight just that one. Without, we loop over a
400
+ // sensible default set (claude / codex / gemini / api-key) and report
401
+ // each independently — a failure in one cli_type doesn't short-circuit
402
+ // the others.
403
+ const PREFLIGHT_DEFAULTS = ["claude", "codex", "gemini", "antigravity"];
404
+ export async function relayPreflightCommand(options) {
405
+ const toCheck = options.cli
406
+ ? [options.cli]
407
+ : PREFLIGHT_DEFAULTS;
408
+ console.log(chalk.bold("\n Relay credential preflight\n"));
409
+ let failed = 0;
410
+ for (const cli of toCheck) {
411
+ const spin = ora(` ${cli}`).start();
412
+ try {
413
+ const fn = await resolvePreflightFn(cli);
414
+ if (!fn) {
415
+ spin.info(chalk.dim(` ${cli}: no preflight (unknown cli_type)`));
416
+ continue;
417
+ }
418
+ await fn();
419
+ spin.succeed(chalk.green(` ${cli}: OK`));
420
+ }
421
+ catch (err) {
422
+ failed++;
423
+ spin.fail(chalk.red(` ${cli}: ${err.message.slice(0, 200)}`));
424
+ }
425
+ }
426
+ console.log("");
427
+ if (failed === 0) {
428
+ console.log(chalk.green(` All ${toCheck.length} preflight checks passed.`));
429
+ }
430
+ else {
431
+ console.log(chalk.yellow(` ${failed} of ${toCheck.length} preflight checks failed.`));
432
+ }
433
+ console.log("");
434
+ process.exit(failed === 0 ? 0 : 1);
435
+ }
436
+ async function resolvePreflightFn(cli) {
437
+ switch (cli) {
438
+ case "claude": {
439
+ const { preflightClaudeApi } = await import("../relay/upstream/claude-api.js");
440
+ return () => preflightClaudeApi();
441
+ }
442
+ case "codex": {
443
+ const { preflightCodexApi } = await import("../relay/upstream/codex-api.js");
444
+ return () => preflightCodexApi();
445
+ }
446
+ case "gemini": {
447
+ const { preflightGeminiApi } = await import("../relay/upstream/gemini-api.js");
448
+ return () => preflightGeminiApi();
449
+ }
450
+ case "antigravity": {
451
+ const { preflightAntigravityApi } = await import("../relay/upstream/antigravity-api.js");
452
+ return () => preflightAntigravityApi();
453
+ }
454
+ case "minimax": {
455
+ const { preflightMinimaxApi } = await import("../relay/upstream/minimax-api.js");
456
+ return () => preflightMinimaxApi();
457
+ }
458
+ default: {
459
+ // Passthrough cli_type (zai / moonshot / kimi-coding / qwen-coding / openai).
460
+ const { preflightPassthroughApi, getPassthroughSpec } = await import("../relay/upstream/passthrough-api.js");
461
+ await import("../relay/upstream/passthrough-specs.js");
462
+ if (!getPassthroughSpec(cli))
463
+ return null;
464
+ return () => preflightPassthroughApi(cli);
465
+ }
466
+ }
467
+ }
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { walletStatusCommand, walletBalanceCommand, walletAddressCommand, wallet
8
8
  import { tweetCommand } from './commands/tweet.js';
9
9
  import { gigCreateCommand, gigBrowseCommand, gigDetailCommand, gigAcceptCommand, gigDeliverCommand, gigApproveCommand, gigDisputeCommand, } from './commands/gig.js';
10
10
  import { hubStartCommand, hubStopCommand, hubStatusCommand, hubSearchCommand, hubCallCommand, hubRegisterCommand, hubSkillsCommand, hubOrderCommand, hubHistoryCommand, } from './commands/hub.js';
11
- import { relayRegisterCommand, relayStartCommand, relayStopCommand, relayStatusCommand, relayModelsCommand, relayLogsCommand, } from './commands/relay.js';
11
+ import { relayRegisterCommand, relayStartCommand, relayStopCommand, relayStatusCommand, relayModelsCommand, relayLogsCommand, relayPreflightCommand, } from './commands/relay.js';
12
12
  import { antigravityLoginCommand, antigravityStatusCommand, } from './commands/antigravity.js';
13
13
  import { createRequire } from 'node:module';
14
14
  const require = createRequire(import.meta.url);
@@ -549,6 +549,19 @@ relay
549
549
  process.exit(1);
550
550
  }
551
551
  });
552
+ relay
553
+ .command('preflight')
554
+ .description('Validate upstream credentials without starting the daemon (useful for verifying openclaw fallback, keychain state, etc.)')
555
+ .option('--cli <type>', 'Check a single cli_type (claude, codex, gemini, antigravity, minimax, zai, zai-coding, moonshot, kimi-coding, qwen-coding, openai). Default: claude+codex+gemini+antigravity.')
556
+ .action(async (options) => {
557
+ try {
558
+ await relayPreflightCommand(options);
559
+ }
560
+ catch (err) {
561
+ console.error(err.message);
562
+ process.exit(1);
563
+ }
564
+ });
552
565
  // antigravity (Google Antigravity IDE OAuth — separate quota pool + Claude access)
553
566
  const antigravity = program
554
567
  .command('antigravity')
@@ -59,6 +59,43 @@ export const API_PRICES = {
59
59
  "antigravity-claude-opus-4-6-thinking": { input: 5, output: 25 },
60
60
  "antigravity-claude-sonnet-4-6": { input: 3, output: 15 },
61
61
  "antigravity-claude-sonnet-4-5": { input: 3, output: 15 },
62
+ // ── Z.AI / GLM ──
63
+ // GLM models are paid per-token; Coding Plan is a flat monthly rate but
64
+ // we still bill relay by tokens so providers see their opportunity cost
65
+ // approximately. Numbers sourced from z.ai/pricing + LiteLLM; trimmed to
66
+ // the bundled openclaw catalog + widely-used legacy ids.
67
+ "glm-5.1": { input: 0.60, output: 3.00 },
68
+ "glm-5": { input: 0.60, output: 3.00 },
69
+ "glm-5-turbo": { input: 0.20, output: 1.00 },
70
+ "glm-4.7": { input: 0.60, output: 2.20 },
71
+ "glm-4.7-flash": { input: 0.10, output: 0.40 },
72
+ "glm-4.7-flashx": { input: 0.05, output: 0.20 },
73
+ "glm-4.6": { input: 0.60, output: 2.20 },
74
+ "glm-4.5": { input: 0.60, output: 2.20 },
75
+ "glm-4.5-air": { input: 0.20, output: 1.10 },
76
+ "glm-4.5-flash": { input: 0.10, output: 0.40 },
77
+ // ── Moonshot / Kimi K2 ──
78
+ // Moonshot public pricing. Kimi K2 family on Moonshot Open Platform.
79
+ "kimi-k2.5": { input: 0.60, output: 2.50 },
80
+ "kimi-k2-thinking": { input: 0.60, output: 2.50 },
81
+ "kimi-k2-thinking-turbo": { input: 1.15, output: 8.00 },
82
+ "kimi-k2-turbo": { input: 1.15, output: 5.00 },
83
+ // Kimi Coding product (separate key / endpoint from Moonshot API).
84
+ // Pricing here is placeholder — Kimi Coding is a subscription product,
85
+ // so there's no official token price; we use Kimi K2.5 Open Platform
86
+ // rates as a proxy so providers see comparable opportunity-cost billing.
87
+ "kimi-code": { input: 0.60, output: 2.50 },
88
+ // ── Qwen / Alibaba ModelStudio Coding Plan ──
89
+ // Coding Plan is flat monthly; same proxy-price approach as kimi-code.
90
+ // Based on public DashScope Qwen pricing (qwen-plus / qwen-max tier).
91
+ "qwen3.6-plus": { input: 1.20, output: 3.60 },
92
+ "qwen3.5-plus": { input: 0.80, output: 2.40 },
93
+ "qwen-coder-plus": { input: 0.80, output: 2.40 },
94
+ "qwen3-coder": { input: 0.80, output: 2.40 },
95
+ // ── MiniMax ──
96
+ // From openclaw provider catalog (docs/providers/minimax.md).
97
+ "MiniMax-M2.7": { input: 0.30, output: 1.20 },
98
+ "MiniMax-M2.7-highspeed": { input: 0.60, output: 2.40 },
62
99
  // ── Google (Gemini) ──
63
100
  // Verified against LiteLLM pricing DB.
64
101
  "gemini-3.1-pro-preview": { input: 2, output: 12 },
@@ -7,6 +7,11 @@ import { callClaudeApi, callClaudeApiPassthrough, preflightClaudeApi, getRateGua
7
7
  import { callCodexApi, callCodexApiPassthrough, preflightCodexApi, getRateGuardSnapshot as getCodexRateGuardSnapshot, } from "./upstream/codex-api.js";
8
8
  import { callGeminiApi, preflightGeminiApi, getGeminiRateGuardSnapshot, } from "./upstream/gemini-api.js";
9
9
  import { callAntigravityApi, preflightAntigravityApi, getAntigravityRateGuardSnapshot, } from "./upstream/antigravity-api.js";
10
+ import { callMinimaxApi, preflightMinimaxApi, getMinimaxRateGuardSnapshot, } from "./upstream/minimax-api.js";
11
+ import { callPassthroughApi, preflightPassthroughApi, getPassthroughRateGuardSnapshot, } from "./upstream/passthrough-api.js";
12
+ // Side-effect import: registers all static-key passthrough specs at module
13
+ // load time (zai, zai-coding, moonshot, kimi-coding, qwen-coding, openai).
14
+ import { PASSTHROUGH_CLI_TYPES, resolveSpecByModel, } from "./upstream/passthrough-specs.js";
10
15
  import { apiGet, apiPost } from "../utils/api.js";
11
16
  /**
12
17
  * Pick the rate-guard snapshot matching this request's cli_type. Fixes a
@@ -22,8 +27,23 @@ function getRateGuardSnapshotForCli(cli) {
22
27
  return getGeminiRateGuardSnapshot();
23
28
  case "antigravity":
24
29
  return getAntigravityRateGuardSnapshot();
30
+ case "minimax":
31
+ return getMinimaxRateGuardSnapshot();
32
+ case "api-key":
33
+ // api-key multiplexes multiple internal specs; without model context
34
+ // we can't pick one snapshot. Hub treats null as "no signal", which
35
+ // is accurate here — each internal spec has its own per-cli rate-guard
36
+ // state, none of which is canonical for the whole api-key bucket.
37
+ return null;
25
38
  case "claude":
39
+ return getClaudeRateGuardSnapshot();
26
40
  default:
41
+ // Passthrough cli_types share the generic rate-guard shape but track
42
+ // per-cli load independently. Returns null for unknown cli_types,
43
+ // which is fine — the Hub treats missing snapshots as "no signal".
44
+ if (PASSTHROUGH_CLI_TYPES.has(cli)) {
45
+ return getPassthroughRateGuardSnapshot(cli);
46
+ }
27
47
  return getClaudeRateGuardSnapshot();
28
48
  }
29
49
  }
@@ -324,6 +344,59 @@ async function executeRelayRequest(request, config, sendChunk) {
324
344
  maxTokens: max_budget_usd ? undefined : 8192,
325
345
  });
326
346
  }
347
+ else if (cliType === "api-key") {
348
+ // Canonical Hub cli_type for every static-key / Bearer-passthrough
349
+ // upstream. The Hub doesn't know (or care) which third-party provider
350
+ // is on the other side — it just sees "OpenAI-compat, uses a Bearer
351
+ // token". Daemon resolves the actual upstream by model prefix.
352
+ const internalSpec = resolveSpecByModel(model);
353
+ if (!internalSpec) {
354
+ throw new Error(`api-key dispatch failed: model "${model}" matches no known passthrough family ` +
355
+ `(supported prefixes: glm-*, zai-*, kimi-k2*, kimi-code, qwen*, MiniMax-*, gpt-*, o3, o4-mini)`);
356
+ }
357
+ if (internalSpec === "minimax") {
358
+ parsed = await callMinimaxApi({
359
+ prompt,
360
+ passthroughBody: request.passthrough_body,
361
+ model,
362
+ maxTokens: max_budget_usd ? undefined : 8192,
363
+ onRawEvent: sendChunk,
364
+ });
365
+ }
366
+ else {
367
+ parsed = await callPassthroughApi({
368
+ cliType: internalSpec,
369
+ prompt,
370
+ passthroughBody: request.passthrough_body,
371
+ model,
372
+ maxTokens: max_budget_usd ? undefined : 4096,
373
+ onRawEvent: sendChunk,
374
+ });
375
+ }
376
+ }
377
+ else if (cliType === "minimax") {
378
+ // Legacy fine-grained cli_type kept for the probe harness; Hub never
379
+ // sends this value in production.
380
+ parsed = await callMinimaxApi({
381
+ prompt,
382
+ passthroughBody: request.passthrough_body,
383
+ model,
384
+ maxTokens: max_budget_usd ? undefined : 8192,
385
+ onRawEvent: sendChunk,
386
+ });
387
+ }
388
+ else if (PASSTHROUGH_CLI_TYPES.has(cliType)) {
389
+ // Same story — fine-grained cli_type path retained so local probe
390
+ // scripts can target a specific spec without faking the Hub side.
391
+ parsed = await callPassthroughApi({
392
+ cliType,
393
+ prompt,
394
+ passthroughBody: request.passthrough_body,
395
+ model,
396
+ maxTokens: max_budget_usd ? undefined : 4096,
397
+ onRawEvent: sendChunk,
398
+ });
399
+ }
327
400
  else {
328
401
  // Claude: two modes.
329
402
  //
@@ -440,7 +513,19 @@ function getPreflightFn(cliType) {
440
513
  return preflightGeminiApi;
441
514
  case "antigravity":
442
515
  return preflightAntigravityApi;
516
+ case "minimax":
517
+ return preflightMinimaxApi;
518
+ case "api-key":
519
+ // Credential validation for api-key happens lazily on first request —
520
+ // we can't know which internal specs to preflight without the list of
521
+ // registered models, and the adapters throw clear errors on missing
522
+ // creds anyway. Return a trivial resolved preflight so the daemon
523
+ // launcher doesn't log a "no preflight registered" warning.
524
+ return async () => undefined;
443
525
  default:
526
+ if (PASSTHROUGH_CLI_TYPES.has(cliType)) {
527
+ return (config) => preflightPassthroughApi(cliType, config);
528
+ }
444
529
  return null;
445
530
  }
446
531
  }
@@ -26,6 +26,7 @@ import { ProxyAgent, setGlobalDispatcher } from "undici";
26
26
  import { relayLogger as logger } from "../logger.js";
27
27
  import { RateGuard, RateGuardBudgetExceededError, RateGuardCooldownError, } from "./rate-guard.js";
28
28
  import { calculateCost } from "../pricing.js";
29
+ import { readOpenclawOAuthProfile, persistOpenclawOAuthProfile, } from "./openclaw-creds.js";
29
30
  export { RateGuardBudgetExceededError, RateGuardCooldownError };
30
31
  // ── Constants (sourced from sub2api + claude-cli/2.1.100 capture) ──
31
32
  const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
@@ -480,24 +481,44 @@ function loadClaudeOAuth() {
480
481
  const fromKeychain = readCredentialsFromKeychain();
481
482
  const fromFile = fromKeychain ? null : readCredentialsFromFile();
482
483
  const raw = fromKeychain ?? fromFile;
483
- if (!raw) {
484
- throw new Error("Claude Code credentials not found. Log in with `claude` first.");
484
+ if (raw) {
485
+ const oauth = raw.claudeAiOauth;
486
+ if (!oauth?.accessToken) {
487
+ throw new Error("Credentials file missing claudeAiOauth.accessToken");
488
+ }
489
+ return {
490
+ source: fromKeychain ? "keychain" : "file",
491
+ filePath: fromKeychain ? undefined : CLAUDE_CREDENTIALS_FILE_PATH,
492
+ accessToken: oauth.accessToken,
493
+ refreshToken: oauth.refreshToken,
494
+ expiresAt: oauth.expiresAt,
495
+ scopes: oauth.scopes ?? [],
496
+ subscriptionType: oauth.subscriptionType,
497
+ rateLimitTier: oauth.rateLimitTier,
498
+ _rawWrapper: raw,
499
+ };
485
500
  }
486
- const oauth = raw.claudeAiOauth;
487
- if (!oauth?.accessToken) {
488
- throw new Error("Credentials file missing claudeAiOauth.accessToken");
501
+ // Fallback: openclaw's auth-profiles.json. Anthropic subscription OAuth
502
+ // stored under provider="anthropic". openclaw does not record scopes /
503
+ // subscriptionType / rateLimitTier, so those stay undefined — preflight
504
+ // logs will show "subscription=? tier=?", which is accurate ("we don't
505
+ // know"). Refresh responses from Anthropic's OAuth endpoint echo the
506
+ // scopes array back, so the field gets populated on the first refresh.
507
+ const openclawProfile = readOpenclawOAuthProfile("anthropic");
508
+ if (openclawProfile) {
509
+ logger.info(`[claude-api] using OpenClaw credential fallback (profile=${openclawProfile.profileKey}, store=${openclawProfile.storePath})`);
510
+ return {
511
+ source: "openclaw",
512
+ openclawProfile,
513
+ accessToken: openclawProfile.access,
514
+ refreshToken: openclawProfile.refresh,
515
+ expiresAt: openclawProfile.expires,
516
+ scopes: [],
517
+ };
489
518
  }
490
- return {
491
- source: fromKeychain ? "keychain" : "file",
492
- filePath: fromKeychain ? undefined : CLAUDE_CREDENTIALS_FILE_PATH,
493
- accessToken: oauth.accessToken,
494
- refreshToken: oauth.refreshToken,
495
- expiresAt: oauth.expiresAt,
496
- scopes: oauth.scopes ?? [],
497
- subscriptionType: oauth.subscriptionType,
498
- rateLimitTier: oauth.rateLimitTier,
499
- _rawWrapper: raw,
500
- };
519
+ throw new Error("Claude Code credentials not found (checked keychain, ~/.claude/.credentials.json, " +
520
+ "and ~/.openclaw/agents/*/agent/auth-profiles.json). " +
521
+ "Log in with `claude` or `openclaw onboard` first.");
501
522
  }
502
523
  function writeCredentialsToKeychain(wrapper) {
503
524
  if (process.platform !== "darwin") {
@@ -579,9 +600,43 @@ function isAuthBrokenError(err) {
579
600
  /token refresh failed:\s*40[0134]/.test(msg));
580
601
  }
581
602
  async function doRefreshAndPersist(current) {
582
- logger.info("[claude-api] refreshing OAuth token...");
603
+ logger.info(`[claude-api] refreshing OAuth token (source=${current.source})...`);
583
604
  const fresh = await refreshUpstreamToken(current.refreshToken);
584
- const wrapper = { ...current._rawWrapper };
605
+ // IMPORTANT: persist BEFORE advancing the in-memory state. If the keychain
606
+ // write silently fails we must NOT start using the new access/refresh token
607
+ // — doing so creates a "two valid tokens in flight" pattern that looks to
608
+ // Anthropic like account hijacking (same account_id, two access_tokens
609
+ // issued within the 3-minute refresh skew window). The correct fallback is
610
+ // to keep serving on the old token until the next refresh cycle retries
611
+ // the persist, so on-disk and in-memory state always agree.
612
+ if (current.source === "openclaw" && current.openclawProfile) {
613
+ try {
614
+ persistOpenclawOAuthProfile(current.openclawProfile, {
615
+ access: fresh.accessToken,
616
+ refresh: fresh.refreshToken,
617
+ expires: fresh.expiresAt,
618
+ });
619
+ logger.info(`[claude-api] OpenClaw profile ${current.openclawProfile.profileKey} updated (${current.openclawProfile.storePath})`);
620
+ }
621
+ catch (err) {
622
+ logger.error(`[claude-api] CRITICAL: openclaw persist failed — keeping old token to avoid account-hijack detection signal: ${err.message}`);
623
+ return current;
624
+ }
625
+ return {
626
+ ...current,
627
+ accessToken: fresh.accessToken,
628
+ refreshToken: fresh.refreshToken,
629
+ expiresAt: fresh.expiresAt,
630
+ scopes: fresh.scopes.length > 0 ? fresh.scopes : current.scopes,
631
+ openclawProfile: {
632
+ ...current.openclawProfile,
633
+ access: fresh.accessToken,
634
+ refresh: fresh.refreshToken,
635
+ expires: fresh.expiresAt,
636
+ },
637
+ };
638
+ }
639
+ const wrapper = { ...(current._rawWrapper ?? {}) };
585
640
  wrapper.claudeAiOauth = {
586
641
  ...wrapper.claudeAiOauth,
587
642
  accessToken: fresh.accessToken,
@@ -591,13 +646,6 @@ async function doRefreshAndPersist(current) {
591
646
  ? fresh.scopes
592
647
  : wrapper.claudeAiOauth.scopes,
593
648
  };
594
- // IMPORTANT: persist BEFORE advancing the in-memory state. If the keychain
595
- // write silently fails we must NOT start using the new access/refresh token
596
- // — doing so creates a "two valid tokens in flight" pattern that looks to
597
- // Anthropic like account hijacking (same account_id, two access_tokens
598
- // issued within the 3-minute refresh skew window). The correct fallback is
599
- // to keep serving on the old token until the next refresh cycle retries
600
- // the persist, so on-disk and in-memory state always agree.
601
649
  if (current.source === "keychain") {
602
650
  try {
603
651
  writeCredentialsToKeychain(wrapper);