diff --git a/packages/coding-agent/src/extensibility/extensions/runner.ts b/packages/coding-agent/src/extensibility/extensions/runner.ts index b0af988fb..43a37f1c3 100644 --- a/packages/coding-agent/src/extensibility/extensions/runner.ts +++ b/packages/coding-agent/src/extensibility/extensions/runner.ts @@ -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; diff --git a/packages/coding-agent/src/extensibility/extensions/types.ts b/packages/coding-agent/src/extensibility/extensions/types.ts index 1f782cc30..0d7493b68 100644 --- a/packages/coding-agent/src/extensibility/extensions/types.ts +++ b/packages/coding-agent/src/extensibility/extensions/types.ts @@ -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; } @@ -1488,8 +1488,11 @@ export interface ExtensionCommandContextActions { reload: () => Promise; } -/** 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 { diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index e750784f1..2d17d3ba3 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -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>(); + expectTypeOf<"flex">().toExtend>(); + expectTypeOf<"priority">().toExtend>(); + expectTypeOf<"scale">().not.toExtend>(); + expectTypeOf<"flex">().not.toExtend>(); + }); + 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 () => {}, }, diff --git a/packages/coding-agent/test/sdk-credential-disabled-bridge.test.ts b/packages/coding-agent/test/sdk-credential-disabled-bridge.test.ts index 1365dbbc1..6ec022e72 100644 --- a/packages/coding-agent/test/sdk-credential-disabled-bridge.test.ts +++ b/packages/coding-agent/test/sdk-credential-disabled-bridge.test.ts @@ -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 () => {}, },