Skip to content

Commit f9fc8ed

Browse files
committed
Merge branch 'dev' into agent-loop
2 parents e6d5494 + 9fd43ec commit f9fc8ed

File tree

10 files changed

+146
-47
lines changed

10 files changed

+146
-47
lines changed

STATS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,4 @@
142142
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
143143
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
144144
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
145+
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |

packages/opencode/src/agent/agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export namespace Agent {
1212
.object({
1313
name: z.string(),
1414
description: z.string().optional(),
15-
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
15+
mode: z.enum(["subagent", "primary", "all"]),
1616
builtIn: z.boolean(),
1717
topP: z.number().optional(),
1818
temperature: z.number().optional(),

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,11 @@ export function Prompt(props: PromptProps) {
665665
return
666666
}
667667

668-
const pastedContent = event.text.trim()
668+
// Normalize line endings at the boundary
669+
// Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
670+
// Replace CRLF first, then any remaining CR
671+
const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
672+
const pastedContent = normalizedText.trim()
669673
if (!pastedContent) {
670674
command.trigger("prompt.paste")
671675
return

packages/opencode/src/config/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export namespace Config {
7575
for (const dir of directories) {
7676
await assertValid(dir)
7777

78-
if (dir.endsWith(".opencode")) {
78+
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
7979
for (const file of ["opencode.jsonc", "opencode.json"]) {
8080
log.debug(`loading config from ${path.join(dir, file)}`)
8181
result = mergeDeep(result, await loadFile(path.join(dir, file)))
@@ -337,7 +337,7 @@ export namespace Config {
337337
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
338338
export type Mcp = z.infer<typeof Mcp>
339339

340-
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
340+
export const Permission = z.enum(["ask", "allow", "deny"])
341341
export type Permission = z.infer<typeof Permission>
342342

343343
export const Command = z.object({
@@ -358,7 +358,7 @@ export namespace Config {
358358
tools: z.record(z.string(), z.boolean()).optional(),
359359
disable: z.boolean().optional(),
360360
description: z.string().optional().describe("Description of when to use the agent"),
361-
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
361+
mode: z.enum(["subagent", "primary", "all"]).optional(),
362362
color: z
363363
.string()
364364
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")

packages/opencode/src/provider/provider.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,22 @@ export namespace Provider {
7878
options: {},
7979
}
8080
},
81+
"azure-cognitive-services": async () => {
82+
const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"]
83+
return {
84+
autoload: false,
85+
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
86+
if (options?.["useCompletionUrls"]) {
87+
return sdk.chat(modelID)
88+
} else {
89+
return sdk.responses(modelID)
90+
}
91+
},
92+
options: {
93+
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
94+
},
95+
}
96+
},
8197
"amazon-bedrock": async () => {
8298
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
8399
return { autoload: false }

packages/opencode/src/server/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const ERRORS = {
5050
schema: resolver(
5151
z
5252
.object({
53-
data: z.any().nullable(),
53+
data: z.any(),
5454
errors: z.array(z.record(z.string(), z.any())),
5555
success: z.literal(false),
5656
})

packages/opencode/src/tool/bash.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,18 @@ export const BashTool = Tool.define("bash", {
8989
.text()
9090
.then((x) => x.trim())
9191
log.info("resolved path", { arg, resolved })
92-
if (resolved && !Filesystem.contains(Instance.directory, resolved)) {
93-
throw new Error(
94-
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
95-
)
92+
if (resolved) {
93+
// Git Bash on Windows returns Unix-style paths like /c/Users/...
94+
const normalized =
95+
process.platform === "win32" && resolved.match(/^\/[a-z]\//)
96+
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
97+
: resolved
98+
99+
if (!Filesystem.contains(Instance.directory, normalized)) {
100+
throw new Error(
101+
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
102+
)
103+
}
96104
}
97105
}
98106
}

packages/opencode/src/tool/batch.ts

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export const BatchTool = Tool.define("batch", async () => {
1717
}),
1818
)
1919
.min(1, "Provide at least one tool call")
20-
.max(10, "Too many tools in batch. Maximum allowed is 10.")
2120
.describe("Array of tool calls to execute in parallel"),
2221
}),
2322
formatValidationError(error) {
@@ -34,34 +33,16 @@ export const BatchTool = Tool.define("batch", async () => {
3433
const { Session } = await import("../session")
3534
const { Identifier } = await import("../id/id")
3635

37-
const toolCalls = params.tool_calls
36+
const toolCalls = params.tool_calls.slice(0, 10)
37+
const discardedCalls = params.tool_calls.slice(10)
3838

3939
const { ToolRegistry } = await import("./registry")
4040
const availableTools = await ToolRegistry.tools("", "")
4141
const toolMap = new Map(availableTools.map((t) => [t.id, t]))
4242

43-
const partIDs = new Map<(typeof toolCalls)[0], string>()
44-
for (const call of toolCalls) {
45-
const partID = Identifier.ascending("part")
46-
partIDs.set(call, partID)
47-
Session.updatePart({
48-
id: partID,
49-
messageID: ctx.messageID,
50-
sessionID: ctx.sessionID,
51-
type: "tool",
52-
tool: call.tool,
53-
callID: partID,
54-
state: {
55-
status: "pending",
56-
input: call.parameters,
57-
raw: JSON.stringify(call),
58-
},
59-
})
60-
}
61-
6243
const executeCall = async (call: (typeof toolCalls)[0]) => {
6344
const callStartTime = Date.now()
64-
const partID = partIDs.get(call)!
45+
const partID = Identifier.ascending("part")
6546

6647
try {
6748
if (DISALLOWED.has(call.tool)) {
@@ -77,6 +58,22 @@ export const BatchTool = Tool.define("batch", async () => {
7758
}
7859
const validatedParams = tool.parameters.parse(call.parameters)
7960

61+
await Session.updatePart({
62+
id: partID,
63+
messageID: ctx.messageID,
64+
sessionID: ctx.sessionID,
65+
type: "tool",
66+
tool: call.tool,
67+
callID: partID,
68+
state: {
69+
status: "running",
70+
input: call.parameters,
71+
time: {
72+
start: callStartTime,
73+
},
74+
},
75+
})
76+
8077
const result = await tool.execute(validatedParams, { ...ctx, callID: partID })
8178

8279
await Session.updatePart({
@@ -126,31 +123,48 @@ export const BatchTool = Tool.define("batch", async () => {
126123

127124
const results = await Promise.all(toolCalls.map((call) => executeCall(call)))
128125

129-
const successfulCalls = results.filter((r) => r.success).length
130-
const failedCalls = toolCalls.length - successfulCalls
126+
// Add discarded calls as errors
127+
const now = Date.now()
128+
for (const call of discardedCalls) {
129+
const partID = Identifier.ascending("part")
130+
await Session.updatePart({
131+
id: partID,
132+
messageID: ctx.messageID,
133+
sessionID: ctx.sessionID,
134+
type: "tool",
135+
tool: call.tool,
136+
callID: partID,
137+
state: {
138+
status: "error",
139+
input: call.parameters,
140+
error: "Maximum of 10 tools allowed in batch",
141+
time: { start: now, end: now },
142+
},
143+
})
144+
results.push({
145+
success: false as const,
146+
tool: call.tool,
147+
error: new Error("Maximum of 10 tools allowed in batch"),
148+
})
149+
}
131150

132-
const outputParts = results.map((r) => {
133-
if (r.success) {
134-
return `<tool_result name="${r.tool}">\n${r.result.output}\n</tool_result>`
135-
}
136-
const errorMessage = r.error instanceof Error ? r.error.message : String(r.error)
137-
return `<tool_result name="${r.tool}">\nError: ${errorMessage}\n</tool_result>`
138-
})
151+
const successfulCalls = results.filter((r) => r.success).length
152+
const failedCalls = results.length - successfulCalls
139153

140154
const outputMessage =
141155
failedCalls > 0
142-
? `Executed ${successfulCalls}/${toolCalls.length} tools successfully. ${failedCalls} failed.\n\n${outputParts.join("\n\n")}`
143-
: `All ${successfulCalls} tools executed successfully.\n\n${outputParts.join("\n\n")}\n\nKeep using the batch tool for optimal performance in your next response!`
156+
? `Executed ${successfulCalls}/${results.length} tools successfully. ${failedCalls} failed.`
157+
: `All ${successfulCalls} tools executed successfully.\n\nKeep using the batch tool for optimal performance in your next response!`
144158

145159
return {
146-
title: `Batch execution (${successfulCalls}/${toolCalls.length} successful)`,
160+
title: `Batch execution (${successfulCalls}/${results.length} successful)`,
147161
output: outputMessage,
148162
attachments: results.filter((result) => result.success).flatMap((r) => r.result.attachments ?? []),
149163
metadata: {
150-
totalCalls: toolCalls.length,
164+
totalCalls: results.length,
151165
successful: successfulCalls,
152166
failed: failedCalls,
153-
tools: toolCalls.map((c) => c.tool),
167+
tools: params.tool_calls.map((c) => c.tool),
154168
details: results.map((r) => ({ tool: r.tool, success: r.success })),
155169
},
156170
}

packages/sdk/js/src/gen/types.gen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1183,7 +1183,7 @@ export type Config = {
11831183
}
11841184

11851185
export type BadRequestError = {
1186-
data: unknown | null
1186+
data: unknown
11871187
errors: Array<{
11881188
[key: string]: unknown
11891189
}>

packages/web/src/content/docs/providers.mdx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,62 @@ Or if you already have an API key, you can select **Manually enter API Key** and
229229

230230
---
231231

232+
### Azure Cognitive Services
233+
234+
1. Head over to the [Azure portal](https://portal.azure.com/) and create an **Azure OpenAI** resource. You'll need:
235+
- **Resource name**: This becomes part of your API endpoint (`https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/`)
236+
- **API key**: Either `KEY 1` or `KEY 2` from your resource
237+
238+
2. Go to [Azure AI Foundry](https://ai.azure.com/) and deploy a model.
239+
240+
:::note
241+
The deployment name must match the model name for opencode to work properly.
242+
:::
243+
244+
3. Run `opencode auth login` and select **Azure**.
245+
246+
```bash
247+
$ opencode auth login
248+
249+
┌ Add credential
250+
251+
◆ Select provider
252+
│ ● Azure Cognitive Services
253+
│ ...
254+
255+
```
256+
257+
4. Enter your API key.
258+
259+
```bash
260+
$ opencode auth login
261+
262+
┌ Add credential
263+
264+
◇ Select provider
265+
│ Azure Cognitive Services
266+
267+
◇ Enter your API key
268+
│ _
269+
270+
```
271+
272+
5. Set your resource name as an environment variable:
273+
274+
```bash
275+
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME=XXX opencode
276+
```
277+
278+
Or add it to your bash profile:
279+
280+
```bash title="~/.bash_profile"
281+
export AZURE_COGNITIVE_SERVICES_RESOURCE_NAME=XXX
282+
```
283+
284+
6. Run the `/models` command to select your deployed model.
285+
286+
---
287+
232288
### Baseten
233289

234290
1. Head over to the [Baseten](https://app.baseten.co/), create an account, and generate an API key.

0 commit comments

Comments
 (0)