diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70a8477fb51f..510f682549ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,9 @@ permissions: contents: read checks: write +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: unit: name: unit (${{ matrix.settings.name }}) @@ -38,6 +41,11 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "24" + - name: Setup Bun uses: ./.github/actions/setup-bun @@ -102,6 +110,11 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "24" + - name: Setup Bun uses: ./.github/actions/setup-bun diff --git a/packages/app/e2e/backend.ts b/packages/app/e2e/backend.ts index 9febc4b3ff4d..a03d1d437504 100644 --- a/packages/app/e2e/backend.ts +++ b/packages/app/e2e/backend.ts @@ -44,8 +44,12 @@ async function waitForHealth(url: string, probe = "/global/health") { throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`) } +function done(proc: ReturnType) { + return proc.exitCode !== null || proc.signalCode !== null +} + async function waitExit(proc: ReturnType, timeout = 10_000) { - if (proc.exitCode !== null) return + if (done(proc)) return await Promise.race([ new Promise((resolve) => proc.once("exit", () => resolve())), new Promise((resolve) => setTimeout(resolve, timeout)), @@ -123,11 +127,11 @@ export async function startBackend(label: string, input?: { llmUrl?: string }): return { url, async stop() { - if (proc.exitCode === null) { + if (!done(proc)) { proc.kill("SIGTERM") await waitExit(proc) } - if (proc.exitCode === null) { + if (!done(proc)) { proc.kill("SIGKILL") await waitExit(proc) } diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index c0425b7efe83..37fd688dfb76 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -1,12 +1,25 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import type { Model } from "@opencode-ai/sdk/v2" +import { Global } from "@/global" import { Installation } from "@/installation" import { iife } from "@/util/iife" +import { Filesystem } from "@/util/filesystem" +import { Hash } from "@/util/hash" import { Log } from "../../util/log" import { setTimeout as sleep } from "node:timers/promises" +import path from "path" import { CopilotModels } from "./models" const log = Log.create({ service: "plugin.copilot" }) +const ttl = 60 * 60 * 1000 + +function cachefile(url: string) { + return path.join(Global.Path.cache, `copilot-models-${Hash.fast(url)}.json`) +} + +function fresh(file: string) { + return Date.now() - Number(Filesystem.stat(file)?.mtimeMs ?? 0) < ttl +} const CLIENT_ID = "Ov23li8tweQw6odWQebz" // Add a small safety buffer when polling to avoid hitting the server @@ -47,17 +60,29 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)])) } - return CopilotModels.get( - base(ctx.auth.enterpriseUrl), - { - Authorization: `Bearer ${ctx.auth.refresh}`, - "User-Agent": `opencode/${Installation.VERSION}`, - }, - provider.models, - ).catch((error) => { - log.error("failed to fetch copilot models", { error }) - return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)])) - }) + const url = base(ctx.auth.enterpriseUrl) + const file = cachefile(url) + const headers = { + Authorization: `Bearer ${ctx.auth.refresh}`, + "User-Agent": `opencode/${Installation.VERSION}`, + } + + if (fresh(file)) { + const cached = await Bun.file(file) + .json() + .catch(() => undefined) + if (cached) return cached as Record + } + + return CopilotModels.get(url, headers, provider.models) + .then(async (result) => { + await Bun.write(file, JSON.stringify(result)) + return result + }) + .catch((error) => { + log.error("failed to fetch copilot models", { error }) + return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)])) + }) }, }, auth: {