Skip to content

Commit e674e4f

Browse files
author
Zeph Gillen
committed
Merge remote-tracking branch 'upstream/main' into local/merge-upstream-3.55.1
2 parents 062657a + 6470431 commit e674e4f

90 files changed

Lines changed: 3589 additions & 901 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.
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/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export const SECRET_STATE_KEYS = [
279279
"zaiApiKey",
280280
"fireworksApiKey",
281281
"vercelAiGatewayApiKey",
282+
"opencodeGoApiKey",
282283
"basetenApiKey",
283284
] as const
284285

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/provider-settings.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const dynamicProviders = [
4343
"unbound",
4444
"poe",
4545
"deepseek",
46+
"opencode-go",
4647
] as const
4748

4849
export type DynamicProvider = (typeof dynamicProviders)[number]
@@ -402,6 +403,11 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({
402403
vercelAiGatewayModelId: z.string().optional(),
403404
})
404405

406+
const opencodeGoSchema = baseProviderSettingsSchema.extend({
407+
opencodeGoApiKey: z.string().optional(),
408+
opencodeGoModelId: z.string().optional(),
409+
})
410+
405411
const basetenSchema = apiModelIdProviderModelSchema.extend({
406412
basetenApiKey: z.string().optional(),
407413
})
@@ -440,6 +446,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
440446
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
441447
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
442448
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
449+
opencodeGoSchema.merge(z.object({ apiProvider: z.literal("opencode-go") })),
443450
defaultSchema,
444451
])
445452

@@ -474,6 +481,7 @@ export const providerSettingsSchema = z.object({
474481
...fireworksSchema.shape,
475482
...qwenCodeSchema.shape,
476483
...vercelAiGatewaySchema.shape,
484+
...opencodeGoSchema.shape,
477485
...codebaseIndexProviderSchema.shape,
478486
})
479487

@@ -504,6 +512,7 @@ export const modelIdKeys = [
504512
"unboundModelId",
505513
"litellmModelId",
506514
"vercelAiGatewayModelId",
515+
"opencodeGoModelId",
507516
] as const satisfies readonly (keyof ProviderSettings)[]
508517

509518
export type ModelIdKey = (typeof modelIdKeys)[number]
@@ -549,6 +558,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
549558
zai: "apiModelId",
550559
fireworks: "apiModelId",
551560
"vercel-ai-gateway": "vercelAiGatewayModelId",
561+
"opencode-go": "opencodeGoModelId",
552562
}
553563

554564
/**
@@ -665,6 +675,7 @@ export const MODELS_BY_PROVIDER: Record<
665675
requesty: { id: "requesty", label: "Requesty", models: [] },
666676
unbound: { id: "unbound", label: "Unbound", models: [] },
667677
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
678+
"opencode-go": { id: "opencode-go", label: "Opencode Go", models: [] },
668679

669680
// Local providers; models discovered from localhost endpoints.
670681
lmstudio: { id: "lmstudio", label: "LM Studio", models: [] },

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>

packages/types/src/providers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export * from "./vertex.js"
2222
export * from "./vscode-llm.js"
2323
export * from "./xai.js"
2424
export * from "./vercel-ai-gateway.js"
25+
export * from "./opencode-go.js"
2526
export * from "./zai.js"
2627
export * from "./minimax.js"
2728
export * from "./mimo.js"
@@ -46,6 +47,7 @@ import { vertexDefaultModelId } from "./vertex.js"
4647
import { vscodeLlmDefaultModelId } from "./vscode-llm.js"
4748
import { xaiDefaultModelId } from "./xai.js"
4849
import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js"
50+
import { opencodeGoDefaultModelId } from "./opencode-go.js"
4951
import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js"
5052
import { minimaxDefaultModelId } from "./minimax.js"
5153
import { mimoDefaultModelId } from "./mimo.js"
@@ -115,6 +117,8 @@ export function getProviderDefaultModelId(
115117
return unboundDefaultModelId
116118
case "vercel-ai-gateway":
117119
return vercelAiGatewayDefaultModelId
120+
case "opencode-go":
121+
return opencodeGoDefaultModelId
118122
case "anthropic":
119123
case "gemini-cli":
120124
case "fake-ai":

0 commit comments

Comments
 (0)