fix(extensions): preserve service-tier host compatibility

This commit is contained in:
Alex TYRODE
2026-07-23 22:14:31 +00:00
parent e0928070c2
commit 7626effec5
4 changed files with 30 additions and 23 deletions
@@ -72,6 +72,10 @@ export type ExtensionErrorListener = (error: ExtensionError) => void;
export const EXTENSION_HANDLER_TIMEOUT_MS = 30_000;
let extensionHandlerTimeoutMs = EXTENSION_HANDLER_TIMEOUT_MS;
function throwUnsupportedServiceTierAction(): never {
throw new Error("This extension host does not support service-tier actions");
}
export function testSetExtensionHandlerTimeoutMs(timeoutMs: number): void {
extensionHandlerTimeoutMs = timeoutMs;
}
@@ -298,8 +302,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.getServiceTiers = actions.getServiceTiers ?? throwUnsupportedServiceTierAction;
this.runtime.setServiceTier = actions.setServiceTier ?? throwUnsupportedServiceTierAction;
this.runtime.getSessionName = actions.getSessionName;
this.runtime.setSessionName = actions.setSessionName;
@@ -1455,8 +1455,8 @@ export interface ExtensionActions {
setModel: SetModelHandler;
getThinkingLevel: GetThinkingLevelHandler;
setThinkingLevel: SetThinkingLevelHandler;
getServiceTiers: GetServiceTiersHandler;
setServiceTier: SetServiceTierHandler;
getServiceTiers?: GetServiceTiersHandler;
setServiceTier?: SetServiceTierHandler;
getSessionName: () => string | undefined;
setSessionName: (name: string) => Promise<void>;
}
@@ -1488,8 +1488,11 @@ export interface ExtensionCommandContextActions {
reload: () => Promise<void>;
}
/** Full runtime = state + actions. */
export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions {}
/** Full runtime = state + actions, including host-compatible service-tier fallbacks. */
export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions {
getServiceTiers: GetServiceTiersHandler;
setServiceTier: SetServiceTierHandler;
}
/** Loaded extension with all registered items. */
export interface Extension {
@@ -2,7 +2,7 @@
* Tests for ExtensionRunner - conflict detection, error handling, tool wrapping.
*/
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, expectTypeOf, it, vi } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
@@ -15,7 +15,7 @@ import {
ExtensionRunner,
testSetExtensionHandlerTimeoutMs,
} from "@oh-my-pi/pi-coding-agent/extensibility/extensions/runner";
import type { ExtensionError } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/types";
import type { ExtensionError, ExtensionServiceTier } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/types";
import { ExtensionToolWrapper } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/wrapper";
import { Type } from "@oh-my-pi/pi-coding-agent/extensibility/typebox";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
@@ -529,8 +529,6 @@ describe("ExtensionRunner", () => {
setModel: async () => false,
getThinkingLevel: () => undefined,
setThinkingLevel: () => {},
getServiceTiers: () => ({}),
setServiceTier: () => {},
getSessionName: () => undefined,
setSessionName: async () => {},
},
@@ -1208,8 +1206,6 @@ describe("ExtensionRunner", () => {
setModel: async () => false,
getThinkingLevel: () => undefined,
setThinkingLevel: () => {},
getServiceTiers: () => ({}),
setServiceTier: () => {},
getSessionName: () => undefined,
setSessionName: async () => {},
},
@@ -1237,6 +1233,14 @@ describe("ExtensionRunner", () => {
});
describe("service tier API", () => {
it("restricts tiers to values supported by each provider family", () => {
expectTypeOf<"scale">().toExtend<ExtensionServiceTier<"openai">>();
expectTypeOf<"flex">().toExtend<ExtensionServiceTier<"google">>();
expectTypeOf<"priority">().toExtend<ExtensionServiceTier<"anthropic">>();
expectTypeOf<"scale">().not.toExtend<ExtensionServiceTier<"google">>();
expectTypeOf<"flex">().not.toExtend<ExtensionServiceTier<"anthropic">>();
});
it("returns a detached snapshot, forwards valid changes, and rejects invalid family tiers", async () => {
const extCode = `
export default function(pi) {
@@ -1246,8 +1250,13 @@ describe("ExtensionRunner", () => {
pi.appendEntry("service-tier-snapshot", tiers);
pi.setServiceTier("google", "flex");
pi.setServiceTier("openai", undefined);
});
pi.on("session_start", () => {
pi.setServiceTier("anthropic", "scale");
});
pi.on("session_start", () => {
pi.setServiceTier("bogus", "priority");
});
}
`;
const explicitExtensionPath = path.join(tempDir.path(), "service-tiers.ts");
@@ -1309,8 +1318,9 @@ describe("ExtensionRunner", () => {
["google", "flex"],
["openai", undefined],
]);
expect(errors).toHaveLength(1);
expect(errors).toHaveLength(2);
expect(errors[0]).toContain('Invalid service tier "scale" for family "anthropic"');
expect(errors[1]).toContain('Invalid service tier "priority" for family "bogus"');
});
});
@@ -1350,8 +1360,6 @@ describe("ExtensionRunner", () => {
setModel: async () => false,
getThinkingLevel: () => undefined,
setThinkingLevel: () => {},
getServiceTiers: () => ({}),
setServiceTier: () => {},
getSessionName: () => sessionManager.getSessionName(),
setSessionName: async name => {
await sessionManager.setSessionName(name);
@@ -1410,8 +1418,6 @@ describe("ExtensionRunner", () => {
setModel: async () => false,
getThinkingLevel: () => undefined,
setThinkingLevel: () => {},
getServiceTiers: () => ({}),
setServiceTier: () => {},
getSessionName: () => undefined,
setSessionName: async () => {},
},
@@ -2079,8 +2085,6 @@ describe("ExtensionRunner", () => {
setModel: async () => false,
getThinkingLevel: () => undefined,
setThinkingLevel: () => {},
getServiceTiers: () => ({}),
setServiceTier: () => {},
getSessionName: () => sessionManager.getSessionName(),
setSessionName: async () => {},
},
@@ -60,8 +60,6 @@ const initializeRunnerForTest = (runner: ExtensionRunner | undefined): void => {
setModel: async () => false,
getThinkingLevel: () => undefined,
setThinkingLevel: () => {},
getServiceTiers: () => ({}),
setServiceTier: () => {},
getSessionName: () => undefined,
setSessionName: async () => {},
},
@@ -529,8 +527,6 @@ describe("createAgentSession credential_disabled subscription", () => {
setModel: async () => false,
getThinkingLevel: () => undefined,
setThinkingLevel: () => {},
getServiceTiers: () => ({}),
setServiceTier: () => {},
getSessionName: () => undefined,
setSessionName: async () => {},
},