diff --git a/package-lock.json b/package-lock.json index 6b0efd9f23a..f9de2d7495a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@types/turndown": "^5.0.5", "@types/vscode": "^1.95.0", "@vscode/codicons": "^0.0.36", + "aurelo.ai": "^0.0.2", "axios": "^1.7.4", "cheerio": "^1.0.0", "chokidar": "^4.0.1", @@ -3173,6 +3174,74 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", @@ -3189,6 +3258,329 @@ "node": ">=18" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -6711,6 +7103,12 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/aurelo.ai": { + "version": "0.0.2", + "resolved": "file:../../aurelo code editor as extension/aurelo-sdk/aurelo.ai-0.0.2.tgz", + "integrity": "sha512-k1AfQlhmYWmZaW4cIEv+ht5l0czBLqSdALKw7T47/VOY5SbIO+ggDXQ4D3LoqNY/vDaI9tR8eqDt8r9K2rne1w==", + "license": "Apache-2.0" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", diff --git a/package.json b/package.json index 6aee40b1ac5..9861d74545d 100644 --- a/package.json +++ b/package.json @@ -358,6 +358,7 @@ "@types/turndown": "^5.0.5", "@types/vscode": "^1.95.0", "@vscode/codicons": "^0.0.36", + "aurelo.ai": "^0.0.7", "axios": "^1.7.4", "cheerio": "^1.0.0", "chokidar": "^4.0.1", diff --git a/src/api/index.ts b/src/api/index.ts index 93284ace184..64f5bbe60df 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -20,6 +20,7 @@ import { ApiStream } from "./transform/stream" import { UnboundHandler } from "./providers/unbound" import { RequestyHandler } from "./providers/requesty" import { PearAiHandler } from "./providers/pearai" +import { AureloHandler } from "./providers/aurelo" import { HumanRelayHandler } from "./providers/human-relay" import { FakeAIHandler } from "./providers/fake-ai" @@ -77,6 +78,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new RequestyHandler(options) case "pearai": return new PearAiHandler(options) + case "aurelo": + return new AureloHandler(options) case "human-relay": return new HumanRelayHandler(options) case "fake-ai": diff --git a/src/api/providers/aurelo.ts b/src/api/providers/aurelo.ts new file mode 100644 index 00000000000..bc0d9ba490e --- /dev/null +++ b/src/api/providers/aurelo.ts @@ -0,0 +1,298 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import AureloSDK, { type AureloStreamEvent } from "aurelo.ai" + +import { + ApiHandlerOptions, + aureloDefaultModelId, + aureloModelInfoSaneDefaults, + aureloModels, + ModelInfo, +} from "../../shared/api" + +import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { BaseProvider } from "./base-provider" +import { SingleCompletionHandler } from "../index" + +// A content part sent to Aurelo: either plain text or a native Anthropic image block. +// Text and tool blocks are flattened to strings (the gateway converts them to input_text anyway). +// Image blocks are preserved as-is so the gateway can convert them to input_image for OpenAI. +type AureloContentPart = { type: "text"; text: string } | Anthropic.Messages.ImageBlockParam + +type AureloInputMessage = { + role: "system" | "user" | "assistant" + content: string | AureloContentPart[] +} + +export class AureloHandler extends BaseProvider implements SingleCompletionHandler { + private readonly options: ApiHandlerOptions + private readonly client: AureloSDK + private lastSystemPrompt: string | null = null + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + + if (!options.aureloApiKey) { + throw new Error("Aurelo API key not found.") + } + + this.client = new AureloSDK({ + apiKey: options.aureloApiKey, + baseUrl: options.aureloBaseUrl || "https://aurelo.tech", + }) + } + + override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const model = this.getModel().id + let sawTextDelta = false + let doneText = "" + + // Detect system prompt changes (mode switch, MCP server connect/disconnect, etc.) + // Signal the gateway via rotateReason in metadata — gateway responds with + // rotateSession: true, SDK retries with full sessionStartInput (new system prompt). + const systemPromptChanged = this.lastSystemPrompt !== null && this.lastSystemPrompt !== systemPrompt + this.lastSystemPrompt = systemPrompt + + for await (const event of this.client.streamResponse({ + model, + sessionStartInput: buildFullContextInput(systemPrompt, messages), + latestInput: buildLatestInput(messages), + metadata: { + source: "pearai-roo-code", + model, + ...(systemPromptChanged && { rotateReason: "system_prompt_changed" }), + }, + })) { + const chunk = parseAureloStreamEvent(event, sawTextDelta) + + if (chunk?.type === "text") { + sawTextDelta = true + doneText += chunk.text + } + + if (chunk) { + yield chunk + } + } + + if (!sawTextDelta && doneText) { + yield { + type: "text", + text: doneText, + } + } + } + + override getModel(): { id: string; info: ModelInfo } { + const modelId = this.options.apiModelId || aureloDefaultModelId + return { + id: modelId, + info: aureloModels[modelId as keyof typeof aureloModels] || aureloModelInfoSaneDefaults, + } + } + + async completePrompt(prompt: string): Promise { + let text = "" + for await (const event of this.client.streamResponse({ + model: this.getModel().id, + sessionStartInput: prompt, + latestInput: prompt, + metadata: { + source: "pearai-roo-code", + mode: "single_completion", + }, + })) { + const chunk = parseAureloStreamEvent(event, false) + if (chunk?.type === "text") { + text += chunk.text + } + } + return text + } +} + +function buildFullContextInput( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], +): AureloInputMessage[] { + return [ + { + role: "system", + content: systemPrompt, + }, + ...messages.map((message) => ({ + role: message.role, + content: buildAureloContent(message.content), + })), + ] +} + +function buildLatestInput(messages: Anthropic.Messages.MessageParam[]): AureloInputMessage[] { + const latestMessage = messages[messages.length - 1] + if (!latestMessage) { + return [] + } + + return [ + { + role: latestMessage.role, + content: buildAureloContent(latestMessage.content), + }, + ] +} + +/** + * Converts an Anthropic message content block into the format the Aurelo SDK accepts. + * + * - If the message has no images: returns a flat string (text + tool context joined). + * The Responses API converts these to input_text anyway, so flattening is harmless. + * - If the message has images: returns a mixed AureloContentPart[] where: + * - All text, tool_use, and tool_result blocks are grouped into { type: "text" } parts + * - Image blocks are preserved as Anthropic ImageBlockParam so the gateway can convert + * them into proper { type: "input_image", image_url: "data:..." } parts for OpenAI. + */ +function buildAureloContent(content: Anthropic.Messages.MessageParam["content"]): string | AureloContentPart[] { + if (typeof content === "string") { + return content + } + + const hasImages = content.some((block) => block.type === "image") + + if (!hasImages) { + // No images — flatten everything to a plain string, same as before. + return content + .map((block) => { + if (block.type === "text") return block.text + if (block.type === "tool_use") { + return `[Tool Call: ${block.name}]\n${JSON.stringify(block.input, null, 2)}` + } + if (block.type === "tool_result") { + if (typeof block.content === "string") return `[Tool Result]: ${block.content}` + if (Array.isArray(block.content)) { + return `[Tool Result]: ${block.content + .map((p) => (p.type === "text" ? p.text : "")) + .filter(Boolean) + .join("\n")}` + } + } + return "" + }) + .filter(Boolean) + .join("\n") + } + + // Has images — build a mixed array, grouping consecutive non-image blocks into + // a single text part and keeping each image block as a native Anthropic ImageBlockParam. + const parts: AureloContentPart[] = [] + let pendingText: string[] = [] + + const flushText = () => { + const text = pendingText.filter(Boolean).join("\n") + if (text) { + parts.push({ type: "text", text }) + } + pendingText = [] + } + + for (const block of content) { + if (block.type === "image") { + flushText() + parts.push(block) // preserved natively — gateway converts to input_image + } else if (block.type === "text") { + pendingText.push(block.text) + } else if (block.type === "tool_use") { + pendingText.push(`[Tool Call: ${block.name}]\n${JSON.stringify(block.input, null, 2)}`) + } else if (block.type === "tool_result") { + if (typeof block.content === "string") { + pendingText.push(`[Tool Result]: ${block.content}`) + } else if (Array.isArray(block.content)) { + const resultText = block.content + .map((p) => (p.type === "text" ? p.text : "")) + .filter(Boolean) + .join("\n") + if (resultText) pendingText.push(`[Tool Result]: ${resultText}`) + } + } + } + flushText() + + return parts +} + +function parseAureloStreamEvent( + event: AureloStreamEvent, + ignoreDoneText: boolean, +): { type: "text"; text: string } | { type: "reasoning"; text: string } | ApiStreamUsageChunk | null { + if (event.type === "comment") { + return parseUsageComment(event.comment || "") + } + + if (event.type !== "event") { + return null + } + + const data = event.data + if (!data || typeof data !== "object") { + return null + } + + const payload = data as Record + const type = String(payload.type || "") + + if (type === "error") { + const message = payload.error?.message || payload.message || "Aurelo request failed" + throw new Error(message) + } + + if (type === "response.output_text.delta" && typeof payload.delta === "string") { + return { + type: "text", + text: payload.delta, + } + } + + if (!ignoreDoneText && type === "response.output_text.done" && typeof payload.text === "string") { + return { + type: "text", + text: payload.text, + } + } + + if (type.includes("reasoning") && typeof payload.delta === "string") { + return { + type: "reasoning", + text: payload.delta, + } + } + + const usage = payload.usage || payload.response?.usage + if (usage) { + return parseUsageObject(usage) + } + + return null +} + +function parseUsageComment(comment: string): ApiStreamUsageChunk | null { + const match = comment.match(/\bUsage:\s*input=(\d+)\s+cached=(\d+)\s+output=(\d+)/) + if (!match) { + return null + } + + return { + type: "usage", + inputTokens: Number(match[1]) || 0, + cacheReadTokens: Number(match[2]) || 0, + outputTokens: Number(match[3]) || 0, + } +} + +function parseUsageObject(usage: Record): ApiStreamUsageChunk { + return { + type: "usage", + inputTokens: Number(usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0, + outputTokens: Number(usage.output_tokens ?? usage.completion_tokens ?? 0) || 0, + cacheWriteTokens: Number(usage.input_tokens_details?.cache_creation_tokens ?? 0) || undefined, + cacheReadTokens: Number(usage.input_tokens_details?.cached_tokens ?? 0) || undefined, + } +} diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 20d3d7bb6f5..1d37c18024b 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -163,6 +163,7 @@ export type SecretKey = | "unboundApiKey" | "requestyApiKey" | "pearaiApiKey" + | "aureloApiKey" export type GlobalStateKey = | "apiProvider" @@ -259,6 +260,7 @@ export type GlobalStateKey = | "pearaiModelId" | "pearaiModelInfo" | "pearaiBaseUrl" + | "aureloBaseUrl" export type ConfigurationKey = GlobalStateKey | SecretKey diff --git a/src/shared/api.ts b/src/shared/api.ts index 362ea95a362..b09699e1b2e 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -17,6 +17,7 @@ export type ApiProvider = | "unbound" | "requesty" | "pearai" + | "aurelo" | "human-relay" | "fake-ai" @@ -81,6 +82,8 @@ export interface ApiHandlerOptions { pearaiBaseUrl?: string pearaiModelId?: string pearaiModelInfo?: ModelInfo + aureloApiKey?: string + aureloBaseUrl?: string modelMaxThinkingTokens?: number fakeAi?: unknown } @@ -138,6 +141,7 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [ "unboundModelInfo", "requestyModelId", "requestyModelInfo", + "aureloBaseUrl", "modelTemperature", "modelMaxTokens", "modelMaxThinkingTokens", @@ -1077,3 +1081,55 @@ export const pearAiModels = { "PearAI Model automatically routes you to the most best / most suitable model on the market. Recommended for most users.", }, } as const satisfies Record + +export type AureloModelId = keyof typeof aureloModels +export const aureloDefaultModelId: AureloModelId = "M1" +export const aureloModelInfoSaneDefaults: ModelInfo = { + maxTokens: 100_000, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + cacheWritesPrice: 0, + cacheReadsPrice: 0, + inputPrice: 0, + outputPrice: 0, +} + +export const aureloModels = { + M1: { + ...aureloModelInfoSaneDefaults, + contextWindow: 200_000, + maxTokens: 100_000, + inputPrice: 2, // $2 per million input tokens + outputPrice: 8, // $8 per million output tokens + description: + "Aurelo M1. Fast, highly responsive model optimized for standard coding, autocomplete, and quick refactoring. Features a 200K context window with free prompt caching ($0.00/1M tokens) and a 1-hour write/read cache window.", + }, + "M1 - Long Context": { + ...aureloModelInfoSaneDefaults, + contextWindow: 800_000, + maxTokens: 100_000, + inputPrice: 3, // $3 per million input tokens + outputPrice: 15, // $15 per million output tokens + description: + "Aurelo M1 - Long Context. Designed for scanning large codebases, processing entire folders, and handling massive multi-file refactoring. Features an 800K context window with free prompt caching ($0.00/1M tokens) and a 1-hour write/read cache window.", + }, + M2: { + ...aureloModelInfoSaneDefaults, + contextWindow: 200_000, + maxTokens: 100_000, + inputPrice: 4, // $4 per million input tokens + outputPrice: 16, // $16 per million output tokens + description: + "Aurelo M2. Premium reasoning model optimized for complex systems architecture design, advanced debugging, and logical problem-solving. Features a 200K context window with free prompt caching ($0.00/1M tokens) and a 1-hour write/read cache window.", + }, + "M2 - Long Context": { + ...aureloModelInfoSaneDefaults, + contextWindow: 800_000, + maxTokens: 100_000, + inputPrice: 5, // $5 per million input tokens + outputPrice: 30, // $30 per million output tokens + description: + "Aurelo M2 - Long Context. Ultimate reasoning model with an expanded 800K context window. Best suited for deep codebase-wide analysis and complex multi-file database refactoring. Features free prompt caching ($0.00/1M tokens) and a 1-hour write/read cache window.", + }, +} as const satisfies Record diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index 01e43fef122..3924f5e0718 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -27,6 +27,7 @@ export const SECRET_KEYS = [ "unboundApiKey", "requestyApiKey", "pearaiApiKey", + "aureloApiKey", ] as const // type CheckSecretKeysExhaustiveness = Exclude extends never ? true : false @@ -128,6 +129,7 @@ export const GLOBAL_STATE_KEYS = [ "pearaiModelId", "pearaiModelInfo", "pearaiBaseUrl", + "aureloBaseUrl", ] as const export const PASS_THROUGH_STATE_KEYS = ["taskHistory"] as const diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index a7d9f71c03b..a2fcd4645c9 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -15,6 +15,8 @@ import { ModelInfo, anthropicDefaultModelId, anthropicModels, + aureloDefaultModelId, + aureloModels, azureOpenAiDefaultApiVersion, bedrockDefaultModelId, bedrockModels, @@ -308,6 +310,26 @@ const ApiOptions = ({ )} {errorMessage && } + {selectedProvider === "aurelo" && ( + <> + + + + + + + + )} + {selectedProvider === "openrouter" && ( <> >> = { anthropic: anthropicModels, + aurelo: aureloModels, bedrock: bedrockModels, deepseek: deepSeekModels, gemini: geminiModels, @@ -23,6 +25,7 @@ export const MODELS_BY_PROVIDER: Partial