@2tle/pi-provider-manager 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/index.ts +55 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -86,6 +86,24 @@ The extension targets OpenAI Chat Completions-compatible services that provide a
86
86
 
87
87
  Provider implementations differ in support for developer roles, reasoning options, token fields, and streaming usage. This extension currently uses Pi's standard `openai-completions` transport.
88
88
 
89
+ ### Thinking / reasoning
90
+
91
+ During each catalog refresh, the extension recognizes OpenAI-compatible reasoning metadata from either top-level fields or `capabilities`:
92
+
93
+ - `supports_reasoning`, `supports_reasoning_effort`, or `capabilities.supports_reasoning`
94
+ - `reasoning_efforts` or `capabilities.reasoning_effort` (an array of strings or `{ "value": "..." }` objects)
95
+
96
+ Reasoning-capable models are registered with Pi's `reasoning_effort` compatibility enabled. Supported effort values are exposed as Pi thinking levels; unavailable levels are hidden. An upstream `ultra` effort is mapped to Pi's highest available level, `max`.
97
+
98
+ For example, after `/provider reload opencodex`, select a discovered reasoning model with a thinking suffix:
99
+
100
+ ```text
101
+ /provider reload opencodex
102
+ # Then select: opencodex/gpt-5.6-sol:max
103
+ ```
104
+
105
+ An endpoint must actually accept the OpenAI Chat Completions `reasoning_effort` request field. Providers that use a different thinking protocol (for example, Qwen's `enable_thinking`) are outside this extension's OpenAI-compatible transport scope.
106
+
89
107
  ## Stored configuration
90
108
 
91
109
  Provider metadata and API keys are stored separately:
package/index.ts CHANGED
@@ -11,6 +11,8 @@ const API = "openai-completions" as const;
11
11
  const DEFAULT_CONTEXT_WINDOW = 128_000;
12
12
  const DEFAULT_MAX_TOKENS = 16_384;
13
13
  const REFRESH_TIMEOUT_MS = 30_000;
14
+ const PI_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
15
+ type PiThinkingLevel = (typeof PI_THINKING_LEVELS)[number];
14
16
 
15
17
  interface StoredProvider {
16
18
  id: string;
@@ -31,6 +33,10 @@ interface ProviderModelConfig {
31
33
  name: string;
32
34
  api: typeof API;
33
35
  reasoning: boolean;
36
+ thinkingLevelMap?: Partial<Record<PiThinkingLevel, string | null>>;
37
+ compat?: {
38
+ supportsReasoningEffort: boolean;
39
+ };
34
40
  input: ("text" | "image")[];
35
41
  cost: {
36
42
  input: number;
@@ -60,8 +66,11 @@ interface OpenAIModelPayload {
60
66
  maxTokens?: unknown;
61
67
  reasoning?: unknown;
62
68
  supports_reasoning?: unknown;
69
+ supports_reasoning_effort?: unknown;
70
+ reasoning_efforts?: unknown;
63
71
  input?: unknown;
64
72
  cost?: unknown;
73
+ capabilities?: unknown;
65
74
  }
66
75
 
67
76
  interface OpenAIModelsPayload {
@@ -80,6 +89,27 @@ function asPositiveNumber(value: unknown, fallback: number): number {
80
89
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
81
90
  }
82
91
 
92
+ function getReasoningEfforts(value: unknown): Set<string> {
93
+ if (!Array.isArray(value)) return new Set();
94
+ return new Set(value.flatMap((item) => {
95
+ if (typeof item === "string") return [item.toLowerCase()];
96
+ const effort = asNonEmptyString(asRecord(item)?.value);
97
+ return effort ? [effort.toLowerCase()] : [];
98
+ }));
99
+ }
100
+
101
+ function thinkingLevelMap(efforts: Set<string>): Partial<Record<PiThinkingLevel, string | null>> | undefined {
102
+ if (efforts.size === 0) return undefined;
103
+
104
+ const map: Partial<Record<PiThinkingLevel, string | null>> = {};
105
+ for (const level of PI_THINKING_LEVELS) {
106
+ if (efforts.has(level)) map[level] = level;
107
+ else if (level === "max" && efforts.has("ultra")) map[level] = "ultra";
108
+ else map[level] = null;
109
+ }
110
+ return map;
111
+ }
112
+
83
113
  function normalizeBaseUrl(value: string): string {
84
114
  return value.trim().replace(/\/+$/, "");
85
115
  }
@@ -182,15 +212,30 @@ function modelFromPayload(
182
212
  const id = asNonEmptyString(payload.id);
183
213
  if (!id) return undefined;
184
214
 
215
+ const capabilities = asRecord(payload.capabilities);
185
216
  const cost = asRecord(payload.cost);
217
+ const inputModalities = Array.isArray(payload.input)
218
+ ? payload.input
219
+ : capabilities?.input_modalities;
186
220
  const input: ("text" | "image")[] =
187
- Array.isArray(payload.input) && payload.input.includes("image") ? ["text", "image"] : ["text"];
221
+ Array.isArray(inputModalities) && inputModalities.includes("image") ? ["text", "image"] : ["text"];
222
+ const efforts = getReasoningEfforts(payload.reasoning_efforts ?? capabilities?.reasoning_effort);
223
+ const reasoning =
224
+ payload.reasoning === true ||
225
+ payload.supports_reasoning === true ||
226
+ payload.supports_reasoning_effort === true ||
227
+ capabilities?.supports_reasoning === true ||
228
+ efforts.size > 0;
188
229
 
189
230
  return {
190
231
  id,
191
232
  name: asNonEmptyString(payload.name) ?? id,
192
233
  api: API,
193
- reasoning: payload.reasoning === true || payload.supports_reasoning === true,
234
+ reasoning,
235
+ ...(reasoning ? {
236
+ thinkingLevelMap: thinkingLevelMap(efforts),
237
+ compat: { supportsReasoningEffort: true },
238
+ } : {}),
194
239
  input,
195
240
  cost: {
196
241
  input: asPositiveNumber(cost?.input, 0),
@@ -198,8 +243,14 @@ function modelFromPayload(
198
243
  cacheRead: asPositiveNumber(cost?.cacheRead, 0),
199
244
  cacheWrite: asPositiveNumber(cost?.cacheWrite, 0),
200
245
  },
201
- contextWindow: asPositiveNumber(payload.context_window ?? payload.contextWindow, DEFAULT_CONTEXT_WINDOW),
202
- maxTokens: asPositiveNumber(payload.max_tokens ?? payload.maxTokens, DEFAULT_MAX_TOKENS),
246
+ contextWindow: asPositiveNumber(
247
+ payload.context_window ?? payload.contextWindow ?? capabilities?.context_length,
248
+ DEFAULT_CONTEXT_WINDOW,
249
+ ),
250
+ maxTokens: asPositiveNumber(
251
+ payload.max_tokens ?? payload.maxTokens ?? capabilities?.max_output_tokens,
252
+ DEFAULT_MAX_TOKENS,
253
+ ),
203
254
  };
204
255
  }
205
256
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2tle/pi-provider-manager",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Manage OpenAI-compatible providers and model catalogs in Pi Agent",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",