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
1 change: 0 additions & 1 deletion apps/mobile/src/components/ComposerModelControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export function ComposerModelControl(props: {
const promptTraits = getPromptControlledTraits(descriptors, props.prompt);
const summary = props.provider
? buildTraitsTriggerDisplay({
provider: props.provider,
descriptors,
primarySelectDescriptorId: promptTraits.primarySelectDescriptor?.id ?? null,
ultrathinkPromptControlled: promptTraits.ultrathinkPromptControlled,
Expand Down
35 changes: 8 additions & 27 deletions apps/web/src/components/chat/ComposerModelEffort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,12 @@ import {
type SelectDescriptor = Extract<ProviderOptionDescriptor, { type: "select" }>;
type BooleanDescriptor = Extract<ProviderOptionDescriptor, { type: "boolean" }>;

/** Codex service tiers, Claude's Fast mode, and Ultracode sit in the footer, not the column. */
/** Service tiers, Claude's Fast mode, and Ultracode sit in the footer, not the column. */
function splitSpeed(
provider: TraitsSelectionInput["provider"],
selects: ReadonlyArray<SelectDescriptor>,
toggles: ReadonlyArray<BooleanDescriptor>,
) {
const speedSelect =
provider === "codex"
? (selects.find((descriptor) => descriptor.id === "serviceTier") ?? null)
: null;
const speedSelect = selects.find((descriptor) => descriptor.id === "serviceTier") ?? null;
const fastToggle = toggles.find((descriptor) => descriptor.id === "fastMode") ?? null;
const ultracodeToggle = toggles.find((descriptor) => descriptor.id === "ultracode") ?? null;
return {
Expand All @@ -68,11 +64,10 @@ type ColumnControl =
* such as an agent's session mode, sit below as one row each.
*/
function columnControls(
provider: TraitsSelectionInput["provider"],
selects: ReadonlyArray<SelectDescriptor>,
toggles: ReadonlyArray<BooleanDescriptor>,
) {
const { columnSelects, columnToggles } = splitSpeed(provider, selects, toggles);
const { columnSelects, columnToggles } = splitSpeed(selects, toggles);
const controls: ReadonlyArray<ColumnControl> = [
...columnSelects.map((descriptor) => ({ kind: "select" as const, descriptor })),
...columnToggles.map((descriptor) => ({ kind: "boolean" as const, descriptor })),
Expand Down Expand Up @@ -100,7 +95,6 @@ export function describeComposerEffort(
: null;
const primary = visibility.primarySelectDescriptor;
const display = buildTraitsTriggerDisplay({
provider: traits.provider,
descriptors: visibility.descriptors,
primarySelectDescriptorId: primary?.id ?? null,
ultrathinkPromptControlled: visibility.ultrathinkPromptControlled,
Expand All @@ -109,8 +103,8 @@ export function describeComposerEffort(
});
// The same reasoning control the column lists, wherever the agent put it among its options.
const reasoning =
columnControls(traits.provider, visibility.selectDescriptors, visibility.booleanDescriptors)
.effort?.descriptor ?? null;
columnControls(visibility.selectDescriptors, visibility.booleanDescriptors).effort
?.descriptor ?? null;
const ultrathink = visibility.ultrathinkPromptControlled && reasoning === primary;
const glyph =
reasoning?.type === "select"
Expand Down Expand Up @@ -253,11 +247,7 @@ function moveBetweenRows(event: KeyboardEvent<HTMLElement>) {

function EffortLevels(props: { traits: TraitsSelectionInput }) {
const traits = useTraitsSelection(props.traits);
const { effort, others } = columnControls(
props.traits.provider,
traits.selectDescriptors,
traits.booleanDescriptors,
);
const { effort, others } = columnControls(traits.selectDescriptors, traits.booleanDescriptors);
if (traits.modelIsUnavailable) {
return (
<>
Expand Down Expand Up @@ -505,11 +495,7 @@ export function ComposerEffortColumn(props: { traits: TraitsSelectionInput | nul
const visibility = props.traits ? getTraitsSectionVisibility(props.traits) : null;
const controls =
props.traits && visibility
? columnControls(
props.traits.provider,
visibility.selectDescriptors,
visibility.booleanDescriptors,
)
? columnControls(visibility.selectDescriptors, visibility.booleanDescriptors)
: null;
return (
<>
Expand Down Expand Up @@ -573,7 +559,6 @@ function SpeedSegment(props: {
function SpeedControl(props: { traits: TraitsSelectionInput }) {
const traits = useTraitsSelection(props.traits);
const { speedSelect, fastToggle } = splitSpeed(
props.traits.provider,
traits.selectDescriptors,
traits.booleanDescriptors,
);
Expand Down Expand Up @@ -610,11 +595,7 @@ function SpeedControl(props: { traits: TraitsSelectionInput }) {

function UltracodeControl(props: { traits: TraitsSelectionInput }) {
const traits = useTraitsSelection(props.traits);
const { ultracodeToggle } = splitSpeed(
props.traits.provider,
traits.selectDescriptors,
traits.booleanDescriptors,
);
const { ultracodeToggle } = splitSpeed(traits.selectDescriptors, traits.booleanDescriptors);
if (traits.modelIsUnavailable || !ultracodeToggle) return null;
const on = ultracodeToggle.currentValue === true;
return (
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/components/chat/SubagentTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,18 @@ export function SubagentTooltipContent(props: {
descriptor.type === "boolean" &&
typeof saved?.value === "boolean"
) {
return getTraitsSpeedDisplay(provider.driver, {
return getTraitsSpeedDisplay({
...descriptor,
currentValue: saved.value,
});
}
if (
provider.driver === "codex" &&
descriptor.id === "serviceTier" &&
descriptor.type === "select" &&
typeof saved?.value === "string" &&
descriptor.options.some((option) => option.id === saved.value)
) {
return getTraitsSpeedDisplay(provider.driver, {
return getTraitsSpeedDisplay({
...descriptor,
currentValue: saved.value,
});
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/components/chat/TraitsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,6 @@ export const TraitsPicker = memo(function TraitsPicker({
return null;
}
const { label: triggerLabel } = buildTraitsTriggerDisplay({
provider: props.provider,
descriptors: traits.descriptors,
primarySelectDescriptorId: traits.primarySelectDescriptor?.id ?? null,
ultrathinkPromptControlled: traits.ultrathinkPromptControlled,
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/components/chat/TraitsSpeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { cn } from "~/lib/utils";
import { ComposerControlIcon, type ComposerControlSize } from "./ComposerControl";

export function getTraitsSpeedDisplay(
provider: ProviderDriverKind,
descriptor: ProviderOptionDescriptor,
): { label: string; speedIcon: "fast" | "ultrafast" | null } | null {
if (descriptor.id === "fastMode" && descriptor.type === "boolean") {
Expand All @@ -15,7 +14,7 @@ export function getTraitsSpeedDisplay(
speedIcon: descriptor.currentValue === true ? "fast" : null,
};
}
if (provider !== "codex" || descriptor.id !== "serviceTier" || descriptor.type !== "select") {
if (descriptor.id !== "serviceTier" || descriptor.type !== "select") {
return null;
}
const currentValue = getProviderOptionCurrentValue(descriptor);
Expand Down
5 changes: 4 additions & 1 deletion docs/user/providers-pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ update it through that manager instead. For example, a Nix profile installation

T3 Code discovers the models reported by Pi and exposes their supported thinking levels. The
thinking picker marks the discovered active model's configured level as its default without
overriding it. Other models keep their Pi defaults when no level is selected. Threads
overriding it. Other models keep their Pi defaults when no level is selected. A model that lists
`serviceTiers` (for example `[{ "id": "priority", "name": "Fast" }]`) in its Pi definition gets
a speed picker with Standard plus those tiers. A chosen tier is sent as the request's
`service_tier`; Standard sends none and leaves the endpoint's default. Threads
use Pi's native session files for resume, rollback, and forks within the same Pi instance. Forks
preserve the native conversation through the selected turn in the destination workspace.
Switching providers uses portable conversation context. Extension
Expand Down
3 changes: 1 addition & 2 deletions packages/client-runtime/src/modelTraits.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ProviderDriverKind, type ProviderOptionDescriptor } from "@t3tools/contracts";
import type { ProviderOptionDescriptor } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { buildTraitsTriggerDisplay, getDescriptorStringValue } from "./modelTraits.ts";
Expand All @@ -25,7 +25,6 @@ describe("buildTraitsTriggerDisplay", () => {

expect(
buildTraitsTriggerDisplay({
provider: ProviderDriverKind.make("pi"),
descriptors: [descriptor],
primarySelectDescriptorId: descriptor.id,
ultrathinkPromptControlled: false,
Expand Down
13 changes: 2 additions & 11 deletions packages/client-runtime/src/modelTraits.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type {
ModelSelection,
ProviderDriverKind,
ProviderOptionDescriptor,
} from "@t3tools/contracts";
import type { ModelSelection, ProviderOptionDescriptor } from "@t3tools/contracts";
import {
buildProviderOptionSelectionsFromDescriptors,
getProviderOptionCurrentLabel,
Expand Down Expand Up @@ -110,7 +106,6 @@ export function getTraitSelectChange(
}

export function buildTraitsTriggerDisplay(input: {
provider: ProviderDriverKind;
descriptors: ReadonlyArray<ProviderOptionDescriptor>;
primarySelectDescriptorId: string | null;
ultrathinkPromptControlled: boolean;
Expand Down Expand Up @@ -140,11 +135,7 @@ export function buildTraitsTriggerDisplay(input: {
}
continue;
}
if (
input.provider === "codex" &&
descriptor.id === "serviceTier" &&
descriptor.type === "select"
) {
if (descriptor.id === "serviceTier" && descriptor.type === "select") {
const currentValue = getProviderOptionCurrentValue(descriptor);
const fastTier = descriptor.options.find(({ label }) => label === "Fast");
const ultrafastTier = descriptor.options.find(({ label }) => label === "Ultrafast");
Expand Down
97 changes: 95 additions & 2 deletions packages/provider-pi/src/server/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
} from "./adapter.ts";
import { makePiRpcConnection, type PiRpcRecord } from "./rpc.ts";
import { loadMcpBridge } from "./mcpBridge.testkit.ts";
import { PI_T3_SERVICE_TIER_COMMAND } from "./mcpExtensionSource.ts";
import { turnItemOutputText } from "../../../client-runtime/src/work-log/itemDetail.ts";

const layerTest = Layer.mergeAll(
Expand Down Expand Up @@ -173,6 +174,12 @@ interface FakePi {
readonly allRequests: () => ReadonlyArray<PiRpcRecord>;
readonly promptThinkingLevels: ReadonlyArray<string>;
readonly promptModels: ReadonlyArray<string>;
/** T3 extension's service tier as each prompt was sent; Pi resets it with each session. */
readonly promptServiceTiers: ReadonlyArray<string | null>;
/** Pi reloads extensions without the adapter seeing it, e.g. a user extension's ctx.newSession(). */
readonly reloadExtensions: () => void;
/** Pi before 0.99 acknowledges prompts without a disposition. */
readonly omitPromptDisposition: () => void;
readonly queueStats: (data: unknown) => void;
/** Pi before 0.84.4 answers clear_queue as an unknown command. */
readonly setClearQueue: (
Expand Down Expand Up @@ -230,7 +237,11 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
let thinkingLevel = "high";
const promptThinkingLevels: Array<string> = [];
const promptModels: Array<string> = [];
const promptServiceTiers: Array<string | null> = [];
let serviceTier: string | null = null;
let promptDisposition = true;
let currentModel = "default";
const serviceTierCommand = `/${PI_T3_SERVICE_TIER_COMMAND}`;

const emit = (record: PiRpcRecord, chunkBytes?: number) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -280,6 +291,10 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
thinkingLevel = String(record["level"]);
return base;
case "prompt":
if (String(record["message"]).startsWith(serviceTierCommand)) {
serviceTier = String(record["message"]).slice(serviceTierCommand.length).trim() || null;
return promptDisposition ? { ...base, data: { disposition: "handled" } } : base;
}
if (isCompacting) {
return { ...base, success: false, error: PI_COMPACTING_PROMPT_ERROR };
}
Expand All @@ -297,12 +312,16 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
case "new_session": {
const cancelled = vetoNewSession;
vetoNewSession = false;
if (!cancelled) sessionFile = `/fake/new-${++sessionGeneration}.jsonl`;
if (!cancelled) {
sessionFile = `/fake/new-${++sessionGeneration}.jsonl`;
serviceTier = null;
}
return { ...base, data: { cancelled } };
}
case "switch_session": {
const cancelled = vetoSwitch;
vetoSwitch = false;
if (!cancelled) serviceTier = null;
return { ...base, data: { cancelled } };
}
case "get_entries":
Expand All @@ -312,6 +331,7 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
case "get_session_stats":
return { ...base, data: statsQueue.shift() ?? {} };
case "fork":
serviceTier = null;
return { ...base, data: { text: "Hello pi", cancelled: false } };
case "clear_queue":
return clearQueue === "unknown_command"
Expand All @@ -332,9 +352,13 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
stdinBuffer = stdinBuffer.slice(newline + 1);
if (line.length === 0) continue;
const record = decodeJsonLine(line) as PiRpcRecord;
if (record["type"] === "prompt") {
if (
record["type"] === "prompt" &&
!String(record["message"]).startsWith(serviceTierCommand)
) {
promptThinkingLevels.push(thinkingLevel);
promptModels.push(currentModel);
promptServiceTiers.push(serviceTier);
}
allRequests.push(record);
yield* Queue.offer(requests, record);
Expand Down Expand Up @@ -395,6 +419,13 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
},
promptThinkingLevels,
promptModels,
promptServiceTiers,
reloadExtensions: () => {
serviceTier = null;
},
omitPromptDisposition: () => {
promptDisposition = false;
},
emit,
takeRequest,
queueEntries: (data) => entriesQueue.push(data),
Expand Down Expand Up @@ -2293,6 +2324,68 @@ describe("PiAdapterV2", () => {
}).pipe(Effect.scoped, Effect.provide(layerTest)),
);

it.effect("applies the selected service tier without changing thinking", () =>
Effect.gen(function* () {
const fake = yield* makeFakePi;
const { runtime, takeEvent } = yield* openRuntime(fake);
const providerThread = yield* runtime.ensureThread({
threadId: THREAD_ID,
modelSelection: modelSelection("default"),
runtimePolicy,
});
const turn = (text: string, serviceTier: string | undefined, ordinal: number) =>
Effect.gen(function* () {
yield* startTurn(
runtime,
providerThread,
"default",
[],
text,
{
instanceId: PI_INSTANCE_ID,
model: "columbia-codex/gpt-6.1-sol",
options: [
{ id: "thinking", value: "high" },
...(serviceTier === undefined ? [] : [{ id: "serviceTier", value: serviceTier }]),
],
},
ordinal,
);
while (String((yield* fake.takeRequest("prompt"))["message"]).startsWith("/")) {}
yield* fake.emit({ type: "agent_start" });
yield* fake.emit({ type: "agent_end", messages: [], willRetry: false });
yield* fake.emit({ type: "agent_settled" });
yield* takeEvent((event) => event.type === "turn.terminal");
});

yield* turn("Fast", "priority", 1);
fake.reloadExtensions();
yield* turn("Still fast", "priority", 2);
yield* turn("Ultrafast", "ultrafast", 3);
fake.omitPromptDisposition();
yield* turn("Standard", "default", 4);
yield* turn("Unset", undefined, 5);

assert.deepEqual(fake.promptServiceTiers, ["priority", "priority", "ultrafast", null, null]);
assert.deepEqual(fake.promptThinkingLevels, ["high", "high", "high", "high", "high"]);
assert.deepEqual(
fake
.allRequests()
.flatMap((request) =>
String(request["message"]).startsWith(`/${PI_T3_SERVICE_TIER_COMMAND}`)
? [request["message"]]
: [],
),
[
`/${PI_T3_SERVICE_TIER_COMMAND} priority`,
`/${PI_T3_SERVICE_TIER_COMMAND} priority`,
`/${PI_T3_SERVICE_TIER_COMMAND} ultrafast`,
`/${PI_T3_SERVICE_TIER_COMMAND}`,
],
);
}).pipe(Effect.scoped, Effect.provide(layerTest)),
);

it.effect.each([false, true, "previous"] as const)(
"resets applied thinking when returning to Pi default (failed first restore: %s)",
(failFirstRestore) =>
Expand Down
Loading
Loading