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:
ffyuuu
2026-08-11 11:15:18 +08:00
committed by GitHub
parent 4e22cee056
commit efe5d8d76e
7 changed files with 224 additions and 3 deletions
+2 -2
View File
@@ -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: [] });
}
});
});
+104
View File
@@ -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."],
};
}
+4 -1
View File
@@ -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,
+12
View File
@@ -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

+2
View File
@@ -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",