diff --git a/packages/types/src/__tests__/lite-llm.test.ts b/packages/types/src/__tests__/lite-llm.test.ts index 74436c1848..d276333aa5 100644 --- a/packages/types/src/__tests__/lite-llm.test.ts +++ b/packages/types/src/__tests__/lite-llm.test.ts @@ -20,7 +20,7 @@ describe("LiteLLM preserveReasoning model detection", () => { it("matches case-insensitively", () => { expect(isLiteLLMPreserveReasoningModel("MiniMax-M2.7-Highspeed")).toBe(true) - expect(isLiteLLMPreserveReasoningModel("GLM-5.2")).toBe(true) + expect(isLiteLLMPreserveReasoningModel("GLM-5.3")).toBe(true) }) it("does not match model ids that merely contain a known family as a substring", () => { diff --git a/packages/types/src/__tests__/opencode-go.test.ts b/packages/types/src/__tests__/opencode-go.test.ts index 8079fe1bcb..99376e3c35 100644 --- a/packages/types/src/__tests__/opencode-go.test.ts +++ b/packages/types/src/__tests__/opencode-go.test.ts @@ -21,6 +21,7 @@ describe("opencode-go registry", () => { "glm-5", "glm-5.1", "glm-5.2", + "glm-5.3", "kimi-k3", "kimi-k2.5", "kimi-k2.6", @@ -78,6 +79,22 @@ describe("opencode-go registry", () => { expect(info?.outputPrice).toBe(15.0) expect(info?.cacheReadsPrice).toBe(0.3) }) + + it("glm-5.3 exposes its native context, pricing, and always-on reasoning levels", () => { + const info = getOpencodeGoModelInfo("glm-5.3") + expect(info).toBeDefined() + expect(info?.maxTokens).toBe(131_072) + expect(info?.contextWindow).toBe(1_000_000) + expect(info?.supportsImages).toBe(false) + expect(info?.supportsPromptCache).toBe(true) + expect(info?.supportsMaxTokens).toBe(true) + expect(info?.supportsReasoningEffort).toEqual(["low", "high", "max"]) + expect(info?.reasoningEffort).toBe("max") + expect(info?.preserveReasoning).toBe(true) + expect(info?.inputPrice).toBe(1.4) + expect(info?.outputPrice).toBe(4.4) + expect(info?.cacheReadsPrice).toBe(0.26) + }) }) describe("OPENCODE_GO_ANTHROPIC_FORMAT_MODELS", () => { diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 33fa4e3aac..ddf1ff4f5d 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -139,7 +139,7 @@ describe("getApiProtocol", () => { }) it("should return 'openai' for opencode-go OpenAI-format models (GLM/DeepSeek/etc.)", () => { - expect(getApiProtocol(providerIdentifiers.opencodeGo, "glm-5.2")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "glm-5.3")).toBe(OPENAI_API_PROTOCOL) expect(getApiProtocol(providerIdentifiers.opencodeGo, "deepseek-v4-pro")).toBe(OPENAI_API_PROTOCOL) expect(getApiProtocol(providerIdentifiers.opencodeGo, "kimi-k2.5")).toBe(OPENAI_API_PROTOCOL) expect(getApiProtocol(providerIdentifiers.opencodeGo, "mimo-v2.5")).toBe(OPENAI_API_PROTOCOL) diff --git a/packages/types/src/providers/lite-llm.ts b/packages/types/src/providers/lite-llm.ts index 2b72de9d51..36277774d7 100644 --- a/packages/types/src/providers/lite-llm.ts +++ b/packages/types/src/providers/lite-llm.ts @@ -56,6 +56,7 @@ export const LITELLM_PRESERVE_REASONING_MODEL_IDS = [ "glm-5", "glm-5.1", "glm-5.2", + "glm-5.3", "glm-5-turbo", // bedrock.ts, minimax.ts, opencode-go.ts diff --git a/packages/types/src/providers/opencode-go.ts b/packages/types/src/providers/opencode-go.ts index bd60c4d349..77e89fb80f 100644 --- a/packages/types/src/providers/opencode-go.ts +++ b/packages/types/src/providers/opencode-go.ts @@ -86,6 +86,21 @@ export const opencodeGoModels: Record = { description: "GLM-5.1 is Zhipu's most capable model with a 200k context window, 128k max output, and built-in thinking capabilities. Available via the Opencode Go plan.", }, + "glm-5.3": { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "high", "max"], + reasoningEffort: "max", + preserveReasoning: true, + inputPrice: 1.4, + outputPrice: 4.4, + cacheReadsPrice: 0.26, + description: + "GLM-5.3 is Zhipu's flagship coding and agent model with a 1M context window, 128k max output, and always-on reasoning with configurable effort (Low/High/Max). Available via the Opencode Go plan.", + }, "glm-5.2": { maxTokens: 131_072, contextWindow: 1_000_000, diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index c2bd079264..a14edfe9e3 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -5,6 +5,7 @@ import { ZaiApiLine } from "../provider-settings.js" // https://docs.z.ai/guides/llm/glm-4-32b-0414-128k // https://docs.z.ai/guides/llm/glm-4.5 // https://docs.z.ai/guides/llm/glm-4.6 +// https://docs.z.ai/guides/llm/glm-5.3 // https://docs.z.ai/guides/llm/glm-5.1 // https://docs.z.ai/guides/llm/glm-5-turbo // https://docs.z.ai/guides/overview/pricing @@ -473,6 +474,42 @@ export const mainlandZAiModels = { }, } as const satisfies Record +const glm53CodingPlanModelInfo = { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "high", "max"], + requiredReasoningEffort: true, + reasoningEffort: "max", + preserveReasoning: true, + description: + "GLM-5.3 is Zhipu's flagship coding and agent model with a 1M context window, 128k max output, and always-on reasoning with configurable effort (Low/High/Max). Available to GLM Coding Plan users.", +} as const satisfies ModelInfo + +export const internationalZAiCodingPlanOnlyModels = { + "glm-5.3": { + ...glm53CodingPlanModelInfo, + // GLM-5.3 API pricing is not published yet; use GLM-5.2 pricing provisionally. + inputPrice: 1.4, + outputPrice: 4.4, + cacheWritesPrice: 0, + cacheReadsPrice: 0.26, + }, +} as const satisfies Record + +export const mainlandZAiCodingPlanOnlyModels = { + "glm-5.3": { + ...glm53CodingPlanModelInfo, + // GLM-5.3 API pricing is not published yet; use GLM-5.2 pricing provisionally. + inputPrice: 0.68, + outputPrice: 2.28, + cacheWritesPrice: 0, + cacheReadsPrice: 0.13, + }, +} as const satisfies Record + export const ZAI_DEFAULT_TEMPERATURE = 0.6 export const zaiApiLineConfigs = { @@ -497,3 +534,10 @@ export const zaiApiLineConfigs = { isChina: true, }, } satisfies Record + +export function getZAiModels(apiLine: ZaiApiLine = "international_coding"): Record { + const isChina = zaiApiLineConfigs[apiLine].isChina + const regionalModels = isChina ? mainlandZAiModels : internationalZAiModels + const codingPlanOnlyModels = isChina ? mainlandZAiCodingPlanOnlyModels : internationalZAiCodingPlanOnlyModels + return apiLine.endsWith("_coding") ? { ...regionalModels, ...codingPlanOnlyModels } : regionalModels +} diff --git a/src/api/providers/__tests__/opencode-go.spec.ts b/src/api/providers/__tests__/opencode-go.spec.ts index 721e795eb4..98209ae325 100644 --- a/src/api/providers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/__tests__/opencode-go.spec.ts @@ -802,7 +802,7 @@ describe("OpencodeGoHandler", () => { }) it("classifies OpenAI-compatible Go models as non-Anthropic-format", () => { - expect(isOpencodeGoAnthropicFormatModel("glm-5.2")).toBe(false) + expect(isOpencodeGoAnthropicFormatModel("glm-5.3")).toBe(false) expect(isOpencodeGoAnthropicFormatModel("kimi-k2.6")).toBe(false) expect(isOpencodeGoAnthropicFormatModel("deepseek-v4-pro")).toBe(false) expect(isOpencodeGoAnthropicFormatModel("mimo-v2.5")).toBe(false) diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts index ac13152f37..440bbc2328 100644 --- a/src/api/providers/__tests__/zai.spec.ts +++ b/src/api/providers/__tests__/zai.spec.ts @@ -11,6 +11,7 @@ import { internationalZAiModels, mainlandZAiModels, ZAI_DEFAULT_TEMPERATURE, + getZAiModels, } from "@roo-code/types" import { ZAiHandler } from "../zai" @@ -141,6 +142,30 @@ describe("ZAiHandler", () => { expect(model.info.cacheReadsPrice).toBe(0.26) }) + it("should expose GLM-5.3 for the international Coding Plan with provisional GLM-5.2 pricing", () => { + const handlerWithModel = new ZAiHandler({ + apiModelId: "glm-5.3", + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe("glm-5.3") + expect(model.info).toMatchObject({ + contextWindow: 1_000_000, + maxTokens: 131_072, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "high", "max"], + requiredReasoningEffort: true, + reasoningEffort: "max", + preserveReasoning: true, + }) + expect(model.info.inputPrice).toBe(1.4) + expect(model.info.outputPrice).toBe(4.4) + expect(model.info.cacheReadsPrice).toBe(0.26) + }) + it("should return GLM-5-Turbo international model with thinking support", () => { const testModelId: InternationalZAiModelId = "glm-5-turbo" const handlerWithModel = new ZAiHandler({ @@ -277,6 +302,22 @@ describe("ZAiHandler", () => { expect(model.info.cacheReadsPrice).toBe(0.13) }) + it("should expose GLM-5.3 for the China Coding Plan", () => { + const handlerWithModel = new ZAiHandler({ + apiModelId: "glm-5.3", + zaiApiKey: "test-zai-api-key", + zaiApiLine: "china_coding", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe("glm-5.3") + expect(model.info.supportsReasoningEffort).toEqual(["low", "high", "max"]) + expect(model.info.requiredReasoningEffort).toBe(true) + expect(model.info.reasoningEffort).toBe("max") + expect(model.info.inputPrice).toBe(0.68) + expect(model.info.outputPrice).toBe(2.28) + expect(model.info.cacheReadsPrice).toBe(0.13) + }) + it("should return GLM-4.7 China model with thinking support", () => { const testModelId: MainlandZAiModelId = "glm-4.7" const handlerWithModel = new ZAiHandler({ @@ -348,6 +389,16 @@ describe("ZAiHandler", () => { expect(model.id).toBe(testModelId) expect(model.info).toEqual(internationalZAiModels[testModelId]) }) + + it("should not expose Coding Plan-only models", () => { + expect(getZAiModels("international_api")).not.toHaveProperty("glm-5.3") + const handlerWithModel = new ZAiHandler({ + apiModelId: "glm-5.3", + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_api", + }) + expect(handlerWithModel.getModel().id).toBe(internationalZAiDefaultModelId) + }) }) describe("China API", () => { @@ -387,6 +438,10 @@ describe("ZAiHandler", () => { expect(model.id).toBe(testModelId) expect(model.info).toEqual(mainlandZAiModels[testModelId]) }) + + it("should not expose Coding Plan-only models", () => { + expect(getZAiModels("china_api")).not.toHaveProperty("glm-5.3") + }) }) describe("Default behavior", () => { @@ -613,6 +668,50 @@ describe("ZAiHandler", () => { ) }) + it("should keep GLM-5.3 reasoning enabled when a persisted setting requests disable", async () => { + const handlerWithModel = new ZAiHandler({ + apiModelId: "glm-5.3", + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + reasoningEffort: "disable", + }) + + mockCreate.mockImplementationOnce(() => asyncStreamFrom([])) + + const messageGenerator = handlerWithModel.createMessage("system prompt", []) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "glm-5.3", + thinking: { type: "enabled" }, + reasoning_effort: "max", + }), + ) + }) + + it("should keep GLM-5.3 reasoning enabled when the master reasoning setting is disabled", async () => { + const handlerWithModel = new ZAiHandler({ + apiModelId: "glm-5.3", + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + enableReasoningEffort: false, + }) + + mockCreate.mockImplementationOnce(() => asyncStreamFrom([])) + + const messageGenerator = handlerWithModel.createMessage("system prompt", []) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "glm-5.3", + thinking: { type: "enabled" }, + reasoning_effort: "max", + }), + ) + }) + it("should omit reasoning_effort for GLM-5.2 when reasoningEffort is set to disable", async () => { const handlerWithModel = new ZAiHandler({ apiModelId: "glm-5.2", diff --git a/src/api/providers/fetchers/__tests__/litellm.spec.ts b/src/api/providers/fetchers/__tests__/litellm.spec.ts index 8e6d49ddae..9f7b80cb31 100644 --- a/src/api/providers/fetchers/__tests__/litellm.spec.ts +++ b/src/api/providers/fetchers/__tests__/litellm.spec.ts @@ -765,7 +765,7 @@ describe("getLiteLLMModels", () => { data: { data: [ { - model_name: "glm-5.2", + model_name: "glm-5.3", model_info: { max_tokens: 8192, max_input_tokens: 128000, @@ -782,7 +782,7 @@ describe("getLiteLLMModels", () => { const result = await getLiteLLMModels("test-api-key", "http://localhost:4000") - expect(result["glm-5.2"]).toMatchObject({ preserveReasoning: true }) + expect(result["glm-5.3"]).toMatchObject({ preserveReasoning: true }) }) it("does not match a model id that merely contains a known family as a substring", async () => { diff --git a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts index c8607db4d3..20040ffb22 100644 --- a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts @@ -157,6 +157,20 @@ describe("Opencode Go Fetchers", () => { expect(info.outputPrice).toBe(4.4) }) + it("resolves GLM-5.3 with always-on Low/High/Max reasoning effort", () => { + const info = parseOpencodeGoModel({ id: "glm-5.3" }) + expect(info.contextWindow).toBe(1_000_000) + expect(info.maxTokens).toBe(131_072) + expect(info.supportsPromptCache).toBe(true) + expect(info.supportsMaxTokens).toBe(true) + expect(info.supportsReasoningEffort).toEqual(["low", "high", "max"]) + expect(info.reasoningEffort).toBe("max") + expect(info.preserveReasoning).toBe(true) + expect(info.inputPrice).toBe(1.4) + expect(info.outputPrice).toBe(4.4) + expect(info.cacheReadsPrice).toBe(0.26) + }) + it("falls back to defaults for an unknown model with no cache pricing", () => { const info = parseOpencodeGoModel({ id: "x", context_window: 100000, max_tokens: 8000 }) expect(info.supportsPromptCache).toBe(false) diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index 4854c814fd..65a6bf458c 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -2,13 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import { - internationalZAiModels, - mainlandZAiModels, internationalZAiDefaultModelId, mainlandZAiDefaultModelId, type ModelInfo, ZAI_DEFAULT_TEMPERATURE, zaiApiLineConfigs, + getZAiModels, } from "@roo-code/types" import { type ApiHandlerOptions, getModelMaxOutputTokens } from "../../shared/api" @@ -29,14 +28,15 @@ type ZAiChatCompletionParams = Omit { constructor(options: ApiHandlerOptions) { - const isChina = zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].isChina - const models = (isChina ? mainlandZAiModels : internationalZAiModels) as unknown as Record + const apiLine = options.zaiApiLine ?? "international_coding" + const isChina = zaiApiLineConfigs[apiLine].isChina + const models = getZAiModels(apiLine) const defaultModelId = (isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId) as string super({ ...options, providerName: "Z.ai", - baseURL: zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].baseUrl, + baseURL: zaiApiLineConfigs[apiLine].baseUrl, apiKey: options.zaiApiKey ?? "not-provided", defaultProviderModelId: defaultModelId, providerModels: models, @@ -85,12 +85,15 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { this.options.enableReasoningEffort === false ? undefined : (this.options.reasoningEffort ?? info.reasoningEffort) + const requiresReasoning = info.requiredReasoningEffort === true const effort = - raw && raw !== "disable" && Array.isArray(supported) && !supported.includes(raw) + requiresReasoning && (!raw || raw === "disable") ? info.reasoningEffort - : raw + : raw && Array.isArray(supported) && !supported.includes(raw) + ? info.reasoningEffort + : raw const reasoningEffort = effort && effort !== "disable" ? effort : undefined - const useReasoning = reasoningEffort !== undefined + const useReasoning = requiresReasoning || reasoningEffort !== undefined const max_tokens = this.options.modelMaxTokens || @@ -114,7 +117,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { messages: [{ role: "system", content: systemPrompt }, ...convertedMessages], stream: true, stream_options: { include_usage: true }, - // Thinking is ON by default for these models, so explicitly disable it when needed. + // Models with required reasoning stay enabled even when an old setting requests disable. thinking: useReasoning ? { type: "enabled" } : { type: "disabled" }, reasoning_effort: reasoningEffort, tools: this.convertToolsForOpenAI(metadata?.tools), diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 0cc61052db..be7dad7542 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -722,6 +722,7 @@ const ApiOptions = ({ models={getStaticModelsForProvider( activeSelectedProvider, t("settings:labels.useCustomArn"), + apiConfiguration, )} modelIdKey="apiModelId" serviceName={getProviderServiceConfig(activeSelectedProvider).serviceName} diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 9525ba7a95..d8ee0cd448 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -109,22 +109,27 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod // Clamp to availableOptions so the Select trigger always renders a valid option. const storedReasoningEffort = apiConfiguration.reasoningEffort as ReasoningEffortOption | undefined const rawReasoningEffort: ReasoningEffortOption = storedReasoningEffort || defaultReasoningEffort + const fallbackReasoningEffort = availableOptions.includes(defaultReasoningEffort) + ? defaultReasoningEffort + : (availableOptions[0] ?? rawReasoningEffort) const currentReasoningEffort: ReasoningEffortOption = availableOptions.includes(rawReasoningEffort) ? rawReasoningEffort - : (availableOptions[0] ?? rawReasoningEffort) + : fallbackReasoningEffort // Set default reasoning effort when model supports it and no value is set useEffect(() => { - if (isReasoningEffortSupported && !apiConfiguration.reasoningEffort) { - // Only set a default if reasoning is required, otherwise leave as undefined (which maps to "disable") - if (modelInfo?.requiredReasoningEffort && defaultReasoningEffort !== "disable") { - setApiConfigurationField("reasoningEffort", defaultReasoningEffort as ReasoningEffortExtended, false) - } + if ( + isReasoningEffortSupported && + modelInfo?.requiredReasoningEffort && + storedReasoningEffort !== currentReasoningEffort && + currentReasoningEffort !== "disable" + ) { + setApiConfigurationField("reasoningEffort", currentReasoningEffort as ReasoningEffortExtended, false) } }, [ isReasoningEffortSupported, - apiConfiguration.reasoningEffort, - defaultReasoningEffort, + storedReasoningEffort, + currentReasoningEffort, modelInfo?.requiredReasoningEffort, setApiConfigurationField, ]) diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx index 5f9b74dfe0..3c97ee20e9 100644 --- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx @@ -289,6 +289,26 @@ describe("ThinkingBudget", () => { expect(screen.getByTestId("select")).toHaveAttribute("data-value", "low") }) + it("should normalize an invalid disabled value to the default for required reasoning", () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + expect(screen.getByTestId("select")).toHaveAttribute("data-value", "max") + expect(setApiConfigurationField).toHaveBeenCalledWith("reasoningEffort", "max", false) + }) + it("should fall back to rawReasoningEffort when availableOptions is empty", () => { // Covers the ?? rawReasoningEffort branch when availableOptions[0] is undefined render( 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 153f30b0e2..868bdac319 100644 --- a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts +++ b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts @@ -110,6 +110,15 @@ describe("providerModelConfig", () => { expect(defaultId.length).toBeGreaterThan(0) }) + it("returns mainland default for Z.ai with china_api entrypoint", () => { + expect( + getDefaultModelIdForProvider("zai", { + apiProvider: "zai", + zaiApiLine: "china_api", + }), + ).toBe(mainlandZAiDefaultModelId) + }) + it("returns international default for Z.ai with international_coding entrypoint", () => { const defaultId = getDefaultModelIdForProvider("zai", { apiProvider: "zai", @@ -177,6 +186,30 @@ describe("providerModelConfig", () => { const models = getStaticModelsForProvider("openrouter") expect(Object.keys(models).length).toBe(0) }) + + it("shows GLM-5.3 only for Z.ai Coding Plan entrypoints", () => { + const internationalCoding = getStaticModelsForProvider("zai", undefined, { + apiProvider: "zai", + zaiApiLine: "international_coding", + }) + const chinaCoding = getStaticModelsForProvider("zai", undefined, { + apiProvider: "zai", + zaiApiLine: "china_coding", + }) + const internationalApi = getStaticModelsForProvider("zai", undefined, { + apiProvider: "zai", + zaiApiLine: "international_api", + }) + const chinaApi = getStaticModelsForProvider("zai", undefined, { + apiProvider: "zai", + zaiApiLine: "china_api", + }) + + expect(internationalCoding).toHaveProperty("glm-5.3") + expect(chinaCoding).toHaveProperty("glm-5.3") + expect(internationalApi).not.toHaveProperty("glm-5.3") + expect(chinaApi).not.toHaveProperty("glm-5.3") + }) }) describe("isStaticModelProvider", () => { diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index eccbf7ba1d..5e18944951 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -30,6 +30,8 @@ import { opencodeGoDefaultModelId, kenariDefaultModelId, zooGatewayDefaultModelId, + zaiApiLineConfigs, + getZAiModels, } from "@roo-code/types" import { MODELS_BY_PROVIDER } from "../constants" @@ -97,9 +99,8 @@ export const getProviderServiceConfig = (provider: ProviderName): ProviderServic export const getDefaultModelIdForProvider = (provider: ProviderName, apiConfiguration?: ProviderSettings): string => { // Handle Z.ai's China/International entrypoint distinction if (provider === providerIdentifiers.zai && apiConfiguration) { - return apiConfiguration.zaiApiLine === "china_coding" - ? mainlandZAiDefaultModelId - : internationalZAiDefaultModelId + const apiLine = apiConfiguration.zaiApiLine ?? "international_coding" + return zaiApiLineConfigs[apiLine].isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId } return PROVIDER_DEFAULT_MODEL_IDS[provider] ?? "" @@ -176,7 +177,12 @@ export function getProviderDocsSlug(provider: string) { export const getStaticModelsForProvider = ( provider: ProviderName, customArnLabel?: string, + apiConfiguration?: ProviderSettings, ): Record => { + if (provider === providerIdentifiers.zai) { + return getZAiModels(apiConfiguration?.zaiApiLine) + } + const models = MODELS_BY_PROVIDER[provider] ?? {} // Add custom-arn option for Bedrock 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 2571614085..1619270457 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -30,6 +30,9 @@ import { kimiCodeDefaultModelInfo, lMStudioDefaultModelInfo, opencodeGoDefaultModelInfo, + getZAiModels, + internationalZAiDefaultModelId, + mainlandZAiDefaultModelId, providerIdentifiers, retiredProviderIdentifiers, } from "@roo-code/types" @@ -1281,6 +1284,50 @@ describe("useSelectedModel", () => { }) }) + describe("Z AI provider", () => { + it("uses the International Coding catalog when no API line is configured", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.zai, + apiModelId: "glm-5.3", + } + + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper: createWrapper() }) + + expect(result.current.id).toBe("glm-5.3") + expect(result.current.info).toEqual(getZAiModels("international_coding")["glm-5.3"]) + }) + + it("uses the China Coding catalog for GLM-5.3", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.zai, + apiModelId: "glm-5.3", + zaiApiLine: "china_coding", + } + + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper: createWrapper() }) + + expect(result.current.id).toBe("glm-5.3") + expect(result.current.info).toEqual(getZAiModels("china_coding")["glm-5.3"]) + expect(result.current.info?.inputPrice).toBe(0.68) + }) + + it.each([ + ["international_api", internationalZAiDefaultModelId], + ["china_api", mainlandZAiDefaultModelId], + ] as const)("falls back when GLM-5.3 is unavailable on %s", (zaiApiLine, expectedModelId) => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.zai, + apiModelId: "glm-5.3", + zaiApiLine, + } + + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper: createWrapper() }) + + expect(result.current.id).toBe(expectedModelId) + expect(result.current.info).toEqual(getZAiModels(zaiApiLine)[expectedModelId]) + }) + }) + describe("Kimi Code provider", () => { it("should use the Kimi Code default while router models are loading and no model is configured", () => { mockUseRouterModels.mockReturnValue(createRouterModelsResult(undefined, { isLoading: true })) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 6ed20ef47d..d73edf57f9 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -20,8 +20,8 @@ import { vscodeLlmDefaultModelId, openAiCodexModels, sambaNovaModels, - internationalZAiModels, - mainlandZAiModels, + getZAiModels, + zaiApiLineConfigs, fireworksModels, friendliModels, basetenModels, @@ -305,11 +305,12 @@ function getSelectedModel({ return { id, info } } case providerIdentifiers.zai: { - const isChina = apiConfiguration.zaiApiLine === "china_coding" - const models = isChina ? mainlandZAiModels : internationalZAiModels + const apiLine = apiConfiguration.zaiApiLine ?? "international_coding" + const isChina = zaiApiLineConfigs[apiLine].isChina + const models = getZAiModels(apiLine) const defaultModelId = getProviderDefaultModelId(provider, { isChina }) - const id = apiConfiguration.apiModelId ?? defaultModelId - const info = models[id as keyof typeof models] + const id = getValidatedModelId(apiConfiguration.apiModelId, models, defaultModelId) + const info = models[id] return { id, info } } case providerIdentifiers.openaiNative: {