mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(longcat): correct cache pricing and add provider icon (#121369)
* longcat: align baseUrl with docs, fix cacheWrite pricing, add brand icon - Point baseUrl at the documented https://api.longcat.chat/openai/v1 path instead of the undocumented unversioned alias route. - Fix cacheWrite pricing to 0: the LongCat pricing page has no separate cache-write charge (matches deepseek/moonshot/zai catalog conventions). - Update the pricing page link to the current docs path. - Add the official LongCat brand icon (provenance recorded in ATTRIBUTION.md) and display-name mapping so the Control UI shows proper branding instead of the letter-badge fallback. - Migrate the persisted legacy default baseUrl via a plugin-owned doctor configRepair contract: onboarding persists models.providers.longcat.baseUrl and the runtime reads the stored value, so the contract rewrites exactly the former default to /openai/v1, preserves custom endpoints, and warns via a legacy-config rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(longcat): correct provider metadata * fix(longcat): migrate persisted stock pricing * style(longcat): format doctor repair * fix(longcat): preserve doctor migration after compat cleanup * fix(longcat): avoid map spread in doctor repair --------- Co-authored-by: yuzehui02 <yuzehui02@meituan.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
This commit is contained in:
@@ -8,7 +8,7 @@ read_when:
|
||||
|
||||
[LongCat](https://longcat.ai) provides a hosted API for LongCat-2.0, a
|
||||
reasoning model built for coding and agentic workloads. OpenClaw provides the
|
||||
official `longcat` plugin for LongCat's OpenAI-compatible endpoint.
|
||||
official LongCat plugin for LongCat's OpenAI-compatible endpoint.
|
||||
|
||||
| Property | Value |
|
||||
| ---------- | ---------------------------------- |
|
||||
@@ -77,7 +77,7 @@ the provider's expected message shape.
|
||||
|
||||
The built-in catalog uses LongCat's pay-as-you-go list prices in USD per million
|
||||
tokens: $0.75 uncached input, $0.015 cached input, and $2.95 output. LongCat may
|
||||
offer temporary discounts; the [pricing page](https://longcat.chat/platform/docs/Pricing/LongCat-2.0.html)
|
||||
offer temporary discounts; the [pricing page](https://longcat.chat/platform/docs/pricing/long-cat-2.0)
|
||||
and your billing records are authoritative.
|
||||
|
||||
## Self-hosted LongCat-2.0
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// LongCat tests cover the plugin-owned persisted catalog repair.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { legacyConfigRules, normalizeCompatibilityConfig } from "./doctor-contract-api.js";
|
||||
|
||||
const LEGACY_STOCK_MODEL = {
|
||||
id: "LongCat-2.0",
|
||||
name: "LongCat 2.0",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 131_072,
|
||||
cost: { input: 0.75, output: 2.95, cacheRead: 0.015, cacheWrite: 0.75 },
|
||||
compat: {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
supportsUsageInStreaming: false,
|
||||
supportsStrictMode: false,
|
||||
maxTokensField: "max_tokens",
|
||||
requiresReasoningContentOnAssistantMessages: true,
|
||||
thinkingFormat: "deepseek",
|
||||
},
|
||||
};
|
||||
|
||||
function longcatConfig(models: unknown[]): OpenClawConfig {
|
||||
return {
|
||||
models: {
|
||||
providers: {
|
||||
longcat: {
|
||||
baseUrl: "https://api.longcat.chat/openai",
|
||||
api: "openai-completions",
|
||||
models,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
describe("LongCat doctor contract", () => {
|
||||
it("repairs only the exact historical stock row", () => {
|
||||
const custom = {
|
||||
...LEGACY_STOCK_MODEL,
|
||||
name: "My LongCat",
|
||||
cost: { ...LEGACY_STOCK_MODEL.cost },
|
||||
};
|
||||
const other = { id: "custom-model", name: "Custom" };
|
||||
const config = longcatConfig([structuredClone(LEGACY_STOCK_MODEL), custom, other]);
|
||||
|
||||
expect(legacyConfigRules[0]?.match?.(config.models?.providers?.longcat?.models)).toBe(true);
|
||||
|
||||
const result = normalizeCompatibilityConfig({ cfg: config });
|
||||
expect(result.changes).toEqual([
|
||||
"Updated the historical stock LongCat-2.0 cache-write price from $0.75 to $0.",
|
||||
]);
|
||||
expect(result.config.models?.providers?.longcat?.models).toEqual([
|
||||
{
|
||||
...LEGACY_STOCK_MODEL,
|
||||
cost: { ...LEGACY_STOCK_MODEL.cost, cacheWrite: 0 },
|
||||
},
|
||||
custom,
|
||||
other,
|
||||
]);
|
||||
expect(config.models?.providers?.longcat?.models?.[0]?.cost.cacheWrite).toBe(0.75);
|
||||
expect(normalizeCompatibilityConfig({ cfg: result.config })).toEqual({
|
||||
config: result.config,
|
||||
changes: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs the historical row after core Doctor removes catalog-owned compat", () => {
|
||||
const { compat: _compat, ...normalizedLegacyStockModel } = LEGACY_STOCK_MODEL;
|
||||
const custom = {
|
||||
...normalizedLegacyStockModel,
|
||||
compat: { supportsStore: true },
|
||||
};
|
||||
const config = longcatConfig([normalizedLegacyStockModel, custom]);
|
||||
|
||||
const result = normalizeCompatibilityConfig({ cfg: config });
|
||||
expect(result.config.models?.providers?.longcat?.models).toEqual([
|
||||
{
|
||||
...normalizedLegacyStockModel,
|
||||
cost: { ...normalizedLegacyStockModel.cost, cacheWrite: 0 },
|
||||
},
|
||||
custom,
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves customized prices and already-correct rows", () => {
|
||||
for (const cacheWrite of [0, 0.5]) {
|
||||
const model = {
|
||||
...LEGACY_STOCK_MODEL,
|
||||
cost: { ...LEGACY_STOCK_MODEL.cost, cacheWrite },
|
||||
};
|
||||
const config = longcatConfig([model]);
|
||||
expect(normalizeCompatibilityConfig({ cfg: config })).toEqual({ config, changes: [] });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
// LongCat doctor contract repairs the historical stock model price persisted
|
||||
// by onboarding. Match the complete stock row so operator-customized models
|
||||
// and prices are never rewritten when the vendor catalog changes.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { asObjectRecord } from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
||||
|
||||
const MODELS_PATH = ["models", "providers", "longcat", "models"];
|
||||
const LEGACY_CACHE_WRITE_PRICE = 0.75;
|
||||
|
||||
function isStringArray(value: unknown, expected: readonly string[]): boolean {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.length === expected.length &&
|
||||
value.every((entry, index) => entry === expected[index])
|
||||
);
|
||||
}
|
||||
|
||||
function isLegacyStockLongCatModel(value: unknown): boolean {
|
||||
const model = asObjectRecord(value);
|
||||
const cost = asObjectRecord(model?.cost);
|
||||
const compatValue = model?.compat;
|
||||
const compat = asObjectRecord(compatValue);
|
||||
// Core Doctor removes catalog-owned compat fields before plugin repairs run.
|
||||
// Accept that normalized shape, but preserve rows with any divergent compat override.
|
||||
const hasHistoricalCompat =
|
||||
compatValue === undefined ||
|
||||
Boolean(
|
||||
compat &&
|
||||
compat.supportsStore === false &&
|
||||
compat.supportsDeveloperRole === false &&
|
||||
compat.supportsReasoningEffort === false &&
|
||||
compat.supportsUsageInStreaming === false &&
|
||||
compat.supportsStrictMode === false &&
|
||||
compat.maxTokensField === "max_tokens" &&
|
||||
compat.requiresReasoningContentOnAssistantMessages === true &&
|
||||
compat.thinkingFormat === "deepseek" &&
|
||||
Object.keys(compat).length === 8,
|
||||
);
|
||||
return Boolean(
|
||||
model &&
|
||||
model.id === "LongCat-2.0" &&
|
||||
model.name === "LongCat 2.0" &&
|
||||
model.reasoning === true &&
|
||||
isStringArray(model.input, ["text"]) &&
|
||||
model.contextWindow === 1_048_576 &&
|
||||
model.maxTokens === 131_072 &&
|
||||
cost?.input === 0.75 &&
|
||||
cost.output === 2.95 &&
|
||||
cost.cacheRead === 0.015 &&
|
||||
cost.cacheWrite === LEGACY_CACHE_WRITE_PRICE &&
|
||||
hasHistoricalCompat,
|
||||
);
|
||||
}
|
||||
|
||||
function hasLegacyStockLongCatModel(value: unknown): boolean {
|
||||
return Array.isArray(value) && value.some(isLegacyStockLongCatModel);
|
||||
}
|
||||
|
||||
export const legacyConfigRules = [
|
||||
{
|
||||
path: MODELS_PATH,
|
||||
message:
|
||||
'models.providers.longcat.models contains the historical stock LongCat-2.0 cache-write price; run "openclaw doctor --fix" to update it without changing customized rows.',
|
||||
match: hasLegacyStockLongCatModel,
|
||||
},
|
||||
];
|
||||
|
||||
export function normalizeCompatibilityConfig({ cfg }: { cfg: OpenClawConfig }): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
} {
|
||||
const models = asObjectRecord(cfg.models);
|
||||
const providers = asObjectRecord(models?.providers);
|
||||
const provider = asObjectRecord(providers?.longcat);
|
||||
const configuredModels = provider?.models;
|
||||
if (!hasLegacyStockLongCatModel(configuredModels) || !Array.isArray(configuredModels)) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const nextModels = configuredModels.map((model) => {
|
||||
if (!isLegacyStockLongCatModel(model)) {
|
||||
return model;
|
||||
}
|
||||
const row = asObjectRecord(model) ?? {};
|
||||
const cost = asObjectRecord(row.cost) ?? {};
|
||||
return Object.assign({}, row, {
|
||||
cost: Object.assign({}, cost, { cacheWrite: 0 }),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
models: {
|
||||
...models,
|
||||
providers: {
|
||||
...providers,
|
||||
longcat: { ...provider, models: nextModels },
|
||||
},
|
||||
} as unknown as OpenClawConfig["models"],
|
||||
},
|
||||
changes: ["Updated the historical stock LongCat-2.0 cache-write price from $0.75 to $0."],
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
"id": "longcat",
|
||||
"name": "LongCat",
|
||||
"description": "OpenClaw LongCat provider plugin.",
|
||||
"doctorContract": {
|
||||
"configRepair": true
|
||||
},
|
||||
"activation": {
|
||||
"onStartup": false
|
||||
},
|
||||
@@ -31,7 +34,7 @@
|
||||
"input": 0.75,
|
||||
"output": 2.95,
|
||||
"cacheRead": 0.015,
|
||||
"cacheWrite": 0.75
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"compat": {
|
||||
"supportsStore": false,
|
||||
|
||||
@@ -34,6 +34,18 @@ by LM Studio:
|
||||
- Brand guidelines:
|
||||
https://lmstudio.ai/brand
|
||||
|
||||
## LongCat icon
|
||||
|
||||
`ProviderIcon-longcat.svg` is a metadata-cleaned copy of the official LongCat
|
||||
brand mark used across the LongCat API Platform (favicon and docs branding),
|
||||
contributed by the LongCat team at Meituan with permission to use it here:
|
||||
|
||||
- Source: https://s3plus.meituan.net/aigc-media-resources/longcat/yeqian-logo.svg
|
||||
(the favicon of https://longcat.chat/platform/docs/)
|
||||
- The white background plate was removed and the inner glyph recolored to
|
||||
`currentColor` so the mark renders on both light and dark themes; the
|
||||
geometry otherwise matches the cited source.
|
||||
|
||||
## llama.cpp icon
|
||||
|
||||
`ProviderIcon-llamacpp.svg` is a metadata-cleaned copy of
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="2.9 2.7 34.2 34.2" fill="none"><title>LongCat</title><path d="M3.7180590000000002,31.9163C3.24403,31.9163,2.9002264,31.4649,3.0262264,31.0079L9.07884,9.056280000000001C9.337579999999999,8.117891,10.43555,7.703758,11.24956,8.237532L19.2136,13.459869999999999C19.6909,13.77281,20.3081,13.77329,20.7859,13.46111L28.7837,8.234667C29.5984,7.702255,30.6956,8.11792,30.953,9.056519999999999L36.974,31.0089C37.0993,31.4656,36.7556,31.9163,36.2819,31.9163L28.5948,31.9163C29.9941,30.2961,30.7641,28.2266,30.7641,26.0857L30.7641,25.8358C30.7641,23.7417,30.0023,21.719,28.6209,20.1451L27.6344,15.19322C27.5764,14.90227,27.321,14.69275,27.0243,14.69275C26.8898,14.69275,26.7588,14.7364,26.6511,14.81715L22.9316,17.60681C22.6667,17.80548,22.3241,17.868740000000003,22.0057,17.77777C20.6944,17.403100000000002,19.3043,17.403100000000002,17.9929,17.77777C17.674599999999998,17.868740000000003,17.332,17.80548,17.0671,17.60681L13.3459,14.815909999999999C13.2393,14.73596,13.1096,14.69275,12.97639,14.69275C12.67948,14.69275,12.42483,14.90461,12.37083,15.196570000000001L11.41369,20.371000000000002C10.01719,21.7911,9.2346,23.703,9.2346,25.6947L9.2346,26.168C9.2346,28.2566,9.98171,30.2762,11.3409,31.8619L11.38755,31.9163L3.7180590000000002,31.9163Z" fill-rule="evenodd" fill="#29E154"/><path d="M16.05224895477295,27.610614743041992L18.20519895477295,27.610614743041992L18.20519895477295,22.587064743041992L16.37845295477295,22.587064743041992L16.05224895477295,27.610614743041992ZM23.94638895477295,27.610614743041992L21.79344895477295,27.610614743041992L21.79344895477295,22.587064743041992L23.62018895477295,22.587064743041992L23.94638895477295,27.610614743041992Z" fill="currentColor"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -40,6 +40,7 @@ const PROVIDER_ICON_NAMES = new Set([
|
||||
"llamacpp",
|
||||
"llmproxy",
|
||||
"lmstudio",
|
||||
"longcat",
|
||||
"manus",
|
||||
"mimo",
|
||||
"minimax",
|
||||
@@ -93,6 +94,7 @@ const PROVIDER_DISPLAY_LABELS: Readonly<Record<string, string>> = {
|
||||
"github-copilot": "GitHub",
|
||||
"llama-cpp": "llama.cpp",
|
||||
lmstudio: "LM Studio",
|
||||
longcat: "LongCat",
|
||||
openai: "OpenAI",
|
||||
moonshot: "Moonshot AI",
|
||||
opencode: "OpenCode",
|
||||
|
||||
Reference in New Issue
Block a user