diff --git a/src/infra/heartbeat-runner.cost-cap.test.ts b/src/infra/heartbeat-runner.cost-cap.test.ts index e65b48fb11c..f91e7b7157b 100644 --- a/src/infra/heartbeat-runner.cost-cap.test.ts +++ b/src/infra/heartbeat-runner.cost-cap.test.ts @@ -198,6 +198,12 @@ describe("config catalog lookup", () => { expect(cost).toBeCloseTo(0.0025, 4); // gpt-4o = $2.5/M }); + it("matches hardcoded table with nested provider prefix", () => { + // "openrouter/anthropic/claude-opus-4" should strip to "claude-opus-4" + const cost = estimateRunCost("x".repeat(4000), "openrouter/anthropic/claude-opus-4"); + expect(cost).toBeCloseTo(0.015, 3); // claude-opus-4 = $15/M + }); + it("ignores catalog entry with negative cost.input", () => { const cfg = { models: { diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 6d2f2f0e0c8..61aaa513178 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -1262,8 +1262,9 @@ function resolveInputPricePerToken(modelId: string, cfg?: OpenClawConfig): numbe } } } - // Strip provider prefix (e.g. "openai/gpt-4o" -> "gpt-4o") for hardcoded table match. - const slash = modelId.indexOf("/"); + // Strip provider prefix (e.g. "openai/gpt-4o" or "openrouter/anthropic/claude-opus-4" + // -> "gpt-4o" / "claude-opus-4") for hardcoded table match. + const slash = modelId.lastIndexOf("/"); const bareModel = slash !== -1 ? modelId.slice(slash + 1) : modelId; const lower = bareModel.toLowerCase(); for (const [prefix, price] of SORTED_PRICING_ENTRIES) {