@rivus/gateway 0.16.2
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/LICENSE +21 -0
- package/README.md +6 -0
- package/dist/bootstrap/pi-feishu.d.ts +20 -0
- package/dist/bootstrap/pi-feishu.js +671 -0
- package/dist/chunks/background-session-authority.js +230 -0
- package/dist/chunks/background-session-control-input.js +45 -0
- package/dist/chunks/background-session-service.d.ts +390 -0
- package/dist/chunks/index.d.ts +4703 -0
- package/dist/chunks/node-rivus-deployment-manifest.js +1650 -0
- package/dist/chunks/rivus-node-entrypoint.js +4464 -0
- package/dist/chunks/service.js +12112 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1215 -0
- package/dist/mcp.d.ts +92 -0
- package/dist/mcp.js +455 -0
- package/package.json +64 -0
- package/skills/runtime-management/SKILL.md +65 -0
- package/templates/a-share-briefing-analysis.mjs +93 -0
- package/templates/a-share-briefing-renderer.mjs +257 -0
- package/templates/a-share-index-evidence.mjs +99 -0
- package/templates/a-share-market-briefing.mjs +83 -0
- package/templates/a-share-market-date.mjs +10 -0
- package/templates/a-share-overseas-evidence.mjs +86 -0
- package/templates/a-share-policy-evidence.mjs +145 -0
- package/templates/a-share-provider-response.mjs +21 -0
- package/templates/a-share-sector-evidence.mjs +70 -0
- package/templates/acp-stdio-proxy.mjs +58 -0
- package/templates/current-weather.mjs +117 -0
- package/templates/html-drive-tools.mjs +262 -0
- package/templates/https-response-reader.mjs +36 -0
- package/templates/langfuse-drive-e2e.mjs +175 -0
- package/templates/pi-feishu-deployment.bootstrap.ts +3 -0
- package/templates/pi-feishu.bootstrap.ts +242 -0
- package/templates/rivus-agents.plugin.mjs +290 -0
- package/templates/rivus-langfuse-demo.config.json +37 -0
- package/templates/rivus-starter.plugin.mjs +47 -0
- package/templates/rivus.config.json +114 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { get } from "node:https";
|
|
2
|
+
|
|
3
|
+
export function readBoundedHttpsResponse(url, options) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const request = get(
|
|
6
|
+
url,
|
|
7
|
+
{
|
|
8
|
+
family: 4,
|
|
9
|
+
headers: options.headers,
|
|
10
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 10_000)
|
|
11
|
+
},
|
|
12
|
+
(response) => {
|
|
13
|
+
const status = response.statusCode ?? 0;
|
|
14
|
+
response.once("error", reject);
|
|
15
|
+
if (status < 200 || status >= 300) {
|
|
16
|
+
response.resume();
|
|
17
|
+
resolve({ body: Buffer.alloc(0), status });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let length = 0;
|
|
22
|
+
response.on("data", (chunk) => {
|
|
23
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24
|
+
length += buffer.length;
|
|
25
|
+
if (length > options.maxBytes) {
|
|
26
|
+
request.destroy(new Error(`HTTPS response exceeds ${options.maxBytes} bytes`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
chunks.push(buffer);
|
|
30
|
+
});
|
|
31
|
+
response.once("end", () => resolve({ body: Buffer.concat(chunks), status }));
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
request.once("error", reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
import { runLangfuseDriveE2E } from "../dist/testing/index.js";
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const baseUrl = requiredEnv("LANGFUSE_BASE_URL");
|
|
12
|
+
const publicKey = requiredEnv("LANGFUSE_PUBLIC_KEY");
|
|
13
|
+
const secretKey = requiredEnv("LANGFUSE_SECRET_KEY");
|
|
14
|
+
const marker = `${new Date()
|
|
15
|
+
.toISOString()
|
|
16
|
+
.replace(/[^0-9]/g, "")
|
|
17
|
+
.slice(0, 14)}-${randomUUID().slice(0, 8)}`;
|
|
18
|
+
const sessionKey = `e2e:langfuse-drive:${marker}`;
|
|
19
|
+
const expectedDriveTitle = `langfuse-e2e-${marker}.html`;
|
|
20
|
+
const discoveredTraceIds = new Set();
|
|
21
|
+
const deploymentStateDirectory = await mkdtemp(join(tmpdir(), "rivus-langfuse-e2e-state-"));
|
|
22
|
+
const prompt = [
|
|
23
|
+
"Read the granted Langfuse HTML publishing Skill.",
|
|
24
|
+
"Create a polished, accessible, self-contained Chinese Langfuse introduction page.",
|
|
25
|
+
`Include the visible E2E marker ${marker}, then upload it as ${expectedDriveTitle}.`,
|
|
26
|
+
"Return the verified Feishu Drive URL."
|
|
27
|
+
].join(" ");
|
|
28
|
+
|
|
29
|
+
const result = await runLangfuseDriveE2E({
|
|
30
|
+
dependencies: {
|
|
31
|
+
inspectDrive: async (url) => {
|
|
32
|
+
const output = await command(process.env.RIVUS_LARK_CLI_PATH?.trim() || "lark-cli", [
|
|
33
|
+
"drive",
|
|
34
|
+
"+inspect",
|
|
35
|
+
"--as",
|
|
36
|
+
"user",
|
|
37
|
+
"--url",
|
|
38
|
+
url,
|
|
39
|
+
"--format",
|
|
40
|
+
"json"
|
|
41
|
+
]);
|
|
42
|
+
const parsed = JSON.parse(output);
|
|
43
|
+
return isRecord(parsed.data) ? parsed.data : parsed;
|
|
44
|
+
},
|
|
45
|
+
listObservations: async ({ deadlineAt, fromStartTime, sessionKey: expectedSessionKey, toStartTime }) => {
|
|
46
|
+
if (discoveredTraceIds.size === 0) {
|
|
47
|
+
let cursor;
|
|
48
|
+
do {
|
|
49
|
+
const endpoint = new URL("/api/public/v2/observations", baseUrl);
|
|
50
|
+
endpoint.searchParams.set("fields", "core,basic");
|
|
51
|
+
endpoint.searchParams.set("fromStartTime", fromStartTime);
|
|
52
|
+
endpoint.searchParams.set("limit", "1000");
|
|
53
|
+
endpoint.searchParams.set("toStartTime", toStartTime);
|
|
54
|
+
if (cursor) endpoint.searchParams.set("cursor", cursor);
|
|
55
|
+
const body = await fetchLangfuseJson(endpoint, deadlineAt);
|
|
56
|
+
if (!isRecord(body) || !Array.isArray(body.data))
|
|
57
|
+
throw new Error("Langfuse returned an invalid observation page");
|
|
58
|
+
for (const observation of body.data.filter(isObservation)) {
|
|
59
|
+
if (observation.sessionId === expectedSessionKey) discoveredTraceIds.add(observation.traceId);
|
|
60
|
+
}
|
|
61
|
+
cursor = isRecord(body.meta) && typeof body.meta.cursor === "string" ? body.meta.cursor : undefined;
|
|
62
|
+
} while (cursor && discoveredTraceIds.size === 0);
|
|
63
|
+
}
|
|
64
|
+
const traces = [];
|
|
65
|
+
for (const traceId of [...discoveredTraceIds].slice(0, 4)) {
|
|
66
|
+
traces.push(await fetchLangfuseJson(new URL(`/api/public/traces/${traceId}`, baseUrl), deadlineAt));
|
|
67
|
+
}
|
|
68
|
+
return traces.flatMap((trace) => {
|
|
69
|
+
if (
|
|
70
|
+
!isRecord(trace) ||
|
|
71
|
+
typeof trace.id !== "string" ||
|
|
72
|
+
typeof trace.sessionId !== "string" ||
|
|
73
|
+
!Array.isArray(trace.observations)
|
|
74
|
+
)
|
|
75
|
+
return [];
|
|
76
|
+
return trace.observations.filter(isTraceObservation).map((observation) => ({
|
|
77
|
+
...observation,
|
|
78
|
+
sessionId: trace.sessionId,
|
|
79
|
+
traceId: trace.id
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
now: () => new Date(),
|
|
84
|
+
readDriveFile: async (fileToken) => {
|
|
85
|
+
const directory = await mkdtemp(join(tmpdir(), "rivus-langfuse-drive-e2e-"));
|
|
86
|
+
const outputName = `./${expectedDriveTitle}`;
|
|
87
|
+
try {
|
|
88
|
+
await command(
|
|
89
|
+
process.env.RIVUS_LARK_CLI_PATH?.trim() || "lark-cli",
|
|
90
|
+
["drive", "+download", "--as", "user", "--file-token", fileToken, "--output", outputName],
|
|
91
|
+
directory
|
|
92
|
+
);
|
|
93
|
+
return await readFile(join(directory, expectedDriveTitle), "utf8");
|
|
94
|
+
} finally {
|
|
95
|
+
await rm(directory, { force: true, recursive: true });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
runAgent: ({ prompt: agentPrompt, sessionKey: agentSessionKey }) =>
|
|
99
|
+
command(
|
|
100
|
+
process.execPath,
|
|
101
|
+
[
|
|
102
|
+
"dist/cli.js",
|
|
103
|
+
"--bootstrap",
|
|
104
|
+
"./examples/pi-feishu-deployment.bootstrap.ts",
|
|
105
|
+
"--manifest",
|
|
106
|
+
"./examples/rivus-langfuse-demo.config.json",
|
|
107
|
+
"--prompt",
|
|
108
|
+
agentPrompt,
|
|
109
|
+
"--session-key",
|
|
110
|
+
agentSessionKey
|
|
111
|
+
],
|
|
112
|
+
process.cwd(),
|
|
113
|
+
{ RIVUS_DEPLOYMENT_STATE_DIR: deploymentStateDirectory }
|
|
114
|
+
),
|
|
115
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
|
|
116
|
+
},
|
|
117
|
+
expectedDriveContentMarker: marker,
|
|
118
|
+
expectedDriveTitle,
|
|
119
|
+
prompt,
|
|
120
|
+
sessionKey
|
|
121
|
+
}).finally(() => rm(deploymentStateDirectory, { force: true, recursive: true }));
|
|
122
|
+
|
|
123
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
124
|
+
|
|
125
|
+
async function fetchLangfuseJson(endpoint, deadlineAt) {
|
|
126
|
+
const remainingMs = new Date(deadlineAt).getTime() - Date.now();
|
|
127
|
+
if (!Number.isFinite(remainingMs) || remainingMs <= 0) throw new Error("Langfuse request deadline expired");
|
|
128
|
+
const response = await fetch(endpoint, {
|
|
129
|
+
headers: {
|
|
130
|
+
authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`
|
|
131
|
+
},
|
|
132
|
+
signal: AbortSignal.timeout(Math.min(remainingMs, 10_000))
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok) throw new Error(`Langfuse request failed with HTTP ${response.status}`);
|
|
135
|
+
return response.json();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function command(executable, args, cwd = process.cwd(), extraEnv = {}) {
|
|
139
|
+
const { stdout } = await execFileAsync(executable, args, {
|
|
140
|
+
cwd,
|
|
141
|
+
env: {
|
|
142
|
+
...process.env,
|
|
143
|
+
...extraEnv,
|
|
144
|
+
LARKSUITE_CLI_NO_SKILLS_NOTIFIER: "1",
|
|
145
|
+
LARKSUITE_CLI_NO_UPDATE_NOTIFIER: "1"
|
|
146
|
+
},
|
|
147
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
148
|
+
timeout: 300_000
|
|
149
|
+
});
|
|
150
|
+
return stdout;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function requiredEnv(name) {
|
|
154
|
+
const value = process.env[name]?.trim();
|
|
155
|
+
if (!value) throw new Error(`${name} is required`);
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isObservation(value) {
|
|
160
|
+
return isTraceObservation(value) && typeof value.traceId === "string" && typeof value.sessionId === "string";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isTraceObservation(value) {
|
|
164
|
+
return (
|
|
165
|
+
isRecord(value) &&
|
|
166
|
+
typeof value.id === "string" &&
|
|
167
|
+
typeof value.name === "string" &&
|
|
168
|
+
typeof value.startTime === "string" &&
|
|
169
|
+
typeof value.type === "string"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isRecord(value) {
|
|
174
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
175
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
5
|
+
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
createJsonFetchRequest,
|
|
8
|
+
createJsonFileFeishuCardTargetRegistry,
|
|
9
|
+
createJsonlAgentEventLog,
|
|
10
|
+
createLazyFeishuWebSocketEventDispatcher,
|
|
11
|
+
mergePiProviderBaseUrlOverride,
|
|
12
|
+
createRivusDaemonStatusHttpServer,
|
|
13
|
+
createPiAgentLoop,
|
|
14
|
+
createPiSessionRegistry,
|
|
15
|
+
createSystemClock,
|
|
16
|
+
createUuidRunIds,
|
|
17
|
+
restoreConfiguredRivusDaemonBootstrap,
|
|
18
|
+
type ConfiguredRivusDaemonBootstrapRequest,
|
|
19
|
+
type ConfiguredRivusDaemonBootstrapResponse,
|
|
20
|
+
type FeishuWebSocketClient,
|
|
21
|
+
type RivusDaemonBootstrapContext,
|
|
22
|
+
type RivusDaemonFeishuReplayRunner,
|
|
23
|
+
type RivusDaemonProcess,
|
|
24
|
+
type RivusDaemonPromptRunner,
|
|
25
|
+
type RivusDaemonStatusReporter,
|
|
26
|
+
type FeishuReceiveMessagePayload,
|
|
27
|
+
type FeishuReceiveMessageReplayOptions
|
|
28
|
+
} from "@rivus/agent";
|
|
29
|
+
import {
|
|
30
|
+
createPiSkillReadTools,
|
|
31
|
+
createPiSessionResources,
|
|
32
|
+
resolvePiSessionToolNames,
|
|
33
|
+
validatePiSkillCommand
|
|
34
|
+
} from "@rivus/agent/pi";
|
|
35
|
+
|
|
36
|
+
type PiSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
|
|
37
|
+
|
|
38
|
+
const STATE_DIR = "./.rivus";
|
|
39
|
+
const CARD_TARGETS_FILE = `${STATE_DIR}/feishu-card-targets.json`;
|
|
40
|
+
const EVENT_LOG_FILE = `${STATE_DIR}/agent-events.jsonl`;
|
|
41
|
+
const PI_AUTH_FILE = `${STATE_DIR}/pi-auth.json`;
|
|
42
|
+
const PI_MODELS_FILE = `${STATE_DIR}/pi-models.json`;
|
|
43
|
+
const PI_AGENT_DIR = `${STATE_DIR}/pi-agent`;
|
|
44
|
+
|
|
45
|
+
export async function createRivusDaemonProcess(
|
|
46
|
+
context: RivusDaemonBootstrapContext
|
|
47
|
+
): Promise<RivusDaemonProcess & RivusDaemonPromptRunner & RivusDaemonStatusReporter & RivusDaemonFeishuReplayRunner> {
|
|
48
|
+
await mkdir(PI_AGENT_DIR, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const piSessionOptions = await createPiSessionOptions(context);
|
|
51
|
+
const sessionRegistry = createPiSessionRegistry({
|
|
52
|
+
createSession: async () => {
|
|
53
|
+
const cwd = process.cwd();
|
|
54
|
+
const resources = await createPiSessionResources({
|
|
55
|
+
agentDir: PI_AGENT_DIR,
|
|
56
|
+
cwd,
|
|
57
|
+
homeDirectory: homedir()
|
|
58
|
+
});
|
|
59
|
+
const customTools = createPiSkillReadTools({
|
|
60
|
+
cwd,
|
|
61
|
+
runtimeToolIds: [],
|
|
62
|
+
skillPaths: resources.skillNames.size > 0 ? resources.skillPaths : []
|
|
63
|
+
});
|
|
64
|
+
const result = await createAgentSession(
|
|
65
|
+
resources.withSessionOptions({
|
|
66
|
+
...piSessionOptions,
|
|
67
|
+
customTools,
|
|
68
|
+
excludeTools: [],
|
|
69
|
+
sessionManager: SessionManager.create(cwd),
|
|
70
|
+
tools: [...resolvePiSessionToolNames([], customTools)]
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
dispose: () => result.session.dispose(),
|
|
75
|
+
preparePrompt: (input) => {
|
|
76
|
+
validatePiSkillCommand(input.text, resources.skillNames);
|
|
77
|
+
return input.text;
|
|
78
|
+
},
|
|
79
|
+
session: result.session
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const piLoop = createPiAgentLoop({
|
|
84
|
+
supportsSteering: true,
|
|
85
|
+
disposeSessionAfterRun: false,
|
|
86
|
+
resolveSession: (input) => sessionRegistry.resolve(input)
|
|
87
|
+
});
|
|
88
|
+
const statusPort = readOptionalPort(context.env.RIVUS_STATUS_PORT);
|
|
89
|
+
const request = createJsonFetchRequest();
|
|
90
|
+
|
|
91
|
+
const bootstrap = await Effect.runPromise(
|
|
92
|
+
restoreConfiguredRivusDaemonBootstrap({
|
|
93
|
+
cardTargets: createJsonFileFeishuCardTargetRegistry({
|
|
94
|
+
filePath: CARD_TARGETS_FILE
|
|
95
|
+
}),
|
|
96
|
+
clock: createSystemClock(),
|
|
97
|
+
config: context.config,
|
|
98
|
+
...(statusPort === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: {
|
|
101
|
+
createStatusTransport: (statusReporter: RivusDaemonStatusReporter) =>
|
|
102
|
+
createRivusDaemonStatusHttpServer({
|
|
103
|
+
port: statusPort,
|
|
104
|
+
statusReporter
|
|
105
|
+
})
|
|
106
|
+
}),
|
|
107
|
+
eventDispatcher: createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
|
|
108
|
+
eventLog: createJsonlAgentEventLog({
|
|
109
|
+
filePath: EVENT_LOG_FILE
|
|
110
|
+
}),
|
|
111
|
+
loop: piLoop,
|
|
112
|
+
request: (input: ConfiguredRivusDaemonBootstrapRequest) =>
|
|
113
|
+
request(input).pipe(Effect.map((response) => response as ConfiguredRivusDaemonBootstrapResponse)),
|
|
114
|
+
runIds: createUuidRunIds(),
|
|
115
|
+
sleep: (ms) => Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms))),
|
|
116
|
+
websocketClient: createLazyFeishuWebSocketClient({
|
|
117
|
+
appId: context.config.feishu.appId,
|
|
118
|
+
appSecret: context.config.feishu.appSecret
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
promptText: (command) => bootstrap.promptText(command),
|
|
125
|
+
replayReceiveMessage: (payload: FeishuReceiveMessagePayload, options?: FeishuReceiveMessageReplayOptions) =>
|
|
126
|
+
bootstrap.runtime.replayReceiveMessage(payload, options),
|
|
127
|
+
running: () => bootstrap.process.running(),
|
|
128
|
+
start: () => bootstrap.process.start(),
|
|
129
|
+
status: () => bootstrap.status(),
|
|
130
|
+
stop: () =>
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
yield* bootstrap.process.stop();
|
|
133
|
+
yield* Effect.tryPromise({
|
|
134
|
+
try: () => sessionRegistry.disposeAll(),
|
|
135
|
+
catch: (error) => error
|
|
136
|
+
});
|
|
137
|
+
})
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createLazyFeishuWebSocketClient(options: {
|
|
142
|
+
readonly appId: string;
|
|
143
|
+
readonly appSecret: string;
|
|
144
|
+
}): FeishuWebSocketClient {
|
|
145
|
+
let client:
|
|
146
|
+
| {
|
|
147
|
+
close(): void;
|
|
148
|
+
start(options: Parameters<FeishuWebSocketClient["start"]>[0]): Promise<void> | void;
|
|
149
|
+
}
|
|
150
|
+
| undefined;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
start: (startOptions) => {
|
|
154
|
+
client ??= new Lark.WSClient({
|
|
155
|
+
appId: options.appId,
|
|
156
|
+
appSecret: options.appSecret
|
|
157
|
+
});
|
|
158
|
+
return client.start(startOptions);
|
|
159
|
+
},
|
|
160
|
+
close: () => {
|
|
161
|
+
client?.close();
|
|
162
|
+
client = undefined;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function createPiSessionOptions(context: RivusDaemonBootstrapContext): Promise<PiSessionOptions> {
|
|
168
|
+
const provider = readProviderFromModel(context.config.pi.model);
|
|
169
|
+
|
|
170
|
+
if (context.config.pi.baseUrl) {
|
|
171
|
+
if (!provider) {
|
|
172
|
+
throw new Error("PI_BASE_URL requires PI_MODEL in provider/model form, for example zai/glm-5.2");
|
|
173
|
+
}
|
|
174
|
+
await writeProviderBaseUrlOverride(provider, context.config.pi.baseUrl);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const modelRuntime = await ModelRuntime.create({
|
|
178
|
+
allowModelNetwork: false,
|
|
179
|
+
authPath: PI_AUTH_FILE,
|
|
180
|
+
modelsPath: context.config.pi.baseUrl ? PI_MODELS_FILE : null
|
|
181
|
+
});
|
|
182
|
+
if (context.config.pi.apiKey) {
|
|
183
|
+
if (!provider) {
|
|
184
|
+
throw new Error("PI_API_KEY requires PI_MODEL in provider/model form, for example zai/glm-5.2");
|
|
185
|
+
}
|
|
186
|
+
await modelRuntime.setRuntimeApiKey(provider, context.config.pi.apiKey);
|
|
187
|
+
}
|
|
188
|
+
const model = context.config.pi.model ? resolveConfiguredPiModel(modelRuntime, context.config.pi.model) : undefined;
|
|
189
|
+
return {
|
|
190
|
+
cwd: process.cwd(),
|
|
191
|
+
modelRuntime,
|
|
192
|
+
...(model ? { model } : {}),
|
|
193
|
+
...(context.config.pi.thinkingLevel ? { thinkingLevel: context.config.pi.thinkingLevel } : {})
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveConfiguredPiModel(modelRuntime: ModelRuntime, modelReference: string): PiSessionOptions["model"] {
|
|
198
|
+
const provider = readProviderFromModel(modelReference);
|
|
199
|
+
const modelId = readModelIdFromModel(modelReference);
|
|
200
|
+
if (!provider || !modelId) {
|
|
201
|
+
throw new Error("PI_MODEL must use provider/model form, for example zai/glm-5.2");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const model = modelRuntime.getModel(provider, modelId);
|
|
205
|
+
if (!model) {
|
|
206
|
+
throw new Error(`PI_MODEL ${modelReference} was not found in the Pi model runtime`);
|
|
207
|
+
}
|
|
208
|
+
return model as PiSessionOptions["model"];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readProviderFromModel(model: string | undefined): string | undefined {
|
|
212
|
+
const slashIndex = model?.indexOf("/") ?? -1;
|
|
213
|
+
if (!model || slashIndex <= 0) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return model.slice(0, slashIndex);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readModelIdFromModel(model: string | undefined): string | undefined {
|
|
220
|
+
const slashIndex = model?.indexOf("/") ?? -1;
|
|
221
|
+
if (!model || slashIndex < 0 || slashIndex === model.length - 1) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
return model.slice(slashIndex + 1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function readOptionalPort(value: string | undefined): number | undefined {
|
|
228
|
+
const trimmed = value?.trim();
|
|
229
|
+
if (!trimmed) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const port = Number(trimmed);
|
|
234
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
235
|
+
throw new Error("RIVUS_STATUS_PORT must be an integer between 0 and 65535");
|
|
236
|
+
}
|
|
237
|
+
return port;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function writeProviderBaseUrlOverride(provider: string, baseUrl: string): Promise<void> {
|
|
241
|
+
await mergePiProviderBaseUrlOverride({ baseUrl, filePath: PI_MODELS_FILE, provider });
|
|
242
|
+
}
|