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
1 change: 1 addition & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const dynamicProviders = [
providerIdentifiers.opencodeGo,
providerIdentifiers.kenari,
providerIdentifiers.kimiCode,
providerIdentifiers.friendli,
] as const

export type DynamicProvider = (typeof dynamicProviders)[number]
Expand Down
12 changes: 10 additions & 2 deletions packages/types/src/providers/friendli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ export type FriendliModelId =

export const friendliDefaultModelId: FriendliModelId = "zai-org/GLM-5.2"

// Static fallback for the Friendli provider. Used as a fallback when dynamic
// models cannot be fetched (cold start, network errors, API lag), in tests,
// and in the webview's MODELS_BY_PROVIDER fallback. The provider itself fetches
// the live list from https://api.friendli.ai/serverless/v1/models at runtime.
// Pricing sourced from https://friendli.ai/api/public/model-apis (per 1M tokens).
export const friendliModels = {
export const friendliModels: Record<string, ModelInfo> = {
"zai-org/GLM-5.2": {
maxTokens: 131_072,
contextWindow: 1_000_000,
Expand All @@ -20,6 +24,8 @@ export const friendliModels = {
outputPrice: 4.4,
cacheWritesPrice: 0,
cacheReadsPrice: 0.26,
supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh", "max"],
reasoningEffort: "high",
description:
"GLM-5.2 is Zhipu's flagship model with a 1M context window and 128k max output, served via Friendli Model APIs. It delivers top-tier long-context reasoning, coding, and agentic performance for extended engineering sessions.",
},
Expand All @@ -33,6 +39,8 @@ export const friendliModels = {
outputPrice: 4.4,
cacheWritesPrice: 0,
cacheReadsPrice: 0.26,
supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh", "max"],
reasoningEffort: "high",
description:
"GLM-5.1 is Zhipu's most capable model with a 200k context window and 128k max output, served via Friendli Model APIs. It delivers top-tier reasoning, coding, and agentic performance.",
},
Expand Down Expand Up @@ -60,4 +68,4 @@ export const friendliModels = {
description:
"MiniMax M2.5 is a high-performance language model with a 204.8K context window, optimized for long-context understanding and generation tasks, served via Friendli Model APIs.",
},
} as const satisfies Record<string, ModelInfo>
}
225 changes: 224 additions & 1 deletion src/api/providers/__tests__/friendli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,16 @@ describe("FriendliHandler", () => {
},
])(
"should expose newly added model $modelId",
({ modelId, contextWindow, maxTokens, supportsMaxTokens, inputPrice, outputPrice, cacheWritesPrice, cacheReadsPrice }) => {
({
modelId,
contextWindow,
maxTokens,
supportsMaxTokens,
inputPrice,
outputPrice,
cacheWritesPrice,
cacheReadsPrice,
}) => {
expect(friendliModels[modelId]).toBeDefined()
const info = friendliModels[modelId] as import("@roo-code/types").ModelInfo
expect(info.maxTokens).toBe(maxTokens)
Expand Down Expand Up @@ -394,3 +403,217 @@ describe("Friendli model max output tokens (clamping behavior)", () => {
expect(result).toBe(80_000)
})
})

describe("FriendliHandler — Friendli-specific reasoning params", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it("should include reasoning_effort, chat_template_kwargs, parse_reasoning for GLM-5.2 with reasoning enabled", async () => {
const handler = new FriendliHandler({
apiModelId: "zai-org/GLM-5.2",
friendliApiKey: "test-key",
enableReasoningEffort: true,
reasoningEffort: "high",
})

mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}))

await handler.createMessage("system", []).next()

expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "zai-org/GLM-5.2",
reasoning_effort: "high",
chat_template_kwargs: { enable_thinking: true },
parse_reasoning: true,
include_reasoning: true,
}),
undefined,
)
})

it("should send enable_thinking: false when enableReasoningEffort is false on controllable model", async () => {
const handler = new FriendliHandler({
apiModelId: "zai-org/GLM-5.2",
friendliApiKey: "test-...ey",
enableReasoningEffort: false,
})

mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}))

await handler.createMessage("system", []).next()

const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
expect(callArgs.reasoning_effort).toBeUndefined()
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false })
expect(callArgs.parse_reasoning).toBeUndefined()
expect(callArgs.include_reasoning).toBeUndefined()
})

it("should send enable_thinking: false when reasoningEffort is none on controllable model", async () => {
const handler = new FriendliHandler({
apiModelId: "zai-org/GLM-5.2",
friendliApiKey: "test-...ey",
enableReasoningEffort: true,
reasoningEffort: "none",
})

mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}))

await handler.createMessage("system", []).next()

const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
expect(callArgs.reasoning_effort).toBeUndefined()
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false })
expect(callArgs.parse_reasoning).toBeUndefined()
expect(callArgs.include_reasoning).toBeUndefined()
})

it("should send enable_thinking: false when reasoningEffort is disable on controllable model", async () => {
const handler = new FriendliHandler({
apiModelId: "zai-org/GLM-5.2",
friendliApiKey: "test-...ey",
enableReasoningEffort: true,
reasoningEffort: "disable",
})

mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}))

await handler.createMessage("system", []).next()

const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
expect(callArgs.reasoning_effort).toBeUndefined()
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false })
expect(callArgs.parse_reasoning).toBeUndefined()
expect(callArgs.include_reasoning).toBeUndefined()
})

it("should use model default reasoningEffort when no explicit settings are provided", async () => {
const handler = new FriendliHandler({
apiModelId: "zai-org/GLM-5.2",
friendliApiKey: "test-...ey",
// No enableReasoningEffort or reasoningEffort — model default "high" kicks in
})

mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}))

await handler.createMessage("system", []).next()

const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
expect(callArgs.reasoning_effort).toBe("high")
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: true })
expect(callArgs.parse_reasoning).toBe(true)
expect(callArgs.include_reasoning).toBe(true)
})

it("should not include any reasoning params for non-reasoning DeepSeek-V3.2", async () => {
const handler = new FriendliHandler({
apiModelId: "deepseek-ai/DeepSeek-V3.2",
friendliApiKey: "test-key",
enableReasoningEffort: true,
reasoningEffort: "high",
})

mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}))

await handler.createMessage("system", []).next()

const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
expect(callArgs.reasoning_effort).toBeUndefined()
expect(callArgs.chat_template_kwargs).toBeUndefined()
expect(callArgs.parse_reasoning).toBeUndefined()
})

it("should handle delta.reasoning_content from parse_reasoning=true stream", async () => {
const handler = new FriendliHandler({
apiModelId: "zai-org/GLM-5.2",
friendliApiKey: "test-key",
enableReasoningEffort: true,
reasoningEffort: "high",
})

mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { reasoning_content: "Let me think..." } }],
usage: null,
}
yield {
choices: [{ delta: { content: "The answer is 42" } }],
usage: null,
}
yield {
choices: [{ delta: {} }],
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
}
},
}))

const stream = handler.createMessage("system", [])
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "Let me think..." })
expect(chunks).toContainEqual({ type: "text", text: "The answer is 42" })
})

it("completePrompt should include reasoning params when enabled", async () => {
const handler = new FriendliHandler({
apiModelId: "zai-org/GLM-5.2",
friendliApiKey: "test-key",
enableReasoningEffort: true,
reasoningEffort: "medium",
})

mockCreate.mockResolvedValueOnce({
choices: [{ message: { content: "test result" } }],
})

await handler.completePrompt("test")

const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
expect(callArgs.reasoning_effort).toBe("medium")
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: true })
expect(callArgs.parse_reasoning).toBe(true)
expect(callArgs.include_reasoning).toBe(true)
})
})
Loading
Loading