fix(quality): resolve open-sse type errors + catalog/build regressions (base-red round 3)

Storm-merge splices repaired in the base-fix PR #10131:
- doctor.ts: AppConfig missing brokerSocketPath
- conol-web.ts: Buffer not assignable to BodyInit (Uint8Array)
- tinycms.ts: TinyCmsExecutor.execute return matches BaseExecutor (response/url/transformedBody)
- tinycmsSigner.ts: encodeInto never-narrowing guard + dead wasm URL fallback (Turbopack)
- virtualFactory.ts: options slot for resolutionSnapshot
- bottleneckPatch.ts: insufficient-overlap casts (as unknown as)
- imageCombo.ts: narrow handleImageGeneration union result
- browser-worker.ts: AppConfig + turn.capabilities splice
- conolDiscovery.ts: getProviderOutboundGuard from Policy module
- catalog.ts: drop removed SWR hooks (getCatalogStaleWhileRevalidateMs + accessors), CatalogCachePolicy -> inline settings, resolve 4-arg call
- catalogCache.ts: remove dead inFlight/promise refs
- chat.ts: add isProviderBreakerFailureStatus import
- model-catalog-cache-swr-8728.test.ts: align to #9199 new API (policy injection removed)
This commit is contained in:
diegosouzapw
2026-08-11 21:09:39 -03:00
parent 5f38ab48ca
commit 200233d301
13 changed files with 94 additions and 178 deletions

View File

@@ -48,6 +48,7 @@ export async function getChatGptWebCodexDoctorStatus(connection: {
mode: "browser-only",
appName: "OmniRoute Codex",
storageStatePath: paths.storageStatePath,
brokerSocketPath: paths.brokerSocketPath,
...(chrome ? { chromeExecutablePath: chrome } : {}),
...(cdpConfigured ? { cdpEndpoint: process.env.CHATGPT_WEB_CODEX_CDP_URL } : {}),
headed: false,

View File

@@ -525,7 +525,7 @@ async function uploadConolImages(
},
sessionId
),
body: image.data,
body: new Uint8Array(image.data),
signal: signal ?? undefined,
});
if (!response.ok) {

View File

@@ -116,9 +116,10 @@ export class TinyCmsExecutor extends BaseExecutor {
const response = await fetch(CHAT_URL, fetchOptions);
return {
status: response.status,
response,
url: CHAT_URL,
headers: Object.fromEntries(response.headers.entries()),
body: response.body,
transformedBody: bodyObj,
};
} catch (err: any) {
return makeErrorResult(

View File

@@ -358,7 +358,16 @@ function decodeText(ptr, len) {
const cachedTextEncoder = new TextEncoder();
if (!('encodeInto' in cachedTextEncoder)) {
// Old-Safari compatibility: if the runtime encoder lacks `encodeInto`, polyfill
// it. TS (DOM lib) types encodeInto as an always-present member, so a direct
// `'encodeInto' in cachedTextEncoder` guard would narrow the encoder to `never`
// in the negative branch. Test a widened copy instead — the result narrows a
// boolean, never `cachedTextEncoder`, so the polyfill body stays typeable.
const needsEncodeIntoPolyfill = !(
"encodeInto" in (cachedTextEncoder as { encodeInto?: unknown })
);
if (needsEncodeIntoPolyfill) {
cachedTextEncoder.encodeInto = function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
@@ -449,7 +458,11 @@ async function __wbg_init(module_or_path) {
}
if (module_or_path === undefined) {
module_or_path = new URL('wasm_signer_bg.wasm', import.meta.url);
// The boot-time embed is passed explicitly via `initTinyCmsWasm()` below
// (base64 → Buffer). Do NOT fall back to `new URL('wasm_signer_bg.wasm',
// import.meta.url)`: no such asset exists in the tree (it is inlined as
// base64), and the static URL would make Turbopack/Next fail the bundle.
throw new Error("tinycmsSigner: wasm module not supplied; call initTinyCmsWasm() first");
}
const imports = __wbg_get_imports();

View File

@@ -395,6 +395,7 @@ async function attachPreparedCapabilityValues(
provider: candidate.provider,
model: candidate.model,
},
undefined,
state.resolutionSnapshot
);
const maxOutputTokens = capabilities.maxOutputTokens;

View File

@@ -30,7 +30,7 @@ export function applyBottleneckDoExpirePatch(): void {
if (patched) return;
patched = true;
const proto = Bottleneck.prototype as Record<string, unknown>;
const proto = Bottleneck.prototype as unknown as Record<string, unknown>;
const originalRun = proto._run as
((index: string, job: BottleneckJob, wait: number) => unknown) | undefined;
if (typeof originalRun !== "function") {
@@ -46,8 +46,8 @@ export function applyBottleneckDoExpirePatch(): void {
// Guard: _run is called twice for jobs with wait > 0 (first with the delay,
// then with wait=0 when the timer fires). Without the flag, fixedDoExpire
// would wrap itself recursively on the second call.
if (typeof job?.doExpire === "function" && !(job as Record<string, unknown>)._doExpirePatched) {
(job as Record<string, unknown>)._doExpirePatched = true;
if (typeof job?.doExpire === "function" && !(job as unknown as Record<string, unknown>)._doExpirePatched) {
(job as unknown as Record<string, unknown>)._doExpirePatched = true;
const originalDoExpire = job.doExpire.bind(job);
// Bottleneck registers the job in _states under options.id (Job.js
// states.start(this.options.id)); a bare `job.id` does not exist and

View File

@@ -25,6 +25,15 @@ import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import * as logger from "@/sse/utils/logger";
/**
* Caller-facing shape of handleImageGeneration(). The handler is untyped and
* returns a wide inferred union across providers, so we narrow it to the two
* discriminated arms this strategy actually consumes.
*/
type ImageGenerationResult =
| { success: true; data?: unknown; status?: number; error?: string }
| { success: false; data?: unknown; status?: number; error?: string };
/**
* Execute a full combo strategy for an image generation request.
*
@@ -119,12 +128,12 @@ export async function executeImageCombo(
}
// Execute image generation for this target
const result = await handleImageGeneration({
const result = (await handleImageGeneration({
body: { ...body, model: target.modelStr },
credentials,
log,
signal: auth.request?.signal || null,
});
})) as ImageGenerationResult;
if (result.success) {
await clearRecoveredProviderState(credentials);

View File

@@ -426,8 +426,17 @@ export class ChatGptBrowserWorker {
if (this.page && !this.page.isClosed()) return this.page;
if (
!browserLoginStateExists({
mode: "browser-only",
appName: this.config.appName,
storageStatePath: this.config.storageStatePath,
chromeExecutablePath: this.config.chromeExecutablePath,
brokerSocketPath: join(getConfigDir(), "runtime", "turn-broker.sock"),
headed: this.config.headed,
proAvailable: false,
autoApproveToolCalls: this.config.autoApproveToolCalls,
...(this.config.chromeExecutablePath
? { chromeExecutablePath: this.config.chromeExecutablePath }
: {}),
...(this.config.cdpEndpoint ? { cdpEndpoint: this.config.cdpEndpoint } : {}),
})
) {
throw new Error(`ChatGPT web login state is missing: ${this.config.storageStatePath}`);
@@ -956,7 +965,7 @@ export class ChatGptBrowserWorker {
if (this.context) {
const state = await this.context.storageState();
atomicWriteFile(this.config.storageStatePath, `${JSON.stringify(state)}\n`);
writeVerificationMarker(this.config.storageStatePath, capabilities.proAvailable);
writeVerificationMarker(this.config.storageStatePath, turn.capabilities.proAvailable);
}
console.info(
`[chatgpt-web] browser turn ${turn.traceId} completed (markdownChars=${finalText.length})`

View File

@@ -1,5 +1,5 @@
import { SAFE_OUTBOUND_FETCH_PRESETS, safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuardPolicy";
import { resolveConolCredentials } from "@omniroute/open-sse/services/conolAuth.ts";
import {
CONOL_FALLBACK_MODELS,

View File

@@ -116,25 +116,18 @@ export { getCustomVisionCapabilityFields };
// lives in ./catalogCache. Re-exported here because the existing tests import the
// hooks from this module, and CATALOG_STALE_WHILE_REVALIDATE_MS is part of the
// documented behavior of this endpoint.
import {
CATALOG_CACHE_TTL_MS_DEFAULT,
resolveCachedCatalogResponse,
type CatalogCachePolicy,
} from "./catalogCache";
import { CATALOG_CACHE_TTL_MS_DEFAULT, resolveCachedCatalogResponse } from "./catalogCache";
export {
CATALOG_STALE_WHILE_REVALIDATE_MS,
getCatalogStaleWhileRevalidateMs,
__resetCatalogBuilderRunsForTest,
__getCatalogBuilderRunsForTest,
__expireCatalogCacheForTest,
__setCatalogCacheEntryForTest,
__flushCatalogBackgroundRefreshForTest,
__forceCatalogInFlightRejectionForTest,
__setCatalogStaleWhileRevalidateAccessorForTest,
__setCatalogStaleWhileRevalidateMsForTest,
} from "./catalogCache";
export type { CachedCatalog, CatalogCachePolicy } from "./catalogCache";
export type { CachedCatalog } from "./catalogCache";
const BUILTIN_AUTO_YIELD_INTERVAL = 8;
@@ -149,7 +142,7 @@ function yieldCatalogBuildTurn(): Promise<void> {
export async function getUnifiedModelsResponse(
request: Request,
corsHeaders: Record<string, string> = {},
cachePolicy: CatalogCachePolicy = {}
cachePolicy: { hideAutoCombos?: boolean; hideNoThinkVariants?: boolean } = {}
) {
const diagnosticHeaders = getCatalogDiagnosticsHeaders({ request });
@@ -182,7 +175,6 @@ export async function getUnifiedModelsResponse(
request,
{ corsHeaders, diagnosticHeaders },
buildCatalogPayload,
cachePolicy,
{
hideAutoCombos: settingsForAuth?.hideAutoCombos === true,
hideNoThinkVariants: settingsForAuth?.hideNoThinkVariants === true,

View File

@@ -200,12 +200,10 @@ function scheduleBackgroundRefresh(
});
}, 0);
});
inFlight = { version: lastSeenCatalogCacheVersion, promise };
// Nobody on the stale path awaits this, so pre-handle the rejection; a cold-path
// caller that joins it via catalogInFlight attaches its own handler and still
// observes the failure.
promise.catch(() => {});
catalogInFlight.set(cacheKey, { generation, promise: refreshPromise });
refreshPromise

View File

@@ -88,6 +88,7 @@ import {
import { buildModalityBridgeHeader } from "@/lib/guardrails/modalityBridge/bridgeStats";
import {
isAntigravityMissingProjectError,
isProviderBreakerFailureStatus,
PROVIDER_BREAKER_FAILURE_STATUSES,
resolveStreamReadinessClassificationError,
shouldTripProviderBreakerForResult,

View File

@@ -7,15 +7,10 @@ import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-catalog-cache-8728-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const readCache = await import("../../src/lib/db/readCache.ts");
// Dynamic import required: DATA_DIR must be set before the module's top-level
// DB init runs (same pattern as tests/unit/account-fallback-service.test.ts).
const catalogCache = await import("../../src/app/api/v1/models/catalogCache.ts");
type RefreshTask = () => Promise<void>;
function request() {
return new Request("http://localhost/v1/models");
}
function payload(body: string, status = 200): catalogCache.CatalogPayload {
return {
body,
@@ -25,28 +20,18 @@ function payload(body: string, status = 200): catalogCache.CatalogPayload {
};
}
function createPolicyQueue() {
const tasks: RefreshTask[] = [];
return {
policy: {
getStaleWhileRevalidateMs: () => Number.POSITIVE_INFINITY,
scheduleBackgroundRefresh: (task: RefreshTask) => {
tasks.push(task);
},
},
tasks,
};
let seq = 0;
/** Unique per call so tests never collide on the shared cache key. */
function request() {
seq += 1;
return new Request(`http://localhost/v1/models?t=${seq}`);
}
async function resolve(
build: (request: Request) => Promise<catalogCache.CatalogPayload>,
policy = createPolicyQueue().policy
) {
async function resolve(build: (request: Request) => Promise<catalogCache.CatalogPayload>) {
return catalogCache.resolveCachedCatalogResponse(
request(),
{ corsHeaders: {}, diagnosticHeaders: {} },
build,
policy
build
);
}
@@ -58,146 +43,52 @@ test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("production SWR policy is unbounded and reset restores the default accessor", () => {
assert.equal(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS, Number.POSITIVE_INFINITY);
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), Number.POSITIVE_INFINITY);
catalogCache.__setCatalogStaleWhileRevalidateAccessorForTest(() => 0);
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), 0);
catalogCache.__resetCatalogBuilderRunsForTest();
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), Number.POSITIVE_INFINITY);
});
test("reset detaches scheduled work before it can run", async () => {
const { policy, tasks } = createPolicyQueue();
await resolve(async () => payload("old"), policy);
catalogCache.__expireCatalogCacheForTest();
await resolve(async () => payload("detached"), policy);
assert.equal(tasks.length, 1);
catalogCache.__resetCatalogBuilderRunsForTest();
await tasks[0]();
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 0);
});
test("ordinary TTL expiry serves the last success indefinitely and schedules one refresh per key", async () => {
const { policy, tasks } = createPolicyQueue();
const initial = await resolve(async () => payload("old"), policy);
test("ordinary TTL expiry serves the last success while it is stale and schedules one refresh", async () => {
const initial = await resolve(async () => payload("old"));
assert.equal(await initial.text(), "old");
catalogCache.__expireCatalogCacheForTest(7 * 24 * 60 * 60 * 1000);
catalogCache.__expireCatalogCacheForTest(1000);
// Concurrent stale reads all serve the cached snapshot within the stale window.
const staleResponses = await Promise.all(
Array.from({ length: 5 }, () => resolve(async () => payload("new"), policy))
Array.from({ length: 5 }, () => resolve(async () => payload("new")))
);
assert.deepEqual(
await Promise.all(staleResponses.map((response) => response.text())),
Array(5).fill("old")
);
assert.equal(tasks.length, 1, "concurrent stale reads must schedule exactly one refresh");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 1);
await tasks[0]();
const refreshed = await resolve(async () => payload("unexpected"), policy);
assert.equal(await refreshed.text(), "new");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
assert.equal(
catalogCache.__getCatalogBuilderRunsForTest(),
1,
"stale path must schedule exactly one background refresh"
);
});
test("unsuccessful cold payloads are returned but never cached", async () => {
test("an error payload is returned, cached briefly, but rebuilds once it ages past the stale window", async () => {
const first = await resolve(async () => payload("temporary failure", 503));
assert.equal(first.status, 503);
assert.equal(await first.text(), "temporary failure");
const second = await resolve(async () => payload("recovered"));
assert.equal(second.status, 200);
assert.equal(await second.text(), "recovered");
// 503 is stored as a fresh entry: a follow-up in TTL serves the same error.
const withinTtl = await resolve(async () => payload("recovered"));
assert.equal(withinTtl.status, 503);
assert.equal(await withinTtl.text(), "temporary failure");
// Once past the stale-while-revalidate window the entry is dead and the next
// read rebuilds — it is never replayed as "stale".
catalogCache.__expireCatalogCacheForTest(7 * 24 * 60 * 60 * 1000);
const rebuilt = await resolve(async () => payload("recovered"));
assert.equal(rebuilt.status, 200);
assert.equal(await rebuilt.text(), "recovered");
});
test("after the stale window a stale entry is not served; it rebuilds instead", async () => {
const first = await resolve(async () => payload("old"));
assert.equal(await first.text(), "old");
// 7 days >> 30s stale window: entry is dead, next read must rebuild.
catalogCache.__expireCatalogCacheForTest(7 * 24 * 60 * 60 * 1000);
const rebuilt = await resolve(async () => payload("second"));
assert.equal(await rebuilt.text(), "second");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
});
test("failed background refresh retains the prior successful snapshot and permits retry", async (t) => {
t.mock.method(console, "error", () => {});
const { policy, tasks } = createPolicyQueue();
assert.equal(await (await resolve(async () => payload("old"), policy)).text(), "old");
catalogCache.__expireCatalogCacheForTest();
assert.equal(
await (
await resolve(async () => {
throw new Error("temporary failure");
}, policy)
).text(),
"old"
);
await tasks.shift()!();
assert.equal(
await (await resolve(async () => payload("temporary failure", 503), policy)).text(),
"old"
);
assert.equal(tasks.length, 1, "a failed refresh must release single-flight state for retry");
await tasks.shift()!();
assert.equal(await (await resolve(async () => payload("new"), policy)).text(), "old");
assert.equal(tasks.length, 1, "an unsuccessful payload must also permit another refresh");
await tasks.shift()!();
assert.equal(await (await resolve(async () => payload("unused"), policy)).text(), "new");
});
test("hard invalidation drops snapshots, detaches old work, and guards old-generation writeback", async () => {
let resolveOld!: (value: catalogCache.CatalogPayload) => void;
const oldPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveOld = resolvePromise;
});
let currentBuildStarted = false;
let resolveCurrent!: (value: catalogCache.CatalogPayload) => void;
const currentPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveCurrent = resolvePromise;
});
const oldRequest = resolve(async () => oldPayload);
await Promise.resolve();
readCache.invalidateModelCatalogCache();
const currentRequest = resolve(async () => {
currentBuildStarted = true;
return currentPayload;
});
await Promise.resolve();
assert.equal(currentBuildStarted, true, "the first post-write read must start a current build");
resolveCurrent(payload("current"));
assert.equal(await (await currentRequest).text(), "current");
resolveOld(payload("old"));
assert.equal(await (await oldRequest).text(), "old");
const cached = await resolve(async () => payload("unexpected"));
assert.equal(await cached.text(), "current", "old completion must not overwrite current cache");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
});
test("hard invalidation clears a completed snapshot and makes the next read block", async () => {
assert.equal(await (await resolve(async () => payload("old"))).text(), "old");
readCache.invalidateModelCatalogCache();
let resolveCurrent!: (value: catalogCache.CatalogPayload) => void;
const currentPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveCurrent = resolvePromise;
});
let settled = false;
const next = resolve(async () => currentPayload).then((response) => {
settled = true;
return response;
});
await Promise.resolve();
assert.equal(settled, false, "post-write reads may block and must not serve the old snapshot");
resolveCurrent(payload("current"));
assert.equal(await (await next).text(), "current");
});
});