Skip to content
Merged
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
27 changes: 24 additions & 3 deletions packages/config-eslint/provider-identifiers.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function createProviderIdentifierConfig({ providerIdentifiers, retiredPro
return undefined
}

function getProviderExpressionBranches(node) {
function getProviderExpressionChildren(node) {
node = unwrapExpression(node)

if (node?.type === "LogicalExpression") {
Expand All @@ -95,6 +95,18 @@ export function createProviderIdentifierConfig({ providerIdentifiers, retiredPro
return [node.right]
}

if (node?.type === "CallExpression") {
return node.arguments
}
Comment thread
WebMad marked this conversation as resolved.

if (node?.type === "ArrayExpression") {
return node.elements.filter((element) => element !== null)
Comment thread
WebMad marked this conversation as resolved.
}

if (node?.type === "SpreadElement") {
return [node.argument]
}

return []
}

Expand All @@ -118,9 +130,18 @@ export function createProviderIdentifierConfig({ providerIdentifiers, retiredPro
},
},
create(context) {
// Parent provider contexts and call visitors can reach the same expression.
const visitedExpressions = new WeakSet()

function reportIfRawProvider(node) {
for (const branch of getProviderExpressionBranches(node)) {
reportIfRawProvider(branch)
node = unwrapExpression(node)
if (!node || visitedExpressions.has(node)) {
return
}
visitedExpressions.add(node)

for (const child of getProviderExpressionChildren(node)) {
reportIfRawProvider(child)
}

const provider = getRawProvider(node)
Expand Down
38 changes: 38 additions & 0 deletions packages/config-eslint/provider-identifiers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,38 @@ ruleTester.run("no-raw-provider-identifiers provider-like values", rule, {
"const provider = configuredProvider || providerIdentifiers.openrouter",
"const apiProvider = useGemini ? providerIdentifiers.gemini : providerIdentifiers.openrouter",
"getProviderServiceConfig(providerIdentifiers.gemini)",
"const provider = getProvider(providerIdentifiers.openrouter)",
"const schema = { imageGenerationProvider: z.enum([...knownValues, ...[providerIdentifiers.openrouter]]) }",
'if (config?.protocol === "gemini") {}',
'const response = { protocol: "anthropic", format: "openrouter" }',
],
invalid: [
...[
'const provider = getProvider("openrouter")',
'const provider = getProvider(getProvider("openrouter"))',
'const provider = getProvider("openrouter" as const)',
'getProvider(...["openrouter"])',
'const config = { apiProvider: getProvider("openrouter") }',
'const schema = { imageGenerationProvider: z.enum([...["openrouter"]]) }',
'const schema = { imageGenerationProvider: z.enum([, ...[...["openrouter"]]]) }',
].map((code) => ({
code,
errors: [
{
messageId: "useCanonical",
data: { replacement: "providerIdentifiers.openrouter", value: "openrouter" },
type: "Literal",
},
],
})),
{
code: 'const provider = getProvider("openrouter", "openrouter")',
errors: Array.from({ length: 2 }, () => ({
messageId: "useCanonical",
data: { replacement: "providerIdentifiers.openrouter", value: "openrouter" },
type: "Literal",
})),
},
{
code: 'const apiProvider = "roo"',
errors: [
Expand Down Expand Up @@ -200,6 +228,16 @@ ruleTester.run("no-raw-provider-identifiers provider-like values", rule, {
},
],
},
{
code: 'const schema = { imageGenerationProvider: z.enum(["openrouter"]) }',
errors: [
{
messageId: "useCanonical",
data: { replacement: "providerIdentifiers.openrouter", value: "openrouter" },
type: "Literal",
},
],
},
{
code: 'const imageProvider = useGemini ? "gemini" : "openrouter"',
errors: [
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/__tests__/provider-identifiers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
retiredProviderNamesSchema,
} from "../index.js"

// Raw values are intentional here: these fixtures protect the persisted provider identifier contract.
/* eslint-disable zoo/no-raw-provider-identifiers */
const expectedProviderIdentifiers = [
"openrouter",
"vercel-ai-gateway",
Expand Down Expand Up @@ -70,6 +72,7 @@ const expectedRetiredProviderIdentifiers = [
"io-intelligence",
"roo",
]
/* eslint-enable zoo/no-raw-provider-identifiers */

describe("provider identifiers", () => {
it("preserves active provider serialized values", () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { telemetrySettingsSchema } from "./telemetry.js"
import { toolNamesSchema } from "./tool.js"
import { type Keys } from "./type-fu.js"
import { languagesSchema } from "./vscode.js"
import { providerIdentifiers } from "./provider-identifiers.js"

/**
* Default delay in milliseconds after writes to allow diagnostics to detect potential problems.
Expand Down Expand Up @@ -114,7 +115,7 @@ export const globalSettingsSchema = z.object({
dismissedUpsells: z.array(z.string()).optional(),

// Image generation settings (experimental) - flattened for simplicity
imageGenerationProvider: z.enum(["openrouter"]).optional(),
imageGenerationProvider: z.enum([providerIdentifiers.openrouter]).optional(),
openRouterImageApiKey: z.string().optional(),
openRouterImageGenerationSelectedModel: z.string().optional(),

Expand Down
10 changes: 8 additions & 2 deletions src/api/providers/vscode-lm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import OpenAI from "openai"

import { type ModelInfo, openAiModelInfoSaneDefaults, vscodeLlmDefaultModelId, vscodeLlmModels } from "@roo-code/types"
import {
type ModelInfo,
openAiModelInfoSaneDefaults,
providerIdentifiers,
vscodeLlmDefaultModelId,
vscodeLlmModels,
} from "@roo-code/types"

import type { ApiHandlerOptions } from "../../shared/api"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
Expand Down Expand Up @@ -555,7 +561,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
// Fallback when no client is available
const fallbackId = this.options.vsCodeLmModelSelector
? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
: "vscode-lm"
: providerIdentifiers.vscodeLm

console.debug("Zoo Code <Language Model API>: No client available, using fallback model info")

Expand Down
52 changes: 52 additions & 0 deletions src/core/config/__tests__/importExport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,58 @@ describe("importExport", () => {
consoleWarnSpy.mockRestore()
})

it.each([
{ imageGenerationProvider: providerIdentifiers.openrouter, expectedWarnings: undefined },
{
imageGenerationProvider: retiredProviderIdentifiers.roo,
expectedWarnings: [
'Setting "globalSettings.imageGenerationProvider" used unsupported value "roo" and was cleared during import.',
],
},
])(
"only clears the retired image provider: $imageGenerationProvider",
async ({ imageGenerationProvider, expectedWarnings }) => {
;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
;(fs.readFile as Mock).mockResolvedValue(
JSON.stringify({
providerProfiles: {
currentApiConfigName: "valid-profile",
apiConfigs: {
"valid-profile": { apiProvider: providerIdentifiers.openai, id: "valid-id" },
},
},
globalSettings: {
imageGenerationProvider,
customInstructions: "roo",
},
}),
)
mockProviderSettingsManager.export.mockResolvedValue({
currentApiConfigName: "default",
apiConfigs: { default: { apiProvider: providerIdentifiers.anthropic, id: "default-id" } },
})
mockProviderSettingsManager.listConfig.mockResolvedValue([
{ name: "valid-profile", id: "valid-id", apiProvider: providerIdentifiers.openai },
])

const result = await importSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
customModesManager: mockCustomModesManager,
})

expect(result.success).toBe(true)
expect(result.warnings).toEqual(expectedWarnings)
expect(mockContextProxy.setValues).toHaveBeenCalledTimes(1)
expect(mockContextProxy.setValues).toHaveBeenCalledWith(
expect.objectContaining({
imageGenerationProvider: expectedWarnings ? undefined : providerIdentifiers.openrouter,
customInstructions: "roo",
}),
)
},
)

it("should normalize imageGenerationProvider roo while preserving other global settings", async () => {
;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])

Expand Down
3 changes: 2 additions & 1 deletion src/core/config/importExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
globalSettingsSchema,
providerSettingsWithIdSchema,
isProviderName,
retiredProviderIdentifiers,
type GlobalSettings,
type ProviderSettingsWithId,
} from "@roo-code/types"
Expand Down Expand Up @@ -106,7 +107,7 @@ function sanitizeGlobalSettings(rawGlobalSettings: unknown): {

let valueToValidate = rawValue

if (key === "imageGenerationProvider" && rawValue === "roo") {
if (key === "imageGenerationProvider" && rawValue === retiredProviderIdentifiers.roo) {
warnings.push(`Setting "${path}" used unsupported value "roo" and was cleared during import.`)
valueToValidate = undefined
}
Expand Down
7 changes: 4 additions & 3 deletions src/integrations/kimi-code/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ExtensionContext } from "vscode"
import { z } from "zod"
import { providerIdentifiers } from "@roo-code/types"

export const KIMI_CODE_OAUTH_CONFIG = {
authHost: "https://auth.kimi.com",
Expand All @@ -16,7 +17,7 @@ const TOKEN_EXPIRY_BUFFER_MS = 60_000
const OAUTH_REQUEST_TIMEOUT_MS = 30_000

const credentialsSchema = z.object({
type: z.literal("kimi-code"),
type: z.literal(providerIdentifiers.kimiCode),
accessToken: z.string().min(1),
refreshToken: z.string().min(1),
expiresAt: z.number(),
Expand Down Expand Up @@ -130,7 +131,7 @@ async function requestDeviceToken(deviceCode: string, signal?: AbortSignal): Pro
const tokens = tokenResponseSchema.parse(await response.json())
if (!tokens.refresh_token) throw new Error("Kimi Code OAuth did not return a refresh token")
return {
type: "kimi-code",
type: providerIdentifiers.kimiCode,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,
Expand All @@ -147,7 +148,7 @@ export async function refreshKimiCodeAccessToken(credentials: KimiCodeCredential
if (!response.ok) throw await readOAuthError(response)
const tokens = tokenResponseSchema.parse(await response.json())
return {
type: "kimi-code",
type: providerIdentifiers.kimiCode,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token ?? credentials.refreshToken,
expiresAt: Date.now() + tokens.expires_in * 1000,
Expand Down
7 changes: 4 additions & 3 deletions src/integrations/openai-codex/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as http from "http"
import { URL } from "url"
import type { ExtensionContext } from "vscode"
import { z } from "zod"
import { providerIdentifiers } from "@roo-code/types"

/**
* OpenAI Codex OAuth Configuration
Expand All @@ -28,7 +29,7 @@ const OPENAI_CODEX_CREDENTIALS_KEY = "openai-codex-oauth-credentials"

// Credentials schema
const openAiCodexCredentialsSchema = z.object({
type: z.literal("openai-codex"),
type: z.literal(providerIdentifiers.openaiCodex),
access_token: z.string().min(1),
refresh_token: z.string().min(1),
// expires is in milliseconds since epoch
Expand Down Expand Up @@ -264,7 +265,7 @@ export async function exchangeCodeForTokens(code: string, codeVerifier: string):
})

return {
type: "openai-codex",
type: providerIdentifiers.openaiCodex,
access_token: tokenResponse.access_token,
refresh_token: tokenResponse.refresh_token,
expires: expiresAt,
Expand Down Expand Up @@ -316,7 +317,7 @@ export async function refreshAccessToken(credentials: OpenAiCodexCredentials): P
})

return {
type: "openai-codex",
type: providerIdentifiers.openaiCodex,
access_token: tokenResponse.access_token,
refresh_token: tokenResponse.refresh_token ?? credentials.refresh_token,
expires: expiresAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const ImageGenerationSettings = ({
value={currentProvider}
onChange={(e: any) => handleProviderChange(e.target.value)}
className="w-full">
<VSCodeOption value="openrouter" className="py-2 px-3">
<VSCodeOption value={providerIdentifiers.openrouter} className="py-2 px-3">
OpenRouter
</VSCodeOption>
</VSCodeDropdown>
Expand Down
51 changes: 51 additions & 0 deletions webview-ui/src/oauth/__tests__/urls.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Package } from "@roo/package"

import { getCallbackUrl, getOpenRouterAuthUrl, getRequestyAuthUrl, getZooCodeAuthUrl } from "../urls"

describe("OAuth URLs", () => {
it.each([undefined, "", "vscode-insiders", "cursor"])("encodes callback URLs for scheme %s", (scheme) => {
expect(getCallbackUrl("auth-callback", scheme)).toBe(
encodeURIComponent(`${scheme || "vscode"}://${Package.publisher}.${Package.name}/auth-callback`),
)
})

it.each([
[getOpenRouterAuthUrl, "https://openrouter.ai/auth", "openrouter"],
[getRequestyAuthUrl, "https://app.requesty.ai/oauth/authorize", "requesty"],
] as const)("preserves the endpoint and provider callback for %s", (getAuthUrl, endpoint, identifier) => {
for (const scheme of [undefined, "", "vscode-insiders", "cursor"]) {
const callback = `${scheme || "vscode"}://${Package.publisher}.${Package.name}/${identifier}`
expect(getAuthUrl(scheme)).toBe(`${endpoint}?callback_url=${encodeURIComponent(callback)}`)
expect(new URL(getAuthUrl(scheme)).searchParams.get("callback_url")).toBe(callback)
}
})

it("uses default Zoo connection settings", () => {
const url = new URL(getZooCodeAuthUrl())
expect(url.origin + url.pathname).toBe("https://www.zoocode.dev/dashboard/connect")
expect(Object.fromEntries(url.searchParams)).toEqual({
device: "VS Code",
editor: "VS Code",
version: Package.version,
callback_uri: `vscode://${Package.publisher}.${Package.name}/auth-callback`,
})
expect(getZooCodeAuthUrl()).toContain(
`callback_uri=${encodeURIComponent(`vscode://${Package.publisher}.${Package.name}/auth-callback`)}`,
)
expect(getZooCodeAuthUrl("", "", "")).toBe(getZooCodeAuthUrl())
})

it("encodes custom device names and uses the supplied Zoo host and editor scheme", () => {
const url = new URL(getZooCodeAuthUrl("cursor", "https://example.com", "Work & Home / ноутбук"))
expect(url.origin + url.pathname).toBe("https://example.com/dashboard/connect")
expect(Object.fromEntries(url.searchParams)).toEqual({
device: "Work & Home / ноутбук",
editor: "VS Code",
version: Package.version,
callback_uri: `cursor://${Package.publisher}.${Package.name}/auth-callback`,
})
expect(getZooCodeAuthUrl("cursor")).toContain(
`callback_uri=${encodeURIComponent(`cursor://${Package.publisher}.${Package.name}/auth-callback`)}`,
)
})
})
5 changes: 3 additions & 2 deletions webview-ui/src/oauth/urls.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { providerIdentifiers } from "@roo-code/types"
import { Package } from "@roo/package"

export function getCallbackUrl(provider: string, uriScheme?: string) {
return encodeURIComponent(`${uriScheme || "vscode"}://${Package.publisher}.${Package.name}/${provider}`)
}

export function getOpenRouterAuthUrl(uriScheme?: string) {
return `https://openrouter.ai/auth?callback_url=${getCallbackUrl("openrouter", uriScheme)}`
return `https://openrouter.ai/auth?callback_url=${getCallbackUrl(providerIdentifiers.openrouter, uriScheme)}`
}

export function getRequestyAuthUrl(uriScheme?: string) {
return `https://app.requesty.ai/oauth/authorize?callback_url=${getCallbackUrl("requesty", uriScheme)}`
return `https://app.requesty.ai/oauth/authorize?callback_url=${getCallbackUrl(providerIdentifiers.requesty, uriScheme)}`
}

const ZOO_CODE_DEFAULT_BASE_URL = "https://www.zoocode.dev"
Expand Down
Loading