Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1539,8 +1539,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
modelDiscovery: {
// Resolves against effectiveBaseUrl (registry baseUrl .../v1) to the same
// canonical endpoint https://inference-api.nousresearch.com/v1/models.
// Nous returns a mixed paid/free catalog whose JSON can exceed 256 KiB;
// keep the provider-specific limit below the process-wide 4 MiB ceiling.
path: "models",
maxResponseBytes: 262_144,
maxResponseBytes: 1_048_576,
maxModels: 512,
},
note: "Nous Research subscription gateway. OAuth device login with your own Portal account; mixed paid + :free models discovered live (fallback seed 2026-08-10: tencent/hy3:free, poolside/laguna-s-2.1:free, stepfun/step-3.7-flash:free, poolside/laguna-xs-2.1:free).",
Expand Down
34 changes: 34 additions & 0 deletions tests/providers/provider-connection-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,40 @@ describe("POST /api/providers/test (WP040 connectivity probe)", () => {
});
});

test("Nous probe accepts 390 synthetic paid/free rows above 256 KiB (#3939)", async () => {
const payload = JSON.stringify({
data: Array.from({ length: 390 }, (_, index) => ({
id: index === 0 ? "tencent/hy3:free" : `vendor/model-${index}`,
metadata: { description: "x".repeat(1_400) },
})),
});
const bytes = new TextEncoder().encode(payload).byteLength;
expect(bytes).toBeGreaterThan(262_144);
expect(bytes).toBeLessThan(1_048_576);
let fetches = 0;
globalThis.fetch = (async (input, init) => {
fetches += 1;
expect(String(input)).toBe("https://inference-api.nousresearch.com/v1/models");
expect(init?.method ?? "GET").toBe("GET");
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer access-token-nous-probe-fixture");
return new Response(payload, { headers: { "content-type": "application/json" } });
}) as typeof fetch;
await saveCredential("nous", {
access: "access-token-nous-probe-fixture",
refresh: "nous-probe-fixture-refresh",
expires: Date.now() + 3_600_000,
});
const config = baseConfig({
nous: { ...structuredClone(OAUTH_PROVIDERS.nous!.providerConfig) },
});

const { status, body } = await probe(config, "nous");

expect(status).toBe(200);
expect(fetches).toBe(1);
expect(body).toMatchObject({ ok: true, models: 390 });
});

test("Google's models-array response shape is accepted (x-goog-api-key path)", async () => {
let requestedUrl = "";
globalThis.fetch = (async (input: RequestInfo | URL) => {
Expand Down
82 changes: 79 additions & 3 deletions tests/providers/provider-model-discovery-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { afterEach, describe, expect, spyOn, test } from "bun:test";
import { readFileSync } from "node:fs";
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
import { mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { gatherRoutedModels } from "../../src/codex/catalog";
import { catalogHintsFromModelsApiItem } from "../../src/codex/catalog/provider-fetch";
import { clearModelCache, getFreshCached, setCached } from "../../src/codex/model-cache";
import { clearModelCache, getFreshCached, getProviderDiscoveryStatus, getProviderLiveModelCount, setCached } from "../../src/codex/model-cache";
import { buildModelsRequest } from "../../src/oauth";
import { saveCredential } from "../../src/oauth/store";
import { KEY_LOGIN_PROVIDERS, validateApiKey } from "../../src/oauth/key-providers";
import { deriveKeyLoginMap, providerConfigSeed } from "../../src/providers/derive";
import {
Expand All @@ -25,6 +27,7 @@ import type { OcxConfig, OcxProviderConfig } from "../../src/types";
import { withStubbedProviderFetch } from "../helpers/catalog-provider-fetch";
import { withRegistryDiscovery } from "../helpers/provider-registry-discovery";
import { fixturePath } from "../helpers/repo-root";
import { removeTreeWithRetry } from "../helpers/remove-tree";

const FIXTURE = readFileSync(fixturePath("provider-model-discovery.json"), "utf8");
const originalFetch = globalThis.fetch;
Expand Down Expand Up @@ -428,6 +431,79 @@ describe("registry-owned provider model discovery", () => {
expect(cancelled).toBe(true);
});

describe("Nous native catalog response cap (#3939)", () => {
let previousHome: string | undefined;
let credentialHome: string;

beforeEach(async () => {
previousHome = process.env.OPENCODEX_HOME;
credentialHome = mkdtempSync(join(tmpdir(), "ocx-nous-discovery-"));
process.env.OPENCODEX_HOME = credentialHome;
clearModelCache("nous");
await saveCredential("nous", {
access: "access-token-nous-discovery-fixture",
refresh: "nous-discovery-fixture-refresh",
expires: Date.now() + 3_600_000,
});
});

afterEach(() => {
clearModelCache("nous");
if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = previousHome;
removeTreeWithRetry(credentialHome);
});

test("gathers and caches 390 synthetic paid/free rows above 256 KiB", async () => {
const entry = PROVIDER_REGISTRY.find(row => row.id === "nous");
if (!entry) throw new Error("missing nous registry entry");
const payload = JSON.stringify({
data: Array.from({ length: 390 }, (_, index) => ({
id: index === 0 ? "tencent/hy3:free" : `vendor/model-${index}`,
metadata: { description: "x".repeat(1_400) },
})),
});
const bytes = new TextEncoder().encode(payload).byteLength;
expect(bytes).toBeGreaterThan(262_144);
expect(bytes).toBeLessThan(1_048_576);

let fetches = 0;
globalThis.fetch = (async (input, init) => {
fetches += 1;
expect(String(input)).toBe("https://inference-api.nousresearch.com/v1/models");
expect(init?.method ?? "GET").toBe("GET");
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer access-token-nous-discovery-fixture");
return new Response(payload, { headers: { "content-type": "application/json" } });
}) as typeof fetch;
const config = withStubbedProviderFetch<OcxConfig>({
defaultProvider: "nous",
providers: { nous: { ...providerConfigSeed(entry), models: ["safe-fallback"] } },
});
const discovery = resolveProviderModelDiscovery("nous", config.providers.nous!);
expect(discovery.maxResponseBytes).toBe(1_048_576);
expect(discovery.maxModels).toBe(512);
const warning = spyOn(console, "warn").mockImplementation(() => {});
try {
const models = (await gatherRoutedModels(config)).filter(model => model.provider === "nous");
expect(fetches).toBe(1);
expect(models).toHaveLength(390);
const ids = models.map(model => model.id);
expect(ids).toContain("tencent/hy3:free");
expect(ids).toContain("vendor/model-1");
expect(ids).toContain("vendor/model-389");
expect(ids).not.toContain("safe-fallback");
// Gather sorts its published rows; the cache retains upstream order.
expect(getFreshCached("nous", 60_000)?.map(model => model.id).sort()).toEqual([...ids].sort());
expect(getProviderLiveModelCount("nous")).toBe(390);
expect(getProviderDiscoveryStatus("nous")).toEqual({ status: "ok" });
expect((await gatherRoutedModels(config)).filter(model => model.provider === "nous")).toEqual(models);
expect(fetches).toBe(1);
} finally {
warning.mockRestore();
}
});
});

test("rejects invalid UTF-8 before JSON parsing", async () => {
const invalidUtf8Json = new Uint8Array([
0x7b, 0x22, 0x78, 0x22, 0x3a, 0x22, 0xc3, 0x28, 0x22, 0x7d,
Expand Down
Loading