From fb6ea85c3a0490c6cb50b75c3cfb15db448dab03 Mon Sep 17 00:00:00 2001 From: "tobias@tobias-weiss.org" Date: Sat, 11 Apr 2026 17:52:55 +0200 Subject: [PATCH] feat: per-model timeout, permissions, fallback, and wildcard glob support - Add per-model timeout field on Config.Model and Provider.Model, used in bash tool execution - Add per-model permission overrides merged with priority: model > agent > global - Add fallback model mechanism with retry logic on 429/5xx/timeout errors - Replace manual regex wildcard matching with minimatch for proper glob support --- packages/opencode/src/config/config.ts | 3 + packages/opencode/src/provider/provider.ts | 6 + packages/opencode/src/session/llm.ts | 588 +++++++++++---------- packages/opencode/src/session/prompt.ts | 6 +- packages/opencode/src/tool/bash.ts | 2 +- packages/opencode/src/util/wildcard.ts | 19 +- 6 files changed, 337 insertions(+), 287 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 086d51abd8af..4a8daf3c0c8f 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -843,6 +843,8 @@ export namespace Config { provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), options: z.record(z.string(), z.any()), headers: z.record(z.string(), z.string()).optional(), + timeout: z.number().int().positive().optional().describe("Per-model bash timeout override in milliseconds"), + permission: Permission.optional().describe("Per-model tool permission override"), variants: z .record( z.string(), @@ -854,6 +856,7 @@ export namespace Config { ) .optional() .describe("Variant-specific configuration"), + fallback: z.array(z.string()).optional().describe("Fallback model IDs to try when this model fails (e.g., rate limits, server errors). Tries the first available model."), }) .partial() diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index e401a067c716..ee7e15ca399d 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -881,8 +881,11 @@ export namespace Provider { status: z.enum(["alpha", "beta", "deprecated", "active"]), options: z.record(z.string(), z.any()), headers: z.record(z.string(), z.string()), + timeout: z.number().int().positive().optional(), release_date: z.string(), + permission: z.record(z.string(), Config.PermissionRule).optional(), variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), + fallback: z.array(z.string()).optional(), }) .meta({ ref: "Model", @@ -1164,9 +1167,12 @@ export namespace Provider { output: model.limit?.output ?? existingModel?.limit?.output ?? 0, }, headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}), + timeout: model.timeout ?? existingModel?.timeout, family: model.family ?? existingModel?.family ?? "", release_date: model.release_date ?? existingModel?.release_date ?? "", + permission: model.permission ?? existingModel?.permission, variants: {}, + fallback: model.fallback ?? existingModel?.fallback, } const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {}) parsedModel.variants = mapValues( diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index f6e5c9a3f2fb..8c9e224bc45b 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -94,306 +94,344 @@ export namespace LLM { modelID: input.model.id, providerID: input.model.providerID, }) - const [language, cfg, provider, auth] = await Promise.all([ - Provider.getLanguage(input.model), - Config.get(), - Provider.getProvider(input.model.providerID), - Auth.get(input.model.providerID), - ]) - // TODO: move this to a proper hook - const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth" - - const system: string[] = [] - system.push( - [ - // use agent prompt otherwise provider prompt - ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), - // any custom prompt passed into this call - ...input.system, - // any custom prompt from last user message - ...(input.user.system ? [input.user.system] : []), - ] - .filter((x) => x) - .join("\n"), - ) - const header = system[0] - await Plugin.trigger( - "experimental.chat.system.transform", - { sessionID: input.sessionID, model: input.model }, - { system }, - ) - // rejoin to maintain 2-part structure for caching if header unchanged - if (system.length > 2 && system[0] === header) { - const rest = system.slice(1) - system.length = 0 - system.push(header, rest.join("\n")) - } + async function attempt(m: Provider.Model) { + const [language, cfg, provider, auth] = await Promise.all([ + Provider.getLanguage(m), + Config.get(), + Provider.getProvider(m.providerID), + Auth.get(m.providerID), + ]) + // TODO: move this to a proper hook + const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth" - const variant = - !input.small && input.model.variants && input.user.model.variant - ? input.model.variants[input.user.model.variant] - : {} - const base = input.small - ? ProviderTransform.smallOptions(input.model) - : ProviderTransform.options({ - model: input.model, - sessionID: input.sessionID, - providerOptions: provider.options, - }) - const options: Record = pipe( - base, - mergeDeep(input.model.options), - mergeDeep(input.agent.options), - mergeDeep(variant), - ) - if (isOpenaiOauth) { - options.instructions = system.join("\n") - } + const system: string[] = [] + system.push( + [ + // use agent prompt otherwise provider prompt + ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(m)), + // any custom prompt passed into this call + ...input.system, + // any custom prompt from last user message + ...(input.user.system ? [input.user.system] : []), + ] + .filter((x) => x) + .join("\n"), + ) + + const header = system[0] + await Plugin.trigger( + "experimental.chat.system.transform", + { sessionID: input.sessionID, model: m }, + { system }, + ) + // rejoin to maintain 2-part structure for caching if header unchanged + if (system.length > 2 && system[0] === header) { + const rest = system.slice(1) + system.length = 0 + system.push(header, rest.join("\n")) + } - const isWorkflow = language instanceof GitLabWorkflowLanguageModel - const messages = isOpenaiOauth - ? input.messages - : isWorkflow + const variant = + !input.small && m.variants && input.user.model.variant + ? m.variants[input.user.model.variant] + : {} + const base = input.small + ? ProviderTransform.smallOptions(m) + : ProviderTransform.options({ + model: m, + sessionID: input.sessionID, + providerOptions: provider.options, + }) + const options: Record = pipe( + base, + mergeDeep(m.options), + mergeDeep(input.agent.options), + mergeDeep(variant), + ) + if (isOpenaiOauth) { + options.instructions = system.join("\n") + } + + const isWorkflow = language instanceof GitLabWorkflowLanguageModel + const messages = isOpenaiOauth ? input.messages - : [ - ...system.map( - (x): ModelMessage => ({ - role: "system", - content: x, - }), - ), - ...input.messages, - ] - - const params = await Plugin.trigger( - "chat.params", - { - sessionID: input.sessionID, - agent: input.agent.name, - model: input.model, - provider, - message: input.user, - }, - { - temperature: input.model.capabilities.temperature - ? (input.agent.temperature ?? ProviderTransform.temperature(input.model)) - : undefined, - topP: input.agent.topP ?? ProviderTransform.topP(input.model), - topK: ProviderTransform.topK(input.model), - maxOutputTokens: ProviderTransform.maxOutputTokens(input.model), - options, - }, - ) + : isWorkflow + ? input.messages + : [ + ...system.map( + (x): ModelMessage => ({ + role: "system", + content: x, + }), + ), + ...input.messages, + ] - const { headers } = await Plugin.trigger( - "chat.headers", - { - sessionID: input.sessionID, - agent: input.agent.name, - model: input.model, - provider, - message: input.user, - }, - { - headers: {}, - }, - ) + const params = await Plugin.trigger( + "chat.params", + { + sessionID: input.sessionID, + agent: input.agent.name, + model: m, + provider, + message: input.user, + }, + { + temperature: m.capabilities.temperature + ? (input.agent.temperature ?? ProviderTransform.temperature(m)) + : undefined, + topP: input.agent.topP ?? ProviderTransform.topP(m), + topK: ProviderTransform.topK(m), + maxOutputTokens: ProviderTransform.maxOutputTokens(m), + options, + }, + ) - const tools = await resolveTools(input) - - // LiteLLM and some Anthropic proxies require the tools parameter to be present - // when message history contains tool calls, even if no tools are being used. - // Add a dummy tool that is never called to satisfy this validation. - // This is enabled for: - // 1. Providers with "litellm" in their ID or API ID (auto-detected) - // 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways) - const isLiteLLMProxy = - provider.options?.["litellmProxy"] === true || - input.model.providerID.toLowerCase().includes("litellm") || - input.model.api.id.toLowerCase().includes("litellm") - - // LiteLLM/Bedrock rejects requests where the message history contains tool - // calls but no tools param is present. When there are no active tools (e.g. - // during compaction), inject a stub tool to satisfy the validation requirement. - // The stub description explicitly tells the model not to call it. - if (isLiteLLMProxy && Object.keys(tools).length === 0 && hasToolCalls(input.messages)) { - tools["_noop"] = tool({ - description: "Do not call this tool. It exists only for API compatibility and must never be invoked.", - inputSchema: jsonSchema({ - type: "object", - properties: { - reason: { type: "string", description: "Unused" }, - }, - }), - execute: async () => ({ output: "", title: "", metadata: {} }), - }) - } + const { headers } = await Plugin.trigger( + "chat.headers", + { + sessionID: input.sessionID, + agent: input.agent.name, + model: m, + provider, + message: input.user, + }, + { + headers: {}, + }, + ) - // Wire up toolExecutor for DWS workflow models so that tool calls - // from the workflow service are executed via opencode's tool system - // and results sent back over the WebSocket. - if (language instanceof GitLabWorkflowLanguageModel) { - const workflowModel = language as GitLabWorkflowLanguageModel & { - sessionID?: string - sessionPreapprovedTools?: string[] - approvalHandler?: (approvalTools: { name: string; args: string }[]) => Promise<{ approved: boolean }> + const tools = await resolveTools(input) + + // LiteLLM and some Anthropic proxies require tools parameter to be present + // when message history contains tool calls, even if no tools are being used. + // Add a dummy tool that is never called to satisfy this validation. + // This is enabled for: + // 1. Providers with "litellm" in their ID or API ID (auto-detected) + // 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways) + const isLiteLLMProxy = + provider.options?.["litellmProxy"] === true || + m.providerID.toLowerCase().includes("litellm") || + m.api.id.toLowerCase().includes("litellm") + + // LiteLLM/Bedrock rejects requests where message history contains tool + // calls but no tools param is present. When there are no active tools (e.g. + // during compaction), inject a stub tool to satisfy the validation requirement. + // The stub description explicitly tells the model not to call it. + if (isLiteLLMProxy && Object.keys(tools).length === 0 && hasToolCalls(input.messages)) { + tools["_noop"] = tool({ + description: "Do not call this tool. It exists only for API compatibility and must never be invoked.", + inputSchema: jsonSchema({ + type: "object", + properties: { + reason: { type: "string", description: "Unused" }, + }, + }), + execute: async () => ({ output: "", title: "", metadata: {} }), + }) } - workflowModel.sessionID = input.sessionID - workflowModel.systemPrompt = system.join("\n") - workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => { - const t = tools[toolName] - if (!t || !t.execute) { - return { result: "", error: `Unknown tool: ${toolName}` } + + // Wire up toolExecutor for DWS workflow models so that tool calls + // from workflow service are executed via opencode's tool system + // and results sent back over the WebSocket. + if (language instanceof GitLabWorkflowLanguageModel) { + const workflowModel = language as GitLabWorkflowLanguageModel & { + sessionID?: string + sessionPreapprovedTools?: string[] + approvalHandler?: (approvalTools: { name: string; args: string }[]) => Promise<{ approved: boolean }> } - try { - const result = await t.execute!(JSON.parse(argsJson), { - toolCallId: _requestID, - messages: input.messages, - abortSignal: input.abort, - }) - const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result)) - return { - result: output, - metadata: typeof result === "object" ? result?.metadata : undefined, - title: typeof result === "object" ? result?.title : undefined, + workflowModel.sessionID = input.sessionID + workflowModel.systemPrompt = system.join("\n") + workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => { + const t = tools[toolName] + if (!t || !t.execute) { + return { result: "", error: `Unknown tool: ${toolName}` } + } + try { + const result = await t.execute!(JSON.parse(argsJson), { + toolCallId: _requestID, + messages: input.messages, + abortSignal: input.abort, + }) + const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result)) + return { + result: output, + metadata: typeof result === "object" ? result?.metadata : undefined, + title: typeof result === "object" ? result?.title : undefined, + } + } catch (e: any) { + return { result: "", error: e.message ?? String(e) } } - } catch (e: any) { - return { result: "", error: e.message ?? String(e) } } - } - const ruleset = Permission.merge(input.agent.permission ?? [], input.permission ?? []) - workflowModel.sessionPreapprovedTools = Object.keys(tools).filter((name) => { - const match = ruleset.findLast((rule) => Wildcard.match(name, rule.permission)) - return !match || match.action !== "ask" - }) - - const approvedToolsForSession = new Set() - workflowModel.approvalHandler = Instance.bind(async (approvalTools) => { - const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[] - // Auto-approve tools that were already approved in this session - // (prevents infinite approval loops for server-side MCP tools) - if (uniqueNames.every((name) => approvedToolsForSession.has(name))) { - return { approved: true } - } + const ruleset = Permission.merge(input.agent.permission ?? [], input.permission ?? []) + workflowModel.sessionPreapprovedTools = Object.keys(tools).filter((name) => { + const match = ruleset.findLast((rule) => Wildcard.match(name, rule.permission)) + return !match || match.action !== "ask" + }) - const id = PermissionID.ascending() - let reply: Permission.Reply | undefined - let unsub: (() => void) | undefined - try { - unsub = Bus.subscribe(Permission.Event.Replied, (evt) => { - if (evt.properties.requestID === id) reply = evt.properties.reply - }) - const toolPatterns = approvalTools.map((t: { name: string; args: string }) => { - try { - const parsed = JSON.parse(t.args) as Record - const title = (parsed?.title ?? parsed?.name ?? "") as string - return title ? `${t.name}: ${title}` : t.name - } catch { - return t.name - } - }) - const uniquePatterns = [...new Set(toolPatterns)] as string[] - await Permission.ask({ - id, - sessionID: SessionID.make(input.sessionID), - permission: "workflow_tool_approval", - patterns: uniquePatterns, - metadata: { tools: approvalTools }, - always: uniquePatterns, - ruleset: [], - }) - for (const name of uniqueNames) approvedToolsForSession.add(name) - workflowModel.sessionPreapprovedTools = [...(workflowModel.sessionPreapprovedTools ?? []), ...uniqueNames] - return { approved: true } - } catch { - return { approved: false } - } finally { - unsub?.() - } - }) - } + const approvedToolsForSession = new Set() + workflowModel.approvalHandler = Instance.bind(async (approvalTools) => { + const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[] + // Auto-approve tools that were already approved in this session + // (prevents infinite approval loops for server-side MCP tools) + if (uniqueNames.every((name) => approvedToolsForSession.has(name))) { + return { approved: true } + } - return streamText({ - onError(error) { - l.error("stream error", { - error, + const id = PermissionID.ascending() + let reply: Permission.Reply | undefined + let unsub: (() => void) | undefined + try { + unsub = Bus.subscribe(Permission.Event.Replied, (evt) => { + if (evt.properties.requestID === id) reply = evt.properties.reply + }) + const toolPatterns = approvalTools.map((t: { name: string; args: string }) => { + try { + const parsed = JSON.parse(t.args) as Record + const title = (parsed?.title ?? parsed?.name ?? "") as string + return title ? `${t.name}: ${title}` : t.name + } catch { + return t.name + } + }) + const uniquePatterns = [...new Set(toolPatterns)] as string[] + await Permission.ask({ + id, + sessionID: SessionID.make(input.sessionID), + permission: "workflow_tool_approval", + patterns: uniquePatterns, + metadata: { tools: approvalTools }, + always: uniquePatterns, + ruleset: [], + }) + for (const name of uniqueNames) approvedToolsForSession.add(name) + workflowModel.sessionPreapprovedTools = [...(workflowModel.sessionPreapprovedTools ?? []), ...uniqueNames] + return { approved: true } + } catch { + return { approved: false } + } finally { + unsub?.() + } }) - }, - async experimental_repairToolCall(failed) { - const lower = failed.toolCall.toolName.toLowerCase() - if (lower !== failed.toolCall.toolName && tools[lower]) { - l.info("repairing tool call", { - tool: failed.toolCall.toolName, - repaired: lower, + } + + return streamText({ + onError(error) { + l.error("stream error", { + error, }) + }, + async experimental_repairToolCall(failed) { + const lower = failed.toolCall.toolName.toLowerCase() + if (lower !== failed.toolCall.toolName && tools[lower]) { + l.info("repairing tool call", { + tool: failed.toolCall.toolName, + repaired: lower, + }) + return { + ...failed.toolCall, + toolName: lower, + } + } return { ...failed.toolCall, - toolName: lower, - } - } - return { - ...failed.toolCall, - input: JSON.stringify({ - tool: failed.toolCall.toolName, - error: failed.error.message, - }), - toolName: "invalid", - } - }, - temperature: params.temperature, - topP: params.topP, - topK: params.topK, - providerOptions: ProviderTransform.providerOptions(input.model, params.options), - activeTools: Object.keys(tools).filter((x) => x !== "invalid"), - tools, - toolChoice: input.toolChoice, - maxOutputTokens: params.maxOutputTokens, - abortSignal: input.abort, - headers: { - ...(input.model.providerID.startsWith("opencode") - ? { - "x-opencode-project": Instance.project.id, - "x-opencode-session": input.sessionID, - "x-opencode-request": input.user.id, - "x-opencode-client": Flag.OPENCODE_CLIENT, - } - : { - "x-session-affinity": input.sessionID, - ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), - "User-Agent": `opencode/${Installation.VERSION}`, + input: JSON.stringify({ + tool: failed.toolCall.toolName, + error: failed.error.message, }), - ...input.model.headers, - ...headers, - }, - maxRetries: input.retries ?? 0, - messages, - model: wrapLanguageModel({ - model: language, - middleware: [ - { - specificationVersion: "v3" as const, - async transformParams(args) { - if (args.type === "stream") { - // @ts-expect-error - args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options) + toolName: "invalid", + } + }, + temperature: params.temperature, + topP: params.topP, + topK: params.topK, + providerOptions: ProviderTransform.providerOptions(m, params.options), + activeTools: Object.keys(tools).filter((x) => x !== "invalid"), + tools, + toolChoice: input.toolChoice, + maxOutputTokens: params.maxOutputTokens, + abortSignal: input.abort, + headers: { + ...(m.providerID.startsWith("opencode") + ? { + "x-opencode-project": Instance.project.id, + "x-opencode-session": input.sessionID, + "x-opencode-request": input.user.id, + "x-opencode-client": Flag.OPENCODE_CLIENT, } - return args.params + : { + "x-session-affinity": input.sessionID, + ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), + "User-Agent": `opencode/${Installation.VERSION}`, + }), + ...m.headers, + ...headers, + }, + maxRetries: input.retries ?? 0, + messages, + model: wrapLanguageModel({ + model: language, + middleware: [ + { + specificationVersion: "v3" as const, + async transformParams(args) { + if (args.type === "stream") { + // @ts-expect-error + args.params.prompt = ProviderTransform.message(args.params.prompt, m, options) + } + return args.params + }, }, + ], + }), + experimental_telemetry: { + isEnabled: cfg.experimental?.openTelemetry, + metadata: { + userId: cfg.username ?? "unknown", + sessionId: input.sessionID, }, - ], - }), - experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, - metadata: { - userId: cfg.username ?? "unknown", - sessionId: input.sessionID, }, - }, - }) + }) + } + + try { + return await attempt(input.model) + } catch (e) { + const err = e as any + const isRetryable = + (err.statusCode === 429) || + (err.statusCode >= 500 && err.statusCode < 600) || + err.message?.toLowerCase().includes("timeout") || + err.name === "AbortError" + + if (isRetryable && input.model.fallback?.length) { + const fbId = input.model.fallback[0] + l.info("model failed, trying fallback", { + primaryModel: input.model.id, + fallbackModel: fbId, + error: err.message, + }) + + try { + const parsed = Provider.parseModel(fbId) + const fbModel = await Provider.getModel(parsed.providerID, parsed.modelID) + return await attempt(fbModel) + } catch (fbErr) { + l.error("fallback model also failed", { + fallbackModel: fbId, + error: fbErr, + }) + throw e + } + } + + throw e + } } + function resolveTools(input: Pick) { const disabled = Permission.disabled( Object.keys(input.tools), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2b092fc8fe2f..893ec6484a28 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -383,7 +383,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the ...req, sessionID: input.session.id, tool: { messageID: input.processor.message.id, callID: options.toolCallId }, - ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []), + ruleset: Permission.merge( + input.session.permission ?? [], + input.agent.permission, + input.model.permission ? Permission.fromConfig(input.model.permission) : [], + ), }) .pipe(Effect.orDie), }) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index eb49159f16dc..5b9b5bc2c90e 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -479,7 +479,7 @@ export const BashTool = Tool.define( if (params.timeout !== undefined && params.timeout < 0) { throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`) } - const timeout = params.timeout ?? DEFAULT_TIMEOUT + const timeout = params.timeout ?? ctx.extra?.model?.timeout ?? DEFAULT_TIMEOUT const ps = PS.has(name) const root = yield* parse(params.command, ps) const scan = yield* collect(root, cwd, ps, shell) diff --git a/packages/opencode/src/util/wildcard.ts b/packages/opencode/src/util/wildcard.ts index f54b6c85fd7f..293e0bfff8a9 100644 --- a/packages/opencode/src/util/wildcard.ts +++ b/packages/opencode/src/util/wildcard.ts @@ -1,22 +1,21 @@ +import { minimatch } from "minimatch" import { sortBy, pipe } from "remeda" export namespace Wildcard { export function match(str: string, pattern: string) { if (str) str = str.replaceAll("\\", "/") if (pattern) pattern = pattern.replaceAll("\\", "/") - let escaped = pattern - .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars - .replace(/\*/g, ".*") // * becomes .* - .replace(/\?/g, ".") // ? becomes . - // If pattern ends with " *" (space + wildcard), make the trailing part optional - // This allows "ls *" to match both "ls" and "ls -la" - if (escaped.endsWith(" .*")) { - escaped = escaped.slice(0, -3) + "( .*)?" + const opts = { nocase: process.platform === "win32" } + + // Preserve trailing " *" special case for backward compatibility + // "ls *" should match both "ls" and "ls -la" + if (pattern.endsWith(" *")) { + const base = pattern.slice(0, -2) + return minimatch(str, base, opts) || minimatch(str, pattern, opts) } - const flags = process.platform === "win32" ? "si" : "s" - return new RegExp("^" + escaped + "$", flags).test(str) + return minimatch(str, pattern, opts) } export function all(input: string, patterns: Record) {