Skip to content

Commit da16c78

Browse files
authored
Merge pull request #174 from yashdev9274/supercode-cli
feat: add Claude Opus 4.8 integration through ConcentrateAI with daily budget caps
2 parents 3f26b74 + 04dcdfb commit da16c78

9 files changed

Lines changed: 232 additions & 16 deletions

File tree

apps/supercode-cli/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "supercode-cli",
3-
"version": "0.1.42",
3+
"version": "0.1.43",
44
"description": "AI-powered coding agent CLI",
55
"main": "dist/main.js",
66
"bin": {

apps/supercode-cli/server/src/cli/ai/concentrate-service.ts

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
2-
import { streamText, stepCountIs, type ModelMessage, type LanguageModel } from "ai"
2+
import { streamText, stepCountIs, type ModelMessage, type LanguageModel, type LanguageModelUsage } from "ai"
33
import chalk from "chalk"
44
import { recordUsage } from "../../lib/track-usage"
55
import { computeCost } from "../../lib/pricing"
6+
import { checkDailyBudget, checkQueryLimit, DAILY_QUERY_LIMIT, OPUS_MODEL_ID, getOrCreateDeviceId } from "../../lib/token-budget"
67
import { isEmptyToolResult, isDeniedToolResult, summarizeToolResult, tcName } from "./tool-result"
78

89
const CONCENTRATE_API_KEY = process.env.CONCENTRATEAI_API_KEY || ""
@@ -77,6 +78,37 @@ export class ConcentrateService {
7778
const signalHandler = signal ? () => streamAbortController.abort() : undefined
7879
signalHandler && signal!.addEventListener("abort", signalHandler, { once: true })
7980

81+
const isOpus = this.modelName === OPUS_MODEL_ID
82+
const deviceId = await getOrCreateDeviceId()
83+
84+
if (isOpus) {
85+
const [tokenBudget, queryLimit] = await Promise.all([
86+
checkDailyBudget(),
87+
checkQueryLimit(deviceId),
88+
])
89+
90+
if (!tokenBudget.allowed || !queryLimit.allowed) {
91+
const reasons: string[] = []
92+
if (!tokenBudget.allowed) reasons.push(`Token budget used: ${tokenBudget.used.toLocaleString()} / ${128_000..toLocaleString()}`)
93+
if (!queryLimit.allowed) reasons.push(`Query limit used: ${queryLimit.used} / ${DAILY_QUERY_LIMIT}`)
94+
const msg = [
95+
chalk.red("╔══ Daily usage limit exceeded ══╗"),
96+
chalk.red(`║ Model: anthropic/claude-opus-4-8`),
97+
...reasons.map(r => chalk.red(`║ ${r}`)),
98+
chalk.red(`║ Resets: ${new Date(tokenBudget.resetTime).toLocaleDateString()}`),
99+
chalk.red(`╚════════════════════════════════════╝`),
100+
``,
101+
chalk.yellow(`ℹ Switch to ${chalk.cyan("/model minimax-m3")} to continue chatting.`),
102+
`Run ${chalk.cyan("/usage")} to check your usage across all models.`,
103+
].join("\n")
104+
return {
105+
content: msg,
106+
finishReason: "stop" as const,
107+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, inputTokenDetails: {}, outputTokenDetails: {} } as LanguageModelUsage,
108+
}
109+
}
110+
}
111+
80112
try {
81113
const systemMessages = messages.filter(m => m.role === "system")
82114
const nonSystemMessages = messages.filter(m => m.role !== "system")
@@ -120,6 +152,7 @@ export class ConcentrateService {
120152
totalTokens: inputTokens + outputTokens,
121153
costUsd: computeCost(this.modelName, inputTokens, outputTokens, 0),
122154
durationMs: null,
155+
userId: deviceId,
123156
})
124157
return {
125158
content,
@@ -155,6 +188,7 @@ export class ConcentrateService {
155188
totalTokens: usage.totalTokens ?? 0,
156189
costUsd: computeCost(this.modelName, usage.inputTokens ?? 0, usage.outputTokens ?? 0, usage.inputTokenDetails?.cacheReadTokens ?? 0),
157190
durationMs: null,
191+
userId: deviceId,
158192
})
159193

160194
return {
@@ -182,7 +216,7 @@ export class ConcentrateService {
182216
messages: nonSystemMessages,
183217
system,
184218
tools,
185-
stopWhen: stepCountIs(8),
219+
stopWhen: stepCountIs(isOpus ? 5 : 8),
186220
abortSignal: streamAbortController.signal,
187221
prepareStep: async ({ messages }) => {
188222
if (stopForDenialLoop) {
@@ -296,6 +330,7 @@ export class ConcentrateService {
296330
totalTokens: inputTokens + outputTokens,
297331
costUsd: computeCost(this.modelName, inputTokens, outputTokens, 0),
298332
durationMs: null,
333+
userId: deviceId,
299334
})
300335
return {
301336
content,
@@ -326,19 +361,20 @@ export class ConcentrateService {
326361
provider: "concentrateai",
327362
model: this.modelName,
328363
inputTokens: usage.inputTokens ?? 0,
329-
outputTokens: usage.outputTokens ?? 0,
330-
cachedInputTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
331-
totalTokens: usage.totalTokens ?? 0,
332-
costUsd: computeCost(this.modelName, usage.inputTokens ?? 0, usage.outputTokens ?? 0, usage.inputTokenDetails?.cacheReadTokens ?? 0),
333-
durationMs: null,
334-
})
364+
outputTokens: usage.outputTokens ?? 0,
365+
cachedInputTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
366+
totalTokens: usage.totalTokens ?? 0,
367+
costUsd: computeCost(this.modelName, usage.inputTokens ?? 0, usage.outputTokens ?? 0, usage.inputTokenDetails?.cacheReadTokens ?? 0),
368+
durationMs: null,
369+
userId: deviceId,
370+
})
335371

336-
return {
337-
content: fullResponse,
338-
finishReason,
339-
usage,
340-
}
341-
} catch (error: any) {
372+
return {
373+
content: fullResponse,
374+
finishReason,
375+
usage,
376+
}
377+
} catch (error: any) {
342378
if (error?.name === "AbortError") throw error
343379
console.error(chalk.red("ConcentrateAI Service Error:"), error instanceof Error ? error.message : String(error))
344380
throw error

apps/supercode-cli/server/src/cli/ai/context-windows.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const CONTEXT_WINDOWS: Record<string, number> = {
1717
"glm-5.2": 203_000,
1818
"glm-5.1": 203_000,
1919
"minimax-m3": 1_000_000,
20+
"anthropic/claude-opus-4-8": 500_000,
2021
}
2122

2223
const FALLBACK_CONTEXT_WINDOW = 128_000

apps/supercode-cli/server/src/cli/commands/slashCommands/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { select, isCancel } from "@clack/prompts"
22
import chalk from "chalk"
33
import { pickModel, formatModelChange } from "./model.ts"
4+
import { usageCommand } from "./usage.ts"
45
import { connectProvider } from "./connect.ts"
56
import { renderHelp } from "./help.ts"
67
import { theme, heavyDivider } from "src/cli/utils/tui.ts"
@@ -28,6 +29,7 @@ export const COMMANDS = [
2829
{ cmd: "/interact", desc: "Browser interaction via Firecrawl" },
2930
{ cmd: "/crawl", desc: "Crawl a website via Firecrawl" },
3031
{ cmd: "/parse", desc: "Parse a file (PDF, DOC, etc.) via Firecrawl" },
32+
{ cmd: "/usage", desc: "Show daily token usage and budget for Opus 4.8" },
3133
{ cmd: "/help", desc: "Show available commands and models" },
3234
{ cmd: "/exit", desc: "End the session" },
3335
]
@@ -83,6 +85,10 @@ const handlers: Record<string, (args: string) => Promise<SlashCommandResult>> =
8385
verbose: async () => {
8486
return { type: "verbose" }
8587
},
88+
usage: async () => {
89+
await usageCommand()
90+
return { type: "help" as const }
91+
},
8692
search: async (args) => ({ type: "message", message: chatify("search", args) }),
8793
scrape: async (args) => ({ type: "message", message: chatify("scrape", args) }),
8894
interact: async (args) => ({ type: "message", message: chatify("interact", args) }),

apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface ModelEntry {
1313
}
1414

1515
const MODELS: ModelEntry[] = [
16+
{ value: "anthropic/claude-opus-4-8", label: "Claude Opus 4.8", provider: "concentrateai", cost: "5.0x", desc: "Top-tier reasoning" },
1617
{ value: "glm-5.2", label: "GLM 5.2", provider: "concentrateai", cost: "0.5x", desc: "Latest GLM" },
1718
{ value: "glm-5.1", label: "GLM 5.1", provider: "concentrateai", cost: "0.4x", desc: "Balanced multilingual" },
1819
{ value: "kimi-k2-6", label: "Kimi K2.6", provider: "concentrateai", cost: "0.8x", desc: "Long context" },
@@ -58,7 +59,8 @@ function renderModelBrowser(currentProvider: string, currentModel: string): void
5859
const cost = chalk.hex(m.cost === "free" ? theme.greenGlow : theme.muted)(m.cost.padEnd(6))
5960
const desc = chalk.hex(theme.muted)(m.desc.padEnd(20))
6061
const marker = isCurrent ? ` ${chalk.bgHex(theme.amber).hex(theme.black).bold(" current ")}` : ""
61-
console.log(` ${prefix} ${name} ${cost}${desc}${marker}`)
62+
const freeTag = !isCurrent && providerKey === "concentrateai" ? ` ${chalk.bgHex(theme.green).hex(theme.black).bold(" FREE ")}` : ""
63+
console.log(` ${prefix} ${name} ${cost}${desc}${marker}${freeTag}`)
6264
}
6365
console.log()
6466
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import chalk from "chalk"
2+
import prisma from "src/lib/prisma"
3+
import { theme, sectionHeader, heavyDivider, formatTokenCount } from "src/cli/utils/tui.ts"
4+
import { OPUS_MODEL_ID, DAILY_BUDGET_TOKENS, DAILY_QUERY_LIMIT, getDailyTokenUsage, getDailyQueryCount, getOrCreateDeviceId } from "src/lib/token-budget"
5+
6+
function todayStart(): Date {
7+
const now = new Date()
8+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
9+
}
10+
11+
export async function usageCommand(): Promise<void> {
12+
const today = todayStart()
13+
const tomorrow = new Date(today.getTime() + 86_400_000)
14+
15+
const [total, deviceId] = await Promise.all([
16+
getDailyTokenUsage(OPUS_MODEL_ID),
17+
getOrCreateDeviceId(),
18+
])
19+
const queryCount = await getDailyQueryCount(deviceId)
20+
21+
const tokenRemaining = Math.max(0, DAILY_BUDGET_TOKENS - total)
22+
const tokenPct = Math.min(100, Math.round((total / DAILY_BUDGET_TOKENS) * 100))
23+
const queryRemaining = Math.max(0, DAILY_QUERY_LIMIT - queryCount)
24+
const queryPct = Math.min(100, Math.round((queryCount / DAILY_QUERY_LIMIT) * 100))
25+
26+
const todayLabel = today.toLocaleDateString()
27+
const w = Math.min(process.stdout.columns ?? 80, 72)
28+
29+
console.log()
30+
console.log(heavyDivider())
31+
console.log()
32+
console.log(sectionHeader("Daily Limits — Claude Opus 4.8", { accent: "green" }))
33+
34+
const barWidth = w - 20
35+
36+
console.log(` ${chalk.hex(theme.greenGlow).bold("Token Budget")}`)
37+
console.log(` ${chalk.hex(theme.muted)(todayLabel)}`)
38+
const tokenBar = chalk.hex(tokenPct >= 80 ? theme.red : theme.green)("█".repeat(Math.round((tokenPct / 100) * barWidth))) +
39+
chalk.hex(theme.greenDim)("█".repeat(Math.max(0, barWidth - Math.round((tokenPct / 100) * barWidth))))
40+
console.log(` ${tokenBar} ${chalk.hex(theme.muted)(`${tokenPct}%`)}`)
41+
console.log(` ${chalk.hex(theme.greenMute)("Used")} ${formatTokenCount(total).padStart(8)} ${chalk.hex(theme.dim)(`/ ${formatTokenCount(DAILY_BUDGET_TOKENS)}`)}`)
42+
console.log(` ${chalk.hex(theme.greenMute)("Remaining")} ${formatTokenCount(tokenRemaining).padStart(8)}`)
43+
console.log()
44+
45+
console.log(` ${chalk.hex(theme.greenGlow).bold("Query Limit")}`)
46+
const queryBar = chalk.hex(queryPct >= 80 ? theme.red : theme.green)("█".repeat(Math.round((queryPct / 100) * barWidth))) +
47+
chalk.hex(theme.greenDim)("█".repeat(Math.max(0, barWidth - Math.round((queryPct / 100) * barWidth))))
48+
console.log(` ${queryBar} ${chalk.hex(theme.muted)(`${queryPct}%`)}`)
49+
console.log(` ${chalk.hex(theme.greenMute)("Used")} ${String(queryCount).padStart(8)} ${chalk.hex(theme.dim)(`/ ${DAILY_QUERY_LIMIT}`)}`)
50+
console.log(` ${chalk.hex(theme.greenMute)("Remaining")} ${String(queryRemaining).padStart(8)}`)
51+
console.log(` ${chalk.hex(theme.greenMute)("Resets")} ${tomorrow.toLocaleDateString()}`)
52+
53+
if (tokenPct >= 80 || queryPct >= 80) {
54+
console.log()
55+
const warnings: string[] = []
56+
if (tokenPct >= 80) warnings.push("Token budget nearly exhausted")
57+
if (queryPct >= 80) warnings.push("Query limit nearly exhausted")
58+
console.log(` ${chalk.hex(theme.red)(`⚠ ${warnings.join(" — ")}`)}`)
59+
console.log(` ${chalk.hex(theme.red)("Requests will be blocked once either limit is depleted.")}`)
60+
}
61+
62+
const allOpusUsage = await prisma.usageEvent.groupBy({
63+
by: ["model"],
64+
where: {
65+
model: OPUS_MODEL_ID,
66+
createdAt: { gte: new Date(Date.now() - 30 * 86_400_000) },
67+
},
68+
_sum: { totalTokens: true },
69+
_count: { id: true },
70+
})
71+
72+
if (allOpusUsage.length > 0) {
73+
console.log()
74+
console.log(sectionHeader("30-Day History", { accent: "green" }))
75+
for (const row of allOpusUsage) {
76+
const tokens = row._sum.totalTokens ?? 0
77+
const calls = row._count.id
78+
console.log(` ${chalk.hex(theme.greenGlow)("Opus 4.8".padEnd(30))} ${formatTokenCount(tokens).padStart(8)} ${chalk.hex(theme.muted)(`${calls} calls`)}`)
79+
}
80+
}
81+
82+
console.log()
83+
console.log(heavyDivider())
84+
console.log()
85+
}

apps/supercode-cli/server/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const MODEL_MAX_TOKENS: Record<string, number> = {
3333
"glm-5.2": 4096,
3434
"glm-5.1": 4096,
3535
"minimax-m3": 8192,
36+
"anthropic/claude-opus-4-8": 4096,
3637
}
3738
function getModelMaxTokens(model: string): number {
3839
const exact = MODEL_MAX_TOKENS[model]

apps/supercode-cli/server/src/lib/pricing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const MODEL_PRICING: Record<string, ModelPricing> = {
2525
"z-ai/glm-5.1": { inputPrice: 0.10, outputPrice: 0.40, cachedPrice: 0 },
2626
"minimax-m3": { inputPrice: 0.20, outputPrice: 0.80, cachedPrice: 0 },
2727
"meta/llama-3.3-70b-instruct": { inputPrice: 0.59, outputPrice: 0.99, cachedPrice: 0 },
28+
"anthropic/claude-opus-4-8": { inputPrice: 5.00, outputPrice: 25.00, cachedPrice: 0.50 },
2829
}
2930

3031
export function computeCost(model: string, inputTokens: number, outputTokens: number, cachedInputTokens: number): number {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { readFile, mkdir, writeFile } from "node:fs/promises"
2+
import { join } from "node:path"
3+
import os from "node:os"
4+
import { randomUUID } from "node:crypto"
5+
import prisma from "./prisma"
6+
7+
export const OPUS_MODEL_ID = "anthropic/claude-opus-4-8"
8+
export const DAILY_BUDGET_TOKENS = 128_000
9+
export const DAILY_QUERY_LIMIT = 20
10+
11+
const DEVICE_ID_PATH = join(os.homedir(), ".config", "supercode", "device-id")
12+
13+
export async function getOrCreateDeviceId(): Promise<string> {
14+
try {
15+
const existing = await readFile(DEVICE_ID_PATH, "utf-8")
16+
const trimmed = existing.trim()
17+
if (trimmed.length > 0) return trimmed
18+
} catch {}
19+
20+
const uuid = randomUUID()
21+
await mkdir(join(os.homedir(), ".config", "supercode"), { recursive: true })
22+
await writeFile(DEVICE_ID_PATH, uuid, "utf-8")
23+
return uuid
24+
}
25+
26+
function todayStart(): Date {
27+
const now = new Date()
28+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
29+
}
30+
31+
export async function getDailyTokenUsage(model: string): Promise<number> {
32+
const result = await prisma.usageEvent.aggregate({
33+
_sum: { totalTokens: true },
34+
where: {
35+
model,
36+
createdAt: { gte: todayStart() },
37+
},
38+
})
39+
return result._sum.totalTokens ?? 0
40+
}
41+
42+
export async function getDailyQueryCount(userId: string): Promise<number> {
43+
return prisma.usageEvent.count({
44+
where: {
45+
model: OPUS_MODEL_ID,
46+
userId,
47+
createdAt: { gte: todayStart() },
48+
},
49+
})
50+
}
51+
52+
export async function checkQueryLimit(userId: string): Promise<{
53+
allowed: boolean
54+
used: number
55+
remaining: number
56+
resetTime: string
57+
}> {
58+
const used = await getDailyQueryCount(userId)
59+
const remaining = Math.max(0, DAILY_QUERY_LIMIT - used)
60+
const tomorrow = new Date(todayStart().getTime() + 86_400_000)
61+
return {
62+
allowed: remaining > 0,
63+
used,
64+
remaining,
65+
resetTime: tomorrow.toISOString(),
66+
}
67+
}
68+
69+
export async function checkDailyBudget(): Promise<{
70+
allowed: boolean
71+
used: number
72+
remaining: number
73+
resetTime: string
74+
}> {
75+
const used = await getDailyTokenUsage(OPUS_MODEL_ID)
76+
const remaining = Math.max(0, DAILY_BUDGET_TOKENS - used)
77+
const tomorrow = new Date(todayStart().getTime() + 86_400_000)
78+
return {
79+
allowed: remaining > 0,
80+
used,
81+
remaining,
82+
resetTime: tomorrow.toISOString(),
83+
}
84+
}

0 commit comments

Comments
 (0)