Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/supercode-cli/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supercode-cli",
"version": "0.1.72",
"version": "0.1.73",
"description": "AI-powered coding agent CLI",
"main": "dist/main.js",
"bin": {
Expand Down
6 changes: 5 additions & 1 deletion apps/supercode-cli/server/src/cli/ai/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi
// All other providers require the user to connect their own key first.
if (!providerConfigs[provider]()) {
if (provider === "supercode") {
const svc = new ServerProxyService(provider, model || meta.defaultModel)
// Cloud models run through ConcentrateAI with the server's own API key.
// Send "concentrateai" as the provider so the server's existing handler
// picks it up — it falls back to CONCENTRATEAI_API_KEY when no forwarded
// key is provided.
const svc = new ServerProxyService("concentrateai", model || meta.defaultModel)
return {
name: provider,
modelName: model || meta.defaultModel,
Expand Down
170 changes: 170 additions & 0 deletions apps/supercode-cli/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,152 @@ app.post("/api/ai/chat", async (req, res) => {
res.end()
break
}
case "supercode": {
const apiKey = process.env.CONCENTRATEAI_API_KEY
if (!apiKey) { res.status(500).json({ error: "Supercode Cloud not configured on server" }); return }
const modelName = modelParam || "deepseek-v4-flash"
const scStart = Date.now()
const bodyObj: any = {
model: modelName,
messages: nonSystemMessages.map((m: any) => {
const msg: any = {
role: m.role,
content: m.content !== null && m.content !== undefined ? String(m.content) : "",
}
if (m.tool_calls) msg.tool_calls = m.tool_calls
if (m.tool_call_id) msg.tool_call_id = m.tool_call_id
return msg
}),
max_tokens: getModelMaxTokens(modelName),
temperature: 0.7,
stream: true,
Comment on lines +831 to +833

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/supercode-cli/server/src/index.ts"

echo "== file size =="
wc -l "$FILE"

echo
echo "== relevant lines around 800-980 =="
sed -n '800,980p' "$FILE" | cat -n

echo
echo "== search for accounting helpers =="
rg -n "recordUsage|computeCost|inputTokenDetails|cached_tokens|stream_options|include_usage|usage" "$FILE"

Repository: yashdev9274/supercli

Length of output: 13524


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/supercode-cli/server/src/index.ts"

echo "== stream request blocks =="
sed -n '820,970p' "$FILE" | nl -ba | sed -n '1,220p'

echo
echo "== definitions and usages =="
rg -n -C 3 "recordUsage|computeCost|inputTokenDetails|cached_tokens|prompt_tokens_details|completion_tokens_details|usage event|stream_options|include_usage" "$FILE"

Repository: yashdev9274/supercli

Length of output: 226


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 "stream_options|include_usage|prompt_tokens_details|cached_tokens|recordUsage|computeCost|inputTokenDetails" apps/supercode-cli/server/src

Repository: yashdev9274/supercli

Length of output: 34747


🌐 Web query:

OpenAI Chat Completions streaming include_usage prompt_tokens_details.cached_tokens usage event docs

💡 Result:

To include token usage statistics in your OpenAI Chat Completions streaming response, you must set the stream_options parameter to {"include_usage": true} in your request [1][2][3]. When this option is enabled: 1. Every streaming chunk will contain a usage field, which is set to null for all chunks except the final one [4][1][3]. 2. An additional, final chunk is streamed before the data: [DONE] message [4][2][3]. This final chunk contains the total token usage statistics for the entire request in its usage field [4][1][2]. 3. The choices array in this final usage-only chunk will be empty [4][1][2]. Regarding prompt_tokens_details and cached_tokens: When usage statistics are included, the usage object may contain a prompt_tokens_details field, which provides a breakdown of tokens used in the prompt [5]. This includes the cached_tokens field, which indicates how many input tokens were read from cache [5]. If prompt caching is not applicable or the request is small (typically under 1024 tokens), cached_tokens is zero [5][6]. The structure and presence of these fields in the final streaming usage chunk follow the same schema as non-streaming response usage objects [5]. Note: If the stream is interrupted, you may not receive this final usage chunk [4][2]. Additionally, ensure your code handles the final usage chunk by checking for the presence of usage data and an empty choices array, as this differs from standard content chunks [1][3].

Citations:


Request streamed usage metadata.
Add stream_options: { include_usage: true } to the streamed request, and carry cached prompt-token counts through recordUsage, computeCost, and the emitted inputTokenDetails; otherwise streamed runs persist zero/incorrect token and cost totals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/supercode-cli/server/src/index.ts` around lines 831 - 833, Update the
streamed request configuration near getModelMaxTokens to include usage metadata
via stream_options. Propagate cached prompt-token counts through recordUsage and
computeCost, then populate emitted inputTokenDetails with the resulting values
so streamed runs persist accurate token and cost totals.

}
if (system && nonSystemMessages.length > 0) {
bodyObj.messages = [{ role: "system", content: system }, ...bodyObj.messages]
}
if (tools) {
bodyObj.tools = Object.entries(tools).map(([name, fn]: [string, any]) => ({
type: "function",
function: { name, description: fn.description || "", parameters: toolParams(fn) },
}))
}
const response = await fetch("https://api.concentrate.ai/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(bodyObj),
})
if (!response.ok) {
const errText = await response.text().catch(() => "unknown error")
res.status(response.status).json({ error: `Supercode Cloud API ${response.status}: ${errText}` })
return
}
const reader = response.body?.getReader()
if (!reader) { res.status(500).json({ error: "No response body" }); return }
const decoder = new TextDecoder()
let buffer = ""
let inputTokens = 0
let outputTokens = 0
let fullContent = ""
let sawToolCalls = false
let pendingToolCalls: Record<number, { id: string; name: string; args: string }> = {}
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")
buffer = lines.pop() || ""
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed || !trimmed.startsWith("data: ")) continue
const jsonStr = trimmed.slice(6)
if (jsonStr === "[DONE]") break
try {
const data = JSON.parse(jsonStr)
const delta = data.choices?.[0]?.delta
if (delta?.content) {
fullContent += delta.content
res.write(JSON.stringify({ type: "text", content: delta.content }) + "\n")
}
if (delta?.tool_calls) {
sawToolCalls = true
for (const tc of delta.tool_calls) {
const index = tc.index ?? 0
if (!pendingToolCalls[index]) {
pendingToolCalls[index] = { id: "", name: "", args: "" }
}
if (tc.id) pendingToolCalls[index].id = tc.id
if (tc.function?.name) pendingToolCalls[index].name = tc.function.name
if (tc.function?.arguments) pendingToolCalls[index].args += tc.function.arguments
}
}
const finishReason = data.choices?.[0]?.finish_reason
if (finishReason === "tool_calls") {
sawToolCalls = true
for (const [, call] of Object.entries(pendingToolCalls)) {
if (call.name && call.args) {
try {
const parsed = JSON.parse(call.args)
res.write(JSON.stringify({ type: "tool-call", toolName: call.name, args: parsed, toolCallId: call.id }) + "\n")
} catch { /* skip malformed args */ }
}
}
pendingToolCalls = {}
}
if (data.usage) {
inputTokens = data.usage.prompt_tokens ?? 0
outputTokens = data.usage.completion_tokens ?? 0
}
} catch { /* skip malformed */ }
}
}
if (!fullContent.trim() && !sawToolCalls) {
const fbBody: any = {
model: modelName,
messages: nonSystemMessages.map((m: any) => {
const msg: any = {
role: m.role,
content: m.content !== null && m.content !== undefined ? String(m.content) : "",
}
if (m.tool_calls) msg.tool_calls = m.tool_calls
if (m.tool_call_id) msg.tool_call_id = m.tool_call_id
return msg
}),
max_tokens: getModelMaxTokens(modelName),
temperature: 0.7,
stream: false,
}
if (system && nonSystemMessages.length > 0) {
fbBody.messages = [{ role: "system", content: system }, ...fbBody.messages]
}
const fbRes = await fetch("https://api.concentrate.ai/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(fbBody),
})
if (fbRes.ok) {
const fbData: any = await fbRes.json()
const fbContent = fbData?.choices?.[0]?.message?.content ?? ""
if (fbContent) {
res.write(JSON.stringify({ type: "text", content: fbContent }) + "\n")
}
inputTokens = fbData?.usage?.prompt_tokens ?? 0
outputTokens = fbData?.usage?.completion_tokens ?? 0
}
Comment on lines +913 to +945

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve tools and propagate fallback failures.

The fallback drops bodyObj.tools, and a non-2xx fbRes falls through to an empty successful completion. Reuse the original request body and handle fallback errors.

Proposed fix
-          const fbBody: any = {
-            model: modelName,
-            messages: nonSystemMessages.map(/* ... */),
-            max_tokens: getModelMaxTokens(modelName),
-            temperature: 0.7,
-            stream: false,
-          }
+          const fbBody = {
+            ...bodyObj,
+            stream: false,
+          }

Also return the fallback upstream error when !fbRes.ok.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/supercode-cli/server/src/index.ts` around lines 913 - 945, The fallback
request in the empty-content path must preserve tools and propagate upstream
failures. Update the fallback body construction around fbBody to reuse the
original bodyObj fields, including tools, while retaining the fallback-specific
messages and settings; when fbRes is not successful, return the upstream error
response instead of continuing as an empty successful completion.

}
recordUsage({
provider: "supercode", model: modelName,
inputTokens, outputTokens, cachedInputTokens: 0,
totalTokens: inputTokens + outputTokens,
costUsd: computeCost(modelName, inputTokens, outputTokens, 0),
durationMs: Date.now() - scStart,
})
res.write(JSON.stringify({
type: "finish", reason: "stop",
usage: { inputTokens, outputTokenDetails: { textTokens: outputTokens, reasoningTokens: 0 }, outputTokens, inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, totalTokens: inputTokens + outputTokens }
}) + "\n")
res.end()
break
}
default: {
res.status(400).json({ error: `Unknown provider: ${provider}` })
}
Expand Down Expand Up @@ -935,6 +1081,30 @@ app.post("/api/ai/generate-object", async (req, res) => {
res.json({ object: { content: data.choices?.[0]?.message?.content || "" } })
break
}
case "supercode": {
const apiKey = process.env.CONCENTRATEAI_API_KEY
if (!apiKey) { res.status(500).json({ error: "Supercode Cloud not configured on server" }); return }
const modelName = modelParam || "deepseek-v4-flash"
const response = await fetch("https://api.concentrate.ai/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({
model: modelName,
messages: [{ role: "user", content: prompt }],
max_tokens: getModelMaxTokens(modelName),
temperature: 0.7,
stream: false,
}),
})
if (!response.ok) {
const errText = await response.text().catch(() => "unknown error")
res.status(response.status).json({ error: `Supercode Cloud API ${response.status}: ${errText}` })
return
}
const data: any = await response.json()
res.json({ object: { content: data.choices?.[0]?.message?.content || "" } })
Comment on lines +1091 to +1105

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/supercode-cli/server/src/index.ts"

echo "== line count =="
wc -l "$FILE"

echo
echo "== outline (if available) =="
ast-grep outline "$FILE" --view expanded || true

echo
echo "== surrounding lines 1040-1135 =="
sed -n '1040,1135p' "$FILE"

echo
echo "== search for schema usage in file =="
rg -n '\bschema\b|response_format|structured|json_schema|json_object' "$FILE"

Repository: yashdev9274/supercli

Length of output: 5247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the route/function that contains the snippet and inspect nearby context
FILE="apps/supercode-cli/server/src/index.ts"
python3 - <<'PY'
from pathlib import Path
p = Path("apps/supercode-cli/server/src/index.ts")
text = p.read_text()
needle = 'res.json({ object: { content: data.choices?.[0]?.message?.content || "" } })'
idx = text.find(needle)
print("needle_index", idx)
if idx != -1:
    start = max(0, text.rfind('\n', 0, idx - 1500))
    end = min(len(text), idx + 700)
    snippet = text[start:end]
    # show with line numbers
    base_line = text[:start].count('\n') + 1
    for i, line in enumerate(snippet.splitlines(), start=base_line):
        print(f"{i}: {line}")
PY

Repository: yashdev9274/supercli

Length of output: 2656


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect whether the API client or downstream parser expects a structured object
rg -n 'object\.content|structured generation|schema-based|response_format|message\.content|choices\?\.\[0\]\?\.message' apps/supercode-cli/server/src

Repository: yashdev9274/supercli

Length of output: 1340


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/supercode-cli/server/src/index.ts"
sed -n '1000,1135p' "$FILE"

Repository: yashdev9274/supercli

Length of output: 6066


Use the schema-driven path for the Supercode provider
apps/supercode-cli/server/src/index.ts:1091-1105 — This branch ignores schema and returns free-form message.content under object.content, so structured-generation callers don’t get the object they requested. Match the other structured-generation branches and return the parsed object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/supercode-cli/server/src/index.ts` around lines 1091 - 1105, Update the
Supercode provider branch around the response handling to honor the requested
schema: parse the model’s structured response using the existing schema-driven
path and return the resulting object instead of wrapping free-form
message.content in object.content. Preserve the current HTTP error handling and
use the same structured-generation utilities or response shape as the other
provider branches.

break
}
default: {
res.status(400).json({ error: `Provider ${provider} not supported for structured generation` })
}
Expand Down
152 changes: 152 additions & 0 deletions apps/supercode-cli/server/src/tools/definitions/question.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { z } from "zod"
import chalk from "chalk"
import boxen from "boxen"
import * as readline from "readline"
import { theme } from "../../cli/utils/tui"
import { serialize, ok, fail } from "../../cli/ai/tool-result"

const questionOptionSchema = z.object({
label: z.string().describe("Display text (1-5 words, concise)"),
description: z.string().optional().describe("Explanation of the choice"),
})

const questionItemSchema = z.object({
question: z.string().describe("Complete question to ask the user"),
header: z.string().optional().describe("Very short label (max 30 chars) for the question category"),
options: z
.array(questionOptionSchema)
.optional()
.describe("Available choices for the user to select from"),
multiple: z
.boolean()
.optional()
.default(false)
.describe("Allow selecting multiple choices. When enabled, user can pick several options."),
})

const questionSchema = z.object({
questions: z
.array(questionItemSchema)
.min(1)
.max(5)
.describe("Questions to ask the user"),
})

export type QuestionArgs = z.infer<typeof questionSchema>

function styledLabel(num: number, label: string): string {
const numTag = chalk.hex(theme.green)(` ${num}.`)
return `${numTag} ${chalk.bold(label)}`
}

function styledDesc(desc: string): string {
return chalk.hex(theme.muted)(` ${desc}`)
}

function askOne(item: z.infer<typeof questionItemSchema>): Promise<string | string[]> {
return new Promise((resolve) => {
const stdin = process.stdin
const wasRaw = stdin.isRaw

if (stdin.isTTY) {
stdin.setRawMode(false)
}

console.log()

const header = item.header
? chalk.hex(theme.green)(` ${item.header} `)
: ""

const boxContent = item.header
? chalk.bold(item.question)
: chalk.bold(item.question)

console.log(
boxen(boxContent, {
title: item.header || undefined,
titleAlignment: "left",
borderColor: theme.green,
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: 0,
borderStyle: "round",
}),
)

const rl = readline.createInterface({ input: stdin, output: process.stdout, terminal: true })

if (!item.options || item.options.length === 0) {
rl.question(chalk.hex(theme.green)(" Your answer: "), (answer) => {
rl.close()
if (stdin.isTTY && wasRaw) stdin.setRawMode(true)
resolve(answer.trim())
})
return
}

console.log()
item.options.forEach((opt, i) => {
console.log(styledLabel(i + 1, opt.label))
if (opt.description) {
console.log(styledDesc(opt.description))
}
})
console.log()

const promptText = item.multiple
? chalk.hex(theme.green)(" Select options (comma-separated numbers, e.g. 1,3): ")
: chalk.hex(theme.green)(" Your choice (number): ")

rl.question(promptText, (raw) => {
rl.close()
if (stdin.isTTY && wasRaw) stdin.setRawMode(true)

const trimmed = raw.trim()
if (!trimmed) {
resolve([])
return
}

if (item.multiple) {
const indices = trimmed
.split(",")
.map((s) => parseInt(s.trim(), 10))
.filter((n) => !isNaN(n) && n >= 1 && n <= item.options!.length)
if (indices.length === 0) {
resolve([])
return
}
resolve(indices.map((i) => item.options![i - 1].label))
} else {
const num = parseInt(trimmed, 10)
if (isNaN(num) || num < 1 || num > item.options.length) {
resolve("(invalid selection)")
return
}
resolve(item.options[num - 1].label)
Comment on lines +100 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Re-prompt until a valid choice is entered.

parseInt("1x", 10) selects option 1, blank single-choice input resolves [], and out-of-range input completes with "(invalid selection)". Validate the entire input and re-prompt instead of finalizing an unintended answer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/supercode-cli/server/src/tools/definitions/question.ts` around lines 100
- 126, The selection handling around rl.question must re-prompt until valid
input is entered. Validate the complete trimmed input rather than accepting
parseInt prefixes; for single-choice input reject blanks, malformed, and
out-of-range values, and for multiple-choice input reject invalid entries
instead of resolving an empty result. Preserve valid option-label resolution
while routing every invalid selection back through the prompt.

}
})
})
}

export const questionTool = {
description:
"Ask the user questions to gather context, clarify requirements, " +
"or get decisions on implementation choices. Renders numbered MCQ-style " +
"options for structured feedback. Supports single-choice, multi-select, " +
"and free-form answers. Use before complex tasks to ensure you understand " +
"what the user needs. When asking for preferences or decisions, provide " +
"options with clear labels and optional descriptions.",
parameters: questionSchema,
execute: async (args: QuestionArgs) =>
serialize(async () => {
const answers: Array<{ question: string; answer: string | string[] }> = []

for (const item of args.questions) {
const answer = await askOne(item)
answers.push({ question: item.question, answer })
}

return ok({ answers })
}),
}
Loading
Loading