@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-model-roles",
3
- "version": "0.4.0",
3
+ "version": "0.5.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,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
  }