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
12 changes: 10 additions & 2 deletions packages/agent/src/adapters/base-acp-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
isAnthropicModel,
isCloudflareModel,
isCloudflareModelId,
isModalModel,
isModalModelId,
pickAllowedModel,
} from "../gateway-models";
import { Logger } from "../utils/logger";
Expand Down Expand Up @@ -152,7 +154,12 @@ export abstract class BaseAcpAgent implements Agent {
const adapterModels = this.gatewayModels
// Cloudflare models are servable on the Claude adapter too — the gateway translates the
// `@cf/` path onto its Anthropic-Messages surface — so include them alongside Anthropic models.
.filter((model) => isAnthropicModel(model) || isCloudflareModel(model));
.filter(
(model) =>
isAnthropicModel(model) ||
isCloudflareModel(model) ||
isModalModel(model),
Comment thread
tatoalo marked this conversation as resolved.
);

const options = adapterModels
.map((model) => ({
Expand All @@ -171,7 +178,8 @@ export abstract class BaseAcpAgent implements Agent {
const isClaudeAdapterModelId = (modelId: string): boolean =>
modelId.startsWith("claude-") ||
modelId.startsWith("anthropic/") ||
isCloudflareModelId(modelId);
isCloudflareModelId(modelId) ||
isModalModelId(modelId);

let currentModelId = currentModelOverride ?? DEFAULT_GATEWAY_MODEL;

Expand Down
9 changes: 9 additions & 0 deletions packages/agent/src/gateway-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
isAnthropicModel,
isBlockedModelId,
isCloudflareModel,
isModalModel,
isModalModelId,
pickAllowedModel,
} from "./gateway-models";

Expand Down Expand Up @@ -87,6 +89,13 @@ describe("formatGatewayModelName", () => {
).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);
Expand Down
12 changes: 12 additions & 0 deletions packages/agent/src/gateway-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ export function isCloudflareModel(model: GatewayModel): boolean {
return isCloudflareModelId(model.id) || model.owned_by === "cloudflare";
}

export function isModalModel(model: GatewayModel): boolean {
return isModalModelId(model.id) || model.owned_by === "modal";
}

export function isModalModelId(modelId: string): boolean {
return modelId === "moonshotai/kimi-k3";
}

export interface ModelInfo {
id: string;
owned_by?: string;
Expand Down Expand Up @@ -322,6 +330,10 @@ export function formatGatewayModelName(model: GatewayModel): string {
return formatProviderModelName(model.id.split("/").pop() ?? model.id);
}

if (isModalModel(model)) {
return formatModelId(model.id.split("/").pop() ?? model.id);
}

if (isOpenAIModel(model)) {
return formatProviderModelName(stripProviderPrefix(model.id));
}
Expand Down
6 changes: 6 additions & 0 deletions packages/harness/src/extensions/posthog-provider/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ const FALLBACK_GATEWAY_MODELS: GatewayModel[] = [
context_window: 128000,
supports_vision: false,
},
{
id: "moonshotai/kimi-k3",
owned_by: "modal",
context_window: 262144,
supports_vision: false,
},
];

export function fallbackModelConfigs(
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const CHANNELS_LAYOUT_FLAG = "code-spaces-layout";
export const LOOPS_FLAG = "loops";
export const TASKS_PREWARM_SANDBOX_FLAG = "tasks-prewarm-sandbox";
export const GLM_MODEL_FLAG = "posthog-code-glm-model";
export const KIMI_MODEL_FLAG = "tasks-kimi-k3";
/** Spoken narration (agent speaks via the `speak` tool). Gated for a staged rollout. */
export const SPOKEN_NARRATION_FLAG = "posthog-code-spoken-narration";
// Gates importing and relaying local MCP servers into cloud task runs.
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/features/loops/components/LoopModelFields.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { LoopSchemas } from "@posthog/api-client/loops";
import { GLM_MODEL_FLAG } from "@posthog/shared";
import { GLM_MODEL_FLAG, KIMI_MODEL_FLAG } from "@posthog/shared";
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
import { SettingsOptionSelect } from "@posthog/ui/features/settings/SettingsOptionSelect";
import { Flex } from "@radix-ui/themes";
Expand Down Expand Up @@ -55,17 +55,19 @@ export function LoopModelFields({
disabled,
}: LoopModelFieldsProps) {
const glmEnabled = useFeatureFlag(GLM_MODEL_FLAG);
const kimiEnabled = useFeatureFlag(KIMI_MODEL_FLAG);
const configOptions = useLoopModelConfigOptions(adapter);

const modelOptions = useMemo(
() => [
{ value: DEFAULT_MODEL_VALUE, label: "Default (recommended)" },
...loopModelOptions(adapter, configOptions, {
glmEnabled,
kimiEnabled,
pinnedModel: model,
}),
],
[adapter, configOptions, glmEnabled, model],
[adapter, configOptions, glmEnabled, kimiEnabled, model],
);

const reasoningOptions = useMemo(
Expand Down
30 changes: 23 additions & 7 deletions packages/ui/src/features/loops/loopModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ function isGlmModelId(modelId: string): boolean {
return modelId.toLowerCase().includes("glm");
}

function isKimiModelId(modelId: string): boolean {
return modelId === "moonshotai/kimi-k3";
}

// Served-catalog stand-in while the preview config loads or when the request
// fails, so the picker never collapses to "Default" alone. Matches the
// backend's per-adapter catalogs in process_task/utils.py minus client-blocked
Expand All @@ -40,6 +44,7 @@ const FALLBACK_MODEL_OPTIONS: Record<
{ value: "claude-sonnet-5", label: "Claude Sonnet 5" },
{ value: "claude-fable-5", label: "Claude Fable 5" },
{ value: "@cf/zai-org/glm-5.2", label: "GLM-5.2" },
{ value: "moonshotai/kimi-k3", label: "Kimi K3" },
],
codex: [
{ value: "gpt-5", label: "GPT-5" },
Expand Down Expand Up @@ -70,7 +75,11 @@ export function formatLoopModel(
export function loopModelOptions(
adapter: LoopSchemas.LoopRuntimeAdapterEnum,
configOptions: SessionConfigOption[],
{ glmEnabled, pinnedModel }: { glmEnabled: boolean; pinnedModel: string },
{
glmEnabled,
kimiEnabled,
pinnedModel,
}: { glmEnabled: boolean; kimiEnabled?: boolean; pinnedModel: string },
): LoopModelOption[] {
const modelOption = configOptions.find(
(option) => option.category === "model" || option.id === "model",
Expand All @@ -84,12 +93,19 @@ export function loopModelOptions(
label: option.name ?? option.value,
}))
: [];
const options = (
served.length > 0 ? served : FALLBACK_MODEL_OPTIONS[adapter]
).filter(
(option) =>
glmEnabled || option.value === pinnedModel || !isGlmModelId(option.value),
);
const options = (served.length > 0 ? served : FALLBACK_MODEL_OPTIONS[adapter])
.filter(
(option) =>
glmEnabled ||
option.value === pinnedModel ||
!isGlmModelId(option.value),
)
.filter(
(option) =>
kimiEnabled ||
option.value === pinnedModel ||
!isKimiModelId(option.value),
);
if (pinnedModel && !options.some((option) => option.value === pinnedModel)) {
options.push({ value: pinnedModel, label: pinnedModel });
}
Expand Down
29 changes: 22 additions & 7 deletions packages/ui/src/features/sessions/components/ModelSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { SessionConfigSelectGroup } from "@agentclientprotocol/sdk";
import type {
SessionConfigOption,
SessionConfigSelectGroup,
} from "@agentclientprotocol/sdk";
import { CaretDown } from "@phosphor-icons/react";
import type { SessionService } from "@posthog/core/sessions/sessionService";
import { SESSION_SERVICE } from "@posthog/core/sessions/sessionService";
Expand All @@ -12,11 +15,14 @@ import {
DropdownMenuTrigger,
MenuLabel,
} from "@posthog/quill";
import { type Adapter, GLM_MODEL_FLAG } from "@posthog/shared";
import { type Adapter, GLM_MODEL_FLAG, KIMI_MODEL_FLAG } from "@posthog/shared";
import { gateRestrictedModelPick } from "@posthog/ui/features/billing/modelGate";
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
import { ModelRadioItem } from "@posthog/ui/features/sessions/components/ModelRadioItem";
import { stripGlmModelOption } from "@posthog/ui/features/sessions/modelOptionFilters";
import {
stripGlmModelOption,
stripKimiModelOption,
} from "@posthog/ui/features/sessions/modelOptionFilters";
import {
flattenSelectOptions,
useModelConfigOptionForTask,
Expand Down Expand Up @@ -44,10 +50,10 @@ export function ModelSelector({
const sessionIsCloud = useSessionIsCloud(taskId);
const rawModelOption = useModelConfigOptionForTask(taskId);
const glmEnabled = useFeatureFlag(GLM_MODEL_FLAG);
const modelOption =
glmEnabled || !rawModelOption
? rawModelOption
: stripGlmModelOption(rawModelOption);
const kimiEnabled = useFeatureFlag(KIMI_MODEL_FLAG);
const modelOption = rawModelOption
? stripDisabledPreviewModels(rawModelOption, glmEnabled, kimiEnabled)
: rawModelOption;

const selectOption = modelOption?.type === "select" ? modelOption : undefined;
const options = selectOption
Expand Down Expand Up @@ -133,3 +139,12 @@ export function ModelSelector({
</DropdownMenu>
);
}

function stripDisabledPreviewModels(
option: SessionConfigOption,
glmEnabled: boolean,
kimiEnabled: boolean,
): SessionConfigOption {
const withoutGlm = glmEnabled ? option : stripGlmModelOption(option);
return kimiEnabled ? withoutGlm : stripKimiModelOption(withoutGlm);
}
23 changes: 23 additions & 0 deletions packages/ui/src/features/sessions/modelOptionFilters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
import { describe, expect, it } from "vitest";
import { stripKimiModelOption } from "./modelOptionFilters";

describe("modelOptionFilters", () => {
it("selects an available model when Kimi is hidden", () => {
const option: SessionConfigOption = {
type: "select",
id: "model",
name: "Model",
currentValue: "moonshotai/kimi-k3",
options: [
{ value: "moonshotai/kimi-k3", name: "Kimi K3" },
{ value: "claude-opus-4-8", name: "Claude Opus 4.8" },
],
};

expect(stripKimiModelOption(option)).toMatchObject({
currentValue: "claude-opus-4-8",
options: [{ value: "claude-opus-4-8" }],
});
});
});
35 changes: 35 additions & 0 deletions packages/ui/src/features/sessions/modelOptionFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,38 @@ export function stripGlmModelOption(
),
};
}

export function stripKimiModelOption(
option: SessionConfigOption,
): SessionConfigOption {
if (option.type !== "select") return option;

if (isSelectGroup(option.options)) {
const options = option.options.map((group) => ({
...group,
options: group.options.filter(
(model) => model.value !== "moonshotai/kimi-k3",
),
}));
return {
...option,
options,
currentValue:
option.currentValue === "moonshotai/kimi-k3"
? (options.flatMap((group) => group.options)[0]?.value ?? "")
: option.currentValue,
};
}

const options = option.options.filter(
(model) => model.value !== "moonshotai/kimi-k3",
);
return {
...option,
options,
currentValue:
option.currentValue === "moonshotai/kimi-k3"
? (options[0]?.value ?? "")
: option.currentValue,
};
}
16 changes: 11 additions & 5 deletions packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import {
defaultEligibleModel,
GLM_MODEL_FLAG,
getCloudUrlFromRegion,
KIMI_MODEL_FLAG,
} from "@posthog/shared";
import { stripGlmModelOption } from "@posthog/ui/features/sessions/modelOptionFilters";
import {
stripGlmModelOption,
stripKimiModelOption,
} from "@posthog/ui/features/sessions/modelOptionFilters";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { logger } from "../../../shell/logger";
import { useAuthStateValue } from "../../auth/store";
Expand Down Expand Up @@ -48,6 +52,7 @@ function getOptionByCategory(
export function usePreviewConfig(adapter: Adapter): PreviewConfigResult {
const hostClient = useHostTRPCClient();
const glmEnabled = useFeatureFlag(GLM_MODEL_FLAG);
const kimiEnabled = useFeatureFlag(KIMI_MODEL_FLAG);
const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
const apiHost = useMemo(
() => (cloudRegion ? getCloudUrlFromRegion(cloudRegion) : null),
Expand Down Expand Up @@ -83,9 +88,10 @@ export function usePreviewConfig(adapter: Adapter): PreviewConfigResult {
.then((serverOptions) => {
if (abort.signal.aborted) return;

const options = glmEnabled
? serverOptions
: serverOptions.map(stripGlmModelOption);
const options = serverOptions.map((option) => {
const withoutGlm = glmEnabled ? option : stripGlmModelOption(option);
return kimiEnabled ? withoutGlm : stripKimiModelOption(withoutGlm);
});

const {
defaultInitialTaskMode,
Expand Down Expand Up @@ -145,7 +151,7 @@ export function usePreviewConfig(adapter: Adapter): PreviewConfigResult {
return () => {
abort.abort();
};
}, [adapter, apiHost, hostClient, hasHydrated, glmEnabled]);
}, [adapter, apiHost, hostClient, hasHydrated, glmEnabled, kimiEnabled]);

const setConfigOption = useCallback(
(configId: string, value: string) => {
Expand Down
Loading