@d3ara1n/pi-model-roles 0.4.0 → 0.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-model-roles",
3
- "version": "0.4.0",
3
+ "version": "0.6.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
@@ -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,56 @@ 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
+ // Apply the role's thinking level unless the caller explicitly overrides it.
172
+ if (roleConfig.thinking && streamOptions.reasoningEffort === undefined) {
173
+ streamOptions.reasoningEffort = roleConfig.thinking;
174
+ }
175
+ return piAiComplete(model, context, streamOptions);
176
+ },
177
+
178
+ async streamWithRole(roleName: string, context: any, options?: any): Promise<any> {
179
+ const roleConfig = getConfig().roles[roleName] ?? { model: null };
180
+ const model =
181
+ options?.model ??
182
+ resolveModelForRole(roleConfig, state.modelRegistry, state.currentModel).model;
183
+ if (!model) {
184
+ throw new Error(`streamWithRole: role "${roleName}" has no available model`);
185
+ }
186
+ const auth = await state.modelRegistry.getApiKeyAndHeaders(model);
187
+ if (!auth.ok) {
188
+ throw new Error(
189
+ `streamWithRole: auth failed for ${model.provider}/${model.id}: ${auth.error}`,
190
+ );
191
+ }
192
+ const { model: _omitModel, ...streamOptions } = options ?? {};
193
+ if (auth.apiKey) streamOptions.apiKey = auth.apiKey;
194
+ if (auth.headers) streamOptions.headers = auth.headers;
195
+ // Apply the role's thinking level unless the caller explicitly overrides it.
196
+ if (roleConfig.thinking && streamOptions.reasoning === undefined) {
197
+ streamOptions.reasoning = roleConfig.thinking;
198
+ }
199
+ return piAiStreamSimple(model, context, streamOptions);
200
+ },
150
201
  };
151
202
 
152
203
  // 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,51 @@ 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 role's
116
+ * {@link RoleConfig.thinking} level is applied as `reasoningEffort` unless
117
+ * the caller sets it explicitly. No fallback, no retry, no error swallowing;
118
+ * throws if the role has no available model or auth resolution fails.
119
+ *
120
+ * @param roleName - Role whose model + auth + thinking to use
121
+ * @param context - Conversation context (systemPrompt + messages)
122
+ * @param options - Stream options forwarded to pi-ai's complete(). Pass
123
+ * `model` to override the role's model (auth is then resolved for that
124
+ * model); pass `reasoningEffort` to override the role's thinking level;
125
+ * all other fields pass through unchanged.
126
+ */
127
+ complete<TApi extends Api = Api>(
128
+ roleName: string,
129
+ context: Context,
130
+ options?: ProviderStreamOptions & { model?: Model<TApi> },
131
+ ): Promise<AssistantMessage>;
132
+
133
+ /**
134
+ * Call pi-ai's streamSimple() with auth resolved internally from the role's model.
135
+ *
136
+ * Streaming counterpart to {@link complete}. Same auth resolution (including
137
+ * OAuth token refresh), same thinking-level application (role's
138
+ * {@link RoleConfig.thinking} → `reasoning` unless overridden), and same
139
+ * error semantics (throws on no model / auth failure). Returns the pi-ai
140
+ * event stream — iterate it for events, call `.result()` for the final
141
+ * AssistantMessage.
142
+ *
143
+ * Note: unlike pi-ai's synchronous `streamSimple`, this is async because auth
144
+ * resolution (OAuth token refresh) must complete before the stream starts.
145
+ *
146
+ * @param roleName - Role whose model + auth + thinking to use
147
+ * @param context - Conversation context (systemPrompt + messages)
148
+ * @param options - Stream options forwarded to pi-ai's streamSimple(). Pass
149
+ * `model` to override the role's model; pass `reasoning` to override the
150
+ * role's thinking level; all other fields pass through.
151
+ */
152
+ streamWithRole<TApi extends Api = Api>(
153
+ roleName: string,
154
+ context: Context,
155
+ options?: SimpleStreamOptions & { model?: Model<TApi> },
156
+ ): Promise<AssistantMessageEventStream>;
100
157
  }