@d3ara1n/pi-model-roles 0.4.0 → 0.5.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/package.json +1 -1
- package/src/api.ts +43 -0
- package/src/index.ts +10 -0
- package/src/types.ts +52 -0
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Exported functions provide type-safe access — consumers never touch globalThis.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { complete as piAiComplete, streamSimple as piAiStreamSimple } from "@earendil-works/pi-ai";
|
|
9
10
|
import type { ModelRolesAPI, ModelRolesConfig, RoleConfig, ResolvedRole } from "./types.ts";
|
|
10
11
|
import { loadRolesConfig } from "./config.ts";
|
|
11
12
|
import { resolveModelForRole, resolveModelForRoleAsync } from "./resolver.ts";
|
|
@@ -147,6 +148,48 @@ export function initModelRolesAPI(
|
|
|
147
148
|
.getAvailable()
|
|
148
149
|
.map((m: { provider: string; id: string }) => `${m.provider}/${m.id}`);
|
|
149
150
|
},
|
|
151
|
+
|
|
152
|
+
async complete(roleName: string, context: any, options?: any): Promise<any> {
|
|
153
|
+
// Resolve model: explicit override wins, else the role's declared model
|
|
154
|
+
// (model=null transparently uses pi's current model).
|
|
155
|
+
const roleConfig = getConfig().roles[roleName] ?? { model: null };
|
|
156
|
+
const model =
|
|
157
|
+
options?.model ??
|
|
158
|
+
resolveModelForRole(roleConfig, state.modelRegistry, state.currentModel).model;
|
|
159
|
+
if (!model) {
|
|
160
|
+
throw new Error(`complete: role "${roleName}" has no available model`);
|
|
161
|
+
}
|
|
162
|
+
// Resolve auth for the model actually used (refreshes OAuth tokens).
|
|
163
|
+
const auth = await state.modelRegistry.getApiKeyAndHeaders(model);
|
|
164
|
+
if (!auth.ok) {
|
|
165
|
+
throw new Error(`complete: auth failed for ${model.provider}/${model.id}: ${auth.error}`);
|
|
166
|
+
}
|
|
167
|
+
// Forward everything except `model` to pi-ai's complete().
|
|
168
|
+
const { model: _omitModel, ...streamOptions } = options ?? {};
|
|
169
|
+
if (auth.apiKey) streamOptions.apiKey = auth.apiKey;
|
|
170
|
+
if (auth.headers) streamOptions.headers = auth.headers;
|
|
171
|
+
return piAiComplete(model, context, streamOptions);
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async streamWithRole(roleName: string, context: any, options?: any): Promise<any> {
|
|
175
|
+
const roleConfig = getConfig().roles[roleName] ?? { model: null };
|
|
176
|
+
const model =
|
|
177
|
+
options?.model ??
|
|
178
|
+
resolveModelForRole(roleConfig, state.modelRegistry, state.currentModel).model;
|
|
179
|
+
if (!model) {
|
|
180
|
+
throw new Error(`streamWithRole: role "${roleName}" has no available model`);
|
|
181
|
+
}
|
|
182
|
+
const auth = await state.modelRegistry.getApiKeyAndHeaders(model);
|
|
183
|
+
if (!auth.ok) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`streamWithRole: auth failed for ${model.provider}/${model.id}: ${auth.error}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const { model: _omitModel, ...streamOptions } = options ?? {};
|
|
189
|
+
if (auth.apiKey) streamOptions.apiKey = auth.apiKey;
|
|
190
|
+
if (auth.headers) streamOptions.headers = auth.headers;
|
|
191
|
+
return piAiStreamSimple(model, context, streamOptions);
|
|
192
|
+
},
|
|
150
193
|
};
|
|
151
194
|
|
|
152
195
|
// Store on globalThis — survives module identity mismatches
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,16 @@ export type {
|
|
|
17
17
|
ModelRolesConfig,
|
|
18
18
|
ThinkingLevel,
|
|
19
19
|
} from "./types.ts";
|
|
20
|
+
// Re-export pi-ai types so consumers depend on model-roles alone, not pi-ai.
|
|
21
|
+
export type {
|
|
22
|
+
Api,
|
|
23
|
+
AssistantMessage,
|
|
24
|
+
AssistantMessageEventStream,
|
|
25
|
+
Context,
|
|
26
|
+
Model,
|
|
27
|
+
ProviderStreamOptions,
|
|
28
|
+
SimpleStreamOptions,
|
|
29
|
+
} from "@earendil-works/pi-ai";
|
|
20
30
|
|
|
21
31
|
export default function registerModelRolesExtension(pi: ExtensionAPI): void {
|
|
22
32
|
pi.on("session_start", async (_event, ctx) => {
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
* Shared types for pi-model-roles.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type {
|
|
6
|
+
Api,
|
|
7
|
+
AssistantMessage,
|
|
8
|
+
AssistantMessageEventStream,
|
|
9
|
+
Context,
|
|
10
|
+
Model,
|
|
11
|
+
ProviderStreamOptions,
|
|
12
|
+
SimpleStreamOptions,
|
|
13
|
+
} from "@earendil-works/pi-ai";
|
|
14
|
+
|
|
5
15
|
/** Thinking level configuration for a role. */
|
|
6
16
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
7
17
|
|
|
@@ -97,4 +107,46 @@ export interface ModelRolesAPI {
|
|
|
97
107
|
getCurrentRole(modelId: string): string | undefined;
|
|
98
108
|
/** List all available models from pi's model registry. Returns provider/id strings (e.g. "anthropic/claude-sonnet-4"). */
|
|
99
109
|
listModels(): string[];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Call pi-ai's complete() with auth resolved internally from the role's model.
|
|
113
|
+
*
|
|
114
|
+
* Auth (including OAuth token refresh) is resolved for the model actually
|
|
115
|
+
* used, so callers never handle API keys or headers. The underlying call is
|
|
116
|
+
* pi-ai's complete() verbatim — no fallback, no retry, no error swallowing;
|
|
117
|
+
* throws if the role has no available model or auth resolution fails.
|
|
118
|
+
*
|
|
119
|
+
* @param roleName - Role whose model + auth to use
|
|
120
|
+
* @param context - Conversation context (systemPrompt + messages)
|
|
121
|
+
* @param options - Stream options forwarded to pi-ai's complete(). Pass
|
|
122
|
+
* `model` to override the role's model (auth is then resolved for that
|
|
123
|
+
* model); all other fields pass through unchanged.
|
|
124
|
+
*/
|
|
125
|
+
complete<TApi extends Api = Api>(
|
|
126
|
+
roleName: string,
|
|
127
|
+
context: Context,
|
|
128
|
+
options?: ProviderStreamOptions & { model?: Model<TApi> },
|
|
129
|
+
): Promise<AssistantMessage>;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Call pi-ai's streamSimple() with auth resolved internally from the role's model.
|
|
133
|
+
*
|
|
134
|
+
* Streaming counterpart to {@link complete}. Same auth resolution (including
|
|
135
|
+
* OAuth token refresh) and same error semantics (throws on no model / auth
|
|
136
|
+
* failure). Returns the pi-ai event stream verbatim — iterate it for events,
|
|
137
|
+
* call `.result()` for the final AssistantMessage.
|
|
138
|
+
*
|
|
139
|
+
* Note: unlike pi-ai's synchronous `streamSimple`, this is async because auth
|
|
140
|
+
* resolution (OAuth token refresh) must complete before the stream starts.
|
|
141
|
+
*
|
|
142
|
+
* @param roleName - Role whose model + auth to use
|
|
143
|
+
* @param context - Conversation context (systemPrompt + messages)
|
|
144
|
+
* @param options - Stream options forwarded to pi-ai's streamSimple(). Pass
|
|
145
|
+
* `model` to override the role's model; all other fields pass through.
|
|
146
|
+
*/
|
|
147
|
+
streamWithRole<TApi extends Api = Api>(
|
|
148
|
+
roleName: string,
|
|
149
|
+
context: Context,
|
|
150
|
+
options?: SimpleStreamOptions & { model?: Model<TApi> },
|
|
151
|
+
): Promise<AssistantMessageEventStream>;
|
|
100
152
|
}
|