@d3ara1n/pi-model-roles 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,10 @@ Defines named model roles (e.g. "heavy", "fast", "utility") and resolves them to
11
11
  - Exposes a `ModelRolesAPI` singleton for other extensions to consume via direct import
12
12
  - **Pure library**: no tools, no commands, no event hooks
13
13
 
14
+ ## Dependencies
15
+
16
+ None.
17
+
14
18
  ## Installation
15
19
 
16
20
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-model-roles",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Model role configuration library for pi extensions — defines named model roles and resolves them to Model instances",
6
6
  "main": "src/index.ts",
package/src/api.ts CHANGED
@@ -14,142 +14,156 @@ const GLOBAL_KEY = "__piModelRoles";
14
14
 
15
15
  /** Mutable state. */
16
16
  interface APIState {
17
- config: ModelRolesConfig | undefined;
18
- currentModel: any;
19
- modelRegistry: any;
17
+ config: ModelRolesConfig | undefined;
18
+ currentModel: any;
19
+ modelRegistry: any;
20
20
  }
21
21
 
22
- export function initModelRolesAPI(modelRegistry: any, currentModel: any, cwd?: string): ModelRolesAPI {
23
- const state: APIState = {
24
- config: undefined,
25
- currentModel,
26
- modelRegistry,
27
- };
28
-
29
- function getConfig(): ModelRolesConfig {
30
- if (!state.config) {
31
- state.config = loadRolesConfig(cwd);
32
- }
33
- return state.config;
34
- }
35
-
36
- const api: ModelRolesAPI = {
37
- getRoles(): Record<string, RoleConfig> {
38
- return getConfig().roles;
39
- },
40
-
41
- getRole(name: string): RoleConfig | undefined {
42
- return getConfig().roles[name];
43
- },
44
-
45
- resolveRole(name: string): ResolvedRole {
46
- const roleConfig = getConfig().roles[name];
47
- if (!roleConfig) {
48
- return {
49
- name,
50
- config: { model: null },
51
- model: state.currentModel,
52
- apiKey: undefined,
53
- headers: undefined,
54
- };
55
- }
56
-
57
- const resolved = resolveModelForRole(roleConfig, state.modelRegistry, state.currentModel);
58
- return { name, config: roleConfig, ...resolved };
59
- },
60
-
61
- async resolveRoleAsync(name: string): Promise<ResolvedRole> {
62
- const roleConfig = getConfig().roles[name];
63
- if (!roleConfig) {
64
- if (state.currentModel) {
65
- const auth = await state.modelRegistry.getApiKeyAndHeaders(state.currentModel);
66
- return {
67
- name,
68
- config: { model: null },
69
- model: state.currentModel,
70
- apiKey: auth.ok ? auth.apiKey : undefined,
71
- headers: auth.ok ? auth.headers : undefined,
72
- };
73
- }
74
- return {
75
- name,
76
- config: { model: null },
77
- model: undefined,
78
- apiKey: undefined,
79
- headers: undefined,
80
- };
81
- }
82
-
83
- const resolved = await resolveModelForRoleAsync(roleConfig, state.modelRegistry, state.currentModel);
84
- if (!resolved) {
85
- return {
86
- name,
87
- config: roleConfig,
88
- model: undefined,
89
- apiKey: undefined,
90
- headers: undefined,
91
- };
92
- }
93
-
94
- return { name, config: roleConfig, ...resolved };
95
- },
96
-
97
- getDefaultRole(): string {
98
- return getConfig().defaultRole ?? "default";
99
- },
100
-
101
- getVisibleRoles(): Record<string, RoleConfig> {
102
- const roles = getConfig().roles;
103
- const result: Record<string, RoleConfig> = {};
104
- for (const [name, config] of Object.entries(roles)) {
105
- if (!config.hidden) {
106
- result[name] = config;
107
- }
108
- }
109
- return result;
110
- },
111
-
112
- findRoleByModel(modelId: string): string | undefined {
113
- const roles = getConfig().roles;
114
- for (const [name, config] of Object.entries(roles)) {
115
- if (config.model === modelId) {
116
- return name;
117
- }
118
- }
119
- return undefined;
120
- },
121
-
122
- getCurrentRole(modelId: string): string | undefined {
123
- const roles = getConfig().roles;
124
- // 1. Exact match wins: a role explicitly bound to this model.
125
- const exact = Object.entries(roles).find(([, c]) => c.model === modelId)?.[0];
126
- if (exact) return exact;
127
- // 2. Default role — when model=null it transparently uses the current
128
- // model, so it is the meaningful base in the all-null config.
129
- const defaultName = getConfig().defaultRole ?? "default";
130
- const defaultConfig = roles[defaultName];
131
- if (defaultConfig && !defaultConfig.model) return defaultName;
132
- // 3. First model=null role (reached only when default is bound elsewhere).
133
- const nullRole = Object.entries(roles).find(([, c]) => !c.model)?.[0];
134
- return nullRole;
135
- },
136
- };
137
-
138
- // Store on globalThis — survives module identity mismatches
139
- (globalThis as any)[GLOBAL_KEY] = api;
140
- return api;
22
+ export function initModelRolesAPI(
23
+ modelRegistry: any,
24
+ currentModel: any,
25
+ cwd?: string,
26
+ ): ModelRolesAPI {
27
+ const state: APIState = {
28
+ config: undefined,
29
+ currentModel,
30
+ modelRegistry,
31
+ };
32
+
33
+ function getConfig(): ModelRolesConfig {
34
+ if (!state.config) {
35
+ state.config = loadRolesConfig(cwd);
36
+ }
37
+ return state.config;
38
+ }
39
+
40
+ const api: ModelRolesAPI = {
41
+ getRoles(): Record<string, RoleConfig> {
42
+ return getConfig().roles;
43
+ },
44
+
45
+ getRole(name: string): RoleConfig | undefined {
46
+ return getConfig().roles[name];
47
+ },
48
+
49
+ resolveRole(name: string): ResolvedRole {
50
+ const roleConfig = getConfig().roles[name];
51
+ if (!roleConfig) {
52
+ return {
53
+ name,
54
+ config: { model: null },
55
+ model: state.currentModel,
56
+ apiKey: undefined,
57
+ headers: undefined,
58
+ };
59
+ }
60
+
61
+ const resolved = resolveModelForRole(roleConfig, state.modelRegistry, state.currentModel);
62
+ return { name, config: roleConfig, ...resolved };
63
+ },
64
+
65
+ async resolveRoleAsync(name: string): Promise<ResolvedRole> {
66
+ const roleConfig = getConfig().roles[name];
67
+ if (!roleConfig) {
68
+ if (state.currentModel) {
69
+ const auth = await state.modelRegistry.getApiKeyAndHeaders(state.currentModel);
70
+ return {
71
+ name,
72
+ config: { model: null },
73
+ model: state.currentModel,
74
+ apiKey: auth.ok ? auth.apiKey : undefined,
75
+ headers: auth.ok ? auth.headers : undefined,
76
+ };
77
+ }
78
+ return {
79
+ name,
80
+ config: { model: null },
81
+ model: undefined,
82
+ apiKey: undefined,
83
+ headers: undefined,
84
+ };
85
+ }
86
+
87
+ const resolved = await resolveModelForRoleAsync(
88
+ roleConfig,
89
+ state.modelRegistry,
90
+ state.currentModel,
91
+ );
92
+ if (!resolved) {
93
+ return {
94
+ name,
95
+ config: roleConfig,
96
+ model: undefined,
97
+ apiKey: undefined,
98
+ headers: undefined,
99
+ };
100
+ }
101
+
102
+ return { name, config: roleConfig, ...resolved };
103
+ },
104
+
105
+ getDefaultRole(): string {
106
+ return getConfig().defaultRole ?? "default";
107
+ },
108
+
109
+ getVisibleRoles(): Record<string, RoleConfig> {
110
+ const roles = getConfig().roles;
111
+ const result: Record<string, RoleConfig> = {};
112
+ for (const [name, config] of Object.entries(roles)) {
113
+ if (!config.hidden) {
114
+ result[name] = config;
115
+ }
116
+ }
117
+ return result;
118
+ },
119
+
120
+ findRoleByModel(modelId: string): string | undefined {
121
+ const roles = getConfig().roles;
122
+ for (const [name, config] of Object.entries(roles)) {
123
+ if (config.model === modelId) {
124
+ return name;
125
+ }
126
+ }
127
+ return undefined;
128
+ },
129
+
130
+ getCurrentRole(modelId: string): string | undefined {
131
+ const roles = getConfig().roles;
132
+ // 1. Exact match wins: a role explicitly bound to this model.
133
+ const exact = Object.entries(roles).find(([, c]) => c.model === modelId)?.[0];
134
+ if (exact) return exact;
135
+ // 2. Default role — when model=null it transparently uses the current
136
+ // model, so it is the meaningful base in the all-null config.
137
+ const defaultName = getConfig().defaultRole ?? "default";
138
+ const defaultConfig = roles[defaultName];
139
+ if (defaultConfig && !defaultConfig.model) return defaultName;
140
+ // 3. First model=null role (reached only when default is bound elsewhere).
141
+ const nullRole = Object.entries(roles).find(([, c]) => !c.model)?.[0];
142
+ return nullRole;
143
+ },
144
+
145
+ listModels(): string[] {
146
+ return state.modelRegistry
147
+ .getAvailable()
148
+ .map((m: { provider: string; id: string }) => `${m.provider}/${m.id}`);
149
+ },
150
+ };
151
+
152
+ // Store on globalThis — survives module identity mismatches
153
+ (globalThis as any)[GLOBAL_KEY] = api;
154
+ return api;
141
155
  }
142
156
 
143
157
  /**
144
158
  * Update the tracked current model.
145
159
  */
146
160
  export function updateCurrentModel(model: any): void {
147
- const api = (globalThis as any)[GLOBAL_KEY] as ModelRolesAPI | undefined;
148
- if (!api) return;
149
- const state = (api as any).__state as APIState | undefined;
150
- if (state) {
151
- state.currentModel = model;
152
- }
161
+ const api = (globalThis as any)[GLOBAL_KEY] as ModelRolesAPI | undefined;
162
+ if (!api) return;
163
+ const state = (api as any).__state as APIState | undefined;
164
+ if (state) {
165
+ state.currentModel = model;
166
+ }
153
167
  }
154
168
 
155
169
  /**
@@ -157,12 +171,12 @@ export function updateCurrentModel(model: any): void {
157
171
  * Throws if initModelRolesAPI() has not been called yet.
158
172
  */
159
173
  export function getModelRolesAPI(): ModelRolesAPI {
160
- const api = (globalThis as any)[GLOBAL_KEY] as ModelRolesAPI | undefined;
161
- if (!api) {
162
- throw new Error(
163
- "ModelRolesAPI not initialized. " +
164
- "Ensure @d3ara1n/pi-model-roles extension is loaded and session_start has fired.",
165
- );
166
- }
167
- return api;
174
+ const api = (globalThis as any)[GLOBAL_KEY] as ModelRolesAPI | undefined;
175
+ if (!api) {
176
+ throw new Error(
177
+ "ModelRolesAPI not initialized. " +
178
+ "Ensure @d3ara1n/pi-model-roles extension is loaded and session_start has fired.",
179
+ );
180
+ }
181
+ return api;
168
182
  }
package/src/config.ts CHANGED
@@ -16,35 +16,35 @@ const DEFAULT_ROLE_NAME = "default";
16
16
 
17
17
  /** Get the pi agent directory path. */
18
18
  function getAgentDir(): string {
19
- const envDir = process.env.PI_AGENT_DIR;
20
- if (envDir) return envDir;
21
- return path.join(os.homedir(), ".pi", "agent");
19
+ const envDir = process.env.PI_AGENT_DIR;
20
+ if (envDir) return envDir;
21
+ return path.join(os.homedir(), ".pi", "agent");
22
22
  }
23
23
 
24
24
  /** Read and parse a settings.json file. Returns parsed object or empty. */
25
25
  function readSettingsFile(filePath: string): any {
26
- try {
27
- if (!fs.existsSync(filePath)) return {};
28
- const content = fs.readFileSync(filePath, "utf-8");
29
- return JSON.parse(content);
30
- } catch {
31
- return {};
32
- }
26
+ try {
27
+ if (!fs.existsSync(filePath)) return {};
28
+ const content = fs.readFileSync(filePath, "utf-8");
29
+ return JSON.parse(content);
30
+ } catch {
31
+ return {};
32
+ }
33
33
  }
34
34
 
35
35
  /** Deep merge source into target (source wins on conflict). Only handles plain objects. */
36
36
  function merge(target: any, source: any): any {
37
- if (!source || typeof source !== "object") return target;
38
- if (!target || typeof target !== "object") return source;
39
- const result = { ...target };
40
- for (const key of Object.keys(source)) {
41
- if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
42
- result[key] = merge(result[key], source[key]);
43
- } else {
44
- result[key] = source[key];
45
- }
46
- }
47
- return result;
37
+ if (!source || typeof source !== "object") return target;
38
+ if (!target || typeof target !== "object") return source;
39
+ const result = { ...target };
40
+ for (const key of Object.keys(source)) {
41
+ if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
42
+ result[key] = merge(result[key], source[key]);
43
+ } else {
44
+ result[key] = source[key];
45
+ }
46
+ }
47
+ return result;
48
48
  }
49
49
 
50
50
  /**
@@ -52,37 +52,37 @@ function merge(target: any, source: any): any {
52
52
  * @param cwd - Project working directory (for .pi/settings.json lookup)
53
53
  */
54
54
  export function loadRolesConfig(cwd?: string): ModelRolesConfig {
55
- // Read global settings
56
- const globalSettings = readSettingsFile(path.join(getAgentDir(), "settings.json"));
55
+ // Read global settings
56
+ const globalSettings = readSettingsFile(path.join(getAgentDir(), "settings.json"));
57
57
 
58
- // Read project settings
59
- const projectSettings = cwd
60
- ? readSettingsFile(path.join(cwd, ".pi", "settings.json"))
61
- : {};
58
+ // Read project settings
59
+ const projectSettings = cwd ? readSettingsFile(path.join(cwd, ".pi", "settings.json")) : {};
62
60
 
63
- // Merge: project overrides global
64
- const settings = merge(globalSettings, projectSettings);
61
+ // Merge: project overrides global
62
+ const settings = merge(globalSettings, projectSettings);
65
63
 
66
- // Start from built-in defaults
67
- const mergedRoles: Record<string, RoleConfig> = {};
68
- for (const [name, config] of Object.entries(BUILTIN_DEFAULT_ROLES)) {
69
- mergedRoles[name] = { ...config };
70
- }
64
+ // Start from built-in defaults
65
+ const mergedRoles: Record<string, RoleConfig> = {};
66
+ for (const [name, config] of Object.entries(BUILTIN_DEFAULT_ROLES)) {
67
+ mergedRoles[name] = { ...config };
68
+ }
71
69
 
72
- // Merge user config over defaults
73
- const userConfig = settings?.modelRoles;
74
- if (userConfig?.roles && typeof userConfig.roles === "object") {
75
- for (const [name, config] of Object.entries(userConfig.roles as Record<string, Partial<RoleConfig>>)) {
76
- if (mergedRoles[name]) {
77
- mergedRoles[name] = { ...mergedRoles[name], ...config };
78
- } else {
79
- mergedRoles[name] = config as RoleConfig;
80
- }
81
- }
82
- }
70
+ // Merge user config over defaults
71
+ const userConfig = settings?.modelRoles;
72
+ if (userConfig?.roles && typeof userConfig.roles === "object") {
73
+ for (const [name, config] of Object.entries(
74
+ userConfig.roles as Record<string, Partial<RoleConfig>>,
75
+ )) {
76
+ if (mergedRoles[name]) {
77
+ mergedRoles[name] = { ...mergedRoles[name], ...config };
78
+ } else {
79
+ mergedRoles[name] = config as RoleConfig;
80
+ }
81
+ }
82
+ }
83
83
 
84
- return {
85
- roles: mergedRoles,
86
- defaultRole: userConfig?.defaultRole ?? DEFAULT_ROLE_NAME,
87
- };
84
+ return {
85
+ roles: mergedRoles,
86
+ defaultRole: userConfig?.defaultRole ?? DEFAULT_ROLE_NAME,
87
+ };
88
88
  }
package/src/defaults.ts CHANGED
@@ -13,22 +13,26 @@ import type { RoleConfig } from "./types.ts";
13
13
  export const BUILTIN_DEFAULT_ROLES: Record<string, RoleConfig> = {
14
14
  default: {
15
15
  model: null,
16
- description: "General development tasks: writing new features, modifying existing code, code review, adding tests, routine debugging, file-level changes",
16
+ description:
17
+ "General development tasks: writing new features, modifying existing code, code review, adding tests, routine debugging, file-level changes",
17
18
  thinking: "medium",
18
19
  },
19
20
  heavy: {
20
21
  model: null,
21
- description: "Tasks requiring deep thinking: cross-file refactoring, architecture design, complex bug debugging, performance optimization, security analysis, database schema changes, multi-module migrations",
22
+ description:
23
+ "Tasks requiring deep thinking: cross-file refactoring, architecture design, complex bug debugging, performance optimization, security analysis, database schema changes, multi-module migrations",
22
24
  thinking: "high",
23
25
  },
24
26
  fast: {
25
27
  model: null,
26
- description: "Simple deterministic tasks: one-line edits, formatting tweaks, simple Q&A, doc lookups, git operations, confirmation replies",
28
+ description:
29
+ "Simple deterministic tasks: one-line edits, formatting tweaks, simple Q&A, doc lookups, git operations, confirmation replies",
27
30
  thinking: "low",
28
31
  },
29
32
  utility: {
30
33
  model: null,
31
- description: "Lightweight utilities: model routing, commit generation, title/summary, etc. (hidden)",
34
+ description:
35
+ "Lightweight utilities: model routing, commit generation, title/summary, etc. (hidden)",
32
36
  thinking: "off",
33
37
  hidden: true,
34
38
  },
package/src/index.ts CHANGED
@@ -6,43 +6,65 @@
6
6
  */
7
7
 
8
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+ import { Type } from "typebox";
9
10
  import { initModelRolesAPI, getModelRolesAPI, updateCurrentModel } from "./api.ts";
10
11
 
11
12
  export { getModelRolesAPI } from "./api.ts";
12
- export type { ModelRolesAPI, RoleConfig, ResolvedRole, ModelRolesConfig, ThinkingLevel } from "./types.ts";
13
+ export type {
14
+ ModelRolesAPI,
15
+ RoleConfig,
16
+ ResolvedRole,
17
+ ModelRolesConfig,
18
+ ThinkingLevel,
19
+ } from "./types.ts";
13
20
 
14
21
  export default function registerModelRolesExtension(pi: ExtensionAPI): void {
15
- pi.on("session_start", async (_event, ctx) => {
16
- initModelRolesAPI(ctx.modelRegistry, ctx.model, ctx.cwd);
17
- });
18
-
19
- pi.on("model_select", async (event) => {
20
- updateCurrentModel(event.model);
21
- });
22
-
23
- pi.registerCommand("roles", {
24
- description: "Show model role definitions and resolved models",
25
- handler: async (_args, ctx) => {
26
- const api = getModelRolesAPI();
27
- const roles = api.getRoles();
28
- const lines: string[] = ["Model Roles:", ""];
29
-
30
- for (const [name, config] of Object.entries(roles)) {
31
- const resolved = api.resolveRole(name);
32
- const hidden = config.hidden ? " (hidden)" : "";
33
- const modelLabel = resolved.model
34
- ? `${resolved.model.provider}/${resolved.model.id}`
35
- : config.model === null
36
- ? "→ current model"
37
- : `→ NOT FOUND (${config.model})`;
38
- const thinking = config.thinking ? ` thinking:${config.thinking}` : "";
39
- lines.push(` ${name}: ${modelLabel}${thinking}${hidden}`);
40
- }
41
-
42
- lines.push("");
43
- lines.push(`Default role: ${api.getDefaultRole()}`);
44
-
45
- ctx.ui.notify(lines.join("\n"), "info");
46
- },
47
- });
22
+ pi.on("session_start", async (_event, ctx) => {
23
+ initModelRolesAPI(ctx.modelRegistry, ctx.model, ctx.cwd);
24
+ });
25
+
26
+ pi.on("model_select", async (event) => {
27
+ updateCurrentModel(event.model);
28
+ });
29
+
30
+ pi.registerTool({
31
+ name: "list_models",
32
+ label: "List available models",
33
+ description: "List all available models from pi's model registry. Returns provider/model-id strings (e.g. 'anthropic/claude-sonnet-4'). Useful for confirming model IDs before referencing a model by name.",
34
+ parameters: Type.Object({}),
35
+ async execute() {
36
+ const api = getModelRolesAPI();
37
+ const models = api.listModels();
38
+ return {
39
+ content: [{ type: "text", text: models.join("\n") }],
40
+ details: undefined as any,
41
+ };
42
+ },
43
+ });
44
+
45
+ pi.registerCommand("roles", {
46
+ description: "Show model role definitions and resolved models",
47
+ handler: async (_args, ctx) => {
48
+ const api = getModelRolesAPI();
49
+ const roles = api.getRoles();
50
+ const lines: string[] = ["Model Roles:", ""];
51
+
52
+ for (const [name, config] of Object.entries(roles)) {
53
+ const resolved = api.resolveRole(name);
54
+ const hidden = config.hidden ? " (hidden)" : "";
55
+ const modelLabel = resolved.model
56
+ ? `${resolved.model.provider}/${resolved.model.id}`
57
+ : config.model === null
58
+ ? "→ current model"
59
+ : `→ NOT FOUND (${config.model})`;
60
+ const thinking = config.thinking ? ` thinking:${config.thinking}` : "";
61
+ lines.push(` ${name}: ${modelLabel}${thinking}${hidden}`);
62
+ }
63
+
64
+ lines.push("");
65
+ lines.push(`Default role: ${api.getDefaultRole()}`);
66
+
67
+ ctx.ui.notify(lines.join("\n"), "info");
68
+ },
69
+ });
48
70
  }
package/src/resolver.ts CHANGED
@@ -9,70 +9,72 @@ import type { RoleConfig, ResolvedRole } from "./types.ts";
9
9
 
10
10
  /** Minimal interface from ModelRegistry. */
11
11
  interface ModelRegistryLike {
12
- getAvailable(): any[];
13
- getApiKeyAndHeaders(model: any): Promise<{ ok: boolean; apiKey?: string; headers?: Record<string, string> }>;
12
+ getAvailable(): any[];
13
+ getApiKeyAndHeaders(
14
+ model: any,
15
+ ): Promise<{ ok: boolean; apiKey?: string; headers?: Record<string, string> }>;
14
16
  }
15
17
 
16
18
  interface ModelLike {
17
- provider: string;
18
- id: string;
19
+ provider: string;
20
+ id: string;
19
21
  }
20
22
 
21
23
  function parseModelIdentifier(modelRef: string): { provider: string | undefined; modelId: string } {
22
- const parts = modelRef.split("/");
23
- if (parts.length > 1) {
24
- return { provider: parts[0], modelId: parts.slice(1).join("/") };
25
- }
26
- return { provider: undefined, modelId: parts[0] };
24
+ const parts = modelRef.split("/");
25
+ if (parts.length > 1) {
26
+ return { provider: parts[0], modelId: parts.slice(1).join("/") };
27
+ }
28
+ return { provider: undefined, modelId: parts[0] };
27
29
  }
28
30
 
29
31
  function findModel(modelRef: string, available: ModelLike[]): ModelLike | undefined {
30
- const { provider, modelId } = parseModelIdentifier(modelRef);
31
- return available.find((m) => {
32
- if (provider) return m.provider === provider && m.id === modelId;
33
- return m.id === modelId;
34
- });
32
+ const { provider, modelId } = parseModelIdentifier(modelRef);
33
+ return available.find((m) => {
34
+ if (provider) return m.provider === provider && m.id === modelId;
35
+ return m.id === modelId;
36
+ });
35
37
  }
36
38
 
37
39
  /**
38
40
  * Sync resolve. model=null → uses currentModel if provided, else undefined.
39
41
  */
40
42
  export function resolveModelForRole(
41
- roleConfig: RoleConfig,
42
- modelRegistry: ModelRegistryLike,
43
- currentModel: any | undefined,
43
+ roleConfig: RoleConfig,
44
+ modelRegistry: ModelRegistryLike,
45
+ currentModel: any | undefined,
44
46
  ): Pick<ResolvedRole, "model" | "apiKey" | "headers"> {
45
- // model=null: fill with pi's current model
46
- if (!roleConfig.model) {
47
- return { model: currentModel, apiKey: undefined, headers: undefined };
48
- }
47
+ // model=null: fill with pi's current model
48
+ if (!roleConfig.model) {
49
+ return { model: currentModel, apiKey: undefined, headers: undefined };
50
+ }
49
51
 
50
- const match = findModel(roleConfig.model, modelRegistry.getAvailable());
51
- return { model: match ?? undefined, apiKey: undefined, headers: undefined };
52
+ const match = findModel(roleConfig.model, modelRegistry.getAvailable());
53
+ return { model: match ?? undefined, apiKey: undefined, headers: undefined };
52
54
  }
53
55
 
54
56
  /**
55
57
  * Async resolve with auth. model=null → resolves currentModel's auth too.
56
58
  */
57
59
  export async function resolveModelForRoleAsync(
58
- roleConfig: RoleConfig,
59
- modelRegistry: ModelRegistryLike,
60
- currentModel: any | undefined,
60
+ roleConfig: RoleConfig,
61
+ modelRegistry: ModelRegistryLike,
62
+ currentModel: any | undefined,
61
63
  ): Promise<Pick<ResolvedRole, "model" | "apiKey" | "headers"> | undefined> {
62
- // model=null: fill with pi's current model + its auth
63
- if (!roleConfig.model) {
64
- if (!currentModel) return undefined;
65
- const auth = await modelRegistry.getApiKeyAndHeaders(currentModel);
66
- if (!auth.ok) return undefined;
67
- return { model: currentModel, apiKey: auth.apiKey, headers: auth.headers };
68
- }
64
+ // model=null: fill with pi's current model + its auth
65
+ if (!roleConfig.model) {
66
+ if (!currentModel) return undefined;
67
+ const auth = await modelRegistry.getApiKeyAndHeaders(currentModel);
68
+ if (!auth.ok) return undefined;
69
+ return { model: currentModel, apiKey: auth.apiKey, headers: auth.headers };
70
+ }
69
71
 
70
- const available = modelRegistry.getAvailable();
71
- const match = findModel(roleConfig.model, available);
72
- if (!match) return undefined;
72
+ const available = modelRegistry.getAvailable();
73
+ const match = findModel(roleConfig.model, available);
74
+ if (!match) return undefined;
73
75
 
74
- const auth = await modelRegistry.getApiKeyAndHeaders(match);
75
- if (!auth.ok) return undefined;
76
+ const auth = await modelRegistry.getApiKeyAndHeaders(match);
77
+ if (!auth.ok) return undefined;
76
78
 
77
- return { model: match, apiKey: auth.apiKey, headers: auth.headers };
79
+ return { model: match, apiKey: auth.apiKey, headers: auth.headers };
78
80
  }
package/src/types.ts CHANGED
@@ -95,4 +95,6 @@ export interface ModelRolesAPI {
95
95
  * Returns undefined only if no role matches by any rule.
96
96
  */
97
97
  getCurrentRole(modelId: string): string | undefined;
98
+ /** List all available models from pi's model registry. Returns provider/id strings (e.g. "anthropic/claude-sonnet-4"). */
99
+ listModels(): string[];
98
100
  }