merge main into fix/tool-call-streaming
This commit is contained in:
@@ -34,6 +34,7 @@ export function getConfiguredApiKey(
|
||||
} = {},
|
||||
): 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()
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
|
||||
@@ -51,6 +51,57 @@ export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCod
|
||||
"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"],
|
||||
@@ -82,3 +133,9 @@ export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasonin
|
||||
"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,
|
||||
}
|
||||
|
||||
+42
-11
@@ -107,6 +107,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,10 +139,11 @@ export function getApiKey(
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Hosts such as OMP may pass the literal env-var name "$COMMANDCODE_API_KEY"
|
||||
// (or "COMMANDCODE_API_KEY") as the "resolved" registry key instead of the
|
||||
// actual credential. Treat those as unresolved.
|
||||
// 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",
|
||||
])
|
||||
@@ -162,6 +164,16 @@ export function pickCommandCodeApiKey(
|
||||
}
|
||||
|
||||
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) ?? "")
|
||||
@@ -182,7 +194,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>()
|
||||
|
||||
@@ -194,12 +211,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(
|
||||
@@ -210,7 +227,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") {
|
||||
@@ -220,23 +237,37 @@ 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
|
||||
out.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
|
||||
+49
-16
@@ -148,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()
|
||||
@@ -232,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 ??
|
||||
@@ -262,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 })
|
||||
@@ -484,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)
|
||||
@@ -507,6 +518,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
break
|
||||
}
|
||||
|
||||
case "abort": {
|
||||
throw abortError("Request aborted")
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const message =
|
||||
commandCodeErrorMessage(event.error) ??
|
||||
@@ -524,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
|
||||
|
||||
@@ -552,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,
|
||||
@@ -583,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)
|
||||
@@ -696,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")
|
||||
@@ -719,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
|
||||
@@ -756,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"
|
||||
|
||||
+25
-14
@@ -4,11 +4,13 @@ import { dirname } from "node:path"
|
||||
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 }
|
||||
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"
|
||||
@@ -55,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[]
|
||||
@@ -64,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 {
|
||||
@@ -162,13 +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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +284,7 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
|
||||
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),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
+33
-10
@@ -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,9 +96,34 @@ 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)
|
||||
}
|
||||
|
||||
@@ -130,9 +156,10 @@ async function chooseLoginFlow(callbacks: OAuthLoginCallbacks): Promise<LoginCho
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -140,7 +167,6 @@ async function browserLogin(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)}`
|
||||
|
||||
@@ -164,12 +190,6 @@ async function browserLogin(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)
|
||||
}
|
||||
|
||||
@@ -182,7 +202,10 @@ async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
|
||||
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const choice = await chooseLoginFlow(callbacks)
|
||||
|
||||
if (choice.type === "apiKey") return credentialsFromApiKey(choice.apiKey)
|
||||
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:")
|
||||
}
|
||||
|
||||
+42
-2
@@ -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-22"
|
||||
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 },
|
||||
@@ -76,7 +76,14 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
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,
|
||||
@@ -142,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.
|
||||
@@ -168,6 +182,12 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
"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": {
|
||||
@@ -183,6 +203,21 @@ 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[] = [
|
||||
@@ -191,4 +226,9 @@ export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [
|
||||
expiresOn: "2026-08-31",
|
||||
description: "introductory pricing",
|
||||
},
|
||||
{
|
||||
models: ["google/gemini-3.7-flash"],
|
||||
expiresOn: "2026-12-31",
|
||||
description: "50% promotional pricing",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -45,7 +45,7 @@ export function registerCommandCodeQuota(
|
||||
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 COMMANDCODE_API_KEY.",
|
||||
"Command Code quota requires an API key. Run /login and select Command Code, or set COMMAND_CODE_API_KEY.",
|
||||
"warning",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -112,6 +112,8 @@ export interface StreamOptions {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user