tokenfactory-pi 0.2.4 → 0.2.6

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/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Nebius Token Factory pi extension
2
+ * Nebius Token Factory - pi extension
3
3
  *
4
4
  * Fetches the current model catalog from the Token Factory API on startup
5
5
  * and registers all tool-capable text-generation models as a "nebius" provider.
6
6
  *
7
7
  * Environment:
8
- * NEBIUS_API_KEY required, Token Factory API key
8
+ * NEBIUS_API_KEY - required, Token Factory API key
9
9
  *
10
10
  * Usage:
11
11
  * pi -e /path/to/tokenfactory-pi
package/dist/index.js CHANGED
@@ -1,27 +1,30 @@
1
1
  /**
2
- * Nebius Token Factory pi extension
2
+ * Nebius Token Factory - pi extension
3
3
  *
4
4
  * Fetches the current model catalog from the Token Factory API on startup
5
5
  * and registers all tool-capable text-generation models as a "nebius" provider.
6
6
  *
7
7
  * Environment:
8
- * NEBIUS_API_KEY required, Token Factory API key
8
+ * NEBIUS_API_KEY - required, Token Factory API key
9
9
  *
10
10
  * Usage:
11
11
  * pi -e /path/to/tokenfactory-pi
12
12
  * pi -e /path/to/tokenfactory-pi --provider nebius
13
13
  * pi -e /path/to/tokenfactory-pi --provider nebius --model Qwen/Qwen3-32B
14
14
  */
15
+ import { gunzipSync } from "node:zlib";
15
16
  const PROVIDER_NAME = "nebius";
16
17
  const BASE_URL = "https://api.tokenfactory.nebius.com/v1";
17
18
  const ENV_VAR = "NEBIUS_API_KEY";
19
+ function isGzip(bytes) {
20
+ return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
21
+ }
18
22
  // ============================================================================
19
23
  // Helpers
20
24
  // ============================================================================
21
- function isToolCapableTextModel(m) {
22
- const features = m.supported_features || [];
25
+ function isTextModel(m) {
23
26
  const modality = m.architecture?.modality || "";
24
- return features.includes("tools") && modality.includes("->text");
27
+ return modality.includes("->text");
25
28
  }
26
29
  function parseInputModalities(modality) {
27
30
  const input = ["text"];
@@ -30,11 +33,25 @@ function parseInputModalities(modality) {
30
33
  return input;
31
34
  }
32
35
  function parseCostPerMillion(raw) {
33
- return parseFloat(raw || "0") * 1_000_000;
36
+ const parsed = parseFloat(raw || "0");
37
+ return isNaN(parsed) ? 0 : parsed * 1_000_000;
34
38
  }
35
39
  function isReasoningModel(id) {
36
40
  return /(-R1|-Thinking|QwQ)/.test(id);
37
41
  }
42
+ async function readTokenFactoryResponse(res) {
43
+ const bytes = Buffer.from(await res.arrayBuffer());
44
+ const body = (isGzip(bytes) ? gunzipSync(bytes) : bytes).toString("utf8");
45
+ try {
46
+ return JSON.parse(body);
47
+ }
48
+ catch {
49
+ const preview = body.slice(0, 200).replace(/\s+/g, " ");
50
+ throw new Error(`Invalid Token Factory JSON response (${res.status} ${res.statusText}, ` +
51
+ `content-type=${res.headers.get("content-type") || "unknown"}, ` +
52
+ `content-encoding=${res.headers.get("content-encoding") || "none"}): ${preview}`);
53
+ }
54
+ }
38
55
  // ============================================================================
39
56
  // Extension entry point
40
57
  // ============================================================================
@@ -46,31 +63,51 @@ export default async function (pi) {
46
63
  let response;
47
64
  try {
48
65
  const res = await fetch(`${BASE_URL}/models?verbose=true`, {
49
- headers: { Authorization: `Bearer ${apiKey}` },
66
+ headers: {
67
+ Authorization: `Bearer ${apiKey}`,
68
+ "Accept-Encoding": "identity",
69
+ },
50
70
  });
51
71
  if (!res.ok) {
52
72
  console.warn(`[${PROVIDER_NAME}] API returned ${res.status}: ${res.statusText}`);
73
+ console.warn(`[${PROVIDER_NAME}] Response headers:`, [...res.headers.entries()]);
53
74
  return;
54
75
  }
55
- response = (await res.json());
76
+ response = await readTokenFactoryResponse(res);
56
77
  }
57
78
  catch (error) {
58
79
  console.warn(`[${PROVIDER_NAME}] Failed to fetch models:`, error);
59
80
  return;
60
81
  }
61
82
  if (!Array.isArray(response.data)) {
62
- console.warn(`[${PROVIDER_NAME}] Unexpected API response shape`);
83
+ console.warn(`[${PROVIDER_NAME}] Unexpected API response shape. Expected array, got:`, typeof response.data);
84
+ console.warn(`[${PROVIDER_NAME}] Response data:`, response);
63
85
  return;
64
86
  }
65
87
  const models = [];
66
88
  for (const m of response.data) {
67
- if (!isToolCapableTextModel(m))
89
+ // Skip models without valid IDs
90
+ if (!m.id || m.id.trim() === "") {
91
+ console.log(`[${PROVIDER_NAME}] Skipping model with empty ID:`, m);
92
+ continue;
93
+ }
94
+ if (!isTextModel(m)) {
68
95
  continue;
96
+ }
69
97
  const modality = m.architecture?.modality || "";
98
+ // Validate and set defaults for critical fields
99
+ const contextLength = m.context_length && m.context_length > 0 ? m.context_length : 131072;
100
+ const modelName = m.name || m.id || "unknown-model";
101
+ const modelId = m.id || modelName;
102
+ // Skip models with zero context length even after defaults
103
+ if (contextLength <= 0) {
104
+ console.log(`[${PROVIDER_NAME}] Skipping model with invalid context length:`, m.id, m.context_length);
105
+ continue;
106
+ }
70
107
  models.push({
71
- id: m.id,
72
- name: m.name || m.id,
73
- reasoning: isReasoningModel(m.id),
108
+ id: modelId,
109
+ name: modelName,
110
+ reasoning: isReasoningModel(modelId),
74
111
  input: parseInputModalities(modality),
75
112
  cost: {
76
113
  input: parseCostPerMillion(m.pricing?.prompt),
@@ -78,8 +115,8 @@ export default async function (pi) {
78
115
  cacheRead: 0,
79
116
  cacheWrite: 0,
80
117
  },
81
- contextWindow: m.context_length || 131072,
82
- maxTokens: Math.min(m.context_length || 32768, 32768),
118
+ contextWindow: contextLength,
119
+ maxTokens: Math.min(contextLength, 32768),
83
120
  compat: {
84
121
  supportsDeveloperRole: false,
85
122
  maxTokensField: "max_tokens",
@@ -90,6 +127,9 @@ export default async function (pi) {
90
127
  baseUrl: BASE_URL,
91
128
  apiKey: ENV_VAR,
92
129
  api: "openai-completions",
130
+ headers: {
131
+ "Accept-Encoding": "identity",
132
+ },
93
133
  models,
94
134
  });
95
135
  // /nebius-models command to list and select a model
@@ -101,7 +141,7 @@ export default async function (pi) {
101
141
  return;
102
142
  }
103
143
  const items = models
104
- .sort((a, b) => a.id.localeCompare(b.id))
144
+ .sort((a, b) => b.cost.output - a.cost.output)
105
145
  .map((m) => {
106
146
  const tags = [];
107
147
  if (m.reasoning)
@@ -111,7 +151,33 @@ export default async function (pi) {
111
151
  const suffix = tags.length > 0 ? ` (${tags.join(", ")})` : "";
112
152
  return `${m.id}${suffix}`;
113
153
  });
114
- await ctx.ui.select(`Nebius Token Factory ${models.length} models`, items);
154
+ // Show the selection dialog and capture the result
155
+ const selectedItem = await ctx.ui.select(`Nebius Token Factory - ${models.length} models`, items);
156
+ // If user cancelled (selectedItem is undefined), do nothing
157
+ if (!selectedItem) {
158
+ ctx.ui.notify("Model selection cancelled", "warning");
159
+ return;
160
+ }
161
+ // Extract the model ID from the selection (remove the suffix)
162
+ const selectedId = selectedItem.split(" (")[0];
163
+ const selectedModel = models.find(m => m.id === selectedId);
164
+ if (!selectedModel) {
165
+ ctx.ui.notify(`Model not found: ${selectedId}`, "error");
166
+ return;
167
+ }
168
+ // Find the model in the registry and switch to it
169
+ const model = ctx.modelRegistry.find(PROVIDER_NAME, selectedModel.id);
170
+ if (!model) {
171
+ ctx.ui.notify(`Model not found in registry: ${PROVIDER_NAME}/${selectedModel.id}`, "error");
172
+ return;
173
+ }
174
+ const success = await pi.setModel(model);
175
+ if (success) {
176
+ ctx.ui.notify(`Switched to model: ${model.provider}/${model.id}`, "info");
177
+ }
178
+ else {
179
+ ctx.ui.notify(`Failed to switch to model: ${model.provider}/${model.id}. Check API key?`, "error");
180
+ }
115
181
  },
116
182
  });
117
183
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenfactory-pi",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Nebius Token Factory provider extension for pi coding agent. Requires `npm install -g @mariozechner/pi-coding-agent`. Install with `pi install npm:tokenfactory-pi`",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -43,4 +43,4 @@
43
43
  "check": "tsc --noEmit",
44
44
  "prepare": "npm run build"
45
45
  }
46
- }
46
+ }