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.79",
"version": "0.1.80",
"description": "AI-powered coding agent CLI",
"main": "dist/main.js",
"bin": {
Expand Down
17 changes: 15 additions & 2 deletions apps/supercode-cli/server/src/cli/ai/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface AIProvider {
export const providerMeta: Record<ModelProvider, { env: string; label: string; defaultModel: string; link?: string }> = {
supercode: { env: "", label: "Supercode Cloud", defaultModel: "deepseek-v4-flash" },
google: { env: "GOOGLE_BYOK_PROD_KEY / GOOGLE_BYOK_DEV_KEY", label: "Google Gemini", defaultModel: "gemini-2.5-flash", link: "https://aistudio.google.com/apikey" },
minimax: { env: "MINIMAX_API_KEY", label: "MiniMax", defaultModel: "MiniMax-M2" },
minimax: { env: "MINIMAX_BYOK_PROD_KEY / MINIMAX_BYOK_DEV_KEY", label: "MiniMax", defaultModel: "MiniMax-M2", link: "https://platform.minimax.ai" },
openrouter: { env: "OPENROUTER_BYOK_PROD_KEY / OPENROUTER_BYOK_DEV_KEY", label: "OpenRouter", defaultModel: "openai/gpt-oss-120b:free", link: "https://openrouter.ai/keys" },
nvidia: { env: "NVIDIA_BYOK_PROD_KEY / NVIDIA_BYOK_DEV_KEY", label: "NVIDIA NIM", defaultModel: "minimaxai/minimax-m3" },
concentrateai: { env: "CONCENTRATE_BYOK_PROD_KEY / CONCENTRATE_BYOK_DEV_KEY", label: "ConcentrateAI", defaultModel: "deepseek-v4-flash", link: "https://concentrate.ai" },
Expand All @@ -55,7 +55,7 @@ export const providerMeta: Record<ModelProvider, { env: string; label: string; d
const providerConfigs: Record<ModelProvider, () => string> = {
supercode: () => "",
google: () => process.env.GOOGLE_BYOK_PROD_KEY || process.env.GOOGLE_BYOK_DEV_KEY || config.googleApiKey,
minimax: () => minimaxConfig.apiKey,
minimax: () => process.env.MINIMAX_BYOK_PROD_KEY || process.env.MINIMAX_BYOK_DEV_KEY || minimaxConfig.apiKey,

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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether provider.ts routes "minimax" to a direct service.
rg -n 'case "minimax"' apps/supercode-cli/server/src/cli/ai/provider.ts
rg -n 'MinimaxService' apps/supercode-cli/server/src/cli/ai/provider.ts
ast-grep run --pattern 'switch ($P) { $$$ }' --lang ts apps/supercode-cli/server/src/cli/ai/provider.ts

Repository: yashdev9274/supercli

Length of output: 6657


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' apps/supercode-cli/server/src/cli/ai/provider.ts
printf '\n--- connect.ts ---\n'
sed -n '1,220p' apps/supercode-cli/server/src/cli/ai/connect.ts
printf '\n--- minimax-service.ts ---\n'
sed -n '1,220p' apps/supercode-cli/server/src/cli/ai/minimax-service.ts

Repository: yashdev9274/supercli

Length of output: 9268


Add a minimax switch branch

providerConfigs.minimax now returns a BYOK/config key, but createProvider() has no case "minimax". With a valid key, this skips the no-key path and falls through to the default "paused or unavailable" error. Wire MinimaxService into the switch so MiniMax can actually be selected.

🤖 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/cli/ai/provider.ts` at line 58, Add a case for
"minimax" in createProvider(), constructing and returning MinimaxService with
the resolved providerConfigs.minimax key. Preserve the existing no-key
validation and default error behavior for other providers.

openrouter: () => process.env.OPENROUTER_BYOK_PROD_KEY || process.env.OPENROUTER_BYOK_DEV_KEY || openRouterConfig.apiKey,
nvidia: () => process.env.NVIDIA_BYOK_PROD_KEY || process.env.NVIDIA_BYOK_DEV_KEY || nvidiaConfig.apiKey,
concentrateai: () => process.env.CONCENTRATE_BYOK_PROD_KEY || process.env.CONCENTRATE_BYOK_DEV_KEY || process.env.CONCENTRATEAI_API_KEY || "",
Expand Down Expand Up @@ -84,6 +84,19 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi
generateObject: (schema, prompt) => svc.generateObject(schema, prompt),
}
}
if (provider === "orcarouter") {
// Fall back to Supercode Cloud proxy when no BYOK key is configured.
// The server uses its own ORCAROUTER_API_KEY to handle the request.
const svc = new ServerProxyService("orcarouter", model || meta.defaultModel)
return {
name: provider,
modelName: model || meta.defaultModel,
connectionType: "proxy",
sendMessage: (messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish) =>
svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish),
generateObject: (schema, prompt) => svc.generateObject(schema, prompt),
}
}
throw new Error(
`No API key configured for ${meta.label}. ` +
`Run \`/connect\` and select "${meta.label}" to save your key, ` +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const PROVIDERS: Array<{ value: ModelProvider; label: string; hint: string; link
{ value: "openrouter", label: "OpenRouter", hint: "multi-provider access", link: "https://openrouter.ai/keys" },
{ value: "nvidia", label: "NVIDIA NIM", hint: "free NVIDIA hosted models", link: "https://build.nvidia.com/explore/discover" },
{ value: "orcarouter", label: "OrcaRouter", hint: "multi-provider router", link: "https://orcarouter.ai" },
{ value: "minimax", label: "MiniMax", hint: "fast & smart models", link: "https://platform.minimax.ai" },
]

export async function connectProvider(): Promise<{ type: "connect"; provider?: ModelProvider; model?: string }> {
Expand Down
9 changes: 8 additions & 1 deletion apps/supercode-cli/server/src/config/orcarouter.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export function getOrcaRouterApiKey(): string {
return process.env.ORCAROUTER_BYOK_PROD_KEY
|| process.env.ORCAROUTER_BYOK_DEV_KEY
|| process.env.ORCAROUTER_API_KEY
|| ""
}

export const orcarouterConfig = {
get apiKey() { return process.env.ORCAROUTER_API_KEY || "" },
get apiKey() { return getOrcaRouterApiKey() },
get model() { return process.env.ORCAROUTER_MODEL || "openai/gpt-4o-mini" },
baseUrl: "https://api.orcarouter.ai/v1",
get maxTokens() { return Number(process.env.ORCAROUTER_MAX_TOKENS) || 128000 },
Expand Down
130 changes: 130 additions & 0 deletions apps/supercode-cli/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,113 @@ app.post("/api/ai/chat", async (req, res) => {
res.end()
break
}
case "orcarouter": {
const apiKey = process.env.ORCAROUTER_API_KEY
if (!apiKey) { res.status(500).json({ error: "OrcaRouter not configured on server" }); return }
const modelName = modelParam || "openai/gpt-4o-mini"
const orStart = 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,
}
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.orcarouter.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: `OrcaRouter API ${response.status}: ${errText}` })
return
}
Comment on lines +692 to +701

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No timeout on the OrcaRouter fetch call.

The streaming request to https://api.orcarouter.ai/v1/chat/completions has no AbortSignal/timeout. If OrcaRouter hangs or is unreachable, this request-handling thread can block indefinitely, tying up server resources with no way for the client to recover other than disconnecting.

🛡️ Suggested fix
+        const controller = new AbortController()
+        const timeoutId = setTimeout(() => controller.abort(), 60_000)
         const response = await fetch("https://api.orcarouter.ai/v1/chat/completions", {
           method: "POST",
           headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
           body: JSON.stringify(bodyObj),
+          signal: controller.signal,
         })
+        clearTimeout(timeoutId)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch("https://api.orcarouter.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: `OrcaRouter API ${response.status}: ${errText}` })
return
}
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 60_000)
const response = await fetch("https://api.orcarouter.ai/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(bodyObj),
signal: controller.signal,
})
clearTimeout(timeoutId)
if (!response.ok) {
const errText = await response.text().catch(() => "unknown error")
res.status(response.status).json({ error: `OrcaRouter API ${response.status}: ${errText}` })
return
}
🤖 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 692 - 701, Update the
OrcaRouter fetch call in the request handler to use an AbortSignal with a finite
timeout, ensuring hung or unreachable streaming requests are aborted and handled
through the existing error path. Preserve the current request payload, headers,
and non-OK response handling.

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 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) {
res.write(JSON.stringify({ type: "text", content: delta.content }) + "\n")
}
if (delta?.tool_calls) {
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") {
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 */ }
}
}
recordUsage({
provider: "orcarouter", model: modelName,
inputTokens, outputTokens, cachedInputTokens: 0,
totalTokens: inputTokens + outputTokens,
costUsd: computeCost(modelName, inputTokens, outputTokens, 0),
durationMs: Date.now() - orStart,
})
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")
Comment on lines +763 to +766

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

Finish reason always reported as "stop".

The final finish event hardcodes reason: "stop" regardless of the actual finish_reason streamed by OrcaRouter (e.g., "length" or "content_filter"). Callers lose the ability to distinguish a truncated response from a normal completion.

🐛 Suggested fix
+        let lastFinishReason = "stop"
         while (true) {
           ...
               const finishReason = data.choices?.[0]?.finish_reason
+              if (finishReason) lastFinishReason = finishReason
               if (finishReason === "tool_calls") {
         ...
         res.write(JSON.stringify({
-          type: "finish", reason: "stop",
+          type: "finish", reason: lastFinishReason === "tool_calls" ? "tool-calls" : lastFinishReason,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.write(JSON.stringify({
type: "finish", reason: lastFinishReason === "tool_calls" ? "tool-calls" : lastFinishReason,
usage: { inputTokens, outputTokenDetails: { textTokens: outputTokens, reasoningTokens: 0 }, outputTokens, inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, totalTokens: inputTokens + outputTokens }
}) + "\n")
🤖 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 763 - 766, Update the
final finish event in the streaming response to use the actual OrcaRouter
finish_reason captured during streaming instead of hardcoding "stop". Preserve
the existing usage payload and ensure the streamed reason is propagated for
cases such as "length" and "content_filter".

res.end()
break
}
case "concentrateai": {
const { concentrateAiKey: forwardedKey } = req.body
const apiKey = forwardedKey || process.env.CONCENTRATEAI_API_KEY
Expand Down Expand Up @@ -1056,6 +1163,29 @@ app.post("/api/ai/generate-object", async (req, res) => {
res.json({ object: { content: data.choices?.[0]?.message?.content || "" } })
break
}
case "orcarouter": {
const apiKey = process.env.ORCAROUTER_API_KEY
if (!apiKey) { res.status(500).json({ error: "OrcaRouter not configured on server" }); return }
const modelName = modelParam || "openai/gpt-4o-mini"
const response = await fetch("https://api.orcarouter.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),
stream: false,
}),
})
if (!response.ok) {
const errText = await response.text().catch(() => "unknown error")
res.status(response.status).json({ error: `OrcaRouter API ${response.status}: ${errText}` })
return
}
const data: any = await response.json()
res.json({ object: { content: data.choices?.[0]?.message?.content || "" } })
break
}
Comment on lines +1166 to +1188

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Non-streaming OrcaRouter call also lacks a timeout — shares root cause with the streaming handler.

See the fetch-timeout comment on Lines 692-701 (same issue, same fix pattern applies here).

🤖 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 1166 - 1188, The
non-streaming OrcaRouter request in the “orcarouter” case lacks the required
timeout. Update its fetch call to use the same timeout mechanism and duration as
the streaming OrcaRouter handler, preserving the existing request and response
handling.

case "concentrateai": {
const { concentrateAiKey: forwardedKey } = req.body
const apiKey = forwardedKey || process.env.CONCENTRATEAI_API_KEY
Expand Down
3 changes: 2 additions & 1 deletion apps/supercode-cli/server/src/lib/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,15 @@ const BYOK_ENV_OVERRIDES: Partial<Record<ModelProvider, { prod: string; dev: str
mergedev: { prod: "MERGE_DEV_BYOK_PROD_KEY", dev: "MERGE_DEV_BYOK_DEV_KEY" },
google: { prod: "GOOGLE_BYOK_PROD_KEY", dev: "GOOGLE_BYOK_DEV_KEY" },
openrouter: { prod: "OPENROUTER_BYOK_PROD_KEY", dev: "OPENROUTER_BYOK_DEV_KEY" },
minimax: { prod: "MINIMAX_BYOK_PROD_KEY", dev: "MINIMAX_BYOK_DEV_KEY" },
nvidia: { prod: "NVIDIA_BYOK_PROD_KEY", dev: "NVIDIA_BYOK_DEV_KEY" },
orcarouter: { prod: "ORCAROUTER_BYOK_PROD_KEY", dev: "ORCAROUTER_BYOK_DEV_KEY" },
}

export function getByokSessionKey(provider: ModelProvider): string | undefined {
const override = BYOK_ENV_OVERRIDES[provider]
if (override) {
return process.env[override.prod] || process.env[override.dev]
return process.env[override.prod] || process.env[override.dev] || process.env[API_KEY_ENV_MAP[provider]] || undefined
}
return process.env[API_KEY_ENV_MAP[provider]] || undefined
}
Expand Down
Loading