Skip to content

Commit e26cb2d

Browse files
authored
Merge branch 'dev' into tui-question-recovery
2 parents 81eefd7 + e00890c commit e26cb2d

33 files changed

Lines changed: 204 additions & 72 deletions

File tree

‎bun.lock‎

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎nix/hashes.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"nodeModules": {
3-
"x86_64-linux": "sha256-yQ8EIxxYkzlEWIMY/UiIR+7lbGBuizsoehMPZzFA65Y=",
4-
"aarch64-linux": "sha256-JF9VVgnl5QUZ430fqb5Qu8y0kchYJ00LO3FbYcd0lBM=",
5-
"aarch64-darwin": "sha256-f3Tu6eu463NWcHgr7dupfD/zUTh26bJ6N2vVXuEyi6c=",
6-
"x86_64-darwin": "sha256-miv9Sv4KdhD0UIi2O5LVS1wQOfq29IV+9S+BvIzAvgo="
3+
"x86_64-linux": "sha256-Be1I6OG6UitofhcGu2BeNzevmoQXc4Or5r/NPzwtft4=",
4+
"aarch64-linux": "sha256-O+d+26CQIjZ08Rn8Qm3IytdDqIMbPdhzaGOZeUKAvIU=",
5+
"aarch64-darwin": "sha256-ObS50y/oy6fM9wSGUL/wx6O0+fTWHC04mXJNd7w/2Z0=",
6+
"x86_64-darwin": "sha256-eoR7ZSyH62Fq2ZaW2b2QqU2FC97rYxTMeEe+djT0nto="
77
}
88
}

‎packages/console/app/src/routes/download/[channel]/[platform].ts‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { APIEvent } from "@solidjs/start"
2+
import { waitUntil } from "@opencode-ai/console-resource"
23
import type { DownloadPlatform } from "../types"
34

45
const prodAssetNames: Record<string, string> = {
@@ -30,14 +31,34 @@ export async function GET({ params: { platform, channel } }: APIEvent) {
3031
const assetName = channel === "stable" ? prodAssetNames[platform] : betaAssetNames[platform]
3132
if (!assetName) return new Response(null, { status: 404 })
3233

33-
const resp = await fetch(
34+
const latest = await fetch(
3435
`https://github.com/anomalyco/${channel === "stable" ? "opencode" : "opencode-beta"}/releases/latest/download/${assetName}`,
36+
{ redirect: "manual" },
3537
)
38+
const location = latest.headers.get("location")
39+
if (!location) return new Response(null, { status: 502 })
3640

37-
const downloadName = downloadNames[platform]
41+
const key = new Request(location)
42+
const cache = (caches as CacheStorage & { default: Cache }).default
43+
const cached = await cache.match(key)
44+
if (cached) return download(cached, platform, "HIT")
45+
46+
const resp = await fetch(location)
47+
if (!resp.ok) return resp
48+
49+
const headers = new Headers(resp.headers)
50+
headers.set("cache-control", "public, max-age=31536000, immutable")
51+
headers.delete("set-cookie")
52+
const result = new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers })
53+
waitUntil(cache.put(key, result.clone()))
54+
return download(result, platform, "MISS")
55+
}
3856

57+
function download(resp: Response, platform: string, cache: "HIT" | "MISS") {
58+
const downloadName = downloadNames[platform]
3959
const headers = new Headers(resp.headers)
4060
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
61+
headers.set("x-opencode-cache", cache)
4162

4263
return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers })
4364
}

‎packages/core/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
},
6363
"dependencies": {
6464
"@ai-sdk/alibaba": "1.0.17",
65-
"@ai-sdk/amazon-bedrock": "4.0.112",
65+
"@ai-sdk/amazon-bedrock": "4.0.158",
6666
"@ai-sdk/anthropic": "3.0.82",
6767
"@ai-sdk/azure": "3.0.88",
6868
"@ai-sdk/cerebras": "2.0.41",

‎packages/core/src/plugin/provider/opencode.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,18 @@ function oauth(http: HttpClient.HttpClient) {
4545
authorize: () =>
4646
Effect.gen(function* () {
4747
const device = yield* post(http, `${defaultServer}/auth/device/code`, { client_id: clientID }, Device)
48+
const verification = yield* Effect.try({
49+
try: () => {
50+
const url = new URL(device.verification_uri_complete, `${defaultServer}/`)
51+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)")
52+
return url
53+
},
54+
catch: (cause) =>
55+
new Error(`Invalid device verification URL: ${cause instanceof Error ? cause.message : String(cause)}`),
56+
})
4857
return {
4958
mode: "auto" as const,
50-
url: `${defaultServer}${device.verification_uri_complete}`,
59+
url: verification.href,
5160
instructions: `Enter code: ${device.user_code}`,
5261
callback: poll(http, defaultServer, device.device_code, Duration.seconds(device.interval)),
5362
}

‎packages/core/test/plugin/provider-opencode.test.ts‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect } from "bun:test"
22
import { Effect } from "effect"
3+
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
34
import { Catalog } from "@opencode-ai/core/catalog"
45
import { Credential } from "@opencode-ai/core/credential"
56
import { EventV2 } from "@opencode-ai/core/event"
@@ -14,14 +15,16 @@ import { PluginTestLayer } from "./fixture"
1415

1516
const it = testEffect(PluginTestLayer)
1617

17-
const addPlugin = Effect.fn(function* () {
18+
const addPlugin = Effect.fn(function* (http?: HttpClient.HttpClient) {
1819
const plugin = yield* PluginV2.Service
1920
const host = yield* PluginHost.make(plugin)
2021
const events = yield* EventV2.Service
2122
const integration = yield* Integration.Service
23+
const client = yield* HttpClient.HttpClient
2224
yield* OpencodePlugin.effect(host).pipe(
2325
Effect.provideService(EventV2.Service, events),
2426
Effect.provideService(Integration.Service, integration),
27+
Effect.provideService(HttpClient.HttpClient, http ?? client),
2528
)
2629
})
2730

@@ -82,6 +85,63 @@ describe("OpencodePlugin", () => {
8285
}),
8386
)
8487

88+
it.effect("resolves origin-rooted device verification URLs", () =>
89+
Effect.gen(function* () {
90+
const http = HttpClient.make((request) =>
91+
Effect.succeed(
92+
HttpClientResponse.fromWeb(
93+
request,
94+
Response.json({
95+
device_code: "device",
96+
user_code: "user",
97+
verification_uri_complete: "/console/device?user_code=user&client_id=opencode-cli",
98+
expires_in: 60,
99+
interval: 60,
100+
}),
101+
),
102+
),
103+
)
104+
yield* addPlugin(http)
105+
const integration = yield* Integration.Service
106+
const attempt = yield* integration.connection.oauth({
107+
integrationID: Integration.ID.make("opencode"),
108+
methodID: Integration.MethodID.make("device"),
109+
inputs: {},
110+
})
111+
expect(attempt.url).toBe("https://opencode.ai/console/device?user_code=user&client_id=opencode-cli")
112+
}),
113+
)
114+
115+
it.effect("rejects malformed device verification URLs", () =>
116+
Effect.gen(function* () {
117+
const http = HttpClient.make((request) =>
118+
Effect.succeed(
119+
HttpClientResponse.fromWeb(
120+
request,
121+
Response.json({
122+
device_code: "device",
123+
user_code: "user",
124+
verification_uri_complete: "http://[::1",
125+
expires_in: 60,
126+
interval: 60,
127+
}),
128+
),
129+
),
130+
)
131+
yield* addPlugin(http)
132+
const integration = yield* Integration.Service
133+
const error = yield* integration.connection
134+
.oauth({
135+
integrationID: Integration.ID.make("opencode"),
136+
methodID: Integration.MethodID.make("device"),
137+
inputs: {},
138+
})
139+
.pipe(Effect.flip)
140+
expect(error).toBeInstanceOf(Integration.AuthorizationError)
141+
expect(String(error.cause)).toContain("Invalid device verification URL")
142+
}),
143+
)
144+
85145
it.live("loads providers and models from the connected OpenCode server", () =>
86146
Effect.acquireUseRelease(
87147
Effect.sync(() => {

‎packages/opencode/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@actions/github": "6.0.1",
5757
"@agentclientprotocol/sdk": "0.21.0",
5858
"@ai-sdk/alibaba": "1.0.17",
59-
"@ai-sdk/amazon-bedrock": "4.0.112",
59+
"@ai-sdk/amazon-bedrock": "4.0.158",
6060
"@ai-sdk/anthropic": "3.0.82",
6161
"@ai-sdk/azure": "3.0.88",
6262
"@ai-sdk/cerebras": "2.0.60",

‎packages/opencode/src/account/account.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,18 @@ const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpCl
396396
const parsed = yield* HttpClientResponse.schemaBodyJson(DeviceAuth)(response).pipe(
397397
mapAccountServiceError("Failed to decode response"),
398398
)
399+
const verification = yield* Effect.try({
400+
try: () => {
401+
const url = new URL(parsed.verification_uri_complete, `${normalizedServer}/`)
402+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)")
403+
return url.href
404+
},
405+
catch: (cause) => new AccountServiceError({ message: "Invalid device verification URL", cause }),
406+
})
399407
return new Login({
400408
code: parsed.device_code,
401409
user: parsed.user_code,
402-
url: `${normalizedServer}${parsed.verification_uri_complete}`,
410+
url: verification,
403411
server: normalizedServer,
404412
expiry: parsed.expires_in,
405413
interval: parsed.interval,

‎packages/opencode/src/provider/transform.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,13 +1303,13 @@ export function options(input: {
13031303
}
13041304
}
13051305

1306-
// Only set textVerbosity for non-chat gpt-5.x models
1307-
// Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity
1306+
// Generic OpenAI-compatible APIs do not necessarily support OpenAI's verbosity parameter.
1307+
// Only enable the default for integrations known to implement it.
13081308
if (
13091309
input.model.api.id.includes("gpt-5.") &&
13101310
!input.model.api.id.includes("codex") &&
13111311
!input.model.api.id.includes("-chat") &&
1312-
input.model.providerID !== "azure"
1312+
(input.model.api.npm === "@ai-sdk/openai" || input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle")
13131313
) {
13141314
result["textVerbosity"] = "low"
13151315
}

‎packages/opencode/test/account/service.test.ts‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Account } from "../../src/account/account"
1010
import {
1111
AccessToken,
1212
AccountID,
13+
AccountServiceError,
1314
AccountTransportError,
1415
DeviceCode,
1516
Login,
@@ -71,18 +72,18 @@ const deviceTokenClient = (body: unknown, status = 400) =>
7172
const poll = (body: unknown, status = 400) =>
7273
Account.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(deviceTokenClient(body, status))))
7374

74-
it.live("login normalizes trailing slashes in the provided server URL", () =>
75+
it.live("login resolves origin-rooted verification URLs from servers with base paths", () =>
7576
Effect.gen(function* () {
7677
const seen: Array<string> = []
7778
const client = HttpClient.make((req) =>
7879
Effect.gen(function* () {
7980
seen.push(`${req.method} ${req.url}`)
8081

81-
if (req.url === "https://one.example.com/auth/device/code") {
82+
if (req.url === "https://one.example.com/console/auth/device/code") {
8283
return json(req, {
8384
device_code: "device-code",
8485
user_code: "user-code",
85-
verification_uri_complete: "/device?user_code=user-code",
86+
verification_uri_complete: "/console/device?user_code=user-code",
8687
expires_in: 600,
8788
interval: 5,
8889
})
@@ -92,11 +93,31 @@ it.live("login normalizes trailing slashes in the provided server URL", () =>
9293
}),
9394
)
9495

95-
const result = yield* Account.use.login("https://one.example.com/").pipe(Effect.provide(live(client)))
96+
const result = yield* Account.use.login("https://one.example.com/console/").pipe(Effect.provide(live(client)))
9697

97-
expect(seen).toEqual(["POST https://one.example.com/auth/device/code"])
98-
expect(result.server).toBe("https://one.example.com")
99-
expect(result.url).toBe("https://one.example.com/device?user_code=user-code")
98+
expect(seen).toEqual(["POST https://one.example.com/console/auth/device/code"])
99+
expect(result.server).toBe("https://one.example.com/console")
100+
expect(result.url).toBe("https://one.example.com/console/device?user_code=user-code")
101+
}),
102+
)
103+
104+
it.live("login rejects malformed device verification URLs", () =>
105+
Effect.gen(function* () {
106+
const client = HttpClient.make((req) =>
107+
Effect.succeed(
108+
json(req, {
109+
device_code: "device-code",
110+
user_code: "user-code",
111+
verification_uri_complete: "http://[::1",
112+
expires_in: 600,
113+
interval: 5,
114+
}),
115+
),
116+
)
117+
118+
const error = yield* Effect.flip(Account.use.login("https://one.example.com").pipe(Effect.provide(live(client))))
119+
expect(error).toBeInstanceOf(AccountServiceError)
120+
if (error instanceof AccountServiceError) expect(error.message).toBe("Invalid device verification URL")
100121
}),
101122
)
102123

0 commit comments

Comments
 (0)