Compare commits

..

1 Commits

Author SHA1 Message Date
opencode 7902e04c3a release: v1.18.10 2026-07-30 14:43:37 +00:00
14 changed files with 47 additions and 208 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-AD5kt1o035hdvYPyrvjiz2mcsXFJ7UJVEmbE9AJFDFs=",
"aarch64-linux": "sha256-f1gVXUpIRWC/I8dxIigdGGvPsju04MXVjZhtq5EYL64=",
"aarch64-darwin": "sha256-c+JRpjs2gsgQ2czyM6bTbYEdMbX/erOSQdX6TdrI59k=",
"x86_64-darwin": "sha256-iB7l4PtQQ9Mx/XAdtyayiLe6pZKhuXB9lpmrbTr+uWc="
"x86_64-linux": "sha256-fK6zAHJC3ut/KUdfqLPxVMH7Z1yv65YZ7qcHF45kPas=",
"aarch64-linux": "sha256-CSWZY/vVUCKzlwfisQQHqHjawUwL0BRc8Y9o2y/QZYA=",
"aarch64-darwin": "sha256-IyFm5NbnU63BaOO/F4/v1exz3VbvkY96yjg9iun+O9Q=",
"x86_64-darwin": "sha256-XnXFoUxqXmEx7AKx5YhaJuLY2NYMtI0ICafaenU5L6k="
}
}
+2 -8
View File
@@ -15,11 +15,6 @@ import { httpClient } from "./effect/app-node-platform"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type
const InterleavedField = Schema.Union([
Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]),
Schema.String,
])
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
const CostTier = Schema.Struct({
@@ -76,10 +71,9 @@ export const Model = Schema.Struct({
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
interleaved: Schema.optional(
Schema.Union([
Schema.Boolean,
InterleavedField,
Schema.Literal(true),
Schema.Struct({
field: InterleavedField,
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
}),
]),
),
+2 -8
View File
@@ -5,11 +5,6 @@ import { PositiveInt } from "../../schema"
export const ModelStatus = Schema.Literals(["alpha", "beta", "deprecated", "active"])
const InterleavedField = Schema.Union([
Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]),
Schema.String,
])
export const Model = Schema.Struct({
id: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
@@ -21,10 +16,9 @@ export const Model = Schema.Struct({
tool_call: Schema.optional(Schema.Boolean),
interleaved: Schema.optional(
Schema.Union([
Schema.Boolean,
InterleavedField,
Schema.Literal(true),
Schema.Struct({
field: InterleavedField,
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
}),
]),
),
+23 -27
View File
@@ -123,18 +123,6 @@ async function toolError(part: ToolPart) {
}
}
async function embeddedServer(auth?: string) {
const { Server } = await import("@/server/server")
const server = Server.Default()
const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init)
const headers = new Headers(request.headers)
if (auth) headers.set("Authorization", auth)
return server.app.fetch(new Request(request, { headers }))
}) as typeof globalThis.fetch
return { fetch, dispose: server.dispose }
}
export const RunCommand = effectCmd({
command: "run [message..]",
describe: "run opencode with a message",
@@ -914,12 +902,19 @@ export const RunCommand = effectCmd({
if (interactive && !args.attach && !args.session && !args.continue) {
const model = pick(args.model)
const { runInteractiveLocalMode } = await import("./run/runtime")
const server = await embeddedServer(ServerAuth.header())
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
const { Server } = await import("@/server/server")
const request = new Request(input, init)
const headers = new Headers(request.headers)
const auth = ServerAuth.header()
if (auth) headers.set("Authorization", auth)
return Server.Default().app.fetch(new Request(request, { headers }))
}) as typeof globalThis.fetch
try {
return await runInteractiveLocalMode({
directory: directory ?? root,
fetch: server.fetch,
fetch: fetchFn,
resolveAgent: localAgent,
session,
share,
@@ -937,8 +932,6 @@ export const RunCommand = effectCmd({
})
} catch (error) {
dieInteractive(error)
} finally {
await server.dispose()
}
}
@@ -947,17 +940,20 @@ export const RunCommand = effectCmd({
return await execute(sdk)
}
const server = await embeddedServer(ServerAuth.header())
try {
const sdk = createOpencodeClient({
baseUrl: "http://opencode.internal",
fetch: server.fetch,
directory,
})
await execute(sdk)
} finally {
await server.dispose()
}
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
const { Server } = await import("@/server/server")
const request = new Request(input, init)
const headers = new Headers(request.headers)
const auth = ServerAuth.header()
if (auth) headers.set("Authorization", auth)
return Server.Default().app.fetch(new Request(request, { headers }))
}) as typeof globalThis.fetch
const sdk = createOpencodeClient({
baseUrl: "http://opencode.internal",
fetch: fetchFn,
directory,
})
await execute(sdk)
})
}),
})
-1
View File
@@ -72,7 +72,6 @@ export const rpc = {
async shutdown() {
await InstanceRuntime.disposeAllInstances()
if (server) await server.stop(true)
if (Server.Default.loaded()) await Server.Default().dispose()
process.off("unhandledRejection", onUnhandledRejection)
process.off("uncaughtException", onUncaughtException)
},
+3 -8
View File
@@ -976,15 +976,10 @@ const ProviderModalities = Schema.Struct({
pdf: Schema.Boolean,
})
const ProviderInterleavedField = Schema.Union([
Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]),
Schema.String,
])
const ProviderInterleaved = Schema.Union([
Schema.Boolean,
Schema.Struct({
field: ProviderInterleavedField,
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
}),
])
@@ -1248,7 +1243,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
video: model.modalities?.output?.includes("video") ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? false,
},
interleaved: typeof model.interleaved === "string" ? { field: model.interleaved } : (model.interleaved ?? false),
interleaved: model.interleaved ?? false,
},
release_date: model.release_date ?? "",
variants: {},
@@ -1480,7 +1475,7 @@ const layer = Layer.effect(
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
},
interleaved:
(typeof model.interleaved === "string" ? { field: model.interleaved } : model.interleaved) ??
model.interleaved ??
existingModel?.capabilities.interleaved ??
(!existingModel && apiNpm === "@ai-sdk/openai-compatible" && apiID.includes("deepseek")
? { field: "reasoning_content" }
+3 -3
View File
@@ -54,14 +54,14 @@ class ListenerServerService extends Context.Service<ListenerServerService, Liste
) {}
export const Default = lazy(() => {
const web = HttpApiApp.webHandler()
const handler = HttpApiApp.webHandler().handler
const app: ServerApp = {
fetch: (request: Request) => web.handler(request, HttpApiApp.context),
fetch: (request: Request) => handler(request, HttpApiApp.context),
request(input, init) {
return app.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init))
},
}
return { app, dispose: web.dispose }
return { app }
})
export async function openapi() {
@@ -23,35 +23,6 @@ describe("opencode run (non-interactive subprocess)", () => {
60_000,
)
cliIt.concurrent(
"flushes server telemetry before exiting",
({ llm, opencode }) =>
Effect.gen(function* () {
const requests: string[] = []
const collector = yield* Effect.acquireRelease(
Effect.sync(() =>
Bun.serve({
port: 0,
fetch(request) {
requests.push(new URL(request.url).pathname)
return new Response(null, { status: 200 })
},
}),
),
(server) => Effect.sync(() => server.stop(true)),
)
yield* llm.text("telemetry response")
const result = yield* opencode.run("say hi", {
env: { OTEL_EXPORTER_OTLP_ENDPOINT: collector.url.toString().replace(/\/$/, "") },
})
opencode.expectExit(result, 0)
expect(requests).toContain("/v1/traces")
}),
60_000,
)
cliIt.concurrent(
"prints each completed text part in order around a tool continuation",
({ llm, opencode }) =>
@@ -1,38 +0,0 @@
import { expect, test } from "bun:test"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
test("does not reconnect an SSE stream after a JSON-RPC error response", async () => {
let requests = 0
const transport = new StreamableHTTPClientTransport(new URL("http://mcp.invalid"), {
fetch: async () => {
requests += 1
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("id: prime\nretry: 1\ndata:\n\n"))
controller.enqueue(
new TextEncoder().encode(
'id: error\ndata: {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":1}\n\n',
),
)
controller.close()
},
}),
{ status: 200, headers: { "content-type": "text/event-stream" } },
)
},
reconnectionOptions: {
initialReconnectionDelay: 1,
maxReconnectionDelay: 1,
reconnectionDelayGrowFactor: 1,
maxRetries: 2,
},
})
await transport.start()
await transport.send({ jsonrpc: "2.0", method: "resources/list", id: 1 })
await Bun.sleep(25)
await transport.close()
expect(requests).toBe(1)
})
@@ -252,8 +252,6 @@ it.instance(
const provider = providers[ProviderV2.ID.make("custom-provider")]
expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" })
expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" })
expect(provider.models["deepseek-text"].capabilities.interleaved).toEqual({ field: "reasoning_text" })
expect(provider.models["custom-reasoning"].capabilities.interleaved).toEqual({ field: "vendor_reasoning" })
expect(provider.models["custom-model"].capabilities.interleaved).toBe(false)
expect(
providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved,
@@ -269,8 +267,6 @@ it.instance(
models: {
"deepseek-r1": { name: "DeepSeek R1" },
"deepseek-details": { name: "DeepSeek Details", interleaved: { field: "reasoning_details" } },
"deepseek-text": { name: "DeepSeek Text", interleaved: "reasoning_text" },
"custom-reasoning": { name: "Custom Reasoning", interleaved: { field: "vendor_reasoning" } },
"custom-model": { name: "Custom Model" },
},
options: { apiKey: "custom-key" },
@@ -1466,7 +1462,6 @@ test("models.dev normalization fills required response fields", () => {
id: "gpt-5.4",
name: "GPT-5.4",
family: "gpt",
interleaved: "reasoning_text",
cost: { input: 2.5, output: 15 },
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
},
@@ -1479,7 +1474,6 @@ test("models.dev normalization fills required response fields", () => {
expect(model.capabilities.reasoning).toBe(false)
expect(model.capabilities.attachment).toBe(false)
expect(model.capabilities.toolcall).toBe(true)
expect(model.capabilities.interleaved).toEqual({ field: "reasoning_text" })
expect(model.release_date).toBe("")
})
+3 -7
View File
@@ -1768,13 +1768,9 @@ export type ProviderConfig = {
temperature?: boolean
tool_call?: boolean
interleaved?:
| boolean
| "reasoning"
| "reasoning_content"
| "reasoning_text"
| string
| true
| {
field: "reasoning" | "reasoning_content" | "reasoning_text" | string
field: "reasoning" | "reasoning_content" | "reasoning_details"
}
cost?: {
input: number
@@ -2061,7 +2057,7 @@ export type Model = {
interleaved:
| boolean
| {
field: "reasoning" | "reasoning_content" | "reasoning_text" | string
field: "reasoning" | "reasoning_content" | "reasoning_details"
}
}
cost: {
+6 -26
View File
@@ -20820,28 +20820,15 @@
"interleaved": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
"type": "boolean",
"enum": [true]
},
{
"type": "object",
"properties": {
"field": {
"anyOf": [
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
}
]
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_details"]
}
},
"required": ["field"],
@@ -21654,15 +21641,8 @@
"type": "object",
"properties": {
"field": {
"anyOf": [
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
}
]
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_details"]
}
},
"required": ["field"],
@@ -1505,30 +1505,6 @@ To use Kimi K2 from Moonshot AI:
---
### Modal
1. Create a [shared Endpoint](https://modal.com/endpoints) for the model you want to use.
2. Create a [proxy token](https://modal.com/docs/guide/endpoints#proxy-tokens), then join its ID and secret with a period:
```txt
wk-<id>.ws-<secret>
```
3. Run the `/connect` command, search for **Modal**, and enter the combined proxy token.
```txt
/connect
```
4. Run the `/models` command to select one of the endpoints in your Modal workspace.
```txt
/models
```
---
### NVIDIA
NVIDIA provides access to Nemotron models and many other open models through [build.nvidia.com](https://build.nvidia.com) for free.
@@ -141,15 +141,6 @@ diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.
index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
--- a/dist/cjs/client/streamableHttp.js
+++ b/dist/cjs/client/streamableHttp.js
@@ -204,7 +204,7 @@ class StreamableHTTPClientTransport {
if (!event.event || event.event === 'message') {
try {
const message = types_js_1.JSONRPCMessageSchema.parse(JSON.parse(event.data));
- if ((0, types_js_1.isJSONRPCResultResponse)(message)) {
+ if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
// Mark that we received a response - no need to reconnect for this request
receivedResponse = true;
if (replayMessageId !== undefined) {
@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport {
this.onclose?.();
}
@@ -527,19 +518,10 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
@@ -1,5 +1,5 @@
import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
-import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
+import { isInitializedNotification, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
+import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
import { EventSourceParserStream } from 'eventsource-parser/stream';
// Default reconnection options for StreamableHTTP connections
@@ -200,7 +200,7 @@ export class StreamableHTTPClientTransport {
if (!event.event || event.event === 'message') {
try {
const message = JSONRPCMessageSchema.parse(JSON.parse(event.data));
- if (isJSONRPCResultResponse(message)) {
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
// Mark that we received a response - no need to reconnect for this request
receivedResponse = true;
if (replayMessageId !== undefined) {
@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport {
this.onclose?.();
}