Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 24 additions & 5 deletions apps/server/src/provider/Layers/PiAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface FakePi {

const makeFakePi = Effect.fn("makeFakePi")(function* (input?: {
readonly subagentsCommand?: boolean;
readonly contextCommand?: boolean;
readonly fastCommand?: boolean;
}) {
const stdoutQueue = yield* Queue.unbounded<Uint8Array>();
Expand Down Expand Up @@ -100,6 +101,9 @@ const makeFakePi = Effect.fn("makeFakePi")(function* (input?: {
...(input?.subagentsCommand === false
? []
: [{ name: "subagents-rpc", source: "extension" }]),
...(input?.contextCommand === true
? [{ name: "context", source: "extension" }]
: []),
...(input?.fastCommand === true
? [{ name: "fast", source: "extension" }]
: []),
Expand Down Expand Up @@ -253,18 +257,19 @@ describe("makePiAdapter", () => {
}).pipe(Effect.scoped, Effect.provide(TestEnv)),
);

it.effect("synchronizes Codex Fast service before the user prompt", () =>
it.effect("synchronizes Pi context and Codex Fast service before the user prompt", () =>
Effect.gen(function* () {
const fake = yield* makeFakePi({ fastCommand: true });
const fake = yield* makeFakePi({ contextCommand: true, fastCommand: true });
const adapter = yield* makePiAdapter(settings, { instanceId: INSTANCE }).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, fake.spawner),
);
const threadId = ThreadId.make("88888888-8888-4888-8888-888888888888");
const fastSelection: ModelSelection = {
instanceId: INSTANCE,
model: "openai-codex/gpt-5.5",
model: "openai-codex/gpt-5.6-sol",
options: [
{ id: "reasoning", value: "off" },
{ id: "contextWindow", value: "372k" },
{ id: "serviceTier", value: "priority" },
],
};
Expand All @@ -283,12 +288,16 @@ describe("makePiAdapter", () => {
const thinkingIndex = fake.written.findIndex(
(command) => command.type === "set_thinking_level" && command.level === "off",
);
const contextIndex = fake.written.findIndex(
(command) => command.type === "prompt" && command.message === "/context 372k",
);
const fastIndex = fake.written.findIndex(
(command) => command.type === "prompt" && command.message === "/fast on",
);
expect(modelIndex).toBeGreaterThanOrEqual(0);
expect(thinkingIndex).toBeGreaterThan(modelIndex);
expect(fastIndex).toBeGreaterThan(thinkingIndex);
expect(contextIndex).toBeGreaterThan(thinkingIndex);
expect(fastIndex).toBeGreaterThan(contextIndex);

yield* adapter.sendTurn({
threadId,
Expand All @@ -297,6 +306,7 @@ describe("makePiAdapter", () => {
...fastSelection,
options: [
{ id: "reasoning", value: "off" },
{ id: "contextWindow", value: "auto" },
{ id: "serviceTier", value: "default" },
],
},
Expand All @@ -305,6 +315,14 @@ describe("makePiAdapter", () => {
(command) => command.type === "prompt" && command.message === "/fast off",
);
expect(disabled.message).toBe("/fast off");
const resetContextIndex = fake.written.findIndex(
(command) => command.type === "prompt" && command.message === "/context auto",
);
const disableFastIndex = fake.written.findIndex(
(command) => command.type === "prompt" && command.message === "/fast off",
);
expect(resetContextIndex).toBeGreaterThan(fastIndex);
expect(disableFastIndex).toBeGreaterThan(resetContextIndex);
const userPrompt = yield* fake.takeStdinUntil(
(command) => command.type === "prompt" && command.message === "Use the standard tier now",
);
Expand Down Expand Up @@ -361,7 +379,8 @@ describe("makePiAdapter", () => {
(command) => command.type === "prompt" && command.message === "first-fast-prompt",
);
const fastOffIndex = fake.written.findIndex(
(command) => command.type === "prompt" && command.message === "/fast off",
(command, index) =>
index > firstPromptIndex && command.type === "prompt" && command.message === "/fast off",
);
const secondPromptIndex = fake.written.findIndex(
(command) => command.type === "steer" && command.message === "second-standard-steer",
Expand Down
93 changes: 71 additions & 22 deletions apps/server/src/provider/Layers/PiAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,14 @@ import {
buildPiRpcArgs,
buildPiRpcEnv,
extractPiAssistantText,
parsePiContextWindow,
parsePiFastServiceEnabled,
parsePiSubagentNotification,
parsePiThinkingLevel,
PI_AUTO_CONTEXT_WINDOW,
PI_CODEX_FAST_COMMAND,
PI_CONTEXT_COMMAND,
PI_CONTEXT_WINDOW_OPTION_ID,
PI_SERVICE_TIER_OPTION_ID,
PI_THINKING_OPTION_ID,
resolvePiBinary,
Expand Down Expand Up @@ -94,10 +98,11 @@ interface PiSessionContext {
assistantItemId: ProviderItemId | undefined;
assistantText: string;
reasoningText: string;
/** Cached `/fast` command availability and synchronized session state. */
fastCommandAvailable: boolean | undefined;
/** Cached extension-command availability and synchronized session state. */
extensionCommandNames: ReadonlySet<string> | undefined;
contextWindowSelectionKey: string | undefined;
fastServiceEnabled: boolean | undefined;
/** Keeps model/thinking/service-tier synchronization atomic with its prompt. */
/** Keeps model/thinking/context/service-tier synchronization atomic with its prompt. */
sendSemaphore: Semaphore.Semaphore;
stopped: boolean;
}
Expand Down Expand Up @@ -155,12 +160,14 @@ export function splitPiModelSlug(slug: string): { provider: string; modelId: str
return { provider: trimmed.slice(0, slashIndex), modelId: trimmed.slice(slashIndex + 1) };
}

function piRpcAdvertisesCommand(response: PiRpcResponse, commandName: string): boolean {
function piRpcCommandNames(response: PiRpcResponse): ReadonlySet<string> {
if (!isRecord(response.data) || !Array.isArray(response.data.commands)) {
return false;
return new Set();
}
return response.data.commands.some(
(command) => isRecord(command) && command.name === commandName,
return new Set(
response.data.commands.flatMap((command) =>
isRecord(command) && typeof command.name === "string" ? [command.name] : [],
),
);
}

Expand Down Expand Up @@ -251,14 +258,47 @@ export function makePiAdapter(piSettings: PiSettings, options?: PiAdapterLiveOpt
});
});

const syncFastService = (ctx: PiSessionContext, enabled: boolean | undefined) =>
const piAdvertisesCommand = (ctx: PiSessionContext, commandName: string) =>
Effect.gen(function* () {
if (enabled === undefined || enabled === ctx.fastServiceEnabled) return;
if (ctx.fastCommandAvailable === undefined) {
if (ctx.extensionCommandNames === undefined) {
const commands = yield* request(ctx, { type: "get_commands" });
ctx.fastCommandAvailable = piRpcAdvertisesCommand(commands, PI_CODEX_FAST_COMMAND);
ctx.extensionCommandNames = piRpcCommandNames(commands);
}
if (!ctx.fastCommandAvailable) {
return ctx.extensionCommandNames.has(commandName);
});

const syncContextWindow = (
ctx: PiSessionContext,
model: string | undefined,
selection: string | undefined,
) =>
Effect.gen(function* () {
if (selection === undefined) return;
const selectionKey = `${model ?? ""}\u0000${selection}`;
if (selectionKey === ctx.contextWindowSelectionKey) return;
if (!(yield* piAdvertisesCommand(ctx, PI_CONTEXT_COMMAND))) {
if (selection === PI_AUTO_CONTEXT_WINDOW) {
ctx.contextWindowSelectionKey = selectionKey;
return;
}
return yield* new ProviderAdapterRequestError({
provider: PROVIDER,
method: PI_CONTEXT_COMMAND,
detail:
"Pi does not advertise the /context command required for context-window selection. Enable the effort-commands extension in the selected Pi profile.",
});
}
yield* request(ctx, {
type: "prompt",
message: `/${PI_CONTEXT_COMMAND} ${selection}`,
});
ctx.contextWindowSelectionKey = selectionKey;
});

const syncFastService = (ctx: PiSessionContext, enabled: boolean | undefined) =>
Effect.gen(function* () {
if (enabled === undefined || enabled === ctx.fastServiceEnabled) return;
if (!(yield* piAdvertisesCommand(ctx, PI_CODEX_FAST_COMMAND))) {
if (!enabled) {
ctx.fastServiceEnabled = false;
return;
Expand Down Expand Up @@ -575,13 +615,22 @@ export function makePiAdapter(piSettings: PiSettings, options?: PiAdapterLiveOpt
const thinkingLevel = parsePiThinkingLevel(
getModelSelectionStringOptionValue(selection, PI_THINKING_OPTION_ID),
);
const contextWindow = parsePiContextWindow(
getModelSelectionStringOptionValue(selection, PI_CONTEXT_WINDOW_OPTION_ID),
);
const fastServiceEnabled = supportsPiCodexFastService(model)
? (parsePiFastServiceEnabled(
getModelSelectionStringOptionValue(selection, PI_SERVICE_TIER_OPTION_ID),
) ?? false)
: undefined;
const profile = getModelSelectionStringOptionValue(selection, PI_PROFILE_OPTION_ID)?.trim();
return { model, thinkingLevel, fastServiceEnabled, profile: profile || undefined };
return {
model,
thinkingLevel,
contextWindow,
fastServiceEnabled,
profile: profile || undefined,
};
};

const startSession: PiAdapterShape["startSession"] = (input) =>
Expand All @@ -606,9 +655,8 @@ export function makePiAdapter(piSettings: PiSettings, options?: PiAdapterLiveOpt
yield* stopSessionInternal(existing);
}

const { model, thinkingLevel, fastServiceEnabled, profile } = resolveModelSelection(
input.modelSelection,
);
const { model, thinkingLevel, contextWindow, fastServiceEnabled, profile } =
resolveModelSelection(input.modelSelection);
const resumeSessionId =
isRecord(input.resumeCursor) && typeof input.resumeCursor.piSessionId === "string"
? input.resumeCursor.piSessionId
Expand All @@ -632,7 +680,8 @@ export function makePiAdapter(piSettings: PiSettings, options?: PiAdapterLiveOpt
assistantItemId: undefined,
assistantText: "",
reasoningText: "",
fastCommandAvailable: undefined,
extensionCommandNames: undefined,
contextWindowSelectionKey: undefined,
fastServiceEnabled: undefined,
sendSemaphore,
stopped: false,
Expand Down Expand Up @@ -698,6 +747,7 @@ export function makePiAdapter(piSettings: PiSettings, options?: PiAdapterLiveOpt
updatedAt: now,
};
(ctx as { session: ProviderSession }).session = session;
yield* syncContextWindow(ctx, model, contextWindow);
yield* syncFastService(ctx, fastServiceEnabled);

sessions.set(input.threadId, ctx);
Expand Down Expand Up @@ -732,9 +782,8 @@ export function makePiAdapter(piSettings: PiSettings, options?: PiAdapterLiveOpt
Effect.flatMap(requireSession(input.threadId), (ctx) =>
ctx.sendSemaphore.withPermit(
Effect.gen(function* () {
const { model, thinkingLevel, fastServiceEnabled } = resolveModelSelection(
input.modelSelection,
);
const { model, thinkingLevel, contextWindow, fastServiceEnabled } =
resolveModelSelection(input.modelSelection);

// In-session model / thinking switch.
if (model && model !== ctx.session.model) {
Expand All @@ -744,6 +793,7 @@ export function makePiAdapter(piSettings: PiSettings, options?: PiAdapterLiveOpt
if (thinkingLevel) {
yield* request(ctx, { type: "set_thinking_level", level: thinkingLevel });
}
yield* syncContextWindow(ctx, model, contextWindow);
yield* syncFastService(ctx, fastServiceEnabled);

const text = input.input?.trim();
Expand Down Expand Up @@ -899,8 +949,7 @@ export function makePiAdapter(piSettings: PiSettings, options?: PiAdapterLiveOpt
// model by Pi. Verify the private bridge command exists before sending
// it so opening a thread without the optional pi-subagents extension
// can never create an unintended user turn.
const commands = yield* request(ctx, { type: "get_commands" });
if (!piRpcAdvertisesCommand(commands, "subagents-rpc")) {
if (!(yield* piAdvertisesCommand(ctx, "subagents-rpc"))) {
return yield* new ProviderAdapterValidationError({
provider: PROVIDER,
operation: "controlSubagent",
Expand Down
67 changes: 66 additions & 1 deletion apps/server/src/provider/pi/piModelDiscovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,43 @@ describe("piModelCapabilities", () => {
expect(optionIds).toEqual(["off", "minimal", "low", "medium", "high"]);
});

it("advertises context-window choices from the configured default through the catalog max", () => {
const capabilities = piModelCapabilities(
{
id: "gpt-5.6-sol",
provider: "openai-codex",
contextWindow: 272_000,
},
{ contextCommandAvailable: true },
);
const descriptor = capabilities.optionDescriptors?.find(
(option) => option.id === "contextWindow",
);
expect(descriptor).toMatchObject({
id: "contextWindow",
label: "Context Window",
type: "select",
options: [
{ id: "auto", label: "Auto (272K)", isDefault: true },
{ id: "128k", label: "128K" },
{ id: "200k", label: "200K" },
{ id: "256k", label: "256K" },
{ id: "272k", label: "272K" },
{ id: "372k", label: "372K" },
],
});
});

it("does not advertise context-window controls without the /context extension command", () => {
const capabilities = piModelCapabilities(
{ id: "gpt-x", provider: "custom", contextWindow: 200_000 },
{ contextCommandAvailable: false },
);
expect(capabilities.optionDescriptors?.some((option) => option.id === "contextWindow")).toBe(
false,
);
});

it("advertises Standard and Fast service tiers for supported OpenAI Codex models", () => {
const capabilities = piModelCapabilities(
{
Expand Down Expand Up @@ -82,7 +119,7 @@ describe("piModelCapabilities", () => {
it("does not advertise Fast service for unsupported Codex model ids", () => {
const capabilities = piModelCapabilities(
{
id: "gpt-5.6-sol",
id: "gpt-5.4-mini",
provider: "openai-codex",
reasoning: true,
},
Expand Down Expand Up @@ -123,6 +160,34 @@ describe("discoverPiModelsWithSdk", () => {
);
});

it("exposes context controls when the loaded profile registers the command", async () => {
const result = await discoverPiModelsWithSdk({
createAgentSessionServices: async () => ({
modelRegistry: {
getAvailable: () => [
{
id: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
provider: "openai-codex",
contextWindow: 272_000,
},
],
getError: () => undefined,
},
resourceLoader: {
getExtensions: () => ({
extensions: [{ commands: new Map([["context", {}]]) }],
}),
},
diagnostics: [],
}),
});

expect(result.models[0]?.capabilities?.optionDescriptors).toContainEqual(
expect.objectContaining({ id: "contextWindow", label: "Context Window" }),
);
});

it("loads extension-registered providers before enumerating available models", async () => {
let receivedOptions: Record<string, unknown> | undefined;
const result = await discoverPiModelsWithSdk(
Expand Down
Loading
Loading