Skip to content
Open
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
34 changes: 34 additions & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Bug report
description: Report a reproducible provider problem
title: "Bug: "
body:
- type: markdown
attributes:
value: "Do not paste API keys, auth files, prompts, private tool results, or full session exports. Use a synthetic reproduction."
- type: textarea
id: diagnostics
attributes:
label: Safe diagnostics
description: "Run /commandcode-diagnostics in the failing instance and paste its output. This report excludes prompts, credentials and local paths. If the extension does not load, give the plugin, Pi/OMP, Node/Bun and OS versions instead."
render: text
validations:
required: true
- type: dropdown
id: plan
attributes:
label: Plan
options: [GOAT, Pro, Max, Team, Provider, Go, Unknown]
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Minimal reproduction
description: "Steps, expected/actual behavior, exact model ID, and whether this followed a model switch, compaction, update or session resume. Use synthetic data."
validations:
required: true
- type: textarea
id: error
attributes:
label: Error and timing
description: "Share the error code and approximate timestamp. Review/redact any error text yourself; do not include entire request or response bodies."
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

- Add content-free `/commandcode-diagnostics` reports, per-request body-chunk timing and error categories, explicit unknown-price warnings, and a structured GitHub bug form. Generate requests now honor the caller's fetch hook so both transports can be measured without retaining request bodies.

- Add `/commandcode-endpoint <model-id> auto|responses|chat-completions` for persistent per-model Provider API routing. Choices survive catalog refreshes and restarts, are validated against the model wire, and are shown in `/commandcode-status` (#156). No automatic mid-stream endpoint switching is performed.

## 0.7.8 - 2026-10-15
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ While pi is running, use these provider commands without restarting:

- `/commandcode-refresh` fetches and re-registers the current model catalog. Overlapping refreshes are coalesced, and a failed refresh keeps the last valid catalog active.
- `/commandcode-status` shows redacted discovery diagnostics, including the source, model count, timestamps, cache path, endpoint, and warning. If a Responses reasoning event has an unsupported shape, a separate stream warning remains visible until the extension is reloaded or the host restarts. The event is preserved unchanged; no reasoning text is included in the warning.
- `/commandcode-diagnostics` shows a shareable support report: plugin/host version, CLI snapshot, selected route and override, capability provenance, price status, and the latest request's HTTP status, attempts, outcome, first body-chunk latency and longest gap. It keeps only one request summary in memory. It never stores or exports prompts, tool content, credentials, raw errors, account information or local paths. Request IDs are included only when they match a bounded hexadecimal/UUID format. Timing is measured between body chunks, not tokens; first-chunk latency may include an error response. A request started most recently owns the report even if another request finishes later. Retry counts are scoped to that stream call, not host-level retries. Review the report before sharing it.
- `/commandcode-quota` shows your Command Code account usage and quota in a dashboard-style layout: credits remaining and used with a percentage, monthly/purchased/free sources, the current plan, available usage totals, the API key name, and the 5-hour and weekly usage windows.

The `commandcode-quota` command reads from the Command Code alpha usage endpoints (the same ones the `cmd` CLI `/usage` command uses): `whoami`, `billing/credits`, `billing/subscriptions`, and `usage/summary`. It authenticates with the same API key the provider already uses. If the command cannot reach those endpoints or an endpoint schema changes, unavailable sections are reported explicitly instead of being displayed as zero usage. Output is plain text (via `ui.notify`) so it works across pi and compatible hosts such as OMP.
Expand Down Expand Up @@ -173,7 +174,7 @@ The legacy generate transport resolves image support from the host's `model.inpu

The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi and OMP can display estimated request costs. DeepSeek V4 uses time-dependent rates: the extension applies the documented 2x peak rates on weekdays from 01:00-04:00 and 06:00-10:00 UTC, and off-peak rates at all other times, including the entire weekend. The same request-time selection is used by the native Provider API path and the legacy generate fallback.

Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. The Command Code Usage page remains authoritative for each request. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.
Models missing from that table display zero cost in pi. The provider warns once per selected unpriced model per load, and diagnostics distinguishes an unknown price from an explicitly free model. An expired tracked promotion is marked `review-required`; the snapshot verification date is always shown. This does **not** mean that Command Code will bill the request at zero. The Command Code Usage page remains authoritative for each request. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.

A read-only companion check, `npm run check:commandcode-pricing`, compares the live Provider API catalog and the official pricing page with the static table. It never writes prices: exit code `0` means every compared rate matches, `1` means drift or an expired promotion needs manual review, and `2` means the price could not be checked. The daily catalog workflow and applicable pull requests publish the Markdown report to the GitHub Actions summary. The pricing page has no public pricing API, so the check parses its private Next.js Flight payload; if that format changes the check fails loudly instead of reporting a false pass, and prices stay manually reviewed.

Expand Down
62 changes: 60 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
type ProviderConfig,
} from "@earendil-works/pi-coding-agent"
import { join } from "node:path"
import { readFile } from "node:fs/promises"
import * as hostPackage from "@earendil-works/pi-coding-agent"
import { createRequestDiagnostics, formatDiagnostics, priceStatus } from "./src/diagnostics.ts"

import { getConfiguredApiKey } from "./src/api-key.ts"
import { pickCommandCodeApiKey, withResolvedCommandCodeApiKey } from "./src/converters.ts"
Expand Down Expand Up @@ -197,6 +200,31 @@ function legacyApiBase(providerApiBase: string): string {
}

export default async function (pi: ExtensionAPI) {
const packageData: unknown = JSON.parse(
await readFile(new URL("./package.json", import.meta.url), "utf8"),
)
const pluginVersion =
typeof packageData === "object" &&
packageData !== null &&
"version" in packageData &&
typeof packageData.version === "string"
? packageData.version
: "unknown"
const hostVersion =
"VERSION" in hostPackage && typeof hostPackage.VERSION === "string"
? hostPackage.VERSION
: "unknown"
const diagnostics = createRequestDiagnostics()
const warnedPrices = new Set<string>()
pi.on("before_agent_start", (_event, ctx) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fires on every host start for anyone whose daily model is unpriced — a recurring nag for a condition the user cannot fix. Consider warning once per model per catalog snapshot (persist next to the routing file), or only when the model is actually switched to, rather than on every before_agent_start.

if (ctx.model?.provider !== "commandcode" || warnedPrices.has(ctx.model.id)) return
if (priceStatus(ctx.model.id) === "known") return
warnedPrices.add(ctx.model.id)
ctx.ui.notify(
"Command Code display price is unknown or needs review. A zero estimate does not mean free usage; check /commandcode-diagnostics.",
"warning",
)
})
// Hosts may select their built-in Command Code model before loading extensions.
// Rebind that stale selection to our registered transport before the first turn.
pi.on("session_start", async (_event, ctx) => {
Expand Down Expand Up @@ -320,8 +348,20 @@ export default async function (pi: ExtensionAPI) {
// custom api there so those calls reach the same transport. The registry
// resolves no credentials for extension providers, so fall back to the
// configured key when the caller passes none or a placeholder.
const measuredStream = (
model: Parameters<typeof transport.stream>[0],
context: Parameters<typeof transport.stream>[1],
options?: Parameters<typeof transport.stream>[2],
) =>
diagnostics.stream(
transport.stream,
() => new AssistantMessageEventStream(),
model,
context,
resolveStreamOptions(options),
)
const compatStream: CompatStreamFunction = (model, context, options) =>
transport.stream(model, context, resolveStreamOptions(options)) as AssistantMessageEventStream
measuredStream(model, context, options) as AssistantMessageEventStream
registerCompatApiProvider(compatStream)

pi.on("message_end", async (event, ctx) => {
Expand Down Expand Up @@ -349,7 +389,25 @@ export default async function (pi: ExtensionAPI) {
createProviderConfig: (models) => {
catalogModels.clear()
for (const model of models) catalogModels.set(model.id, model)
return createProviderConfig(models, apiBase, transport.stream)
return createProviderConfig(models, apiBase, measuredStream)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createProviderConfig now receives measuredStream, which also applies resolveStreamOptions to the main host path — previously only the compat path did that. Probably a no-op because the host already supplies apiKey, but it is an unannounced change on the primary path. Mention it in the PR/commit body, or add an assertion that a host-supplied key still wins over the configured key.

},
getDiagnostics: (model) => {
let route = "not selected"
if (model?.provider === "commandcode") {
try {
route = `${transport.getTransport()}/${resolveModelApi(model.id)} (${routing[model.id] ?? "auto"})`
} catch {
route = "invalid routing configuration"
}
}
return formatDiagnostics({
pluginVersion,
host: compatApiProviderRegistrar() ? "pi" : "omp-compatible",
hostVersion,
modelId: model?.provider === "commandcode" ? model.id : undefined,
route,
request: diagnostics.snapshot(),
})
},
getRoutingStatus: (model) => {
if (routingWarning) return `routing warning: ${routingWarning}`
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"LICENSE"
],
"scripts": {
"test": "npm run typecheck && npm run test:routing && node --test tests/test-release.mjs tests/test-pi-child-cleanup.mjs && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-pricing-check.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
"test": "npm run typecheck && npm run test:diagnostics && npm run test:routing && node --test tests/test-release.mjs tests/test-pi-child-cleanup.mjs && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-pricing-check.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && 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,9 +40,10 @@
"sync:commandcode-catalog": "tsx .github/scripts/check-commandcode-model-metadata.ts command-code@latest --write",
"test:quota": "tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts",
"test:usage": "tsx tests/test-usage.ts",
"test:unit": "npm run test:routing && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-pricing-check.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts",
"test:unit": "npm run test:diagnostics && npm run test:routing && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-pricing-check.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts",
"test:api-key": "tsx tests/test-api-key.ts",
"test:models": "tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts",
"test:diagnostics": "tsx tests/test-diagnostics.ts",
"test:routing": "tsx tests/test-routing.ts",
"test:runtime": "tsx tests/test-runtime.ts",
"test:pricing": "tsx tests/test-pricing.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {

try {
try {
response = await fetchImpl(`${apiBase}/alpha/generate`, {
response = await (options?.fetch ?? fetchImpl)(`${apiBase}/alpha/generate`, {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options.fetch now takes precedence over deps.fetchImpl on the generate path — the inverse of the previous precedence. Fine for the diagnostics use case, but worth a sentence in the commit body. (Becomes dead code if #164 lands.)

method: "POST",
headers: requestHeaders,
body: bodyStr,
Expand Down
Loading
Loading