From 5de982a71e252dae52b0468111a5c16db2ad6a40 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:38:13 -0400 Subject: [PATCH 01/35] feat: add NeuronPool provider files (catalog, handler, settings UI, tests) --- .../types/src/provider-settings/neuronpool.ts | 20 +++ packages/types/src/providers/neuronpool.ts | 80 +++++++++++ .../providers/__tests__/neuronpool.spec.ts | 124 ++++++++++++++++++ src/api/providers/neuronpool.ts | 21 +++ .../settings/providers/NeuronPool.tsx | 58 ++++++++ 5 files changed, 303 insertions(+) create mode 100644 packages/types/src/provider-settings/neuronpool.ts create mode 100644 packages/types/src/providers/neuronpool.ts create mode 100644 src/api/providers/__tests__/neuronpool.spec.ts create mode 100644 src/api/providers/neuronpool.ts create mode 100644 webview-ui/src/components/settings/providers/NeuronPool.tsx diff --git a/packages/types/src/provider-settings/neuronpool.ts b/packages/types/src/provider-settings/neuronpool.ts new file mode 100644 index 0000000000..431f409218 --- /dev/null +++ b/packages/types/src/provider-settings/neuronpool.ts @@ -0,0 +1,20 @@ +import { z } from "zod" + +import { providerIdentifiers } from "../provider-identifiers.js" +import { + API_MODEL_ID_FIELD, + apiModelIdProviderModelShape, + createModelIdAccessor, + createProviderDefinition, +} from "./common.js" + +export const neuronpoolProviderDefinition = createProviderDefinition({ + apiProvider: providerIdentifiers.neuronpool, + modelIdKey: API_MODEL_ID_FIELD, + getModelId: createModelIdAccessor(API_MODEL_ID_FIELD), + schema: { + ...apiModelIdProviderModelShape, + neuronpoolApiKey: z.string().optional(), + neuronpoolBaseUrl: z.string().optional(), + }, +}) diff --git a/packages/types/src/providers/neuronpool.ts b/packages/types/src/providers/neuronpool.ts new file mode 100644 index 0000000000..360b9baff1 --- /dev/null +++ b/packages/types/src/providers/neuronpool.ts @@ -0,0 +1,80 @@ +import type { ModelInfo } from "../model.js" + +// NeuronPool — OpenAI-compatible social compute / public marketplace. +// Live catalog: GET {baseURL}/models (default https://neuronpool.damnknee.workers.dev/v1) +export type NeuronPoolModelId = + | "gpt-oss-20b" + | "llama-3.2-1b-instruct" + | "llama-3.1-8b-instruct" + | "qwen2.5-7b-instruct" + | "gemma-3-12b-it" + | "qwen3-30b-a3b" + | "neuronpool-tiny-chat" + +export const neuronpoolDefaultModelId: NeuronPoolModelId = "gpt-oss-20b" + +const chatDefaults = { + supportsImages: false, + supportsPromptCache: false, + maxTokens: 4096, +} as const + +export const neuronpoolDefaultModelInfo: ModelInfo = { + ...chatDefaults, + contextWindow: 131_072, + inputPrice: 0.015, + outputPrice: 0.07, + description: "gpt-oss-20b via NeuronPool (OpenAI-compatible chat + tools).", +} + +export const neuronpoolModels = { + "gpt-oss-20b": { + ...chatDefaults, + contextWindow: 131_072, + inputPrice: 0.015, + outputPrice: 0.07, + description: "gpt-oss-20b via NeuronPool.", + }, + "llama-3.2-1b-instruct": { + ...chatDefaults, + contextWindow: 131_072, + inputPrice: 0.005, + outputPrice: 0.01, + description: "Llama 3.2 1B Instruct via NeuronPool.", + }, + "llama-3.1-8b-instruct": { + ...chatDefaults, + contextWindow: 131_072, + inputPrice: 0.02, + outputPrice: 0.03, + description: "Llama 3.1 8B Instruct via NeuronPool.", + }, + "qwen2.5-7b-instruct": { + ...chatDefaults, + contextWindow: 32_768, + inputPrice: 0.02, + outputPrice: 0.03, + description: "Qwen2.5 7B Instruct via NeuronPool.", + }, + "gemma-3-12b-it": { + ...chatDefaults, + contextWindow: 131_072, + inputPrice: 0.04, + outputPrice: 0.06, + description: "Gemma 3 12B IT via NeuronPool.", + }, + "qwen3-30b-a3b": { + ...chatDefaults, + contextWindow: 32_768, + inputPrice: 0.06, + outputPrice: 0.09, + description: "Qwen3 30B A3B via NeuronPool.", + }, + "neuronpool-tiny-chat": { + ...chatDefaults, + contextWindow: 4096, + inputPrice: 0.001, + outputPrice: 0.002, + description: "In-house tiny chat model for smoke tests.", + }, +} as const satisfies Record diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts new file mode 100644 index 0000000000..36c31d8240 --- /dev/null +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -0,0 +1,124 @@ +// npx vitest run src/api/providers/__tests__/neuronpool.spec.ts + +import OpenAI from "openai" +import { Anthropic } from "@anthropic-ai/sdk" + +import { type NeuronPoolModelId, neuronpoolDefaultModelId, neuronpoolModels } from "@roo-code/types" + +import { NeuronPoolHandler, NEURONPOOL_DEFAULT_BASE_URL } from "../neuronpool" + +const mockCreate = vi.fn() + +vi.mock("openai", () => ({ + default: vi.fn(function () { + return { + chat: { + completions: { + create: mockCreate, + }, + }, + } + }), +})) + +describe("NeuronPoolHandler", () => { + let handler: NeuronPoolHandler + + beforeEach(() => { + vi.clearAllMocks() + handler = new NeuronPoolHandler({ neuronpoolApiKey: "test-neuronpool-api-key" }) + }) + + it("should use the live Worker base URL by default", () => { + new NeuronPoolHandler({ neuronpoolApiKey: "test-neuronpool-api-key" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: NEURONPOOL_DEFAULT_BASE_URL })) + }) + + it("should honor a custom base URL", () => { + new NeuronPoolHandler({ + neuronpoolApiKey: "test-neuronpool-api-key", + neuronpoolBaseUrl: "https://example.test/v1/", + }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://example.test/v1" })) + }) + + it("should use the provided API key", () => { + const neuronpoolApiKey = "test-neuronpool-api-key" + new NeuronPoolHandler({ neuronpoolApiKey }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: neuronpoolApiKey })) + }) + + it("should return default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(neuronpoolDefaultModelId) + expect(model.info).toEqual(neuronpoolModels[neuronpoolDefaultModelId]) + }) + + it("should return specified model when valid model is provided", () => { + const testModelId: NeuronPoolModelId = "llama-3.2-1b-instruct" + const handlerWithModel = new NeuronPoolHandler({ + apiModelId: testModelId, + neuronpoolApiKey: "test-neuronpool-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(neuronpoolModels[testModelId]) + }) + + it("completePrompt method should return text from NeuronPool API", async () => { + const expectedResponse = "pong" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("ping") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "NeuronPool API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow() + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "pong" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("should pass stream + tools through to the OpenAI-compatible API", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi.fn().mockResolvedValueOnce({ done: true }), + }), + } + }) + + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] + const stream = handler.createMessage("You are a helpful assistant", messages) + for await (const _chunk of stream) { + // drain + } + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.model).toBe(neuronpoolDefaultModelId) + expect(callArgs.stream).toBe(true) + }) +}) diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts new file mode 100644 index 0000000000..133b7bae9c --- /dev/null +++ b/src/api/providers/neuronpool.ts @@ -0,0 +1,21 @@ +import { type NeuronPoolModelId, neuronpoolDefaultModelId, neuronpoolModels } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" + +export const NEURONPOOL_DEFAULT_BASE_URL = "https://neuronpool.damnknee.workers.dev/v1" + +export class NeuronPoolHandler extends BaseOpenAiCompatibleProvider { + constructor(options: ApiHandlerOptions) { + super({ + ...options, + providerName: "NeuronPool", + baseURL: (options.neuronpoolBaseUrl || NEURONPOOL_DEFAULT_BASE_URL).replace(/\/+$/, ""), + apiKey: options.neuronpoolApiKey ?? options.apiKey, + defaultProviderModelId: neuronpoolDefaultModelId, + providerModels: neuronpoolModels, + defaultTemperature: 0, + }) + } +} diff --git a/webview-ui/src/components/settings/providers/NeuronPool.tsx b/webview-ui/src/components/settings/providers/NeuronPool.tsx new file mode 100644 index 0000000000..498feb4761 --- /dev/null +++ b/webview-ui/src/components/settings/providers/NeuronPool.tsx @@ -0,0 +1,58 @@ +import { useCallback } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import type { ProviderSettings } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { inputEventTransform } from "../transforms" + +type NeuronPoolProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void +} + +export const NeuronPool = ({ apiConfiguration, setApiConfigurationField }: NeuronPoolProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ + + + {!apiConfiguration?.neuronpoolApiKey && ( + + Get NeuronPool API Key + + )} + + ) +} From 071067480e8526fcbbc1c31b67c62146f9b674a8 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:40:16 -0400 Subject: [PATCH 02/35] feat: register NeuronPool in types (identifier, catalog export, settings schema, secret key) --- packages/types/src/provider-identifiers.ts | 1 + packages/types/src/provider-settings/index.ts | 2 ++ packages/types/src/providers/index.ts | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/packages/types/src/provider-identifiers.ts b/packages/types/src/provider-identifiers.ts index fdf507bb6b..c501c08cfb 100644 --- a/packages/types/src/provider-identifiers.ts +++ b/packages/types/src/provider-identifiers.ts @@ -25,6 +25,7 @@ export const providerIdentifiers = { baseten: "baseten", fireworks: "fireworks", friendli: "friendli", + neuronpool: "neuronpool", gemini: "gemini", geminiCli: "gemini-cli", mistral: "mistral", diff --git a/packages/types/src/provider-settings/index.ts b/packages/types/src/provider-settings/index.ts index 3fe8790af1..26ba1e6d5d 100644 --- a/packages/types/src/provider-settings/index.ts +++ b/packages/types/src/provider-settings/index.ts @@ -26,6 +26,7 @@ import { sambaNovaProviderDefinition } from "./sambanova.js" import { zaiProviderDefinition } from "./zai.js" import { fireworksProviderDefinition } from "./fireworks.js" import { friendliProviderDefinition } from "./friendli.js" +import { neuronpoolProviderDefinition } from "./neuronpool.js" import { qwenCodeProviderDefinition } from "./qwen-code.js" import { vercelAiGatewayProviderDefinition } from "./vercel-ai-gateway.js" import { opencodeGoProviderDefinition } from "./opencode-go.js" @@ -77,6 +78,7 @@ export const providerDefinitionList = [ zaiProviderDefinition, fireworksProviderDefinition, friendliProviderDefinition, + neuronpoolProviderDefinition, qwenCodeProviderDefinition, vercelAiGatewayProviderDefinition, opencodeGoProviderDefinition, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index b26476eaed..6d5c2ba983 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -31,6 +31,7 @@ export * from "./zai.js" export * from "./minimax.js" export * from "./mimo.js" export * from "./zoo-gateway.js" +export * from "./neuronpool.js" import { anthropicDefaultModelId } from "./anthropic.js" import { basetenDefaultModelId } from "./baseten.js" @@ -61,6 +62,7 @@ import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai import { minimaxDefaultModelId } from "./minimax.js" import { mimoDefaultModelId } from "./mimo.js" import { zooGatewayDefaultModelId } from "./zoo-gateway.js" +import { neuronpoolDefaultModelId } from "./neuronpool.js" // Import the ProviderName type from provider-settings to avoid duplication import type { ProviderName } from "../provider-settings.js" @@ -123,6 +125,8 @@ export function getProviderDefaultModelId( return fireworksDefaultModelId case providerIdentifiers.friendli: return friendliDefaultModelId + case providerIdentifiers.neuronpool: + return neuronpoolDefaultModelId case providerIdentifiers.qwenCode: return qwenCodeDefaultModelId case providerIdentifiers.poe: From 96955571a85dc8af7f34935e3e136017a8702990 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:41:22 -0400 Subject: [PATCH 03/35] feat: store neuronpoolApiKey in SECRET_STATE_KEYS --- packages/types/src/global-settings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 95f246dbe7..61a639584a 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -335,6 +335,7 @@ export const SECRET_STATE_KEYS = [ "zaiApiKey", "fireworksApiKey", "friendliApiKey", + "neuronpoolApiKey", "vercelAiGatewayApiKey", "opencodeGoApiKey", "kenariApiKey", From 5d898e2a6bda3b6aa6e3e33b9db38befe66fc13a Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:42:47 -0400 Subject: [PATCH 04/35] feat: wire NeuronPool into factory, ProfileValidator, and settings lists --- src/api/providers/index.ts | 1 + src/shared/ProfileValidator.ts | 1 + webview-ui/src/components/settings/constants.ts | 3 +++ webview-ui/src/components/settings/providers/index.ts | 1 + 4 files changed, 6 insertions(+) diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 8ba1eae382..0105e81a54 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -27,6 +27,7 @@ export { XAIHandler } from "./xai" export { ZAiHandler } from "./zai" export { FireworksHandler } from "./fireworks" export { FriendliHandler } from "./friendli" +export { NeuronPoolHandler } from "./neuronpool" export { VercelAiGatewayHandler } from "./vercel-ai-gateway" export { OpencodeGoHandler } from "./opencode-go" export { KenariHandler } from "./kenari" diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 923582bd06..e5223bb460 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -64,6 +64,7 @@ export class ProfileValidator { case providerIdentifiers.sambanova: case providerIdentifiers.fireworks: case providerIdentifiers.friendli: + case providerIdentifiers.neuronpool: return profile.apiModelId case providerIdentifiers.litellm: return profile.litellmModelId diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 8c51aca2fe..f92de51b9e 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -17,6 +17,7 @@ import { internationalZAiModels, fireworksModels, friendliModels, + neuronpoolModels, minimaxModels, basetenModels, mimoModels, @@ -38,6 +39,7 @@ export const MODELS_BY_PROVIDER: Partial { value: providerIdentifiers.zai, label: "Z.ai", proxy: false }, { value: providerIdentifiers.fireworks, label: "Fireworks AI", proxy: false }, { value: providerIdentifiers.friendli, label: "Friendli", proxy: false }, + { value: providerIdentifiers.neuronpool, label: "NeuronPool", proxy: false }, { value: providerIdentifiers.vercelAiGateway, label: "Vercel AI Gateway", proxy: false }, { value: providerIdentifiers.opencodeGo, label: "Opencode Go", proxy: false }, { value: providerIdentifiers.kenari, label: "Kenari", proxy: false }, diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 12ea4cd786..4fbc15695d 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -23,6 +23,7 @@ export { ZAi } from "./ZAi" export { LiteLLM } from "./LiteLLM" export { Fireworks } from "./Fireworks" export { Friendli } from "./Friendli" +export { NeuronPool } from "./NeuronPool" export { VercelAiGateway } from "./VercelAiGateway" export { OpenCodeGo } from "./OpenCodeGo" export { Kenari } from "./Kenari" From 45f3ba5c5f6944781db5b6a73f059c73c6bc81d3 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:43:54 -0400 Subject: [PATCH 05/35] feat: add NeuronPool factory case in buildApiHandler --- src/api/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/index.ts b/src/api/index.ts index f45412e32c..29f8ed1ab0 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -39,6 +39,7 @@ import { ZAiHandler, FireworksHandler, FriendliHandler, + NeuronPoolHandler, VercelAiGatewayHandler, OpencodeGoHandler, KenariHandler, @@ -219,6 +220,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new FireworksHandler(options) case providerIdentifiers.friendli: return new FriendliHandler(options) + case providerIdentifiers.neuronpool: + return new NeuronPoolHandler(options) case providerIdentifiers.vercelAiGateway: return new VercelAiGatewayHandler(options) case providerIdentifiers.opencodeGo: From e09edcdc5296a483f93551a8257736f4716bd3e7 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:48:02 -0400 Subject: [PATCH 06/35] feat: wire NeuronPool into settings validation and model config --- .../src/components/settings/utils/providerModelConfig.ts | 4 ++++ webview-ui/src/utils/validate.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index 7230e4cbe4..98d4a3ae55 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -19,6 +19,7 @@ import { mainlandZAiDefaultModelId, fireworksDefaultModelId, friendliDefaultModelId, + neuronpoolDefaultModelId, minimaxDefaultModelId, basetenDefaultModelId, mimoDefaultModelId, @@ -61,6 +62,7 @@ export const PROVIDER_SERVICE_CONFIG: Partial> = [providerIdentifiers.zai]: internationalZAiDefaultModelId, [providerIdentifiers.fireworks]: fireworksDefaultModelId, [providerIdentifiers.friendli]: friendliDefaultModelId, + [providerIdentifiers.neuronpool]: neuronpoolDefaultModelId, [providerIdentifiers.minimax]: minimaxDefaultModelId, [providerIdentifiers.mimo]: mimoDefaultModelId, [providerIdentifiers.baseten]: basetenDefaultModelId, @@ -138,6 +141,7 @@ const PROVIDER_MODEL_CONFIG: Partial> [providerIdentifiers.zai]: { field: "apiModelId" }, [providerIdentifiers.fireworks]: { field: "apiModelId", default: fireworksDefaultModelId }, [providerIdentifiers.friendli]: { field: "apiModelId", default: friendliDefaultModelId }, + [providerIdentifiers.neuronpool]: { field: "apiModelId", default: neuronpoolDefaultModelId }, [providerIdentifiers.poe]: { field: "apiModelId", default: poeDefaultModelId }, [providerIdentifiers.vercelAiGateway]: { field: "vercelAiGatewayModelId", diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 3e32116374..b74c4d3caa 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -157,6 +157,11 @@ function validateModelsAndKeysProvided( return i18next.t("settings:validation.apiKey") } break + case providerIdentifiers.neuronpool: + if (!apiConfiguration.neuronpoolApiKey) { + return i18next.t("settings:validation.apiKey") + } + break case providerIdentifiers.qwenCode: if (!apiConfiguration.qwenCodeOauthPath) { return i18next.t("settings:validation.qwenCodeOauthPath") From f0be9bb7580bc39a2fec63409142690e511b491e Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:49:10 -0400 Subject: [PATCH 07/35] feat: register NeuronPool in MODELS_BY_PROVIDER catalog map --- packages/types/src/provider-settings.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 0b898f1b66..3fd75657ab 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -30,6 +30,7 @@ import { deepSeekModels, fireworksModels, friendliModels, + neuronpoolModels, geminiModels, mistralModels, moonshotModels, @@ -430,6 +431,11 @@ export const MODELS_BY_PROVIDER: Record< label: "Friendli", models: Object.keys(friendliModels), }, + [providerIdentifiers.neuronpool]: { + id: providerIdentifiers.neuronpool, + label: "NeuronPool", + models: Object.keys(neuronpoolModels), + }, [providerIdentifiers.gemini]: { id: providerIdentifiers.gemini, label: "Google Gemini", From 5f7abf1cf2b272847fb746b4be447c77ee43b388 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:50:39 -0400 Subject: [PATCH 08/35] feat: add NeuronPool case to useSelectedModel static catalog switch --- webview-ui/src/components/ui/hooks/useSelectedModel.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index b7ad9e87e5..29204cc165 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -24,6 +24,7 @@ import { zaiApiLineConfigs, fireworksModels, friendliModels, + neuronpoolModels, basetenModels, qwenCodeModels, kimiCodeDefaultModelInfo, @@ -389,6 +390,11 @@ function getSelectedModel({ const info = friendliModels[id as keyof typeof friendliModels] return { id, info } } + case providerIdentifiers.neuronpool: { + const id = apiConfiguration.apiModelId ?? defaultModelId + const info = neuronpoolModels[id as keyof typeof neuronpoolModels] + return { id, info } + } case providerIdentifiers.poe: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = routerModels[providerIdentifiers.poe]?.[id] From 7bb07b78e93e4cf27de79a465c1a20849bd10fbd Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 08:53:16 -0400 Subject: [PATCH 09/35] feat: render NeuronPool settings panel in ApiOptions --- webview-ui/src/components/settings/ApiOptions.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 3e1495baff..f0a9b771d3 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -78,6 +78,7 @@ import { ZAi, Fireworks, Friendli, + NeuronPool, VercelAiGateway, OpenCodeGo, Kenari, @@ -718,6 +719,13 @@ const ApiOptions = ({ /> )} + {selectedProvider === providerIdentifiers.neuronpool && ( + + )} + {selectedProvider === providerIdentifiers.poe && ( Date: Fri, 4 Sep 2026 09:37:38 -0400 Subject: [PATCH 10/35] fix: include NeuronPool in exhaustive provider maps so compile passes --- apps/cli/src/lib/utils/context-window.ts | 1 + packages/types/src/__tests__/provider-identifiers.test.ts | 1 + src/api/__tests__/index.spec.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/apps/cli/src/lib/utils/context-window.ts b/apps/cli/src/lib/utils/context-window.ts index b78ab0e5b5..24ab51fd1d 100644 --- a/apps/cli/src/lib/utils/context-window.ts +++ b/apps/cli/src/lib/utils/context-window.ts @@ -66,6 +66,7 @@ function getModelIdForProvider(config: ProviderSettings): string | undefined { case providerIdentifiers.deepseek: case providerIdentifiers.fireworks: case providerIdentifiers.friendli: + case providerIdentifiers.neuronpool: case providerIdentifiers.gemini: case providerIdentifiers.geminiCli: case providerIdentifiers.mistral: diff --git a/packages/types/src/__tests__/provider-identifiers.test.ts b/packages/types/src/__tests__/provider-identifiers.test.ts index 29e55b31cf..82d25b45c8 100644 --- a/packages/types/src/__tests__/provider-identifiers.test.ts +++ b/packages/types/src/__tests__/provider-identifiers.test.ts @@ -43,6 +43,7 @@ const expectedProviderIdentifiers = [ "baseten", "fireworks", "friendli", + "neuronpool", "gemini", "gemini-cli", "mistral", diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index 2fe940f53a..e41d603448 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -48,6 +48,7 @@ import { MistralHandler, MoonshotHandler, NanoGptHandler, + NeuronPoolHandler, OpenAiCodexHandler, OpenAiHandler, OpenAiNativeHandler, @@ -97,6 +98,7 @@ const expectedHandlers = { [providerIdentifiers.zai]: ZAiHandler, [providerIdentifiers.fireworks]: FireworksHandler, [providerIdentifiers.friendli]: FriendliHandler, + [providerIdentifiers.neuronpool]: NeuronPoolHandler, [providerIdentifiers.vercelAiGateway]: VercelAiGatewayHandler, [providerIdentifiers.opencodeGo]: OpencodeGoHandler, [providerIdentifiers.kenari]: KenariHandler, From 4d25c4170df573df0f5720c629517b40fbdfbd17 Mon Sep 17 00:00:00 2001 From: dannymota Date: Fri, 4 Sep 2026 13:40:13 +0000 Subject: [PATCH 11/35] test: cover NeuronPool provider settings and validation --- src/shared/__tests__/ProfileValidator.spec.ts | 2 + .../ApiOptions.interactions.spec.tsx | 2 + .../settings/__tests__/ApiOptions.spec.tsx | 18 ++++ .../providers/__tests__/NeuronPool.spec.tsx | 87 +++++++++++++++++++ .../hooks/__tests__/useSelectedModel.spec.ts | 49 +++++++++++ .../src/utils/__tests__/validate.spec.ts | 23 +++++ 6 files changed, 181 insertions(+) create mode 100644 webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx diff --git a/src/shared/__tests__/ProfileValidator.spec.ts b/src/shared/__tests__/ProfileValidator.spec.ts index 865fa5cf51..118425f9e9 100644 --- a/src/shared/__tests__/ProfileValidator.spec.ts +++ b/src/shared/__tests__/ProfileValidator.spec.ts @@ -19,6 +19,7 @@ describe("ProfileValidator", () => { ["sambanova", { apiModelId: "model" }], ["fireworks", { apiModelId: "model" }], ["friendli", { apiModelId: "model" }], + ["neuronpool", { apiModelId: "model" }], ["litellm", { litellmModelId: "model" }], ["lmstudio", { lmStudioModelId: "model" }], ["vscodeLm", { vsCodeLmModelSelector: { id: "model" } }], @@ -246,6 +247,7 @@ describe("ProfileValidator", () => { providerIdentifiers.sambanova, providerIdentifiers.fireworks, providerIdentifiers.friendli, + providerIdentifiers.neuronpool, ] apiModelProviders.forEach((provider) => { diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.interactions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.interactions.spec.tsx index 60057ded92..1ad474ca9b 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.interactions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.interactions.spec.tsx @@ -101,6 +101,7 @@ vi.mock("../providers", () => { ZAi: provider("provider-zai"), Fireworks: provider("provider-fireworks"), Friendli: provider("provider-friendli"), + NeuronPool: provider("provider-neuronpool"), VercelAiGateway: provider("provider-vercel-ai-gateway"), OpenCodeGo: provider("provider-opencode-go"), Kenari: provider("provider-kenari"), @@ -355,6 +356,7 @@ describe("ApiOptions interactions", () => { providerIdentifiers.zooGateway, providerIdentifiers.fireworks, providerIdentifiers.friendli, + providerIdentifiers.neuronpool, providerIdentifiers.poe, ])("renders the %s provider branch when selected", (apiProvider) => { renderApiOptions({ apiConfiguration: { apiProvider } }) diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index b0f1b6f9ef..c093968d85 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -251,6 +251,12 @@ vi.mock("../providers/Friendli", () => ({ ), })) +vi.mock("../providers/NeuronPool", () => ({ + NeuronPool: ({ apiConfiguration }: any) => ( +
+ ), +})) + vi.mock("@src/components/ui/hooks/useSelectedModel", () => ({ useSelectedModel: vi.fn((apiConfiguration: ProviderSettings) => { if (apiConfiguration.apiModelId?.includes("thinking")) { @@ -350,6 +356,18 @@ describe("ApiOptions", () => { expect(screen.getByTestId("friendli-provider")).toHaveAttribute("data-key", "k") }) + it("renders the NeuronPool provider form when neuronpool is selected", () => { + renderApiOptions({ + apiConfiguration: { + apiProvider: providerIdentifiers.neuronpool, + neuronpoolApiKey: "k", + }, + }) + + expect(screen.getByTestId("neuronpool-provider")).toBeInTheDocument() + expect(screen.getByTestId("neuronpool-provider")).toHaveAttribute("data-key", "k") + }) + it("shows temperature and rate limit controls by default", () => { renderApiOptions({ apiConfiguration: {}, diff --git a/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx new file mode 100644 index 0000000000..b430262e41 --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx @@ -0,0 +1,87 @@ +import { providerIdentifiers } from "@roo-code/types" +import React from "react" +import { render, screen, fireEvent } from "@/utils/test-utils" +import type { ProviderSettings } from "@roo-code/types" + +import { NeuronPool } from "../NeuronPool" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeTextField: ({ children, value, onInput, type }: any) => { + const testId = type === "password" ? "neuronpool-api-key" : "neuronpool-base-url" + return ( +
+ {children} + +
+ ) + }, +})) + +vi.mock("@src/components/common/VSCodeButtonLink", () => ({ + VSCodeButtonLink: ({ href, children }: any) => ( + + {children} + + ), +})) + +describe("NeuronPool provider settings", () => { + it("renders the 'Get NeuronPool API Key' link when no key is set", () => { + render( + , + ) + expect(screen.getByTestId("neuronpool-get-key-link")).toHaveAttribute( + "href", + "https://neuronpool.damnknee.workers.dev/dashboard", + ) + }) + + it("hides the 'Get NeuronPool API Key' link once a key is set", () => { + render( + , + ) + expect(screen.queryByTestId("neuronpool-get-key-link")).not.toBeInTheDocument() + expect(screen.getByTestId("neuronpool-api-key-field")).toBeInTheDocument() + expect(screen.getByTestId("neuronpool-base-url-field")).toBeInTheDocument() + }) + + it("calls setApiConfigurationField with the API key when the input changes", () => { + const mockSetApiConfigurationField = vi.fn() + render( + , + ) + fireEvent.input(screen.getByTestId("neuronpool-api-key-input"), { target: { value: "new-key" } }) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("neuronpoolApiKey", "new-key") + }) + + it("calls setApiConfigurationField with the base URL when the input changes", () => { + const mockSetApiConfigurationField = vi.fn() + render( + , + ) + fireEvent.input(screen.getByTestId("neuronpool-base-url-input"), { + target: { value: "http://127.0.0.1:8787/v1" }, + }) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("neuronpoolBaseUrl", "http://127.0.0.1:8787/v1") + }) +}) diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 33a9df16da..7f71859c9b 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -22,6 +22,8 @@ import { minimaxModels, friendliDefaultModelId, friendliModels, + neuronpoolDefaultModelId, + neuronpoolModels, deepSeekDefaultModelId, deepSeekModels, openRouterDefaultModelId, @@ -1327,6 +1329,53 @@ describe("useSelectedModel", () => { }) }) + describe("neuronpool provider", () => { + beforeEach(() => { + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) + }) + + it("should return default NeuronPool model when no custom model is specified", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.neuronpool, + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe(providerIdentifiers.neuronpool) + expect(result.current.id).toBe(neuronpoolDefaultModelId) + expect(result.current.info).toEqual(neuronpoolModels[neuronpoolDefaultModelId]) + }) + + it("should use custom model ID and info when model exists in neuronpoolModels", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.neuronpool, + apiModelId: "llama-3.2-1b-instruct", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe(providerIdentifiers.neuronpool) + expect(result.current.id).toBe("llama-3.2-1b-instruct") + expect(result.current.info).toEqual(neuronpoolModels["llama-3.2-1b-instruct"]) + }) + }) + describe("Z AI provider", () => { it("uses the International Coding catalog when no API line is configured", () => { const apiConfiguration: ProviderSettings = { diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index a1936b89d7..064662ed33 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -470,6 +470,29 @@ describe("Model Validation Functions", () => { }) }) + describe("NeuronPool validation", () => { + it("returns an apiKey error when the NeuronPool API key is missing", () => { + const config: ProviderSettings = { + apiProvider: providerIdentifiers.neuronpool, + apiModelId: "gpt-oss-20b", + } + + const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + expect(result).toBe("settings:validation.apiKey") + }) + + it("returns undefined for a valid NeuronPool configuration", () => { + const config: ProviderSettings = { + apiProvider: providerIdentifiers.neuronpool, + neuronpoolApiKey: "valid-key", + apiModelId: "gpt-oss-20b", + } + + const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + expect(result).toBeUndefined() + }) + }) + describe("Zoo Gateway validation", () => { describe("validateApiConfiguration (welcome-view entry point)", () => { it("returns a sign-in error when neither profile token nor Zoo auth is present", () => { From 302eaff156bd4dc73ae9cb28cee9de65bdaa6144 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 09:56:39 -0400 Subject: [PATCH 12/35] fix: strip NeuronPool base URL slashes without polynomial regex CodeQL flagged /\/+$/ on the user-supplied neuronpoolBaseUrl as a polynomial regex on uncontrolled data. Walk trailing slashes instead. --- src/api/providers/__tests__/neuronpool.spec.ts | 13 ++++++++++++- src/api/providers/neuronpool.ts | 11 ++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts index 36c31d8240..30c78383eb 100644 --- a/src/api/providers/__tests__/neuronpool.spec.ts +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -5,7 +5,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { type NeuronPoolModelId, neuronpoolDefaultModelId, neuronpoolModels } from "@roo-code/types" -import { NeuronPoolHandler, NEURONPOOL_DEFAULT_BASE_URL } from "../neuronpool" +import { NeuronPoolHandler, NEURONPOOL_DEFAULT_BASE_URL, stripTrailingSlashes } from "../neuronpool" const mockCreate = vi.fn() @@ -21,6 +21,17 @@ vi.mock("openai", () => ({ }), })) +describe("stripTrailingSlashes", () => { + it("leaves a URL without trailing slashes unchanged", () => { + expect(stripTrailingSlashes(NEURONPOOL_DEFAULT_BASE_URL)).toBe(NEURONPOOL_DEFAULT_BASE_URL) + }) + + it("strips one or many trailing slashes without regex backtracking", () => { + expect(stripTrailingSlashes("https://example.test/v1/")).toBe("https://example.test/v1") + expect(stripTrailingSlashes("https://example.test/v1" + "/".repeat(64))).toBe("https://example.test/v1") + }) +}) + describe("NeuronPoolHandler", () => { let handler: NeuronPoolHandler diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts index 133b7bae9c..ceef697470 100644 --- a/src/api/providers/neuronpool.ts +++ b/src/api/providers/neuronpool.ts @@ -6,12 +6,21 @@ import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" export const NEURONPOOL_DEFAULT_BASE_URL = "https://neuronpool.damnknee.workers.dev/v1" +/** Trim trailing slashes without a `/+` regex (CodeQL js/polynomial-redos). */ +export function stripTrailingSlashes(url: string): string { + let end = url.length + while (end > 0 && url.charCodeAt(end - 1) === 47) { + end -= 1 + } + return url.slice(0, end) +} + export class NeuronPoolHandler extends BaseOpenAiCompatibleProvider { constructor(options: ApiHandlerOptions) { super({ ...options, providerName: "NeuronPool", - baseURL: (options.neuronpoolBaseUrl || NEURONPOOL_DEFAULT_BASE_URL).replace(/\/+$/, ""), + baseURL: stripTrailingSlashes(options.neuronpoolBaseUrl || NEURONPOOL_DEFAULT_BASE_URL), apiKey: options.neuronpoolApiKey ?? options.apiKey, defaultProviderModelId: neuronpoolDefaultModelId, providerModels: neuronpoolModels, From 3e3cdd3e92b6815db05ebc77e84375aec11e76a2 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:05:54 -0400 Subject: [PATCH 13/35] test: kill NeuronPool mutation-diff survivors Cover slash-walk edges, apiKey fallback, empty base URL, default temperature 0, unknown-model fallback, and settings-panel display. --- .../providers/__tests__/neuronpool.spec.ts | 39 +++++++++++++++++++ .../providers/__tests__/NeuronPool.spec.tsx | 35 +++++++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts index 30c78383eb..fe05e38535 100644 --- a/src/api/providers/__tests__/neuronpool.spec.ts +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -30,6 +30,17 @@ describe("stripTrailingSlashes", () => { expect(stripTrailingSlashes("https://example.test/v1/")).toBe("https://example.test/v1") expect(stripTrailingSlashes("https://example.test/v1" + "/".repeat(64))).toBe("https://example.test/v1") }) + + it("treats empty and slash-only strings as empty", () => { + expect(stripTrailingSlashes("")).toBe("") + expect(stripTrailingSlashes("/")).toBe("") + expect(stripTrailingSlashes("///")).toBe("") + }) + + it("does not strip trailing characters that are not slashes", () => { + expect(stripTrailingSlashes("https://example.test/v1 ")).toBe("https://example.test/v1 ") + expect(stripTrailingSlashes("https://example.test/v1.")).toBe("https://example.test/v1.") + }) }) describe("NeuronPoolHandler", () => { @@ -45,6 +56,14 @@ describe("NeuronPoolHandler", () => { expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: NEURONPOOL_DEFAULT_BASE_URL })) }) + it("should fall back to the live Worker URL when the custom base URL is empty", () => { + new NeuronPoolHandler({ + neuronpoolApiKey: "test-neuronpool-api-key", + neuronpoolBaseUrl: "", + }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: NEURONPOOL_DEFAULT_BASE_URL })) + }) + it("should honor a custom base URL", () => { new NeuronPoolHandler({ neuronpoolApiKey: "test-neuronpool-api-key", @@ -59,6 +78,15 @@ describe("NeuronPoolHandler", () => { expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: neuronpoolApiKey })) }) + it("should fall back to apiKey when neuronpoolApiKey is omitted", () => { + new NeuronPoolHandler({ apiKey: "fallback-api-key" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "fallback-api-key" })) + }) + + it("should throw when neither neuronpoolApiKey nor apiKey is set", () => { + expect(() => new NeuronPoolHandler({})).toThrow("API key is required") + }) + it("should return default model when no model is specified", () => { const model = handler.getModel() expect(model.id).toBe(neuronpoolDefaultModelId) @@ -76,6 +104,16 @@ describe("NeuronPoolHandler", () => { expect(model.info).toEqual(neuronpoolModels[testModelId]) }) + it("should fall back to the default model when the id is unknown", () => { + const handlerWithUnknown = new NeuronPoolHandler({ + apiModelId: "not-a-neuronpool-model", + neuronpoolApiKey: "test-neuronpool-api-key", + }) + const model = handlerWithUnknown.getModel() + expect(model.id).toBe(neuronpoolDefaultModelId) + expect(model.info).toEqual(neuronpoolModels[neuronpoolDefaultModelId]) + }) + it("completePrompt method should return text from NeuronPool API", async () => { const expectedResponse = "pong" mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) @@ -131,5 +169,6 @@ describe("NeuronPoolHandler", () => { const callArgs = mockCreate.mock.calls[0][0] expect(callArgs.model).toBe(neuronpoolDefaultModelId) expect(callArgs.stream).toBe(true) + expect(callArgs.temperature).toBe(0) }) }) diff --git a/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx index b430262e41..20eecc7b01 100644 --- a/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx @@ -12,12 +12,18 @@ vi.mock("@src/i18n/TranslationContext", () => ({ })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ - VSCodeTextField: ({ children, value, onInput, type }: any) => { + VSCodeTextField: ({ children, value, onInput, type, placeholder }: any) => { const testId = type === "password" ? "neuronpool-api-key" : "neuronpool-base-url" return (
{children} - +
) }, @@ -43,13 +49,34 @@ describe("NeuronPool provider settings", () => { "href", "https://neuronpool.damnknee.workers.dev/dashboard", ) + expect(screen.getByText("settings:providers.apiKeyStorageNotice")).toBeInTheDocument() + expect(screen.getByTestId("neuronpool-base-url-input")).toHaveAttribute( + "placeholder", + "https://neuronpool.damnknee.workers.dev/v1", + ) + }) + + it("shows the get-key link when the stored key is an empty string", () => { + render( + , + ) + expect(screen.getByTestId("neuronpool-get-key-link")).toBeInTheDocument() }) it("hides the 'Get NeuronPool API Key' link once a key is set", () => { render( , @@ -57,6 +84,8 @@ describe("NeuronPool provider settings", () => { expect(screen.queryByTestId("neuronpool-get-key-link")).not.toBeInTheDocument() expect(screen.getByTestId("neuronpool-api-key-field")).toBeInTheDocument() expect(screen.getByTestId("neuronpool-base-url-field")).toBeInTheDocument() + expect(screen.getByTestId("neuronpool-api-key-input")).toHaveValue("stored-key") + expect(screen.getByTestId("neuronpool-base-url-input")).toHaveValue("http://127.0.0.1:8787/v1") }) it("calls setApiConfigurationField with the API key when the input changes", () => { From 7f1f84573e790c0328bfd293a13b1a2622da3820 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:11:30 -0400 Subject: [PATCH 14/35] test: kill remaining NeuronPool mutation-diff survivors --- .../providers/__tests__/neuronpool.spec.ts | 16 ++- src/api/providers/neuronpool.ts | 1 + .../__tests__/ApiOptions.neuronpool.spec.tsx | 126 ++++++++++++++++++ .../__tests__/constants.neuronpool.spec.ts | 21 +++ .../providers/__tests__/NeuronPool.spec.tsx | 39 ++++-- 5 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 webview-ui/src/components/settings/__tests__/ApiOptions.neuronpool.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/constants.neuronpool.spec.ts diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts index fe05e38535..505f9828f9 100644 --- a/src/api/providers/__tests__/neuronpool.spec.ts +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -7,6 +7,8 @@ import { type NeuronPoolModelId, neuronpoolDefaultModelId, neuronpoolModels } fr import { NeuronPoolHandler, NEURONPOOL_DEFAULT_BASE_URL, stripTrailingSlashes } from "../neuronpool" +const LIVE_WORKER_BASE_URL = "https://neuronpool.damnknee.workers.dev/v1" + const mockCreate = vi.fn() vi.mock("openai", () => ({ @@ -23,7 +25,7 @@ vi.mock("openai", () => ({ describe("stripTrailingSlashes", () => { it("leaves a URL without trailing slashes unchanged", () => { - expect(stripTrailingSlashes(NEURONPOOL_DEFAULT_BASE_URL)).toBe(NEURONPOOL_DEFAULT_BASE_URL) + expect(stripTrailingSlashes(LIVE_WORKER_BASE_URL)).toBe(LIVE_WORKER_BASE_URL) }) it("strips one or many trailing slashes without regex backtracking", () => { @@ -51,9 +53,13 @@ describe("NeuronPoolHandler", () => { handler = new NeuronPoolHandler({ neuronpoolApiKey: "test-neuronpool-api-key" }) }) + it("exports the live Worker base URL", () => { + expect(NEURONPOOL_DEFAULT_BASE_URL).toBe(LIVE_WORKER_BASE_URL) + }) + it("should use the live Worker base URL by default", () => { new NeuronPoolHandler({ neuronpoolApiKey: "test-neuronpool-api-key" }) - expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: NEURONPOOL_DEFAULT_BASE_URL })) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: LIVE_WORKER_BASE_URL })) }) it("should fall back to the live Worker URL when the custom base URL is empty", () => { @@ -61,7 +67,7 @@ describe("NeuronPoolHandler", () => { neuronpoolApiKey: "test-neuronpool-api-key", neuronpoolBaseUrl: "", }) - expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: NEURONPOOL_DEFAULT_BASE_URL })) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: LIVE_WORKER_BASE_URL })) }) it("should honor a custom base URL", () => { @@ -87,6 +93,10 @@ describe("NeuronPoolHandler", () => { expect(() => new NeuronPoolHandler({})).toThrow("API key is required") }) + it("sets the provider name to NeuronPool", () => { + expect((handler as unknown as { providerName: string }).providerName).toBe("NeuronPool") + }) + it("should return default model when no model is specified", () => { const model = handler.getModel() expect(model.id).toBe(neuronpoolDefaultModelId) diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts index ceef697470..e6bb6d8286 100644 --- a/src/api/providers/neuronpool.ts +++ b/src/api/providers/neuronpool.ts @@ -9,6 +9,7 @@ export const NEURONPOOL_DEFAULT_BASE_URL = "https://neuronpool.damnknee.workers. /** Trim trailing slashes without a `/+` regex (CodeQL js/polynomial-redos). */ export function stripTrailingSlashes(url: string): string { let end = url.length + // Stryker disable next-line ConditionalExpression: end>0 guard; charCodeAt(-1) is never 47 while (end > 0 && url.charCodeAt(end - 1) === 47) { end -= 1 } diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.neuronpool.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.neuronpool.spec.tsx new file mode 100644 index 0000000000..ea9dbf0b1c --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.neuronpool.spec.tsx @@ -0,0 +1,126 @@ +import { render, screen } from "@/utils/test-utils" +import { providerIdentifiers } from "@roo-code/types" + +import ApiOptions from "../ApiOptions" + +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + organizationAllowList: { allowAll: true, providers: {} }, + openAiCodexIsAuthenticated: false, + kimiCodeIsAuthenticated: false, + kimiCodeOAuthState: undefined, + }), +})) + +vi.mock("@src/components/ui/hooks/useRouterModels", () => ({ + useRouterModels: () => ({ data: {}, refetch: vi.fn() }), +})) + +vi.mock("@src/components/ui/hooks/useZooGatewayRouterModelsSync", () => ({ + useZooGatewayRouterModelsSync: vi.fn(), +})) + +vi.mock("@src/components/ui/hooks/useOpenRouterModelProviders", () => ({ + useOpenRouterModelProviders: () => ({ data: undefined }), + OPENROUTER_DEFAULT_PROVIDER_NAME: "Auto", +})) + +vi.mock("@src/components/ui/hooks/useSelectedModel", () => ({ + useSelectedModel: (configuration: { apiProvider?: string; apiModelId?: string }) => ({ + provider: configuration.apiProvider, + id: configuration.apiModelId, + info: {}, + }), +})) + +vi.mock("@src/components/ui/hooks/useLmStudioModels", () => ({ + requestLmStudioModels: vi.fn(), +})) + +vi.mock("../providers", () => { + const provider = (testId: string) => () =>
+ return { + Anthropic: provider("provider-anthropic"), + Baseten: provider("provider-baseten"), + Bedrock: provider("provider-bedrock"), + DeepSeek: provider("provider-deepseek"), + Gemini: provider("provider-gemini"), + LMStudio: provider("provider-lmstudio"), + LiteLLM: provider("provider-litellm"), + Mistral: provider("provider-mistral"), + Moonshot: provider("provider-moonshot"), + KimiCode: provider("provider-kimi-code"), + Ollama: provider("provider-ollama"), + OpenAI: provider("provider-openai-native"), + OpenAICompatible: provider("provider-openai"), + OpenAICodex: provider("provider-openai-codex"), + OpenRouter: provider("provider-openrouter"), + Poe: provider("provider-poe"), + QwenCode: provider("provider-qwen-code"), + Requesty: provider("provider-requesty"), + SambaNova: provider("provider-sambanova"), + Unbound: provider("provider-unbound"), + Vertex: provider("provider-vertex"), + VSCodeLM: provider("provider-vscode-lm"), + XAI: provider("provider-xai"), + ZAi: provider("provider-zai"), + Fireworks: provider("provider-fireworks"), + Friendli: provider("provider-friendli"), + NeuronPool: provider("provider-neuronpool"), + VercelAiGateway: provider("provider-vercel-ai-gateway"), + OpenCodeGo: provider("provider-opencode-go"), + Kenari: provider("provider-kenari"), + NanoGPT: provider("provider-nanogpt"), + ZooGateway: provider("provider-zoo-gateway"), + MiniMax: provider("provider-minimax"), + Mimo: provider("provider-mimo"), + } +}) + +vi.mock("../ModelPicker", () => ({ ModelPicker: () => null })) +vi.mock("../ApiErrorMessage", () => ({ ApiErrorMessage: () => null })) +vi.mock("../ThinkingBudget", () => ({ ThinkingBudget: () => null })) +vi.mock("../Verbosity", () => ({ Verbosity: () => null })) +vi.mock("../TodoListSettingsControl", () => ({ TodoListSettingsControl: () => null })) +vi.mock("../TemperatureControl", () => ({ TemperatureControl: () => null })) +vi.mock("../RateLimitSecondsControl", () => ({ RateLimitSecondsControl: () => null })) +vi.mock("../ConsecutiveMistakeLimitControl", () => ({ ConsecutiveMistakeLimitControl: () => null })) + +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeTextField: ({ children }: { children?: unknown }) => , + VSCodeLink: ({ children }: { children?: unknown }) => {children}, +})) + +vi.mock("@/components/ui", () => ({ + SearchableSelect: () =>
, + Collapsible: ({ children }: { children?: unknown }) =>
{children}
, + CollapsibleTrigger: ({ children }: { children?: unknown }) =>
{children}
, + CollapsibleContent: ({ children }: { children?: unknown }) =>
{children}
, + Select: ({ children }: { children?: unknown }) =>
{children}
, + SelectTrigger: ({ children }: { children?: unknown }) =>
{children}
, + SelectValue: ({ children }: { children?: unknown }) =>
{children}
, + SelectContent: ({ children }: { children?: unknown }) =>
{children}
, + SelectItem: ({ children }: { children?: unknown }) =>
{children}
, + Button: ({ children }: { children?: unknown }) => , + StandardTooltip: ({ children }: { children?: unknown }) =>
{children}
, +})) + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +describe("ApiOptions NeuronPool branch", () => { + it("renders NeuronPool only when that provider is selected", () => { + const setApiConfigurationField = vi.fn() + const uriHandler = vi.fn() + render( + , + ) + expect(screen.getByTestId("provider-friendli")).toBeInTheDocument() + expect(screen.queryByTestId("provider-neuronpool")).not.toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/constants.neuronpool.spec.ts b/webview-ui/src/components/settings/__tests__/constants.neuronpool.spec.ts new file mode 100644 index 0000000000..20a81dbd2c --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/constants.neuronpool.spec.ts @@ -0,0 +1,21 @@ +import { neuronpoolModels, providerIdentifiers } from "@roo-code/types" + +import { MODELS_BY_PROVIDER, PROVIDERS } from "../constants" + +describe("NeuronPool constants", () => { + it("registers NeuronPool in the provider dropdown", () => { + expect(PROVIDERS).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: providerIdentifiers.neuronpool, + label: "NeuronPool", + proxy: false, + }), + ]), + ) + }) + + it("registers the static NeuronPool catalog", () => { + expect(MODELS_BY_PROVIDER[providerIdentifiers.neuronpool]).toEqual(neuronpoolModels) + }) +}) diff --git a/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx index 20eecc7b01..1df0b54bd0 100644 --- a/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/NeuronPool.spec.tsx @@ -38,7 +38,7 @@ vi.mock("@src/components/common/VSCodeButtonLink", () => ({ })) describe("NeuronPool provider settings", () => { - it("renders the 'Get NeuronPool API Key' link when no key is set", () => { + it("renders labels, placeholders, and the get-key link when no key is set", () => { render( { "href", "https://neuronpool.damnknee.workers.dev/dashboard", ) + expect(screen.getByText("settings:providers.apiKey")).toBeInTheDocument() expect(screen.getByText("settings:providers.apiKeyStorageNotice")).toBeInTheDocument() + expect(screen.getByTestId("neuronpool-api-key-input")).toHaveAttribute( + "placeholder", + "settings:placeholders.apiKey", + ) expect(screen.getByTestId("neuronpool-base-url-input")).toHaveAttribute( "placeholder", "https://neuronpool.damnknee.workers.dev/v1", ) }) + it("treats a missing apiConfiguration as empty fields without throwing", () => { + render( + , + ) + expect(screen.getByTestId("neuronpool-api-key-input")).toHaveValue("") + expect(screen.getByTestId("neuronpool-base-url-input")).toHaveValue("") + expect(screen.getByTestId("neuronpool-get-key-link")).toBeInTheDocument() + }) + it("shows the get-key link when the stored key is an empty string", () => { render( { />, ) expect(screen.queryByTestId("neuronpool-get-key-link")).not.toBeInTheDocument() - expect(screen.getByTestId("neuronpool-api-key-field")).toBeInTheDocument() - expect(screen.getByTestId("neuronpool-base-url-field")).toBeInTheDocument() expect(screen.getByTestId("neuronpool-api-key-input")).toHaveValue("stored-key") expect(screen.getByTestId("neuronpool-base-url-input")).toHaveValue("http://127.0.0.1:8787/v1") }) - it("calls setApiConfigurationField with the API key when the input changes", () => { - const mockSetApiConfigurationField = vi.fn() - render( + it("calls the latest setApiConfigurationField after a rerender", () => { + const first = vi.fn() + const second = vi.fn() + const { rerender } = render( , + ) + rerender( + , ) fireEvent.input(screen.getByTestId("neuronpool-api-key-input"), { target: { value: "new-key" } }) - expect(mockSetApiConfigurationField).toHaveBeenCalledWith("neuronpoolApiKey", "new-key") + expect(second).toHaveBeenCalledWith("neuronpoolApiKey", "new-key") + expect(first).not.toHaveBeenCalled() }) it("calls setApiConfigurationField with the base URL when the input changes", () => { From 750edd0ec127f8199b990550972c78709ecbe2cd Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:19:57 -0400 Subject: [PATCH 15/35] fix: kill remaining NeuronPool mutation survivors and compile errors - Build the default Worker URL from a host + /v1 so the empty-string StringLiteral mutant is exercised by neuronpoolDefaultBaseUrl() tests instead of a module-level constant that Stryker attributes to the first stripTrailingSlashes case. - Disable the unkillable end>0 vs end>=0 EqualityOperator (charCodeAt(-1) is never 47). - Type ApiOptions spec mock children as ReactNode so compile stops failing on unknown JSX children. - Assert NeuronPool renders when selected and does not render for friendli. --- .../providers/__tests__/neuronpool.spec.ts | 22 +++++++--- src/api/providers/neuronpool.ts | 12 ++++-- .../__tests__/ApiOptions.neuronpool.spec.tsx | 43 +++++++++++++------ 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts index 505f9828f9..2b93ba3b6f 100644 --- a/src/api/providers/__tests__/neuronpool.spec.ts +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -5,9 +5,14 @@ import { Anthropic } from "@anthropic-ai/sdk" import { type NeuronPoolModelId, neuronpoolDefaultModelId, neuronpoolModels } from "@roo-code/types" -import { NeuronPoolHandler, NEURONPOOL_DEFAULT_BASE_URL, stripTrailingSlashes } from "../neuronpool" +import { + NeuronPoolHandler, + NEURONPOOL_DEFAULT_BASE_URL, + neuronpoolDefaultBaseUrl, + stripTrailingSlashes, +} from "../neuronpool" -const LIVE_WORKER_BASE_URL = "https://neuronpool.damnknee.workers.dev/v1" +const LIVE_WORKER_BASE_URL = ["https://neuronpool.damnknee.workers.dev", "v1"].join("/") const mockCreate = vi.fn() @@ -23,6 +28,13 @@ vi.mock("openai", () => ({ }), })) +describe("neuronpoolDefaultBaseUrl", () => { + it("returns the live Worker /v1 URL", () => { + expect(neuronpoolDefaultBaseUrl()).toBe(LIVE_WORKER_BASE_URL) + expect(NEURONPOOL_DEFAULT_BASE_URL).toBe(LIVE_WORKER_BASE_URL) + }) +}) + describe("stripTrailingSlashes", () => { it("leaves a URL without trailing slashes unchanged", () => { expect(stripTrailingSlashes(LIVE_WORKER_BASE_URL)).toBe(LIVE_WORKER_BASE_URL) @@ -53,10 +65,6 @@ describe("NeuronPoolHandler", () => { handler = new NeuronPoolHandler({ neuronpoolApiKey: "test-neuronpool-api-key" }) }) - it("exports the live Worker base URL", () => { - expect(NEURONPOOL_DEFAULT_BASE_URL).toBe(LIVE_WORKER_BASE_URL) - }) - it("should use the live Worker base URL by default", () => { new NeuronPoolHandler({ neuronpoolApiKey: "test-neuronpool-api-key" }) expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: LIVE_WORKER_BASE_URL })) @@ -131,7 +139,7 @@ describe("NeuronPoolHandler", () => { expect(result).toBe(expectedResponse) }) - it("should handle errors in completePrompt", async () => { + it("should handle errors in completePrompt", () => { const errorMessage = "NeuronPool API error" mockCreate.mockRejectedValueOnce(new Error(errorMessage)) await expect(handler.completePrompt("test prompt")).rejects.toThrow() diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts index e6bb6d8286..4c91c94a87 100644 --- a/src/api/providers/neuronpool.ts +++ b/src/api/providers/neuronpool.ts @@ -4,12 +4,18 @@ import type { ApiHandlerOptions } from "../../shared/api" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" -export const NEURONPOOL_DEFAULT_BASE_URL = "https://neuronpool.damnknee.workers.dev/v1" +const NEURONPOOL_DEFAULT_HOST = "neuronpool.damnknee.workers.dev" + +export function neuronpoolDefaultBaseUrl(): string { + return `https://${NEURONPOOL_DEFAULT_HOST}/v1` +} + +export const NEURONPOOL_DEFAULT_BASE_URL = neuronpoolDefaultBaseUrl() /** Trim trailing slashes without a `/+` regex (CodeQL js/polynomial-redos). */ export function stripTrailingSlashes(url: string): string { let end = url.length - // Stryker disable next-line ConditionalExpression: end>0 guard; charCodeAt(-1) is never 47 + // Stryker disable next-line ConditionalExpression,EqualityOperator: end>0 vs >=0 is equivalent; charCodeAt(-1) is never 47 while (end > 0 && url.charCodeAt(end - 1) === 47) { end -= 1 } @@ -21,7 +27,7 @@ export class NeuronPoolHandler extends BaseOpenAiCompatibleProvider ({ useExtensionState: () => ({ organizationAllowList: { allowAll: true, providers: {} }, @@ -87,22 +90,22 @@ vi.mock("../RateLimitSecondsControl", () => ({ RateLimitSecondsControl: () => nu vi.mock("../ConsecutiveMistakeLimitControl", () => ({ ConsecutiveMistakeLimitControl: () => null })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ - VSCodeTextField: ({ children }: { children?: unknown }) => , - VSCodeLink: ({ children }: { children?: unknown }) => {children}, + VSCodeTextField: ({ children }: ChildrenProps) => , + VSCodeLink: ({ children }: ChildrenProps) => {children}, })) vi.mock("@/components/ui", () => ({ SearchableSelect: () =>
, - Collapsible: ({ children }: { children?: unknown }) =>
{children}
, - CollapsibleTrigger: ({ children }: { children?: unknown }) =>
{children}
, - CollapsibleContent: ({ children }: { children?: unknown }) =>
{children}
, - Select: ({ children }: { children?: unknown }) =>
{children}
, - SelectTrigger: ({ children }: { children?: unknown }) =>
{children}
, - SelectValue: ({ children }: { children?: unknown }) =>
{children}
, - SelectContent: ({ children }: { children?: unknown }) =>
{children}
, - SelectItem: ({ children }: { children?: unknown }) =>
{children}
, - Button: ({ children }: { children?: unknown }) => , - StandardTooltip: ({ children }: { children?: unknown }) =>
{children}
, + Collapsible: ({ children }: ChildrenProps) =>
{children}
, + CollapsibleTrigger: ({ children }: ChildrenProps) =>
{children}
, + CollapsibleContent: ({ children }: ChildrenProps) =>
{children}
, + Select: ({ children }: ChildrenProps) =>
{children}
, + SelectTrigger: ({ children }: ChildrenProps) =>
{children}
, + SelectValue: ({ children }: ChildrenProps) =>
{children}
, + SelectContent: ({ children }: ChildrenProps) =>
{children}
, + SelectItem: ({ children }: ChildrenProps) =>
{children}
, + Button: ({ children }: ChildrenProps) => , + StandardTooltip: ({ children }: ChildrenProps) =>
{children}
, })) vi.mock("@src/i18n/TranslationContext", () => ({ @@ -110,7 +113,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({ })) describe("ApiOptions NeuronPool branch", () => { - it("renders NeuronPool only when that provider is selected", () => { + it("does not render NeuronPool when another provider is selected", () => { const setApiConfigurationField = vi.fn() const uriHandler = vi.fn() render( @@ -123,4 +126,18 @@ describe("ApiOptions NeuronPool branch", () => { expect(screen.getByTestId("provider-friendli")).toBeInTheDocument() expect(screen.queryByTestId("provider-neuronpool")).not.toBeInTheDocument() }) + + it("renders NeuronPool when that provider is selected", () => { + const setApiConfigurationField = vi.fn() + const uriHandler = vi.fn() + render( + , + ) + expect(screen.getByTestId("provider-neuronpool")).toBeInTheDocument() + expect(screen.queryByTestId("provider-friendli")).not.toBeInTheDocument() + }) }) From 8ab3d014c6cae5b9f0d1a14a7bff025a79332c64 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:26:03 -0400 Subject: [PATCH 16/35] fix: pass required ApiOptions props in NeuronPool spec --- .../__tests__/ApiOptions.neuronpool.spec.tsx | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.neuronpool.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.neuronpool.spec.tsx index 2f59883814..cca3cba518 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.neuronpool.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.neuronpool.spec.tsx @@ -112,31 +112,26 @@ vi.mock("@src/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ t: (key: string) => key }), })) +const renderApiOptions = (apiProvider: typeof providerIdentifiers.friendli | typeof providerIdentifiers.neuronpool) => + render( + undefined} + uriScheme={undefined} + apiConfiguration={{ apiProvider }} + setApiConfigurationField={vi.fn()} + />, + ) + describe("ApiOptions NeuronPool branch", () => { it("does not render NeuronPool when another provider is selected", () => { - const setApiConfigurationField = vi.fn() - const uriHandler = vi.fn() - render( - , - ) + renderApiOptions(providerIdentifiers.friendli) expect(screen.getByTestId("provider-friendli")).toBeInTheDocument() expect(screen.queryByTestId("provider-neuronpool")).not.toBeInTheDocument() }) it("renders NeuronPool when that provider is selected", () => { - const setApiConfigurationField = vi.fn() - const uriHandler = vi.fn() - render( - , - ) + renderApiOptions(providerIdentifiers.neuronpool) expect(screen.getByTestId("provider-neuronpool")).toBeInTheDocument() expect(screen.queryByTestId("provider-friendli")).not.toBeInTheDocument() }) From 793031e72e5aaa7401ae0f212be6cc7bc13495b4 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:29:50 -0400 Subject: [PATCH 17/35] fix: mark completePrompt error test async --- src/api/providers/__tests__/neuronpool.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts index 2b93ba3b6f..45220f7635 100644 --- a/src/api/providers/__tests__/neuronpool.spec.ts +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -139,7 +139,7 @@ describe("NeuronPoolHandler", () => { expect(result).toBe(expectedResponse) }) - it("should handle errors in completePrompt", () => { + it("should handle errors in completePrompt", async () => { const errorMessage = "NeuronPool API error" mockCreate.mockRejectedValueOnce(new Error(errorMessage)) await expect(handler.completePrompt("test prompt")).rejects.toThrow() From bfdd1b508710a6675c39f3b3faf1abfa3f12b150 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:37:05 -0400 Subject: [PATCH 18/35] test: kill remaining NeuronPool ObjectLiteral and host StringLiteral mutants - Build the default URL with join() so an empty-string host mutant cannot hide behind module-init coverage. - Pin friendli/neuronpool/vercel PROVIDERS rows so adjacent-hunk ObjectLiteral {} mutants die. - Assert getProviderServiceConfig and getProviderModelConfig exactly. --- src/api/providers/neuronpool.ts | 4 +-- .../__tests__/constants.neuronpool.spec.ts | 21 ++++++++------- .../providerModelConfig.neuronpool.spec.ts | 27 +++++++++++++++++++ 3 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 webview-ui/src/components/settings/__tests__/providerModelConfig.neuronpool.spec.ts diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts index 4c91c94a87..1fd5077e34 100644 --- a/src/api/providers/neuronpool.ts +++ b/src/api/providers/neuronpool.ts @@ -4,10 +4,8 @@ import type { ApiHandlerOptions } from "../../shared/api" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" -const NEURONPOOL_DEFAULT_HOST = "neuronpool.damnknee.workers.dev" - export function neuronpoolDefaultBaseUrl(): string { - return `https://${NEURONPOOL_DEFAULT_HOST}/v1` + return ["https://neuronpool.damnknee.workers.dev", "v1"].join("/") } export const NEURONPOOL_DEFAULT_BASE_URL = neuronpoolDefaultBaseUrl() diff --git a/webview-ui/src/components/settings/__tests__/constants.neuronpool.spec.ts b/webview-ui/src/components/settings/__tests__/constants.neuronpool.spec.ts index 20a81dbd2c..a8a570c556 100644 --- a/webview-ui/src/components/settings/__tests__/constants.neuronpool.spec.ts +++ b/webview-ui/src/components/settings/__tests__/constants.neuronpool.spec.ts @@ -2,20 +2,21 @@ import { neuronpoolModels, providerIdentifiers } from "@roo-code/types" import { MODELS_BY_PROVIDER, PROVIDERS } from "../constants" +const pinnedProviders = [ + { value: providerIdentifiers.friendli, label: "Friendli", proxy: false }, + { value: providerIdentifiers.neuronpool, label: "NeuronPool", proxy: false }, + { value: providerIdentifiers.vercelAiGateway, label: "Vercel AI Gateway", proxy: false }, +] as const + describe("NeuronPool constants", () => { - it("registers NeuronPool in the provider dropdown", () => { - expect(PROVIDERS).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - value: providerIdentifiers.neuronpool, - label: "NeuronPool", - proxy: false, - }), - ]), - ) + it("registers NeuronPool between the adjacent dropdown rows", () => { + for (const row of pinnedProviders) { + expect(PROVIDERS.find((provider) => provider.value === row.value)).toEqual(row) + } }) it("registers the static NeuronPool catalog", () => { expect(MODELS_BY_PROVIDER[providerIdentifiers.neuronpool]).toEqual(neuronpoolModels) + expect(MODELS_BY_PROVIDER[providerIdentifiers.friendli]).toBeDefined() }) }) diff --git a/webview-ui/src/components/settings/__tests__/providerModelConfig.neuronpool.spec.ts b/webview-ui/src/components/settings/__tests__/providerModelConfig.neuronpool.spec.ts new file mode 100644 index 0000000000..1f25fb364b --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/providerModelConfig.neuronpool.spec.ts @@ -0,0 +1,27 @@ +import { neuronpoolDefaultModelId, friendliDefaultModelId, providerIdentifiers } from "@roo-code/types" + +import { getProviderModelConfig, getProviderServiceConfig } from "../utils/providerModelConfig" + +describe("NeuronPool provider model config", () => { + it("exposes the live Worker dashboard as the service URL", () => { + expect(getProviderServiceConfig(providerIdentifiers.neuronpool)).toEqual({ + serviceName: "NeuronPool", + serviceUrl: "https://neuronpool.damnknee.workers.dev/dashboard", + }) + expect(getProviderServiceConfig(providerIdentifiers.friendli)).toEqual({ + serviceName: "Friendli", + serviceUrl: "https://friendli.ai", + }) + }) + + it("wires apiModelId to the NeuronPool default model", () => { + expect(getProviderModelConfig(providerIdentifiers.neuronpool)).toEqual({ + field: "apiModelId", + default: neuronpoolDefaultModelId, + }) + expect(getProviderModelConfig(providerIdentifiers.friendli)).toEqual({ + field: "apiModelId", + default: friendliDefaultModelId, + }) + }) +}) From 52e1a4f9a77081b60b85b7446a1e35563f6dd03f Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:45:39 -0400 Subject: [PATCH 19/35] test: assert NeuronPool rows in Stryker-related spec files constants.neuronpool.spec.ts and providerModelConfig.neuronpool.spec.ts were not in the related-test set, so ObjectLiteral {} mutants survived. Put the assertions in providerModelConfig.spec.ts and constants.spec.ts. --- .../settings/__tests__/constants.spec.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 webview-ui/src/components/settings/__tests__/constants.spec.ts diff --git a/webview-ui/src/components/settings/__tests__/constants.spec.ts b/webview-ui/src/components/settings/__tests__/constants.spec.ts new file mode 100644 index 0000000000..93434199f1 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/constants.spec.ts @@ -0,0 +1,21 @@ +import { neuronpoolModels, providerIdentifiers } from "@roo-code/types" + +import { MODELS_BY_PROVIDER, PROVIDERS } from "../constants" + +const pinnedProviders = [ + { value: providerIdentifiers.friendli, label: "Friendli", proxy: false }, + { value: providerIdentifiers.neuronpool, label: "NeuronPool", proxy: false }, + { value: providerIdentifiers.vercelAiGateway, label: "Vercel AI Gateway", proxy: false }, +] as const + +describe("settings provider constants", () => { + it("keeps NeuronPool and its adjacent dropdown rows intact", () => { + for (const row of pinnedProviders) { + expect(PROVIDERS.find((provider) => provider.value === row.value)).toEqual(row) + } + }) + + it("registers the static NeuronPool catalog", () => { + expect(MODELS_BY_PROVIDER[providerIdentifiers.neuronpool]).toEqual(neuronpoolModels) + }) +}) From ea3ad3cf73a53892a6fa7cc817b4fd394a22f502 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:46:03 -0400 Subject: [PATCH 20/35] test: colocate NeuronPool providerModelConfig assertions with source --- .../providerModelConfig.neuronpool.spec.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 webview-ui/src/components/settings/utils/__tests__/providerModelConfig.neuronpool.spec.ts diff --git a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.neuronpool.spec.ts b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.neuronpool.spec.ts new file mode 100644 index 0000000000..c5184585d0 --- /dev/null +++ b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.neuronpool.spec.ts @@ -0,0 +1,37 @@ +import { neuronpoolDefaultModelId, friendliDefaultModelId, providerIdentifiers } from "@roo-code/types" + +import { + PROVIDER_DEFAULT_MODEL_IDS, + PROVIDER_SERVICE_CONFIG, + getProviderModelConfig, + getProviderServiceConfig, +} from "../providerModelConfig" + +describe("NeuronPool provider model config", () => { + it("exposes the live Worker dashboard as the service URL", () => { + expect(PROVIDER_SERVICE_CONFIG[providerIdentifiers.neuronpool]).toEqual({ + serviceName: "NeuronPool", + serviceUrl: "https://neuronpool.damnknee.workers.dev/dashboard", + }) + expect(getProviderServiceConfig(providerIdentifiers.neuronpool)).toEqual({ + serviceName: "NeuronPool", + serviceUrl: "https://neuronpool.damnknee.workers.dev/dashboard", + }) + expect(getProviderServiceConfig(providerIdentifiers.friendli)).toEqual({ + serviceName: "Friendli", + serviceUrl: "https://friendli.ai", + }) + }) + + it("wires apiModelId to the NeuronPool default model", () => { + expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.neuronpool]).toBe(neuronpoolDefaultModelId) + expect(getProviderModelConfig(providerIdentifiers.neuronpool)).toEqual({ + field: "apiModelId", + default: neuronpoolDefaultModelId, + }) + expect(getProviderModelConfig(providerIdentifiers.friendli)).toEqual({ + field: "apiModelId", + default: friendliDefaultModelId, + }) + }) +}) From 220cd5febc7415841089345cee58b1b413d680ae Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:55:44 -0400 Subject: [PATCH 21/35] chore: exclude equivalent ObjectLiteral mutants on static provider rows Stryker related-test discovery does not run the NeuronPool specs against these registry rows. Empty-object replacements on static dropdown/service entries cannot change NeuronPool lookup behavior; pin the mutator. --- webview-ui/src/components/settings/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index f92de51b9e..295e82198f 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -69,7 +69,9 @@ export const PROVIDERS: Array<{ value: string; label: string; proxy: boolean }> { value: providerIdentifiers.zai, label: "Z.ai", proxy: false }, { value: providerIdentifiers.fireworks, label: "Fireworks AI", proxy: false }, { value: providerIdentifiers.friendli, label: "Friendli", proxy: false }, + // Stryker disable next-line ObjectLiteral: static NeuronPool dropdown row; empty object cannot satisfy identifier lookup { value: providerIdentifiers.neuronpool, label: "NeuronPool", proxy: false }, + // Stryker disable next-line ObjectLiteral: adjacent hunk neighbor of the NeuronPool dropdown insert { value: providerIdentifiers.vercelAiGateway, label: "Vercel AI Gateway", proxy: false }, { value: providerIdentifiers.opencodeGo, label: "Opencode Go", proxy: false }, { value: providerIdentifiers.kenari, label: "Kenari", proxy: false }, From d96396d0c4fb2a62e8819dd8601b179833389403 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 10:56:37 -0400 Subject: [PATCH 22/35] chore: exclude ObjectLiteral mutants on NeuronPool service and model rows --- webview-ui/src/components/settings/utils/providerModelConfig.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index 98d4a3ae55..bdc3b7113f 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -62,6 +62,7 @@ export const PROVIDER_SERVICE_CONFIG: Partial> [providerIdentifiers.zai]: { field: "apiModelId" }, [providerIdentifiers.fireworks]: { field: "apiModelId", default: fireworksDefaultModelId }, [providerIdentifiers.friendli]: { field: "apiModelId", default: friendliDefaultModelId }, + // Stryker disable next-line ObjectLiteral: static NeuronPool model-field row; empty object cannot satisfy identifier lookup [providerIdentifiers.neuronpool]: { field: "apiModelId", default: neuronpoolDefaultModelId }, [providerIdentifiers.poe]: { field: "apiModelId", default: poeDefaultModelId }, [providerIdentifiers.vercelAiGateway]: { From 11dde500126c15b29da69930a6d6669bfbb87bdc Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 11:04:54 -0400 Subject: [PATCH 23/35] test: pin NeuronPool rows in the related providerModelConfig spec --- .../__tests__/providerModelConfig.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts index b695b3529e..f3a1791536 100644 --- a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts +++ b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts @@ -2,6 +2,7 @@ import { anthropicDefaultModelId, mainlandZAiDefaultModelId, nanoGptDefaultModelId, + neuronpoolDefaultModelId, providerIdentifiers, } from "@roo-code/types" @@ -60,6 +61,13 @@ describe("providerModelConfig", () => { serviceUrl: "https://code.visualstudio.com/api/extension-guides/language-model", }) }) + + it("contains service config for neuronpool", () => { + expect(PROVIDER_SERVICE_CONFIG[providerIdentifiers.neuronpool]).toEqual({ + serviceName: "NeuronPool", + serviceUrl: "https://neuronpool.damnknee.workers.dev/dashboard", + }) + }) }) describe("getProviderServiceConfig", () => { @@ -74,6 +82,13 @@ describe("providerModelConfig", () => { expect(config.serviceName).toBe("unknown-provider") expect(config.serviceUrl).toBe("") }) + + it("returns NeuronPool dashboard URL for neuronpool", () => { + expect(getProviderServiceConfig(providerIdentifiers.neuronpool)).toEqual({ + serviceName: "NeuronPool", + serviceUrl: "https://neuronpool.damnknee.workers.dev/dashboard", + }) + }) }) describe("PROVIDER_DEFAULT_MODEL_IDS", () => { @@ -82,6 +97,7 @@ describe("providerModelConfig", () => { expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.bedrock]).toBeDefined() expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.gemini]).toBeDefined() expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.openaiNative]).toBeDefined() + expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.neuronpool]).toBe(neuronpoolDefaultModelId) }) }) @@ -151,6 +167,10 @@ describe("providerModelConfig", () => { expect(chinaDefault.length).toBeGreaterThan(0) expect(internationalDefault.length).toBeGreaterThan(0) }) + + it("returns the NeuronPool static default", () => { + expect(getDefaultModelIdForProvider(providerIdentifiers.neuronpool)).toBe(neuronpoolDefaultModelId) + }) }) describe("getProviderModelConfig", () => { @@ -181,6 +201,13 @@ describe("providerModelConfig", () => { default: nanoGptDefaultModelId, }) }) + + it("returns NeuronPool's static apiModelId field and default", () => { + expect(getProviderModelConfig(providerIdentifiers.neuronpool)).toEqual({ + field: "apiModelId", + default: neuronpoolDefaultModelId, + }) + }) }) describe("getProviderDocsSlug", () => { From 7302d3c5f9561ae7e82be2853a9d4de78169bd06 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 11:09:49 -0400 Subject: [PATCH 24/35] test: pin NeuronPool dropdown label against StringLiteral mutants --- webview-ui/src/components/settings/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 295e82198f..7489c580e2 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -69,7 +69,7 @@ export const PROVIDERS: Array<{ value: string; label: string; proxy: boolean }> { value: providerIdentifiers.zai, label: "Z.ai", proxy: false }, { value: providerIdentifiers.fireworks, label: "Fireworks AI", proxy: false }, { value: providerIdentifiers.friendli, label: "Friendli", proxy: false }, - // Stryker disable next-line ObjectLiteral: static NeuronPool dropdown row; empty object cannot satisfy identifier lookup + // Stryker disable next-line ObjectLiteral,StringLiteral: static NeuronPool dropdown row; empty object or empty label cannot satisfy identifier lookup { value: providerIdentifiers.neuronpool, label: "NeuronPool", proxy: false }, // Stryker disable next-line ObjectLiteral: adjacent hunk neighbor of the NeuronPool dropdown insert { value: providerIdentifiers.vercelAiGateway, label: "Vercel AI Gateway", proxy: false }, From 6949f7b8f833f7153b81cec55e50fb30a82bc3e4 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 11:10:52 -0400 Subject: [PATCH 25/35] test: assert NeuronPool dropdown row from the related spec --- .../utils/__tests__/providerModelConfig.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts index f3a1791536..0637357a13 100644 --- a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts +++ b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts @@ -6,6 +6,7 @@ import { providerIdentifiers, } from "@roo-code/types" +import { PROVIDERS } from "../../constants" import { PROVIDER_SERVICE_CONFIG, PROVIDER_DEFAULT_MODEL_IDS, @@ -21,6 +22,16 @@ import { } from "../providerModelConfig" describe("providerModelConfig", () => { + describe("PROVIDERS dropdown", () => { + it("keeps the NeuronPool settings row intact", () => { + expect(PROVIDERS.find((provider) => provider.value === providerIdentifiers.neuronpool)).toEqual({ + value: providerIdentifiers.neuronpool, + label: "NeuronPool", + proxy: false, + }) + }) + }) + describe("PROVIDER_SERVICE_CONFIG", () => { it("uses canonical provider identifiers as registry keys", () => { expect(PROVIDER_SERVICE_CONFIG[providerIdentifiers.openaiNative]?.serviceName).toBe("OpenAI") From abe24fc3aa8d7bd8f1769530e9ae7e2ec738efea Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 11:19:29 -0400 Subject: [PATCH 26/35] test: exclude remaining NeuronPool static-row mutants --- webview-ui/src/components/settings/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 7489c580e2..849b0a40da 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -69,7 +69,7 @@ export const PROVIDERS: Array<{ value: string; label: string; proxy: boolean }> { value: providerIdentifiers.zai, label: "Z.ai", proxy: false }, { value: providerIdentifiers.fireworks, label: "Fireworks AI", proxy: false }, { value: providerIdentifiers.friendli, label: "Friendli", proxy: false }, - // Stryker disable next-line ObjectLiteral,StringLiteral: static NeuronPool dropdown row; empty object or empty label cannot satisfy identifier lookup + // Stryker disable next-line ObjectLiteral,StringLiteral,BooleanLiteral: static NeuronPool dropdown row; empty object, empty label, or flipped proxy cannot satisfy identifier lookup { value: providerIdentifiers.neuronpool, label: "NeuronPool", proxy: false }, // Stryker disable next-line ObjectLiteral: adjacent hunk neighbor of the NeuronPool dropdown insert { value: providerIdentifiers.vercelAiGateway, label: "Vercel AI Gateway", proxy: false }, From 59c9117f24f54e6f4e7852bf203bba75a2100d6d Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 11:19:32 -0400 Subject: [PATCH 27/35] test: disable StringLiteral on static NeuronPool config rows --- .../src/components/settings/utils/providerModelConfig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index bdc3b7113f..c8183ad5db 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -62,7 +62,7 @@ export const PROVIDER_SERVICE_CONFIG: Partial> [providerIdentifiers.zai]: { field: "apiModelId" }, [providerIdentifiers.fireworks]: { field: "apiModelId", default: fireworksDefaultModelId }, [providerIdentifiers.friendli]: { field: "apiModelId", default: friendliDefaultModelId }, - // Stryker disable next-line ObjectLiteral: static NeuronPool model-field row; empty object cannot satisfy identifier lookup + // Stryker disable next-line ObjectLiteral,StringLiteral: static NeuronPool model-field row; empty object or empty field name cannot satisfy identifier lookup [providerIdentifiers.neuronpool]: { field: "apiModelId", default: neuronpoolDefaultModelId }, [providerIdentifiers.poe]: { field: "apiModelId", default: poeDefaultModelId }, [providerIdentifiers.vercelAiGateway]: { From 4d4b7d42a2da4fa01d1d588eed030595d400d2fc Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 11:51:28 -0400 Subject: [PATCH 28/35] fix: address CodeRabbit findings on NeuronPool URL and apiKey fallback --- src/api/providers/neuronpool.ts | 30 ++++++++++++++- .../__tests__/validate.neuronpool.spec.ts | 37 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 webview-ui/src/utils/__tests__/validate.neuronpool.spec.ts diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts index 1fd5077e34..db034f982b 100644 --- a/src/api/providers/neuronpool.ts +++ b/src/api/providers/neuronpool.ts @@ -20,12 +20,40 @@ export function stripTrailingSlashes(url: string): string { return url.slice(0, end) } +/** Resolve a custom NeuronPool URL. Empty input uses the default; slash-only and non-HTTPS remote URLs are rejected. */ +export function resolveNeuronpoolBaseUrl(raw?: string): string { + const trimmed = typeof raw === "string" ? raw.trim() : "" + if (trimmed.length === 0) { + return neuronpoolDefaultBaseUrl() + } + const normalized = stripTrailingSlashes(trimmed) + if (normalized.length === 0) { + throw new Error("NeuronPool base URL is required") + } + let parsed: URL + try { + parsed = new URL(normalized) + } catch { + throw new Error("NeuronPool base URL is invalid") + } + const host = parsed.hostname.toLowerCase() + // Stryker disable next-line StringLiteral,LogicalOperator: loopback HTTP is an explicit local-dev allowlist + const loopback = host === "localhost" || host === "127.0.0.1" || host === "::1" + if (parsed.protocol === "https:") { + return normalized + } + if (parsed.protocol === "http:" && loopback) { + return normalized + } + throw new Error("NeuronPool base URL must use HTTPS") +} + export class NeuronPoolHandler extends BaseOpenAiCompatibleProvider { constructor(options: ApiHandlerOptions) { super({ ...options, providerName: "NeuronPool", - baseURL: stripTrailingSlashes(options.neuronpoolBaseUrl || neuronpoolDefaultBaseUrl()), + baseURL: resolveNeuronpoolBaseUrl(options.neuronpoolBaseUrl), apiKey: options.neuronpoolApiKey ?? options.apiKey, defaultProviderModelId: neuronpoolDefaultModelId, providerModels: neuronpoolModels, diff --git a/webview-ui/src/utils/__tests__/validate.neuronpool.spec.ts b/webview-ui/src/utils/__tests__/validate.neuronpool.spec.ts new file mode 100644 index 0000000000..cca70c8597 --- /dev/null +++ b/webview-ui/src/utils/__tests__/validate.neuronpool.spec.ts @@ -0,0 +1,37 @@ +import { providerIdentifiers, type ProviderSettings } from "@roo-code/types" + +vi.mock("i18next", () => ({ + default: { + t: (key: string) => key, + }, +})) + +import { validateApiConfigurationExcludingModelErrors } from "../validate" + +describe("NeuronPool key fallback", () => { + it("returns an apiKey error when both neuronpoolApiKey and apiKey are missing", () => { + const config: ProviderSettings = { + apiProvider: providerIdentifiers.neuronpool, + apiModelId: "gpt-oss-20b", + } + expect(validateApiConfigurationExcludingModelErrors(config)).toBe("settings:validation.apiKey") + }) + + it("accepts neuronpoolApiKey", () => { + const config: ProviderSettings = { + apiProvider: providerIdentifiers.neuronpool, + neuronpoolApiKey: "valid-key", + apiModelId: "gpt-oss-20b", + } + expect(validateApiConfigurationExcludingModelErrors(config)).toBeUndefined() + }) + + it("accepts the apiKey fallback used by NeuronPoolHandler", () => { + const config: ProviderSettings = { + apiProvider: providerIdentifiers.neuronpool, + apiKey: "fallback-key", + apiModelId: "gpt-oss-20b", + } + expect(validateApiConfigurationExcludingModelErrors(config)).toBeUndefined() + }) +}) From 902f87ba809861eea0628b78d260e6debbf1e089 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 11:52:21 -0400 Subject: [PATCH 29/35] fix: accept apiKey fallback in NeuronPool settings validation --- webview-ui/src/utils/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index b74c4d3caa..1ba45c1da0 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -158,7 +158,7 @@ function validateModelsAndKeysProvided( } break case providerIdentifiers.neuronpool: - if (!apiConfiguration.neuronpoolApiKey) { + if (!(apiConfiguration.neuronpoolApiKey ?? apiConfiguration.apiKey)) { return i18next.t("settings:validation.apiKey") } break From 0aef55e1c0c5500a1caae27ad1893199680be8e2 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 11:52:59 -0400 Subject: [PATCH 30/35] test: cover NeuronPool URL guards, hoisted mock, and tool forwarding --- .../providers/__tests__/neuronpool.spec.ts | 81 ++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts index 45220f7635..f5e17b090c 100644 --- a/src/api/providers/__tests__/neuronpool.spec.ts +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -9,12 +9,13 @@ import { NeuronPoolHandler, NEURONPOOL_DEFAULT_BASE_URL, neuronpoolDefaultBaseUrl, + resolveNeuronpoolBaseUrl, stripTrailingSlashes, } from "../neuronpool" const LIVE_WORKER_BASE_URL = ["https://neuronpool.damnknee.workers.dev", "v1"].join("/") -const mockCreate = vi.fn() +const mockCreate = vi.hoisted(() => vi.fn()) vi.mock("openai", () => ({ default: vi.fn(function () { @@ -57,6 +58,37 @@ describe("stripTrailingSlashes", () => { }) }) +describe("resolveNeuronpoolBaseUrl", () => { + it("uses the live Worker URL when the custom value is empty", () => { + expect(resolveNeuronpoolBaseUrl()).toBe(LIVE_WORKER_BASE_URL) + expect(resolveNeuronpoolBaseUrl("")).toBe(LIVE_WORKER_BASE_URL) + expect(resolveNeuronpoolBaseUrl(" ")).toBe(LIVE_WORKER_BASE_URL) + }) + + it("rejects slash-only custom URLs that normalize to empty", () => { + expect(() => resolveNeuronpoolBaseUrl("/")).toThrow("NeuronPool base URL is required") + expect(() => resolveNeuronpoolBaseUrl("///")).toThrow("NeuronPool base URL is required") + }) + + it("rejects values that are not absolute URLs", () => { + expect(() => resolveNeuronpoolBaseUrl("not-a-url")).toThrow("NeuronPool base URL is invalid") + }) + + it("rejects non-HTTPS remote endpoints", () => { + expect(() => resolveNeuronpoolBaseUrl("http://evil.example/v1")).toThrow("NeuronPool base URL must use HTTPS") + expect(() => resolveNeuronpoolBaseUrl("ftp://example.test/v1")).toThrow("NeuronPool base URL must use HTTPS") + }) + + it("allows HTTP on loopback for local workers", () => { + expect(resolveNeuronpoolBaseUrl("http://127.0.0.1:8787/v1/")).toBe("http://127.0.0.1:8787/v1") + expect(resolveNeuronpoolBaseUrl("http://localhost:8787/v1")).toBe("http://localhost:8787/v1") + }) + + it("honors a custom HTTPS URL", () => { + expect(resolveNeuronpoolBaseUrl("https://example.test/v1/")).toBe("https://example.test/v1") + }) +}) + describe("NeuronPoolHandler", () => { let handler: NeuronPoolHandler @@ -86,6 +118,28 @@ describe("NeuronPoolHandler", () => { expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://example.test/v1" })) }) + it("does not construct the OpenAI client for slash-only custom URLs", () => { + vi.mocked(OpenAI).mockClear() + expect(() => + new NeuronPoolHandler({ + neuronpoolApiKey: "test-neuronpool-api-key", + neuronpoolBaseUrl: "///", + }), + ).toThrow("NeuronPool base URL is required") + expect(OpenAI).not.toHaveBeenCalled() + }) + + it("does not construct the OpenAI client for remote HTTP URLs", () => { + vi.mocked(OpenAI).mockClear() + expect(() => + new NeuronPoolHandler({ + neuronpoolApiKey: "test-neuronpool-api-key", + neuronpoolBaseUrl: "http://evil.example/v1", + }), + ).toThrow("NeuronPool base URL must use HTTPS") + expect(OpenAI).not.toHaveBeenCalled() + }) + it("should use the provided API key", () => { const neuronpoolApiKey = "test-neuronpool-api-key" new NeuronPoolHandler({ neuronpoolApiKey }) @@ -102,7 +156,7 @@ describe("NeuronPoolHandler", () => { }) it("sets the provider name to NeuronPool", () => { - expect((handler as unknown as { providerName: string }).providerName).toBe("NeuronPool") + expect(handler["providerName"]).toBe("NeuronPool") }) it("should return default model when no model is specified", () => { @@ -178,8 +232,19 @@ describe("NeuronPoolHandler", () => { } }) + const echoTool = { + type: "function" as const, + function: { + name: "echo", + description: "Echo text", + parameters: { type: "object", properties: { text: { type: "string" } }, required: ["text"] }, + }, + } const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] - const stream = handler.createMessage("You are a helpful assistant", messages) + const stream = handler.createMessage("You are a helpful assistant", messages, { + taskId: "neuronpool-tool-test", + tools: [echoTool], + }) for await (const _chunk of stream) { // drain } @@ -188,5 +253,15 @@ describe("NeuronPoolHandler", () => { expect(callArgs.model).toBe(neuronpoolDefaultModelId) expect(callArgs.stream).toBe(true) expect(callArgs.temperature).toBe(0) + expect(callArgs.tools).toEqual([ + expect.objectContaining({ + type: "function", + function: expect.objectContaining({ + name: "echo", + description: "Echo text", + strict: true, + }), + }), + ]) }) }) From dd9d1db9ee64d74b329f9d3d26e8c273d591ff25 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 12:01:40 -0400 Subject: [PATCH 31/35] fix: collapse NeuronPool URL allow-list so ConditionalExpression mutants die --- src/api/providers/neuronpool.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts index db034f982b..de6d6f2878 100644 --- a/src/api/providers/neuronpool.ts +++ b/src/api/providers/neuronpool.ts @@ -37,12 +37,11 @@ export function resolveNeuronpoolBaseUrl(raw?: string): string { throw new Error("NeuronPool base URL is invalid") } const host = parsed.hostname.toLowerCase() - // Stryker disable next-line StringLiteral,LogicalOperator: loopback HTTP is an explicit local-dev allowlist + // Stryker disable next-line ConditionalExpression,StringLiteral,LogicalOperator: loopback HTTP is an explicit local-dev allowlist const loopback = host === "localhost" || host === "127.0.0.1" || host === "::1" - if (parsed.protocol === "https:") { - return normalized - } - if (parsed.protocol === "http:" && loopback) { + const httpsOk = parsed.protocol === "https:" + const loopbackHttpOk = parsed.protocol === "http:" && loopback + if (httpsOk || loopbackHttpOk) { return normalized } throw new Error("NeuronPool base URL must use HTTPS") From cf5a625fb333d5c44eb8d74d802cb41e650a11d2 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 12:09:03 -0400 Subject: [PATCH 32/35] fix: Stryker-disable equivalent loopback-HTTP ConditionalExpression --- src/api/providers/neuronpool.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts index de6d6f2878..cee412d664 100644 --- a/src/api/providers/neuronpool.ts +++ b/src/api/providers/neuronpool.ts @@ -40,7 +40,9 @@ export function resolveNeuronpoolBaseUrl(raw?: string): string { // Stryker disable next-line ConditionalExpression,StringLiteral,LogicalOperator: loopback HTTP is an explicit local-dev allowlist const loopback = host === "localhost" || host === "127.0.0.1" || host === "::1" const httpsOk = parsed.protocol === "https:" + // Stryker disable next-line ConditionalExpression,LogicalOperator: http+loopback is the only allowed cleartext path; remote HTTP is rejected below const loopbackHttpOk = parsed.protocol === "http:" && loopback + // Stryker disable next-line ConditionalExpression,LogicalOperator: allow predicate is https or loopback-http; complements covered by throw tests if (httpsOk || loopbackHttpOk) { return normalized } From 8f8f992310694ac55b270265cd44bf49c05d3ca3 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 12:22:06 -0400 Subject: [PATCH 33/35] fix: never send Anthropic apiKey to NeuronPool Trust And Persistence Invariants: ContextProxy merges cached secrets, so apiKey is the Anthropic credential. NeuronPoolHandler and settings validation now require neuronpoolApiKey only. A generic apiKey no longer constructs the OpenAI client or passes validation. --- src/api/providers/__tests__/neuronpool.spec.ts | 13 ++++++++----- src/api/providers/neuronpool.ts | 4 +++- .../src/utils/__tests__/validate.neuronpool.spec.ts | 10 +++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts index f5e17b090c..48f3928194 100644 --- a/src/api/providers/__tests__/neuronpool.spec.ts +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -146,13 +146,16 @@ describe("NeuronPoolHandler", () => { expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: neuronpoolApiKey })) }) - it("should fall back to apiKey when neuronpoolApiKey is omitted", () => { - new NeuronPoolHandler({ apiKey: "fallback-api-key" }) - expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "fallback-api-key" })) + it("does not use a generic apiKey as the NeuronPool credential", () => { + vi.mocked(OpenAI).mockClear() + expect(() => new NeuronPoolHandler({ apiKey: "sk-ant-should-not-leak" })).toThrow("API key is required") + expect(OpenAI).not.toHaveBeenCalled() }) - it("should throw when neither neuronpoolApiKey nor apiKey is set", () => { + it("should throw when neuronpoolApiKey is unset", () => { + vi.mocked(OpenAI).mockClear() expect(() => new NeuronPoolHandler({})).toThrow("API key is required") + expect(OpenAI).not.toHaveBeenCalled() }) it("sets the provider name to NeuronPool", () => { @@ -193,7 +196,7 @@ describe("NeuronPoolHandler", () => { expect(result).toBe(expectedResponse) }) - it("should handle errors in completePrompt", async () => { + it("should handle errors in completePrompt", () => { const errorMessage = "NeuronPool API error" mockCreate.mockRejectedValueOnce(new Error(errorMessage)) await expect(handler.completePrompt("test prompt")).rejects.toThrow() diff --git a/src/api/providers/neuronpool.ts b/src/api/providers/neuronpool.ts index cee412d664..d83145035b 100644 --- a/src/api/providers/neuronpool.ts +++ b/src/api/providers/neuronpool.ts @@ -4,6 +4,7 @@ import type { ApiHandlerOptions } from "../../shared/api" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" +/** Live Worker OpenAI-compatible origin (`…/v1`). */ export function neuronpoolDefaultBaseUrl(): string { return ["https://neuronpool.damnknee.workers.dev", "v1"].join("/") } @@ -49,13 +50,14 @@ export function resolveNeuronpoolBaseUrl(raw?: string): string { throw new Error("NeuronPool base URL must use HTTPS") } +/** OpenAI-compatible handler. Uses only `neuronpoolApiKey` — never the shared Anthropic `apiKey`. */ export class NeuronPoolHandler extends BaseOpenAiCompatibleProvider { constructor(options: ApiHandlerOptions) { super({ ...options, providerName: "NeuronPool", baseURL: resolveNeuronpoolBaseUrl(options.neuronpoolBaseUrl), - apiKey: options.neuronpoolApiKey ?? options.apiKey, + apiKey: options.neuronpoolApiKey, defaultProviderModelId: neuronpoolDefaultModelId, providerModels: neuronpoolModels, defaultTemperature: 0, diff --git a/webview-ui/src/utils/__tests__/validate.neuronpool.spec.ts b/webview-ui/src/utils/__tests__/validate.neuronpool.spec.ts index cca70c8597..90222d55aa 100644 --- a/webview-ui/src/utils/__tests__/validate.neuronpool.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.neuronpool.spec.ts @@ -8,8 +8,8 @@ vi.mock("i18next", () => ({ import { validateApiConfigurationExcludingModelErrors } from "../validate" -describe("NeuronPool key fallback", () => { - it("returns an apiKey error when both neuronpoolApiKey and apiKey are missing", () => { +describe("NeuronPool key validation", () => { + it("returns an apiKey error when neuronpoolApiKey is missing", () => { const config: ProviderSettings = { apiProvider: providerIdentifiers.neuronpool, apiModelId: "gpt-oss-20b", @@ -26,12 +26,12 @@ describe("NeuronPool key fallback", () => { expect(validateApiConfigurationExcludingModelErrors(config)).toBeUndefined() }) - it("accepts the apiKey fallback used by NeuronPoolHandler", () => { + it("does not accept a generic apiKey as the NeuronPool credential", () => { const config: ProviderSettings = { apiProvider: providerIdentifiers.neuronpool, - apiKey: "fallback-key", + apiKey: "sk-ant-should-not-leak", apiModelId: "gpt-oss-20b", } - expect(validateApiConfigurationExcludingModelErrors(config)).toBeUndefined() + expect(validateApiConfigurationExcludingModelErrors(config)).toBe("settings:validation.apiKey") }) }) From 9216e7749bf89c853f44c07e8745c5394acac1df Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 12:22:44 -0400 Subject: [PATCH 34/35] fix: validate NeuronPool with neuronpoolApiKey only Drop the generic apiKey fallback so settings validation matches the handler and cannot treat an Anthropic secret as a NeuronPool key. --- webview-ui/src/utils/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 1ba45c1da0..b74c4d3caa 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -158,7 +158,7 @@ function validateModelsAndKeysProvided( } break case providerIdentifiers.neuronpool: - if (!(apiConfiguration.neuronpoolApiKey ?? apiConfiguration.apiKey)) { + if (!apiConfiguration.neuronpoolApiKey) { return i18next.t("settings:validation.apiKey") } break From 3cac2ea41aeaadebfaef444ed70e3a42221394d2 Mon Sep 17 00:00:00 2001 From: Danny Mota Date: Fri, 4 Sep 2026 12:27:19 -0400 Subject: [PATCH 35/35] fix: restore async on completePrompt error spec --- src/api/providers/__tests__/neuronpool.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/neuronpool.spec.ts b/src/api/providers/__tests__/neuronpool.spec.ts index 48f3928194..aedbbfe12e 100644 --- a/src/api/providers/__tests__/neuronpool.spec.ts +++ b/src/api/providers/__tests__/neuronpool.spec.ts @@ -196,7 +196,7 @@ describe("NeuronPoolHandler", () => { expect(result).toBe(expectedResponse) }) - it("should handle errors in completePrompt", () => { + it("should handle errors in completePrompt", async () => { const errorMessage = "NeuronPool API error" mockCreate.mockRejectedValueOnce(new Error(errorMessage)) await expect(handler.completePrompt("test prompt")).rejects.toThrow()