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.
233 lines
8.4 KiB
TypeScript
233 lines
8.4 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { createServer, type Server } from "node:http"
|
|
import type { AddressInfo } from "node:net"
|
|
import { test } from "node:test"
|
|
|
|
import type { RefreshModelsContext } from "@earendil-works/pi-ai"
|
|
import type {
|
|
ExtensionAPI,
|
|
ExtensionCommandContext,
|
|
ProviderConfig,
|
|
} from "@earendil-works/pi-coding-agent"
|
|
|
|
import commandCodeProvider from "../../index.ts"
|
|
import { CATALOG } from "../../src/catalog.ts"
|
|
|
|
/** Minimal ExtensionAPI stand-in that records what the extension registers. */
|
|
function createStubPi(): {
|
|
pi: ExtensionAPI
|
|
provider: { id: string; config: ProviderConfig } | undefined
|
|
commands: Map<string, (args: string, ctx: ExtensionCommandContext) => Promise<void>>
|
|
handlers: Map<string, (event: unknown, ctx: unknown) => unknown>
|
|
} {
|
|
const state = {
|
|
pi: undefined as unknown as ExtensionAPI,
|
|
provider: undefined as { id: string; config: ProviderConfig } | undefined,
|
|
commands: new Map<string, (args: string, ctx: ExtensionCommandContext) => Promise<void>>(),
|
|
handlers: new Map<string, (event: unknown, ctx: unknown) => unknown>(),
|
|
}
|
|
|
|
state.pi = {
|
|
registerProvider: (id: string, config: ProviderConfig) => {
|
|
state.provider = { id, config }
|
|
},
|
|
registerCommand: (name: string, options: { handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> }) => {
|
|
state.commands.set(name, options.handler)
|
|
},
|
|
on: (event: string, handler: (event: unknown, ctx: unknown) => unknown) => {
|
|
state.handlers.set(event, handler)
|
|
},
|
|
} as unknown as ExtensionAPI
|
|
|
|
return state
|
|
}
|
|
|
|
function createRefreshContext(options: {
|
|
allowNetwork: boolean
|
|
stored?: { models: unknown[] }
|
|
}): { context: RefreshModelsContext; published: unknown[] } {
|
|
const published: unknown[] = []
|
|
const context = {
|
|
allowNetwork: options.allowNetwork,
|
|
signal: new AbortController().signal,
|
|
stored: options.stored,
|
|
publish: async (publication: unknown) => {
|
|
published.push(publication)
|
|
return true
|
|
},
|
|
} as unknown as RefreshModelsContext
|
|
return { context, published }
|
|
}
|
|
|
|
/** Serves the Provider API catalog from a local port for one test. */
|
|
async function withCatalogServer(
|
|
payload: unknown,
|
|
run: (url: string) => Promise<void>,
|
|
): Promise<void> {
|
|
const server: Server = createServer((_request, response) => {
|
|
response.writeHead(200, { "content-type": "application/json" })
|
|
response.end(JSON.stringify(payload))
|
|
})
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
|
|
const { port } = server.address() as AddressInfo
|
|
|
|
try {
|
|
await run(`http://127.0.0.1:${port}/provider/v1/models`)
|
|
} finally {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
}
|
|
}
|
|
|
|
test("extension registers the Command Code provider with the generated catalog", () => {
|
|
const stub = createStubPi()
|
|
commandCodeProvider(stub.pi)
|
|
const provider = stub.provider
|
|
|
|
assert.equal(provider?.id, "commandcode")
|
|
assert.equal(provider?.config.name, "Command Code")
|
|
assert.equal(provider?.config.baseUrl, "https://api.commandcode.ai/provider/v1")
|
|
assert.equal(provider?.config.apiKey, "$COMMAND_CODE_API_KEY")
|
|
assert.equal(provider?.config.models?.length, CATALOG.length)
|
|
assert.equal(provider?.config.oauth?.name, "Command Code")
|
|
assert.ok(provider?.config.oauth?.isSubscription)
|
|
|
|
const claude = provider?.config.models?.find((model) => model.id === "claude-sonnet-4-6")
|
|
assert.equal(claude?.api, "anthropic-messages")
|
|
assert.equal(claude?.baseUrl, "https://api.commandcode.ai/provider")
|
|
|
|
const deepseek = provider?.config.models?.find((model) => model.id === "deepseek/deepseek-v4.1-flash")
|
|
assert.equal(deepseek?.api, "openai-completions")
|
|
assert.deepEqual(deepseek?.input, ["text", "image"])
|
|
})
|
|
|
|
test("refreshModels merges the live catalog and persists it for offline starts", async () => {
|
|
await withCatalogServer(
|
|
{
|
|
object: "list",
|
|
data: [
|
|
{
|
|
id: "deepseek/deepseek-v4.1-flash",
|
|
object: "model",
|
|
name: "DeepSeek V4.1 Flash",
|
|
context_length: 1_000_000,
|
|
},
|
|
{ id: "vendor/fresh", object: "model", name: "Fresh Model", context_length: 64_000 },
|
|
],
|
|
},
|
|
async (url) => {
|
|
process.env.COMMANDCODE_MODELS_URL = url
|
|
try {
|
|
// The endpoint is read when the extension loads, like pi does at startup.
|
|
const stub = createStubPi()
|
|
commandCodeProvider(stub.pi)
|
|
const provider = stub.provider
|
|
const { context, published } = createRefreshContext({ allowNetwork: true })
|
|
const models = await provider?.config.refreshModels?.(context)
|
|
|
|
assert.deepEqual(models?.map((model) => model.id), [
|
|
"deepseek/deepseek-v4.1-flash",
|
|
"vendor/fresh",
|
|
])
|
|
const persisted = published[0] as { persist?: { models: { id: string; provider: string }[] } }
|
|
assert.deepEqual(persisted.persist?.models.map((model) => model.id), [
|
|
"deepseek/deepseek-v4.1-flash",
|
|
"vendor/fresh",
|
|
])
|
|
assert.equal(persisted.persist?.models[0]?.provider, "commandcode")
|
|
} finally {
|
|
delete process.env.COMMANDCODE_MODELS_URL
|
|
}
|
|
},
|
|
)
|
|
})
|
|
|
|
test("refreshModels keeps the generated catalog when the endpoint is unreachable", async () => {
|
|
process.env.COMMANDCODE_MODELS_URL = "http://127.0.0.1:1/provider/v1/models"
|
|
try {
|
|
const stub = createStubPi()
|
|
commandCodeProvider(stub.pi)
|
|
const { context, published } = createRefreshContext({ allowNetwork: true })
|
|
const models = await stub.provider?.config.refreshModels?.(context)
|
|
|
|
assert.equal(models?.length, CATALOG.length)
|
|
assert.deepEqual(published, [])
|
|
} finally {
|
|
delete process.env.COMMANDCODE_MODELS_URL
|
|
}
|
|
})
|
|
|
|
test("refreshModels restores the persisted catalog without network access", async () => {
|
|
const stub = createStubPi()
|
|
commandCodeProvider(stub.pi)
|
|
const provider = stub.provider
|
|
|
|
const stored = [{ id: "stored/model", provider: "commandcode", name: "Stored" }]
|
|
const { context } = createRefreshContext({ allowNetwork: false, stored: { models: stored } })
|
|
const models = await provider?.config.refreshModels?.(context)
|
|
|
|
assert.deepEqual(models, stored)
|
|
})
|
|
|
|
test("message_end rewrites Command Code context overflow errors", () => {
|
|
const stub = createStubPi()
|
|
commandCodeProvider(stub.pi)
|
|
|
|
const result = stub.handlers.get("message_end")?.(
|
|
{
|
|
message: {
|
|
role: "assistant",
|
|
provider: "commandcode",
|
|
stopReason: "error",
|
|
errorMessage: "prompt is too long: 210000 tokens > 200000 maximum",
|
|
},
|
|
},
|
|
{ model: { provider: "commandcode" } },
|
|
) as { message?: { errorMessage?: string } } | undefined
|
|
|
|
assert.match(result?.message?.errorMessage ?? "", /^context_length_exceeded: /)
|
|
})
|
|
|
|
test("/commandcode-quota reports the account snapshot", async () => {
|
|
const stub = createStubPi()
|
|
commandCodeProvider(stub.pi)
|
|
|
|
const originalFetch = globalThis.fetch
|
|
const originalKey = process.env.COMMAND_CODE_API_KEY
|
|
process.env.COMMAND_CODE_API_KEY = "user_abc"
|
|
globalThis.fetch = (async (input: Parameters<typeof fetch>[0]) => {
|
|
const url = String(input)
|
|
if (url.includes("/alpha/whoami")) {
|
|
return new Response(JSON.stringify({ user: { userName: "shark-cat" } }), { status: 200 })
|
|
}
|
|
if (url.includes("/alpha/billing/credits")) {
|
|
return new Response(
|
|
JSON.stringify({ credits: { monthlyCredits: 12.5, purchasedCredits: 0, freeCredits: 0 } }),
|
|
{ status: 200 },
|
|
)
|
|
}
|
|
if (url.includes("/alpha/billing/subscriptions")) {
|
|
return new Response(JSON.stringify({ data: { planId: "go", status: "active" } }), { status: 200 })
|
|
}
|
|
return new Response(JSON.stringify({ totalCost: 1.23, totalCount: 45 }), { status: 200 })
|
|
}) as typeof fetch
|
|
|
|
const notifications: { message: string; type?: string }[] = []
|
|
const ctx = {
|
|
waitForIdle: async () => {},
|
|
ui: { notify: (message: string, type?: string) => notifications.push({ message, type }) },
|
|
} as unknown as ExtensionCommandContext
|
|
|
|
try {
|
|
await stub.commands.get("commandcode-quota")?.("", ctx)
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
if (originalKey === undefined) delete process.env.COMMAND_CODE_API_KEY
|
|
else process.env.COMMAND_CODE_API_KEY = originalKey
|
|
}
|
|
|
|
assert.equal(notifications.length, 1)
|
|
assert.equal(notifications[0]?.type, "info")
|
|
assert.match(notifications[0]?.message ?? "", /Account: shark-cat/)
|
|
assert.match(notifications[0]?.message ?? "", /Credits remaining: 12\.50/)
|
|
})
|