pi-nebius 0.3.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/CHANGELOG.md +49 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/SECURITY.md +47 -0
- package/benchmarks/add-api-endpoint/benchmark.yaml +13 -0
- package/benchmarks/add-api-endpoint/fixture/app.mjs +6 -0
- package/benchmarks/add-api-endpoint/fixture/app.test.mjs +8 -0
- package/benchmarks/add-api-endpoint/fixture/package.json +8 -0
- package/benchmarks/add-api-endpoint/validation/check.test.mjs +37 -0
- package/benchmarks/fix-auth-bug/benchmark.yaml +14 -0
- package/benchmarks/fix-auth-bug/fixture/auth.mjs +4 -0
- package/benchmarks/fix-auth-bug/fixture/auth.test.mjs +26 -0
- package/benchmarks/fix-auth-bug/fixture/package.json +8 -0
- package/benchmarks/fix-auth-bug/validation/check.test.mjs +25 -0
- package/benchmarks/multi-file-feature/benchmark.yaml +15 -0
- package/benchmarks/multi-file-feature/fixture/package.json +8 -0
- package/benchmarks/multi-file-feature/fixture/routes.mjs +11 -0
- package/benchmarks/multi-file-feature/fixture/routes.test.mjs +14 -0
- package/benchmarks/multi-file-feature/fixture/serialize.mjs +3 -0
- package/benchmarks/multi-file-feature/fixture/store.mjs +10 -0
- package/benchmarks/multi-file-feature/validation/check.test.mjs +52 -0
- package/benchmarks/refactor-module/benchmark.yaml +12 -0
- package/benchmarks/refactor-module/fixture/invoice.mjs +8 -0
- package/benchmarks/refactor-module/fixture/invoice.test.mjs +9 -0
- package/benchmarks/refactor-module/fixture/package.json +8 -0
- package/benchmarks/refactor-module/validation/check.test.mjs +36 -0
- package/dist/benchmark/cli.js +112 -0
- package/dist/benchmark/command.js +194 -0
- package/dist/benchmark/definition.js +109 -0
- package/dist/benchmark/host-worker.js +14 -0
- package/dist/benchmark/instrumentation.js +296 -0
- package/dist/benchmark/metrics.js +78 -0
- package/dist/benchmark/process.js +122 -0
- package/dist/benchmark/project.js +70 -0
- package/dist/benchmark/report.js +94 -0
- package/dist/benchmark/runner.js +376 -0
- package/dist/benchmark/types.js +1 -0
- package/dist/benchmark/worker.js +134 -0
- package/dist/benchmark/workspace.js +55 -0
- package/dist/discovery.js +154 -0
- package/dist/errors.js +32 -0
- package/dist/index.js +86 -0
- package/dist/model-settings-command.js +130 -0
- package/dist/model-settings.js +101 -0
- package/dist/models.js +62 -0
- package/dist/provider.js +48 -0
- package/docs/benchmark-research.md +35 -0
- package/docs/benchmarking.md +253 -0
- package/docs/security-review.md +49 -0
- package/docs/validation.md +51 -0
- package/examples/models.json +31 -0
- package/package.json +74 -0
- package/src/benchmark/cli.ts +118 -0
- package/src/benchmark/command.ts +218 -0
- package/src/benchmark/definition.ts +112 -0
- package/src/benchmark/host-worker.ts +14 -0
- package/src/benchmark/instrumentation.ts +298 -0
- package/src/benchmark/metrics.ts +101 -0
- package/src/benchmark/process.ts +120 -0
- package/src/benchmark/project.ts +71 -0
- package/src/benchmark/report.ts +111 -0
- package/src/benchmark/runner.ts +458 -0
- package/src/benchmark/types.ts +180 -0
- package/src/benchmark/worker.ts +150 -0
- package/src/benchmark/workspace.ts +64 -0
- package/src/discovery.ts +176 -0
- package/src/errors.ts +32 -0
- package/src/index.ts +96 -0
- package/src/model-settings-command.ts +151 -0
- package/src/model-settings.ts +129 -0
- package/src/models.ts +73 -0
- package/src/provider.ts +63 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { isRecord, type NebiusModel } from "./models.ts";
|
|
5
|
+
|
|
6
|
+
export const efforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
7
|
+
export interface ModelSettings {
|
|
8
|
+
temperature?: number;
|
|
9
|
+
reasoningEffort?: (typeof efforts)[number];
|
|
10
|
+
maxTokens?: number;
|
|
11
|
+
contextWindow?: number;
|
|
12
|
+
reasoning?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export type ModelSettingsMap = Record<string, ModelSettings>;
|
|
15
|
+
export const settingsPath = (agentDir: string) =>
|
|
16
|
+
join(agentDir, "pi-nebius", "model-settings.json");
|
|
17
|
+
|
|
18
|
+
export function validateSettings(value: unknown): ModelSettings {
|
|
19
|
+
if (!isRecord(value)) throw new Error("Model settings must be an object.");
|
|
20
|
+
const allowed = ["temperature", "reasoningEffort", "maxTokens", "contextWindow", "reasoning"];
|
|
21
|
+
for (const key of Object.keys(value)) {
|
|
22
|
+
if (!allowed.includes(key)) throw new Error(`Unknown model setting: ${key}`);
|
|
23
|
+
const v = value[key];
|
|
24
|
+
if (key === "temperature" && (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 2))
|
|
25
|
+
throw new Error("Temperature must be between 0 and 2.");
|
|
26
|
+
if (
|
|
27
|
+
(key === "maxTokens" || key === "contextWindow") &&
|
|
28
|
+
(typeof v !== "number" || !Number.isSafeInteger(v) || v <= 0)
|
|
29
|
+
)
|
|
30
|
+
throw new Error(`${key} must be a positive integer.`);
|
|
31
|
+
if (key === "reasoningEffort" && !efforts.includes(v as (typeof efforts)[number]))
|
|
32
|
+
throw new Error("Invalid reasoning effort.");
|
|
33
|
+
if (key === "reasoning" && typeof v !== "boolean")
|
|
34
|
+
throw new Error("Reasoning must be a boolean.");
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
typeof value.maxTokens === "number" &&
|
|
38
|
+
typeof value.contextWindow === "number" &&
|
|
39
|
+
value.maxTokens > value.contextWindow
|
|
40
|
+
)
|
|
41
|
+
throw new Error("Maximum output tokens cannot exceed the context window.");
|
|
42
|
+
return { ...value } as ModelSettings;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function loadSettings(path: string): Promise<ModelSettingsMap> {
|
|
46
|
+
let text: string;
|
|
47
|
+
try {
|
|
48
|
+
text = await readFile(path, "utf8");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
const value: unknown = JSON.parse(text);
|
|
54
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.models))
|
|
55
|
+
throw new Error("Invalid Nebius model settings file.");
|
|
56
|
+
return Object.fromEntries(
|
|
57
|
+
Object.entries(value.models).map(([id, settings]) => [id, validateSettings(settings)]),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function saveModelSettings(
|
|
62
|
+
path: string,
|
|
63
|
+
id: string,
|
|
64
|
+
settings: ModelSettings,
|
|
65
|
+
): Promise<ModelSettingsMap> {
|
|
66
|
+
// Re-read before each edit so unrelated changes from another Pi session are retained.
|
|
67
|
+
const models = await loadSettings(path);
|
|
68
|
+
const next = validateSettings(settings);
|
|
69
|
+
const entries = Object.entries(models).filter(([key]) => key !== id);
|
|
70
|
+
if (Object.keys(next).length) entries.push([id, next]);
|
|
71
|
+
const updated = Object.fromEntries(entries);
|
|
72
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
73
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
74
|
+
try {
|
|
75
|
+
await writeFile(temporary, `${JSON.stringify({ version: 1, models: updated }, null, 2)}\n`, {
|
|
76
|
+
mode: 0o600,
|
|
77
|
+
flag: "wx",
|
|
78
|
+
});
|
|
79
|
+
await rename(temporary, path);
|
|
80
|
+
} finally {
|
|
81
|
+
await rm(temporary, { force: true });
|
|
82
|
+
}
|
|
83
|
+
return updated;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function applyModelSettings(model: NebiusModel, settings: ModelSettings = {}): NebiusModel {
|
|
87
|
+
const contextWindow = settings.contextWindow ?? model.contextWindow;
|
|
88
|
+
return {
|
|
89
|
+
...model,
|
|
90
|
+
contextWindow,
|
|
91
|
+
maxTokens: Math.min(settings.maxTokens ?? model.maxTokens, contextWindow),
|
|
92
|
+
reasoning: settings.reasoning ?? model.reasoning,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Apply only supported request overrides; never store prompts or messages. */
|
|
97
|
+
export function applyRequestSettings(
|
|
98
|
+
payload: unknown,
|
|
99
|
+
model: NebiusModel,
|
|
100
|
+
settings: ModelSettings = {},
|
|
101
|
+
): unknown {
|
|
102
|
+
if (!isRecord(payload)) return payload;
|
|
103
|
+
const result = { ...payload };
|
|
104
|
+
if (
|
|
105
|
+
settings.temperature !== undefined &&
|
|
106
|
+
model.nebiusSupportedParameters?.includes("temperature")
|
|
107
|
+
)
|
|
108
|
+
result.temperature = settings.temperature;
|
|
109
|
+
if (
|
|
110
|
+
settings.reasoningEffort !== undefined &&
|
|
111
|
+
model.reasoning &&
|
|
112
|
+
model.compat?.supportsReasoningEffort
|
|
113
|
+
)
|
|
114
|
+
result.reasoning_effort = settings.reasoningEffort;
|
|
115
|
+
if (settings.maxTokens !== undefined) {
|
|
116
|
+
delete result.max_completion_tokens;
|
|
117
|
+
result.max_tokens = Math.min(settings.maxTokens, model.contextWindow);
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function requestSettings(payload: unknown): Record<string, unknown> {
|
|
123
|
+
if (!isRecord(payload)) return {};
|
|
124
|
+
return Object.fromEntries(
|
|
125
|
+
["temperature", "reasoning_effort", "max_tokens", "max_completion_tokens", "top_p"]
|
|
126
|
+
.filter((key) => payload[key] !== undefined)
|
|
127
|
+
.map((key) => [key, payload[key]]),
|
|
128
|
+
);
|
|
129
|
+
}
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export const BASE_URL = "https://api.tokenfactory.nebius.com/v1";
|
|
4
|
+
export type NebiusModel = Model<"openai-completions"> & { nebiusSupportedParameters?: string[] };
|
|
5
|
+
|
|
6
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Only retain fields we understand: no credentials or arbitrary server data reach the cache. */
|
|
11
|
+
export function parseModels(payload: unknown): NebiusModel[] {
|
|
12
|
+
if (!isRecord(payload) || payload.object !== "list" || !Array.isArray(payload.data)) {
|
|
13
|
+
throw new Error("Malformed Token Factory model list: expected { object: 'list', data: [...] }");
|
|
14
|
+
}
|
|
15
|
+
const models = new Map<string, NebiusModel>();
|
|
16
|
+
for (const entry of payload.data) {
|
|
17
|
+
if (
|
|
18
|
+
!isRecord(entry) ||
|
|
19
|
+
typeof entry.id !== "string" ||
|
|
20
|
+
!entry.id.trim() ||
|
|
21
|
+
entry.id.length > 512 ||
|
|
22
|
+
[...entry.id].some(
|
|
23
|
+
(char) => /\s/.test(char) || char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127,
|
|
24
|
+
)
|
|
25
|
+
) {
|
|
26
|
+
throw new Error("Malformed Token Factory model list: invalid model identifier");
|
|
27
|
+
}
|
|
28
|
+
if (entry.status != null && entry.status !== "active") continue;
|
|
29
|
+
const modality = isRecord(entry.architecture) ? entry.architecture.modality : undefined;
|
|
30
|
+
// Nebius also serves embeddings and image generation. Only expose text-output models.
|
|
31
|
+
if (typeof modality === "string" && !/->text$/.test(modality)) continue;
|
|
32
|
+
const parameters = Array.isArray(entry.supported_sampling_parameters)
|
|
33
|
+
? entry.supported_sampling_parameters
|
|
34
|
+
: [];
|
|
35
|
+
const features = Array.isArray(entry.supported_features) ? entry.supported_features : [];
|
|
36
|
+
const effort = parameters.includes("reasoning_effort");
|
|
37
|
+
const contextWindow = positiveInteger(entry.context_length) ?? 32768;
|
|
38
|
+
models.set(entry.id, {
|
|
39
|
+
id: entry.id,
|
|
40
|
+
name: typeof entry.name === "string" && entry.name.trim() ? entry.name : entry.id,
|
|
41
|
+
provider: "nebius",
|
|
42
|
+
api: "openai-completions",
|
|
43
|
+
baseUrl: BASE_URL,
|
|
44
|
+
reasoning: effort || features.includes("reasoning"),
|
|
45
|
+
input:
|
|
46
|
+
typeof modality === "string" && modality.split("->")[0]?.includes("image")
|
|
47
|
+
? ["text", "image"]
|
|
48
|
+
: ["text"],
|
|
49
|
+
nebiusSupportedParameters: parameters.filter(
|
|
50
|
+
(value): value is string => typeof value === "string",
|
|
51
|
+
),
|
|
52
|
+
contextWindow,
|
|
53
|
+
// No documented output-token limit.
|
|
54
|
+
maxTokens: Math.min(4096, Math.max(1, Math.floor(contextWindow / 4))),
|
|
55
|
+
// Required by Pi's Model type; unused by our benchmark.
|
|
56
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
57
|
+
compat: {
|
|
58
|
+
supportsStore: false,
|
|
59
|
+
supportsDeveloperRole: false,
|
|
60
|
+
supportsStrictMode: false,
|
|
61
|
+
supportsOpenAIGrammarTools: false,
|
|
62
|
+
supportsReasoningEffort: effort,
|
|
63
|
+
supportsUsageInStreaming: true,
|
|
64
|
+
maxTokensField: "max_tokens",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return [...models.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
72
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
73
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createProvider, type ProviderStreams } from "@earendil-works/pi-ai";
|
|
2
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/compat";
|
|
3
|
+
import { withErrorDetails } from "./errors.ts";
|
|
4
|
+
import { applyRequestSettings, type ModelSettingsMap } from "./model-settings.ts";
|
|
5
|
+
import { BASE_URL, type NebiusModel } from "./models.ts";
|
|
6
|
+
|
|
7
|
+
export function nebiusProvider(
|
|
8
|
+
models: NebiusModel[],
|
|
9
|
+
settings: ModelSettingsMap = {},
|
|
10
|
+
onRequest?: (payload: unknown) => void,
|
|
11
|
+
) {
|
|
12
|
+
const adapter = openAICompletionsApi();
|
|
13
|
+
const api: ProviderStreams = {
|
|
14
|
+
stream: (model, context, options) =>
|
|
15
|
+
adapter.stream(model, context, {
|
|
16
|
+
...options,
|
|
17
|
+
onPayload: async (payload, selected) => {
|
|
18
|
+
const previous = await options?.onPayload?.(payload, selected);
|
|
19
|
+
const adjusted = applyRequestSettings(
|
|
20
|
+
previous ?? payload,
|
|
21
|
+
model as NebiusModel,
|
|
22
|
+
settings[model.id],
|
|
23
|
+
);
|
|
24
|
+
onRequest?.(adjusted);
|
|
25
|
+
return adjusted;
|
|
26
|
+
},
|
|
27
|
+
fetch: withErrorDetails(options?.fetch ?? globalThis.fetch),
|
|
28
|
+
}),
|
|
29
|
+
streamSimple: (model, context, options) =>
|
|
30
|
+
adapter.streamSimple(model, context, {
|
|
31
|
+
...options,
|
|
32
|
+
onPayload: async (payload, selected) => {
|
|
33
|
+
const previous = await options?.onPayload?.(payload, selected);
|
|
34
|
+
const adjusted = applyRequestSettings(
|
|
35
|
+
previous ?? payload,
|
|
36
|
+
model as NebiusModel,
|
|
37
|
+
settings[model.id],
|
|
38
|
+
);
|
|
39
|
+
onRequest?.(adjusted);
|
|
40
|
+
return adjusted;
|
|
41
|
+
},
|
|
42
|
+
fetch: withErrorDetails(options?.fetch ?? globalThis.fetch),
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
return createProvider({
|
|
46
|
+
id: "nebius",
|
|
47
|
+
name: "Nebius Token Factory",
|
|
48
|
+
baseUrl: BASE_URL,
|
|
49
|
+
models,
|
|
50
|
+
api,
|
|
51
|
+
auth: {
|
|
52
|
+
apiKey: {
|
|
53
|
+
name: "NEBIUS_API_KEY",
|
|
54
|
+
// Ambient-only auth: no login flow and no credential persistence.
|
|
55
|
+
async resolve({ ctx, signal }) {
|
|
56
|
+
signal.throwIfAborted();
|
|
57
|
+
const key = (await ctx.env("NEBIUS_API_KEY"))?.trim();
|
|
58
|
+
return key ? { auth: { apiKey: key }, source: "NEBIUS_API_KEY" } : undefined;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|