Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ for this exact support, so if you are having problems or if you have question, j
## What's New in v3.62.0

- **GLM-5.2 support** — the latest GLM model is now available in your provider settings
- **Novita AI support** — The AI-Native Cloud for Builders and Agents: run models, scale GPUs, and build AI agents, all on one platform
- **OpenCode-Go improvements** — native model parameters, Anthropic-format routing, and a context-token fix for more reliable responses
- **Tool-writer mode** — a new specialized mode for writing and maintaining tool definitions, now available in the Marketplace
- **LiteLLM session header** — forward taskId as X-Zoo-Session-ID request header for better request tracing
Expand Down
11 changes: 9 additions & 2 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ By default, the CLI auto-approves actions and runs in interactive TUI mode:

```bash
export OPENROUTER_API_KEY=sk-or-v1-...
# or use Novita AI, the AI-native cloud for builders and agents:
export NOVITA_API_KEY=...
Comment thread
coderabbitai[bot] marked this conversation as resolved.

roo "What is this project?" -w ~/Documents/my-project
roo "What is this project?" --provider novita -w ~/Documents/my-project
```

You can also run without a prompt and enter it interactively in TUI mode:
Expand Down Expand Up @@ -160,7 +162,7 @@ If you never used Roo Code Router, you can ignore this section entirely.
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
| `-a, --require-approval` | Require manual approval before actions execute | `false` |
| `-k, --api-key <key>` | API key for the LLM provider | From env var |
| `--provider <provider>` | API provider (anthropic, openai-native, gemini, openrouter, vercel-ai-gateway) | `openrouter` |
| `--provider <provider>` | API provider (anthropic, openai-native, gemini, openrouter, novita, vercel-ai-gateway) | `openrouter` |
| `-m, --model <model>` | Model to use | `anthropic/claude-opus-4.6` |
| `--mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
| `--terminal-shell <path>` | Absolute shell path for inline terminal command execution | Auto-detected shell |
Expand All @@ -186,6 +188,7 @@ The CLI will look for API keys in environment variables if not provided via `--a
| anthropic | `ANTHROPIC_API_KEY` |
| openai-native | `OPENAI_API_KEY` |
| openrouter | `OPENROUTER_API_KEY` |
| novita | `NOVITA_API_KEY` |
| gemini | `GOOGLE_API_KEY` |
| vercel-ai-gateway | `VERCEL_AI_GATEWAY_API_KEY` |

Expand Down Expand Up @@ -234,6 +237,10 @@ The CLI will look for API keys in environment variables if not provided via `--a
# Run directly from source (no build required)
pnpm dev --provider openrouter --api-key $OPENROUTER_API_KEY --print "Hello"

# Novita AI: The AI-Native Cloud for Builders and Agents.
# Run models, scale GPUs, and build AI agents, all on one platform.
pnpm dev --provider novita --api-key $NOVITA_API_KEY --model moonshotai/kimi-k2.7-code --print "Hello"

# Run tests
pnpm test

Expand Down
17 changes: 16 additions & 1 deletion apps/cli/src/lib/utils/__tests__/provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getApiKeyFromEnv } from "../provider.js"
import { getApiKeyFromEnv, getProviderSettings } from "../provider.js"

describe("getApiKeyFromEnv", () => {
const originalEnv = process.env
Expand All @@ -22,6 +22,11 @@ describe("getApiKeyFromEnv", () => {
expect(getApiKeyFromEnv("openrouter")).toBe("test-openrouter-key")
})

it("should return API key from environment variable for novita", () => {
process.env.NOVITA_API_KEY = "test-novita-key"
expect(getApiKeyFromEnv("novita")).toBe("test-novita-key")
})

it("should return API key from environment variable for openai", () => {
process.env.OPENAI_API_KEY = "test-openai-key"
expect(getApiKeyFromEnv("openai-native")).toBe("test-openai-key")
Expand All @@ -32,3 +37,13 @@ describe("getApiKeyFromEnv", () => {
expect(getApiKeyFromEnv("anthropic")).toBeUndefined()
})
})

describe("getProviderSettings", () => {
it("should map Novita key and model into provider settings", () => {
expect(getProviderSettings("novita", "test-novita-key", "moonshotai/kimi-k2.7-code")).toEqual({
apiProvider: "novita",
novitaApiKey: "test-novita-key",
apiModelId: "moonshotai/kimi-k2.7-code",
})
})
})
5 changes: 5 additions & 0 deletions apps/cli/src/lib/utils/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const envVarMap: Record<SupportedProvider, string> = {
"openai-native": "OPENAI_API_KEY",
gemini: "GOOGLE_API_KEY",
openrouter: "OPENROUTER_API_KEY",
novita: "NOVITA_API_KEY",
"vercel-ai-gateway": "VERCEL_AI_GATEWAY_API_KEY",
}

Expand Down Expand Up @@ -43,6 +44,10 @@ export function getProviderSettings(
if (apiKey) config.openRouterApiKey = apiKey
if (model) config.openRouterModelId = model
break
case "novita":
if (apiKey) config.novitaApiKey = apiKey
if (model) config.apiModelId = model
break
case "vercel-ai-gateway":
if (apiKey) config.vercelAiGatewayApiKey = apiKey
if (model) config.vercelAiGatewayModelId = model
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const supportedProviders = [
"openai-native",
"gemini",
"openrouter",
"novita",
"vercel-ai-gateway",
] as const satisfies ProviderName[]

Expand Down
19 changes: 19 additions & 0 deletions apps/vscode-e2e/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,25 @@ After converting the generated `openai-*.json` files into stable named fixtures,
USE_MOCK=true TEST_FILE=deepseek-v4.test pnpm --filter @roo-code/vscode-e2e test:run
```

### Novita AI (`suite/providers/novita.test.ts`)

Novita exposes an OpenAI-compatible endpoint, so the suite redirects the provider through aimock with
`novitaBaseUrl: ${AIMOCK_URL}/v1`. The default model is `moonshotai/kimi-k2.7-code`; override it with
`NOVITA_MODEL_ID` only when refreshing matching fixtures.

Record Novita fixtures with the targeted file filter so aimock proxies OpenAI-compatible traffic to
`https://api.novita.ai/openai`:

```sh
NOVITA_API_KEY=<key> TEST_FILE=novita.test pnpm --filter @roo-code/vscode-e2e test:record
```

After converting generated `openai-*.json` files into `fixtures/novita.json`, verify in mock mode:

```sh
USE_MOCK=true TEST_FILE=novita.test pnpm --filter @roo-code/vscode-e2e test:run
```

## Tests that use a non-default provider

If your test calls `api.setConfiguration({ apiProvider: "anthropic", ... })`, point aimock at the
Expand Down
51 changes: 51 additions & 0 deletions apps/vscode-e2e/fixtures/novita.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"fixtures": [
{
"match": {
"model": "moonshotai/kimi-k2.7-code",
"userMessage": "novita-e2e:tool-use",
"sequenceIndex": 0
},
"response": {
"toolCalls": [
{
"name": "read_file",
"arguments": "{\"path\":\"novita-e2e-marker.txt\"}",
"id": "call_novita_read"
}
]
}
},
{
"match": {
"model": "moonshotai/kimi-k2.7-code",
"toolCallId": "call_novita_read"
},
"response": {
"toolCalls": [
{
"name": "attempt_completion",
"arguments": "{\"result\":\"NOVITA_E2E_MARKER\"}",
"id": "call_novita_done"
}
]
}
},
{
"match": {
"model": "moonshotai/kimi-k2.7-code",
"userMessage": "[ERROR] You did not use a tool in your previous response!",
"sequenceIndex": 0
},
"response": {
"toolCalls": [
{
"name": "attempt_completion",
"arguments": "{\"result\":\"NOVITA_E2E_MARKER\"}",
"id": "call_novita_retry_done"
}
]
}
}
]
}
29 changes: 24 additions & 5 deletions apps/vscode-e2e/src/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ function isDeepSeekTargetedRun(testFile?: string, testGrep?: string) {
return testGrep?.toLowerCase().includes("deepseek") ?? false
}

function isNovitaTargetedRun(testFile?: string, testGrep?: string) {
if (testFile?.toLowerCase().includes("novita.test")) {
return true
}

return testGrep?.toLowerCase().includes("novita") ?? false
}

function isBedrockTargetedRun(testFile?: string, testGrep?: string) {
if (testFile?.toLowerCase().includes("bedrock.test")) {
return true
Expand All @@ -42,28 +50,35 @@ async function main() {
const testGrep = getCliFlagValue("--grep") || process.env.TEST_GREP
const testFile = getCliFlagValue("--file") || process.env.TEST_FILE
const isDeepSeekTest = isDeepSeekTargetedRun(testFile, testGrep)
const isNovitaTest = isNovitaTargetedRun(testFile, testGrep)
const isGeminiTest = testFile?.toLowerCase().includes("gemini.test") ?? false
const isBedrockTest = isBedrockTargetedRun(testFile, testGrep)

if (isRecord && isDeepSeekTest && !process.env.DEEPSEEK_API_KEY) {
throw new Error("AIMOCK_RECORD=true requires DEEPSEEK_API_KEY to record DeepSeek fixtures")
}

if (isRecord && isNovitaTest && !process.env.NOVITA_API_KEY) {
throw new Error("AIMOCK_RECORD=true requires NOVITA_API_KEY to record Novita fixtures")
}

if (isRecord && isGeminiTest && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
throw new Error("AIMOCK_RECORD=true requires GEMINI_API_KEY to record Gemini fixtures")
}

if (isRecord && !isDeepSeekTest && !isGeminiTest && !process.env.OPENROUTER_API_KEY) {
if (isRecord && !isDeepSeekTest && !isNovitaTest && !isGeminiTest && !process.env.OPENROUTER_API_KEY) {
throw new Error("AIMOCK_RECORD=true requires OPENROUTER_API_KEY to record fixtures")
}

// Record mode always needs aimock running (to capture traffic).
// Replay mode starts aimock when no real API key is present or USE_MOCK is forced.
const hasRealApiKey = isDeepSeekTest
? !!process.env.DEEPSEEK_API_KEY
: isBedrockTest
? true // Bedrock test starts its own binary-event-stream mock server when no real token
: !!(process.env.OPENROUTER_API_KEY || process.env.ANTHROPIC_API_KEY)
: isNovitaTest
? !!process.env.NOVITA_API_KEY
: isBedrockTest
? true // Bedrock test starts its own binary-event-stream mock server when no real token
: !!(process.env.OPENROUTER_API_KEY || process.env.ANTHROPIC_API_KEY)
const useMock = isRecord || !hasRealApiKey || process.env.USE_MOCK === "true"

let mock: InstanceType<typeof LLMock> | undefined
Expand Down Expand Up @@ -94,7 +109,11 @@ async function main() {
// Use /api (not /api/v1) — aimock appends the request path (/v1/chat/completions)
// so including /v1 here would produce a doubled /v1/v1 upstream URL.
providers: {
openai: isDeepSeekTest ? "https://api.deepseek.com" : "https://openrouter.ai/api",
openai: isDeepSeekTest
? "https://api.deepseek.com"
: isNovitaTest
? "https://api.novita.ai/openai"
: "https://openrouter.ai/api",
// aimock forwards the x-api-key header from the Anthropic SDK to the real API.
anthropic: "https://api.anthropic.com",
// aimock forwards the x-goog-api-key header from the Google AI SDK.
Expand Down
Loading
Loading