refactor: unwrap ConfigMCP namespace + self-reexport (#22948)

This commit is contained in:
Kit Langton 2026-04-16 19:52:04 -04:00 committed by GitHub
commit 5d47ea0918
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 104 additions and 92 deletions

View file

@ -12,7 +12,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
const [store, setStore] = createStore<Record<string, any>>() const [store, setStore] = createStore<Record<string, any>>()
const filePath = path.join(Global.Path.state, "kv.json") const filePath = path.join(Global.Path.state, "kv.json")
Filesystem.readJson(filePath) Filesystem.readJson<Record<string, any>>(filePath)
.then((x) => { .then((x) => {
setStore(x) setStore(x)
}) })

View file

@ -28,10 +28,10 @@ export function FormatError(input: unknown) {
// ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] } // ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] }
if (NamedError.hasName(input, "ProviderModelNotFoundError")) { if (NamedError.hasName(input, "ProviderModelNotFoundError")) {
const data = (input as ErrorLike).data const data = (input as ErrorLike).data
const suggestions = data?.suggestions as string[] | undefined const suggestions: string[] = Array.isArray(data?.suggestions) ? data.suggestions : []
return [ return [
`Model not found: ${data?.providerID}/${data?.modelID}`, `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`, `Try: \`opencode models\` to list available models`,
`Or check your config (opencode.json) provider/model names`, `Or check your config (opencode.json) provider/model names`,
].join("\n") ].join("\n")
@ -64,10 +64,10 @@ export function FormatError(input: unknown) {
const data = (input as ErrorLike).data const data = (input as ErrorLike).data
const path = data?.path const path = data?.path
const message = data?.message 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 [ return [
`Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""), `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") ].join("\n")
} }

View file

@ -1,70 +1,70 @@
import z from "zod" import z from "zod"
export namespace ConfigMCP { export const Local = z
export const Local = z .object({
.object({ type: z.literal("local").describe("Type of MCP server connection"),
type: z.literal("local").describe("Type of MCP server connection"), command: z.string().array().describe("Command and arguments to run the MCP server"),
command: z.string().array().describe("Command and arguments to run the MCP server"), environment: z
environment: z .record(z.string(), z.string())
.record(z.string(), z.string()) .optional()
.optional() .describe("Environment variables to set when running the MCP server"),
.describe("Environment variables to set when running the MCP server"), enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), timeout: z
timeout: z .number()
.number() .int()
.int() .positive()
.positive() .optional()
.optional() .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
.describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), })
}) .strict()
.strict() .meta({
.meta({ ref: "McpLocalConfig",
ref: "McpLocalConfig", })
})
export const OAuth = z export const OAuth = z
.object({ .object({
clientId: z clientId: z
.string() .string()
.optional() .optional()
.describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), .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)"), 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"), scope: z.string().optional().describe("OAuth scopes to request during authorization"),
redirectUri: z redirectUri: z
.string() .string()
.optional() .optional()
.describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."),
}) })
.strict() .strict()
.meta({ .meta({
ref: "McpOAuthConfig", ref: "McpOAuthConfig",
}) })
export type OAuth = z.infer<typeof OAuth> export type OAuth = z.infer<typeof OAuth>
export const Remote = z export const Remote = z
.object({ .object({
type: z.literal("remote").describe("Type of MCP server connection"), type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"), url: z.string().describe("URL of the remote MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), 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"), headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
oauth: z oauth: z
.union([OAuth, z.literal(false)]) .union([OAuth, z.literal(false)])
.optional() .optional()
.describe( .describe(
"OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
), ),
timeout: z timeout: z
.number() .number()
.int() .int()
.positive() .positive()
.optional() .optional()
.describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
}) })
.strict() .strict()
.meta({ .meta({
ref: "McpRemoteConfig", ref: "McpRemoteConfig",
}) })
export const Info = z.discriminatedUnion("type", [Local, Remote]) export const Info = z.discriminatedUnion("type", [Local, Remote])
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
}
export * as ConfigMCP from "./mcp"

View file

@ -440,12 +440,11 @@ export const layer = Layer.effect(
const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) { const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) {
const results = yield* runAll((client) => const results = yield* runAll((client) =>
client.connection client.connection
.sendRequest("workspace/symbol", { query }) .sendRequest<Symbol[]>("workspace/symbol", { query })
.then((result: any) => result.filter((x: Symbol) => kinds.includes(x.kind))) .then((result) => result.filter((x) => kinds.includes(x.kind)).slice(0, 10))
.then((result: any) => result.slice(0, 10)) .catch(() => [] as Symbol[]),
.catch(() => []),
) )
return results.flat() as Symbol[] return results.flat()
}) })
const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) { const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) {

View file

@ -124,8 +124,17 @@ export async function install(dir: string) {
return return
} }
const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({})) type PackageDeps = Record<string, string>
const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({})) type PackageJson = {
dependencies?: PackageDeps
devDependencies?: PackageDeps
peerDependencies?: PackageDeps
optionalDependencies?: PackageDeps
}
const pkg: PackageJson = await Filesystem.readJson<PackageJson>(path.join(dir, "package.json")).catch(() => ({}))
const lock: { packages?: Record<string, PackageJson> } = await Filesystem.readJson<{
packages?: Record<string, PackageJson>
}>(path.join(dir, "package-lock.json")).catch(() => ({}))
const declared = new Set([ const declared = new Set([
...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.dependencies || {}),

View file

@ -547,12 +547,14 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
}, },
async getModel(sdk: any, modelID: string, options?: Record<string, any>) { async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
if (modelID.startsWith("duo-workflow-")) { 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 // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow" const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
const workflowDefinition =
typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined
const model = sdk.workflowChat(sdkModelID, { const model = sdk.workflowChat(sdkModelID, {
featureFlags, featureFlags,
workflowDefinition: options?.workflowDefinition as string | undefined, workflowDefinition,
}) })
if (workflowRef) { if (workflowRef) {
model.selectedModelRef = workflowRef model.selectedModelRef = workflowRef

View file

@ -272,16 +272,18 @@ export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsa
input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0, input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0,
) )
const cacheWriteInputTokens = safe( const cacheWriteInputTokens = safe(
(input.usage.inputTokenDetails?.cacheWriteTokens ?? Number(
input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? input.usage.inputTokenDetails?.cacheWriteTokens ??
// google-vertex-anthropic returns metadata under "vertex" key input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages') // google-vertex-anthropic returns metadata under "vertex" key
input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages')
// @ts-expect-error input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ??
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? // @ts-expect-error
// @ts-expect-error input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? // @ts-expect-error
0) as number, input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ??
0,
),
) )
// AI SDK v6 normalized inputTokens to include cached tokens across all providers // AI SDK v6 normalized inputTokens to include cached tokens across all providers

View file

@ -19,7 +19,7 @@ export type Context<M extends Metadata = Metadata> = {
agent: string agent: string
abort: AbortSignal abort: AbortSignal
callID?: string callID?: string
extra?: { [key: string]: any } extra?: { [key: string]: unknown }
messages: MessageV2.WithParts[] messages: MessageV2.WithParts[]
metadata(input: { title?: string; metadata?: M }): Effect.Effect<void> metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void> ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>

View file

@ -39,7 +39,7 @@ export async function readText(p: string): Promise<string> {
return readFile(p, "utf-8") return readFile(p, "utf-8")
} }
export async function readJson<T = any>(p: string): Promise<T> { export async function readJson<T = unknown>(p: string): Promise<T> {
return JSON.parse(await readFile(p, "utf-8")) return JSON.parse(await readFile(p, "utf-8"))
} }

View file

@ -757,7 +757,7 @@ test("updates config and writes to file", async () => {
const newConfig = { model: "updated/model" } const newConfig = { model: "updated/model" }
await save(newConfig as any) 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") expect(writtenConfig.model).toBe("updated/model")
}, },
}) })