Compare commits

...
7 changed files with 81 additions and 17 deletions
+47 -2
View File
@@ -27,6 +27,7 @@ const requestedTarget = process.argv.find((arg) => arg.startsWith("--target="))?
const skipInstall = process.argv.includes("--skip-install")
const skipWebUi = process.argv.includes("--skip-web-ui")
const solidPlugin = createSolidTransformPlugin()
const releaseAssets = new Map<string, Promise<Map<string, string>>>()
const allTargets: {
os: string
@@ -161,7 +162,13 @@ async function compileExecutable(item: (typeof allTargets)[number]) {
if (!release) return
const platform = item.os === "win32" ? "windows" : item.os
const name = ["bun", platform, item.arch, item.abi, item.avx2 === false ? "baseline" : undefined]
const name = [
"bun",
platform,
item.arch === "arm64" ? "aarch64" : item.arch,
item.abi,
item.avx2 === false ? "baseline" : undefined,
]
.filter(Boolean)
.join("-")
const cache = path.join(outdir, ".bun", release)
@@ -170,7 +177,13 @@ async function compileExecutable(item: (typeof allTargets)[number]) {
await mkdir(cache, { recursive: true })
const archive = path.join(cache, `${name}.zip`)
const response = await fetch(`https://github.com/oven-sh/bun/releases/download/${release}/${name}.zip`)
const assets = await compileReleaseAssets(release)
const url = assets.get(`${name}.zip`)
if (!url) throw new Error(`Bun release ${release} does not include ${name}.zip`)
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
const response = await fetch(url, {
headers: { Accept: "application/octet-stream", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
})
if (!response.ok) throw new Error(`Failed to download ${name} from Bun release ${release}: ${response.status}`)
await Bun.write(archive, response)
await $`unzip -oq ${archive} -d ${cache}`
@@ -178,6 +191,38 @@ async function compileExecutable(item: (typeof allTargets)[number]) {
return executable
}
function compileReleaseAssets(release: string) {
const existing = releaseAssets.get(release)
if (existing) return existing
const pending = fetch(`https://api.github.com/repos/oven-sh/bun/releases/tags/${release}?cache=${Date.now()}`)
.then(async (response) => {
if (!response.ok) throw new Error(`Failed to resolve Bun release ${release}: ${response.status}`)
const data: unknown = await response.json()
if (typeof data !== "object" || data === null || !("assets" in data) || !Array.isArray(data.assets)) {
throw new Error(`Bun release ${release} returned invalid metadata`)
}
return new Map(
data.assets
.filter(
(asset): asset is { name: string; url: string } =>
typeof asset === "object" &&
asset !== null &&
"name" in asset &&
typeof asset.name === "string" &&
"url" in asset &&
typeof asset.url === "string",
)
.map((asset) => [asset.name, asset.url]),
)
})
.catch((error) => {
releaseAssets.delete(release)
throw error
})
releaseAssets.set(release, pending)
return pending
}
function targetName(item: (typeof allTargets)[number]) {
return [
binary,
@@ -150,6 +150,10 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
if (!current) return yield* Effect.die(new Error(`${name} delta before start: ${id}`))
if (!current.pending) return undefined
const now = yield* Clock.currentTimeMillis
if (!force && current.publishedAt === undefined) {
current.publishedAt = now
return undefined
}
if (!force && current.publishedAt !== undefined && now - current.publishedAt < deltaBatchInterval)
return undefined
yield* delta(id, current.pending, current.ordinal)
@@ -217,16 +217,13 @@ it.effect("batches text deltas and flushes pending text before the terminal even
{ discard: true },
)
expect(published.filter((event) => event.type === "session.text.delta").map((event) => event.data)).toMatchObject([
{ delta: "one" },
])
expect(published.filter((event) => event.type === "session.text.delta")).toHaveLength(0)
yield* TestClock.adjust("99 millis")
expect(published.filter((event) => event.type === "session.text.delta")).toHaveLength(1)
expect(published.filter((event) => event.type === "session.text.delta")).toHaveLength(0)
yield* TestClock.adjust("1 millis")
yield* publisher.publish(LLMEvent.textDelta({ id: "text", text: " four" }))
expect(published.filter((event) => event.type === "session.text.delta").map((event) => event.data)).toMatchObject([
{ delta: "one" },
{ delta: " two three four" },
{ delta: "one two three four" },
])
yield* publisher.publish(LLMEvent.textDelta({ id: "text", text: " five" }))
@@ -253,7 +250,7 @@ it.effect("batches reasoning deltas and flushes pending reasoning before the ter
expect(
published.filter((event) => event.type === "session.reasoning.delta").map((event) => event.data),
).toMatchObject([{ delta: "one" }, { delta: " two three" }])
).toMatchObject([{ delta: "one two three" }])
expect(published.slice(-2).map((event) => event.type)).toEqual([
"session.reasoning.delta",
"session.reasoning.ended.1",
+2 -2
View File
@@ -767,7 +767,7 @@ const verifyEphemeralDeltas = (kind: FragmentKind) =>
yield* admit(session, prompt)
const bus = yield* Bus.Service
const live = fixture.delta
? yield* bus.subscribe(fixture.delta).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
? yield* bus.subscribe(fixture.delta).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
: undefined
yield* Effect.yieldNow
yield* TestLLM.push(fixture.completeEvents)
@@ -785,7 +785,7 @@ const verifyEphemeralDeltas = (kind: FragmentKind) =>
: []
if (live) {
const streamed = Array.from(yield* Fiber.join(live))
expect(streamed).toHaveLength(2)
expect(streamed).toHaveLength(1)
expect(
streamed
.map((event) => {
+12 -5
View File
@@ -10,6 +10,7 @@ import { useClient } from "./client"
import { RGBA } from "@opentui/core"
import { readJson, writeJsonAtomic } from "../util/persistence"
import {
availableModelVariant,
createModelPreferenceRepository,
cycleModelVariant,
modelPreferenceKey,
@@ -221,7 +222,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (route.data.type === "session") return sessionSelection(route.data.sessionID)
const model = newSessionModel()
if (!model) return
return { ...model, variant: normalizeModelVariant(preferences.variant[modelPreferenceKey(model)]) }
const info = models()?.find((item) => item.providerID === model.providerID && item.id === model.modelID)
return {
...model,
variant: availableModelVariant(
preferences.variant[modelPreferenceKey(model)],
info?.variants?.map((item) => item.id) ?? [],
),
}
})
const currentModel = createMemo(() => {
@@ -262,13 +270,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (route.data.type === "session") {
const sessionID = route.data.sessionID
const current = sessionSelection(sessionID)
const preferred = normalizeModelVariant(
const preferred =
current?.providerID === model.providerID && current.modelID === model.modelID
? current.variant
: preferences.variant[modelPreferenceKey(model)],
)
: preferences.variant[modelPreferenceKey(model)]
const info = models()?.find((item) => item.providerID === model.providerID && item.id === model.modelID)
const variant = preferred && info?.variants?.some((item) => item.id === preferred) ? preferred : undefined
const variant = availableModelVariant(preferred, info?.variants?.map((item) => item.id) ?? [])
setSessionDraft(sessionID, { ...model, variant })
return true
}
+5
View File
@@ -39,6 +39,11 @@ export function normalizeModelVariant(value: string | undefined) {
return value === "default" ? undefined : value
}
export function availableModelVariant(value: string | undefined, variants: string[]) {
const variant = normalizeModelVariant(value)
return variant && variants.includes(variant) ? variant : undefined
}
export function modelPreferenceKey(model: ModelPreferenceModel) {
return `${model.providerID}/${model.modelID}`
}
+7 -1
View File
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test"
import path from "node:path"
import { createModelPreferenceRepository, decodeModelPreference } from "../src/model-preference"
import { availableModelVariant, createModelPreferenceRepository, decodeModelPreference } from "../src/model-preference"
import { tmpdir } from "./fixture/fixture"
test("repairs known model preferences and preserves unrelated fields", () => {
@@ -19,6 +19,12 @@ test("repairs known model preferences and preserves unrelated fields", () => {
})
})
test("drops a saved variant that is no longer available", () => {
expect(availableModelVariant("medium", [])).toBeUndefined()
expect(availableModelVariant("medium", ["low", "high"])).toBeUndefined()
expect(availableModelVariant("medium", ["low", "medium", "high"])).toBe("medium")
})
test("atomically serializes patches and variant updates", async () => {
await using tmp = await tmpdir()
const file = path.join(tmp.path, "model.json")