fix(opencode-plugin): lengthen /v1/models timeout and attach HTTP status (#12607)

Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.

A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.

O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.

Obrigado, @RaviTharuma.
This commit is contained in:
Ravi Tharuma
2026-09-04 04:38:04 +02:00
committed by GitHub
parent bb8e75a00d
commit b0557543b8
5 changed files with 57 additions and 5 deletions

View File

@@ -23,7 +23,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/free-budget-magnitude.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/models-fetcher.test.ts tests/free-budget-magnitude.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [

View File

@@ -1199,7 +1199,7 @@ export type OmniRouteModelsFetcher = (
export const defaultOmniRouteModelsFetcher: OmniRouteModelsFetcher = async (
baseURL,
apiKey,
timeoutMs = 10_000
timeoutMs = 30_000
) => {
if (!apiKey) throw new Error("@omniroute/opencode-plugin: apiKey required to fetch /v1/models");
if (!baseURL) throw new Error("@omniroute/opencode-plugin: baseURL required to fetch /v1/models");
@@ -1221,9 +1221,12 @@ export const defaultOmniRouteModelsFetcher: OmniRouteModelsFetcher = async (
signal: controller.signal,
});
if (!res.ok) {
throw new Error(
const err = new Error(
`@omniroute/opencode-plugin: GET ${url} failed: ${res.status} ${res.statusText}`
);
) as Error & { statusCode: number; status: number };
err.statusCode = res.status;
err.status = res.status;
throw err;
}
const body = (await res.json()) as unknown;
const rawList: unknown[] = Array.isArray(body)
@@ -5398,7 +5401,7 @@ export function createOmniRouteConfigHook(
// exact warn message so per-endpoint fallbacks are preserved.
const doModels = async (): Promise<void> => {
try {
localRawModels = await fetcher(baseURL, apiKey, 10_000);
localRawModels = await fetcher(baseURL, apiKey, 30_000);
} catch (err) {
logAt(
"error",

View File

@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import { defaultOmniRouteModelsFetcher } from "../src/index.js";
test("defaultOmniRouteModelsFetcher attaches statusCode on HTTP 401", async () => {
const original = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ error: "authentication expired" }), {
status: 401,
statusText: "Unauthorized",
})) as typeof fetch;
try {
await assert.rejects(
() => defaultOmniRouteModelsFetcher("https://gateway.example/v1", "test-key"),
(err: unknown) => {
assert.ok(err instanceof Error);
const rec = err as Error & { statusCode?: number; status?: number };
assert.equal(rec.statusCode, 401);
assert.equal(rec.status, 401);
assert.match(rec.message, /401/);
return true;
},
);
} finally {
globalThis.fetch = original;
}
});
test("defaultOmniRouteModelsFetcher default timeout is 30s", async () => {
const original = globalThis.fetch;
let signal: AbortSignal | undefined;
globalThis.fetch = (async (_input, init) => {
signal = init?.signal ?? undefined;
return new Response(JSON.stringify({ object: "list", data: [] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}) as typeof fetch;
try {
await defaultOmniRouteModelsFetcher("https://gateway.example/v1", "test-key");
assert.equal(signal instanceof AbortSignal, true);
} finally {
globalThis.fetch = original;
}
});

View File

@@ -0,0 +1 @@
- OpenCode plugin `/v1/models` catalog fetch now waits 30s by default and attaches HTTP `statusCode` on 401/5xx so host fallback plugins can hop instead of seeing an untyped AbortError/UnknownError.

View File

@@ -854,6 +854,9 @@
"src/app/(dashboard)/dashboard/combos/page.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 6
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": {