merge main (0.6.0) into fix/omp-developer-messages
Align the developer-message fix with the Provider API transport router. The fix stays scoped to the legacy /alpha/generate converter, and the OMP advisory compat test now forces the upgrade_required fallback so the advisory is asserted on the generate request body.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
function defaultAuthPaths(home: string): string[] {
|
||||
return [
|
||||
join(home, ".commandcode", "auth.json"),
|
||||
join(home, ".pi", "agent", "auth.json"),
|
||||
join(home, ".omp", "agent", "auth.json"),
|
||||
]
|
||||
}
|
||||
|
||||
function apiKeyFromCredential(value: unknown): string | undefined {
|
||||
if (!isRecord(value)) return undefined
|
||||
|
||||
if (stringValue(value.type) === "oauth") return stringValue(value.access)
|
||||
if (stringValue(value.type) === "api") return stringValue(value.key)
|
||||
return stringValue(value.access) ?? stringValue(value.key)
|
||||
}
|
||||
|
||||
export function getConfiguredApiKey(
|
||||
options: {
|
||||
env?: NodeJS.ProcessEnv
|
||||
authPaths?: readonly string[]
|
||||
homeDir?: () => string
|
||||
} = {},
|
||||
): string | undefined {
|
||||
const env = options.env ?? process.env
|
||||
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY
|
||||
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
||||
|
||||
const home = options.homeDir?.() ?? homedir()
|
||||
const authPaths = options.authPaths ?? defaultAuthPaths(home)
|
||||
|
||||
for (const authPath of authPaths) {
|
||||
try {
|
||||
if (!existsSync(authPath)) continue
|
||||
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
||||
if (!isRecord(parsed)) continue
|
||||
|
||||
const apiKey = stringValue(parsed.apiKey)
|
||||
if (apiKey) return apiKey
|
||||
|
||||
const commandcode = stringValue(parsed.commandcode)
|
||||
if (commandcode) return commandcode
|
||||
|
||||
const providerKey = apiKeyFromCredential(parsed.commandcode)
|
||||
if (providerKey) return providerKey
|
||||
|
||||
const commandCode = stringValue(parsed["command-code"])
|
||||
if (commandCode) return commandCode
|
||||
|
||||
const commandCodeKey = apiKeyFromCredential(parsed["command-code"])
|
||||
if (commandCodeKey) return commandCodeKey
|
||||
} catch {
|
||||
// Ignore malformed or unreadable auth files.
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export interface AuthServer {
|
||||
export interface AuthServerOptions {
|
||||
startPort?: number
|
||||
portRange?: number
|
||||
expectedState?: string
|
||||
}
|
||||
|
||||
function listenOnAvailablePort(
|
||||
@@ -181,6 +182,12 @@ export async function startAuthServer(options: AuthServerOptions = {}): Promise<
|
||||
return
|
||||
}
|
||||
|
||||
if (options.expectedState !== undefined && state !== options.expectedState) {
|
||||
res.writeHead(403)
|
||||
res.end(JSON.stringify({ success: false, error: "Invalid state token" }))
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(200)
|
||||
res.end(JSON.stringify({ success: true }))
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
export const COMMAND_CODE_CLI_VERSION = "1.32.2"
|
||||
|
||||
export type CommandCodeInputType = "text" | "image"
|
||||
export type CommandCodeReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
|
||||
|
||||
/**
|
||||
* Generated from command-code@1.32.2 by `npm run sync:commandcode-catalog`.
|
||||
* Do not edit manually.
|
||||
*/
|
||||
export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>> = {
|
||||
"claude-fable-5": ["text", "image"],
|
||||
"claude-haiku-4-5-20251001": ["text", "image"],
|
||||
"claude-opus-4-7": ["text", "image"],
|
||||
"claude-opus-4-8": ["text", "image"],
|
||||
"claude-opus-5": ["text", "image"],
|
||||
"claude-sonnet-4-6": ["text", "image"],
|
||||
"claude-sonnet-5": ["text", "image"],
|
||||
"deepseek/deepseek-v4-flash-vision-exp": ["text", "image"],
|
||||
"google/gemini-3.1-flash-lite": ["text", "image"],
|
||||
"google/gemini-3.5-flash": ["text", "image"],
|
||||
"google/gemini-3.5-flash-lite": ["text", "image"],
|
||||
"google/gemini-3.6-flash": ["text", "image"],
|
||||
"google/gemini-3.7-flash": ["text", "image"],
|
||||
"gpt-5.3-codex": ["text", "image"],
|
||||
"gpt-5.4": ["text", "image"],
|
||||
"gpt-5.4-mini": ["text", "image"],
|
||||
"gpt-5.5": ["text", "image"],
|
||||
"gpt-5.6-luna": ["text", "image"],
|
||||
"gpt-5.6-sol": ["text", "image"],
|
||||
"gpt-5.6-terra": ["text", "image"],
|
||||
"meta/muse-spark-1.1": ["text", "image"],
|
||||
"meta/muse-spark-1.2": ["text", "image"],
|
||||
"meta/muse-spark-1.2-contributor": ["text", "image"],
|
||||
"MiniMaxAI/MiniMax-M3": ["text", "image"],
|
||||
"moonshotai/Kimi-K2.5": ["text", "image"],
|
||||
"moonshotai/Kimi-K2.6": ["text", "image"],
|
||||
"moonshotai/Kimi-K2.7-Code": ["text", "image"],
|
||||
"moonshotai/Kimi-K2.7-Code-Highspeed": ["text", "image"],
|
||||
"moonshotai/Kimi-K3": ["text", "image"],
|
||||
"Qwen/Qwen3.6-Plus": ["text", "image"],
|
||||
"Qwen/Qwen3.7-Flash": ["text", "image"],
|
||||
"Qwen/Qwen3.7-Plus": ["text", "image"],
|
||||
"Qwen/Qwen3.8-27B": ["text", "image"],
|
||||
"Qwen/Qwen3.8-Max": ["text", "image"],
|
||||
"sakana/fugu-ultra": ["text", "image"],
|
||||
"stealth/ox-alpha": ["text", "image"],
|
||||
"stepfun/Step-3.7-Flash": ["text", "image"],
|
||||
"thinkingmachines/inkling": ["text", "image"],
|
||||
"thinkingmachines/inkling-small": ["text", "image"],
|
||||
"xai/grok-4.5": ["text", "image"],
|
||||
"xiaomi/mimo-v2.5": ["text", "image"],
|
||||
}
|
||||
|
||||
export const MODEL_REASONING: Readonly<Record<string, true>> = {
|
||||
"claude-fable-5": true,
|
||||
"claude-opus-4-7": true,
|
||||
"claude-opus-4-8": true,
|
||||
"claude-opus-5": true,
|
||||
"claude-sonnet-4-6": true,
|
||||
"claude-sonnet-5": true,
|
||||
"deepseek/deepseek-v4-flash": true,
|
||||
"deepseek/deepseek-v4-flash-vision-exp": true,
|
||||
"deepseek/deepseek-v4-pro": true,
|
||||
"google/gemini-3.1-flash-lite": true,
|
||||
"google/gemini-3.5-flash": true,
|
||||
"google/gemini-3.5-flash-lite": true,
|
||||
"google/gemini-3.6-flash": true,
|
||||
"google/gemini-3.7-flash": true,
|
||||
"gpt-5.3-codex": true,
|
||||
"gpt-5.4": true,
|
||||
"gpt-5.4-mini": true,
|
||||
"gpt-5.5": true,
|
||||
"gpt-5.6-luna": true,
|
||||
"gpt-5.6-sol": true,
|
||||
"gpt-5.6-terra": true,
|
||||
"meta/muse-spark-1.1": true,
|
||||
"meta/muse-spark-1.2": true,
|
||||
"meta/muse-spark-1.2-contributor": true,
|
||||
"MiniMaxAI/MiniMax-M3": true,
|
||||
"moonshotai/Kimi-K2.7-Code": true,
|
||||
"moonshotai/Kimi-K2.7-Code-Highspeed": true,
|
||||
"moonshotai/Kimi-K3": true,
|
||||
"nvidia/nemotron-3-ultra-550b-a55b": true,
|
||||
"poolside/laguna-s-2.1-free": true,
|
||||
"Qwen/Qwen3.6-Max-Preview": true,
|
||||
"Qwen/Qwen3.6-Plus": true,
|
||||
"Qwen/Qwen3.7-Flash": true,
|
||||
"Qwen/Qwen3.7-Max": true,
|
||||
"Qwen/Qwen3.7-Plus": true,
|
||||
"Qwen/Qwen3.8-27B": true,
|
||||
"Qwen/Qwen3.8-Max": true,
|
||||
"sakana/fugu-ultra": true,
|
||||
"stealth/ox-alpha": true,
|
||||
"stepfun/Step-3.5-Flash": true,
|
||||
"stepfun/Step-3.7-Flash": true,
|
||||
"tencent/hy3-paid": true,
|
||||
"thinkingmachines/inkling": true,
|
||||
"thinkingmachines/inkling-small": true,
|
||||
"xai/grok-4.5": true,
|
||||
"xai/grok-4.6": true,
|
||||
"zai-org/GLM-5.2": true,
|
||||
"zai-org/GLM-5.3": true,
|
||||
}
|
||||
|
||||
export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
|
||||
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
||||
"deepseek/deepseek-v4-flash": ["high", "max"],
|
||||
"deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
|
||||
"deepseek/deepseek-v4-pro": ["high", "max"],
|
||||
"google/gemini-3.1-flash-lite": ["low", "medium", "high"],
|
||||
"google/gemini-3.5-flash": ["low", "medium", "high"],
|
||||
"google/gemini-3.5-flash-lite": ["low", "medium", "high"],
|
||||
"google/gemini-3.6-flash": ["low", "medium", "high"],
|
||||
"google/gemini-3.7-flash": ["low", "medium", "high"],
|
||||
"gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
|
||||
"gpt-5.4": ["low", "medium", "high", "xhigh"],
|
||||
"gpt-5.4-mini": ["low", "medium", "high"],
|
||||
"gpt-5.5": ["low", "medium", "high", "xhigh"],
|
||||
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
|
||||
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
|
||||
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
|
||||
"Qwen/Qwen3.8-27B": ["low", "medium", "xhigh"],
|
||||
"Qwen/Qwen3.8-Max": ["low", "medium", "xhigh"],
|
||||
"sakana/fugu-ultra": ["high", "xhigh"],
|
||||
"stealth/ox-alpha": ["low", "high", "max"],
|
||||
"xai/grok-4.5": ["low", "medium", "high"],
|
||||
"xai/grok-4.6": ["low", "medium", "high", "xhigh"],
|
||||
"zai-org/GLM-5.2": ["high", "max"],
|
||||
"zai-org/GLM-5.3": ["low", "high", "max"],
|
||||
}
|
||||
|
||||
export const MODEL_MAX_OUTPUT_TOKENS: Readonly<Record<string, number>> = {
|
||||
"poolside/laguna-s-2.1-free": 32_768,
|
||||
"Qwen/Qwen3.8-27B": 32_768,
|
||||
"stealth/ox-alpha": 131_072,
|
||||
}
|
||||
+72
-16
@@ -66,9 +66,8 @@ function imageContentError(role: string): Error {
|
||||
|
||||
export function assertTextOnlyMessages(messages?: readonly MessageLike[]): void {
|
||||
for (const message of messages ?? []) {
|
||||
if (imageParts(message.content).length > 0) {
|
||||
const role = message.role === "toolResult" ? "tool results" : `${message.role} messages`
|
||||
throw imageContentError(role)
|
||||
if (message.role !== "toolResult" && imageParts(message.content).length > 0) {
|
||||
throw imageContentError(`${message.role} messages`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,6 +106,7 @@ export function getApiKey(
|
||||
} = {},
|
||||
): string | undefined {
|
||||
const env = options.env ?? process.env
|
||||
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY
|
||||
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
||||
|
||||
const home = options.homeDir?.() ?? homedir()
|
||||
@@ -138,7 +138,41 @@ export function getApiKey(
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Hosts such as OMP may pass a literal env-var name as the "resolved" registry
|
||||
// key instead of the actual credential. Treat those as unresolved.
|
||||
export const COMMAND_CODE_PLACEHOLDER_KEYS = new Set([
|
||||
"$COMMAND_CODE_API_KEY",
|
||||
"COMMAND_CODE_API_KEY",
|
||||
"$COMMANDCODE_API_KEY",
|
||||
"COMMANDCODE_API_KEY",
|
||||
])
|
||||
|
||||
/**
|
||||
* Pick the real API key from a host registry value and/or the env/auth-file
|
||||
* fallback, never returning a literal placeholder or an empty/whitespace value.
|
||||
* Pure/testable.
|
||||
*/
|
||||
export function pickCommandCodeApiKey(
|
||||
registryKey: string | undefined,
|
||||
hostKey: string | undefined,
|
||||
): string | undefined {
|
||||
const trimmed = typeof registryKey === "string" ? registryKey.trim() : undefined
|
||||
if (!trimmed) return hostKey
|
||||
if (COMMAND_CODE_PLACEHOLDER_KEYS.has(trimmed)) return hostKey
|
||||
return trimmed
|
||||
}
|
||||
|
||||
export function textContent(message: { content?: unknown }): string {
|
||||
if (typeof message.content === "string") return message.content
|
||||
if (message.content === null || message.content === undefined) return ""
|
||||
if (!Array.isArray(message.content)) {
|
||||
try {
|
||||
return JSON.stringify(message.content) ?? String(message.content)
|
||||
} catch {
|
||||
return String(message.content)
|
||||
}
|
||||
}
|
||||
|
||||
return recordArray(message.content)
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => stringValue(part.text) ?? "")
|
||||
@@ -159,7 +193,12 @@ export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
||||
interface ToolCallState {
|
||||
callIds: ReadonlySet<string>
|
||||
resultIds: ReadonlySet<string>
|
||||
}
|
||||
|
||||
function toolCallState(messages?: readonly MessageLike[]): ToolCallState {
|
||||
const callIds = new Set<string>()
|
||||
const resultIds = new Set<string>()
|
||||
|
||||
@@ -171,12 +210,12 @@ function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
||||
if (id) callIds.add(id)
|
||||
}
|
||||
}
|
||||
} else if (message.role === "toolResult") {
|
||||
if (message.toolCallId) resultIds.add(message.toolCallId)
|
||||
} else if (message.role === "toolResult" && message.toolCallId) {
|
||||
resultIds.add(message.toolCallId)
|
||||
}
|
||||
}
|
||||
|
||||
return new Set([...callIds].filter((id) => resultIds.has(id)))
|
||||
return { callIds, resultIds }
|
||||
}
|
||||
|
||||
export function messagesToCC(
|
||||
@@ -187,7 +226,7 @@ export function messagesToCC(
|
||||
if (!allowImages) assertTextOnlyMessages(messages)
|
||||
|
||||
const out: unknown[] = []
|
||||
const pairedToolCallIds = completeToolCallIds(messages)
|
||||
const { callIds, resultIds } = toolCallState(messages)
|
||||
|
||||
for (const message of messages ?? []) {
|
||||
if (message.role === "user" || message.role === "developer") {
|
||||
@@ -202,23 +241,42 @@ export function messagesToCC(
|
||||
})
|
||||
} else if (message.role === "assistant") {
|
||||
const parts: unknown[] = []
|
||||
const missingResults: unknown[] = []
|
||||
for (const content of recordArray(message.content)) {
|
||||
if (content.type === "text") {
|
||||
parts.push({ type: "text", text: stringValue(content.text) ?? "" })
|
||||
} else if (content.type === "toolCall") {
|
||||
const toolCallId = stringValue(content.id) ?? ""
|
||||
if (!pairedToolCallIds.has(toolCallId)) continue
|
||||
const toolName = stringValue(content.name) ?? ""
|
||||
if (!toolCallId) continue
|
||||
parts.push({
|
||||
type: "tool-call",
|
||||
toolCallId,
|
||||
toolName: stringValue(content.name) ?? "",
|
||||
toolName,
|
||||
input: recordOrEmpty(content.arguments),
|
||||
})
|
||||
if (!resultIds.has(toolCallId)) {
|
||||
missingResults.push({
|
||||
type: "tool-result",
|
||||
toolCallId,
|
||||
toolName,
|
||||
output: {
|
||||
type: "error-text",
|
||||
value: "No result — the tool call did not complete (interrupted or lost).",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) out.push({ role: "assistant", content: parts })
|
||||
if (missingResults.length > 0) out.push({ role: "tool", content: missingResults })
|
||||
} else if (message.role === "toolResult") {
|
||||
if (!message.toolCallId || !pairedToolCallIds.has(message.toolCallId)) continue
|
||||
if (!message.toolCallId || !callIds.has(message.toolCallId)) continue
|
||||
const images = imageParts(message.content)
|
||||
const text = textContent(message)
|
||||
const outputText =
|
||||
text ||
|
||||
(images.length > 0 && !allowImages ? "[Image omitted: model does not support images]" : "")
|
||||
out.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
@@ -227,15 +285,13 @@ export function messagesToCC(
|
||||
toolCallId: message.toolCallId,
|
||||
toolName: message.toolName,
|
||||
output: message.isError
|
||||
? { type: "error-text", value: textContent(message) }
|
||||
: { type: "text", value: textContent(message) },
|
||||
? { type: "error-text", value: outputText }
|
||||
: { type: "text", value: outputText },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const images = imageParts(message.content)
|
||||
if (images.length > 0) {
|
||||
if (!allowImages) throw imageContentError("tool results")
|
||||
if (images.length > 0 && allowImages) {
|
||||
out.push({
|
||||
role: "user",
|
||||
content: images.map(imageToCommandCode),
|
||||
|
||||
+122
-28
@@ -7,6 +7,7 @@
|
||||
|
||||
import { randomUUID } from "node:crypto"
|
||||
|
||||
import { COMMAND_CODE_CLI_VERSION } from "./commandcode-catalog.ts"
|
||||
import { commandCodeErrorMessage, redactCommandCodeErrorText } from "./overflow.ts"
|
||||
import { modelSupportsImageInput } from "./models.ts"
|
||||
import {
|
||||
@@ -43,7 +44,7 @@ export * from "./overflow.ts"
|
||||
export * from "./types.ts"
|
||||
|
||||
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
||||
export const COMMAND_CODE_CLI_VERSION = "1.15.1"
|
||||
export { COMMAND_CODE_CLI_VERSION }
|
||||
|
||||
const DEFAULT_GENERATE_MAX_TOKENS = 64_000
|
||||
const DEFAULT_MAX_RETRIES = 0
|
||||
@@ -147,6 +148,10 @@ function mappedReasoningEffort(model: ModelLike, options?: StreamOptions): strin
|
||||
return typeof mapped === "string" && mapped !== "off" ? mapped : undefined
|
||||
}
|
||||
|
||||
function isUuid(value: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
||||
}
|
||||
|
||||
export function projectSlugFromPath(pathName: string): string {
|
||||
const slug = pathName
|
||||
.toLowerCase()
|
||||
@@ -231,17 +236,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
const stream = deps.createStream()
|
||||
|
||||
async function run() {
|
||||
// OMP may pass the legacy env-var name "COMMANDCODE_API_KEY" (old pi)
|
||||
// or "$COMMANDCODE_API_KEY" (new pi) as the apiKey value instead of
|
||||
// resolving it. Filter out these specific strings.
|
||||
const LEGACY_API_KEY_REF = "$COMMANDCODE_API_KEY"
|
||||
const OLD_API_KEY_REF = "COMMANDCODE_API_KEY"
|
||||
// Some hosts pass a literal env-var reference instead of resolving it.
|
||||
const PLACEHOLDER_API_KEYS = new Set([
|
||||
"$COMMAND_CODE_API_KEY",
|
||||
"COMMAND_CODE_API_KEY",
|
||||
"$COMMANDCODE_API_KEY",
|
||||
"COMMANDCODE_API_KEY",
|
||||
])
|
||||
const hostKey =
|
||||
options?.apiKey &&
|
||||
options.apiKey !== LEGACY_API_KEY_REF &&
|
||||
options.apiKey !== OLD_API_KEY_REF
|
||||
? options.apiKey
|
||||
: undefined
|
||||
options?.apiKey && !PLACEHOLDER_API_KEYS.has(options.apiKey) ? options.apiKey : undefined
|
||||
|
||||
const apiKey =
|
||||
hostKey ??
|
||||
@@ -261,7 +264,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
usage: defaultUsage(),
|
||||
stopReason: "error",
|
||||
errorMessage:
|
||||
"No Command Code API key. Run /login and select Command Code, set the COMMANDCODE_API_KEY env var, or configure ~/.commandcode/auth.json, ~/.pi/agent/auth.json or ~/.omp/agent/auth.json",
|
||||
"No Command Code API key. Run /login and select Command Code, set COMMAND_CODE_API_KEY (or legacy COMMANDCODE_API_KEY), or configure ~/.commandcode/auth.json, ~/.pi/agent/auth.json or ~/.omp/agent/auth.json",
|
||||
timestamp: now(),
|
||||
}
|
||||
stream.push({ type: "error", reason: "error", error: msg })
|
||||
@@ -285,6 +288,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
let textBlock: TextContent | undefined
|
||||
let currentTextIdx = -1
|
||||
let thinkingIdx = -1
|
||||
const streamingToolCalls = new Map<
|
||||
string,
|
||||
{ contentIndex: number; toolCall: ToolCallContent; partialArgs: string }
|
||||
>()
|
||||
let finished = false
|
||||
|
||||
const abortUpstream = () => {
|
||||
@@ -397,25 +404,81 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-input-start": {
|
||||
endTextBlock()
|
||||
endThinking()
|
||||
const id = stringValue(event.id)
|
||||
if (!id || streamingToolCalls.has(id)) break
|
||||
|
||||
const toolCall: ToolCallContent = {
|
||||
type: "toolCall",
|
||||
id,
|
||||
name: stringValue(event.toolName) ?? "",
|
||||
arguments: {},
|
||||
}
|
||||
output.content.push(toolCall)
|
||||
const contentIndex = output.content.length - 1
|
||||
streamingToolCalls.set(id, { contentIndex, toolCall, partialArgs: "" })
|
||||
stream.push({
|
||||
type: "toolcall_start",
|
||||
contentIndex,
|
||||
partial: output,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-input-delta": {
|
||||
const id = stringValue(event.id)
|
||||
const delta = stringValue(event.delta)
|
||||
if (!id || delta === undefined) break
|
||||
const active = streamingToolCalls.get(id)
|
||||
if (!active) break
|
||||
|
||||
active.partialArgs += delta
|
||||
active.toolCall.arguments = recordOrEmpty(active.partialArgs)
|
||||
stream.push({
|
||||
type: "toolcall_delta",
|
||||
contentIndex: active.contentIndex,
|
||||
delta,
|
||||
partial: output,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-input-end": {
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-call": {
|
||||
endTextBlock()
|
||||
endThinking()
|
||||
const toolCall: ToolCallContent = {
|
||||
const id = stringValue(event.toolCallId) ?? ""
|
||||
const active = streamingToolCalls.get(id)
|
||||
const toolCall: ToolCallContent = active?.toolCall ?? {
|
||||
type: "toolCall",
|
||||
id: stringValue(event.toolCallId) ?? "",
|
||||
id,
|
||||
name: stringValue(event.toolName) ?? "",
|
||||
arguments: recordOrEmpty(event.input ?? event.args ?? event.arguments),
|
||||
arguments: {},
|
||||
}
|
||||
toolCall.name = stringValue(event.toolName) ?? toolCall.name
|
||||
toolCall.arguments = recordOrEmpty(event.input ?? event.args ?? event.arguments)
|
||||
|
||||
let contentIndex: number
|
||||
if (active) {
|
||||
contentIndex = active.contentIndex
|
||||
streamingToolCalls.delete(id)
|
||||
} else {
|
||||
output.content.push(toolCall)
|
||||
contentIndex = output.content.length - 1
|
||||
stream.push({
|
||||
type: "toolcall_start",
|
||||
contentIndex,
|
||||
partial: output,
|
||||
})
|
||||
}
|
||||
output.content.push(toolCall)
|
||||
const idx = output.content.length - 1
|
||||
stream.push({
|
||||
type: "toolcall_start",
|
||||
contentIndex: idx,
|
||||
partial: output,
|
||||
})
|
||||
stream.push({
|
||||
type: "toolcall_end",
|
||||
contentIndex: idx,
|
||||
contentIndex,
|
||||
toolCall,
|
||||
partial: output,
|
||||
})
|
||||
@@ -423,6 +486,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
}
|
||||
|
||||
case "finish": {
|
||||
const rawFinishReason = stringValue(event.rawFinishReason)
|
||||
if (
|
||||
rawFinishReason &&
|
||||
/^(?:network|connection|upstream)[-_\s]?error$/i.test(rawFinishReason)
|
||||
) {
|
||||
throw new Error(
|
||||
`Provider finished with reason "${rawFinishReason}" — upstream connection failed mid-stream`,
|
||||
)
|
||||
}
|
||||
const usage = commandCodeUsage(event)
|
||||
if (usage) {
|
||||
const details = commandCodeInputTokenDetails(usage)
|
||||
@@ -446,6 +518,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
break
|
||||
}
|
||||
|
||||
case "abort": {
|
||||
throw abortError("Request aborted")
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const message =
|
||||
commandCodeErrorMessage(event.error) ??
|
||||
@@ -463,7 +539,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
if (controller.signal.aborted) throw abortError("Aborted")
|
||||
|
||||
const workingDir = cwd()
|
||||
const threadId = uuid()
|
||||
const threadId = options?.sessionId
|
||||
? isUuid(options.sessionId)
|
||||
? options.sessionId
|
||||
: undefined
|
||||
: uuid()
|
||||
const reasoningEffort = mappedReasoningEffort(model, options)
|
||||
const timeoutMs = options?.timeoutMs
|
||||
|
||||
@@ -491,8 +571,8 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
tools: toolsToJson(context.tools),
|
||||
system: systemPromptToText(context.systemPrompt),
|
||||
max_tokens: generateMaxTokens(model, options),
|
||||
temperature: 0.3,
|
||||
stream: true,
|
||||
...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
|
||||
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
||||
},
|
||||
threadId,
|
||||
@@ -522,7 +602,8 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
"x-cli-environment": "production",
|
||||
"x-project-slug": projectSlugFromPath(workingDir),
|
||||
"x-taste-learning": "true",
|
||||
"x-co-flag": "false",
|
||||
...(options?.sessionId ? { "x-session-id": options.sessionId } : {}),
|
||||
"User-Agent": "cli",
|
||||
...options?.headers,
|
||||
}
|
||||
const bodyStr = JSON.stringify(body)
|
||||
@@ -635,6 +716,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
const { done, value } = await raceAbort(reader.read(), attemptController.signal)
|
||||
if (done) {
|
||||
if (buffer.trim()) handleEvent(parseStreamEventLine(buffer))
|
||||
if (!finished) {
|
||||
throw new Error(
|
||||
"Stream ended unexpectedly before completion (no finish event) — response was truncated",
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
if (controller.signal.aborted) throw abortError("Aborted")
|
||||
@@ -658,7 +744,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
} catch {}
|
||||
reader = undefined
|
||||
|
||||
if (controller.signal.aborted) throw streamError
|
||||
if (
|
||||
controller.signal.aborted ||
|
||||
(streamError instanceof Error && streamError.name === "AbortError")
|
||||
) {
|
||||
throw streamError
|
||||
}
|
||||
|
||||
// Never retry after visible content was emitted (including timeout mid-stream).
|
||||
const canRetry = output.content.length === 0 && attempt < maxRetries
|
||||
@@ -695,7 +786,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const reason: ErrorReason = controller.signal.aborted ? "aborted" : "error"
|
||||
const reason: ErrorReason =
|
||||
controller.signal.aborted || (error instanceof Error && error.name === "AbortError")
|
||||
? "aborted"
|
||||
: "error"
|
||||
output.stopReason = reason
|
||||
output.errorMessage =
|
||||
reason === "aborted"
|
||||
|
||||
+50
-97
@@ -1,58 +1,26 @@
|
||||
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
||||
import { dirname } from "node:path"
|
||||
|
||||
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
|
||||
import {
|
||||
MODEL_EFFORTS,
|
||||
MODEL_INPUT_MODALITIES,
|
||||
MODEL_MAX_OUTPUT_TOKENS,
|
||||
MODEL_REASONING,
|
||||
type CommandCodeInputType,
|
||||
type CommandCodeReasoningEffort,
|
||||
} from "./commandcode-catalog.ts"
|
||||
|
||||
export { MODEL_EFFORTS, MODEL_INPUT_MODALITIES, MODEL_MAX_OUTPUT_TOKENS, MODEL_REASONING }
|
||||
export type { CommandCodeInputType }
|
||||
|
||||
export const DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1"
|
||||
export const DEFAULT_MODELS_URL = `${DEFAULT_PROVIDER_API_BASE}/models`
|
||||
export const DEFAULT_MODELS_TIMEOUT_MS = 10_000
|
||||
|
||||
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
|
||||
const MODEL_CACHE_VERSION = 1
|
||||
|
||||
export type CommandCodeInputType = "text" | "image"
|
||||
|
||||
/**
|
||||
* Model input modalities from the command-code@1.15.1 bundled catalog.
|
||||
* Models omitted here remain text-only so newly discovered IDs never claim
|
||||
* image support without upstream evidence.
|
||||
*/
|
||||
export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>> = {
|
||||
"MiniMaxAI/MiniMax-M3": ["text", "image"],
|
||||
"Qwen/Qwen3.6-Plus": ["text", "image"],
|
||||
"Qwen/Qwen3.7-Flash": ["text", "image"],
|
||||
"Qwen/Qwen3.7-Plus": ["text", "image"],
|
||||
"Qwen/Qwen3.8-Max": ["text", "image"],
|
||||
"claude-fable-5": ["text", "image"],
|
||||
"claude-haiku-4-5-20251001": ["text", "image"],
|
||||
"claude-opus-4-7": ["text", "image"],
|
||||
"claude-opus-4-8": ["text", "image"],
|
||||
"claude-opus-5": ["text", "image"],
|
||||
"claude-sonnet-4-6": ["text", "image"],
|
||||
"claude-sonnet-5": ["text", "image"],
|
||||
"google/gemini-3.1-flash-lite": ["text", "image"],
|
||||
"google/gemini-3.5-flash": ["text", "image"],
|
||||
"google/gemini-3.5-flash-lite": ["text", "image"],
|
||||
"google/gemini-3.6-flash": ["text", "image"],
|
||||
"gpt-5.3-codex": ["text", "image"],
|
||||
"gpt-5.4": ["text", "image"],
|
||||
"gpt-5.4-mini": ["text", "image"],
|
||||
"gpt-5.5": ["text", "image"],
|
||||
"gpt-5.6-luna": ["text", "image"],
|
||||
"gpt-5.6-sol": ["text", "image"],
|
||||
"gpt-5.6-terra": ["text", "image"],
|
||||
"meta/muse-spark-1.1": ["text", "image"],
|
||||
"meta/muse-spark-1.2": ["text", "image"],
|
||||
"meta/muse-spark-1.2-contributor": ["text", "image"],
|
||||
"moonshotai/Kimi-K2.5": ["text", "image"],
|
||||
"moonshotai/Kimi-K2.6": ["text", "image"],
|
||||
"moonshotai/Kimi-K2.7-Code": ["text", "image"],
|
||||
"moonshotai/Kimi-K2.7-Code-Highspeed": ["text", "image"],
|
||||
"moonshotai/Kimi-K3": ["text", "image"],
|
||||
"sakana/fugu-ultra": ["text", "image"],
|
||||
"stepfun/Step-3.7-Flash": ["text", "image"],
|
||||
"thinkingmachines/inkling": ["text", "image"],
|
||||
"thinkingmachines/inkling-small": ["text", "image"],
|
||||
"xai/grok-4.5": ["text", "image"],
|
||||
"xiaomi/mimo-v2.5": ["text", "image"],
|
||||
}
|
||||
export type CommandCodeApi = "openai-completions" | "anthropic-messages"
|
||||
|
||||
const TEXT_INPUT_ONLY = ["text"] as const
|
||||
|
||||
@@ -66,43 +34,6 @@ export function modelSupportsImageInput(modelId: string): boolean {
|
||||
|
||||
export type PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
|
||||
|
||||
type CommandCodeReasoningEffort = Exclude<PiThinkingLevel, "off">
|
||||
|
||||
/**
|
||||
* Per-model reasoning efforts supported by Command Code's generate endpoint.
|
||||
*
|
||||
* The Provider API does not expose reasoning metadata. This is an exact
|
||||
* snapshot of `reasoningEfforts` from the command-code@1.15.1 model catalog
|
||||
* (`packages/shared/src/model-catalog.ts`, also published in the generated
|
||||
* `dist/bundled/command-code-knowledge/reference/models.md`). Models omitted
|
||||
* here let Command Code choose their reasoning depth, matching the CLI.
|
||||
*/
|
||||
export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
|
||||
"Qwen/Qwen3.8-Max": ["low", "medium", "xhigh"],
|
||||
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
||||
"deepseek/deepseek-v4-flash": ["high", "max"],
|
||||
"deepseek/deepseek-v4-pro": ["high", "max"],
|
||||
"gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
|
||||
"gpt-5.4": ["low", "medium", "high", "xhigh"],
|
||||
"gpt-5.4-mini": ["low", "medium", "high"],
|
||||
"gpt-5.5": ["low", "medium", "high", "xhigh"],
|
||||
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
|
||||
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
|
||||
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
|
||||
"google/gemini-3.1-flash-lite": ["low", "medium", "high"],
|
||||
"google/gemini-3.5-flash": ["low", "medium", "high"],
|
||||
"google/gemini-3.5-flash-lite": ["low", "medium", "high"],
|
||||
"google/gemini-3.6-flash": ["low", "medium", "high"],
|
||||
"sakana/fugu-ultra": ["high", "xhigh"],
|
||||
"xai/grok-4.5": ["low", "medium", "high"],
|
||||
"zai-org/GLM-5.2": ["high", "max"],
|
||||
}
|
||||
|
||||
const PI_THINKING_LEVELS: readonly PiThinkingLevel[] = [
|
||||
"off",
|
||||
"minimal",
|
||||
@@ -126,7 +57,7 @@ export function thinkingLevelMapForEfforts(
|
||||
|
||||
export interface ThinkingMetadata {
|
||||
thinkingLevelMap: Partial<Record<PiThinkingLevel, string | null>>
|
||||
thinking: {
|
||||
thinking?: {
|
||||
mode: "effort"
|
||||
effortMap: Partial<Record<CommandCodeReasoningEffort, string>>
|
||||
efforts: readonly CommandCodeReasoningEffort[]
|
||||
@@ -135,19 +66,26 @@ export interface ThinkingMetadata {
|
||||
|
||||
export function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined {
|
||||
const efforts = MODEL_EFFORTS[modelId]
|
||||
if (!efforts) return undefined
|
||||
return {
|
||||
thinkingLevelMap: thinkingLevelMapForEfforts(efforts),
|
||||
thinking: {
|
||||
mode: "effort",
|
||||
effortMap: Object.fromEntries(efforts.map((effort) => [effort, effort])),
|
||||
efforts,
|
||||
},
|
||||
if (efforts) {
|
||||
return {
|
||||
thinkingLevelMap: thinkingLevelMapForEfforts(efforts),
|
||||
thinking: {
|
||||
mode: "effort",
|
||||
effortMap: Object.fromEntries(efforts.map((effort) => [effort, effort])),
|
||||
efforts,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (!isReasoningModel(modelId)) return undefined
|
||||
return { thinkingLevelMap: thinkingLevelMapForEfforts([]) }
|
||||
}
|
||||
|
||||
function isReasoningModel(modelId: string): boolean {
|
||||
return MODEL_EFFORTS[modelId] !== undefined
|
||||
return MODEL_REASONING[modelId] === true
|
||||
}
|
||||
|
||||
function maxOutputTokensForModel(modelId: string, contextLength: number): number {
|
||||
return Math.min(contextLength, MODEL_MAX_OUTPUT_TOKENS[modelId] ?? DEFAULT_MAX_OUTPUT_TOKENS)
|
||||
}
|
||||
|
||||
interface ApiModel {
|
||||
@@ -159,11 +97,22 @@ interface ApiModel {
|
||||
export interface CommandCodeModel {
|
||||
id: string
|
||||
name: string
|
||||
api: CommandCodeApi
|
||||
reasoning: boolean
|
||||
contextWindow: number
|
||||
maxTokens: number
|
||||
}
|
||||
|
||||
export function apiForModelId(id: string): CommandCodeApi {
|
||||
return id.startsWith("claude-") ? "anthropic-messages" : "openai-completions"
|
||||
}
|
||||
|
||||
export function baseUrlForModel(apiBase: string, api: CommandCodeApi): string {
|
||||
const normalized = apiBase.replace(/\/+$/g, "")
|
||||
if (api !== "anthropic-messages") return normalized
|
||||
return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized
|
||||
}
|
||||
|
||||
interface FetchCommandCodeModelsOptions {
|
||||
url?: string
|
||||
fetchImpl?: typeof fetch
|
||||
@@ -222,12 +171,15 @@ function parseCachedModel(value: unknown): CommandCodeModel {
|
||||
|
||||
const id = stringField(value, "id")
|
||||
booleanField(value, "reasoning")
|
||||
positiveNumberField(value, "maxTokens")
|
||||
const contextWindow = positiveNumberField(value, "contextWindow")
|
||||
return {
|
||||
id,
|
||||
name: stringField(value, "name"),
|
||||
api: apiForModelId(id),
|
||||
reasoning: isReasoningModel(id),
|
||||
contextWindow: positiveNumberField(value, "contextWindow"),
|
||||
maxTokens: positiveNumberField(value, "maxTokens"),
|
||||
contextWindow,
|
||||
maxTokens: maxOutputTokensForModel(id, contextWindow),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,9 +281,10 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
|
||||
return data.map(parseApiModel).map((model) => ({
|
||||
id: model.id,
|
||||
name: `${model.name} (CC)`,
|
||||
api: apiForModelId(model.id),
|
||||
reasoning: isReasoningModel(model.id),
|
||||
contextWindow: model.contextLength,
|
||||
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
||||
maxTokens: maxOutputTokensForModel(model.id, model.contextLength),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
+84
-22
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Command Code OAuth provider for pi's /login flow.
|
||||
*
|
||||
* Implements a browser-assisted API key retrieval flow:
|
||||
* 1. Starts a local HTTP server on a Command Code CLI-compatible port
|
||||
* 2. Opens the Command Code Studio auth page in the browser
|
||||
* 3. The user authenticates on the Command Code website
|
||||
* 4. The website POSTs the API key back to the local server
|
||||
* 5. If browser transfer fails, the user can paste the API key manually
|
||||
* 6. The API key is stored in pi's auth.json as OAuth credentials
|
||||
* Implements two API key retrieval flows:
|
||||
* 1. Browser-assisted login opens Command Code Studio and waits for the
|
||||
* website to POST the API key back to a local callback server.
|
||||
* 2. Direct API key login prompts the user to paste a Studio API key.
|
||||
*
|
||||
* If browser transfer fails, the user can still paste the API key manually.
|
||||
* The API key is stored in pi's auth.json as OAuth credentials.
|
||||
*
|
||||
* Since Command Code API keys don't expire, we store them as
|
||||
* OAuth credentials with a far-future expiry.
|
||||
@@ -18,7 +18,8 @@ import { startAuthServer } from "./auth-server.ts"
|
||||
|
||||
const STUDIO_BASE_URL = "https://commandcode.ai"
|
||||
const TEN_YEARS_MS = 10 * 365 * 24 * 60 * 60 * 1000 // API keys don't expire
|
||||
const DEFAULT_AUTH_TIMEOUT_MS = 15_000
|
||||
const DEFAULT_AUTH_TIMEOUT_MS = 120_000
|
||||
const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
||||
|
||||
export interface OAuthLoginCallbacks {
|
||||
onAuth(params: { url: string }): void
|
||||
@@ -95,22 +96,70 @@ export function sanitizeApiKey(input: string): string {
|
||||
.trim()
|
||||
}
|
||||
|
||||
export async function validateApiKey(
|
||||
apiKey: string,
|
||||
options: { fetchImpl?: typeof fetch; apiBase?: string } = {},
|
||||
): Promise<void> {
|
||||
let response: Response
|
||||
try {
|
||||
response = await (options.fetchImpl ?? fetch)(
|
||||
`${options.apiBase ?? DEFAULT_API_BASE}/alpha/whoami`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Could not validate the Command Code API key: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (response.status === 401) throw new Error("Invalid Command Code API key")
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not validate the Command Code API key (${response.status})`)
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForApiKey(callbacks: OAuthLoginCallbacks, message: string) {
|
||||
const apiKey = sanitizeApiKey(await callbacks.onPrompt({ message }))
|
||||
if (!apiKey) throw new Error("No Command Code API key provided")
|
||||
await validateApiKey(apiKey)
|
||||
return credentialsFromApiKey(apiKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the browser-based login flow for Command Code.
|
||||
*
|
||||
* Returns OAuth credentials where access == refresh == the user's API key.
|
||||
* The keys don't expire, so we set a far-future expiry.
|
||||
*/
|
||||
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
type LoginChoice = { type: "browser" } | { type: "prompt" } | { type: "apiKey"; apiKey: string }
|
||||
|
||||
async function chooseLoginFlow(callbacks: OAuthLoginCallbacks): Promise<LoginChoice> {
|
||||
const input = sanitizeApiKey(
|
||||
await callbacks.onPrompt({
|
||||
message:
|
||||
"Command Code login: press Enter for browser login, type 'key' to paste an API key, or paste the API key directly:",
|
||||
}),
|
||||
)
|
||||
const normalized = input.toLowerCase()
|
||||
|
||||
if (!input || normalized === "1" || normalized === "b" || normalized === "browser") {
|
||||
return { type: "browser" }
|
||||
}
|
||||
|
||||
if (
|
||||
normalized === "2" ||
|
||||
normalized === "k" ||
|
||||
normalized === "key" ||
|
||||
normalized === "api" ||
|
||||
normalized === "paste"
|
||||
) {
|
||||
return { type: "prompt" }
|
||||
}
|
||||
|
||||
return { type: "apiKey", apiKey: input }
|
||||
}
|
||||
|
||||
async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const stateToken = generateStateToken()
|
||||
let authServer
|
||||
try {
|
||||
authServer = await startAuthServer()
|
||||
authServer = await startAuthServer({ expectedState: stateToken })
|
||||
} catch {
|
||||
return promptForApiKey(
|
||||
callbacks,
|
||||
@@ -118,7 +167,6 @@ export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
|
||||
)
|
||||
}
|
||||
|
||||
const stateToken = generateStateToken()
|
||||
const callbackUrl = `http://localhost:${authServer.port}/callback`
|
||||
const authUrl = `${STUDIO_BASE_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(stateToken)}`
|
||||
|
||||
@@ -142,13 +190,27 @@ export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
|
||||
throw error
|
||||
}
|
||||
|
||||
// Validate state token to prevent CSRF.
|
||||
if (callback.state !== stateToken) {
|
||||
authServer.server.close()
|
||||
throw new Error("State token mismatch. Authentication may have been tampered with.")
|
||||
return credentialsFromApiKey(callback.apiKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the login flow for Command Code.
|
||||
*
|
||||
* Returns OAuth credentials where access == refresh == the user's API key.
|
||||
* The keys don't expire, so we set a far-future expiry.
|
||||
*/
|
||||
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const choice = await chooseLoginFlow(callbacks)
|
||||
|
||||
if (choice.type === "apiKey") {
|
||||
await validateApiKey(choice.apiKey)
|
||||
return credentialsFromApiKey(choice.apiKey)
|
||||
}
|
||||
if (choice.type === "prompt") {
|
||||
return promptForApiKey(callbacks, "Paste your Command Code API key:")
|
||||
}
|
||||
|
||||
return credentialsFromApiKey(callback.apiKey)
|
||||
return browserLogin(callbacks)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+53
-45
@@ -20,7 +20,7 @@ export interface TemporaryPricing {
|
||||
}
|
||||
|
||||
export const PRICING_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits"
|
||||
export const PRICING_LAST_VERIFIED = "2026-08-04"
|
||||
export const PRICING_LAST_VERIFIED = "2026-08-25"
|
||||
|
||||
export const ZERO_MODEL_COST: CommandCodeModelCost = {
|
||||
input: 0,
|
||||
@@ -40,7 +40,7 @@ export const ZERO_MODEL_COST: CommandCodeModelCost = {
|
||||
export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
// Free models
|
||||
"poolside/laguna-s-2.1-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
"inclusionai/ling-3.0-flash-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
"stealth/ox-alpha": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
|
||||
// Open and open-weight models
|
||||
"tencent/hy3-paid": { input: 0.14, output: 0.58, cacheRead: 0.035, cacheWrite: 0 },
|
||||
@@ -54,6 +54,7 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
},
|
||||
"moonshotai/Kimi-K2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
|
||||
"moonshotai/Kimi-K2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
||||
"zai-org/GLM-5.3": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
||||
"zai-org/GLM-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
||||
"zai-org/GLM-5.2-Fast": { input: 3, output: 10.25, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"zai-org/GLM-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
||||
@@ -61,20 +62,28 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
"MiniMaxAI/MiniMax-M3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
||||
"MiniMaxAI/MiniMax-M2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
||||
"MiniMaxAI/MiniMax-M2.5": { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 },
|
||||
// Permanent 75% discount.
|
||||
// DeepSeek V4 uses time-dependent rates. Display the documented off-peak
|
||||
// rates, which apply for 17 hours per day; the Usage page remains authoritative.
|
||||
"deepseek/deepseek-v4-pro": {
|
||||
input: 0.435,
|
||||
output: 0.87,
|
||||
cacheRead: 0.003625,
|
||||
input: 0.66,
|
||||
output: 1.98,
|
||||
cacheRead: 0.022,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
"deepseek/deepseek-v4-flash": {
|
||||
input: 0.14,
|
||||
output: 0.28,
|
||||
cacheRead: 0.0028,
|
||||
input: 0.22,
|
||||
output: 0.66,
|
||||
cacheRead: 0.007,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
"deepseek/deepseek-v4-flash-vision-exp": {
|
||||
input: 0.22,
|
||||
output: 0.66,
|
||||
cacheRead: 0.007,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
"Qwen/Qwen3.8-Max": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 },
|
||||
"Qwen/Qwen3.8-27B": { input: 0.4, output: 3, cacheRead: 0.04, cacheWrite: 0 },
|
||||
"Qwen/Qwen3.7-Max": { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.13 },
|
||||
"Qwen/Qwen3.7-Plus": {
|
||||
input: 0.4,
|
||||
@@ -140,6 +149,13 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
cacheWrite: 0,
|
||||
},
|
||||
"meta/muse-spark-1.1": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"meta/muse-spark-1.2": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"meta/muse-spark-1.2-contributor": {
|
||||
input: 0.1,
|
||||
output: 0.2,
|
||||
cacheRead: 0.002,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
|
||||
// Anthropic
|
||||
// Introductory pricing through 2026-08-31.
|
||||
@@ -158,43 +174,20 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
|
||||
// OpenAI
|
||||
"gpt-5.6-sol": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
// Discounted rates through 2026-08-14.
|
||||
"gpt-5.6-terra": {
|
||||
input: 1,
|
||||
output: 6,
|
||||
cacheRead: 0.1,
|
||||
cacheWrite: 1.25,
|
||||
tiers: [
|
||||
{
|
||||
inputTokensAbove: 272_000,
|
||||
input: 2,
|
||||
output: 9,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 2.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
"gpt-5.6-luna": {
|
||||
input: 0.1,
|
||||
output: 0.6,
|
||||
cacheRead: 0.01,
|
||||
cacheWrite: 0.125,
|
||||
tiers: [
|
||||
{
|
||||
inputTokensAbove: 272_000,
|
||||
input: 0.2,
|
||||
output: 0.9,
|
||||
cacheRead: 0.02,
|
||||
cacheWrite: 0.25,
|
||||
},
|
||||
],
|
||||
},
|
||||
"gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
|
||||
"gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
|
||||
"gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
||||
"gpt-5.3-codex": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
||||
|
||||
// Google and xAI
|
||||
"google/gemini-3.7-flash": {
|
||||
input: 0.75,
|
||||
output: 3.75,
|
||||
cacheRead: 0.075,
|
||||
cacheWrite: 0.04167,
|
||||
},
|
||||
"google/gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"google/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"google/gemini-3.5-flash-lite": {
|
||||
@@ -210,17 +203,32 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
cacheWrite: 0,
|
||||
},
|
||||
"xai/grok-4.5": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"xai/grok-4.6": {
|
||||
input: 2,
|
||||
output: 6,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 0,
|
||||
tiers: [
|
||||
{
|
||||
inputTokensAbove: 200_000,
|
||||
input: 4,
|
||||
output: 12,
|
||||
cacheRead: 1,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [
|
||||
{
|
||||
models: ["gpt-5.6-terra", "gpt-5.6-luna"],
|
||||
expiresOn: "2026-08-14",
|
||||
description: "50% promotional rates",
|
||||
},
|
||||
{
|
||||
models: ["claude-sonnet-5"],
|
||||
expiresOn: "2026-08-31",
|
||||
description: "introductory pricing",
|
||||
},
|
||||
{
|
||||
models: ["google/gemini-3.7-flash"],
|
||||
expiresOn: "2026-12-31",
|
||||
description: "50% promotional pricing",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { getConfiguredApiKey } from "./api-key.ts"
|
||||
import { pickCommandCodeApiKey } from "./converters.ts"
|
||||
import { fetchCommandCodeQuota, redactValue } from "./quota.ts"
|
||||
import { formatQuota } from "./quota-format.ts"
|
||||
|
||||
export interface QuotaCommandContext {
|
||||
waitForIdle?: () => Promise<void>
|
||||
modelRegistry?: {
|
||||
getApiKeyForProvider?: (provider: string) => Promise<string | undefined>
|
||||
}
|
||||
ui: {
|
||||
notify(message: string, type?: "info" | "warning" | "error"): void
|
||||
}
|
||||
}
|
||||
|
||||
interface QuotaCommandApi {
|
||||
registerCommand(
|
||||
name: string,
|
||||
options: {
|
||||
description: string
|
||||
handler: (args: string, ctx: QuotaCommandContext) => Promise<void>
|
||||
},
|
||||
): void
|
||||
}
|
||||
|
||||
interface RegisterQuotaCommandOptions {
|
||||
apiBase: string
|
||||
headers?: Record<string, string>
|
||||
getConfiguredKey?: () => string | undefined
|
||||
fetchQuota?: typeof fetchCommandCodeQuota
|
||||
}
|
||||
|
||||
export function registerCommandCodeQuota(
|
||||
pi: QuotaCommandApi,
|
||||
options: RegisterQuotaCommandOptions,
|
||||
): void {
|
||||
const getConfiguredKey = options.getConfiguredKey ?? getConfiguredApiKey
|
||||
const fetchQuota = options.fetchQuota ?? fetchCommandCodeQuota
|
||||
|
||||
pi.registerCommand("commandcode-quota", {
|
||||
description: "Show Command Code account usage and quota",
|
||||
handler: async (_args, ctx) => {
|
||||
await ctx.waitForIdle?.()
|
||||
const registryKey = await ctx.modelRegistry?.getApiKeyForProvider?.("commandcode")
|
||||
const apiKey = pickCommandCodeApiKey(registryKey, getConfiguredKey())
|
||||
if (!apiKey) {
|
||||
ctx.ui.notify(
|
||||
"Command Code quota requires an API key. Run /login and select Command Code, or set COMMAND_CODE_API_KEY.",
|
||||
"warning",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const result = await fetchQuota({
|
||||
apiKey,
|
||||
baseUrl: options.apiBase,
|
||||
extraHeaders: options.headers,
|
||||
})
|
||||
if (!result.ok) {
|
||||
ctx.ui.notify(redactValue(result.error.message), "error")
|
||||
return
|
||||
}
|
||||
ctx.ui.notify(formatQuota(result.quota), "info")
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type {
|
||||
CommandCodeCredits,
|
||||
CommandCodeQuota,
|
||||
CommandCodeSubscription,
|
||||
CommandCodeWindowLimit,
|
||||
} from "./quota-types.ts"
|
||||
|
||||
export function formatWindowLimits(
|
||||
limits: readonly CommandCodeWindowLimit[],
|
||||
now: () => number = Date.now,
|
||||
): string[] {
|
||||
const labels: Record<CommandCodeWindowLimit["window"], string> = {
|
||||
fiveHour: "5-hour",
|
||||
weekly: "Weekly",
|
||||
}
|
||||
|
||||
return limits.map((limit) => {
|
||||
const used = limit.used.toFixed(2)
|
||||
const cap = limit.cap.toFixed(2)
|
||||
const percent = limit.cap > 0 ? Math.round((limit.used / limit.cap) * 100) : 0
|
||||
const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})`
|
||||
return `${labels[limit.window]}: ${used} / ${cap} credits (${percent}% used)${reset}`
|
||||
})
|
||||
}
|
||||
|
||||
function formatResetClock(resetAtSeconds: number, now: () => number): string {
|
||||
const date = new Date(resetAtSeconds * 1000)
|
||||
if (Number.isNaN(date.getTime())) return "unknown"
|
||||
const diffMs = date.getTime() - now()
|
||||
if (diffMs <= 0) return "soon"
|
||||
const minutes = Math.ceil(diffMs / 60_000)
|
||||
if (minutes < 60) return `in ${minutes}m`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const remainingMinutes = minutes % 60
|
||||
if (hours < 24) {
|
||||
return remainingMinutes > 0 ? `in ${hours}h ${remainingMinutes}m` : `in ${hours}h`
|
||||
}
|
||||
const days = Math.floor(hours / 24)
|
||||
return days === 1 ? "in 1 day" : `in ${days} days`
|
||||
}
|
||||
|
||||
function creditsDetail(credits: CommandCodeCredits | null): string | undefined {
|
||||
if (!credits) return undefined
|
||||
const parts = [
|
||||
`monthly $${credits.monthlyCredits.toFixed(2)}`,
|
||||
`purchased $${credits.purchasedCredits.toFixed(2)}`,
|
||||
]
|
||||
if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`)
|
||||
return `Sources: ${parts.join(" / ")}`
|
||||
}
|
||||
|
||||
function subscriptionLine(subscription: CommandCodeSubscription): string {
|
||||
const plan = (subscription.planId ?? "Unknown").replace(/[_-]+/g, " ").trim()
|
||||
const status = subscription.status ? ` (${subscription.status})` : ""
|
||||
return `Plan: ${plan}${status}`
|
||||
}
|
||||
|
||||
function formatTokens(tokens: number): string {
|
||||
if (tokens >= 1_000_000_000) return `${(tokens / 1_000_000_000).toFixed(1)}B`
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
|
||||
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`
|
||||
return String(tokens)
|
||||
}
|
||||
|
||||
export function formatQuota(quota: CommandCodeQuota, now: () => number = Date.now): string {
|
||||
const lines: string[] = []
|
||||
const remaining = quota.credits?.remainingCredits ?? 0
|
||||
const spent = quota.summary?.totalCost ?? 0
|
||||
const pool = remaining + spent
|
||||
|
||||
if (quota.credits || quota.summary) {
|
||||
lines.push("Credits")
|
||||
lines.push(` Remaining: $${remaining.toFixed(2)} of $${pool.toFixed(2)}`)
|
||||
lines.push(` Used: $${spent.toFixed(2)}`)
|
||||
lines.push(` ${pool > 0 ? Math.round((spent / pool) * 100) : 0}% used`)
|
||||
}
|
||||
|
||||
const detail = creditsDetail(quota.credits)
|
||||
if (detail) lines.push(detail)
|
||||
if (quota.subscription) lines.push(subscriptionLine(quota.subscription))
|
||||
|
||||
if (quota.summary) {
|
||||
lines.push("")
|
||||
lines.push(quota.subscription?.currentPeriodStart ? "Usage (billing period)" : "Usage")
|
||||
lines.push(` Cost: $${quota.summary.totalCost.toFixed(2)}`)
|
||||
lines.push(` Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`)
|
||||
if (quota.summary.totalTokens !== undefined) {
|
||||
lines.push(` Tokens: ${formatTokens(quota.summary.totalTokens)}`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
lines.push("Account")
|
||||
lines.push(` ${quota.account.keyName ?? quota.account.login}`)
|
||||
|
||||
const limits = quota.credits?.windowLimits ?? []
|
||||
if (limits.length > 0) {
|
||||
lines.push("")
|
||||
lines.push("Usage windows:")
|
||||
lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`))
|
||||
}
|
||||
|
||||
if ((quota.unavailable?.length ?? 0) > 0) {
|
||||
lines.push("")
|
||||
lines.push(`Unavailable: ${quota.unavailable?.join(", ")}`)
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
lines.push("Full detail: https://commandcode.ai/usage")
|
||||
return lines.join("\n")
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export interface CommandCodeWindowLimit {
|
||||
window: "fiveHour" | "weekly"
|
||||
used: number
|
||||
cap: number
|
||||
resetAt: number | null
|
||||
}
|
||||
|
||||
export interface CommandCodeCredits {
|
||||
monthlyCredits: number
|
||||
purchasedCredits: number
|
||||
freeCredits: number
|
||||
remainingCredits: number
|
||||
windowLimits: CommandCodeWindowLimit[]
|
||||
}
|
||||
|
||||
export interface CommandCodeSubscription {
|
||||
planId: string | null
|
||||
status: string | null
|
||||
currentPeriodStart: string | null
|
||||
currentPeriodEnd: string | null
|
||||
}
|
||||
|
||||
export interface CommandCodeUsageSummary {
|
||||
totalCost: number
|
||||
totalCount: number
|
||||
totalTokens?: number
|
||||
}
|
||||
|
||||
export type CommandCodeQuotaSection = "credits" | "subscription" | "usage"
|
||||
|
||||
export interface CommandCodeQuota {
|
||||
account: {
|
||||
login: string
|
||||
orgId: string | null
|
||||
keyName?: string
|
||||
}
|
||||
credits: CommandCodeCredits | null
|
||||
subscription: CommandCodeSubscription | null
|
||||
summary: CommandCodeUsageSummary | null
|
||||
unavailable?: readonly CommandCodeQuotaSection[]
|
||||
}
|
||||
|
||||
export type CommandCodeQuotaErrorKind = "config" | "http" | "network" | "timeout"
|
||||
|
||||
export type CommandCodeQuotaResult =
|
||||
| { ok: true; quota: CommandCodeQuota }
|
||||
| { ok: false; error: { message: string; kind: CommandCodeQuotaErrorKind } }
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
import { redactCommandCodeErrorText } from "./overflow.ts"
|
||||
import type {
|
||||
CommandCodeCredits,
|
||||
CommandCodeQuotaResult,
|
||||
CommandCodeQuotaSection,
|
||||
CommandCodeSubscription,
|
||||
CommandCodeUsageSummary,
|
||||
CommandCodeWindowLimit,
|
||||
} from "./quota-types.ts"
|
||||
|
||||
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
||||
export const QUOTA_TIMEOUT_MS = 15_000
|
||||
|
||||
interface FetchOptions {
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
fetchImpl?: typeof fetch
|
||||
timeoutMs?: number
|
||||
extraHeaders?: Record<string, string>
|
||||
}
|
||||
|
||||
interface HttpErrorShape {
|
||||
__httpError: true
|
||||
message: string
|
||||
status: number
|
||||
body: string
|
||||
}
|
||||
|
||||
interface QuotaErrorShape {
|
||||
__quotaError: true
|
||||
kind: "timeout" | "network"
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function normalizeResetAt(value: unknown): number | null {
|
||||
let timestamp: number | undefined
|
||||
if (typeof value === "number" && Number.isFinite(value)) timestamp = value
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
const trimmed = value.trim()
|
||||
timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed)
|
||||
}
|
||||
if (timestamp === undefined || !Number.isFinite(timestamp) || timestamp < 0) return null
|
||||
return timestamp >= 1e12 ? Math.round(timestamp / 1000) : timestamp
|
||||
}
|
||||
|
||||
export function windowLimitsFromCredits(value: unknown): CommandCodeWindowLimit[] {
|
||||
if (!isRecord(value)) return []
|
||||
const limits: CommandCodeWindowLimit[] = []
|
||||
for (const [window, entry] of [
|
||||
["fiveHour", value.fiveHour],
|
||||
["weekly", value.weekly],
|
||||
] as const) {
|
||||
if (!isRecord(entry)) continue
|
||||
const used = numberValue(entry.used)
|
||||
const cap = numberValue(entry.cap)
|
||||
if (used === undefined || cap === undefined || (used === 0 && cap === 0)) continue
|
||||
limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) })
|
||||
}
|
||||
return limits
|
||||
}
|
||||
|
||||
function parseCredits(value: unknown): CommandCodeCredits | null {
|
||||
if (!isRecord(value) || !isRecord(value.credits)) return null
|
||||
const credits = value.credits
|
||||
const monthlyCredits = numberValue(credits.monthlyCredits)
|
||||
const purchasedCredits = numberValue(credits.purchasedCredits)
|
||||
const freeCredits = numberValue(credits.freeCredits)
|
||||
if (monthlyCredits === undefined && purchasedCredits === undefined && freeCredits === undefined) {
|
||||
return null
|
||||
}
|
||||
const monthly = monthlyCredits ?? 0
|
||||
const purchased = purchasedCredits ?? 0
|
||||
const free = freeCredits ?? 0
|
||||
return {
|
||||
monthlyCredits: monthly,
|
||||
purchasedCredits: purchased,
|
||||
freeCredits: free,
|
||||
remainingCredits: monthly + purchased + free,
|
||||
windowLimits: windowLimitsFromCredits(value.windowLimits),
|
||||
}
|
||||
}
|
||||
|
||||
function parseSubscription(value: unknown): CommandCodeSubscription | null {
|
||||
if (!isRecord(value) || !isRecord(value.data)) return null
|
||||
const data = value.data
|
||||
const planId = stringValue(data.planId)
|
||||
const status = stringValue(data.status)
|
||||
const currentPeriodStart = stringValue(data.currentPeriodStart)
|
||||
const currentPeriodEnd = stringValue(data.currentPeriodEnd)
|
||||
if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null
|
||||
return {
|
||||
planId: planId ?? null,
|
||||
status: status ?? null,
|
||||
currentPeriodStart: currentPeriodStart ?? null,
|
||||
currentPeriodEnd: currentPeriodEnd ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function parseSummary(value: unknown): CommandCodeUsageSummary | null {
|
||||
if (!isRecord(value)) return null
|
||||
const totalCost = numberValue(value.totalCost)
|
||||
const totalCount = numberValue(value.totalCount)
|
||||
if (totalCost === undefined || totalCount === undefined) return null
|
||||
const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens)
|
||||
return { totalCost, totalCount, ...(totalTokens === undefined ? {} : { totalTokens }) }
|
||||
}
|
||||
|
||||
function parseWhoami(value: unknown): {
|
||||
login: string
|
||||
orgId: string | null
|
||||
keyName?: string
|
||||
} | null {
|
||||
if (!isRecord(value)) return null
|
||||
const org = isRecord(value.org) ? value.org : undefined
|
||||
const user = isRecord(value.user) ? value.user : undefined
|
||||
const login =
|
||||
(org ? stringValue(org.login) : undefined) ??
|
||||
(user ? (stringValue(user.userName) ?? stringValue(user.name)) : undefined)
|
||||
if (!login) return null
|
||||
const orgId = org ? stringValue(org.id) : undefined
|
||||
const keyName = user ? (stringValue(user.keyName) ?? stringValue(user.displayName)) : undefined
|
||||
return { login, orgId: orgId ?? null, ...(keyName ? { keyName } : {}) }
|
||||
}
|
||||
|
||||
function buildUrl(path: string, params: Record<string, string | undefined>): string {
|
||||
const search = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value) search.set(key, value)
|
||||
}
|
||||
const query = search.toString()
|
||||
return `${path}${query ? `?${query}` : ""}`
|
||||
}
|
||||
|
||||
function isHttpError(value: unknown): value is HttpErrorShape {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
value.__httpError === true &&
|
||||
typeof value.message === "string" &&
|
||||
typeof value.status === "number" &&
|
||||
typeof value.body === "string"
|
||||
)
|
||||
}
|
||||
|
||||
function isQuotaError(value: unknown): value is QuotaErrorShape {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
value.__quotaError === true &&
|
||||
(value.kind === "timeout" || value.kind === "network")
|
||||
)
|
||||
}
|
||||
|
||||
function isBlockingHttpError(error: HttpErrorShape): boolean {
|
||||
return error.status === 401 || error.status === 403
|
||||
}
|
||||
|
||||
function httpFailure(error: HttpErrorShape, context: string): CommandCodeQuotaResult {
|
||||
const detail = error.body.trim().slice(0, 200)
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "http",
|
||||
message: redactValue(
|
||||
`${context} request failed (${error.status}): ${detail || error.message}`,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
class QuotaTimeoutError extends Error {}
|
||||
|
||||
export async function fetchCommandCodeQuota(
|
||||
options: FetchOptions,
|
||||
): Promise<CommandCodeQuotaResult> {
|
||||
if (!options.apiKey) {
|
||||
return { ok: false, error: { message: "No Command Code API key found", kind: "config" } }
|
||||
}
|
||||
|
||||
const baseUrl = options.baseUrl ?? DEFAULT_API_BASE
|
||||
const fetchImpl = options.fetchImpl ?? fetch
|
||||
const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS
|
||||
const overallController = new AbortController()
|
||||
const overallTimer = setTimeout(() => overallController.abort(), timeoutMs)
|
||||
const headers = {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${options.apiKey}`,
|
||||
...options.extraHeaders,
|
||||
}
|
||||
|
||||
const request = async (path: string): Promise<unknown> => {
|
||||
if (overallController.signal.aborted) throw new QuotaTimeoutError()
|
||||
try {
|
||||
const response = await fetchImpl(`${baseUrl}${path}`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: overallController.signal,
|
||||
})
|
||||
if (!response.ok) {
|
||||
return {
|
||||
__httpError: true,
|
||||
message:
|
||||
response.status === 401 || response.status === 403
|
||||
? "Command Code rejected the API key"
|
||||
: response.statusText,
|
||||
status: response.status,
|
||||
body: await response.text().catch(() => ""),
|
||||
} satisfies HttpErrorShape
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
if (overallController.signal.aborted) throw new QuotaTimeoutError()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const safeRequest = async (path: string): Promise<unknown> => {
|
||||
try {
|
||||
return await request(path)
|
||||
} catch (error) {
|
||||
return {
|
||||
__quotaError: true,
|
||||
kind: error instanceof QuotaTimeoutError ? "timeout" : "network",
|
||||
} satisfies QuotaErrorShape
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const whoamiRaw = await request("/alpha/whoami")
|
||||
if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami")
|
||||
const account = parseWhoami(whoamiRaw)
|
||||
if (!account) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { kind: "http", message: "Command Code returned an unrecognized account response" },
|
||||
}
|
||||
}
|
||||
|
||||
const orgId = account.orgId ?? undefined
|
||||
const [creditsRaw, subscriptionRaw] = await Promise.all([
|
||||
safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
|
||||
safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId })),
|
||||
])
|
||||
if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
|
||||
return httpFailure(creditsRaw, "credits")
|
||||
}
|
||||
if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
|
||||
return httpFailure(subscriptionRaw, "subscription")
|
||||
}
|
||||
|
||||
const unavailable: CommandCodeQuotaSection[] = []
|
||||
const credits =
|
||||
isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw)
|
||||
if (!credits) unavailable.push("credits")
|
||||
const subscription =
|
||||
isHttpError(subscriptionRaw) || isQuotaError(subscriptionRaw)
|
||||
? null
|
||||
: parseSubscription(subscriptionRaw)
|
||||
if (!subscription) unavailable.push("subscription")
|
||||
|
||||
const summaryRaw = await safeRequest(
|
||||
buildUrl("/alpha/usage/summary", {
|
||||
orgId,
|
||||
since: subscription?.currentPeriodStart ?? undefined,
|
||||
}),
|
||||
)
|
||||
if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
|
||||
return httpFailure(summaryRaw, "summary")
|
||||
}
|
||||
const summary =
|
||||
isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw)
|
||||
if (!summary) unavailable.push("usage")
|
||||
|
||||
if (!credits && !subscription && !summary) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
kind: overallController.signal.aborted ? "timeout" : "http",
|
||||
message: overallController.signal.aborted
|
||||
? "Command Code quota request timed out"
|
||||
: "Command Code returned no recognized usage data for the account",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
quota: {
|
||||
account,
|
||||
credits,
|
||||
subscription,
|
||||
summary,
|
||||
...(unavailable.length > 0 ? { unavailable } : {}),
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: "Command Code quota request timed out", kind: "timeout" },
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: redactValue(`Failed to fetch Command Code quota: ${errorMessage(error)}`),
|
||||
kind: "network",
|
||||
},
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(overallTimer)
|
||||
}
|
||||
}
|
||||
|
||||
export function redactValue(value: string): string {
|
||||
return redactCommandCodeErrorText(value)
|
||||
.replace(
|
||||
/("\s*(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*"\s*:\s*")([^"]{8,})/gi,
|
||||
"$1[redacted]",
|
||||
)
|
||||
.trim()
|
||||
}
|
||||
+10
-5
@@ -28,11 +28,13 @@ export interface CommandCodeRuntimeOptions<TProviderConfig> {
|
||||
cachePath: string
|
||||
loadModels: () => Promise<LoadCommandCodeModelsResult>
|
||||
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
|
||||
getTransport?: () => "unknown" | "provider" | "generate"
|
||||
now?: () => number
|
||||
logWarning?: (message: string) => void
|
||||
}
|
||||
|
||||
export interface CommandCodeRuntimeStatus {
|
||||
transport: "unknown" | "provider" | "generate"
|
||||
source: LoadCommandCodeModelsResult["source"]
|
||||
modelCount: number
|
||||
lastSuccess?: number
|
||||
@@ -86,6 +88,7 @@ function formatTimestamp(timestamp: number | undefined): string {
|
||||
|
||||
export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
|
||||
const lines = [
|
||||
`transport: ${status.transport}`,
|
||||
`source: ${status.source}`,
|
||||
`model count: ${status.modelCount}`,
|
||||
`last success: ${formatTimestamp(status.lastSuccess)}`,
|
||||
@@ -113,6 +116,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
||||
this.now = options.now ?? Date.now
|
||||
this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
|
||||
const initialStatus: CommandCodeRuntimeStatus = {
|
||||
transport: "unknown",
|
||||
source: "empty",
|
||||
modelCount: 0,
|
||||
cachePath: options.cachePath,
|
||||
@@ -123,7 +127,10 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
||||
}
|
||||
|
||||
getStatus(): CommandCodeRuntimeStatus {
|
||||
return { ...this.status }
|
||||
return {
|
||||
...this.status,
|
||||
transport: this.options.getTransport?.() ?? "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
@@ -259,10 +266,8 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
||||
this.pi.registerCommand("commandcode-status", {
|
||||
description: "Show redacted Command Code provider diagnostics",
|
||||
handler: async (_args, ctx) => {
|
||||
ctx.ui.notify(
|
||||
formatCommandCodeStatus(this.status),
|
||||
this.status.warning ? "warning" : "info",
|
||||
)
|
||||
const status = this.getStatus()
|
||||
ctx.ui.notify(formatCommandCodeStatus(status), status.warning ? "warning" : "info")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import type {
|
||||
AssistantMessageEvent,
|
||||
AssistantMessageEventStreamLike,
|
||||
ContextLike,
|
||||
ModelLike,
|
||||
StreamOptions,
|
||||
} from "./types.ts"
|
||||
|
||||
export type CommandCodeTransport = "unknown" | "provider" | "generate"
|
||||
|
||||
interface TransportDependencies {
|
||||
createStream: () => AssistantMessageEventStreamLike
|
||||
streamProvider: (
|
||||
model: ModelLike,
|
||||
context: ContextLike,
|
||||
options?: StreamOptions,
|
||||
) => AssistantMessageEventStreamLike
|
||||
streamGenerate: (
|
||||
model: ModelLike,
|
||||
context: ContextLike,
|
||||
options?: StreamOptions,
|
||||
) => AssistantMessageEventStreamLike
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
async function isUpgradeRequired(response: Response): Promise<boolean> {
|
||||
if (response.status !== 403) return false
|
||||
|
||||
try {
|
||||
const body: unknown = await response.clone().json()
|
||||
if (!isRecord(body)) return false
|
||||
const error = isRecord(body.error) ? body.error : body
|
||||
return error.code === "upgrade_required"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function createCommandCodeTransportRouter(deps: TransportDependencies) {
|
||||
let transport: CommandCodeTransport = "unknown"
|
||||
let apiKey: string | undefined
|
||||
|
||||
function pipe(
|
||||
source: AssistantMessageEventStreamLike,
|
||||
target: AssistantMessageEventStreamLike,
|
||||
): Promise<void> {
|
||||
return (async () => {
|
||||
for await (const event of source) target.push(event)
|
||||
})()
|
||||
}
|
||||
|
||||
return {
|
||||
getTransport(): CommandCodeTransport {
|
||||
return transport
|
||||
},
|
||||
|
||||
reset(): void {
|
||||
transport = "unknown"
|
||||
apiKey = undefined
|
||||
},
|
||||
|
||||
stream(
|
||||
model: ModelLike,
|
||||
context: ContextLike,
|
||||
options?: StreamOptions,
|
||||
): AssistantMessageEventStreamLike {
|
||||
if (options?.apiKey !== apiKey) {
|
||||
apiKey = options?.apiKey
|
||||
transport = "unknown"
|
||||
}
|
||||
const requestApiKey = options?.apiKey
|
||||
if (transport === "generate") return deps.streamGenerate(model, context, options)
|
||||
|
||||
const output = deps.createStream()
|
||||
let upgradeRequired = false
|
||||
const fetchImpl = options?.fetch ?? fetch
|
||||
const providerOptions: StreamOptions = {
|
||||
...options,
|
||||
fetch: async (input, init) => {
|
||||
const response = await fetchImpl(input, init)
|
||||
if (await isUpgradeRequired(response)) upgradeRequired = true
|
||||
return response
|
||||
},
|
||||
onResponse: async (response, responseModel) => {
|
||||
if (upgradeRequired) return
|
||||
await options?.onResponse?.(response, responseModel)
|
||||
},
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const providerStream = deps.streamProvider(model, context, providerOptions)
|
||||
|
||||
for await (const event of providerStream) {
|
||||
if (!upgradeRequired) {
|
||||
if (apiKey === requestApiKey) transport = "provider"
|
||||
output.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
if (upgradeRequired) {
|
||||
if (apiKey === requestApiKey) transport = "generate"
|
||||
await pipe(deps.streamGenerate(model, context, options), output)
|
||||
}
|
||||
output.end()
|
||||
}
|
||||
|
||||
run().catch((error: unknown) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
output.push({
|
||||
type: "error",
|
||||
reason: "error",
|
||||
error: {
|
||||
role: "assistant",
|
||||
content: [],
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "error",
|
||||
errorMessage: message,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
})
|
||||
output.end()
|
||||
})
|
||||
|
||||
return output
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,10 @@ export interface StreamOptions {
|
||||
apiKey?: string
|
||||
signal?: AbortSignal
|
||||
headers?: Record<string, string>
|
||||
fetch?: typeof fetch
|
||||
maxTokens?: number
|
||||
temperature?: number
|
||||
sessionId?: string
|
||||
/** Resolved pi thinking level; forwarded only through the model's map. */
|
||||
reasoning?: string
|
||||
onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
|
||||
@@ -171,6 +174,12 @@ export type AssistantMessageEvent =
|
||||
contentIndex: number
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| {
|
||||
type: "toolcall_delta"
|
||||
contentIndex: number
|
||||
delta: string
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| {
|
||||
type: "toolcall_end"
|
||||
contentIndex: number
|
||||
|
||||
Reference in New Issue
Block a user