feat: rewrite the Command Code provider on pi's native provider API
Replace the previous implementation with one that registers the Provider API catalog through pi's own provider layer instead of shipping a custom transport, cache file, and hand-maintained pricing table. - models: derive the catalog from the published command-code CLI package (context windows, reasoning efforts, image input, output limits, rates) and keep it as the offline baseline; scripts/sync-catalog.mjs regenerates it and supports --check - refresh: use refreshModels plus context.publish so pi persists the live /provider/v1/models listing in models-store.json and restores it offline - auth: /login browser transfer through a localhost callback server with a pasted-key fallback; $COMMAND_CODE_API_KEY, --api-key and auth.json keep working - streaming: pi's native openai-completions and anthropic-messages adapters; the generate-transport fallback and Oh My Pi branches are gone - keep the context-overflow rewrite that enables pi's compaction retry and the /commandcode-quota command - tests: 51 cases under tests/<module>/ covering models, catalog sync, auth, the callback server, overflow handling, quota, and the extension factory Verified against the live API: chat, tool round trip, image input and --thinking max on deepseek/deepseek-v4.1-flash, quota output, and catalog persistence in an interactive session.
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Regenerates src/catalog.ts from the published Command Code CLI package.
|
||||
*
|
||||
* The CLI ships the authoritative model reference (ids, context windows,
|
||||
* reasoning efforts, advertised rates) and its bundle carries the
|
||||
* input-modality and max-output metadata that the Provider API does not
|
||||
* expose. Run `npm run sync:catalog` after Command Code publishes a new CLI
|
||||
* release; review the printed diff before committing the regenerated file.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/sync-catalog.mjs [--version 1.54.0] [--check]
|
||||
*/
|
||||
|
||||
import { execFile } from "node:child_process"
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { promisify } from "node:util"
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "..")
|
||||
const catalogPath = join(projectRoot, "src", "catalog.ts")
|
||||
const modelsReferencePath = "package/dist/bundled/command-code-knowledge/reference/models.md"
|
||||
const cliBundlePath = "package/dist/cli.mjs"
|
||||
const validEfforts = ["minimal", "low", "medium", "high", "xhigh", "max"]
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = { version: undefined, check: false }
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index]
|
||||
if (argument === "--check") options.check = true
|
||||
else if (argument === "--version") {
|
||||
options.version = argv[index + 1]
|
||||
index += 1
|
||||
} else throw new Error(`Unknown argument: ${argument}`)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
/** Parse one `| \`id\` | Name | Context | Efforts | Rates | Min plan | Notes |` row. */
|
||||
export function parseReferenceRow(line) {
|
||||
const match = /^\|\s*`([^`]+)`\s*\|([^|]*)\|([^|]*)\|([^|]*)\|([^|]*)\|/.exec(line)
|
||||
if (!match) return undefined
|
||||
const [, id, name, context, efforts, rates] = match
|
||||
const effortText = efforts.trim()
|
||||
return {
|
||||
id: id.trim(),
|
||||
name: name.trim(),
|
||||
context: context.trim(),
|
||||
efforts: effortText === "—" || effortText === "" ? [] : effortText.split(",").map((e) => e.trim()),
|
||||
rates: rates.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
/** `$0.66/$1.98 · cache $0.022 (write $2.5)` → per-million-token rates. */
|
||||
export function parseRates(text) {
|
||||
const match = /\$([\d.]+)\s*\/\s*\$([\d.]+)\s*·\s*cache\s*\$([\d.]+)(?:\s*\(write\s*\$([\d.]+)\))?/.exec(text)
|
||||
if (!match) throw new Error(`Could not parse rates: ${text}`)
|
||||
return {
|
||||
input: Number(match[1]),
|
||||
output: Number(match[2]),
|
||||
cacheRead: Number(match[3]),
|
||||
cacheWrite: match[4] === undefined ? 0 : Number(match[4]),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseReference(markdown) {
|
||||
const models = []
|
||||
for (const line of markdown.split("\n")) {
|
||||
const row = parseReferenceRow(line)
|
||||
if (!row) continue
|
||||
if (models.some((model) => model.id === row.id)) throw new Error(`Duplicate model id: ${row.id}`)
|
||||
for (const effort of row.efforts) {
|
||||
if (!validEfforts.includes(effort)) throw new Error(`Unknown effort for ${row.id}: ${effort}`)
|
||||
}
|
||||
models.push({ ...row, cost: parseRates(row.rates) })
|
||||
}
|
||||
if (models.length === 0) throw new Error("No model rows found in the Command Code reference")
|
||||
return models
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice the `{id:"<id>",inputModalities:...}` object literal out of the
|
||||
* minified CLI bundle, tracking nested braces and string literals.
|
||||
*/
|
||||
export function sliceModelObject(bundle, id) {
|
||||
const start = bundle.indexOf(`{id:${JSON.stringify(id)},inputModalities:`)
|
||||
if (start < 0) return undefined
|
||||
|
||||
let depth = 0
|
||||
let quote = ""
|
||||
let escaped = false
|
||||
for (let index = start; index < bundle.length; index += 1) {
|
||||
const character = bundle[index]
|
||||
if (quote) {
|
||||
if (escaped) escaped = false
|
||||
else if (character === "\\") escaped = true
|
||||
else if (character === quote) quote = ""
|
||||
continue
|
||||
}
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
quote = character
|
||||
} else if (character === "{") {
|
||||
depth += 1
|
||||
} else if (character === "}") {
|
||||
depth -= 1
|
||||
if (depth === 0) return bundle.slice(start, index + 1)
|
||||
}
|
||||
}
|
||||
throw new Error(`Unterminated model object for ${id}`)
|
||||
}
|
||||
|
||||
function numberField(entry, key) {
|
||||
const match = new RegExp(`${key}:(-?[0-9][0-9.e+]*)`).exec(entry)
|
||||
if (!match) return undefined
|
||||
const value = Number(match[1])
|
||||
return Number.isFinite(value) ? value : undefined
|
||||
}
|
||||
|
||||
function stringArrayField(entry, key) {
|
||||
const match = new RegExp(`${key}:\\[([^\\]]*)\\]`).exec(entry)
|
||||
if (!match) return undefined
|
||||
return [...match[1].matchAll(/"([^"]*)"/g)].map((item) => item[1])
|
||||
}
|
||||
|
||||
export function parseBundleMetadata(bundle, ids) {
|
||||
const metadata = new Map()
|
||||
for (const id of ids) {
|
||||
const entry = sliceModelObject(bundle, id)
|
||||
if (!entry) continue
|
||||
const input = stringArrayField(entry, "inputModalities") ?? ["text"]
|
||||
metadata.set(id, {
|
||||
input,
|
||||
reasoning: entry.includes("reasoning:!0") || entry.includes("reasoningEfforts:["),
|
||||
maxOutputTokens: numberField(entry, "maxOutputTokens"),
|
||||
contextWindow: numberField(entry, "contextWindow"),
|
||||
})
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
export function buildCatalog(version, reference, metadata) {
|
||||
return reference.map((model) => {
|
||||
const bundle = metadata.get(model.id)
|
||||
if (!bundle) throw new Error(`No CLI bundle metadata found for ${model.id}`)
|
||||
const efforts = model.efforts
|
||||
return {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
contextWindow: bundle.contextWindow ?? 0,
|
||||
efforts,
|
||||
reasoning: bundle.reasoning || model.efforts.length > 0,
|
||||
input: bundle.input,
|
||||
maxOutputTokens: bundle.maxOutputTokens ?? 0,
|
||||
cost: model.cost,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function renderCatalog(version, models) {
|
||||
const lines = []
|
||||
lines.push("/**")
|
||||
lines.push(" * Generated by scripts/sync-catalog.mjs from the published Command Code CLI")
|
||||
lines.push(" * package. Do not edit manually: run `npm run sync:catalog` instead.")
|
||||
lines.push(" */")
|
||||
lines.push("")
|
||||
lines.push(`export const COMMAND_CODE_CLI_VERSION = ${JSON.stringify(version)}`)
|
||||
lines.push("")
|
||||
lines.push('export type CommandCodeInputModality = "text" | "image"')
|
||||
lines.push("")
|
||||
lines.push('export type CommandCodeReasoningEffort =')
|
||||
for (const effort of validEfforts) lines.push(` | ${JSON.stringify(effort)}`)
|
||||
lines.push("")
|
||||
lines.push("export interface CommandCodeModelCost {")
|
||||
lines.push(" input: number")
|
||||
lines.push(" output: number")
|
||||
lines.push(" cacheRead: number")
|
||||
lines.push(" cacheWrite: number")
|
||||
lines.push("}")
|
||||
lines.push("")
|
||||
lines.push("export interface CatalogModel {")
|
||||
lines.push(" id: string")
|
||||
lines.push(" name: string")
|
||||
lines.push(" contextWindow: number")
|
||||
lines.push(" efforts: readonly CommandCodeReasoningEffort[]")
|
||||
lines.push(" reasoning: boolean")
|
||||
lines.push(" input: readonly CommandCodeInputModality[]")
|
||||
lines.push(" maxOutputTokens: number")
|
||||
lines.push(" cost: CommandCodeModelCost")
|
||||
lines.push("}")
|
||||
lines.push("")
|
||||
lines.push("export const CATALOG: readonly CatalogModel[] = [")
|
||||
for (const model of models) {
|
||||
lines.push(" {")
|
||||
lines.push(` id: ${JSON.stringify(model.id)},`)
|
||||
lines.push(` name: ${JSON.stringify(model.name)},`)
|
||||
lines.push(` contextWindow: ${model.contextWindow},`)
|
||||
lines.push(` efforts: [${model.efforts.map((effort) => JSON.stringify(effort)).join(", ")}],`)
|
||||
lines.push(` reasoning: ${model.reasoning},`)
|
||||
lines.push(` input: [${model.input.map((item) => JSON.stringify(item)).join(", ")}],`)
|
||||
lines.push(` maxOutputTokens: ${model.maxOutputTokens},`)
|
||||
lines.push(
|
||||
` cost: { input: ${model.cost.input}, output: ${model.cost.output}, cacheRead: ${model.cost.cacheRead}, cacheWrite: ${model.cost.cacheWrite} },`,
|
||||
)
|
||||
lines.push(" },")
|
||||
}
|
||||
lines.push("]")
|
||||
lines.push("")
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
async function latestVersion() {
|
||||
const { stdout } = await execFileAsync("npm", ["view", "command-code", "version"], { encoding: "utf-8" })
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
async function downloadCli(version, directory) {
|
||||
const { stdout } = await execFileAsync("npm", ["pack", `command-code@${version}`, "--silent"], {
|
||||
cwd: directory,
|
||||
encoding: "utf-8",
|
||||
})
|
||||
const tarball = stdout.trim().split("\n").pop()
|
||||
if (!tarball) throw new Error("npm pack did not report a tarball")
|
||||
await execFileAsync("tar", ["xzf", tarball], { cwd: directory })
|
||||
return {
|
||||
reference: await readFile(join(directory, modelsReferencePath), "utf-8"),
|
||||
bundle: await readFile(join(directory, cliBundlePath), "utf-8"),
|
||||
}
|
||||
}
|
||||
|
||||
function summarize(previous, next) {
|
||||
const parse = (content) =>
|
||||
content
|
||||
? new Map(
|
||||
[...content.matchAll(/^ {4}id: "([^"]+)",$/gm)].map((match) => [match[1], match[1]]),
|
||||
)
|
||||
: new Map()
|
||||
const before = parse(previous)
|
||||
const after = parse(next)
|
||||
const added = [...after.keys()].filter((id) => !before.has(id))
|
||||
const removed = [...before.keys()].filter((id) => !after.has(id))
|
||||
return { added, removed }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2))
|
||||
const version = options.version ?? (await latestVersion())
|
||||
const directory = await mkdtemp(join(tmpdir(), "commandcode-catalog-"))
|
||||
|
||||
try {
|
||||
const { reference, bundle } = await downloadCli(version, directory)
|
||||
const models = parseReference(reference)
|
||||
const metadata = parseBundleMetadata(
|
||||
bundle,
|
||||
models.map((model) => model.id),
|
||||
)
|
||||
const catalog = buildCatalog(version, models, metadata)
|
||||
const previous = await readFile(catalogPath, "utf-8").catch(() => undefined)
|
||||
const next = renderCatalog(version, catalog)
|
||||
const { added, removed } = summarize(previous, next)
|
||||
|
||||
console.log(`command-code@${version}: ${catalog.length} models`)
|
||||
console.log(`added: ${added.length > 0 ? added.join(", ") : "none"}`)
|
||||
console.log(`removed: ${removed.length > 0 ? removed.join(", ") : "none"}`)
|
||||
|
||||
if (options.check) {
|
||||
if (previous !== next) {
|
||||
console.error("src/catalog.ts is out of date; run `npm run sync:catalog`.")
|
||||
process.exitCode = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await mkdir(dirname(catalogPath), { recursive: true })
|
||||
await writeFile(catalogPath, next, "utf-8")
|
||||
console.log(`wrote ${catalogPath}`)
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
await main()
|
||||
}
|
||||
Reference in New Issue
Block a user