pi routes the main chat through the registered provider, but sibling extensions that call streamSimple from @earendil-works/pi-ai/compat with the active Command Code model resolve model.api through the compat api-registry, which only knows built-in APIs. On plain pi that failed with "No API provider registered for api: commandcode-custom". Register commandcode-custom there and delegate to the transport router. The registry resolves no credentials for extension providers, so fall back to the configured Command Code key when the caller passes none. Closes #68 (cherry picked from commit 7e9659e672771c6a9223b95938e50fb2a051a0a7)
174 lines
5.9 KiB
TypeScript
174 lines
5.9 KiB
TypeScript
/**
|
|
* Command Code provider for pi.
|
|
*
|
|
* Uses Command Code's documented Provider API:
|
|
* https://api.commandcode.ai/provider/v1
|
|
*/
|
|
|
|
import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
|
|
import {
|
|
registerApiProvider,
|
|
streamSimple as streamNativeProvider,
|
|
type ApiStreamSimpleFunction,
|
|
} from "@earendil-works/pi-ai/compat"
|
|
import {
|
|
getAgentDir,
|
|
type ExtensionAPI,
|
|
type ExtensionCommandContext,
|
|
type ProviderConfig,
|
|
} from "@earendil-works/pi-coding-agent"
|
|
import { join } from "node:path"
|
|
|
|
import { getConfiguredApiKey } from "./src/api-key.ts"
|
|
import { createStreamCommandCode } from "./src/core.ts"
|
|
import { calculateCommandCodeCost } from "./src/cost.ts"
|
|
import {
|
|
apiForModelId,
|
|
baseUrlForModel,
|
|
DEFAULT_MODELS_URL,
|
|
DEFAULT_PROVIDER_API_BASE,
|
|
getModelsTimeoutMs,
|
|
inputModalitiesForModel,
|
|
loadCommandCodeModels,
|
|
MODEL_EFFORTS,
|
|
thinkingMetadataForModel,
|
|
type CommandCodeModel,
|
|
} from "./src/models.ts"
|
|
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
|
|
import { normalizeCommandCodeMessage } from "./src/overflow.ts"
|
|
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
|
|
import { registerCommandCodeQuota } from "./src/quota-command.ts"
|
|
import { createCommandCodeRuntime } from "./src/runtime.ts"
|
|
import { createCommandCodeTransportRouter } from "./src/transport.ts"
|
|
|
|
const COMMAND_CODE_API = "commandcode-custom"
|
|
const COMPAT_SOURCE_ID = "pi-commandcode-provider"
|
|
|
|
function commandCodeHeaders(): Record<string, string> | undefined {
|
|
if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
|
|
return { "x-cmd-zdr": "1" }
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function createProviderConfig(
|
|
models: readonly CommandCodeModel[],
|
|
apiBase: string,
|
|
streamCommandCode: ProviderConfig["streamSimple"],
|
|
): ProviderConfig {
|
|
const headers = commandCodeHeaders()
|
|
return {
|
|
name: "Command Code",
|
|
baseUrl: apiBase,
|
|
apiKey: getConfiguredApiKey() ?? "$COMMAND_CODE_API_KEY",
|
|
api: COMMAND_CODE_API,
|
|
streamSimple: streamCommandCode,
|
|
headers,
|
|
oauth: {
|
|
name: "Command Code",
|
|
login,
|
|
refreshToken,
|
|
getApiKey: getOAuthApiKey,
|
|
},
|
|
models: models.map((model) => ({
|
|
id: model.id,
|
|
name: model.name,
|
|
api: COMMAND_CODE_API,
|
|
baseUrl: baseUrlForModel(apiBase, model.api),
|
|
reasoning: model.reasoning,
|
|
...(thinkingMetadataForModel(model.id) ?? {}),
|
|
input: [...inputModalitiesForModel(model.id)],
|
|
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
|
|
contextWindow: model.contextWindow,
|
|
maxTokens: model.maxTokens,
|
|
headers,
|
|
compat:
|
|
model.api === "openai-completions"
|
|
? {
|
|
supportsStore: false,
|
|
supportsDeveloperRole: false,
|
|
supportsReasoningEffort: MODEL_EFFORTS[model.id] !== undefined,
|
|
maxTokensField: "max_tokens",
|
|
}
|
|
: {
|
|
supportsEagerToolInputStreaming: false,
|
|
supportsLongCacheRetention: false,
|
|
supportsCacheControlOnTools: false,
|
|
supportsToolReferences: false,
|
|
...(model.reasoning ? { forceAdaptiveThinking: true } : {}),
|
|
},
|
|
})),
|
|
}
|
|
}
|
|
|
|
function legacyApiBase(providerApiBase: string): string {
|
|
return providerApiBase.replace(/\/provider\/v1\/?$/, "")
|
|
}
|
|
|
|
export default async function (pi: ExtensionAPI) {
|
|
const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_PROVIDER_API_BASE
|
|
const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
|
|
const modelsTimeoutMs = getModelsTimeoutMs()
|
|
const modelsCachePath =
|
|
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
|
|
const streamGenerate = createStreamCommandCode({
|
|
createStream: () => new AssistantMessageEventStream(),
|
|
calculateCost: calculateCommandCodeCost,
|
|
apiBase: legacyApiBase(apiBase),
|
|
})
|
|
const transport = createCommandCodeTransportRouter({
|
|
createStream: () => new AssistantMessageEventStream(),
|
|
streamProvider: (model, context, options) =>
|
|
streamNativeProvider(
|
|
{ ...model, api: apiForModelId(model.id), compat: model.compatConfig ?? model.compat },
|
|
context,
|
|
options,
|
|
),
|
|
streamGenerate,
|
|
})
|
|
|
|
// pi dispatches the main chat through the registered provider, but sibling
|
|
// extensions that call `streamSimple` from `@earendil-works/pi-ai/compat`
|
|
// with a Command Code model resolve `model.api` through the compat
|
|
// api-registry, which knows nothing about extension providers. Register the
|
|
// 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.
|
|
const compatStream: ApiStreamSimpleFunction = (model, context, options) =>
|
|
transport.stream(
|
|
model,
|
|
context,
|
|
options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() },
|
|
) as AssistantMessageEventStream
|
|
registerApiProvider(
|
|
{ api: COMMAND_CODE_API, stream: compatStream, streamSimple: compatStream },
|
|
COMPAT_SOURCE_ID,
|
|
)
|
|
|
|
pi.on("message_end", async (event, ctx) => {
|
|
if (event.message.role !== "assistant") return
|
|
const normalized = normalizeCommandCodeMessage(event.message, ctx.model?.provider)
|
|
return normalized ? { message: normalized.message } : undefined
|
|
})
|
|
|
|
registerCommandCodeQuota(pi, {
|
|
apiBase: legacyApiBase(apiBase),
|
|
headers: commandCodeHeaders(),
|
|
})
|
|
|
|
const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
|
|
endpoint: modelsUrl,
|
|
cachePath: modelsCachePath,
|
|
loadModels: () =>
|
|
loadCommandCodeModels({
|
|
url: modelsUrl,
|
|
cachePath: modelsCachePath,
|
|
timeoutMs: modelsTimeoutMs,
|
|
}),
|
|
createProviderConfig: (models) => createProviderConfig(models, apiBase, transport.stream),
|
|
getTransport: transport.getTransport,
|
|
})
|
|
|
|
await runtime.initialize()
|
|
}
|