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
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ describe("applyAvailableModelsAllowlist", () => {
"claude-opus-4-8",
]).options,
).toEqual([
{ value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ value: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
]);
});

Expand Down
45 changes: 6 additions & 39 deletions packages/agent/src/adapters/reasoning-effort.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,6 @@
import type { Adapter } from "@posthog/shared";
import { getEffortOptions as getClaudeEffortOptions } from "./claude/session/models";
import { getReasoningEffortOptions as getCodexReasoningEffortOptions } from "./codex-app-server/models";

export type SupportedReasoningEffort =
| "low"
| "medium"
| "high"
| "xhigh"
| "max";

export interface ReasoningEffortOption {
value: SupportedReasoningEffort;
name: string;
}

export function getReasoningEffortOptions(
adapter: Adapter,
modelId: string,
): ReasoningEffortOption[] | null {
const options =
adapter === "codex"
? getCodexReasoningEffortOptions(modelId)
: getClaudeEffortOptions(modelId);

return options as ReasoningEffortOption[] | null;
}

export function isSupportedReasoningEffort(
adapter: Adapter,
modelId: string,
value: string,
): value is SupportedReasoningEffort {
return (
getReasoningEffortOptions(adapter, modelId)?.some(
(option) => option.value === value,
) ?? false
);
}
export {
getReasoningEffortOptions,
isSupportedReasoningEffort,
type ReasoningEffortOption,
type SupportedReasoningEffort,
} from "@posthog/shared";
261 changes: 1 addition & 260 deletions packages/agent/src/gateway-models.test.ts
Original file line number Diff line number Diff line change
@@ -1,172 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
compareModelsForPicker,
fetchGatewayModels,
fetchModelsList,
formatGatewayModelName,
type GatewayModel,
getClaudeModelRecency,
isAnthropicModel,
isBlockedModelId,
isCloudflareModel,
isModalModel,
isModalModelId,
pickAllowedModel,
} from "./gateway-models";

const model = (id: string, owned_by = ""): GatewayModel => ({
id,
owned_by,
context_window: 128000,
supports_streaming: true,
supports_vision: false,
allowed: true,
});

describe("formatGatewayModelName", () => {
it("keeps Claude models in friendly title case", () => {
expect(
formatGatewayModelName({
id: "claude-opus-4-8",
owned_by: "anthropic",
context_window: 200000,
supports_streaming: true,
supports_vision: true,
allowed: true,
}),
).toBe("Claude Opus 4.8");
});

it("uppercases the GPT acronym in OpenAI model ids", () => {
expect(
formatGatewayModelName({
id: "GPT-5.5",
owned_by: "openai",
context_window: 200000,
supports_streaming: true,
supports_vision: true,
allowed: true,
}),
).toBe("GPT-5.5");
});

it("strips the openai/ prefix, uppercases GPT, and title-cases the suffix", () => {
expect(
formatGatewayModelName({
id: "openai/gpt-5.6-sol",
owned_by: "openai",
context_window: 200000,
supports_streaming: true,
supports_vision: true,
allowed: true,
}),
).toBe("GPT-5.6 Sol");
});

it("formats Cloudflare models as the final path segment with GLM uppercased", () => {
expect(
formatGatewayModelName({
id: "@cf/zai-org/glm-5.2",
owned_by: "cloudflare",
context_window: 128000,
supports_streaming: true,
supports_vision: false,
allowed: true,
}),
).toBe("GLM-5.2");
});

it("leaves non-acronym Cloudflare models lowercase", () => {
expect(
formatGatewayModelName({
id: "@cf/meta/llama-3.1-8b-instruct",
owned_by: "cloudflare",
context_window: 128000,
supports_streaming: true,
supports_vision: false,
allowed: true,
}),
).toBe("llama-3.1-8b-instruct");
});

it("formats Kimi K3 for the model picker", () => {
const kimi = model("moonshotai/kimi-k3", "modal");
expect(formatGatewayModelName(kimi)).toBe("Kimi K3");
expect(isModalModel(kimi)).toBe(true);
expect(isModalModelId(kimi.id)).toBe(true);
});

it("blocks deprecated Claude gateway models", () => {
expect(isBlockedModelId("claude-opus-4-5")).toBe(true);
expect(isBlockedModelId("claude-opus-4-6")).toBe(true);
expect(isBlockedModelId("claude-sonnet-4-5")).toBe(true);
expect(isBlockedModelId("claude-haiku-4-5")).toBe(true);
expect(isBlockedModelId("ANTHROPIC/CLAUDE-HAIKU-4-5")).toBe(true);
});

it("blocks deprecated Codex gateway models", () => {
expect(isBlockedModelId("gpt-5.2")).toBe(true);
expect(isBlockedModelId("gpt-5.3")).toBe(true);
expect(isBlockedModelId("gpt-5.3-codex")).toBe(true);
expect(isBlockedModelId("openai/gpt-5.2")).toBe(true);
expect(isBlockedModelId("OPENAI/GPT-5.3")).toBe(true);
expect(isBlockedModelId("OPENAI/GPT-5.3-CODEX")).toBe(true);
});
});

describe("getClaudeModelRecency", () => {
it.each([
["claude-haiku-4-5", 4005],
["claude-sonnet-4-6", 4006],
["claude-opus-4-7", 4007],
["claude-opus-4-8", 4008],
["claude-opus-5", 5000],
["claude-sonnet-5", 5000],
["claude-fable-5", 5000],
])("ranks %s by its embedded version (%i)", (modelId, rank) => {
expect(getClaudeModelRecency(modelId)).toBe(rank);
});

it("ignores a trailing date suffix when reading the version", () => {
expect(getClaudeModelRecency("claude-haiku-4-5-20251001")).toBe(4005);
});

it("ranks a model with no recognisable version as newest", () => {
expect(getClaudeModelRecency("claude-mystery")).toBe(
Number.MAX_SAFE_INTEGER,
);
expect(getClaudeModelRecency("claude-mystery")).toBeGreaterThan(
getClaudeModelRecency("claude-fable-5"),
);
});
});

describe("compareModelsForPicker", () => {
it("groups models by family least capable first, newest version first", () => {
// The picker opens upward, so least-capable-first DOM order puts the most
// capable family (Fable) nearest the trigger — the visual top of the menu.
// Models as the gateway might return them — arbitrary order.
const gatewayOrder = [
"claude-fable-5",
"claude-opus-4-7",
"claude-mystery",
"claude-sonnet-5",
"claude-haiku-4-5",
"claude-sonnet-4-6",
"claude-opus-4-8",
];
const displayed = [...gatewayOrder].sort(compareModelsForPicker);
expect(displayed).toEqual([
"claude-haiku-4-5",
"claude-sonnet-5",
"claude-sonnet-4-6",
"claude-opus-4-8",
"claude-opus-4-7",
"claude-fable-5",
"claude-mystery",
]);
});
});
import { fetchGatewayModels, fetchModelsList } from "./gateway-models";

describe("gateway model fetch timeout", () => {
afterEach(() => {
Expand Down Expand Up @@ -251,96 +84,4 @@ describe("gateway models cache", () => {
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(cached[0]?.allowed).toBe(false);
});

it("corrects stale GLM 5.2 context-window metadata", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
JSON.stringify({
object: "list",
data: [
{
id: "@cf/zai-org/glm-5.2",
owned_by: "cloudflare",
context_window: 128_000,
supports_streaming: true,
supports_vision: false,
},
],
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
);

const models = await fetchGatewayModels({
gatewayUrl: "https://gateway.glm-context-test",
});

expect(models[0]?.context_window).toBe(1_000_000);
});
});

describe("isCloudflareModel", () => {
it.each([
{ id: "@cf/zai-org/glm-5.2", owned_by: "cloudflare", expected: true },
{ id: "claude-opus-4-8", owned_by: "anthropic", expected: false },
{ id: "@cf/zai-org/glm-5.2", owned_by: "", expected: true },
{ id: "gpt-5.5", owned_by: "", expected: false },
// A Cloudflare-served model can report an upstream owner; the `@cf/` prefix still wins.
{ id: "@cf/openai/gpt-oss", owned_by: "openai", expected: true },
])(
"isCloudflareModel($id, owned_by=$owned_by) → $expected",
({ id, owned_by, expected }) => {
expect(isCloudflareModel(model(id, owned_by))).toBe(expected);
},
);

it("does not classify Cloudflare models as Anthropic", () => {
// The Claude adapter accepts both, but they must stay distinguishable.
const glm = model("@cf/zai-org/glm-5.2", "cloudflare");
expect(isCloudflareModel(glm)).toBe(true);
expect(isAnthropicModel(glm)).toBe(false);
});
});

describe("pickAllowedModel", () => {
const entry = (id: string, allowed: boolean) => ({ id, allowed });

it.each([
[
"keeps an allowed preferred model",
[entry("claude-opus-4-8", true)],
"claude-opus-4-8",
"claude-opus-4-8",
],
[
"keeps a preferred model absent from the list",
[entry("claude-opus-4-8", true)],
"claude-sonnet-5",
"claude-sonnet-5",
],
[
"moves a restricted preferred model to the newest allowed one",
[
entry("claude-opus-4-8", false),
entry("claude-sonnet-4-6", true),
entry("@cf/zai-org/glm-5.2", true),
],
"claude-opus-4-8",
"@cf/zai-org/glm-5.2",
],
[
"keeps the preferred model when everything is restricted",
[entry("claude-opus-4-8", false)],
"claude-opus-4-8",
"claude-opus-4-8",
],
[
"keeps the preferred model when the list is empty",
[],
"claude-opus-4-8",
"claude-opus-4-8",
],
] as const)("%s", (_name, models, preferred, expected) => {
expect(pickAllowedModel(models, preferred)).toBe(expected);
});
});
Loading
Loading