Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit c5f360f

Browse files
refactor(shared): extract task contracts and model policy
Generated-By: PostHog Code Task-Id: c1bbe3cf-742b-4b24-bf96-d11a18b4cf22
1 parent 0d728bc commit c5f360f

18 files changed

Lines changed: 1025 additions & 742 deletions
Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,6 @@
1-
import type { Adapter } from "@posthog/shared";
2-
import { getEffortOptions as getClaudeEffortOptions } from "./claude/session/models";
3-
import { getReasoningEffortOptions as getCodexReasoningEffortOptions } from "./codex-app-server/models";
4-
5-
export type SupportedReasoningEffort =
6-
| "low"
7-
| "medium"
8-
| "high"
9-
| "xhigh"
10-
| "max";
11-
12-
export interface ReasoningEffortOption {
13-
value: SupportedReasoningEffort;
14-
name: string;
15-
}
16-
17-
export function getReasoningEffortOptions(
18-
adapter: Adapter,
19-
modelId: string,
20-
): ReasoningEffortOption[] | null {
21-
const options =
22-
adapter === "codex"
23-
? getCodexReasoningEffortOptions(modelId)
24-
: getClaudeEffortOptions(modelId);
25-
26-
return options as ReasoningEffortOption[] | null;
27-
}
28-
29-
export function isSupportedReasoningEffort(
30-
adapter: Adapter,
31-
modelId: string,
32-
value: string,
33-
): value is SupportedReasoningEffort {
34-
return (
35-
getReasoningEffortOptions(adapter, modelId)?.some(
36-
(option) => option.value === value,
37-
) ?? false
38-
);
39-
}
1+
export {
2+
getReasoningEffortOptions,
3+
isSupportedReasoningEffort,
4+
type ReasoningEffortOption,
5+
type SupportedReasoningEffort,
6+
} from "@posthog/shared";
Lines changed: 1 addition & 224 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,5 @@
11
import { afterEach, describe, expect, it, vi } from "vitest";
2-
import {
3-
compareModelsForPicker,
4-
fetchGatewayModels,
5-
fetchModelsList,
6-
formatGatewayModelName,
7-
type GatewayModel,
8-
getClaudeModelRecency,
9-
isAnthropicModel,
10-
isBlockedModelId,
11-
isCloudflareModel,
12-
pickAllowedModel,
13-
} from "./gateway-models";
14-
15-
const model = (id: string, owned_by = ""): GatewayModel => ({
16-
id,
17-
owned_by,
18-
context_window: 128000,
19-
supports_streaming: true,
20-
supports_vision: false,
21-
allowed: true,
22-
});
23-
24-
describe("formatGatewayModelName", () => {
25-
it("keeps Claude models in friendly title case", () => {
26-
expect(
27-
formatGatewayModelName({
28-
id: "claude-opus-4-8",
29-
owned_by: "anthropic",
30-
context_window: 200000,
31-
supports_streaming: true,
32-
supports_vision: true,
33-
allowed: true,
34-
}),
35-
).toBe("Claude Opus 4.8");
36-
});
37-
38-
it("uppercases the GPT acronym in OpenAI model ids", () => {
39-
expect(
40-
formatGatewayModelName({
41-
id: "GPT-5.5",
42-
owned_by: "openai",
43-
context_window: 200000,
44-
supports_streaming: true,
45-
supports_vision: true,
46-
allowed: true,
47-
}),
48-
).toBe("GPT-5.5");
49-
});
50-
51-
it("strips the openai/ prefix, uppercases GPT, and title-cases the suffix", () => {
52-
expect(
53-
formatGatewayModelName({
54-
id: "openai/gpt-5.6-sol",
55-
owned_by: "openai",
56-
context_window: 200000,
57-
supports_streaming: true,
58-
supports_vision: true,
59-
allowed: true,
60-
}),
61-
).toBe("GPT-5.6 Sol");
62-
});
63-
64-
it("formats Cloudflare models as the final path segment with GLM uppercased", () => {
65-
expect(
66-
formatGatewayModelName({
67-
id: "@cf/zai-org/glm-5.2",
68-
owned_by: "cloudflare",
69-
context_window: 128000,
70-
supports_streaming: true,
71-
supports_vision: false,
72-
allowed: true,
73-
}),
74-
).toBe("GLM-5.2");
75-
});
76-
77-
it("leaves non-acronym Cloudflare models lowercase", () => {
78-
expect(
79-
formatGatewayModelName({
80-
id: "@cf/meta/llama-3.1-8b-instruct",
81-
owned_by: "cloudflare",
82-
context_window: 128000,
83-
supports_streaming: true,
84-
supports_vision: false,
85-
allowed: true,
86-
}),
87-
).toBe("llama-3.1-8b-instruct");
88-
});
89-
90-
it("blocks deprecated Claude gateway models", () => {
91-
expect(isBlockedModelId("claude-opus-4-5")).toBe(true);
92-
expect(isBlockedModelId("claude-opus-4-6")).toBe(true);
93-
expect(isBlockedModelId("claude-sonnet-4-5")).toBe(true);
94-
expect(isBlockedModelId("claude-haiku-4-5")).toBe(true);
95-
expect(isBlockedModelId("ANTHROPIC/CLAUDE-HAIKU-4-5")).toBe(true);
96-
});
97-
98-
it("blocks deprecated Codex gateway models", () => {
99-
expect(isBlockedModelId("gpt-5.2")).toBe(true);
100-
expect(isBlockedModelId("gpt-5.3")).toBe(true);
101-
expect(isBlockedModelId("gpt-5.3-codex")).toBe(true);
102-
expect(isBlockedModelId("openai/gpt-5.2")).toBe(true);
103-
expect(isBlockedModelId("OPENAI/GPT-5.3")).toBe(true);
104-
expect(isBlockedModelId("OPENAI/GPT-5.3-CODEX")).toBe(true);
105-
});
106-
});
107-
108-
describe("getClaudeModelRecency", () => {
109-
it.each([
110-
["claude-haiku-4-5", 4005],
111-
["claude-sonnet-4-6", 4006],
112-
["claude-opus-4-7", 4007],
113-
["claude-opus-4-8", 4008],
114-
["claude-sonnet-5", 5000],
115-
["claude-fable-5", 5000],
116-
])("ranks %s by its embedded version (%i)", (modelId, rank) => {
117-
expect(getClaudeModelRecency(modelId)).toBe(rank);
118-
});
119-
120-
it("ignores a trailing date suffix when reading the version", () => {
121-
expect(getClaudeModelRecency("claude-haiku-4-5-20251001")).toBe(4005);
122-
});
123-
124-
it("ranks a model with no recognisable version as newest", () => {
125-
expect(getClaudeModelRecency("claude-mystery")).toBe(
126-
Number.MAX_SAFE_INTEGER,
127-
);
128-
expect(getClaudeModelRecency("claude-mystery")).toBeGreaterThan(
129-
getClaudeModelRecency("claude-fable-5"),
130-
);
131-
});
132-
});
133-
134-
describe("compareModelsForPicker", () => {
135-
it("groups models by family least capable first, newest version first", () => {
136-
// The picker opens upward, so least-capable-first DOM order puts the most
137-
// capable family (Fable) nearest the trigger — the visual top of the menu.
138-
// Models as the gateway might return them — arbitrary order.
139-
const gatewayOrder = [
140-
"claude-fable-5",
141-
"claude-opus-4-7",
142-
"claude-mystery",
143-
"claude-sonnet-5",
144-
"claude-haiku-4-5",
145-
"claude-sonnet-4-6",
146-
"claude-opus-4-8",
147-
];
148-
const displayed = [...gatewayOrder].sort(compareModelsForPicker);
149-
expect(displayed).toEqual([
150-
"claude-haiku-4-5",
151-
"claude-sonnet-5",
152-
"claude-sonnet-4-6",
153-
"claude-opus-4-8",
154-
"claude-opus-4-7",
155-
"claude-fable-5",
156-
"claude-mystery",
157-
]);
158-
});
159-
});
2+
import { fetchGatewayModels, fetchModelsList } from "./gateway-models";
1603

1614
describe("gateway model fetch timeout", () => {
1625
afterEach(() => {
@@ -268,69 +111,3 @@ describe("gateway models cache", () => {
268111
expect(models[0]?.context_window).toBe(1_000_000);
269112
});
270113
});
271-
272-
describe("isCloudflareModel", () => {
273-
it.each([
274-
{ id: "@cf/zai-org/glm-5.2", owned_by: "cloudflare", expected: true },
275-
{ id: "claude-opus-4-8", owned_by: "anthropic", expected: false },
276-
{ id: "@cf/zai-org/glm-5.2", owned_by: "", expected: true },
277-
{ id: "gpt-5.5", owned_by: "", expected: false },
278-
// A Cloudflare-served model can report an upstream owner; the `@cf/` prefix still wins.
279-
{ id: "@cf/openai/gpt-oss", owned_by: "openai", expected: true },
280-
])(
281-
"isCloudflareModel($id, owned_by=$owned_by) → $expected",
282-
({ id, owned_by, expected }) => {
283-
expect(isCloudflareModel(model(id, owned_by))).toBe(expected);
284-
},
285-
);
286-
287-
it("does not classify Cloudflare models as Anthropic", () => {
288-
// The Claude adapter accepts both, but they must stay distinguishable.
289-
const glm = model("@cf/zai-org/glm-5.2", "cloudflare");
290-
expect(isCloudflareModel(glm)).toBe(true);
291-
expect(isAnthropicModel(glm)).toBe(false);
292-
});
293-
});
294-
295-
describe("pickAllowedModel", () => {
296-
const entry = (id: string, allowed: boolean) => ({ id, allowed });
297-
298-
it.each([
299-
[
300-
"keeps an allowed preferred model",
301-
[entry("claude-opus-4-8", true)],
302-
"claude-opus-4-8",
303-
"claude-opus-4-8",
304-
],
305-
[
306-
"keeps a preferred model absent from the list",
307-
[entry("claude-opus-4-8", true)],
308-
"claude-sonnet-5",
309-
"claude-sonnet-5",
310-
],
311-
[
312-
"moves a restricted preferred model to the newest allowed one",
313-
[
314-
entry("claude-opus-4-8", false),
315-
entry("claude-sonnet-4-6", true),
316-
entry("@cf/zai-org/glm-5.2", true),
317-
],
318-
"claude-opus-4-8",
319-
"@cf/zai-org/glm-5.2",
320-
],
321-
[
322-
"keeps the preferred model when everything is restricted",
323-
[entry("claude-opus-4-8", false)],
324-
"claude-opus-4-8",
325-
"claude-opus-4-8",
326-
],
327-
[
328-
"keeps the preferred model when the list is empty",
329-
[],
330-
"claude-opus-4-8",
331-
"claude-opus-4-8",
332-
],
333-
] as const)("%s", (_name, models, preferred, expected) => {
334-
expect(pickAllowedModel(models, preferred)).toBe(expected);
335-
});
336-
});

0 commit comments

Comments
 (0)