Skip to content

Commit ca05c57

Browse files
committed
fix(catalog): include visibility filters in gather flight key
1 parent 772ee11 commit ca05c57

4 files changed

Lines changed: 117 additions & 0 deletions

File tree

src/claude/alias.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
* `claude-ocx-openrouter--anthropic~sclaude-opus-4-8`);
1515
* - model ids MAY contain `~` — encoded as `~t` (so slash encoding cannot
1616
* collide with a literal tilde that older releases already persisted);
17+
* - `~s` / `~t` are reserved escape sequences: decode always treats them as
18+
* `/` and `~` respectively. Model ids that historically contained the literal
19+
* two-char sequences `~s` / `~t` are not preserved (extremely rare; encode would
20+
* write `~ts` / `~tt` for those characters after a literal tilde today);
1721
* - bare `~` not followed by `s`/`t` is left as a literal tilde on decode
1822
* (legacy aliases from before slash encoding);
1923
* - model ids MAY contain `--` (resolve splits on the FIRST `--` only);

src/codex/catalog/provider-fetch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
8686
base: prov.baseUrl ?? "",
8787
adapter: prov.adapter ?? "",
8888
models: [...(prov.models ?? [])].sort(),
89+
selected: [...(prov.selectedModels ?? [])].sort(),
8990
defaultModel: prov.defaultModel ?? null,
9091
ctx: prov.contextWindow ?? null,
9192
ctxW: prov.modelContextWindows ?? null,
@@ -108,6 +109,7 @@ function gatherFlightKey(config: OcxConfig): string {
108109
.sort((a, b) => String(a.n).localeCompare(String(b.n)));
109110
const assembly = stableJson({
110111
providers,
112+
disabledModels: [...(config.disabledModels ?? [])].sort(),
111113
combos: config.combos ?? {},
112114
customModels: (config.customModels ?? []).map((cm) => ({
113115
p: cm.provider,

tests/claude-alias.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ describe("claude discovery aliases", () => {
5757
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX}demo--old~model`)).toBe("demo/old~model");
5858
});
5959

60+
test("~s/~t are reserved escapes: round-trip / and ~; legacy ~s decodes as slash", () => {
61+
// Intentional encode/decode round-trips for the reserved escape alphabet.
62+
expect(aliasForRoute("demo", "a/b")).toBe(`${CLAUDE_ALIAS_PREFIX}demo--a~sb`);
63+
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX}demo--a~sb`)).toBe("demo/a/b");
64+
expect(aliasForRoute("demo", "a~b")).toBe(`${CLAUDE_ALIAS_PREFIX}demo--a~tb`);
65+
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX}demo--a~tb`)).toBe("demo/a~b");
66+
expect(aliasForRoute("demo", "a~/b")).toBe(`${CLAUDE_ALIAS_PREFIX}demo--a~t~sb`);
67+
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX}demo--a~t~sb`)).toBe("demo/a~/b");
68+
69+
// Pre-~s-encoding aliases that stored a literal "~s" sequence are not preserved:
70+
// decode always treats ~s as "/". Document current behavior only.
71+
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX}demo--old~smodel`)).toBe("demo/old/model");
72+
});
73+
6074
test("resolveAlias rejects non-aliases and malformed ids", () => {
6175
expect(resolveAlias("claude-sonnet-4-5")).toBeNull();
6276
expect(resolveAlias("gpt-5.5")).toBeNull();

tests/gather-routed-models-single-flight.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, describe, expect, test } from "bun:test";
22
import {
33
clearGatherRoutedModelsInflight,
4+
filterCatalogVisibleModels,
45
gatherRoutedModels as gatherRoutedModelsDirect,
56
resetCatalogRuntimeStateForTests,
67
type ComboCatalogOmission,
@@ -241,4 +242,100 @@ describe("gatherRoutedModels single-flight", () => {
241242
expect(a.find(m => m.id === "m1")?.contextWindow).toBe(100_000);
242243
expect(b.find(m => m.id === "m1")?.contextWindow).toBe(200_000);
243244
});
245+
246+
test("selectedModels-only config changes do not share a flight", async () => {
247+
let fetchCount = 0;
248+
let release!: () => void;
249+
const gate = new Promise<void>(resolve => {
250+
release = resolve;
251+
});
252+
253+
globalThis.fetch = (async () => {
254+
fetchCount += 1;
255+
await gate;
256+
return new Response(
257+
JSON.stringify({ data: [{ id: "keep-me" }, { id: "drop-me" }] }),
258+
{ status: 200, headers: { "content-type": "application/json" } },
259+
);
260+
}) as typeof fetch;
261+
262+
const base: OcxConfig = {
263+
port: 10100,
264+
defaultProvider: "p",
265+
providers: {
266+
p: {
267+
adapter: "openai-chat",
268+
baseUrl: "https://sel.example.test/v1",
269+
models: [],
270+
},
271+
},
272+
};
273+
const withSel: OcxConfig = {
274+
...base,
275+
providers: {
276+
p: {
277+
...base.providers.p,
278+
selectedModels: ["keep-me"],
279+
},
280+
},
281+
};
282+
283+
const first = gatherRoutedModels(base);
284+
const second = gatherRoutedModels(withSel);
285+
await Promise.resolve();
286+
// Distinct flight keys => two live discoveries (cannot reuse unfiltered result).
287+
expect(fetchCount).toBe(2);
288+
release();
289+
const [all, withSelModels] = await Promise.all([first, second]);
290+
// gather itself is unfiltered; visibility is applied by callers.
291+
expect(all.map(m => m.id).sort()).toEqual(["drop-me", "keep-me"]);
292+
expect(withSelModels.map(m => m.id).sort()).toEqual(["drop-me", "keep-me"]);
293+
expect(filterCatalogVisibleModels(all, base).map(m => m.id).sort()).toEqual(["drop-me", "keep-me"]);
294+
expect(filterCatalogVisibleModels(withSelModels, withSel).map(m => m.id)).toEqual(["keep-me"]);
295+
});
296+
297+
test("disabledModels-only config changes do not share a flight", async () => {
298+
let fetchCount = 0;
299+
let release!: () => void;
300+
const gate = new Promise<void>(resolve => {
301+
release = resolve;
302+
});
303+
304+
globalThis.fetch = (async () => {
305+
fetchCount += 1;
306+
await gate;
307+
return new Response(
308+
JSON.stringify({ data: [{ id: "keep-me" }, { id: "drop-me" }] }),
309+
{ status: 200, headers: { "content-type": "application/json" } },
310+
);
311+
}) as typeof fetch;
312+
313+
const base: OcxConfig = {
314+
port: 10100,
315+
defaultProvider: "p",
316+
providers: {
317+
p: {
318+
adapter: "openai-chat",
319+
baseUrl: "https://dis.example.test/v1",
320+
models: [],
321+
},
322+
},
323+
};
324+
const withDisabled: OcxConfig = {
325+
...base,
326+
disabledModels: ["p/drop-me"],
327+
};
328+
329+
const first = gatherRoutedModels(base);
330+
const second = gatherRoutedModels(withDisabled);
331+
await Promise.resolve();
332+
expect(fetchCount).toBe(2);
333+
release();
334+
const [all, withDisabledModels] = await Promise.all([first, second]);
335+
expect(all.map(m => m.id).sort()).toEqual(["drop-me", "keep-me"]);
336+
expect(withDisabledModels.map(m => m.id).sort()).toEqual(["drop-me", "keep-me"]);
337+
expect(filterCatalogVisibleModels(all, base).map(m => m.id).sort()).toEqual(["drop-me", "keep-me"]);
338+
expect(filterCatalogVisibleModels(withDisabledModels, withDisabled).map(m => m.id)).toEqual(["keep-me"]);
339+
});
340+
244341
});

0 commit comments

Comments
 (0)