Skip to content

Commit 82374ee

Browse files
Merge branch 'main' into issue/368
2 parents 418802e + ded7575 commit 82374ee

86 files changed

Lines changed: 2634 additions & 699 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/types/src/providers/friendli.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export const friendliModels = {
2020
outputPrice: 4.4,
2121
cacheWritesPrice: 0,
2222
cacheReadsPrice: 0.26,
23+
supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh", "max"],
24+
reasoningEffort: "high",
2325
description:
2426
"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.",
2527
},
@@ -33,6 +35,8 @@ export const friendliModels = {
3335
outputPrice: 4.4,
3436
cacheWritesPrice: 0,
3537
cacheReadsPrice: 0.26,
38+
supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh", "max"],
39+
reasoningEffort: "high",
3640
description:
3741
"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.",
3842
},

src/activate/__tests__/registerCommands.spec.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Mock } from "vitest"
22
import * as vscode from "vscode"
33
import { ClineProvider } from "../../core/webview/ClineProvider"
44

5-
import { getVisibleProviderOrLog, registerCommands, setPanel } from "../registerCommands"
5+
import { getVisibleProviderOrLog, openClineInNewTab, registerCommands, setPanel } from "../registerCommands"
66

77
vi.mock("execa", () => ({
88
execa: vi.fn(),
@@ -13,8 +13,16 @@ vi.mock("vscode", () => ({
1313
QuickFix: { value: "quickfix" },
1414
RefactorRewrite: { value: "refactor.rewrite" },
1515
},
16+
Uri: {
17+
joinPath: vi.fn((_base: unknown, ..._pathSegments: string[]) => ({ path: _pathSegments.join("/") })),
18+
},
19+
ViewColumn: {
20+
Two: 2,
21+
},
1622
window: {
1723
createTextEditorDecorationType: vi.fn().mockReturnValue({ dispose: vi.fn() }),
24+
createWebviewPanel: vi.fn(),
25+
visibleTextEditors: [],
1826
},
1927
workspace: {
2028
workspaceFolders: [
@@ -367,3 +375,53 @@ describe("registerCommands handlers", () => {
367375
await handlers["zoo-code.plusButtonClicked"]()
368376
})
369377
})
378+
379+
describe("openClineInNewTab", () => {
380+
let mockOutputChannel: vscode.OutputChannel
381+
let mockContext: vscode.ExtensionContext
382+
383+
beforeEach(() => {
384+
vi.clearAllMocks()
385+
386+
mockOutputChannel = {
387+
appendLine: vi.fn(),
388+
append: vi.fn(),
389+
clear: vi.fn(),
390+
hide: vi.fn(),
391+
name: "mock",
392+
replace: vi.fn(),
393+
show: vi.fn(),
394+
dispose: vi.fn(),
395+
}
396+
397+
mockContext = {
398+
subscriptions: [],
399+
extensionUri: { path: "/mock/ext" },
400+
} as unknown as vscode.ExtensionContext
401+
402+
const mockPanel = {
403+
webview: { postMessage: vi.fn() },
404+
onDidChangeViewState: vi.fn(),
405+
onDidDispose: vi.fn(),
406+
}
407+
;(vscode.window.createWebviewPanel as Mock).mockReturnValue(mockPanel)
408+
409+
// Reset module-level panel state.
410+
setPanel(undefined, "sidebar")
411+
setPanel(undefined, "tab")
412+
})
413+
414+
it("creates a webview panel with title 'Zoo Code'", async () => {
415+
await openClineInNewTab({ context: mockContext, outputChannel: mockOutputChannel })
416+
417+
expect(vscode.window.createWebviewPanel).toHaveBeenCalledWith(
418+
"zoo-code.TabPanelProvider",
419+
"Zoo Code",
420+
expect.any(Number),
421+
expect.objectContaining({
422+
enableScripts: true,
423+
retainContextWhenHidden: true,
424+
}),
425+
)
426+
})
427+
})

src/activate/registerCommands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit<Registe
251251

252252
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
253253

254-
const newPanel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
254+
const newPanel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Zoo Code", targetCol, {
255255
enableScripts: true,
256256
retainContextWhenHidden: true,
257257
localResourceRoots: [context.extensionUri],

src/api/providers/__tests__/friendli.spec.ts

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,16 @@ describe("FriendliHandler", () => {
142142
},
143143
])(
144144
"should expose newly added model $modelId",
145-
({ modelId, contextWindow, maxTokens, supportsMaxTokens, inputPrice, outputPrice, cacheWritesPrice, cacheReadsPrice }) => {
145+
({
146+
modelId,
147+
contextWindow,
148+
maxTokens,
149+
supportsMaxTokens,
150+
inputPrice,
151+
outputPrice,
152+
cacheWritesPrice,
153+
cacheReadsPrice,
154+
}) => {
146155
expect(friendliModels[modelId]).toBeDefined()
147156
const info = friendliModels[modelId] as import("@roo-code/types").ModelInfo
148157
expect(info.maxTokens).toBe(maxTokens)
@@ -394,3 +403,217 @@ describe("Friendli model max output tokens (clamping behavior)", () => {
394403
expect(result).toBe(80_000)
395404
})
396405
})
406+
407+
describe("FriendliHandler — Friendli-specific reasoning params", () => {
408+
beforeEach(() => {
409+
vi.clearAllMocks()
410+
})
411+
412+
it("should include reasoning_effort, chat_template_kwargs, parse_reasoning for GLM-5.2 with reasoning enabled", async () => {
413+
const handler = new FriendliHandler({
414+
apiModelId: "zai-org/GLM-5.2",
415+
friendliApiKey: "test-key",
416+
enableReasoningEffort: true,
417+
reasoningEffort: "high",
418+
})
419+
420+
mockCreate.mockImplementationOnce(() => ({
421+
[Symbol.asyncIterator]: () => ({
422+
async next() {
423+
return { done: true }
424+
},
425+
}),
426+
}))
427+
428+
await handler.createMessage("system", []).next()
429+
430+
expect(mockCreate).toHaveBeenCalledWith(
431+
expect.objectContaining({
432+
model: "zai-org/GLM-5.2",
433+
reasoning_effort: "high",
434+
chat_template_kwargs: { enable_thinking: true },
435+
parse_reasoning: true,
436+
include_reasoning: true,
437+
}),
438+
undefined,
439+
)
440+
})
441+
442+
it("should send enable_thinking: false when enableReasoningEffort is false on controllable model", async () => {
443+
const handler = new FriendliHandler({
444+
apiModelId: "zai-org/GLM-5.2",
445+
friendliApiKey: "test-...ey",
446+
enableReasoningEffort: false,
447+
})
448+
449+
mockCreate.mockImplementationOnce(() => ({
450+
[Symbol.asyncIterator]: () => ({
451+
async next() {
452+
return { done: true }
453+
},
454+
}),
455+
}))
456+
457+
await handler.createMessage("system", []).next()
458+
459+
const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
460+
expect(callArgs.reasoning_effort).toBeUndefined()
461+
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false })
462+
expect(callArgs.parse_reasoning).toBeUndefined()
463+
expect(callArgs.include_reasoning).toBeUndefined()
464+
})
465+
466+
it("should send enable_thinking: false when reasoningEffort is none on controllable model", async () => {
467+
const handler = new FriendliHandler({
468+
apiModelId: "zai-org/GLM-5.2",
469+
friendliApiKey: "test-...ey",
470+
enableReasoningEffort: true,
471+
reasoningEffort: "none",
472+
})
473+
474+
mockCreate.mockImplementationOnce(() => ({
475+
[Symbol.asyncIterator]: () => ({
476+
async next() {
477+
return { done: true }
478+
},
479+
}),
480+
}))
481+
482+
await handler.createMessage("system", []).next()
483+
484+
const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
485+
expect(callArgs.reasoning_effort).toBeUndefined()
486+
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false })
487+
expect(callArgs.parse_reasoning).toBeUndefined()
488+
expect(callArgs.include_reasoning).toBeUndefined()
489+
})
490+
491+
it("should send enable_thinking: false when reasoningEffort is disable on controllable model", async () => {
492+
const handler = new FriendliHandler({
493+
apiModelId: "zai-org/GLM-5.2",
494+
friendliApiKey: "test-...ey",
495+
enableReasoningEffort: true,
496+
reasoningEffort: "disable",
497+
})
498+
499+
mockCreate.mockImplementationOnce(() => ({
500+
[Symbol.asyncIterator]: () => ({
501+
async next() {
502+
return { done: true }
503+
},
504+
}),
505+
}))
506+
507+
await handler.createMessage("system", []).next()
508+
509+
const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
510+
expect(callArgs.reasoning_effort).toBeUndefined()
511+
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false })
512+
expect(callArgs.parse_reasoning).toBeUndefined()
513+
expect(callArgs.include_reasoning).toBeUndefined()
514+
})
515+
516+
it("should use model default reasoningEffort when no explicit settings are provided", async () => {
517+
const handler = new FriendliHandler({
518+
apiModelId: "zai-org/GLM-5.2",
519+
friendliApiKey: "test-...ey",
520+
// No enableReasoningEffort or reasoningEffort — model default "high" kicks in
521+
})
522+
523+
mockCreate.mockImplementationOnce(() => ({
524+
[Symbol.asyncIterator]: () => ({
525+
async next() {
526+
return { done: true }
527+
},
528+
}),
529+
}))
530+
531+
await handler.createMessage("system", []).next()
532+
533+
const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
534+
expect(callArgs.reasoning_effort).toBe("high")
535+
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: true })
536+
expect(callArgs.parse_reasoning).toBe(true)
537+
expect(callArgs.include_reasoning).toBe(true)
538+
})
539+
540+
it("should not include any reasoning params for non-reasoning DeepSeek-V3.2", async () => {
541+
const handler = new FriendliHandler({
542+
apiModelId: "deepseek-ai/DeepSeek-V3.2",
543+
friendliApiKey: "test-key",
544+
enableReasoningEffort: true,
545+
reasoningEffort: "high",
546+
})
547+
548+
mockCreate.mockImplementationOnce(() => ({
549+
[Symbol.asyncIterator]: () => ({
550+
async next() {
551+
return { done: true }
552+
},
553+
}),
554+
}))
555+
556+
await handler.createMessage("system", []).next()
557+
558+
const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
559+
expect(callArgs.reasoning_effort).toBeUndefined()
560+
expect(callArgs.chat_template_kwargs).toBeUndefined()
561+
expect(callArgs.parse_reasoning).toBeUndefined()
562+
})
563+
564+
it("should handle delta.reasoning_content from parse_reasoning=true stream", async () => {
565+
const handler = new FriendliHandler({
566+
apiModelId: "zai-org/GLM-5.2",
567+
friendliApiKey: "test-key",
568+
enableReasoningEffort: true,
569+
reasoningEffort: "high",
570+
})
571+
572+
mockCreate.mockImplementationOnce(async () => ({
573+
[Symbol.asyncIterator]: async function* () {
574+
yield {
575+
choices: [{ delta: { reasoning_content: "Let me think..." } }],
576+
usage: null,
577+
}
578+
yield {
579+
choices: [{ delta: { content: "The answer is 42" } }],
580+
usage: null,
581+
}
582+
yield {
583+
choices: [{ delta: {} }],
584+
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
585+
}
586+
},
587+
}))
588+
589+
const stream = handler.createMessage("system", [])
590+
const chunks = []
591+
for await (const chunk of stream) {
592+
chunks.push(chunk)
593+
}
594+
595+
expect(chunks).toContainEqual({ type: "reasoning", text: "Let me think..." })
596+
expect(chunks).toContainEqual({ type: "text", text: "The answer is 42" })
597+
})
598+
599+
it("completePrompt should include reasoning params when enabled", async () => {
600+
const handler = new FriendliHandler({
601+
apiModelId: "zai-org/GLM-5.2",
602+
friendliApiKey: "test-key",
603+
enableReasoningEffort: true,
604+
reasoningEffort: "medium",
605+
})
606+
607+
mockCreate.mockResolvedValueOnce({
608+
choices: [{ message: { content: "test result" } }],
609+
})
610+
611+
await handler.completePrompt("test")
612+
613+
const callArgs = mockCreate.mock.calls[0][0] as Record<string, unknown>
614+
expect(callArgs.reasoning_effort).toBe("medium")
615+
expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: true })
616+
expect(callArgs.parse_reasoning).toBe(true)
617+
expect(callArgs.include_reasoning).toBe(true)
618+
})
619+
})

src/api/providers/__tests__/lmstudio.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ describe("LmStudioHandler", () => {
125125
for await (const _chunk of stream) {
126126
// Should not reach here
127127
}
128-
}).rejects.toThrow("Please check the LM Studio developer logs to debug what went wrong")
128+
}).rejects.toThrow(
129+
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Zoo Code's prompts.",
130+
)
129131
})
130132
})
131133

@@ -144,7 +146,7 @@ describe("LmStudioHandler", () => {
144146
it("should handle API errors", async () => {
145147
mockCreate.mockRejectedValueOnce(new Error("API Error"))
146148
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(
147-
"Please check the LM Studio developer logs to debug what went wrong",
149+
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Zoo Code's prompts.",
148150
)
149151
})
150152

0 commit comments

Comments
 (0)