refactor(plugin-sdk): share auth, routing, and stream/account helpers

This commit is contained in:
Peter Steinberger
2026-03-02 15:14:46 +00:00
parent e9dd6121f2
commit ed21b63bb8
18 changed files with 457 additions and 286 deletions

View File

@@ -128,6 +128,16 @@ export async function waitForExecApprovalDecision(id: string): Promise<string |
}
}
export async function resolveRegisteredExecApprovalDecision(params: {
approvalId: string;
preResolvedDecision: string | null | undefined;
}): Promise<string | null> {
if (params.preResolvedDecision !== undefined) {
return params.preResolvedDecision ?? null;
}
return await waitForExecApprovalDecision(params.approvalId);
}
export async function requestExecApprovalDecision(
params: RequestExecApprovalDecisionParams,
): Promise<string | null> {

View File

@@ -19,9 +19,9 @@ import { logInfo } from "../logger.js";
import { markBackgrounded, tail } from "./bash-process-registry.js";
import {
buildExecApprovalRequesterContext,
resolveRegisteredExecApprovalDecision,
buildExecApprovalTurnSourceContext,
registerExecApprovalRequestForHostOrThrow,
waitForExecApprovalDecision,
} from "./bash-tools.exec-approval-request.js";
import {
DEFAULT_APPROVAL_TIMEOUT_MS,
@@ -172,13 +172,12 @@ export async function processGatewayAllowlist(
preResolvedDecision = registration.finalDecision;
void (async () => {
let decision: string | null = preResolvedDecision ?? null;
let decision: string | null = null;
try {
// Some gateways may return a final decision inline during registration.
// Only call waitDecision when registration did not already carry one.
if (preResolvedDecision === undefined) {
decision = await waitForExecApprovalDecision(approvalId);
}
decision = await resolveRegisteredExecApprovalDecision({
approvalId,
preResolvedDecision,
});
} catch {
emitExecSystemEvent(
`Exec denied (gateway id=${approvalId}, approval-request-failed): ${params.command}`,

View File

@@ -17,9 +17,9 @@ import { parsePreparedSystemRunPayload } from "../infra/system-run-approval-cont
import { logInfo } from "../logger.js";
import {
buildExecApprovalRequesterContext,
resolveRegisteredExecApprovalDecision,
buildExecApprovalTurnSourceContext,
registerExecApprovalRequestForHostOrThrow,
waitForExecApprovalDecision,
} from "./bash-tools.exec-approval-request.js";
import {
DEFAULT_APPROVAL_TIMEOUT_MS,
@@ -243,13 +243,12 @@ export async function executeNodeHostCommand(
preResolvedDecision = registration.finalDecision;
void (async () => {
let decision: string | null = preResolvedDecision ?? null;
let decision: string | null = null;
try {
// Some gateways may return a final decision inline during registration.
// Only call waitDecision when registration did not already carry one.
if (preResolvedDecision === undefined) {
decision = await waitForExecApprovalDecision(approvalId);
}
decision = await resolveRegisteredExecApprovalDecision({
approvalId,
preResolvedDecision,
});
} catch {
emitExecSystemEvent(
`Exec denied (node=${nodeId} id=${approvalId}, approval-request-failed): ${params.command}`,

View File

@@ -6,11 +6,14 @@ import type {
TextContent,
ToolCall,
Tool,
Usage,
} from "@mariozechner/pi-ai";
import { createAssistantMessageEventStream } from "@mariozechner/pi-ai";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { buildStreamErrorAssistantMessage } from "./stream-message-shared.js";
import {
buildAssistantMessage as buildStreamAssistantMessage,
buildStreamErrorAssistantMessage,
buildUsageWithNoCost,
} from "./stream-message-shared.js";
const log = createSubsystemLogger("ollama-stream");
@@ -343,25 +346,15 @@ export function buildAssistantMessage(
const hasToolCalls = toolCalls && toolCalls.length > 0;
const stopReason: StopReason = hasToolCalls ? "toolUse" : "stop";
const usage: Usage = {
input: response.prompt_eval_count ?? 0,
output: response.eval_count ?? 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: (response.prompt_eval_count ?? 0) + (response.eval_count ?? 0),
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
return {
role: "assistant",
return buildStreamAssistantMessage({
model: modelInfo,
content,
stopReason,
api: modelInfo.api,
provider: modelInfo.provider,
model: modelInfo.id,
usage,
timestamp: Date.now(),
};
usage: buildUsageWithNoCost({
input: response.prompt_eval_count ?? 0,
output: response.eval_count ?? 0,
}),
});
}
// ── NDJSON streaming parser ─────────────────────────────────────────────────

View File

@@ -30,7 +30,6 @@ import type {
StopReason,
TextContent,
ToolCall,
Usage,
} from "@mariozechner/pi-ai";
import { createAssistantMessageEventStream, streamSimple } from "@mariozechner/pi-ai";
import {
@@ -43,7 +42,9 @@ import {
} from "./openai-ws-connection.js";
import { log } from "./pi-embedded-runner/logger.js";
import {
buildAssistantMessage,
buildAssistantMessageWithZeroUsage,
buildUsageWithNoCost,
buildStreamErrorAssistantMessage,
} from "./stream-message-shared.js";
@@ -298,25 +299,16 @@ export function buildAssistantMessageFromResponse(
const hasToolCalls = content.some((c) => c.type === "toolCall");
const stopReason: StopReason = hasToolCalls ? "toolUse" : "stop";
const usage: Usage = {
input: response.usage?.input_tokens ?? 0,
output: response.usage?.output_tokens ?? 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: response.usage?.total_tokens ?? 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
return {
role: "assistant",
return buildAssistantMessage({
model: modelInfo,
content,
stopReason,
api: modelInfo.api,
provider: modelInfo.provider,
model: modelInfo.id,
usage,
timestamp: Date.now(),
};
usage: buildUsageWithNoCost({
input: response.usage?.input_tokens ?? 0,
output: response.usage?.output_tokens ?? 0,
totalTokens: response.usage?.total_tokens ?? 0,
}),
});
}
// ─────────────────────────────────────────────────────────────────────────────

View File

@@ -17,10 +17,32 @@ export function buildZeroUsage(): Usage {
};
}
export function buildAssistantMessageWithZeroUsage(params: {
export function buildUsageWithNoCost(params: {
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
totalTokens?: number;
}): Usage {
const input = params.input ?? 0;
const output = params.output ?? 0;
const cacheRead = params.cacheRead ?? 0;
const cacheWrite = params.cacheWrite ?? 0;
return {
input,
output,
cacheRead,
cacheWrite,
totalTokens: params.totalTokens ?? input + output,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
}
export function buildAssistantMessage(params: {
model: StreamModelDescriptor;
content: AssistantMessage["content"];
stopReason: StopReason;
usage: Usage;
timestamp?: number;
}): AssistantMessage {
return {
@@ -30,11 +52,26 @@ export function buildAssistantMessageWithZeroUsage(params: {
api: params.model.api,
provider: params.model.provider,
model: params.model.id,
usage: buildZeroUsage(),
usage: params.usage,
timestamp: params.timestamp ?? Date.now(),
};
}
export function buildAssistantMessageWithZeroUsage(params: {
model: StreamModelDescriptor;
content: AssistantMessage["content"];
stopReason: StopReason;
timestamp?: number;
}): AssistantMessage {
return buildAssistantMessage({
model: params.model,
content: params.content,
stopReason: params.stopReason,
usage: buildZeroUsage(),
timestamp: params.timestamp,
});
}
export function buildStreamErrorAssistantMessage(params: {
model: StreamModelDescriptor;
errorMessage: string;