@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 +4 -0
- package/package.json +1 -1
- package/src/api.ts +150 -136
- package/src/config.ts +49 -49
- package/src/defaults.ts +8 -4
- package/src/index.ts +56 -34
- package/src/resolver.ts +41 -39
- package/src/types.ts +2 -0
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
package/src/api.ts
CHANGED
|
@@ -14,142 +14,156 @@ const GLOBAL_KEY = "__piModelRoles";
|
|
|
14
14
|
|
|
15
15
|
/** Mutable state. */
|
|
16
16
|
interface APIState {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
config: ModelRolesConfig | undefined;
|
|
18
|
+
currentModel: any;
|
|
19
|
+
modelRegistry: any;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function initModelRolesAPI(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
56
|
-
|
|
55
|
+
// Read global settings
|
|
56
|
+
const globalSettings = readSettingsFile(path.join(getAgentDir(), "settings.json"));
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
61
|
+
// Merge: project overrides global
|
|
62
|
+
const settings = merge(globalSettings, projectSettings);
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
+
provider: string;
|
|
20
|
+
id: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
function parseModelIdentifier(modelRef: string): { provider: string | undefined; modelId: string } {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
roleConfig: RoleConfig,
|
|
44
|
+
modelRegistry: ModelRegistryLike,
|
|
45
|
+
currentModel: any | undefined,
|
|
44
46
|
): Pick<ResolvedRole, "model" | "apiKey" | "headers"> {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
roleConfig: RoleConfig,
|
|
61
|
+
modelRegistry: ModelRegistryLike,
|
|
62
|
+
currentModel: any | undefined,
|
|
61
63
|
): Promise<Pick<ResolvedRole, "model" | "apiKey" | "headers"> | undefined> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
const available = modelRegistry.getAvailable();
|
|
73
|
+
const match = findModel(roleConfig.model, available);
|
|
74
|
+
if (!match) return undefined;
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const auth = await modelRegistry.getApiKeyAndHeaders(match);
|
|
77
|
+
if (!auth.ok) return undefined;
|
|
76
78
|
|
|
77
|
-
|
|
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
|
}
|