clawmoney 0.17.2 → 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.
- package/dist/commands/relay-setup.js +108 -9
- package/dist/commands/relay.d.ts +3 -0
- package/dist/commands/relay.js +78 -0
- package/dist/index.js +14 -1
- package/dist/relay/pricing.js +37 -0
- package/dist/relay/provider.js +85 -0
- package/dist/relay/upstream/claude-api.js +73 -25
- package/dist/relay/upstream/codex-api.js +83 -30
- package/dist/relay/upstream/gemini-api.js +55 -12
- package/dist/relay/upstream/minimax-api.d.ts +38 -0
- package/dist/relay/upstream/minimax-api.js +325 -0
- package/dist/relay/upstream/openclaw-creds.d.ts +135 -0
- package/dist/relay/upstream/openclaw-creds.js +227 -0
- package/dist/relay/upstream/passthrough-api.d.ts +73 -0
- package/dist/relay/upstream/passthrough-api.js +228 -0
- package/dist/relay/upstream/passthrough-specs.d.ts +31 -0
- package/dist/relay/upstream/passthrough-specs.js +151 -0
- package/dist/wallet/x402-client.js +12 -6
- package/package.json +1 -1
- package/scripts/mock-openai-upstream.mjs +150 -0
- package/scripts/probe-relay-call.mjs +289 -0
|
@@ -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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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);
|
package/dist/commands/relay.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/commands/relay.js
CHANGED
|
@@ -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')
|
package/dist/relay/pricing.js
CHANGED
|
@@ -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 },
|
package/dist/relay/provider.js
CHANGED
|
@@ -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 (
|
|
484
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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(
|
|
603
|
+
logger.info(`[claude-api] refreshing OAuth token (source=${current.source})...`);
|
|
583
604
|
const fresh = await refreshUpstreamToken(current.refreshToken);
|
|
584
|
-
|
|
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);
|