Compare commits

...
6 Commits
10 changed files with 164 additions and 18 deletions
+6 -2
View File
@@ -1,6 +1,10 @@
import { getProviderConfig } from '../utils/provider-registry';
import { getOpenClawProviderKeyForType, isOAuthProviderType } from '../utils/provider-keys';
import type { ProviderConfig } from '../utils/secure-storage';
import {
piAiModelsJsonModelEntry,
type PiAiModelCostRates,
} from '../shared/pi-ai-model-cost';
export interface AgentProviderUpdatePayload {
providerKey: string;
@@ -8,7 +12,7 @@ export interface AgentProviderUpdatePayload {
baseUrl: string;
api: string;
apiKey: string | undefined;
models: Array<{ id: string; name: string }>;
models: Array<{ id: string; name: string; cost: PiAiModelCostRates }>;
};
}
@@ -42,7 +46,7 @@ export function buildNonOAuthAgentProviderUpdate(
baseUrl,
api,
apiKey: meta?.apiKeyEnv,
models: modelId ? [{ id: modelId, name: modelId }] : [],
models: modelId ? [piAiModelsJsonModelEntry(modelId)] : [],
},
};
}
@@ -15,6 +15,10 @@ import {
updateAgentModelProvider,
updateSingleAgentModelProvider,
} from '../../utils/openclaw-auth';
import {
piAiModelsJsonModelEntry,
type PiAiModelCostRates,
} from '../../shared/pi-ai-model-cost';
import { logger } from '../../utils/logger';
import { listAgentsSnapshot } from '../../utils/agent-config';
@@ -342,7 +346,7 @@ async function syncCustomProviderAgentModel(
await updateAgentModelProvider(runtimeProviderKey, {
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl, config.apiProtocol || 'openai-completions'),
api: config.apiProtocol || 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [],
models: modelId ? [piAiModelsJsonModelEntry(modelId)] : [],
apiKey: resolvedKey,
});
}
@@ -411,7 +415,7 @@ async function buildAgentModelProviderEntry(
): Promise<{
baseUrl?: string;
api?: string;
models?: Array<{ id: string; name: string }>;
models?: Array<{ id: string; name: string; cost: PiAiModelCostRates }>;
apiKey?: string;
authHeader?: boolean;
} | null> {
@@ -440,7 +444,7 @@ async function buildAgentModelProviderEntry(
return {
baseUrl,
api,
models: [{ id: modelId, name: modelId }],
models: [piAiModelsJsonModelEntry(modelId)],
apiKey,
authHeader,
};
@@ -695,7 +699,7 @@ export async function syncDefaultProviderToRuntime(
api,
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
apiKey: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
models: defaultModelId ? [{ id: defaultModelId, name: defaultModelId }] : [],
models: defaultModelId ? [piAiModelsJsonModelEntry(defaultModelId)] : [],
});
} catch (err) {
logger.warn(`Failed to update models.json for OAuth provider "${targetProviderKey}":`, err);
@@ -711,7 +715,7 @@ export async function syncDefaultProviderToRuntime(
await updateAgentModelProvider(ock, {
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl, provider.apiProtocol || 'openai-completions'),
api: provider.apiProtocol || 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [],
models: modelId ? [piAiModelsJsonModelEntry(modelId)] : [],
apiKey: providerKey,
});
}
+43
View File
@@ -0,0 +1,43 @@
/**
* Per-million-token rates expected by `@mariozechner/pi-ai` `calculateCost`.
* Custom / synced catalog rows often omit pricing; zeros keep accounting stable
* and avoid `Cannot read properties of undefined (reading 'input')` when usage
* chunks arrive during openai-completions streaming.
*/
export const PI_AI_MODEL_ZERO_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
} as const;
export type PiAiModelCostRates = {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
};
export function normalizePiAiModelCost(existing: unknown): PiAiModelCostRates {
if (!existing || typeof existing !== 'object') {
return { ...PI_AI_MODEL_ZERO_COST };
}
const record = existing as Record<string, unknown>;
const num = (value: unknown) =>
typeof value === 'number' && Number.isFinite(value) ? value : 0;
return {
input: num(record.input),
output: num(record.output),
cacheRead: num(record.cacheRead),
cacheWrite: num(record.cacheWrite),
};
}
/** Entry shape suitable for OpenClaw agent `models.json` provider.models[]. */
export function piAiModelsJsonModelEntry(
id: string,
name: string = id,
): { id: string; name: string; cost: PiAiModelCostRates } {
return { id, name, cost: normalizePiAiModelCost(undefined) };
}
+7 -2
View File
@@ -26,6 +26,7 @@ import {
isOAuthProviderType,
isOpenClawOAuthPluginProviderKey,
} from './provider-keys';
import { normalizePiAiModelCost, type PiAiModelCostRates } from '../shared/pi-ai-model-cost';
import { withConfigLock } from './config-mutex';
const AUTH_STORE_VERSION = 1;
@@ -1753,7 +1754,7 @@ export async function batchSyncConfigFields(token: string): Promise<void> {
type AgentModelProviderEntry = {
baseUrl?: string;
api?: string;
models?: Array<{ id: string; name: string }>;
models?: Array<{ id: string; name: string; cost?: PiAiModelCostRates }>;
apiKey?: string;
/** When true, pi-ai sends Authorization: Bearer instead of x-api-key */
authHeader?: boolean;
@@ -1788,7 +1789,11 @@ async function updateModelsJsonProviderEntriesForAgents(
const mergedModels = (entry.models ?? []).map((m) => {
const prev = existingModels.find((e) => e.id === m.id);
return prev ? { ...prev, id: m.id, name: m.name } : { ...m };
const base = prev ? { ...prev, id: m.id, name: m.name } : { ...m };
return {
...base,
cost: normalizePiAiModelCost((base as { cost?: unknown }).cost),
};
});
if (entry.baseUrl !== undefined) existing.baseUrl = entry.baseUrl;
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "clawx",
"version": "0.4.2-alpha.0",
"version": "0.4.2-beta.2",
"pnpm": {
"onlyBuiltDependencies": [
"@discordjs/opus",
@@ -65,6 +65,7 @@
"package:win": "pnpm run prep:win-binaries && pnpm run package && node scripts/run-electron-builder.mjs --win --publish never",
"package:linux": "pnpm run package && node scripts/run-electron-builder.mjs --linux --publish never",
"release": "pnpm run uv:download && pnpm run package && node scripts/run-electron-builder.mjs --publish always",
"preversion": "node scripts/pre-version-fetch-tags.mjs",
"version": "node scripts/assert-release-version.mjs",
"version:patch": "pnpm version patch",
"version:minor": "pnpm version minor",
@@ -73,7 +74,7 @@
"version:prerelease-alpha": "pnpm version prerelease --preid=alpha",
"version:prerelease-beta": "pnpm version prerelease --preid=beta",
"release:validate": "node scripts/assert-tag-matches-package.mjs",
"postversion": "git push && git push --tags"
"postversion": "node scripts/post-version-push.mjs"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
+44 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env node
/**
* npm/pnpm `version` lifecycle hook: runs after package.json is bumped, before
* `git tag`. Aborts if the target tag already exists so we never fail late on
* `fatal: tag 'vX.Y.Z' already exists`.
* `git tag`. Aborts if the target tag already exists locally or on origin so we
* never fail late on `fatal: tag 'vX.Y.Z' already exists` or a rejected push.
*/
import { readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { execFileSync, execSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -18,6 +18,7 @@ function readPackageVersion() {
const version = process.env.npm_package_version || readPackageVersion();
const tag = `v${version}`;
const skipRemote = process.env.SKIP_RELEASE_REMOTE_CHECK === '1';
function localTagExists(t) {
try {
@@ -28,6 +29,17 @@ function localTagExists(t) {
}
}
function remoteTagExists(t) {
try {
const out = execFileSync('git', ['ls-remote', '--tags', 'origin', `refs/tags/${t}`], {
encoding: 'utf8',
}).trim();
return out.length > 0;
} catch {
return null;
}
}
if (localTagExists(tag)) {
console.error(`
Release version check failed: git tag ${tag} already exists locally.
@@ -42,4 +54,32 @@ Typical fixes:
process.exit(1);
}
console.log(`Release version OK: tag ${tag} is not present locally yet.`);
if (!skipRemote) {
const onRemote = remoteTagExists(tag);
if (onRemote === null) {
console.error(`
Release version check failed: could not query origin for refs/tags/${tag}.
Ensure \`origin\` exists and you can reach the network, run
\`pnpm run preversion\` / \`git fetch origin --tags\`, then retry.
To skip this check (offline only): SKIP_RELEASE_REMOTE_CHECK=1
`);
process.exit(1);
}
if (onRemote) {
console.error(`
Release version check failed: tag ${tag} already exists on origin.
Bump to a version that is not on the remote yet (see \`git ls-remote --tags origin\`).
`);
process.exit(1);
}
}
if (skipRemote) {
console.log('Release version OK (remote check skipped): tag is not present locally.');
process.exit(0);
}
console.log(`Release version OK: tag ${tag} is not present locally and not on origin.`);
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env node
/**
* npm/pnpm `postversion`: push the current branch (set upstream if missing) and
* only the new version tag — avoids \`git push --tags\` publishing unrelated tags.
*/
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
function readPackageVersion() {
const raw = readFileSync(join(root, 'package.json'), 'utf8');
return JSON.parse(raw).version;
}
const version = process.env.npm_package_version || readPackageVersion();
const tag = `v${version}`;
execFileSync('git', ['push', '-u', 'origin', 'HEAD'], { stdio: 'inherit' });
execFileSync('git', ['push', 'origin', `refs/tags/${tag}`], { stdio: 'inherit' });
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env node
/**
* npm/pnpm `preversion`: fetch tags from origin so local state matches remote
* before SemVer bump + assert-release-version remote checks.
*/
import { execFileSync } from 'node:child_process';
const skip = process.env.SKIP_RELEASE_FETCH === '1';
if (skip) {
console.log('[pre-version-fetch-tags] Skip: SKIP_RELEASE_FETCH=1');
process.exit(0);
}
try {
execFileSync('git', ['fetch', 'origin', '--tags', '--prune'], {
stdio: 'inherit',
});
} catch {
console.error(`
[pre-version-fetch-tags] git fetch origin --tags failed.
Fix your network/remotes, or retry. To bypass (not recommended), run with
SKIP_RELEASE_FETCH=1 — assert-release-version may still block on remote tags
unless SKIP_RELEASE_REMOTE_CHECK=1.
`);
process.exit(1);
}
+2 -2
View File
@@ -34,7 +34,7 @@ describe('provider-model-sync', () => {
baseUrl: 'https://api.moonshot.cn/v1',
api: 'openai-completions',
apiKey: 'MOONSHOT_API_KEY',
models: [{ id: 'kimi-k2.6', name: 'kimi-k2.6' }],
models: [{ id: 'kimi-k2.6', name: 'kimi-k2.6', cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }],
},
});
});
@@ -74,7 +74,7 @@ describe('provider-model-sync', () => {
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
api: 'openai-completions',
apiKey: 'MODELSTUDIO_API_KEY',
models: [{ id: 'qwen3.5-plus', name: 'qwen3.5-plus' }],
models: [{ id: 'qwen3.5-plus', name: 'qwen3.5-plus', cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }],
},
});
+1 -1
View File
@@ -253,7 +253,7 @@ describe('provider-runtime-sync refresh strategy', () => {
expect.objectContaining({
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
api: 'openai-completions',
models: [{ id: 'ark-code-latest', name: 'ark-code-latest' }],
models: [{ id: 'ark-code-latest', name: 'ark-code-latest', cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }],
}),
);
});