diff --git a/STATS.md b/STATS.md index 0cf4d185f..a4c491bf4 100644 --- a/STATS.md +++ b/STATS.md @@ -142,3 +142,4 @@ | 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | | 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) | | 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) | +| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) | diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 867bc0fe9..740f67b7e 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -12,7 +12,7 @@ export namespace Agent { .object({ name: z.string(), description: z.string().optional(), - mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]), + mode: z.enum(["subagent", "primary", "all"]), builtIn: z.boolean(), topP: z.number().optional(), temperature: z.number().optional(), diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index a7f248758..b33f2ac63 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -665,7 +665,11 @@ export function Prompt(props: PromptProps) { return } - const pastedContent = event.text.trim() + // Normalize line endings at the boundary + // Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste + // Replace CRLF first, then any remaining CR + const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n") + const pastedContent = normalizedText.trim() if (!pastedContent) { command.trigger("prompt.paste") return diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 440d95818..51aa914a3 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -75,7 +75,7 @@ export namespace Config { for (const dir of directories) { await assertValid(dir) - if (dir.endsWith(".opencode")) { + if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) { for (const file of ["opencode.jsonc", "opencode.json"]) { log.debug(`loading config from ${path.join(dir, file)}`) result = mergeDeep(result, await loadFile(path.join(dir, file))) @@ -337,7 +337,7 @@ export namespace Config { export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote]) export type Mcp = z.infer - export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")]) + export const Permission = z.enum(["ask", "allow", "deny"]) export type Permission = z.infer export const Command = z.object({ @@ -358,7 +358,7 @@ export namespace Config { tools: z.record(z.string(), z.boolean()).optional(), disable: z.boolean().optional(), description: z.string().optional().describe("Description of when to use the agent"), - mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(), + mode: z.enum(["subagent", "primary", "all"]).optional(), color: z .string() .regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format") diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 185e3a9aa..b15df8b93 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -78,6 +78,22 @@ export namespace Provider { options: {}, } }, + "azure-cognitive-services": async () => { + const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"] + return { + autoload: false, + async getModel(sdk: any, modelID: string, options?: Record) { + if (options?.["useCompletionUrls"]) { + return sdk.chat(modelID) + } else { + return sdk.responses(modelID) + } + }, + options: { + baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, + }, + } + }, "amazon-bedrock": async () => { if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"]) return { autoload: false } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 1f1d8047a..74087e1bd 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -50,7 +50,7 @@ const ERRORS = { schema: resolver( z .object({ - data: z.any().nullable(), + data: z.any(), errors: z.array(z.record(z.string(), z.any())), success: z.literal(false), }) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index a3ccfc397..f184d5efe 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -89,10 +89,18 @@ export const BashTool = Tool.define("bash", { .text() .then((x) => x.trim()) log.info("resolved path", { arg, resolved }) - if (resolved && !Filesystem.contains(Instance.directory, resolved)) { - throw new Error( - `This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`, - ) + if (resolved) { + // Git Bash on Windows returns Unix-style paths like /c/Users/... + const normalized = + process.platform === "win32" && resolved.match(/^\/[a-z]\//) + ? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\") + : resolved + + if (!Filesystem.contains(Instance.directory, normalized)) { + throw new Error( + `This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`, + ) + } } } } diff --git a/packages/opencode/src/tool/batch.ts b/packages/opencode/src/tool/batch.ts index 45c62eb29..7d6449e7d 100644 --- a/packages/opencode/src/tool/batch.ts +++ b/packages/opencode/src/tool/batch.ts @@ -17,7 +17,6 @@ export const BatchTool = Tool.define("batch", async () => { }), ) .min(1, "Provide at least one tool call") - .max(10, "Too many tools in batch. Maximum allowed is 10.") .describe("Array of tool calls to execute in parallel"), }), formatValidationError(error) { @@ -34,34 +33,16 @@ export const BatchTool = Tool.define("batch", async () => { const { Session } = await import("../session") const { Identifier } = await import("../id/id") - const toolCalls = params.tool_calls + const toolCalls = params.tool_calls.slice(0, 10) + const discardedCalls = params.tool_calls.slice(10) const { ToolRegistry } = await import("./registry") const availableTools = await ToolRegistry.tools("", "") const toolMap = new Map(availableTools.map((t) => [t.id, t])) - const partIDs = new Map<(typeof toolCalls)[0], string>() - for (const call of toolCalls) { - const partID = Identifier.ascending("part") - partIDs.set(call, partID) - Session.updatePart({ - id: partID, - messageID: ctx.messageID, - sessionID: ctx.sessionID, - type: "tool", - tool: call.tool, - callID: partID, - state: { - status: "pending", - input: call.parameters, - raw: JSON.stringify(call), - }, - }) - } - const executeCall = async (call: (typeof toolCalls)[0]) => { const callStartTime = Date.now() - const partID = partIDs.get(call)! + const partID = Identifier.ascending("part") try { if (DISALLOWED.has(call.tool)) { @@ -77,6 +58,22 @@ export const BatchTool = Tool.define("batch", async () => { } const validatedParams = tool.parameters.parse(call.parameters) + await Session.updatePart({ + id: partID, + messageID: ctx.messageID, + sessionID: ctx.sessionID, + type: "tool", + tool: call.tool, + callID: partID, + state: { + status: "running", + input: call.parameters, + time: { + start: callStartTime, + }, + }, + }) + const result = await tool.execute(validatedParams, { ...ctx, callID: partID }) await Session.updatePart({ @@ -126,31 +123,48 @@ export const BatchTool = Tool.define("batch", async () => { const results = await Promise.all(toolCalls.map((call) => executeCall(call))) - const successfulCalls = results.filter((r) => r.success).length - const failedCalls = toolCalls.length - successfulCalls + // Add discarded calls as errors + const now = Date.now() + for (const call of discardedCalls) { + const partID = Identifier.ascending("part") + await Session.updatePart({ + id: partID, + messageID: ctx.messageID, + sessionID: ctx.sessionID, + type: "tool", + tool: call.tool, + callID: partID, + state: { + status: "error", + input: call.parameters, + error: "Maximum of 10 tools allowed in batch", + time: { start: now, end: now }, + }, + }) + results.push({ + success: false as const, + tool: call.tool, + error: new Error("Maximum of 10 tools allowed in batch"), + }) + } - const outputParts = results.map((r) => { - if (r.success) { - return `\n${r.result.output}\n` - } - const errorMessage = r.error instanceof Error ? r.error.message : String(r.error) - return `\nError: ${errorMessage}\n` - }) + const successfulCalls = results.filter((r) => r.success).length + const failedCalls = results.length - successfulCalls const outputMessage = failedCalls > 0 - ? `Executed ${successfulCalls}/${toolCalls.length} tools successfully. ${failedCalls} failed.\n\n${outputParts.join("\n\n")}` - : `All ${successfulCalls} tools executed successfully.\n\n${outputParts.join("\n\n")}\n\nKeep using the batch tool for optimal performance in your next response!` + ? `Executed ${successfulCalls}/${results.length} tools successfully. ${failedCalls} failed.` + : `All ${successfulCalls} tools executed successfully.\n\nKeep using the batch tool for optimal performance in your next response!` return { - title: `Batch execution (${successfulCalls}/${toolCalls.length} successful)`, + title: `Batch execution (${successfulCalls}/${results.length} successful)`, output: outputMessage, attachments: results.filter((result) => result.success).flatMap((r) => r.result.attachments ?? []), metadata: { - totalCalls: toolCalls.length, + totalCalls: results.length, successful: successfulCalls, failed: failedCalls, - tools: toolCalls.map((c) => c.tool), + tools: params.tool_calls.map((c) => c.tool), details: results.map((r) => ({ tool: r.tool, success: r.success })), }, } diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index d15651012..2309f8b77 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1183,7 +1183,7 @@ export type Config = { } export type BadRequestError = { - data: unknown | null + data: unknown errors: Array<{ [key: string]: unknown }> diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index d75c75d50..84a815490 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -229,6 +229,62 @@ Or if you already have an API key, you can select **Manually enter API Key** and --- +### Azure Cognitive Services + +1. Head over to the [Azure portal](https://portal.azure.com/) and create an **Azure OpenAI** resource. You'll need: + - **Resource name**: This becomes part of your API endpoint (`https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/`) + - **API key**: Either `KEY 1` or `KEY 2` from your resource + +2. Go to [Azure AI Foundry](https://ai.azure.com/) and deploy a model. + + :::note + The deployment name must match the model name for opencode to work properly. + ::: + +3. Run `opencode auth login` and select **Azure**. + + ```bash + $ opencode auth login + + ┌ Add credential + │ + ◆ Select provider + │ ● Azure Cognitive Services + │ ... + └ + ``` + +4. Enter your API key. + + ```bash + $ opencode auth login + + ┌ Add credential + │ + ◇ Select provider + │ Azure Cognitive Services + │ + ◇ Enter your API key + │ _ + └ + ``` + +5. Set your resource name as an environment variable: + + ```bash + AZURE_COGNITIVE_SERVICES_RESOURCE_NAME=XXX opencode + ``` + + Or add it to your bash profile: + + ```bash title="~/.bash_profile" + export AZURE_COGNITIVE_SERVICES_RESOURCE_NAME=XXX + ``` + +6. Run the `/models` command to select your deployed model. + +--- + ### Baseten 1. Head over to the [Baseten](https://app.baseten.co/), create an account, and generate an API key.