Skip to content

Commit 1df1c32

Browse files
Merge branch 'main' into feat/172-opencode-go
2 parents baf3dfa + 78d3dac commit 1df1c32

66 files changed

Lines changed: 2670 additions & 840 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.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@types/glob": "^9.0.0",
3535
"@types/node": "^24.1.0",
3636
"@vscode/vsce": "3.3.2",
37-
"esbuild": "^0.25.0",
37+
"esbuild": "0.28.0",
3838
"eslint": "^9.27.0",
3939
"glob": "^11.1.0",
4040
"husky": "^9.1.7",
@@ -60,7 +60,9 @@
6060
],
6161
"overrides": {
6262
"tar-fs": ">=3.1.1",
63-
"esbuild": ">=0.25.0",
63+
"esbuild": "0.28.0",
64+
"rollup": "4.60.4",
65+
"vite": "8.0.14",
6466
"undici": ">=5.29.0",
6567
"form-data": ">=4.0.4",
6668
"bluebird": ">=3.7.2",
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+
}

0 commit comments

Comments
 (0)