feat(extensions): expose session service tiers
This commit is contained in:
@@ -121,9 +121,12 @@ Core methods:
|
||||
- `getCommands`
|
||||
- `getSessionName`, `setSessionName`
|
||||
- `setModel`, `getThinkingLevel`, `setThinkingLevel`
|
||||
- `getServiceTiers`, `setServiceTier`
|
||||
- `registerProvider`
|
||||
- `events` (shared event bus)
|
||||
|
||||
`getServiceTiers()` returns a detached snapshot of the session's live per-family tier map. `setServiceTier(family, tier)` changes one family for subsequent requests; pass `undefined` to clear that session override. OpenAI accepts `auto`, `default`, `flex`, `scale`, or `priority`; Anthropic accepts `priority`; Google accepts `flex` or `priority`. Changes made while a response is streaming do not alter that in-flight request.
|
||||
|
||||
In interactive mode, `input` handlers run before the built-in first-message auto-title check. Extensions that call `await pi.setSessionName(...)` from `input` can set the persisted session name and prevent the default auto-generated title from running for that session.
|
||||
|
||||
Also exposed:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- Added a toggle-list editor in `/settings` for array-of-enum settings: `providers.webSearchOrder` and `providers.imageOrder` (ordered — Enter/Space toggles, ←/→ nudges, 1-9 splices the hovered provider into that position) and `providers.webSearchExclude` now appear under Providers → Services instead of being config-file only.
|
||||
- Added `models.yml` Bedrock Converse prompt-cache capability overrides for bundled and opaque inference profiles.
|
||||
- Documented Vibe mode (`/vibe`) in `docs/vibe-mode.md` and the `/fresh` provider-stream reset in the session-operations doc, and linked both from the README's new "Session controls" section ([#6440](https://github.com/can1357/oh-my-pi/issues/6440)).
|
||||
- Added `getServiceTiers()` and `setServiceTier()` extension APIs for reading and changing the live per-family service tier used by subsequent session requests ([#5860](https://github.com/can1357/oh-my-pi/issues/5860)).
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ServiceTier, ServiceTierByFamily } from "@oh-my-pi/pi-ai";
|
||||
import type { ServiceTier, ServiceTierByFamily, ServiceTierFamily } from "@oh-my-pi/pi-ai";
|
||||
import type { SubmenuOption } from "./settings-schema";
|
||||
|
||||
/**
|
||||
@@ -16,6 +16,31 @@ export type ServiceTierOpenAISettingValue = (typeof SERVICE_TIER_OPENAI_VALUES)[
|
||||
export type ServiceTierAnthropicSettingValue = (typeof SERVICE_TIER_ANTHROPIC_VALUES)[number];
|
||||
export type ServiceTierGoogleSettingValue = (typeof SERVICE_TIER_GOOGLE_VALUES)[number];
|
||||
|
||||
/** Whether a runtime value names a provider family with an independent service-tier knob. */
|
||||
export function isServiceTierFamily(value: unknown): value is ServiceTierFamily {
|
||||
return value === "openai" || value === "anthropic" || value === "google";
|
||||
}
|
||||
|
||||
/** Whether a runtime value is a supported service tier for one provider family. */
|
||||
export function isServiceTierForFamily(family: string, tier: unknown): tier is ServiceTier {
|
||||
if (typeof tier !== "string" || tier === "none") return false;
|
||||
let values: readonly string[];
|
||||
switch (family) {
|
||||
case "openai":
|
||||
values = SERVICE_TIER_OPENAI_VALUES;
|
||||
break;
|
||||
case "anthropic":
|
||||
values = SERVICE_TIER_ANTHROPIC_VALUES;
|
||||
break;
|
||||
case "google":
|
||||
values = SERVICE_TIER_GOOGLE_VALUES;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return values.includes(tier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit-capable single value for the subagent/advisor tiers. The chosen tier
|
||||
* is broadcast across families and applied to whichever family the spawned
|
||||
|
||||
@@ -5,13 +5,22 @@ import type * as fs1 from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, Model, TextContent, TSchema } from "@oh-my-pi/pi-ai";
|
||||
import type {
|
||||
ImageContent,
|
||||
Model,
|
||||
ServiceTier,
|
||||
ServiceTierByFamily,
|
||||
ServiceTierFamily,
|
||||
TextContent,
|
||||
TSchema,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import type { KeyId } from "@oh-my-pi/pi-tui";
|
||||
import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
import { Type } from "arktype";
|
||||
import * as zodModule from "zod/v4";
|
||||
import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
|
||||
import { type Hook, hookCapability } from "../../capability/hook";
|
||||
import { isServiceTierFamily, isServiceTierForFamily } from "../../config/service-tier";
|
||||
import { loadCapability } from "../../discovery";
|
||||
import { getExtensionNameFromPath } from "../../discovery/helpers";
|
||||
import type { ExecOptions } from "../../exec/exec";
|
||||
@@ -107,6 +116,14 @@ export class ExtensionRuntime implements IExtensionRuntime {
|
||||
throw new ExtensionRuntimeNotInitializedError();
|
||||
}
|
||||
|
||||
getServiceTiers(): ServiceTierByFamily {
|
||||
throw new ExtensionRuntimeNotInitializedError();
|
||||
}
|
||||
|
||||
setServiceTier(): void {
|
||||
throw new ExtensionRuntimeNotInitializedError();
|
||||
}
|
||||
|
||||
getSessionName(): string | undefined {
|
||||
throw new ExtensionRuntimeNotInitializedError();
|
||||
}
|
||||
@@ -252,6 +269,17 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
||||
this.runtime.setThinkingLevel(level, persist);
|
||||
}
|
||||
|
||||
getServiceTiers(): Readonly<ServiceTierByFamily> {
|
||||
return { ...this.runtime.getServiceTiers() };
|
||||
}
|
||||
|
||||
setServiceTier(family: ServiceTierFamily, tier: ServiceTier | undefined): void {
|
||||
if (!isServiceTierFamily(family) || (tier !== undefined && !isServiceTierForFamily(family, tier))) {
|
||||
throw new TypeError(`Invalid service tier "${String(tier)}" for family "${String(family)}"`);
|
||||
}
|
||||
this.runtime.setServiceTier(family, tier);
|
||||
}
|
||||
|
||||
getSessionName(): string | undefined {
|
||||
return this.runtime.getSessionName();
|
||||
}
|
||||
|
||||
@@ -298,6 +298,8 @@ export class ExtensionRunner {
|
||||
this.runtime.setModel = actions.setModel;
|
||||
this.runtime.getThinkingLevel = actions.getThinkingLevel;
|
||||
this.runtime.setThinkingLevel = actions.setThinkingLevel;
|
||||
this.runtime.getServiceTiers = actions.getServiceTiers;
|
||||
this.runtime.setServiceTier = actions.setServiceTier;
|
||||
this.runtime.getSessionName = actions.getSessionName;
|
||||
this.runtime.setSessionName = actions.setSessionName;
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ import type {
|
||||
Model,
|
||||
ModelSpec,
|
||||
ProviderResponseMetadata,
|
||||
ServiceTier,
|
||||
ServiceTierByFamily,
|
||||
ServiceTierFamily,
|
||||
SimpleStreamOptions,
|
||||
Static,
|
||||
TextContent,
|
||||
@@ -1044,6 +1047,13 @@ export interface RegisteredCommand {
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements
|
||||
export type ExtensionHandler<E, R = undefined> = (event: E, ctx: ExtensionContext) => Promise<R | void> | R | void;
|
||||
|
||||
/** Service tiers accepted by each provider family. */
|
||||
export type ExtensionServiceTier<Family extends ServiceTierFamily> = Family extends "anthropic"
|
||||
? "priority"
|
||||
: Family extends "google"
|
||||
? "flex" | "priority"
|
||||
: ServiceTier;
|
||||
|
||||
/**
|
||||
* ExtensionAPI passed to extension factory functions.
|
||||
*/
|
||||
@@ -1230,6 +1240,18 @@ export interface ExtensionAPI {
|
||||
/** Set thinking level for the current session. */
|
||||
setThinkingLevel(level: ThinkingLevel): void;
|
||||
|
||||
/** Get a snapshot of the current session's per-family service tiers. */
|
||||
getServiceTiers(): Readonly<ServiceTierByFamily>;
|
||||
|
||||
/**
|
||||
* Set one provider family's service tier for subsequent requests, or clear
|
||||
* its session override with `undefined`.
|
||||
*/
|
||||
setServiceTier<Family extends ServiceTierFamily>(
|
||||
family: Family,
|
||||
tier: ExtensionServiceTier<Family> | undefined,
|
||||
): void;
|
||||
|
||||
/** Get the current session name. */
|
||||
getSessionName(): string | undefined;
|
||||
|
||||
@@ -1409,6 +1431,10 @@ export type GetThinkingLevelHandler = () => ThinkingLevel | undefined;
|
||||
|
||||
export type SetThinkingLevelHandler = (level: ThinkingLevel, persist?: boolean) => void;
|
||||
|
||||
export type GetServiceTiersHandler = () => ServiceTierByFamily;
|
||||
|
||||
export type SetServiceTierHandler = (family: ServiceTierFamily, tier: ServiceTier | undefined) => void;
|
||||
|
||||
/** Shared state created by loader, used during registration and runtime. */
|
||||
export interface ExtensionRuntimeState {
|
||||
flagValues: Map<string, boolean | string>;
|
||||
@@ -1429,6 +1455,8 @@ export interface ExtensionActions {
|
||||
setModel: SetModelHandler;
|
||||
getThinkingLevel: GetThinkingLevelHandler;
|
||||
setThinkingLevel: SetThinkingLevelHandler;
|
||||
getServiceTiers: GetServiceTiersHandler;
|
||||
setServiceTier: SetServiceTierHandler;
|
||||
getSessionName: () => string | undefined;
|
||||
setSessionName: (name: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -2311,6 +2311,8 @@ export class AcpAgent implements Agent {
|
||||
},
|
||||
getThinkingLevel: () => record.session.thinkingLevel,
|
||||
setThinkingLevel: level => record.session.setThinkingLevel(level),
|
||||
getServiceTiers: () => record.session.serviceTierByFamily,
|
||||
setServiceTier: (family, tier) => record.session.setServiceTierFamily(family, tier),
|
||||
getSessionName: () => record.session.sessionManager.getSessionName(),
|
||||
setSessionName: async name => {
|
||||
await record.session.sessionManager.setSessionName(name, "user");
|
||||
|
||||
@@ -165,6 +165,8 @@ export class ExtensionUiController {
|
||||
},
|
||||
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
||||
setThinkingLevel: level => this.ctx.session.setThinkingLevel(level),
|
||||
getServiceTiers: () => this.ctx.session.serviceTierByFamily,
|
||||
setServiceTier: (family, tier) => this.ctx.session.setServiceTierFamily(family, tier),
|
||||
getCommands: () => getSessionSlashCommands(this.ctx.session),
|
||||
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
||||
setSessionName: name => this.#updateSessionName(name),
|
||||
@@ -396,6 +398,8 @@ export class ExtensionUiController {
|
||||
},
|
||||
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
||||
setThinkingLevel: (level, persist) => this.ctx.session.setThinkingLevel(level, persist),
|
||||
getServiceTiers: () => this.ctx.session.serviceTierByFamily,
|
||||
setServiceTier: (family, tier) => this.ctx.session.setServiceTierFamily(family, tier),
|
||||
getCommands: () => getSessionSlashCommands(this.ctx.session),
|
||||
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
||||
setSessionName: name => this.#updateSessionName(name),
|
||||
|
||||
@@ -90,6 +90,8 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
||||
setModel: model => runExtensionSetModel(session, model),
|
||||
getThinkingLevel: () => session.thinkingLevel,
|
||||
setThinkingLevel: level => session.setThinkingLevel(level),
|
||||
getServiceTiers: () => session.serviceTierByFamily,
|
||||
setServiceTier: (family, tier) => session.setServiceTierFamily(family, tier),
|
||||
getSessionName: () => session.sessionManager.getSessionName(),
|
||||
setSessionName: async name => {
|
||||
await session.sessionManager.setSessionName(name, "user");
|
||||
|
||||
@@ -2938,6 +2938,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
setModel: model => runExtensionSetModel(session, model),
|
||||
getThinkingLevel: () => session.thinkingLevel,
|
||||
setThinkingLevel: level => session.setThinkingLevel(level),
|
||||
getServiceTiers: () => session.serviceTierByFamily,
|
||||
setServiceTier: (family, tier) => session.setServiceTierFamily(family, tier),
|
||||
getSessionName: () => session.sessionManager.getSessionName(),
|
||||
setSessionName: async name => {
|
||||
await session.sessionManager.setSessionName(name, "user");
|
||||
|
||||
@@ -529,6 +529,8 @@ describe("ExtensionRunner", () => {
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getServiceTiers: () => ({}),
|
||||
setServiceTier: () => {},
|
||||
getSessionName: () => undefined,
|
||||
setSessionName: async () => {},
|
||||
},
|
||||
@@ -1206,6 +1208,8 @@ describe("ExtensionRunner", () => {
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getServiceTiers: () => ({}),
|
||||
setServiceTier: () => {},
|
||||
getSessionName: () => undefined,
|
||||
setSessionName: async () => {},
|
||||
},
|
||||
@@ -1232,6 +1236,84 @@ describe("ExtensionRunner", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("service tier API", () => {
|
||||
it("returns a detached snapshot, forwards valid changes, and rejects invalid family tiers", async () => {
|
||||
const extCode = `
|
||||
export default function(pi) {
|
||||
pi.on("session_start", () => {
|
||||
const tiers = pi.getServiceTiers();
|
||||
tiers.openai = "scale";
|
||||
pi.appendEntry("service-tier-snapshot", tiers);
|
||||
pi.setServiceTier("google", "flex");
|
||||
pi.setServiceTier("openai", undefined);
|
||||
pi.setServiceTier("anthropic", "scale");
|
||||
});
|
||||
}
|
||||
`;
|
||||
const explicitExtensionPath = path.join(tempDir.path(), "service-tiers.ts");
|
||||
fs.writeFileSync(explicitExtensionPath, extCode);
|
||||
const result = await loadTestExtensions([explicitExtensionPath]);
|
||||
const runner = new ExtensionRunner(
|
||||
result.extensions,
|
||||
result.runtime,
|
||||
tempDir.path(),
|
||||
sessionManager,
|
||||
modelRegistry,
|
||||
);
|
||||
const serviceTiers = { openai: "priority" as const };
|
||||
const snapshots: unknown[] = [];
|
||||
const setCalls: Array<[string, unknown]> = [];
|
||||
const errors: string[] = [];
|
||||
runner.onError(error => {
|
||||
errors.push(error.error);
|
||||
});
|
||||
runner.initialize(
|
||||
{
|
||||
sendMessage: () => {},
|
||||
sendUserMessage: () => {},
|
||||
appendEntry: (_customType, data) => {
|
||||
snapshots.push(data);
|
||||
},
|
||||
setLabel: () => {},
|
||||
getActiveTools: () => [],
|
||||
getAllTools: () => [],
|
||||
setActiveTools: async () => {},
|
||||
getCommands: () => [],
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getServiceTiers: () => serviceTiers,
|
||||
setServiceTier: (family, tier) => {
|
||||
setCalls.push([family, tier]);
|
||||
},
|
||||
getSessionName: () => undefined,
|
||||
setSessionName: async () => {},
|
||||
},
|
||||
{
|
||||
getModel: () => undefined,
|
||||
isIdle: () => true,
|
||||
abort: () => {},
|
||||
hasPendingMessages: () => false,
|
||||
shutdown: () => {},
|
||||
getContextUsage: () => undefined,
|
||||
compact: async () => {},
|
||||
getSystemPrompt: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
await runner.emit({ type: "session_start" });
|
||||
|
||||
expect(serviceTiers).toEqual({ openai: "priority" });
|
||||
expect(snapshots).toEqual([{ openai: "scale" }]);
|
||||
expect(setCalls).toEqual([
|
||||
["google", "flex"],
|
||||
["openai", undefined],
|
||||
]);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0]).toContain('Invalid service tier "scale" for family "anthropic"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("session name API", () => {
|
||||
it("lets extensions read and set the session name after initialization", async () => {
|
||||
const extCode = `
|
||||
@@ -1268,6 +1350,8 @@ describe("ExtensionRunner", () => {
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getServiceTiers: () => ({}),
|
||||
setServiceTier: () => {},
|
||||
getSessionName: () => sessionManager.getSessionName(),
|
||||
setSessionName: async name => {
|
||||
await sessionManager.setSessionName(name);
|
||||
@@ -1326,6 +1410,8 @@ describe("ExtensionRunner", () => {
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getServiceTiers: () => ({}),
|
||||
setServiceTier: () => {},
|
||||
getSessionName: () => undefined,
|
||||
setSessionName: async () => {},
|
||||
},
|
||||
@@ -1993,6 +2079,8 @@ describe("ExtensionRunner", () => {
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getServiceTiers: () => ({}),
|
||||
setServiceTier: () => {},
|
||||
getSessionName: () => sessionManager.getSessionName(),
|
||||
setSessionName: async () => {},
|
||||
},
|
||||
|
||||
@@ -60,6 +60,8 @@ const initializeRunnerForTest = (runner: ExtensionRunner | undefined): void => {
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getServiceTiers: () => ({}),
|
||||
setServiceTier: () => {},
|
||||
getSessionName: () => undefined,
|
||||
setSessionName: async () => {},
|
||||
},
|
||||
@@ -527,6 +529,8 @@ describe("createAgentSession credential_disabled subscription", () => {
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getServiceTiers: () => ({}),
|
||||
setServiceTier: () => {},
|
||||
getSessionName: () => undefined,
|
||||
setSessionName: async () => {},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user