opencode-cmd-provider 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +75 -33
- package/dist/src/catalog/facts.d.ts +5 -3
- package/dist/src/catalog/facts.js +49 -4
- package/dist/src/catalog/snapshot.js +2 -1
- package/dist/src/deals/catalog.d.ts +55 -0
- package/dist/src/deals/catalog.js +76 -0
- package/dist/src/deals/enrichment.d.ts +4 -0
- package/dist/src/deals/enrichment.js +63 -0
- package/dist/src/deals/index.d.ts +5 -0
- package/dist/src/deals/index.js +16 -0
- package/dist/src/deals/plan-summary.d.ts +13 -0
- package/dist/src/deals/plan-summary.js +160 -0
- package/dist/src/deals/tui.d.ts +10 -0
- package/dist/src/deals/tui.js +104 -0
- package/dist/src/deals/vendor.d.ts +1 -0
- package/dist/src/deals/vendor.js +29 -0
- package/dist/src/plugin/index.js +5 -0
- package/dist/src/provider/command-code-model.js +2 -9
- package/dist/src/provider/cost.d.ts +7 -0
- package/dist/src/provider/cost.js +14 -0
- package/dist/src/provider/modalities.d.ts +2 -6
- package/dist/src/provider/modalities.js +4 -48
- package/dist/src/provider/reasoning.js +1 -0
- package/dist/src/provider/stream.js +3 -3
- package/dist/tui.js +180 -0
- package/package.json +13 -5
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// src/deals/plan-summary.ts — cmd_plan_summary tool: plan-aware allowance
|
|
2
|
+
// breakdown. Plan resolution: tool arg → COMMANDCODE_PLAN → live /alpha/whoami
|
|
3
|
+
// (when a key is present and the network works) → default "go". Rendering is a
|
|
4
|
+
// pure function so tests never touch the network.
|
|
5
|
+
import { tool } from "@opencode-ai/plugin";
|
|
6
|
+
import { MODEL_COSTS } from "../catalog/facts.js";
|
|
7
|
+
import { MODEL_DEALS, PLAN_CATALOG, } from "./catalog.js";
|
|
8
|
+
import { getApiBase } from "../env.js";
|
|
9
|
+
const PLAN_DISPLAY = {
|
|
10
|
+
go: "Go",
|
|
11
|
+
goat: "GOAT",
|
|
12
|
+
pro: "Pro",
|
|
13
|
+
max: "Max 10×",
|
|
14
|
+
max20: "Max 20×",
|
|
15
|
+
teampro: "Team Pro",
|
|
16
|
+
provider: "Provider",
|
|
17
|
+
};
|
|
18
|
+
const PLAN_ALIASES = {
|
|
19
|
+
go: "go",
|
|
20
|
+
"individual-go": "go",
|
|
21
|
+
goat: "goat",
|
|
22
|
+
"individual-goat": "goat",
|
|
23
|
+
pro: "pro",
|
|
24
|
+
"individual-pro": "pro",
|
|
25
|
+
"individual-pro-v1": "pro",
|
|
26
|
+
max: "max",
|
|
27
|
+
max10: "max",
|
|
28
|
+
"max-10x": "max",
|
|
29
|
+
"max 10x": "max",
|
|
30
|
+
"individual-max": "max",
|
|
31
|
+
max20: "max20",
|
|
32
|
+
"max-20x": "max20",
|
|
33
|
+
"max 20x": "max20",
|
|
34
|
+
"individual-ultra": "max20",
|
|
35
|
+
ultra: "max20",
|
|
36
|
+
teampro: "teampro",
|
|
37
|
+
"team-pro": "teampro",
|
|
38
|
+
"team pro": "teampro",
|
|
39
|
+
provider: "provider",
|
|
40
|
+
"individual-provider": "provider",
|
|
41
|
+
};
|
|
42
|
+
export function normalizePlan(value) {
|
|
43
|
+
if (typeof value !== "string")
|
|
44
|
+
return undefined;
|
|
45
|
+
return PLAN_ALIASES[value.toLowerCase()];
|
|
46
|
+
}
|
|
47
|
+
export async function resolvePlan(planArg, env = process.env) {
|
|
48
|
+
const fromArg = normalizePlan(planArg);
|
|
49
|
+
if (fromArg)
|
|
50
|
+
return fromArg;
|
|
51
|
+
const fromEnv = normalizePlan(env.COMMANDCODE_PLAN);
|
|
52
|
+
if (fromEnv)
|
|
53
|
+
return fromEnv;
|
|
54
|
+
if (env.COMMANDCODE_API_KEY) {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(`${getApiBase(env)}/alpha/whoami`, {
|
|
57
|
+
headers: { authorization: `Bearer ${env.COMMANDCODE_API_KEY}` },
|
|
58
|
+
signal: AbortSignal.timeout(5000),
|
|
59
|
+
});
|
|
60
|
+
if (response.ok) {
|
|
61
|
+
const body = (await response.json());
|
|
62
|
+
const plan = normalizePlan(body.planId ?? body.plan?.id);
|
|
63
|
+
if (plan)
|
|
64
|
+
return plan;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// offline or unreachable — fall through to the default
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return "go";
|
|
72
|
+
}
|
|
73
|
+
const REQUEST_PROFILE = { input: 800, output: 200, cacheRead: 50_000 };
|
|
74
|
+
export function renderPlanSummary(plan, deals = MODEL_DEALS, catalog = PLAN_CATALOG) {
|
|
75
|
+
const info = catalog[plan];
|
|
76
|
+
const lines = [];
|
|
77
|
+
lines.push(`# Command Code plan: ${PLAN_DISPLAY[plan]}`);
|
|
78
|
+
if (plan === "provider") {
|
|
79
|
+
lines.push("pay-as-you-go at model API rates — no monthly allowances or window caps.");
|
|
80
|
+
}
|
|
81
|
+
else if (info) {
|
|
82
|
+
lines.push(`$${info.price}/mo buys $${info.credits} of credits; 5-hour window $${info.window5h}, weekly window $${info.windowWeek}.`);
|
|
83
|
+
if (plan !== "goat" && plan !== "pro") {
|
|
84
|
+
lines.push("This plan has no per-model allowances — active deals apply to your full credit balance at the discounted rates below.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const rows = Object.entries(deals).filter(([, d]) => d.allowance?.[plan] !== undefined);
|
|
88
|
+
const freeRows = Object.entries(deals).filter(([id, d]) => d.free && !d.allowance?.[plan]);
|
|
89
|
+
const hasAllowances = plan === "goat" || plan === "pro";
|
|
90
|
+
const dealRows = hasAllowances
|
|
91
|
+
? []
|
|
92
|
+
: Object.entries(deals).filter(([, d]) => d.discount || d.peakOffPeak);
|
|
93
|
+
if (rows.length === 0 && freeRows.length === 0 && dealRows.length === 0) {
|
|
94
|
+
lines.push("No deal data is bundled for this plan.");
|
|
95
|
+
lines.push("See https://commandcode.ai/docs/resources/pricing-limits for the live table.");
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
98
|
+
lines.push("");
|
|
99
|
+
if (hasAllowances) {
|
|
100
|
+
lines.push("| Model | $/mo allowance | ~requests/mo | Deal |");
|
|
101
|
+
lines.push("| --- | --- | --- | --- |");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
lines.push("| Model | Deal | Rates |");
|
|
105
|
+
lines.push("| --- | --- | --- |");
|
|
106
|
+
}
|
|
107
|
+
for (const [id, d] of [...rows, ...freeRows, ...dealRows]) {
|
|
108
|
+
const safeId = id.replace(/[|`]/g, " ");
|
|
109
|
+
const dealBits = [];
|
|
110
|
+
if (d.free)
|
|
111
|
+
dealBits.push("FREE");
|
|
112
|
+
if (d.discount)
|
|
113
|
+
dealBits.push(`${d.discount.pct}% off${d.discount.endsAt ? ` until ${d.discount.endsAt}` : ""}`);
|
|
114
|
+
if (d.peakOffPeak)
|
|
115
|
+
dealBits.push(`peak/off-peak (${d.peakOffPeak.windows})`);
|
|
116
|
+
const dealText = dealBits.join("; ") || "—";
|
|
117
|
+
if (hasAllowances) {
|
|
118
|
+
const allowance = d.allowance?.[plan];
|
|
119
|
+
const estimate = allowance && d.free === false
|
|
120
|
+
? estimateMonthlyRequests(id, allowance).toLocaleString("en-US")
|
|
121
|
+
: "—";
|
|
122
|
+
lines.push(`| \`${safeId}\` | ${allowance !== undefined ? `$${allowance}` : "free"} | ${estimate} | ${dealText} |`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const rates = d.discount && d.was
|
|
126
|
+
? `was $${d.was.input}/$${d.was.output} in/out`
|
|
127
|
+
: d.peakOffPeak
|
|
128
|
+
? `$${d.peakOffPeak.peak.input}/$${d.peakOffPeak.peak.output} peak`
|
|
129
|
+
: "—";
|
|
130
|
+
lines.push(`| \`${safeId}\` | ${dealText} | ${rates} |`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
lines.push("");
|
|
134
|
+
lines.push("Estimates assume ~800 fresh input + 50K cache-read + 200 output tokens per request.");
|
|
135
|
+
lines.push("Source: https://commandcode.ai/docs/resources/pricing-limits");
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
function estimateMonthlyRequests(modelId, allowance) {
|
|
139
|
+
const cost = MODEL_COSTS[modelId];
|
|
140
|
+
// No cost row bundled (e.g. not yet in the snapshot): the allowance is the
|
|
141
|
+
// best estimate we have, so report it directly as a placeholder.
|
|
142
|
+
if (!cost)
|
|
143
|
+
return Math.round(allowance);
|
|
144
|
+
const perRequest = (REQUEST_PROFILE.input * cost.input +
|
|
145
|
+
REQUEST_PROFILE.output * cost.output +
|
|
146
|
+
REQUEST_PROFILE.cacheRead * cost.cacheRead) /
|
|
147
|
+
1_000_000;
|
|
148
|
+
if (perRequest <= 0)
|
|
149
|
+
return Math.round(allowance);
|
|
150
|
+
return Math.floor(allowance / perRequest + 1e-9);
|
|
151
|
+
}
|
|
152
|
+
export function planSummaryTool() {
|
|
153
|
+
return tool({
|
|
154
|
+
description: "Show the Command Code plan's credits, usage windows, per-model monthly allowances (GOAT/Pro) or active deals (other plans), with estimated monthly request counts. Set COMMANDCODE_PLAN (go|goat|pro|max|max20|teampro|provider) to pin the plan without network access.",
|
|
155
|
+
args: {
|
|
156
|
+
plan: tool.schema.string().optional().describe("go|goat|pro|max|max20|teampro|provider"),
|
|
157
|
+
},
|
|
158
|
+
execute: async (args) => renderPlanSummary(await resolvePlan(args.plan)),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
2
|
+
export declare function dealsRows(model: {
|
|
3
|
+
options?: {
|
|
4
|
+
cmd?: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
} | undefined): Array<[string, string]>;
|
|
7
|
+
declare const plugin: TuiPluginModule & {
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
export default plugin;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/solid/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @opentui/solid */
|
|
3
|
+
// src/deals/tui.tsx — TUI plugin: "Command Code" deals section in the session
|
|
4
|
+
// sidebar (sidebar_content slot). Renders deal details from the picked model's
|
|
5
|
+
// enriched options.cmd (produced by the server plugin's config hook). Renders
|
|
6
|
+
// nothing when the model has no deals data — zero sidebar noise.
|
|
7
|
+
import { For, Show, createMemo } from "solid-js";
|
|
8
|
+
import { DEAL_SOURCE_URL, PLAN_CATALOG } from "./catalog.js";
|
|
9
|
+
function planDisplay(plan) {
|
|
10
|
+
return PLAN_CATALOG[plan]?.display ?? plan;
|
|
11
|
+
}
|
|
12
|
+
const TIER_DISPLAY = {
|
|
13
|
+
opensource: "Open Source",
|
|
14
|
+
premium: "Premium",
|
|
15
|
+
};
|
|
16
|
+
function tierDisplay(tier) {
|
|
17
|
+
return TIER_DISPLAY[tier] ?? tier;
|
|
18
|
+
}
|
|
19
|
+
function rateString(rates) {
|
|
20
|
+
if (typeof rates.input !== "number" || typeof rates.output !== "number")
|
|
21
|
+
return undefined;
|
|
22
|
+
return `$${rates.input}/$${rates.output} in/out`;
|
|
23
|
+
}
|
|
24
|
+
export function dealsRows(model) {
|
|
25
|
+
const cmd = model?.options?.cmd;
|
|
26
|
+
if (!cmd)
|
|
27
|
+
return [];
|
|
28
|
+
if (cmd.unavailable === true) {
|
|
29
|
+
return [
|
|
30
|
+
[`Deals unavailable — ${DEAL_SOURCE_URL}`, ""],
|
|
31
|
+
["Tier", "—"],
|
|
32
|
+
["Intelligence", "—"],
|
|
33
|
+
["Tok/s", "—"],
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
const rows = [];
|
|
37
|
+
if (typeof cmd.tier === "string")
|
|
38
|
+
rows.push(["Tier", tierDisplay(cmd.tier)]);
|
|
39
|
+
if (cmd.free === true)
|
|
40
|
+
rows.push(["Status", "FREE"]);
|
|
41
|
+
if (cmd.allowance) {
|
|
42
|
+
for (const [plan, value] of Object.entries(cmd.allowance)) {
|
|
43
|
+
if (typeof value === "number")
|
|
44
|
+
rows.push([`${planDisplay(plan)} allowance`, `$${value}/mo`]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (cmd.discount && typeof cmd.discount.pct === "number") {
|
|
48
|
+
rows.push([
|
|
49
|
+
"Deal",
|
|
50
|
+
`${cmd.discount.pct}% off${typeof cmd.discount.endsAt === "string" ? ` until ${cmd.discount.endsAt}` : ""}`,
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
const was = cmd.was ? rateString(cmd.was) : undefined;
|
|
54
|
+
const now = cmd.now ? rateString(cmd.now) : undefined;
|
|
55
|
+
if (was)
|
|
56
|
+
rows.push(["Was", was]);
|
|
57
|
+
if (now)
|
|
58
|
+
rows.push(["Now", now]);
|
|
59
|
+
if (cmd.benchmark) {
|
|
60
|
+
rows.push([
|
|
61
|
+
"Intelligence",
|
|
62
|
+
typeof cmd.benchmark.intelligence === "number" ? String(cmd.benchmark.intelligence) : "—",
|
|
63
|
+
]);
|
|
64
|
+
rows.push([
|
|
65
|
+
"Tok/s",
|
|
66
|
+
typeof cmd.benchmark.tokPerSec === "number" ? String(cmd.benchmark.tokPerSec) : "—",
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
if (cmd.peakOffPeak) {
|
|
70
|
+
rows.push([
|
|
71
|
+
"Rates",
|
|
72
|
+
`peak/off-peak${typeof cmd.peakOffPeak.windows === "string" ? ` (${cmd.peakOffPeak.windows})` : ""}`,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
77
|
+
const id = "commandcode.deals";
|
|
78
|
+
const tui = async (api) => {
|
|
79
|
+
api.slots.register({
|
|
80
|
+
order: 200,
|
|
81
|
+
slots: {
|
|
82
|
+
sidebar_content(_ctx, props) {
|
|
83
|
+
return _jsx(DealsPanel, { api: api, session_id: props.session_id });
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
function DealsPanel(props) {
|
|
89
|
+
const theme = () => props.api.theme.current;
|
|
90
|
+
// Mid-session model switches update the session record (`session.updated`
|
|
91
|
+
// reconciles it into the sync store), so reading `session.model` reactively
|
|
92
|
+
// is enough — no event subscription needed.
|
|
93
|
+
const model = createMemo(() => {
|
|
94
|
+
const current = props.api.state.session.get(props.session_id)?.model;
|
|
95
|
+
if (!current)
|
|
96
|
+
return undefined;
|
|
97
|
+
return props.api.state.provider.find((provider) => provider.id === current.providerID)
|
|
98
|
+
?.models[current.id];
|
|
99
|
+
});
|
|
100
|
+
const rows = createMemo(() => dealsRows(model()));
|
|
101
|
+
return (_jsx(Show, { when: rows().length > 0, children: _jsxs("box", { children: [_jsx("text", { fg: theme().text, children: _jsx("b", { children: "Command Code" }) }), _jsx(For, { each: rows(), children: (row) => (_jsx("text", { fg: theme().textMuted, children: row[1] ? `${row[0]}: ${row[1]}` : row[0] })) })] }) }));
|
|
102
|
+
}
|
|
103
|
+
const plugin = { id, tui };
|
|
104
|
+
export default plugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function vendorFamilyForModel(modelId: string): string | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/deals/vendor.ts — model id → vendor family mapping for the config hook.
|
|
2
|
+
// Derived from the model id namespace, never from scraped data, so it cannot
|
|
3
|
+
// go stale. Unknown ids map to undefined and the family field is left unset.
|
|
4
|
+
const VENDOR_FAMILIES = {
|
|
5
|
+
"claude-": "claude",
|
|
6
|
+
"gpt-": "gpt",
|
|
7
|
+
"google/": "gemini",
|
|
8
|
+
"deepseek/": "deepseek",
|
|
9
|
+
"Qwen/": "qwen",
|
|
10
|
+
"moonshotai/": "kimi",
|
|
11
|
+
"zai-org/": "glm",
|
|
12
|
+
"MiniMaxAI/": "minimax",
|
|
13
|
+
"xiaomi/": "mimo",
|
|
14
|
+
"stepfun/": "step",
|
|
15
|
+
"tencent/": "tencent",
|
|
16
|
+
"nvidia/": "nemotron",
|
|
17
|
+
"thinkingmachines/": "inkling",
|
|
18
|
+
"poolside/": "laguna",
|
|
19
|
+
"meta/": "muse",
|
|
20
|
+
"xai/": "grok",
|
|
21
|
+
"sakana/": "sakana",
|
|
22
|
+
};
|
|
23
|
+
export function vendorFamilyForModel(modelId) {
|
|
24
|
+
for (const [prefix, family] of Object.entries(VENDOR_FAMILIES)) {
|
|
25
|
+
if (modelId.startsWith(prefix))
|
|
26
|
+
return family;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
package/dist/src/plugin/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { MODEL_SNAPSHOT } from "../catalog/snapshot.js";
|
|
3
3
|
import { getApiBase } from "../env.js";
|
|
4
4
|
import { augmentConfigCommandCodeModels, autoRegister } from "./models.js";
|
|
5
|
+
import { enrichCommandCodeModels, planSummaryTool } from "../deals/index.js";
|
|
5
6
|
import { runAuthFlow } from "./auth.js";
|
|
6
7
|
const server = async () => {
|
|
7
8
|
return {
|
|
@@ -12,6 +13,7 @@ const server = async () => {
|
|
|
12
13
|
baseURL: getApiBase(),
|
|
13
14
|
});
|
|
14
15
|
augmentConfigCommandCodeModels(config);
|
|
16
|
+
enrichCommandCodeModels(config);
|
|
15
17
|
},
|
|
16
18
|
auth: {
|
|
17
19
|
provider: "commandcode",
|
|
@@ -23,6 +25,9 @@ const server = async () => {
|
|
|
23
25
|
},
|
|
24
26
|
],
|
|
25
27
|
},
|
|
28
|
+
tool: {
|
|
29
|
+
cmd_plan_summary: planSummaryTool(),
|
|
30
|
+
},
|
|
26
31
|
};
|
|
27
32
|
};
|
|
28
33
|
export default { id: "commandcode", server };
|
|
@@ -9,7 +9,7 @@ import { resolveApiKey } from "./auth-key.js";
|
|
|
9
9
|
import { messagesToCC, toolsToJson, systemPromptToText, getEnvironmentInfo, isRecord, } from "./converters.js";
|
|
10
10
|
import { parseStreamEventLine, ccEventToStreamPart } from "./stream.js";
|
|
11
11
|
import { redactCommandCodeErrorText, commandCodeErrorMessage } from "./redact.js";
|
|
12
|
-
import { calculateCommandCodeCost } from "./cost.js";
|
|
12
|
+
import { calculateCommandCodeCost, costUsageFromAiSdkUsage } from "./cost.js";
|
|
13
13
|
import { ZERO_MODEL_COST, MODEL_COSTS } from "./pricing.js";
|
|
14
14
|
import { mappedReasoningEffort, resolveProviderReasoning, thinkingMetadataForModel, isReasoningModel, } from "./reasoning.js";
|
|
15
15
|
import { modelSupportsImageInput } from "./modalities.js";
|
|
@@ -233,14 +233,7 @@ export class CommandCodeLanguageModel {
|
|
|
233
233
|
for (const part of parts) {
|
|
234
234
|
if (part.type === "finish") {
|
|
235
235
|
finished = true;
|
|
236
|
-
|
|
237
|
-
calculateCommandCodeCost(this.costForModel(), {
|
|
238
|
-
input: usage.inputTokens.total ?? 0,
|
|
239
|
-
output: usage.outputTokens.total ?? 0,
|
|
240
|
-
cacheRead: 0,
|
|
241
|
-
cacheWrite: 0,
|
|
242
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
243
|
-
});
|
|
236
|
+
calculateCommandCodeCost(this.costForModel(), costUsageFromAiSdkUsage(part.usage));
|
|
244
237
|
}
|
|
245
238
|
emit(part);
|
|
246
239
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LanguageModelV3Usage } from "@ai-sdk/provider";
|
|
1
2
|
import type { CommandCodeModelCost } from "./pricing.js";
|
|
2
3
|
export interface CostUsage {
|
|
3
4
|
input: number;
|
|
@@ -16,4 +17,10 @@ export interface CostUsage {
|
|
|
16
17
|
export interface CostModel {
|
|
17
18
|
cost: CommandCodeModelCost;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Maps an emitted AI SDK v3 usage onto billable token counts: fresh input
|
|
22
|
+
* (`noCache`), cache read, and cache write each bill at their own rate, so
|
|
23
|
+
* the cache-inclusive `total` must not be billed as fresh input (issue #36).
|
|
24
|
+
*/
|
|
25
|
+
export declare function costUsageFromAiSdkUsage(usage: LanguageModelV3Usage): CostUsage;
|
|
19
26
|
export declare function calculateCommandCodeCost(model: CostModel, usage: CostUsage): void;
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps an emitted AI SDK v3 usage onto billable token counts: fresh input
|
|
3
|
+
* (`noCache`), cache read, and cache write each bill at their own rate, so
|
|
4
|
+
* the cache-inclusive `total` must not be billed as fresh input (issue #36).
|
|
5
|
+
*/
|
|
6
|
+
export function costUsageFromAiSdkUsage(usage) {
|
|
7
|
+
return {
|
|
8
|
+
input: usage.inputTokens.noCache ?? 0,
|
|
9
|
+
output: usage.outputTokens.total ?? 0,
|
|
10
|
+
cacheRead: usage.inputTokens.cacheRead ?? 0,
|
|
11
|
+
cacheWrite: usage.inputTokens.cacheWrite ?? 0,
|
|
12
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
1
15
|
export function calculateCommandCodeCost(model, usage) {
|
|
2
16
|
const rates = model.cost;
|
|
3
17
|
const longWrite = usage.cacheWrite1h ?? 0;
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
+
import { MODEL_INPUT_MODALITIES } from "../catalog/facts.js";
|
|
1
2
|
export type CommandCodeInputType = "text" | "image";
|
|
2
|
-
|
|
3
|
-
* Model input modalities from the command-code@1.15.1 bundled catalog.
|
|
4
|
-
* Models omitted here remain text-only so newly discovered IDs never claim
|
|
5
|
-
* image support without upstream evidence.
|
|
6
|
-
*/
|
|
7
|
-
export declare const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>>;
|
|
3
|
+
export { MODEL_INPUT_MODALITIES };
|
|
8
4
|
export declare function inputModalitiesForModel(modelId: string): readonly CommandCodeInputType[];
|
|
9
5
|
export declare function modelSupportsImageInput(modelId: string): boolean;
|
|
@@ -1,51 +1,7 @@
|
|
|
1
|
-
// src/provider/modalities.ts — image input
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Models omitted here remain text-only so newly discovered IDs never claim
|
|
6
|
-
* image support without upstream evidence.
|
|
7
|
-
*/
|
|
8
|
-
export const MODEL_INPUT_MODALITIES = {
|
|
9
|
-
"MiniMaxAI/MiniMax-M3": ["text", "image"],
|
|
10
|
-
"Qwen/Qwen3.6-Plus": ["text", "image"],
|
|
11
|
-
"Qwen/Qwen3.7-Flash": ["text", "image"],
|
|
12
|
-
"Qwen/Qwen3.7-Plus": ["text", "image"],
|
|
13
|
-
"Qwen/Qwen3.8-Max": ["text", "image"],
|
|
14
|
-
"claude-fable-5": ["text", "image"],
|
|
15
|
-
"claude-haiku-4-5-20251001": ["text", "image"],
|
|
16
|
-
"claude-opus-4-7": ["text", "image"],
|
|
17
|
-
"claude-opus-4-8": ["text", "image"],
|
|
18
|
-
"claude-opus-5": ["text", "image"],
|
|
19
|
-
"claude-sonnet-4-6": ["text", "image"],
|
|
20
|
-
"claude-sonnet-5": ["text", "image"],
|
|
21
|
-
"google/gemini-3.1-flash-lite": ["text", "image"],
|
|
22
|
-
"google/gemini-3.5-flash": ["text", "image"],
|
|
23
|
-
"google/gemini-3.5-flash-lite": ["text", "image"],
|
|
24
|
-
"google/gemini-3.6-flash": ["text", "image"],
|
|
25
|
-
"google/gemini-3.7-flash": ["text", "image"],
|
|
26
|
-
"gpt-5.3-codex": ["text", "image"],
|
|
27
|
-
"gpt-5.4": ["text", "image"],
|
|
28
|
-
"gpt-5.4-mini": ["text", "image"],
|
|
29
|
-
"gpt-5.5": ["text", "image"],
|
|
30
|
-
"gpt-5.6-luna": ["text", "image"],
|
|
31
|
-
"gpt-5.6-sol": ["text", "image"],
|
|
32
|
-
"gpt-5.6-terra": ["text", "image"],
|
|
33
|
-
"meta/muse-spark-1.1": ["text", "image"],
|
|
34
|
-
"meta/muse-spark-1.2": ["text", "image"],
|
|
35
|
-
"meta/muse-spark-1.2-contributor": ["text", "image"],
|
|
36
|
-
"moonshotai/Kimi-K2.5": ["text", "image"],
|
|
37
|
-
"moonshotai/Kimi-K2.6": ["text", "image"],
|
|
38
|
-
"moonshotai/Kimi-K2.7-Code": ["text", "image"],
|
|
39
|
-
"moonshotai/Kimi-K2.7-Code-Highspeed": ["text", "image"],
|
|
40
|
-
"moonshotai/Kimi-K3": ["text", "image"],
|
|
41
|
-
"Qwen/Qwen3.8-27B": ["text", "image"],
|
|
42
|
-
"sakana/fugu-ultra": ["text", "image"],
|
|
43
|
-
"stepfun/Step-3.7-Flash": ["text", "image"],
|
|
44
|
-
"thinkingmachines/inkling": ["text", "image"],
|
|
45
|
-
"thinkingmachines/inkling-small": ["text", "image"],
|
|
46
|
-
"xai/grok-4.5": ["text", "image"],
|
|
47
|
-
"xiaomi/mimo-v2.5": ["text", "image"],
|
|
48
|
-
};
|
|
1
|
+
// src/provider/modalities.ts — image input modalities from generated catalog
|
|
2
|
+
// facts. Text-only models are intentionally omitted and use the fallback below.
|
|
3
|
+
import { MODEL_INPUT_MODALITIES } from "../catalog/facts.js";
|
|
4
|
+
export { MODEL_INPUT_MODALITIES };
|
|
49
5
|
const TEXT_INPUT_ONLY = ["text"];
|
|
50
6
|
export function inputModalitiesForModel(modelId) {
|
|
51
7
|
return MODEL_INPUT_MODALITIES[modelId] ?? TEXT_INPUT_ONLY;
|
|
@@ -39,13 +39,13 @@ export function ccUsageToAiSdkUsage(event) {
|
|
|
39
39
|
return undefined;
|
|
40
40
|
const details = isRecord(usage.inputTokenDetails) ? usage.inputTokenDetails : undefined;
|
|
41
41
|
const totalInput = numberValue(usage.inputTokens) ?? 0;
|
|
42
|
-
const noCache = numberValue(details?.noCacheTokens);
|
|
43
42
|
const cacheRead = numberValue(details?.cacheReadTokens) ?? 0;
|
|
44
43
|
const cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0;
|
|
45
|
-
const
|
|
44
|
+
const explicitNoCache = numberValue(details?.noCacheTokens);
|
|
45
|
+
const noCache = explicitNoCache ?? Math.max(0, totalInput - cacheRead - cacheWrite);
|
|
46
46
|
const outputTokens = numberValue(usage.outputTokens) ?? 0;
|
|
47
47
|
return {
|
|
48
|
-
inputTokens: { total:
|
|
48
|
+
inputTokens: { total: totalInput, noCache, cacheRead, cacheWrite },
|
|
49
49
|
outputTokens: { total: outputTokens, text: outputTokens, reasoning: 0 },
|
|
50
50
|
};
|
|
51
51
|
}
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/deals/tui.tsx
|
|
3
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
4
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
5
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
6
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
7
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
8
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
9
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
10
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
11
|
+
import { For, Show, createMemo } from "solid-js";
|
|
12
|
+
|
|
13
|
+
// src/deals/catalog.ts
|
|
14
|
+
var PLAN_CATALOG = {
|
|
15
|
+
go: {
|
|
16
|
+
price: 1,
|
|
17
|
+
credits: 10,
|
|
18
|
+
window5h: 3,
|
|
19
|
+
windowWeek: 6,
|
|
20
|
+
display: "Go"
|
|
21
|
+
},
|
|
22
|
+
goat: {
|
|
23
|
+
price: 10,
|
|
24
|
+
credits: 70,
|
|
25
|
+
window5h: 14,
|
|
26
|
+
windowWeek: 35,
|
|
27
|
+
display: "GOAT"
|
|
28
|
+
},
|
|
29
|
+
pro: {
|
|
30
|
+
price: 20,
|
|
31
|
+
credits: 80,
|
|
32
|
+
window5h: 16,
|
|
33
|
+
windowWeek: 40,
|
|
34
|
+
display: "Pro"
|
|
35
|
+
},
|
|
36
|
+
max: {
|
|
37
|
+
price: 100,
|
|
38
|
+
credits: 150,
|
|
39
|
+
window5h: 45,
|
|
40
|
+
windowWeek: 90,
|
|
41
|
+
display: "Max 10\xD7"
|
|
42
|
+
},
|
|
43
|
+
max20: {
|
|
44
|
+
price: 200,
|
|
45
|
+
credits: 300,
|
|
46
|
+
window5h: 90,
|
|
47
|
+
windowWeek: 180,
|
|
48
|
+
display: "Max 20\xD7"
|
|
49
|
+
},
|
|
50
|
+
teampro: {
|
|
51
|
+
price: 40,
|
|
52
|
+
credits: 40,
|
|
53
|
+
window5h: 12,
|
|
54
|
+
windowWeek: 24,
|
|
55
|
+
display: "Team Pro"
|
|
56
|
+
},
|
|
57
|
+
provider: {
|
|
58
|
+
price: 15,
|
|
59
|
+
credits: 0,
|
|
60
|
+
window5h: 0,
|
|
61
|
+
windowWeek: 0,
|
|
62
|
+
display: "Provider"
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var DEAL_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits";
|
|
66
|
+
|
|
67
|
+
// src/deals/tui.tsx
|
|
68
|
+
function planDisplay(plan) {
|
|
69
|
+
return PLAN_CATALOG[plan]?.display ?? plan;
|
|
70
|
+
}
|
|
71
|
+
var TIER_DISPLAY = {
|
|
72
|
+
opensource: "Open Source",
|
|
73
|
+
premium: "Premium"
|
|
74
|
+
};
|
|
75
|
+
function tierDisplay(tier) {
|
|
76
|
+
return TIER_DISPLAY[tier] ?? tier;
|
|
77
|
+
}
|
|
78
|
+
function rateString(rates) {
|
|
79
|
+
if (typeof rates.input !== "number" || typeof rates.output !== "number")
|
|
80
|
+
return;
|
|
81
|
+
return `$${rates.input}/$${rates.output} in/out`;
|
|
82
|
+
}
|
|
83
|
+
function dealsRows(model) {
|
|
84
|
+
const cmd = model?.options?.cmd;
|
|
85
|
+
if (!cmd)
|
|
86
|
+
return [];
|
|
87
|
+
if (cmd.unavailable === true) {
|
|
88
|
+
return [[`Deals unavailable \u2014 ${DEAL_SOURCE_URL}`, ""], ["Tier", "\u2014"], ["Intelligence", "\u2014"], ["Tok/s", "\u2014"]];
|
|
89
|
+
}
|
|
90
|
+
const rows = [];
|
|
91
|
+
if (typeof cmd.tier === "string")
|
|
92
|
+
rows.push(["Tier", tierDisplay(cmd.tier)]);
|
|
93
|
+
if (cmd.free === true)
|
|
94
|
+
rows.push(["Status", "FREE"]);
|
|
95
|
+
if (cmd.allowance) {
|
|
96
|
+
for (const [plan, value] of Object.entries(cmd.allowance)) {
|
|
97
|
+
if (typeof value === "number")
|
|
98
|
+
rows.push([`${planDisplay(plan)} allowance`, `$${value}/mo`]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (cmd.discount && typeof cmd.discount.pct === "number") {
|
|
102
|
+
rows.push(["Deal", `${cmd.discount.pct}% off${typeof cmd.discount.endsAt === "string" ? ` until ${cmd.discount.endsAt}` : ""}`]);
|
|
103
|
+
}
|
|
104
|
+
const was = cmd.was ? rateString(cmd.was) : undefined;
|
|
105
|
+
const now = cmd.now ? rateString(cmd.now) : undefined;
|
|
106
|
+
if (was)
|
|
107
|
+
rows.push(["Was", was]);
|
|
108
|
+
if (now)
|
|
109
|
+
rows.push(["Now", now]);
|
|
110
|
+
if (cmd.benchmark) {
|
|
111
|
+
rows.push(["Intelligence", typeof cmd.benchmark.intelligence === "number" ? String(cmd.benchmark.intelligence) : "\u2014"]);
|
|
112
|
+
rows.push(["Tok/s", typeof cmd.benchmark.tokPerSec === "number" ? String(cmd.benchmark.tokPerSec) : "\u2014"]);
|
|
113
|
+
}
|
|
114
|
+
if (cmd.peakOffPeak) {
|
|
115
|
+
rows.push(["Rates", `peak/off-peak${typeof cmd.peakOffPeak.windows === "string" ? ` (${cmd.peakOffPeak.windows})` : ""}`]);
|
|
116
|
+
}
|
|
117
|
+
return rows;
|
|
118
|
+
}
|
|
119
|
+
var id = "commandcode.deals";
|
|
120
|
+
var tui = async (api) => {
|
|
121
|
+
api.slots.register({
|
|
122
|
+
order: 200,
|
|
123
|
+
slots: {
|
|
124
|
+
sidebar_content(_ctx, props) {
|
|
125
|
+
return _$createComponent(DealsPanel, {
|
|
126
|
+
api,
|
|
127
|
+
get session_id() {
|
|
128
|
+
return props.session_id;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
function DealsPanel(props) {
|
|
136
|
+
const theme = () => props.api.theme.current;
|
|
137
|
+
const model = createMemo(() => {
|
|
138
|
+
const current = props.api.state.session.get(props.session_id)?.model;
|
|
139
|
+
if (!current)
|
|
140
|
+
return;
|
|
141
|
+
return props.api.state.provider.find((provider) => provider.id === current.providerID)?.models[current.id];
|
|
142
|
+
});
|
|
143
|
+
const rows = createMemo(() => dealsRows(model()));
|
|
144
|
+
return _$createComponent(Show, {
|
|
145
|
+
get when() {
|
|
146
|
+
return rows().length > 0;
|
|
147
|
+
},
|
|
148
|
+
get children() {
|
|
149
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("b");
|
|
150
|
+
_$insertNode(_el$, _el$2);
|
|
151
|
+
_$insertNode(_el$2, _el$3);
|
|
152
|
+
_$insertNode(_el$3, _$createTextNode(`Command Code`));
|
|
153
|
+
_$insert(_el$, _$createComponent(For, {
|
|
154
|
+
get each() {
|
|
155
|
+
return rows();
|
|
156
|
+
},
|
|
157
|
+
children: (row) => (() => {
|
|
158
|
+
var _el$5 = _$createElement("text");
|
|
159
|
+
_$insert(_el$5, (() => {
|
|
160
|
+
var _c$ = _$memo(() => !!row[1]);
|
|
161
|
+
return () => _c$() ? `${row[0]}: ${row[1]}` : row[0];
|
|
162
|
+
})());
|
|
163
|
+
_$effect((_$p) => _$setProp(_el$5, "fg", theme().textMuted, _$p));
|
|
164
|
+
return _el$5;
|
|
165
|
+
})()
|
|
166
|
+
}), null);
|
|
167
|
+
_$effect((_$p) => _$setProp(_el$2, "fg", theme().text, _$p));
|
|
168
|
+
return _el$;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
var plugin = {
|
|
173
|
+
id,
|
|
174
|
+
tui
|
|
175
|
+
};
|
|
176
|
+
var tui_default = plugin;
|
|
177
|
+
export {
|
|
178
|
+
dealsRows,
|
|
179
|
+
tui_default as default
|
|
180
|
+
};
|