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 +2 -2
- package/dist/index.js +83 -17
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Nebius Token Factory
|
|
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
|
|
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
|
|
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
|
|
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
|
|
22
|
-
const features = m.supported_features || [];
|
|
25
|
+
function isTextModel(m) {
|
|
23
26
|
const modality = m.architecture?.modality || "";
|
|
24
|
-
return
|
|
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
|
-
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
72
|
-
name:
|
|
73
|
-
reasoning: isReasoningModel(
|
|
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:
|
|
82
|
-
maxTokens: Math.min(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|