Skip to content

Commit cd4b717

Browse files
authored
Merge branch 'main' into feat/gemini-3.5-flash
2 parents f413957 + 78d3dac commit cd4b717

59 files changed

Lines changed: 2038 additions & 243 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"zoo-code": patch
3+
---
4+
5+
Detect when the "Google Cloud Credentials" Vertex field has been given a filesystem path instead of the raw JSON contents of a service-account key file. The runtime now skips JSON.parse for path-shaped input, logs a single specific console warning naming the sibling "Google Cloud Key File Path" field and `GOOGLE_APPLICATION_CREDENTIALS` env var, and the settings UI shows an inline warning under the field while the input still looks like a path. Auth behavior is unchanged for correctly-configured users.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// npx vitest run packages/core/src/message-utils/__tests__/safeJsonParse.spec.ts
2+
3+
import { safeJsonParse } from "../safeJsonParse.js"
4+
5+
describe("safeJsonParse", () => {
6+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
7+
8+
beforeEach(() => {
9+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
10+
})
11+
12+
afterEach(() => {
13+
consoleErrorSpy.mockRestore()
14+
})
15+
16+
it("returns the parsed value for valid JSON", () => {
17+
expect(safeJsonParse<{ a: number }>('{"a":1}')).toEqual({ a: 1 })
18+
expect(consoleErrorSpy).not.toHaveBeenCalled()
19+
})
20+
21+
it("returns the default value for null, undefined, or empty input", () => {
22+
expect(safeJsonParse<string>(null, "fallback")).toBe("fallback")
23+
expect(safeJsonParse<string>(undefined, "fallback")).toBe("fallback")
24+
expect(safeJsonParse<string>("", "fallback")).toBe("fallback")
25+
expect(consoleErrorSpy).not.toHaveBeenCalled()
26+
})
27+
28+
it("returns the default value and logs the generic message when no context is given (backward compatible)", () => {
29+
const result = safeJsonParse<{ a: number }>("not json", undefined)
30+
expect(result).toBeUndefined()
31+
expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
32+
const message = consoleErrorSpy.mock.calls[0]?.[0]
33+
expect(message).toBe("Error parsing JSON:")
34+
})
35+
36+
it("includes the context label in the error log when provided", () => {
37+
const result = safeJsonParse<{ a: number }>("not json", undefined, "foo")
38+
expect(result).toBeUndefined()
39+
expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
40+
const message = consoleErrorSpy.mock.calls[0]?.[0]
41+
expect(message).toBe("Error parsing JSON (foo):")
42+
})
43+
})

packages/core/src/message-utils/safeJsonParse.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
*
44
* @param jsonString The string to parse
55
* @param defaultValue Value to return if parsing fails
6+
* @param context Optional label included in the error log so callers can be
7+
* identified when something other than valid JSON is supplied (e.g. a user
8+
* pasting a file path into a JSON field).
69
* @returns Parsed JSON object or defaultValue if parsing fails
710
*/
8-
export function safeJsonParse<T>(jsonString: string | null | undefined, defaultValue?: T): T | undefined {
11+
export function safeJsonParse<T>(
12+
jsonString: string | null | undefined,
13+
defaultValue?: T,
14+
context?: string,
15+
): T | undefined {
916
if (!jsonString) {
1017
return defaultValue
1118
}
@@ -14,7 +21,7 @@ export function safeJsonParse<T>(jsonString: string | null | undefined, defaultV
1421
return JSON.parse(jsonString) as T
1522
} catch (error) {
1623
// Log the error to the console for debugging.
17-
console.error("Error parsing JSON:", error)
24+
console.error(`Error parsing JSON${context ? ` (${context})` : ""}:`, error)
1825
return defaultValue
1926
}
2027
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// npx vitest run src/__tests__/looksLikeFilePath.spec.ts
2+
3+
import { looksLikeFilePath } from "../utils/looksLikeFilePath.js"
4+
5+
describe("looksLikeFilePath", () => {
6+
describe("nullish, empty, and whitespace-only input", () => {
7+
it.each([
8+
["undefined", undefined],
9+
["null", null],
10+
["empty string", ""],
11+
["whitespace only", " \t\n "],
12+
])("returns false for %s", (_label, value) => {
13+
expect(looksLikeFilePath(value)).toBe(false)
14+
})
15+
})
16+
17+
describe("path-shaped input", () => {
18+
it.each([
19+
["Windows backslash path", "C:\\Users\\dev\\sa.json"],
20+
["Windows forward-slash path", "C:/Users/dev/sa.json"],
21+
["Windows drive lowercase", "d:\\creds.json"],
22+
["POSIX absolute path", "/home/dev/sa.json"],
23+
["POSIX absolute root /tmp", "/tmp/creds.json"],
24+
["POSIX home path", "~/sa.json"],
25+
["POSIX relative ./", "./sa.json"],
26+
["POSIX relative ../", "../secrets/sa.json"],
27+
])("returns true for %s", (_label, value) => {
28+
expect(looksLikeFilePath(value)).toBe(true)
29+
})
30+
31+
it("returns true after trimming surrounding whitespace", () => {
32+
expect(looksLikeFilePath(" /tmp/creds.json ")).toBe(true)
33+
expect(looksLikeFilePath("\tC:\\sa.json\n")).toBe(true)
34+
})
35+
})
36+
37+
describe("JSON-shaped or bare-token input", () => {
38+
it.each([
39+
["JSON object", '{"type":"service_account","client_email":"x@y.z"}'],
40+
["JSON array", "[1,2,3]"],
41+
["JSON with leading whitespace", ' {"type":"service_account"}'],
42+
["bare token", "not-json-and-not-a-path"],
43+
["service-account-style email", "sa@project.iam.gserviceaccount.com"],
44+
])("returns false for %s", (_label, value) => {
45+
expect(looksLikeFilePath(value)).toBe(false)
46+
})
47+
})
48+
})

packages/types/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ export * from "./vscode.js"
3333
export * from "./worktree.js"
3434

3535
export * from "./providers/index.js"
36+
37+
export * from "./utils/looksLikeFilePath.js"

packages/types/src/providers/fireworks.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type FireworksModelId =
55
| "accounts/fireworks/models/kimi-k2-instruct-0905"
66
| "accounts/fireworks/models/kimi-k2-thinking"
77
| "accounts/fireworks/models/kimi-k2p5"
8+
| "accounts/fireworks/models/kimi-k2p6"
89
| "accounts/fireworks/models/minimax-m2"
910
| "accounts/fireworks/models/minimax-m2p1"
1011
| "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
@@ -13,10 +14,12 @@ export type FireworksModelId =
1314
| "accounts/fireworks/models/deepseek-v3"
1415
| "accounts/fireworks/models/deepseek-v3p1"
1516
| "accounts/fireworks/models/deepseek-v3p2"
17+
| "accounts/fireworks/models/deepseek-v4-pro"
1618
| "accounts/fireworks/models/glm-4p5"
1719
| "accounts/fireworks/models/glm-4p5-air"
1820
| "accounts/fireworks/models/glm-4p6"
1921
| "accounts/fireworks/models/glm-4p7"
22+
| "accounts/fireworks/models/glm-5p1"
2023
| "accounts/fireworks/models/gpt-oss-20b"
2124
| "accounts/fireworks/models/gpt-oss-120b"
2225
| "accounts/fireworks/models/llama-v3p3-70b-instruct"
@@ -240,4 +243,37 @@ export const fireworksModels = {
240243
description:
241244
"Llama 4 Scout is a smaller, faster variant of Llama 4 with multimodal capabilities, ideal for quick iterations and cost-effective deployments.",
242245
},
246+
"accounts/fireworks/models/kimi-k2p6": {
247+
maxTokens: 16384,
248+
contextWindow: 262144,
249+
supportsImages: true,
250+
supportsPromptCache: true,
251+
inputPrice: 0.95,
252+
outputPrice: 4.0,
253+
cacheReadsPrice: 0.16,
254+
description:
255+
"Kimi K2.6 is Moonshot AI's latest flagship agentic model, building on K2.5 with stronger long-horizon reasoning, multi-step tool use, and unified vision/text understanding.",
256+
},
257+
"accounts/fireworks/models/deepseek-v4-pro": {
258+
maxTokens: 16384,
259+
contextWindow: 1048576,
260+
supportsImages: false,
261+
supportsPromptCache: true,
262+
inputPrice: 1.74,
263+
outputPrice: 3.48,
264+
cacheReadsPrice: 0.14,
265+
description:
266+
"DeepSeek V4 Pro is the latest iteration of the DeepSeek model family, with improved reasoning, code generation, and instruction following over the V3 series.",
267+
},
268+
"accounts/fireworks/models/glm-5p1": {
269+
maxTokens: 25344,
270+
contextWindow: 202752,
271+
supportsImages: false,
272+
supportsPromptCache: true,
273+
inputPrice: 1.4,
274+
outputPrice: 4.4,
275+
cacheReadsPrice: 0.26,
276+
description:
277+
"Z.ai GLM-5.1 is the latest coding-focused model in the GLM family, with exceptional performance on complex programming tasks and enhanced reasoning and code generation quality.",
278+
},
243279
} as const satisfies Record<string, ModelInfo>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Returns true when a string is shaped like a filesystem path (Windows
2+
// drive-letter, POSIX absolute, POSIX home, POSIX relative). Pure and
3+
// dependency-free so it can be shared between the extension runtime (e.g.
4+
// parseVertexJsonCredentials) and the webview UI (e.g. the Vertex settings
5+
// warning), guaranteeing both surfaces agree on what "looks like a path"
6+
// means.
7+
//
8+
// Returns false for nullish, empty, and whitespace-only input — neither
9+
// call site should warn in those cases.
10+
export function looksLikeFilePath(value: string | null | undefined): boolean {
11+
if (value == null) {
12+
return false
13+
}
14+
const trimmed = value.trim()
15+
if (!trimmed) {
16+
return false
17+
}
18+
return (
19+
/^[A-Za-z]:[\\/]/.test(trimmed) || // Windows: C:\... or C:/...
20+
trimmed.startsWith("/") || // POSIX absolute: /home/...
21+
trimmed.startsWith("~") || // POSIX home: ~/...
22+
trimmed.startsWith(".") // POSIX relative: ./... or ../...
23+
)
24+
}

src/activate/CodeActionProvider.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
import * as vscode from "vscode"
22

3-
import { CodeActionName, CodeActionId } from "@roo-code/types"
3+
import { CodeActionId } from "@roo-code/types"
44
import { Package } from "../shared/package"
55

66
import { getCodeActionCommand } from "../utils/commands"
77
import { EditorUtils } from "../integrations/editor/EditorUtils"
8-
9-
export const TITLES: Record<CodeActionName, string> = {
10-
EXPLAIN: "Explain with Zoo Code",
11-
FIX: "Fix with Zoo Code",
12-
IMPROVE: "Improve with Zoo Code",
13-
ADD_TO_CONTEXT: "Add to Zoo Code",
14-
NEW_TASK: "New Roo Code Task",
15-
} as const
8+
import { t } from "../i18n"
169

1710
export class CodeActionProvider implements vscode.CodeActionProvider {
1811
public static readonly providedCodeActionKinds = [
@@ -51,12 +44,17 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
5144
const actions: vscode.CodeAction[] = []
5245

5346
actions.push(
54-
this.createAction(TITLES.ADD_TO_CONTEXT, vscode.CodeActionKind.QuickFix, "addToContext", [
55-
filePath,
56-
effectiveRange.text,
57-
effectiveRange.range.start.line + 1,
58-
effectiveRange.range.end.line + 1,
59-
]),
47+
this.createAction(
48+
t("common:codeActions.addToContext"),
49+
vscode.CodeActionKind.QuickFix,
50+
"addToContext",
51+
[
52+
filePath,
53+
effectiveRange.text,
54+
effectiveRange.range.start.line + 1,
55+
effectiveRange.range.end.line + 1,
56+
],
57+
),
6058
)
6159

6260
if (context.diagnostics.length > 0) {
@@ -66,7 +64,7 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
6664

6765
if (relevantDiagnostics.length > 0) {
6866
actions.push(
69-
this.createAction(TITLES.FIX, vscode.CodeActionKind.QuickFix, "fixCode", [
67+
this.createAction(t("common:codeActions.fix"), vscode.CodeActionKind.QuickFix, "fixCode", [
7068
filePath,
7169
effectiveRange.text,
7270
effectiveRange.range.start.line + 1,
@@ -77,7 +75,7 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
7775
}
7876
} else {
7977
actions.push(
80-
this.createAction(TITLES.EXPLAIN, vscode.CodeActionKind.QuickFix, "explainCode", [
78+
this.createAction(t("common:codeActions.explain"), vscode.CodeActionKind.QuickFix, "explainCode", [
8179
filePath,
8280
effectiveRange.text,
8381
effectiveRange.range.start.line + 1,
@@ -86,7 +84,7 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
8684
)
8785

8886
actions.push(
89-
this.createAction(TITLES.IMPROVE, vscode.CodeActionKind.QuickFix, "improveCode", [
87+
this.createAction(t("common:codeActions.improve"), vscode.CodeActionKind.QuickFix, "improveCode", [
9088
filePath,
9189
effectiveRange.text,
9290
effectiveRange.range.start.line + 1,

src/activate/__tests__/CodeActionProvider.spec.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,19 @@ import * as vscode from "vscode"
33

44
import { EditorUtils } from "../../integrations/editor/EditorUtils"
55

6-
import { CodeActionProvider, TITLES } from "../CodeActionProvider"
6+
import { CodeActionProvider } from "../CodeActionProvider"
7+
8+
vi.mock("../../i18n", () => ({
9+
t: vi.fn((key: string) => {
10+
const translations: Record<string, string> = {
11+
"common:codeActions.explain": "Explain with Zoo Code",
12+
"common:codeActions.fix": "Fix with Zoo Code",
13+
"common:codeActions.improve": "Improve with Zoo Code",
14+
"common:codeActions.addToContext": "Add to Zoo Code",
15+
}
16+
return translations[key] || key
17+
}),
18+
}))
719

820
vi.mock("vscode", () => ({
921
CodeAction: vi.fn().mockImplementation((title, kind) => ({
@@ -74,9 +86,9 @@ describe("CodeActionProvider", () => {
7486
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
7587

7688
expect(actions).toHaveLength(3)
77-
expect((actions as any)[0].title).toBe(TITLES.ADD_TO_CONTEXT)
78-
expect((actions as any)[1].title).toBe(TITLES.EXPLAIN)
79-
expect((actions as any)[2].title).toBe(TITLES.IMPROVE)
89+
expect((actions as any)[0].title).toBe("Add to Zoo Code")
90+
expect((actions as any)[1].title).toBe("Explain with Zoo Code")
91+
expect((actions as any)[2].title).toBe("Improve with Zoo Code")
8092
})
8193

8294
it("should provide fix action instead of fix logic when diagnostics exist", () => {
@@ -87,8 +99,8 @@ describe("CodeActionProvider", () => {
8799
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
88100

89101
expect(actions).toHaveLength(2)
90-
expect((actions as any).some((a: any) => a.title === `${TITLES.FIX}`)).toBe(true)
91-
expect((actions as any).some((a: any) => a.title === `${TITLES.ADD_TO_CONTEXT}`)).toBe(true)
102+
expect((actions as any).some((a: any) => a.title === "Fix with Zoo Code")).toBe(true)
103+
expect((actions as any).some((a: any) => a.title === "Add to Zoo Code")).toBe(true)
92104
})
93105

94106
it("should return empty array when no effective range", () => {

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,27 @@ describe("FireworksHandler", () => {
9494
expect(model.info).toEqual(expect.objectContaining(fireworksModels[testModelId]))
9595
})
9696

97+
it.each([
98+
{ modelId: "accounts/fireworks/models/glm-5p1" as const, contextWindow: 202752, inputPrice: 1.4, outputPrice: 4.4, cacheReadsPrice: 0.26 },
99+
{ modelId: "accounts/fireworks/models/kimi-k2p6" as const, contextWindow: 262144, inputPrice: 0.95, outputPrice: 4.0, cacheReadsPrice: 0.16 },
100+
{ modelId: "accounts/fireworks/models/deepseek-v4-pro" as const, contextWindow: 1048576, inputPrice: 1.74, outputPrice: 3.48, cacheReadsPrice: 0.14 },
101+
])("should expose newly added model $modelId", ({ modelId, contextWindow, inputPrice, outputPrice, cacheReadsPrice }) => {
102+
expect(fireworksModels[modelId]).toBeDefined()
103+
const info = fireworksModels[modelId]
104+
expect(info.maxTokens).toBeGreaterThan(0)
105+
expect(info.contextWindow).toBe(contextWindow)
106+
expect(info.inputPrice).toBe(inputPrice)
107+
expect(info.outputPrice).toBe(outputPrice)
108+
expect(info.cacheReadsPrice).toBe(cacheReadsPrice)
109+
expect(info.description).toBeTruthy()
110+
111+
const handlerWithModel = new FireworksHandler({
112+
apiModelId: modelId,
113+
fireworksApiKey: "test-fireworks-api-key",
114+
})
115+
expect(handlerWithModel.getModel().id).toBe(modelId)
116+
})
117+
97118
it("should return Kimi K2 Instruct model with correct configuration", () => {
98119
const testModelId: FireworksModelId = "accounts/fireworks/models/kimi-k2-instruct"
99120
const handlerWithModel = new FireworksHandler({

0 commit comments

Comments
 (0)