Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/types/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ describe("GLOBAL_STATE_KEYS", () => {
expect(GLOBAL_STATE_KEYS).not.toContain("openRouterApiKey")
})

it("should not contain Umans API key (secret)", () => {
expect(GLOBAL_STATE_KEYS).not.toContain("umansApiKey")
})

it("should contain OpenAI Compatible base URL setting", () => {
expect(GLOBAL_STATE_KEYS).toContain("codebaseIndexOpenAiCompatibleBaseUrl")
})
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ export type RooCodeSettings = GlobalSettings & ProviderSettings
export const SECRET_STATE_KEYS = [
"apiKey",
"openRouterApiKey",
"umansApiKey",
"awsAccessKey",
"awsApiKey",
"awsSecretKey",
Expand Down
29 changes: 26 additions & 3 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3

export const dynamicProviders = [
"openrouter",
"umans",
"vercel-ai-gateway",
"zoo-gateway",
"litellm",
Expand Down Expand Up @@ -85,7 +86,7 @@ export const isInternalProvider = (key: string): key is InternalProvider =>
* Custom providers are completely configurable within Roo Code settings.
*/

export const customProviders = ["openai"] as const
export const customProviders = ["openai", "anthropic-custom"] as const

export type CustomProvider = (typeof customProviders)[number]

Expand Down Expand Up @@ -221,6 +222,11 @@ const openRouterSchema = baseProviderSettingsSchema.extend({
openRouterSpecificProvider: z.string().optional(),
})

const umansSchema = baseProviderSettingsSchema.extend({
umansApiKey: z.string().optional(),
umansModelId: z.string().optional(),
})

const bedrockSchema = apiModelIdProviderModelSchema.extend({
awsAccessKey: z.string().optional(),
awsSecretKey: z.string().optional(),
Expand Down Expand Up @@ -262,6 +268,15 @@ const openAiSchema = baseProviderSettingsSchema.extend({
openAiHeaders: z.record(z.string(), z.string()).optional(),
})

const anthropicCustomSchema = baseProviderSettingsSchema.extend({
anthropicCustomBaseUrl: z.string().optional(),
anthropicCustomApiKey: z.string().optional(),
anthropicCustomModelId: z.string().optional(),
anthropicCustomModelInfo: modelInfoSchema.nullish(),
anthropicCustomStreamingEnabled: z.boolean().optional(),
anthropicCustomHeaders: z.record(z.string(), z.string()).optional(),
})

const ollamaSchema = baseProviderSettingsSchema.extend({
ollamaModelId: z.string().optional(),
ollamaBaseUrl: z.string().optional(),
Expand Down Expand Up @@ -424,9 +439,11 @@ const defaultSchema = z.object({
export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
umansSchema.merge(z.object({ apiProvider: z.literal("umans") })),
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })),
openAiSchema.merge(z.object({ apiProvider: z.literal("openai") })),
anthropicCustomSchema.merge(z.object({ apiProvider: z.literal("anthropic-custom") })),
ollamaSchema.merge(z.object({ apiProvider: z.literal("ollama") })),
vsCodeLmSchema.merge(z.object({ apiProvider: z.literal("vscode-lm") })),
lmStudioSchema.merge(z.object({ apiProvider: z.literal("lmstudio") })),
Expand Down Expand Up @@ -460,9 +477,11 @@ export const providerSettingsSchema = z.object({
apiProvider: providerNamesWithRetiredSchema.optional(),
...anthropicSchema.shape,
...openRouterSchema.shape,
...umansSchema.shape,
...bedrockSchema.shape,
...vertexSchema.shape,
...openAiSchema.shape,
...anthropicCustomSchema.shape,
...ollamaSchema.shape,
...vsCodeLmSchema.shape,
...lmStudioSchema.shape,
Expand Down Expand Up @@ -511,7 +530,9 @@ export const PROVIDER_SETTINGS_KEYS = providerSettingsSchema.keyof().options
export const modelIdKeys = [
"apiModelId",
"openRouterModelId",
"umansModelId",
"openAiModelId",
"anthropicCustomModelId",
"ollamaModelId",
"lmStudioModelId",
"lmStudioDraftModelId",
Expand Down Expand Up @@ -542,6 +563,7 @@ export const isTypicalProvider = (key: unknown): key is TypicalProvider =>
export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
anthropic: "apiModelId",
openrouter: "openRouterModelId",
umans: "umansModelId",
bedrock: "apiModelId",
vertex: "apiModelId",
"openai-codex": "apiModelId",
Expand Down Expand Up @@ -575,7 +597,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
*/

// Providers that use Anthropic-style API protocol.
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock", "minimax"]
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "anthropic-custom", "bedrock", "minimax"]

export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) {
Expand Down Expand Up @@ -615,7 +637,7 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
*/

export const MODELS_BY_PROVIDER: Record<
Exclude<ProviderName, "fake-ai" | "gemini-cli" | "openai">,
Exclude<ProviderName, "fake-ai" | "gemini-cli" | "openai" | "anthropic-custom">,
{ id: ProviderName; label: string; models: string[] }
> = {
anthropic: {
Expand Down Expand Up @@ -697,6 +719,7 @@ export const MODELS_BY_PROVIDER: Record<
poe: { id: "poe", label: "Poe", models: [] },
litellm: { id: "litellm", label: "LiteLLM", models: [] },
openrouter: { id: "openrouter", label: "OpenRouter", models: [] },
umans: { id: "umans", label: "Umans", models: [] },
requesty: { id: "requesty", label: "Requesty", models: [] },
unbound: { id: "unbound", label: "Unbound", models: [] },
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from "./qwen-code.js"
export * from "./requesty.js"
export * from "./sambanova.js"
export * from "./unbound.js"
export * from "./umans.js"
export * from "./vertex.js"
export * from "./vscode-llm.js"
export * from "./xai.js"
Expand All @@ -44,6 +45,7 @@ import { qwenCodeDefaultModelId } from "./qwen-code.js"
import { requestyDefaultModelId } from "./requesty.js"
import { sambaNovaDefaultModelId } from "./sambanova.js"
import { unboundDefaultModelId } from "./unbound.js"
import { umansDefaultModelId } from "./umans.js"
import { vertexDefaultModelId } from "./vertex.js"
import { vscodeLlmDefaultModelId } from "./vscode-llm.js"
import { xaiDefaultModelId } from "./xai.js"
Expand Down Expand Up @@ -71,6 +73,8 @@ export function getProviderDefaultModelId(
return openRouterDefaultModelId
case "requesty":
return requestyDefaultModelId
case "umans":
return umansDefaultModelId
case "litellm":
return litellmDefaultModelId
case "xai":
Expand Down
18 changes: 18 additions & 0 deletions packages/types/src/providers/umans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ModelInfo } from "../model.js"

export const UMANS_DEFAULT_BASE_URL = "https://api.code.umans.ai/v1"

// Umans
// https://api.code.umans.ai/v1/models/info
export const umansDefaultModelId = "umans-coder"

export const umansDefaultModelInfo: ModelInfo = {
maxTokens: 32_768,
contextWindow: 262_144,
supportsImages: true,
supportsPromptCache: false,
supportsMaxTokens: true,
inputPrice: 0.95,
outputPrice: 4.0,
description: "Umans Coder is Umans' recommended model for complex, coding-heavy workloads and coding agents.",
}
4 changes: 4 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
VsCodeLmHandler,
RequestyHandler,
UnboundHandler,
UmansHandler,
FakeAIHandler,
XAIHandler,
LiteLLMHandler,
Expand Down Expand Up @@ -133,9 +134,12 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {

switch (apiProvider) {
case "anthropic":
case "anthropic-custom":
return new AnthropicHandler(options)
case "openrouter":
return new OpenRouterHandler(options)
case "umans":
return new UmansHandler(options)
case "bedrock":
return new AwsBedrockHandler(options)
case "vertex":
Expand Down
116 changes: 116 additions & 0 deletions src/api/providers/__tests__/umans.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// npx vitest run api/providers/__tests__/umans.spec.ts

vitest.mock("../utils/timeout-config", () => ({
getApiRequestTimeout: vitest.fn().mockReturnValue(300_000),
}))

const MOCK_TIMEOUT_MS = 300_000

import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"

import { UmansHandler } from "../umans"
import type { ApiHandlerOptions } from "../../../shared/api"
import { Package } from "../../../shared/package"

const mockCreate = vitest.fn()

vitest.mock("openai", () => ({
default: vitest.fn().mockImplementation(function () {
return {
chat: {
completions: {
create: mockCreate,
},
},
}
}),
}))

vitest.mock("../fetchers/modelCache", () => ({
getModels: vitest.fn().mockResolvedValue({
"umans-coder": {
maxTokens: 32768,
contextWindow: 262144,
supportsImages: true,
supportsPromptCache: false,
supportsMaxTokens: true,
inputPrice: 0.95,
outputPrice: 4,
description: "Umans Coder",
},
"umans-glm-5.2": {
maxTokens: 131071,
contextWindow: 405504,
supportsImages: true,
supportsPromptCache: false,
supportsMaxTokens: true,
supportsReasoningEffort: ["none", "high", "max"],
reasoningEffort: "high",
inputPrice: 1.4,
outputPrice: 4.4,
description: "Umans GLM 5.2",
},
}),
}))

describe("UmansHandler", () => {
const mockOptions: ApiHandlerOptions = {
umansApiKey: "test-key",
umansModelId: "umans-coder",
}

beforeEach(() => vitest.clearAllMocks())

it("initializes with the Umans base URL and API key", () => {
new UmansHandler(mockOptions)

expect(OpenAI).toHaveBeenCalledWith({
baseURL: "https://api.code.umans.ai/v1",
apiKey: "test-key",
defaultHeaders: {
"HTTP-Referer": "https://github.com/Zoo-Code-Org/Zoo-Code",
"X-Title": "Zoo Code",
"User-Agent": `ZooCode/${Package.version}`,
},
timeout: MOCK_TIMEOUT_MS,
})
})

it("returns the default model when no options are provided", async () => {
const handler = new UmansHandler({})
const result = await handler.fetchModel()

expect(result.id).toBe("umans-coder")
expect(result.info.description).toBe("Umans Coder")
})

it("uses the provider's default OpenAI reasoning payload for Umans GLM models", async () => {
const handler = new UmansHandler({
umansApiKey: "test-key",
umansModelId: "umans-glm-5.2",
reasoningEffort: "max",
})

const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
choices: [{ delta: { content: "done" } }],
}
},
}

mockCreate.mockResolvedValue(mockStream)

const generator = handler.createMessage("system prompt", [{ role: "user" as const, content: "test" }])
await generator.next()

expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "umans-glm-5.2",
reasoning_effort: "max",
stream: true,
}),
)
})
})
23 changes: 17 additions & 6 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type AnthropicModelId,
anthropicDefaultModelId,
anthropicModels,
openAiModelInfoSaneDefaults,
ANTHROPIC_DEFAULT_MAX_TOKENS,
ApiProviderError,
} from "@roo-code/types"
Expand Down Expand Up @@ -38,12 +39,17 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
super()
this.options = options

const baseURL = this.options.anthropicCustomBaseUrl || this.options.anthropicBaseUrl || undefined
const apiKey = this.options.anthropicCustomApiKey || this.options.apiKey
const apiKeyFieldName =
this.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? "authToken" : "apiKey"
baseURL && this.options.anthropicUseAuthToken && !this.options.anthropicCustomApiKey
? "authToken"
: "apiKey"

this.client = new Anthropic({
baseURL: this.options.anthropicBaseUrl || undefined,
[apiKeyFieldName]: this.options.apiKey,
baseURL,
[apiKeyFieldName]: apiKey,
defaultHeaders: this.options.anthropicCustomHeaders,
Comment on lines +42 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate anthropicCustom* overrides to the anthropic-custom provider path.

buildApiHandler constructs AnthropicHandler(options) for both "anthropic" and "anthropic-custom" after removing apiProvider, so these unconditional preferences can make a normal Anthropic profile use stale custom base URL/API key/headers/model settings. Pass an explicit custom-mode flag or retain apiProvider in the handler options before applying these fields.

Possible direction
- case "anthropic":
- case "anthropic-custom":
-   return new AnthropicHandler(options)
+ case "anthropic":
+   return new AnthropicHandler(options, { useCustomAnthropic: false })
+ case "anthropic-custom":
+   return new AnthropicHandler(options, { useCustomAnthropic: true })
- const baseURL = this.options.anthropicCustomBaseUrl || this.options.anthropicBaseUrl || undefined
- const apiKey = this.options.anthropicCustomApiKey || this.options.apiKey
+ const baseURL = useCustomAnthropic
+   ? this.options.anthropicCustomBaseUrl || undefined
+   : this.options.anthropicBaseUrl || undefined
+ const apiKey = useCustomAnthropic ? this.options.anthropicCustomApiKey : this.options.apiKey

Also applies to: 361-368

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/anthropic.ts` around lines 42 - 52, The Anthropic client
setup is applying anthropicCustom* overrides unconditionally, so normal
anthropic profiles can inherit stale custom base URL, API key, headers, or model
settings. Update Anthrop icHandler/buildApiHandler handling so these overrides
are only used for the anthropic-custom path, either by preserving apiProvider in
the handler options or by passing an explicit custom-mode flag before
constructing the Anthropic client in anthropic.ts and any related model
selection logic.

timeout: this.timeoutMs,
})
}
Expand Down Expand Up @@ -352,9 +358,14 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
}

getModel() {
const modelId = this.options.apiModelId
const id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId
let info: ModelInfo = anthropicModels[id]
const customModelId = this.options.anthropicCustomModelId
const modelId = customModelId || this.options.apiModelId
const id =
customModelId ||
(modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId)
let info: ModelInfo = customModelId
? this.options.anthropicCustomModelInfo || openAiModelInfoSaneDefaults
: anthropicModels[id as AnthropicModelId]
Comment on lines +366 to +368

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect getModelParams handling for ModelInfo.maxTokens === -1.
rg -n -C 8 'function getModelParams|const getModelParams|export .*getModelParams|maxTokens' src packages

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 50378


🏁 Script executed:

ast-grep outline src/api/providers/anthropic.ts --view expanded

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 553


🏁 Script executed:

rg -n -C 6 'getModelParams|maxTokens\s*[:=]|max_tokens|anthropicCustomModelInfo|openAiModelInfoSaneDefaults' src packages/types

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 50378


🏁 Script executed:

sed -n '340,470p' src/api/providers/anthropic.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2856


🏁 Script executed:

rg -n -C 6 'getModelParams|maxTokens|max_tokens|anthropicCustomModelInfo|openAiModelInfoSaneDefaults' src/api/providers/anthropic.ts packages/types/src/providers/openai.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 44934


🏁 Script executed:

ast-grep outline src/api/transform/model-params.ts --view expanded

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 625


🏁 Script executed:

sed -n '1,260p' src/api/transform/model-params.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 6545


🏁 Script executed:

ast-grep outline src/shared/api.ts --view expanded

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 483


🏁 Script executed:

rg -n -C 8 'function getModelMaxOutputTokens|getModelMaxOutputTokens|maxTokens === -1|modelMaxTokens' src/shared packages/types

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 30796


🏁 Script executed:

sed -n '120,180p' src/shared/api.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2536


Use an Anthropic-safe fallback for custom models. openAiModelInfoSaneDefaults.maxTokens is -1, and getModelMaxOutputTokens() will pass that through to max_tokens for anthropicCustomModelId when no custom model info is provided. Default to ANTHROPIC_DEFAULT_MAX_TOKENS or clamp negative values before building the request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/anthropic.ts` around lines 366 - 368, The custom-model
fallback in anthropic.ts currently uses openAiModelInfoSaneDefaults, which can
leave maxTokens at -1 and flow into max_tokens for anthropicCustomModelId.
Update the ModelInfo selection in the Anthropic provider to use an
Anthropic-safe default, such as ANTHROPIC_DEFAULT_MAX_TOKENS, or clamp any
negative maxTokens before getModelMaxOutputTokens() builds the request. Keep the
fix localized around the customModelId branch and the Anthropic model info
lookup.


// If 1M context beta is enabled for supported models, update the model info
if (
Expand Down
Loading
Loading