Files
oh-my-pi/packages/catalog/test/variant-collapse.test.ts
T
roboomp 466f769cc8 fix(catalog): collapse Cursor Grok 4.5/4.6 effort siblings
Cursor advertises Grok 4.5/4.6 as per-effort sibling ids
(cursor-grok-4.6-low|-medium|-high|-xhigh plus -fast variants), but
VARIANT_COLLAPSE_TABLES had no cursor entry, so the model hub showed 14
unrouted siblings instead of one logical model with effort routing.
GetUsableModels ships no thinkingDetails and the bundled references read
reasoning:false, so the picker also treated them as non-reasoning.

- Add CURSOR_VARIANT_COLLAPSE_TABLE folding each service-tier lane
  (standard + -fast) into one logical model with effort routing onto the
  live wire ids, mirroring Devin's grok-4-5 collapse.
- Rename the generic devinTierFamily/DevinTierRoutes helper to
  tierFamily/TierRoutes now that both Devin and Cursor tables use it.
- Mark versioned cursor-grok-<version> ids as reasoning during discovery
  (grok-code-* coding models stay non-reasoning).

Fixes #8803
2026-08-19 09:52:51 +00:00

1073 lines
41 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { buildModel } from "@oh-my-pi/pi-catalog/build";
import {
ANTIGRAVITY_PRIMARY_ENDPOINT,
fetchAntigravityDiscoveryModels,
} from "@oh-my-pi/pi-catalog/discovery/antigravity";
import { Effort } from "@oh-my-pi/pi-catalog/effort";
import { stripThinkingVariantToken } from "@oh-my-pi/pi-catalog/identity/family";
import { resolveProviderModels } from "@oh-my-pi/pi-catalog/model-manager";
import { resolveWireModelId } from "@oh-my-pi/pi-catalog/model-thinking";
import { googleGeminiCliModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/google";
import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
import {
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
CURSOR_VARIANT_COLLAPSE_TABLE,
collapseEffortVariants,
collapseEffortVariantsAcrossProviders,
DEVIN_VARIANT_COLLAPSE_TABLE,
deriveThinkingPairFamilies,
GEMINI_CLI_VARIANT_COLLAPSE_TABLE,
getVariantAliasSources,
isVariantCollapsedSpec,
resolveBareVariantAlias,
resolveVariantAlias,
} from "@oh-my-pi/pi-catalog/variant-collapse";
function memberSpec(
id: string,
overrides: Partial<ModelSpec<"google-gemini-cli">> = {},
): ModelSpec<"google-gemini-cli"> {
return {
id,
name: id,
api: "google-gemini-cli",
provider: "google-antigravity",
baseUrl: "https://daily-cloudcode-pa.googleapis.com",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_048_576,
maxTokens: 65_535,
...overrides,
};
}
function pairSpec(
id: string,
overrides: Partial<ModelSpec<"openai-completions">> = {},
): ModelSpec<"openai-completions"> {
return {
id,
name: id,
api: "openai-completions",
provider: "venice",
baseUrl: "https://api.venice.ai/api/v1",
reasoning: false,
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 8_192,
...overrides,
};
}
const PAIR_THINKING = {
mode: "budget",
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
} as const;
const FLASH_TRIPLET = () => [
memberSpec("gemini-3.5-flash-extra-low", { maxTokens: 32_000 }),
memberSpec("gemini-3.5-flash-low"),
memberSpec("gemini-3-flash-agent", { input: ["text"] }),
];
describe("collapseEffortVariants", () => {
it("collapses the 3.5-flash triplet into one routed logical spec", () => {
const out = collapseEffortVariants(
[...FLASH_TRIPLET(), memberSpec("gemini-2.5-flash-lite")],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
expect(out.map(m => m.id).sort()).toEqual(["gemini-2.5-flash-lite", "gemini-3.5-flash"]);
// Non-family specs pass through by reference.
expect(out.find(m => m.id === "gemini-2.5-flash-lite")?.thinking).toBeUndefined();
const flash = out.find(m => m.id === "gemini-3.5-flash");
expect(flash?.name).toBe("Gemini 3.5 Flash");
expect(flash?.requestModelId).toBe("gemini-3.5-flash-extra-low");
// Capability union: max caps, image support from any member.
expect(flash?.maxTokens).toBe(65_535);
expect(flash?.input).toEqual(["text", "image"]);
expect(flash?.thinking?.mode).toBe("budget");
expect(flash?.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
expect(flash?.thinking?.effortBudgets).toEqual({
minimal: 1000,
low: 1000,
medium: 4000,
high: 10000,
});
expect(flash?.thinking?.suppressWhenOff).toBe(true);
expect(flash?.thinking?.effortRouting).toEqual({
off: "gemini-3.5-flash-extra-low",
minimal: "gemini-3.5-flash-extra-low",
low: "gemini-3.5-flash-extra-low",
medium: "gemini-3.5-flash-low",
high: "gemini-3-flash-agent",
});
});
it("collapses Gemini 3.6 Flash tiers into one routed logical spec", () => {
const out = collapseEffortVariants(
[
memberSpec("gemini-3.6-flash-high"),
memberSpec("gemini-3.6-flash-low"),
memberSpec("gemini-3.6-flash-medium"),
memberSpec("gemini-3.6-flash-tiered"),
],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
expect(out).toHaveLength(1);
const flash = out[0];
expect(flash?.id).toBe("gemini-3.6-flash");
expect(flash?.name).toBe("Gemini 3.6 Flash");
expect(flash?.requestModelId).toBe("gemini-3.6-flash-low");
expect(flash?.thinking).toEqual({
mode: "google-level",
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
requiresEffort: true,
effortRouting: {
minimal: "gemini-3.6-flash-low",
low: "gemini-3.6-flash-low",
medium: "gemini-3.6-flash-medium",
high: "gemini-3.6-flash-high",
},
});
const model = buildModel(flash as ModelSpec<"google-gemini-cli">);
expect(resolveWireModelId(model, Effort.Minimal)).toBe("gemini-3.6-flash-low");
expect(resolveWireModelId(model, Effort.Low)).toBe("gemini-3.6-flash-low");
expect(resolveWireModelId(model, Effort.Medium)).toBe("gemini-3.6-flash-medium");
expect(resolveWireModelId(model, Effort.High)).toBe("gemini-3.6-flash-high");
});
it("drops routes whose target member is absent", () => {
const out = collapseEffortVariants(
[memberSpec("gemini-3.5-flash-extra-low")],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
expect(out).toHaveLength(1);
expect(out[0]?.id).toBe("gemini-3.5-flash");
expect(out[0]?.requestModelId).toBe("gemini-3.5-flash-extra-low");
// minimal+low route to extra-low (present); medium (flash-low) and high
// (flash-agent) targets are absent and drop.
expect(out[0]?.thinking?.effortRouting).toEqual({
off: "gemini-3.5-flash-extra-low",
minimal: "gemini-3.5-flash-extra-low",
low: "gemini-3.5-flash-extra-low",
});
});
it("routes both bare and -thinking sonnet 4.6 ids to the bare wire id (backend has no -thinking twin)", () => {
const out = collapseEffortVariants(
[
memberSpec("claude-sonnet-4-6", { maxTokens: 64_000 }),
memberSpec("claude-sonnet-4-6-thinking", { maxTokens: 64_000 }),
],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
expect(out).toHaveLength(1);
const spec = out[0];
expect(spec?.id).toBe("claude-sonnet-4-6");
// Default wire id equals the logical id — requestModelId is omitted and
// no effortRouting is needed; the request-body `thinkingBudget` carries
// per-effort behavior on a single shared wire id.
expect(spec?.requestModelId).toBeUndefined();
expect(spec?.thinking?.effortRouting).toBeUndefined();
expect(spec?.thinking?.mode).toBe("budget");
const model = buildModel(spec as ModelSpec<"google-gemini-cli">);
expect(resolveWireModelId(model, undefined)).toBe("claude-sonnet-4-6");
expect(resolveWireModelId(model, Effort.High)).toBe("claude-sonnet-4-6");
});
it("collapses a bare-only sonnet 4.6 discovery to the bare wire id", () => {
const out = collapseEffortVariants(
[memberSpec("claude-sonnet-4-6", { maxTokens: 64_000 })],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
expect(out).toHaveLength(1);
const spec = out[0];
expect(spec?.id).toBe("claude-sonnet-4-6");
expect(spec?.requestModelId).toBeUndefined();
expect(spec?.thinking?.effortRouting).toBeUndefined();
const model = buildModel(spec as ModelSpec<"google-gemini-cli">);
// Regression: previously this routed thinking efforts to a non-existent
// `claude-sonnet-4-6-thinking` wire id and 404'd on the backend.
expect(resolveWireModelId(model, Effort.High)).toBe("claude-sonnet-4-6");
expect(resolveWireModelId(model, undefined)).toBe("claude-sonnet-4-6");
});
it("routes every opus 4.6 request to the -thinking wire id (the only one the backend exposes)", () => {
const out = collapseEffortVariants([memberSpec("claude-opus-4-6-thinking")], ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
expect(out).toHaveLength(1);
const spec = out[0];
expect(spec?.id).toBe("claude-opus-4-6");
expect(spec?.requestModelId).toBe("claude-opus-4-6-thinking");
expect(spec?.thinking?.effortRouting).toBeUndefined();
const model = buildModel(spec as ModelSpec<"google-gemini-cli">);
// Thinking-off and every effort fall back through requestModelId to
// the only wire id the backend actually serves.
expect(resolveWireModelId(model, undefined)).toBe("claude-opus-4-6-thinking");
expect(resolveWireModelId(model, Effort.High)).toBe("claude-opus-4-6-thinking");
});
it("reconciles a stale Sonnet 4.6 snapshot whose routing still targets the dead -thinking wire id", () => {
// Bundled `models.json` and SQLite cache rows written before #3071
// route every effort to `claude-sonnet-4-6-thinking` (a wire id
// `daily-cloudcode-pa` does not expose). The `retiredMembers` entry
// triggers `reconcileRetiredRouting`, which re-points every retired
// route to the live bare wire id.
const stale: ModelSpec<"google-gemini-cli"> = {
...memberSpec("claude-sonnet-4-6", { maxTokens: 64_000 }),
requestModelId: "claude-sonnet-4-6",
thinking: {
mode: "budget",
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
effortRouting: {
off: "claude-sonnet-4-6",
[Effort.Minimal]: "claude-sonnet-4-6-thinking",
[Effort.Low]: "claude-sonnet-4-6-thinking",
[Effort.Medium]: "claude-sonnet-4-6-thinking",
[Effort.High]: "claude-sonnet-4-6-thinking",
},
},
};
const out = collapseEffortVariants([stale], ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
expect(out).toHaveLength(1);
const spec = out[0];
expect(spec?.id).toBe("claude-sonnet-4-6");
expect(spec?.thinking?.effortRouting).toEqual({
off: "claude-sonnet-4-6",
minimal: "claude-sonnet-4-6",
low: "claude-sonnet-4-6",
medium: "claude-sonnet-4-6",
high: "claude-sonnet-4-6",
});
const model = buildModel(spec as ModelSpec<"google-gemini-cli">);
expect(resolveWireModelId(model, Effort.High)).toBe("claude-sonnet-4-6");
});
it("reconciles a stale Opus 4.6 snapshot whose routing still targets the dead bare wire id", () => {
// Defensive: a stale snapshot with `off`/efforts pointing at the bare
// `claude-opus-4-6` (never exposed by Antigravity) is re-pointed to
// the live `-thinking` wire id by `reconcileRetiredRouting`.
const stale: ModelSpec<"google-gemini-cli"> = {
...memberSpec("claude-opus-4-6"),
requestModelId: "claude-opus-4-6",
thinking: {
mode: "budget",
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
effortRouting: {
off: "claude-opus-4-6",
[Effort.Minimal]: "claude-opus-4-6",
[Effort.Low]: "claude-opus-4-6",
[Effort.Medium]: "claude-opus-4-6",
[Effort.High]: "claude-opus-4-6",
},
},
};
const out = collapseEffortVariants([stale], ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
expect(out).toHaveLength(1);
const spec = out[0];
expect(spec?.id).toBe("claude-opus-4-6");
expect(spec?.requestModelId).toBe("claude-opus-4-6-thinking");
expect(spec?.thinking?.effortRouting).toEqual({
off: "claude-opus-4-6-thinking",
minimal: "claude-opus-4-6-thinking",
low: "claude-opus-4-6-thinking",
medium: "claude-opus-4-6-thinking",
high: "claude-opus-4-6-thinking",
});
const model = buildModel(spec as ModelSpec<"google-gemini-cli">);
expect(resolveWireModelId(model, undefined)).toBe("claude-opus-4-6-thinking");
expect(resolveWireModelId(model, Effort.High)).toBe("claude-opus-4-6-thinking");
});
it("renames single-member families through requestModelId with no routing", () => {
const out = collapseEffortVariants(
[memberSpec("gpt-oss-120b-medium", { input: ["text"] })],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
expect(out[0]?.id).toBe("gpt-oss-120b");
expect(out[0]?.requestModelId).toBe("gpt-oss-120b-medium");
expect(out[0]?.thinking?.effortRouting).toBeUndefined();
expect(out[0]?.thinking?.mode).toBe("budget");
});
it("is idempotent and dedupes mixed raw+collapsed input", () => {
const once = collapseEffortVariants(FLASH_TRIPLET(), ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
expect(collapseEffortVariants(once, ANTIGRAVITY_VARIANT_COLLAPSE_TABLE)).toEqual(once);
// Stale raw members beside the live collapsed entry dedupe away; the
// collapsed entry wins verbatim.
const mixed = [...once, memberSpec("gemini-3.5-flash-low"), memberSpec("gemini-3-flash-agent")];
const deduped = collapseEffortVariants(mixed, ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
expect(deduped).toEqual(once);
});
it("keeps gemini-cli flash on the level transport with the original routing", () => {
const out = collapseEffortVariants(FLASH_TRIPLET(), GEMINI_CLI_VARIANT_COLLAPSE_TABLE);
const flash = out.find(m => m.id === "gemini-3.5-flash");
expect(flash?.thinking?.mode).toBe("google-level");
expect(flash?.thinking?.effortBudgets).toBeUndefined();
expect(flash?.thinking?.effortRouting).toEqual({
off: "gemini-3.5-flash-extra-low",
minimal: "gemini-3-flash-agent",
low: "gemini-3.5-flash-extra-low",
medium: "gemini-3.5-flash-extra-low",
high: "gemini-3.5-flash-low",
});
});
it("collapses the 3.1-pro family on the budget transport with the +1 budgets", () => {
const out = collapseEffortVariants(
[memberSpec("gemini-3.1-pro-low"), memberSpec("gemini-pro-agent")],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
const pro = out.find(m => m.id === "gemini-3.1-pro");
expect(pro?.thinking?.mode).toBe("budget");
expect(pro?.thinking?.effortBudgets).toEqual({ low: 1001, high: 10001 });
expect(pro?.thinking?.effortRouting).toEqual({
off: "gemini-3.1-pro-low",
low: "gemini-3.1-pro-low",
high: "gemini-pro-agent",
});
});
it("refreshes a stale alias-keyed flash snapshot in place to the budget contract", () => {
// Bundled snapshots key the flash family under the recycled `gemini-3-flash`
// id on the old level transport. That exact id is load-bearing, so it is
// refreshed in place (same id) rather than re-keyed to `gemini-3.5-flash`.
const stale: ModelSpec<"google-gemini-cli"> = {
...memberSpec("gemini-3-flash"),
reasoning: true,
thinking: { mode: "google-level", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
};
const out = collapseEffortVariants([stale], ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
const flash = out.find(m => m.id === "gemini-3-flash");
expect(flash).toBeDefined();
expect(flash?.thinking?.mode).toBe("budget");
expect(flash?.thinking?.effortBudgets).toEqual({ minimal: 1000, low: 1000, medium: 4000, high: 10000 });
expect(flash?.thinking?.effortRouting?.high).toBe("gemini-3-flash-agent");
expect(flash?.requestModelId).toBe("gemini-3.5-flash-extra-low");
});
it("heals a stale alias row alongside the canonical row (merge coexistence)", () => {
// The model-manager merge keeps both the bundled exact `gemini-3-flash`
// and the discovered canonical `gemini-3.5-flash` (exact-id merge); both
// must land on the budget transport and neither is dropped.
const stale: ModelSpec<"google-gemini-cli"> = {
...memberSpec("gemini-3-flash"),
reasoning: true,
thinking: { mode: "google-level", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
};
const canonical = collapseEffortVariants(FLASH_TRIPLET(), ANTIGRAVITY_VARIANT_COLLAPSE_TABLE).find(
m => m.id === "gemini-3.5-flash",
);
expect(canonical).toBeDefined();
const out = collapseEffortVariants(
[stale, canonical as ModelSpec<"google-gemini-cli">],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
expect(out.map(m => m.id).sort()).toEqual(["gemini-3-flash", "gemini-3.5-flash"]);
expect(out.find(m => m.id === "gemini-3-flash")?.thinking?.mode).toBe("budget");
expect(out.find(m => m.id === "gemini-3.5-flash")?.thinking?.mode).toBe("budget");
});
it("refreshes a stale family.id-keyed 3.1-pro snapshot in place to the budget contract", () => {
// Pass-through branch: a bundled collapsed `gemini-3.1-pro` on the old level
// transport with no live members refreshes from the hand table.
const stale: ModelSpec<"google-gemini-cli"> = {
...memberSpec("gemini-3.1-pro"),
reasoning: true,
requestModelId: "gemini-3.1-pro-low",
thinking: {
mode: "google-level",
efforts: [Effort.Low, Effort.High],
effortRouting: { off: "gemini-3.1-pro-low", low: "gemini-3.1-pro-low", high: "gemini-pro-agent" },
suppressWhenOff: true,
},
};
const out = collapseEffortVariants([stale], ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
const pro = out.find(m => m.id === "gemini-3.1-pro");
expect(pro?.thinking?.mode).toBe("budget");
expect(pro?.thinking?.effortBudgets).toEqual({ low: 1001, high: 10001 });
});
});
describe("stripThinkingVariantToken", () => {
it("strips trailing and infix tokens case-insensitively", () => {
expect(stripThinkingVariantToken("kimi-k2-thinking")).toBe("kimi-k2");
expect(stripThinkingVariantToken("hf:moonshotai/Kimi-K2-Thinking")).toBe("hf:moonshotai/Kimi-K2");
expect(stripThinkingVariantToken("xiaomi/mimo-v2-flash-thinking-original")).toBe("xiaomi/mimo-v2-flash-original");
expect(stripThinkingVariantToken("[Kiro] claude-opus-4-8-thinking [X]")).toBe("[Kiro] claude-opus-4-8 [X]");
// Reasoning-token spellings pair the same way.
expect(stripThinkingVariantToken("x-ai/grok-4.1-fast-reasoning")).toBe("x-ai/grok-4.1-fast");
expect(stripThinkingVariantToken("claude-3-7-sonnet-reasoner")).toBe("claude-3-7-sonnet");
});
it("ignores ids without a token and negated tokens", () => {
expect(stripThinkingVariantToken("kimi-k2")).toBeUndefined();
// OpenRouter route variants use `:thinking` — a different mechanism.
expect(stripThinkingVariantToken("anthropic/claude-3.7-sonnet:thinking")).toBeUndefined();
expect(stripThinkingVariantToken("thinkingcap-1")).toBeUndefined();
// `non-thinking` names the NON-thinking SKU.
expect(stripThinkingVariantToken("deepseek-non-thinking-v3.2-exp")).toBeUndefined();
});
});
describe("deriveThinkingPairFamilies", () => {
it("derives a pair family routing off to bare and efforts to -thinking", () => {
const specs = [pairSpec("kimi-k2"), pairSpec("kimi-k2-thinking", { reasoning: true, thinking: PAIR_THINKING })];
const families = deriveThinkingPairFamilies(specs);
expect(families).toHaveLength(1);
expect(families[0]?.id).toBe("kimi-k2");
expect(families[0]?.members).toEqual(["kimi-k2", "kimi-k2-thinking"]);
expect(families[0]?.routing).toEqual({
off: "kimi-k2",
minimal: "kimi-k2-thinking",
low: "kimi-k2-thinking",
medium: "kimi-k2-thinking",
high: "kimi-k2-thinking",
});
const out = collapseEffortVariants(specs, { families });
expect(out).toHaveLength(1);
expect(out[0]?.id).toBe("kimi-k2");
expect(out[0]?.requestModelId).toBeUndefined();
expect(out[0]?.reasoning).toBe(true);
expect(out[0]?.thinking?.effortRouting?.[Effort.High]).toBe("kimi-k2-thinking");
expect(out[0]?.thinking?.effortRouting?.off).toBe("kimi-k2");
});
it("pairs infix -thinking tokens", () => {
const specs = [
pairSpec("xiaomi/mimo-v2-flash-original"),
pairSpec("xiaomi/mimo-v2-flash-thinking-original", { reasoning: true, thinking: PAIR_THINKING }),
];
const families = deriveThinkingPairFamilies(specs);
expect(families).toHaveLength(1);
expect(families[0]?.id).toBe("xiaomi/mimo-v2-flash-original");
});
it("collapses metadata-poor twins using the bare member's surface", () => {
// Aggregators routinely ship the twin with reasoning:false, no
// thinking config, and zero (unknown) pricing — name wins.
const base = pairSpec("xiaomi/mimo-v2-flash", {
reasoning: true,
cost: { input: 0.09, output: 0.29, cacheRead: 0.045, cacheWrite: 0 },
thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh] },
});
const twin = pairSpec("xiaomi/mimo-v2-flash-thinking", {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
});
const families = deriveThinkingPairFamilies([base, twin]);
expect(families).toHaveLength(1);
expect(families[0]?.thinking.mode).toBe("effort");
const out = collapseEffortVariants([base, twin], { families });
expect(out).toHaveLength(1);
expect(out[0]?.reasoning).toBe(true);
expect(out[0]?.cost.input).toBe(0.09);
expect(out[0]?.thinking?.effortRouting?.[Effort.XHigh]).toBe("xiaomi/mimo-v2-flash-thinking");
expect(out[0]?.thinking?.effortRouting?.off).toBe("xiaomi/mimo-v2-flash");
});
it("collapses zero-cost metadata-less twins with a derived surface", () => {
const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const base = pairSpec("TEE/kimi-k2.5", { cost: zeroCost });
const twin = pairSpec("TEE/kimi-k2.5-thinking", { cost: zeroCost });
const families = deriveThinkingPairFamilies([base, twin]);
expect(families).toHaveLength(1);
expect(families[0]?.thinking.efforts.length).toBeGreaterThan(0);
const out = collapseEffortVariants([base, twin], { families });
expect(out.map(m => m.id)).toEqual(["TEE/kimi-k2.5"]);
// Effort routing to a live thinking id forces reasoning even though
// upstream marked neither member.
expect(out[0]?.reasoning).toBe(true);
expect(out[0]?.thinking?.effortRouting?.off).toBe("TEE/kimi-k2.5");
});
it("never merges price-divergent twins or orphan thinking ids", () => {
// Different pricing — distinct SKUs.
expect(
deriveThinkingPairFamilies([
pairSpec("kimi-k2"),
pairSpec("kimi-k2-thinking", {
reasoning: true,
thinking: PAIR_THINKING,
cost: { input: 3, output: 6, cacheRead: 0.1, cacheWrite: 0 },
}),
]),
).toEqual([]);
// No bare twin.
expect(
deriveThinkingPairFamilies([
pairSpec("moonshot.kimi-k2-thinking", { reasoning: true, thinking: PAIR_THINKING }),
]),
).toEqual([]);
// Api mismatch.
expect(
deriveThinkingPairFamilies([
pairSpec("kimi-k2"),
{
...pairSpec("kimi-k2-thinking", { reasoning: true, thinking: PAIR_THINKING }),
api: "anthropic-messages",
} as unknown as ModelSpec<"openai-completions">,
]),
).toEqual([]);
});
it("defers to hand-table families for claimed ids", () => {
const specs = [
memberSpec("claude-sonnet-4-6"),
memberSpec("claude-sonnet-4-6-thinking", { thinking: PAIR_THINKING }),
];
expect(deriveThinkingPairFamilies(specs, ANTIGRAVITY_VARIANT_COLLAPSE_TABLE)).toEqual([]);
});
});
describe("collapseEffortVariantsAcrossProviders", () => {
it("applies hand tables and derived pairs per provider", () => {
const out = collapseEffortVariantsAcrossProviders([
memberSpec("gemini-3.5-flash-extra-low"),
pairSpec("kimi-k2"),
pairSpec("kimi-k2-thinking", { reasoning: true, thinking: PAIR_THINKING }),
// Same ids on a provider without a table or a live bare twin stay.
pairSpec("qwen3-vl-32b-thinking", { provider: "aimlapi", reasoning: true, thinking: PAIR_THINKING }),
]);
expect(out.map(m => `${m.provider}/${m.id}`).sort()).toEqual([
"aimlapi/qwen3-vl-32b-thinking",
"google-antigravity/gemini-3.5-flash",
"venice/kimi-k2",
]);
});
});
describe("Devin tier routing", () => {
const family = (id: string) => {
const found = DEVIN_VARIANT_COLLAPSE_TABLE.families.find(f => f.id === id);
if (!found) throw new Error(`Devin family ${id} missing`);
return found;
};
it("routes user efforts 1:1 onto per-tier siblings including max", () => {
const opus = family("claude-opus-4-8");
expect(opus.routing).toEqual({
[Effort.Low]: "claude-opus-4-8-low",
[Effort.Medium]: "claude-opus-4-8-medium",
[Effort.High]: "claude-opus-4-8-high",
[Effort.XHigh]: "claude-opus-4-8-xhigh",
[Effort.Max]: "claude-opus-4-8-max",
});
expect(opus.thinking.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max]);
expect(opus.thinking.requiresEffort).toBe(true);
const sol = family("gpt-5-6-sol");
expect(sol.routing[Effort.Max]).toBe("gpt-5-6-sol-max");
expect(sol.routing[Effort.Low]).toBe("gpt-5-6-sol-low");
expect(sol.routing.off).toBe("gpt-5-6-sol-none");
expect(sol.routing[Effort.Minimal]).toBeUndefined();
const solFast = family("gpt-5-6-sol-fast");
expect(solFast.routing[Effort.Max]).toBe("gpt-5-6-sol-max-priority");
expect(solFast.thinking.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max]);
});
it("keeps pre-5.6 families without a -max sibling on the xhigh ceiling", () => {
const gpt55 = family("gpt-5-5");
expect(gpt55.thinking.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
expect(gpt55.routing[Effort.Minimal]).toBeUndefined();
expect(gpt55.routing[Effort.Max]).toBeUndefined();
});
it("routes current Devin families onto their account-visible wire variants", () => {
const fable = family("claude-fable-5");
expect(fable.routing[Effort.Low]).toBe("claude-5-fable-low");
expect(fable.routing[Effort.Max]).toBe("claude-5-fable-max");
const swe = family("swe-1-7");
expect(swe.thinking.efforts).toEqual([Effort.Medium, Effort.Max]);
expect(swe.routing[Effort.Medium]).toBe("swe-1-7-medium");
expect(swe.routing[Effort.Max]).toBe("swe-1-7");
const gemini = family("gemini-3-6-flash");
expect(gemini.routing[Effort.Minimal]).toBe("gemini-3-6-flash-minimal");
expect(gemini.routing[Effort.High]).toBe("gemini-3-6-flash-high");
const inkling = family("inkling");
expect(inkling.routing.off).toBe("inkling-none");
expect(inkling.routing[Effort.Max]).toBe("inkling-max");
});
it("collapses entitled current variants and resolves the selected effort to the wire UID", () => {
const rawIds = [
"claude-5-fable-low",
"claude-5-fable-medium",
"claude-5-fable-high",
"claude-5-fable-xhigh",
"claude-5-fable-max",
"swe-1-7-medium",
"swe-1-7",
];
const specs = rawIds.map(
(id): ModelSpec<"devin-agent"> => ({
id,
name: id,
api: "devin-agent",
provider: "devin",
baseUrl: "https://server.codeium.com",
reasoning: true,
input: ["text"],
supportsTools: true,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 64_000,
}),
);
const collapsed = collapseEffortVariants(specs, DEVIN_VARIANT_COLLAPSE_TABLE);
expect(collapsed.map(model => model.id).sort()).toEqual(["claude-fable-5", "swe-1-7"]);
const fable = collapsed.find(model => model.id === "claude-fable-5");
const swe = collapsed.find(model => model.id === "swe-1-7");
if (!fable || !swe) throw new Error("Current Devin families did not collapse");
expect(resolveWireModelId(buildModel(fable), Effort.XHigh)).toBe("claude-5-fable-xhigh");
expect(resolveWireModelId(buildModel(swe), Effort.Medium)).toBe("swe-1-7-medium");
expect(resolveWireModelId(buildModel(swe), Effort.Max)).toBe("swe-1-7");
});
});
describe("Cursor Grok tier routing (issue #8803)", () => {
function cursorMemberSpec(id: string): ModelSpec<"cursor-agent"> {
return {
id,
name: id,
api: "cursor-agent",
provider: "cursor",
baseUrl: "https://api2.cursor.sh",
// Live GetUsableModels + bundled references mark these reasoning:false;
// collapse must still recognize the effort ladder.
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 64_000,
};
}
const RAW_SIBLINGS = [
"cursor-grok-4.5-high",
"cursor-grok-4.5-high-fast",
"cursor-grok-4.5-low",
"cursor-grok-4.5-low-fast",
"cursor-grok-4.5-medium",
"cursor-grok-4.5-medium-fast",
"cursor-grok-4.6-high",
"cursor-grok-4.6-high-fast",
"cursor-grok-4.6-low",
"cursor-grok-4.6-low-fast",
"cursor-grok-4.6-medium",
"cursor-grok-4.6-medium-fast",
"cursor-grok-4.6-xhigh",
"cursor-grok-4.6-xhigh-fast",
];
it("collapses the 14 effort siblings into four logical models, split by the -fast lane", () => {
const collapsed = collapseEffortVariants(RAW_SIBLINGS.map(cursorMemberSpec), CURSOR_VARIANT_COLLAPSE_TABLE);
expect(collapsed.map(model => model.id).sort()).toEqual([
"cursor-grok-4.5",
"cursor-grok-4.5-fast",
"cursor-grok-4.6",
"cursor-grok-4.6-fast",
]);
const g46 = collapsed.find(model => model.id === "cursor-grok-4.6");
if (!g46) throw new Error("cursor-grok-4.6 did not collapse");
expect(g46.name).toBe("Grok 4.6");
// Effort route forces reasoning even though every member said false.
expect(g46.reasoning).toBe(true);
expect(g46.thinking?.mode).toBe("effort");
expect(g46.thinking?.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
expect(g46.thinking?.requiresEffort).toBe(true);
});
it("routes each user effort onto the live sibling wire id per service-tier lane", () => {
const collapsed = collapseEffortVariants(RAW_SIBLINGS.map(cursorMemberSpec), CURSOR_VARIANT_COLLAPSE_TABLE);
const model = (id: string) => {
const found = collapsed.find(m => m.id === id);
if (!found) throw new Error(`${id} did not collapse`);
return buildModel(found as ModelSpec<"cursor-agent">);
};
expect(resolveWireModelId(model("cursor-grok-4.6"), Effort.XHigh)).toBe("cursor-grok-4.6-xhigh");
expect(resolveWireModelId(model("cursor-grok-4.6"), Effort.Low)).toBe("cursor-grok-4.6-low");
expect(resolveWireModelId(model("cursor-grok-4.6-fast"), Effort.High)).toBe("cursor-grok-4.6-high-fast");
expect(resolveWireModelId(model("cursor-grok-4.5"), Effort.Medium)).toBe("cursor-grok-4.5-medium");
// 4.5 has no xhigh sibling: the ceiling stays at high.
expect(model("cursor-grok-4.5").thinking?.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High]);
});
});
describe("variant aliases", () => {
it("resolves members and recycled ids per provider", () => {
expect(resolveVariantAlias("google-antigravity", "gemini-3.5-flash-low")).toBe("gemini-3.5-flash");
expect(resolveVariantAlias("google-gemini-cli", "gemini-pro-agent")).toBe("gemini-3.1-pro");
expect(resolveVariantAlias("google-antigravity", "gemini-3-flash")).toBe("gemini-3.5-flash");
expect(resolveVariantAlias("google-antigravity", "gemini-2.5-flash-thinking")).toBe("gemini-2.5-flash");
expect(resolveVariantAlias("google-antigravity", "gemini-2.5-flash-lite")).toBeUndefined();
expect(resolveVariantAlias("anthropic", "claude-sonnet-4-6-thinking")).toBeUndefined();
});
it("names the declaring providers in bare-id lookups", () => {
const hit = resolveBareVariantAlias("GEMINI-3.5-FLASH-LOW");
expect(hit?.id).toBe("gemini-3.5-flash");
expect(hit?.providers).toContain("google-antigravity");
expect(hit?.providers).toContain("google-gemini-cli");
expect(resolveBareVariantAlias("gpt-4o")).toBeUndefined();
});
it("reverse sources cover members and recycled ids", () => {
const sources = getVariantAliasSources("google-antigravity", "gemini-3.5-flash");
expect(sources).toContain("gemini-3.5-flash-extra-low");
expect(sources).toContain("gemini-3.5-flash-low");
expect(sources).toContain("gemini-3-flash");
expect(getVariantAliasSources("openai", "gpt-4o")).toEqual([]);
});
it("scopes collapsed-spec detection to routing and hand-table families", () => {
const collapsed = collapseEffortVariants(
[memberSpec("gemini-3.5-flash-low")],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
)[0];
expect(collapsed && isVariantCollapsedSpec(collapsed)).toBe(true);
expect(isVariantCollapsedSpec(memberSpec("gemini-3.5-flash-low"))).toBe(false);
// Copilot long-context variants carry requestModelId but are NOT
// collapsed specs — the generator rebake must not skip them.
expect(
isVariantCollapsedSpec(
memberSpec("claude-opus-4.7-1m", { provider: "github-copilot", requestModelId: "claude-opus-4.7" }),
),
).toBe(false);
});
});
describe("resolveWireModelId", () => {
it("survives buildModel and routes per effort with requestModelId fallback", () => {
const collapsed = collapseEffortVariants(FLASH_TRIPLET(), ANTIGRAVITY_VARIANT_COLLAPSE_TABLE)[0];
const model = buildModel(collapsed as ModelSpec<"google-gemini-cli">);
expect(model.thinking?.effortRouting).toEqual(collapsed?.thinking?.effortRouting);
expect(model.thinking?.suppressWhenOff).toBe(true);
expect(resolveWireModelId(model, Effort.High)).toBe("gemini-3-flash-agent");
expect(resolveWireModelId(model, Effort.Medium)).toBe("gemini-3.5-flash-low");
expect(resolveWireModelId(model, Effort.Minimal)).toBe("gemini-3.5-flash-extra-low");
expect(resolveWireModelId(model, undefined)).toBe("gemini-3.5-flash-extra-low");
// Dropped route (partial family) falls back to requestModelId.
const partial = collapseEffortVariants(
[memberSpec("gemini-3.5-flash-extra-low")],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
)[0];
const partialModel = buildModel(partial as ModelSpec<"google-gemini-cli">);
expect(resolveWireModelId(partialModel, Effort.High)).toBe("gemini-3.5-flash-extra-low");
// Models without routing serialize their own id.
expect(resolveWireModelId(buildModel(memberSpec("gemini-2.5-flash-lite")), Effort.High)).toBe(
"gemini-2.5-flash-lite",
);
});
});
describe("merge-point collapsing (resolveProviderModels)", () => {
async function tempDb(): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "variant-collapse-"));
return path.join(dir, "models.db");
}
it("converges stale static raw ids with collapsed dynamic results", async () => {
const dbPath = await tempDb();
const staleStatic = [
memberSpec("gemini-3.1-pro-low"),
memberSpec("gemini-3.1-pro-high"),
memberSpec("gemini-2.5-flash-lite"),
];
const liveCollapsed = collapseEffortVariants(
[memberSpec("gemini-3.1-pro-low"), memberSpec("gemini-3.1-pro-high")],
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
);
const result = await resolveProviderModels(
{
providerId: "google-antigravity",
staticModels: staleStatic,
fetchDynamicModels: () => Promise.resolve(liveCollapsed),
cacheDbPath: dbPath,
},
"online",
);
expect(result.models.map(m => m.id).sort()).toEqual(["gemini-2.5-flash-lite", "gemini-3.1-pro"]);
// The cache snapshot written above is collapsed too: a later resolve
// whose dynamic fetch fails must not resurrect raw ids.
const offline = await resolveProviderModels(
{
providerId: "google-antigravity",
staticModels: staleStatic,
fetchDynamicModels: () => Promise.resolve(null),
cacheDbPath: dbPath,
},
"online",
);
expect(offline.models.filter(m => m.id.includes("gemini-3.1-pro")).map(m => m.id)).toEqual(["gemini-3.1-pro"]);
});
it("collapses X/X-thinking pairs for providers without a hand table", async () => {
const dbPath = await tempDb();
const result = await resolveProviderModels(
{
providerId: "venice",
staticModels: [
pairSpec("kimi-k2"),
pairSpec("kimi-k2-thinking", { reasoning: true, thinking: PAIR_THINKING }),
],
cacheDbPath: dbPath,
},
"offline",
);
expect(result.models.map(m => m.id)).toEqual(["kimi-k2"]);
const model = result.models[0];
expect(model?.reasoning).toBe(true);
expect(model && resolveWireModelId(model, Effort.High)).toBe("kimi-k2-thinking");
expect(model && resolveWireModelId(model, undefined)).toBe("kimi-k2");
});
});
describe("antigravity discovery collapsing", () => {
const payload = {
models: {
"gemini-3.5-flash-extra-low": {
displayName: "Gemini 3.5 Flash (Extra Low)",
supportsThinking: true,
supportsImages: true,
maxTokens: 1_048_576,
maxOutputTokens: 65_536,
},
"gemini-3.5-flash-low": {
displayName: "Gemini 3.5 Flash (Low)",
supportsThinking: true,
supportsImages: true,
maxTokens: 1_048_576,
maxOutputTokens: 65_536,
},
"gemini-3-flash-agent": {
displayName: "Gemini 3 Flash Agent",
supportsThinking: true,
supportsImages: true,
thinkingBudget: 10_000,
},
"gemini-3.7-flash-low": {
displayName: "Gemini 3.7 Flash Low",
supportsThinking: true,
supportsImages: true,
maxTokens: 1_048_576,
maxOutputTokens: 65_536,
},
"gemini-3.7-flash-medium": {
displayName: "Gemini 3.7 Flash Medium",
supportsThinking: true,
supportsImages: true,
maxTokens: 1_048_576,
maxOutputTokens: 65_536,
},
"gemini-3.7-flash-high": {
displayName: "Gemini 3.7 Flash High",
supportsThinking: true,
supportsImages: true,
maxTokens: 1_048_576,
maxOutputTokens: 65_536,
},
"claude-sonnet-4-6": { displayName: "Claude Sonnet 4.6", supportsThinking: true, supportsImages: true },
"claude-sonnet-4-6-thinking": {
displayName: "Claude Sonnet 4.6 Thinking",
supportsThinking: true,
supportsImages: true,
},
"gemini-2.5-flash": { displayName: "Gemini 2.5 Flash", supportsThinking: true, supportsImages: true },
"gemini-2.5-flash-thinking": { displayName: "Gemini 2.5 Flash Thinking", supportsThinking: true },
chat_20706: { displayName: "Chat Internal" },
"internal-model": { displayName: "Internal", isInternal: true },
},
};
const fetcher = Object.assign(
(_input: Parameters<typeof fetch>[0], _init?: Parameters<typeof fetch>[1]) =>
Promise.resolve(new Response(JSON.stringify(payload), { status: 200 })),
{ preconnect: fetch.preconnect },
);
it("returns collapsed logical entries and keeps the denylist", async () => {
const models = await fetchAntigravityDiscoveryModels({ token: "t", endpoint: "https://cca.test", fetcher });
expect(models?.map(m => m.id).sort()).toEqual([
"claude-sonnet-4-6",
"gemini-2.5-flash",
"gemini-3.5-flash",
"gemini-3.7-flash",
]);
const flash = models?.find(m => m.id === "gemini-3.5-flash");
expect(flash?.requestModelId).toBe("gemini-3.5-flash-extra-low");
expect(flash?.thinking?.effortRouting?.[Effort.High]).toBe("gemini-3-flash-agent");
expect(flash?.thinking?.effortRouting?.[Effort.Medium]).toBe("gemini-3.5-flash-low");
expect(flash?.thinking?.suppressWhenOff).toBe(true);
// The 2.5 pair collapses instead of denylisting the -thinking twin.
const flash25 = models?.find(m => m.id === "gemini-2.5-flash");
expect(flash25?.thinking?.effortRouting?.[Effort.High]).toBe("gemini-2.5-flash-thinking");
expect(flash25?.thinking?.effortRouting?.off).toBe("gemini-2.5-flash");
const flash37 = models?.find(m => m.id === "gemini-3.7-flash");
expect(flash37?.requestModelId).toBe("gemini-3.7-flash-low");
expect(flash37?.thinking).toEqual({
mode: "google-level",
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
requiresEffort: true,
effortRouting: {
minimal: "gemini-3.7-flash-low",
low: "gemini-3.7-flash-low",
medium: "gemini-3.7-flash-medium",
high: "gemini-3.7-flash-high",
},
});
});
it("keeps collapsed routing through the gemini-cli re-provision", async () => {
const options = googleGeminiCliModelManagerOptions({
oauthToken: "t",
endpoint: "https://cca.test",
fetch: fetcher,
});
const models = await options.fetchDynamicModels?.();
const flash = models?.find(m => m.id === "gemini-3.5-flash");
expect(flash?.provider).toBe("google-gemini-cli");
expect(flash?.baseUrl).toBe("https://cca.test");
expect(flash?.requestModelId).toBe("gemini-3.5-flash-extra-low");
expect(flash?.thinking?.effortRouting?.off).toBe("gemini-3.5-flash-extra-low");
const flash37 = models?.find(m => m.id === "gemini-3.7-flash");
expect(flash37?.requestModelId).toBe("gemini-3.7-flash-low");
expect(flash37?.thinking?.requiresEffort).toBe(true);
expect(flash37?.thinking?.effortRouting?.[Effort.High]).toBe("gemini-3.7-flash-high");
});
it("uses the primary daily endpoint by default", async () => {
const requestedUrls: string[] = [];
const defaultFetcher = Object.assign(
(input: string | URL | Request, _init?: RequestInit) => {
requestedUrls.push(String(input));
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
},
{ preconnect: fetch.preconnect },
);
const models = await fetchAntigravityDiscoveryModels({
token: "t",
fetcher: defaultFetcher,
});
const discoveryUrl = requestedUrls.find(url => url.includes("/v1internal:fetchAvailableModels"));
expect(discoveryUrl).toBeDefined();
expect(discoveryUrl).toContain(ANTIGRAVITY_PRIMARY_ENDPOINT);
expect(models?.[0]?.baseUrl).toBe(ANTIGRAVITY_PRIMARY_ENDPOINT);
});
});
describe("Devin GLM-5.2 collapse", () => {
function devinMemberSpec(id: string, overrides: Partial<ModelSpec<"devin-agent">> = {}): ModelSpec<"devin-agent"> {
return {
id,
name: id,
api: "devin-agent",
provider: "devin",
baseUrl: "https://server.codeium.com",
reasoning: true,
input: ["text"],
supportsTools: true,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 64_000,
...overrides,
};
}
it("collapses the three 200K GLM-5.2 variants into one logical entry routing all efforts to the free glm-5-2 wire UID", () => {
const out = collapseEffortVariants(
[
devinMemberSpec("glm-5-2"),
devinMemberSpec("glm-5-2-max"),
devinMemberSpec("glm-5-2-none", { reasoning: false }),
],
DEVIN_VARIANT_COLLAPSE_TABLE,
);
expect(out).toHaveLength(1);
const spec = out[0];
expect(spec?.id).toBe("glm-5-2");
expect(spec?.thinking?.effortRouting).toEqual({
high: "glm-5-2",
xhigh: "glm-5-2",
});
});
it("routes every effort to glm-5-2 (never to the quota-gated glm-5-2-max or glm-5-2-none)", () => {
const out = collapseEffortVariants(
[devinMemberSpec("glm-5-2"), devinMemberSpec("glm-5-2-max")],
DEVIN_VARIANT_COLLAPSE_TABLE,
);
const spec = out[0];
const routing = spec?.thinking?.effortRouting ?? {};
for (const wire of Object.values(routing)) {
expect(wire).toBe("glm-5-2");
}
});
it("collapses the three 1M GLM-5.2 variants into one paid entry with proper effort routing", () => {
const out = collapseEffortVariants(
[
devinMemberSpec("glm-5-2-1m", { contextWindow: 1_000_000 }),
devinMemberSpec("glm-5-2-max-1m", { contextWindow: 1_000_000 }),
devinMemberSpec("glm-5-2-none-1m", { contextWindow: 1_000_000, reasoning: false }),
],
DEVIN_VARIANT_COLLAPSE_TABLE,
);
expect(out).toHaveLength(1);
const spec = out[0];
expect(spec?.id).toBe("glm-5-2-1m");
expect(spec?.contextWindow).toBe(1_000_000);
expect(spec?.thinking?.effortRouting).toEqual({
off: "glm-5-2-none-1m",
high: "glm-5-2-1m",
xhigh: "glm-5-2-max-1m",
});
});
});