From 23ed876835fbd81bdf23927be1745fca474f0251 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 17:15:18 -0400 Subject: [PATCH 1/2] fix: narrow several `from any` type assertions in opencode core (#22926) --- .../opencode/src/cli/cmd/tui/context/kv.tsx | 2 +- packages/opencode/src/cli/error.ts | 8 +++---- packages/opencode/src/lsp/lsp.ts | 9 ++++---- packages/opencode/src/npm/index.ts | 13 +++++++++-- packages/opencode/src/provider/provider.ts | 6 +++-- packages/opencode/src/session/session.ts | 22 ++++++++++--------- packages/opencode/src/tool/tool.ts | 2 +- packages/opencode/src/util/filesystem.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- 9 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 39e976b0e555..803752e76649 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -12,7 +12,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ const [store, setStore] = createStore>() const filePath = path.join(Global.Path.state, "kv.json") - Filesystem.readJson(filePath) + Filesystem.readJson>(filePath) .then((x) => { setStore(x) }) diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 89b557e2d27c..f286b5166f72 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -28,10 +28,10 @@ export function FormatError(input: unknown) { // ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] } if (NamedError.hasName(input, "ProviderModelNotFoundError")) { const data = (input as ErrorLike).data - const suggestions = data?.suggestions as string[] | undefined + const suggestions: string[] = Array.isArray(data?.suggestions) ? data.suggestions : [] return [ `Model not found: ${data?.providerID}/${data?.modelID}`, - ...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), + ...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), `Try: \`opencode models\` to list available models`, `Or check your config (opencode.json) provider/model names`, ].join("\n") @@ -64,10 +64,10 @@ export function FormatError(input: unknown) { const data = (input as ErrorLike).data const path = data?.path const message = data?.message - const issues = data?.issues as Array<{ message: string; path: string[] }> | undefined + const issues: Array<{ message: string; path: string[] }> = Array.isArray(data?.issues) ? data.issues : [] return [ `Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""), - ...(issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []), + ...issues.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")), ].join("\n") } diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index d4d1e7563492..d895e732562f 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -440,12 +440,11 @@ export const layer = Layer.effect( const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) { const results = yield* runAll((client) => client.connection - .sendRequest("workspace/symbol", { query }) - .then((result: any) => result.filter((x: Symbol) => kinds.includes(x.kind))) - .then((result: any) => result.slice(0, 10)) - .catch(() => []), + .sendRequest("workspace/symbol", { query }) + .then((result) => result.filter((x) => kinds.includes(x.kind)).slice(0, 10)) + .catch(() => [] as Symbol[]), ) - return results.flat() as Symbol[] + return results.flat() }) const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) { diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index 174df12974d5..425b27f42061 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -124,8 +124,17 @@ export async function install(dir: string) { return } - const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({})) - const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({})) + type PackageDeps = Record + type PackageJson = { + dependencies?: PackageDeps + devDependencies?: PackageDeps + peerDependencies?: PackageDeps + optionalDependencies?: PackageDeps + } + const pkg: PackageJson = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({})) + const lock: { packages?: Record } = await Filesystem.readJson<{ + packages?: Record + }>(path.join(dir, "package-lock.json")).catch(() => ({})) const declared = new Set([ ...Object.keys(pkg.dependencies || {}), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 43ae9a5e9f64..a7297634e722 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -547,12 +547,14 @@ function custom(dep: CustomDep): Record { }, async getModel(sdk: any, modelID: string, options?: Record) { if (modelID.startsWith("duo-workflow-")) { - const workflowRef = options?.workflowRef as string | undefined + const workflowRef = typeof options?.workflowRef === "string" ? options.workflowRef : undefined // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow" + const workflowDefinition = + typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined const model = sdk.workflowChat(sdkModelID, { featureFlags, - workflowDefinition: options?.workflowDefinition as string | undefined, + workflowDefinition, }) if (workflowRef) { model.selectedModelRef = workflowRef diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 8c5fc29e4a96..a453b1981501 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -272,16 +272,18 @@ export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsa input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0, ) const cacheWriteInputTokens = safe( - (input.usage.inputTokenDetails?.cacheWriteTokens ?? - input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? - // google-vertex-anthropic returns metadata under "vertex" key - // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages') - input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? - // @ts-expect-error - input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? - // @ts-expect-error - input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? - 0) as number, + Number( + input.usage.inputTokenDetails?.cacheWriteTokens ?? + input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? + // google-vertex-anthropic returns metadata under "vertex" key + // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages') + input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? + // @ts-expect-error + input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? + // @ts-expect-error + input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? + 0, + ), ) // AI SDK v6 normalized inputTokens to include cached tokens across all providers diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 0ea0435fb1f0..179149afd287 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -19,7 +19,7 @@ export type Context = { agent: string abort: AbortSignal callID?: string - extra?: { [key: string]: any } + extra?: { [key: string]: unknown } messages: MessageV2.WithParts[] metadata(input: { title?: string; metadata?: M }): Effect.Effect ask(input: Omit): Effect.Effect diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 3ff2c6e3f466..6c4d455224e9 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -39,7 +39,7 @@ export async function readText(p: string): Promise { return readFile(p, "utf-8") } -export async function readJson(p: string): Promise { +export async function readJson(p: string): Promise { return JSON.parse(await readFile(p, "utf-8")) } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index c41f395e51e1..3e90842e1885 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -757,7 +757,7 @@ test("updates config and writes to file", async () => { const newConfig = { model: "updated/model" } await save(newConfig as any) - const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "config.json")) + const writtenConfig = await Filesystem.readJson<{ model: string }>(path.join(tmp.path, "config.json")) expect(writtenConfig.model).toBe("updated/model") }, }) From 0bba55332d2ee8bb035734c9257a040a300091bb Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 17:29:27 -0400 Subject: [PATCH 2/2] refactor: unwrap ConfigMCP namespace + self-reexport --- packages/opencode/src/config/mcp.ts | 130 ++++++++++++++-------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/opencode/src/config/mcp.ts b/packages/opencode/src/config/mcp.ts index fb8f8caa4118..fda933b421ad 100644 --- a/packages/opencode/src/config/mcp.ts +++ b/packages/opencode/src/config/mcp.ts @@ -1,70 +1,70 @@ import z from "zod" -export namespace ConfigMCP { - export const Local = z - .object({ - type: z.literal("local").describe("Type of MCP server connection"), - command: z.string().array().describe("Command and arguments to run the MCP server"), - environment: z - .record(z.string(), z.string()) - .optional() - .describe("Environment variables to set when running the MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), - }) - .strict() - .meta({ - ref: "McpLocalConfig", - }) +export const Local = z + .object({ + type: z.literal("local").describe("Type of MCP server connection"), + command: z.string().array().describe("Command and arguments to run the MCP server"), + environment: z + .record(z.string(), z.string()) + .optional() + .describe("Environment variables to set when running the MCP server"), + enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), + timeout: z + .number() + .int() + .positive() + .optional() + .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + }) + .strict() + .meta({ + ref: "McpLocalConfig", + }) - export const OAuth = z - .object({ - clientId: z - .string() - .optional() - .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), - clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"), - scope: z.string().optional().describe("OAuth scopes to request during authorization"), - redirectUri: z - .string() - .optional() - .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), - }) - .strict() - .meta({ - ref: "McpOAuthConfig", - }) - export type OAuth = z.infer +export const OAuth = z + .object({ + clientId: z + .string() + .optional() + .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), + clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"), + scope: z.string().optional().describe("OAuth scopes to request during authorization"), + redirectUri: z + .string() + .optional() + .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), + }) + .strict() + .meta({ + ref: "McpOAuthConfig", + }) +export type OAuth = z.infer - export const Remote = z - .object({ - type: z.literal("remote").describe("Type of MCP server connection"), - url: z.string().describe("URL of the remote MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), - headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"), - oauth: z - .union([OAuth, z.literal(false)]) - .optional() - .describe( - "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", - ), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), - }) - .strict() - .meta({ - ref: "McpRemoteConfig", - }) +export const Remote = z + .object({ + type: z.literal("remote").describe("Type of MCP server connection"), + url: z.string().describe("URL of the remote MCP server"), + enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), + headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"), + oauth: z + .union([OAuth, z.literal(false)]) + .optional() + .describe( + "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", + ), + timeout: z + .number() + .int() + .positive() + .optional() + .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + }) + .strict() + .meta({ + ref: "McpRemoteConfig", + }) - export const Info = z.discriminatedUnion("type", [Local, Remote]) - export type Info = z.infer -} +export const Info = z.discriminatedUnion("type", [Local, Remote]) +export type Info = z.infer + +export * as ConfigMCP from "./mcp"