-
Notifications
You must be signed in to change notification settings - Fork 18
feat: add OrcaRouter support and update version to 0.1.80 #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win No timeout on the OrcaRouter The streaming request to 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Finish reason always reported as The final 🐛 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| case "concentrateai": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { concentrateAiKey: forwardedKey } = req.body | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = forwardedKey || process.env.CONCENTRATEAI_API_KEY | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||
| case "concentrateai": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { concentrateAiKey: forwardedKey } = req.body | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = forwardedKey || process.env.CONCENTRATEAI_API_KEY | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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:
Repository: yashdev9274/supercli
Length of output: 6657
🏁 Script executed:
Repository: yashdev9274/supercli
Length of output: 9268
Add a
minimaxswitch branchproviderConfigs.minimaxnow returns a BYOK/config key, butcreateProvider()has nocase "minimax". With a valid key, this skips the no-key path and falls through to the default"paused or unavailable"error. WireMinimaxServiceinto the switch so MiniMax can actually be selected.🤖 Prompt for AI Agents