Skip to content
Closed
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
9 changes: 8 additions & 1 deletion packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const providerNames = [
"doubao",
"unbound",
"requesty",
"qwen-code",
"human-relay",
"fake-ai",
"xai",
Expand Down Expand Up @@ -311,6 +312,10 @@ const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
ioIntelligenceApiKey: z.string().optional(),
})

const qwenCodeSchema = apiModelIdProviderModelSchema.extend({
qwenCodeOAuthPath: z.string().optional(),
})

const rooSchema = apiModelIdProviderModelSchema.extend({
// No additional fields needed - uses cloud authentication
})
Expand Down Expand Up @@ -352,6 +357,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })),
ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
defaultSchema,
])
Expand Down Expand Up @@ -390,6 +396,7 @@ export const providerSettingsSchema = z.object({
...fireworksSchema.shape,
...featherlessSchema.shape,
...ioIntelligenceSchema.shape,
...qwenCodeSchema.shape,
...rooSchema.shape,
...codebaseIndexProviderSchema.shape,
})
Expand Down Expand Up @@ -440,7 +447,7 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
}

export const MODELS_BY_PROVIDER: Record<
Exclude<ProviderName, "fake-ai" | "human-relay" | "gemini-cli" | "lmstudio" | "openai" | "ollama">,
Exclude<ProviderName, "fake-ai" | "human-relay" | "gemini-cli" | "lmstudio" | "openai" | "ollama" | "qwen-code">,
{ id: ProviderName; label: string; models: string[] }
> = {
anthropic: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from "./moonshot.js"
export * from "./ollama.js"
export * from "./openai.js"
export * from "./openrouter.js"
export * from "./qwen-code.js"
export * from "./requesty.js"
export * from "./roo.js"
export * from "./sambanova.js"
Expand Down
44 changes: 44 additions & 0 deletions packages/types/src/providers/qwen-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { ModelInfo } from "../model.js"
import type { ProviderName } from "../provider-settings.js"

export const qwenCodeModels = {
"qwen3-coder-plus": {
id: "qwen3-coder-plus",
name: "Qwen3 Coder Plus",
provider: "qwen-code" as ProviderName,
contextWindow: 1000000,
maxTokens: 65536,
supportsPromptCache: false,
},
"qwen3-coder-flash": {
id: "qwen3-coder-flash",
name: "Qwen3 Coder Flash",
provider: "qwen-code" as ProviderName,
contextWindow: 1000000,
maxTokens: 65536,
supportsPromptCache: false,
},
} as const

export type QwenCodeModelId = keyof typeof qwenCodeModels

export const qwenCodeDefaultModelId: QwenCodeModelId = "qwen3-coder-plus"

export const isQwenCodeModel = (modelId: string): modelId is QwenCodeModelId => {
return modelId in qwenCodeModels
}

export const getQwenCodeModelInfo = (modelId: string): ModelInfo => {
if (isQwenCodeModel(modelId)) {
return qwenCodeModels[modelId]
}
// Fallback to a default or throw an error
return qwenCodeModels[qwenCodeDefaultModelId]
}

export type QwenCodeProvider = {
Copy link
Contributor

Choose a reason for hiding this comment

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

The QwenCodeProvider type includes apiKey and baseUrl fields, but this is an OAuth-based provider that doesn't use API keys. Should these fields be removed to avoid confusion?

id: "qwen-code"
apiKey?: string
baseUrl?: string
model: QwenCodeModelId
}
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
OpenAiHandler,
LmStudioHandler,
GeminiHandler,
QwenCodeHandler,
OpenAiNativeHandler,
DeepSeekHandler,
MoonshotHandler,
Expand Down Expand Up @@ -148,6 +149,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new RooHandler(options)
case "featherless":
return new FeatherlessHandler(options)
case "qwen-code":
return new QwenCodeHandler(options)
default:
apiProvider satisfies "gemini-cli" | undefined
return new AnthropicHandler(options)
Expand Down
54 changes: 54 additions & 0 deletions src/api/providers/__tests__/qwen-code.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, it, expect, vi } from "vitest"
import { QwenCodeHandler } from "../qwen-code"
import { ApiHandlerOptions } from "../../../shared/api"

// Mock fs
vi.mock("fs", () => ({
existsSync: vi.fn(),
readFileSync: vi.fn(),
}))

// Mock os
vi.mock("os", () => ({
homedir: () => "/home/user",
}))

// Mock path
vi.mock("path", () => ({
resolve: vi.fn((...args) => args.join("/")),
join: vi.fn((...args) => args.join("/")),
}))

describe("QwenCodeHandler", () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The test coverage seems insufficient. Consider adding tests for:

  • OAuth authentication flow
  • Token refresh logic
  • API call retry on 401 errors
  • Error handling scenarios
  • The actual createMessage and completePrompt methods

Would you like me to suggest some additional test cases?

it("should initialize with correct model configuration", () => {
const options: ApiHandlerOptions = {
apiModelId: "qwen3-coder-plus",
}
const handler = new QwenCodeHandler(options)

const model = handler.getModel()
expect(model.id).toBe("qwen3-coder-plus")
expect(model.info).toBeDefined()
expect(model.info?.supportsPromptCache).toBe(false)
})

it("should use default model when none specified", () => {
const options: ApiHandlerOptions = {}
const handler = new QwenCodeHandler(options)

const model = handler.getModel()
expect(model.id).toBe("qwen3-coder-plus") // default model
expect(model.info).toBeDefined()
})

it("should use custom oauth path when provided", () => {
const customPath = "/custom/path/oauth.json"
const options: ApiHandlerOptions = {
qwenCodeOAuthPath: customPath,
}
const handler = new QwenCodeHandler(options)

// Handler should initialize without throwing
expect(handler).toBeDefined()
})
})
1 change: 1 addition & 0 deletions src/api/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export { OllamaHandler } from "./ollama"
export { OpenAiNativeHandler } from "./openai-native"
export { OpenAiHandler } from "./openai"
export { OpenRouterHandler } from "./openrouter"
export { QwenCodeHandler } from "./qwen-code"
export { RequestyHandler } from "./requesty"
export { SambaNovaHandler } from "./sambanova"
export { UnboundHandler } from "./unbound"
Expand Down
Loading
Loading