Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Fix Oh My Pi extension validation by avoiding the missing `calculateCost` export from OMP's legacy `pi-ai` shim.

## 0.4.1 - 2026-06-16

- Use the explicit `$COMMANDCODE_API_KEY` provider registration syntax expected by newer pi versions, removing the startup deprecation warning while keeping legacy placeholder compatibility.
Expand Down
5 changes: 3 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
* Models are fetched from Command Code's Provider API at startup.
*/

import { AssistantMessageEventStream, calculateCost } from "@earendil-works/pi-ai"
import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"

import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
import { calculateCommandCodeCost } from "./src/cost.ts"
import { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
import { getApiKey, login, refreshToken } from "./src/oauth.ts"

Expand Down Expand Up @@ -69,7 +70,7 @@ const MODEL_COSTS: Record<string, CommandCodeModelCost> = {

const streamCommandCode = createStreamCommandCode({
createStream: () => new AssistantMessageEventStream(),
calculateCost,
calculateCost: calculateCommandCodeCost,
apiBase: API_BASE,
})

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"LICENSE"
],
"scripts": {
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
"typecheck": "tsc --noEmit",
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
Expand All @@ -40,7 +40,8 @@
"test:stream": "tsx tests/test-stream.ts",
"test:retry": "tsx tests/test-retry.ts",
"test:pi-local": "node tests/test-pi-local.mjs",
"test:smoke": "node tests/test-smoke.mjs"
"test:smoke": "node tests/test-smoke.mjs",
"test:cost": "tsx tests/test-cost.ts"
},
"pi": {
"extensions": [
Expand Down
19 changes: 19 additions & 0 deletions src/cost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Local cost calculation for Command Code usage.
*
* Mirrors pi-ai's `calculateCost` arithmetic exactly. The provider ships its
* own copy because Oh My Pi's legacy pi-ai shim does not export
* `calculateCost`, which broke extension installation there (issue #24).
* `tests/test-cost.ts` locks this implementation to the pi-ai original.
*/

import type { ModelLike, Usage } from "./types.ts"

export function calculateCommandCodeCost(model: ModelLike, usage: Usage): void {
usage.cost.input = (model.cost.input / 1_000_000) * usage.input
usage.cost.output = (model.cost.output / 1_000_000) * usage.output
usage.cost.cacheRead = (model.cost.cacheRead / 1_000_000) * usage.cacheRead
usage.cost.cacheWrite = (model.cost.cacheWrite / 1_000_000) * usage.cacheWrite
usage.cost.total =
usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite
}
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,19 @@ export interface AssistantMessageLike {
timestamp: number
}

export interface ModelCost {
input: number
output: number
cacheRead: number
cacheWrite: number
}

export interface ModelLike {
id: string
api: unknown
provider: string
maxTokens: number
cost: ModelCost
}

export interface MessageLike {
Expand Down
1 change: 1 addition & 0 deletions tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function makeModel(overrides: Partial<ModelLike> = {}): ModelLike {
api: "commandcode-custom",
provider: "commandcode",
maxTokens: 384_000,
cost: { input: 0.14, output: 0.28, cacheRead: 0.028, cacheWrite: 0 },
...overrides,
}
}
Expand Down
99 changes: 99 additions & 0 deletions tests/test-cost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Regression test for the local cost calculation.
*
* The provider ships its own cost function because Oh My Pi's legacy pi-ai
* shim does not export `calculateCost` (see issue #24). This test locks the
* local implementation to pi-ai's upstream `calculateCost` so the two cannot
* drift while pi remains the reference host.
*/

import assert from "node:assert/strict"
import { describe, it } from "node:test"

import { calculateCost, type Model } from "@earendil-works/pi-ai"

import { calculateCommandCodeCost } from "../src/cost.ts"
import type { Usage } from "../src/types.ts"

type CostTable = Model<"openai-completions">["cost"]

const COST_FIXTURES: Record<string, CostTable> = {
"zero-cost-model": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
"deepseek/deepseek-v4-pro": {
input: 0.435,
output: 0.87,
cacheRead: 0.003625,
cacheWrite: 0,
},
"Qwen/Qwen3.7-Max": { input: 1.25, output: 3.75, cacheRead: 0.25, cacheWrite: 1.56 },
"gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
}

const USAGE_CASES = [
{ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
{ input: 1, output: 1, cacheRead: 1, cacheWrite: 1 },
{ input: 812, output: 187, cacheRead: 52_000, cacheWrite: 3_100 },
{ input: 1_000_000, output: 65_536, cacheRead: 998_877, cacheWrite: 123_456 },
{ input: 7, output: 999_999_999, cacheRead: 0.5, cacheWrite: 42 },
]

function piAiModel(id: string, cost: CostTable): Model<"openai-completions"> {
return {
id,
name: id,
api: "openai-completions",
provider: "commandcode",
baseUrl: "https://api.commandcode.ai",
reasoning: false,
input: ["text"],
cost,
contextWindow: 1_000_000,
maxTokens: 65_536,
}
}

function freshUsage(tokens: (typeof USAGE_CASES)[number]): Usage {
return {
...tokens,
totalTokens: tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}
}

describe("calculateCommandCodeCost()", () => {
it("matches pi-ai calculateCost exactly for all cost fields", () => {
for (const [id, cost] of Object.entries(COST_FIXTURES)) {
const model = piAiModel(id, cost)

for (const tokens of USAGE_CASES) {
const ours = freshUsage(tokens)
const upstream = freshUsage(tokens)

calculateCommandCodeCost(model, ours)
calculateCost(model, upstream)

for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) {
assert.equal(
ours.cost[key],
upstream.cost[key],
`${id} cost.${key} for tokens=${JSON.stringify(tokens)}`,
)
}
}
}
})

it("writes the total as the sum of all cost components", () => {
const model = piAiModel("claude-sonnet-4-6", COST_FIXTURES["claude-sonnet-4-6"])
const usage = freshUsage({ input: 1_000, output: 500, cacheRead: 10_000, cacheWrite: 2_000 })

calculateCommandCodeCost(model, usage)

assert.equal(
usage.cost.total,
usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite,
)
assert.ok(usage.cost.total > 0)
})
})
Loading