1731 lines
59 KiB
JavaScript
1731 lines
59 KiB
JavaScript
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __commonJS = (cb, mod) => function __require() {
|
|
try {
|
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
} catch (e) {
|
|
throw mod = 0, e;
|
|
}
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
|
|
// node_modules/partial-json/dist/options.js
|
|
var require_options = __commonJS({
|
|
"node_modules/partial-json/dist/options.js"(exports) {
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Allow = exports.ALL = exports.COLLECTION = exports.ATOM = exports.SPECIAL = exports.INF = exports._INFINITY = exports.INFINITY = exports.NAN = exports.BOOL = exports.NULL = exports.OBJ = exports.ARR = exports.NUM = exports.STR = void 0;
|
|
exports.STR = 1;
|
|
exports.NUM = 2;
|
|
exports.ARR = 4;
|
|
exports.OBJ = 8;
|
|
exports.NULL = 16;
|
|
exports.BOOL = 32;
|
|
exports.NAN = 64;
|
|
exports.INFINITY = 128;
|
|
exports._INFINITY = 256;
|
|
exports.INF = exports.INFINITY | exports._INFINITY;
|
|
exports.SPECIAL = exports.NULL | exports.BOOL | exports.INF | exports.NAN;
|
|
exports.ATOM = exports.STR | exports.NUM | exports.SPECIAL;
|
|
exports.COLLECTION = exports.ARR | exports.OBJ;
|
|
exports.ALL = exports.ATOM | exports.COLLECTION;
|
|
exports.Allow = { STR: exports.STR, NUM: exports.NUM, ARR: exports.ARR, OBJ: exports.OBJ, NULL: exports.NULL, BOOL: exports.BOOL, NAN: exports.NAN, INFINITY: exports.INFINITY, _INFINITY: exports._INFINITY, INF: exports.INF, SPECIAL: exports.SPECIAL, ATOM: exports.ATOM, COLLECTION: exports.COLLECTION, ALL: exports.ALL };
|
|
exports.default = exports.Allow;
|
|
}
|
|
});
|
|
|
|
// node_modules/partial-json/dist/index.js
|
|
var require_dist = __commonJS({
|
|
"node_modules/partial-json/dist/index.js"(exports) {
|
|
"use strict";
|
|
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === void 0) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() {
|
|
return m[k];
|
|
} };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === void 0) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __exportStar = exports && exports.__exportStar || function(m, exports2) {
|
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Allow = exports.MalformedJSON = exports.PartialJSON = exports.parseJSON = exports.parse = void 0;
|
|
var options_1 = require_options();
|
|
Object.defineProperty(exports, "Allow", { enumerable: true, get: function() {
|
|
return options_1.Allow;
|
|
} });
|
|
__exportStar(require_options(), exports);
|
|
var PartialJSON = class extends Error {
|
|
};
|
|
exports.PartialJSON = PartialJSON;
|
|
var MalformedJSON = class extends Error {
|
|
};
|
|
exports.MalformedJSON = MalformedJSON;
|
|
function parseJSON(jsonString, allowPartial = options_1.Allow.ALL) {
|
|
if (typeof jsonString !== "string") {
|
|
throw new TypeError(`expecting str, got ${typeof jsonString}`);
|
|
}
|
|
if (!jsonString.trim()) {
|
|
throw new Error(`${jsonString} is empty`);
|
|
}
|
|
return _parseJSON(jsonString.trim(), allowPartial);
|
|
}
|
|
exports.parseJSON = parseJSON;
|
|
var _parseJSON = (jsonString, allow) => {
|
|
const length = jsonString.length;
|
|
let index = 0;
|
|
const markPartialJSON = (msg) => {
|
|
throw new PartialJSON(`${msg} at position ${index}`);
|
|
};
|
|
const throwMalformedError = (msg) => {
|
|
throw new MalformedJSON(`${msg} at position ${index}`);
|
|
};
|
|
const parseAny = () => {
|
|
skipBlank();
|
|
if (index >= length)
|
|
markPartialJSON("Unexpected end of input");
|
|
if (jsonString[index] === '"')
|
|
return parseStr();
|
|
if (jsonString[index] === "{")
|
|
return parseObj();
|
|
if (jsonString[index] === "[")
|
|
return parseArr();
|
|
if (jsonString.substring(index, index + 4) === "null" || options_1.Allow.NULL & allow && length - index < 4 && "null".startsWith(jsonString.substring(index))) {
|
|
index += 4;
|
|
return null;
|
|
}
|
|
if (jsonString.substring(index, index + 4) === "true" || options_1.Allow.BOOL & allow && length - index < 4 && "true".startsWith(jsonString.substring(index))) {
|
|
index += 4;
|
|
return true;
|
|
}
|
|
if (jsonString.substring(index, index + 5) === "false" || options_1.Allow.BOOL & allow && length - index < 5 && "false".startsWith(jsonString.substring(index))) {
|
|
index += 5;
|
|
return false;
|
|
}
|
|
if (jsonString.substring(index, index + 8) === "Infinity" || options_1.Allow.INFINITY & allow && length - index < 8 && "Infinity".startsWith(jsonString.substring(index))) {
|
|
index += 8;
|
|
return Infinity;
|
|
}
|
|
if (jsonString.substring(index, index + 9) === "-Infinity" || options_1.Allow._INFINITY & allow && 1 < length - index && length - index < 9 && "-Infinity".startsWith(jsonString.substring(index))) {
|
|
index += 9;
|
|
return -Infinity;
|
|
}
|
|
if (jsonString.substring(index, index + 3) === "NaN" || options_1.Allow.NAN & allow && length - index < 3 && "NaN".startsWith(jsonString.substring(index))) {
|
|
index += 3;
|
|
return NaN;
|
|
}
|
|
return parseNum();
|
|
};
|
|
const parseStr = () => {
|
|
const start = index;
|
|
let escape = false;
|
|
index++;
|
|
while (index < length && (jsonString[index] !== '"' || escape && jsonString[index - 1] === "\\")) {
|
|
escape = jsonString[index] === "\\" ? !escape : false;
|
|
index++;
|
|
}
|
|
if (jsonString.charAt(index) == '"') {
|
|
try {
|
|
return JSON.parse(jsonString.substring(start, ++index - Number(escape)));
|
|
} catch (e) {
|
|
throwMalformedError(String(e));
|
|
}
|
|
} else if (options_1.Allow.STR & allow) {
|
|
try {
|
|
return JSON.parse(jsonString.substring(start, index - Number(escape)) + '"');
|
|
} catch (e) {
|
|
return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("\\")) + '"');
|
|
}
|
|
}
|
|
markPartialJSON("Unterminated string literal");
|
|
};
|
|
const parseObj = () => {
|
|
index++;
|
|
skipBlank();
|
|
const obj = {};
|
|
try {
|
|
while (jsonString[index] !== "}") {
|
|
skipBlank();
|
|
if (index >= length && options_1.Allow.OBJ & allow)
|
|
return obj;
|
|
const key = parseStr();
|
|
skipBlank();
|
|
index++;
|
|
try {
|
|
const value = parseAny();
|
|
obj[key] = value;
|
|
} catch (e) {
|
|
if (options_1.Allow.OBJ & allow)
|
|
return obj;
|
|
else
|
|
throw e;
|
|
}
|
|
skipBlank();
|
|
if (jsonString[index] === ",")
|
|
index++;
|
|
}
|
|
} catch (e) {
|
|
if (options_1.Allow.OBJ & allow)
|
|
return obj;
|
|
else
|
|
markPartialJSON("Expected '}' at end of object");
|
|
}
|
|
index++;
|
|
return obj;
|
|
};
|
|
const parseArr = () => {
|
|
index++;
|
|
const arr = [];
|
|
try {
|
|
while (jsonString[index] !== "]") {
|
|
arr.push(parseAny());
|
|
skipBlank();
|
|
if (jsonString[index] === ",") {
|
|
index++;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (options_1.Allow.ARR & allow) {
|
|
return arr;
|
|
}
|
|
markPartialJSON("Expected ']' at end of array");
|
|
}
|
|
index++;
|
|
return arr;
|
|
};
|
|
const parseNum = () => {
|
|
if (index === 0) {
|
|
if (jsonString === "-")
|
|
throwMalformedError("Not sure what '-' is");
|
|
try {
|
|
return JSON.parse(jsonString);
|
|
} catch (e) {
|
|
if (options_1.Allow.NUM & allow)
|
|
try {
|
|
return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf("e")));
|
|
} catch (e2) {
|
|
}
|
|
throwMalformedError(String(e));
|
|
}
|
|
}
|
|
const start = index;
|
|
if (jsonString[index] === "-")
|
|
index++;
|
|
while (jsonString[index] && ",]}".indexOf(jsonString[index]) === -1)
|
|
index++;
|
|
if (index == length && !(options_1.Allow.NUM & allow))
|
|
markPartialJSON("Unterminated number literal");
|
|
try {
|
|
return JSON.parse(jsonString.substring(start, index));
|
|
} catch (e) {
|
|
if (jsonString.substring(start, index) === "-")
|
|
markPartialJSON("Not sure what '-' is");
|
|
try {
|
|
return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("e")));
|
|
} catch (e2) {
|
|
throwMalformedError(String(e2));
|
|
}
|
|
}
|
|
};
|
|
const skipBlank = () => {
|
|
while (index < length && " \n\r ".includes(jsonString[index])) {
|
|
index++;
|
|
}
|
|
};
|
|
return parseAny();
|
|
};
|
|
var parse = parseJSON;
|
|
exports.parse = parse;
|
|
}
|
|
});
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/utils/event-stream.js
|
|
var FifoQueue = class {
|
|
incoming = [];
|
|
outgoing = [];
|
|
get length() {
|
|
return this.incoming.length + this.outgoing.length;
|
|
}
|
|
enqueue(value) {
|
|
this.incoming.push(value);
|
|
}
|
|
dequeue() {
|
|
if (this.outgoing.length === 0) {
|
|
while (this.incoming.length > 0) {
|
|
this.outgoing.push(this.incoming.pop());
|
|
}
|
|
}
|
|
return this.outgoing.pop();
|
|
}
|
|
};
|
|
var EventStream = class {
|
|
queue = new FifoQueue();
|
|
waiting = new FifoQueue();
|
|
done = false;
|
|
finalResultPromise;
|
|
resolveFinalResult;
|
|
isComplete;
|
|
extractResult;
|
|
constructor(isComplete, extractResult) {
|
|
this.isComplete = isComplete;
|
|
this.extractResult = extractResult;
|
|
this.finalResultPromise = new Promise((resolve) => {
|
|
this.resolveFinalResult = resolve;
|
|
});
|
|
}
|
|
push(event) {
|
|
if (this.done)
|
|
return;
|
|
if (this.isComplete(event)) {
|
|
this.done = true;
|
|
this.resolveFinalResult(this.extractResult(event));
|
|
}
|
|
const waiter = this.waiting.dequeue();
|
|
if (waiter) {
|
|
waiter({ value: event, done: false });
|
|
} else {
|
|
this.queue.enqueue(event);
|
|
}
|
|
}
|
|
end(result) {
|
|
this.done = true;
|
|
if (result !== void 0) {
|
|
this.resolveFinalResult(result);
|
|
}
|
|
while (this.waiting.length > 0) {
|
|
const waiter = this.waiting.dequeue();
|
|
waiter({ value: void 0, done: true });
|
|
}
|
|
}
|
|
async *[Symbol.asyncIterator]() {
|
|
while (true) {
|
|
if (this.queue.length > 0) {
|
|
yield this.queue.dequeue();
|
|
} else if (this.done) {
|
|
return;
|
|
} else {
|
|
const result = await new Promise((resolve) => this.waiting.enqueue(resolve));
|
|
if (result.done)
|
|
return;
|
|
yield result.value;
|
|
}
|
|
}
|
|
}
|
|
result() {
|
|
return this.finalResultPromise;
|
|
}
|
|
};
|
|
var AssistantMessageEventStream = class extends EventStream {
|
|
#startedAt = Date.now();
|
|
#startedAtMonotonic = performance.now();
|
|
constructor() {
|
|
super((event) => event.type === "done" || event.type === "error", (event) => {
|
|
if (event.type === "done") {
|
|
return event.message;
|
|
} else if (event.type === "error") {
|
|
return event.error;
|
|
}
|
|
throw new Error("Unexpected event type for final result");
|
|
});
|
|
}
|
|
push(event) {
|
|
if (event.type === "done")
|
|
this.#time(event.message);
|
|
else if (event.type === "error")
|
|
this.#time(event.error);
|
|
super.push(event);
|
|
}
|
|
end(result) {
|
|
if (result !== void 0)
|
|
this.#time(result);
|
|
super.end(result);
|
|
}
|
|
#time(message) {
|
|
if (this.done || message.durationMs !== void 0 || message.timestamp < this.#startedAt)
|
|
return;
|
|
message.durationMs = Math.max(0, Math.round(performance.now() - this.#startedAtMonotonic));
|
|
}
|
|
};
|
|
function createAssistantMessageEventStream() {
|
|
return new AssistantMessageEventStream();
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/api/lazy.js
|
|
function createSetupErrorMessage(model, error, timestamp) {
|
|
return {
|
|
role: "assistant",
|
|
content: [],
|
|
api: model.api,
|
|
provider: model.provider,
|
|
model: model.id,
|
|
usage: {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
},
|
|
stopReason: "error",
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
timestamp
|
|
};
|
|
}
|
|
function hasResult(source) {
|
|
return typeof source.result === "function";
|
|
}
|
|
async function forwardStream(target, source, observer, observeResult) {
|
|
let result;
|
|
for await (const event of source) {
|
|
target.push(event);
|
|
observer?.onEvent?.(event);
|
|
if (event.type === "done")
|
|
result = event.message;
|
|
else if (event.type === "error")
|
|
result = event.error;
|
|
}
|
|
if (hasResult(source))
|
|
result = await source.result();
|
|
target.end(result);
|
|
observeResult(result);
|
|
}
|
|
function lazyStream(model, setup, observer) {
|
|
const startedAt = Date.now();
|
|
const outer = new AssistantMessageEventStream();
|
|
let observedResult = false;
|
|
const observeResult = (result) => {
|
|
if (observedResult)
|
|
return;
|
|
observedResult = true;
|
|
observer?.onResult?.(result);
|
|
};
|
|
setup().then((inner) => forwardStream(outer, inner, observer, observeResult)).catch((error) => {
|
|
const message = createSetupErrorMessage(model, error, startedAt);
|
|
const event = { type: "error", reason: "error", error: message };
|
|
outer.push(event);
|
|
observer?.onEvent?.(event);
|
|
outer.end(message);
|
|
observeResult(message);
|
|
});
|
|
return outer;
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/utils/diagnostics.js
|
|
function formatThrownValue(value) {
|
|
if (value instanceof Error)
|
|
return value.message || value.name;
|
|
if (typeof value === "string")
|
|
return value;
|
|
return String(value);
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/utils/models-error.js
|
|
var ModelsError = class extends Error {
|
|
code;
|
|
constructor(code, message, options) {
|
|
super(withCauseDetail(message, options?.cause), options);
|
|
this.name = "ModelsError";
|
|
this.code = code;
|
|
}
|
|
};
|
|
function withCauseDetail(message, cause) {
|
|
if (cause === void 0 || cause === null)
|
|
return message;
|
|
const detail = formatThrownValue(cause).trim();
|
|
if (!detail || message.includes(detail))
|
|
return message;
|
|
return `${message}: ${detail}`;
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/auth/resolve.js
|
|
var DEFAULT_OAUTH_MINIMUM_VALIDITY_MS = 5 * 60 * 1e3;
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/utils/text.js
|
|
function contentText(content, separator = "\n") {
|
|
if (typeof content === "string")
|
|
return content;
|
|
return content.filter((block) => block.type === "text").map((block) => block.text).join(separator);
|
|
}
|
|
function getSystemMessageText(message) {
|
|
const parts = [contentText(message.content)];
|
|
for (const text of Object.values(message.sections ?? {})) {
|
|
if (text !== null)
|
|
parts.push(text);
|
|
}
|
|
return parts.filter((part) => part.length > 0).join("\n\n");
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/utils/model-operations.js
|
|
function getModelType(model) {
|
|
return model.type ?? "chat";
|
|
}
|
|
function isModelType(model, type) {
|
|
return getModelType(model) === type;
|
|
}
|
|
function imageErrorResult(model, error, aborted = false) {
|
|
return {
|
|
api: model.api,
|
|
provider: model.provider,
|
|
model: model.id,
|
|
output: [],
|
|
stopReason: aborted ? "aborted" : "error",
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
function classifierErrorResult(model, error, aborted = false) {
|
|
return {
|
|
api: model.api,
|
|
provider: model.provider,
|
|
model: model.id,
|
|
answers: {},
|
|
stopReason: aborted ? "aborted" : "error",
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/models.js
|
|
var KNOWN_MODEL_TYPES = { chat: true, image: true, classifier: true };
|
|
function hasKnownModelType(model) {
|
|
return Object.hasOwn(KNOWN_MODEL_TYPES, getModelType(model));
|
|
}
|
|
function createProvider(input) {
|
|
const single = input.api && typeof input.api.stream === "function" ? input.api : void 0;
|
|
const byApi = single || !input.api ? void 0 : input.api;
|
|
const images = input.images;
|
|
const classifiers = input.classifiers;
|
|
const streams = single ? [single] : Object.values(byApi ?? {}).filter((entry) => entry !== void 0);
|
|
const imageImplementations = Object.values(images ?? {}).filter((entry) => entry !== void 0);
|
|
const classifierImplementations = Object.values(classifiers ?? {}).filter((entry) => entry !== void 0);
|
|
if (streams.length === 0 && imageImplementations.length === 0 && classifierImplementations.length === 0) {
|
|
throw new Error(`Provider ${input.id}: at least one of "api", "images", or "classifiers" is required.`);
|
|
}
|
|
const baselineModels = input.models;
|
|
let dynamicModels = [];
|
|
const fetchModels = input.fetchModels;
|
|
const currentModels = () => {
|
|
const merged = [...baselineModels];
|
|
for (const model of dynamicModels) {
|
|
const index = merged.findIndex((entry) => getModelType(entry) === getModelType(model) && entry.id === model.id);
|
|
if (index >= 0)
|
|
merged[index] = model;
|
|
else
|
|
merged.push(model);
|
|
}
|
|
return merged;
|
|
};
|
|
const apiFor = (model) => single ?? byApi?.[model.api];
|
|
const dispatch = (model, run) => {
|
|
const streams2 = apiFor(model);
|
|
if (!streams2) {
|
|
return lazyStream(model, async () => {
|
|
throw new ModelsError("stream", `Provider ${input.id} has no API implementation for "${model.api}"`);
|
|
});
|
|
}
|
|
return run(streams2);
|
|
};
|
|
const provider = {
|
|
id: input.id,
|
|
name: input.name ?? input.id,
|
|
baseUrl: input.baseUrl,
|
|
headers: input.headers,
|
|
auth: input.auth,
|
|
getModels: () => currentModels().filter((model) => isModelType(model, "chat")),
|
|
getAllModels: currentModels,
|
|
refreshModels: fetchModels ? async (context) => {
|
|
if (context.stored) {
|
|
const restored = context.stored.models.filter((model) => model.provider === input.id).map((model) => model);
|
|
if (!await context.publish({
|
|
update: () => {
|
|
dynamicModels = restored;
|
|
}
|
|
})) {
|
|
return;
|
|
}
|
|
}
|
|
if (!context.allowNetwork || context.signal.aborted)
|
|
return;
|
|
const fetched = await fetchModels(context);
|
|
if (context.signal.aborted)
|
|
return;
|
|
const refreshed = fetched.filter(hasKnownModelType);
|
|
await context.publish({
|
|
persist: { models: refreshed, checkedAt: Date.now() },
|
|
update: () => {
|
|
dynamicModels = refreshed;
|
|
}
|
|
});
|
|
} : void 0,
|
|
filterModels: input.filterModels,
|
|
filterAllModels: input.filterAllModels,
|
|
stream: (model, context, options) => dispatch(model, (streams2) => streams2.stream(model, context, options)),
|
|
streamSimple: (model, context, options) => dispatch(model, (streams2) => streams2.streamSimple(model, context, options))
|
|
};
|
|
if (streams.some((entry) => entry.fetchDeferred !== void 0)) {
|
|
provider.fetchDeferred = (model, handle, options) => lazyStream(model, async () => {
|
|
const implementation = apiFor(model);
|
|
if (!implementation?.fetchDeferred) {
|
|
throw new ModelsError("provider", `Provider ${input.id} does not support deferred responses for "${model.api}"`);
|
|
}
|
|
return implementation.fetchDeferred(model, handle, options);
|
|
});
|
|
}
|
|
if (streams.some((entry) => entry.cancelDeferred !== void 0)) {
|
|
provider.cancelDeferred = async (model, handle, options) => {
|
|
const implementation = apiFor(model);
|
|
if (!implementation?.cancelDeferred) {
|
|
throw new ModelsError("provider", `Provider ${input.id} cannot cancel deferred responses for "${model.api}"`);
|
|
}
|
|
await implementation.cancelDeferred(model, handle, options);
|
|
};
|
|
}
|
|
if (images && imageImplementations.length > 0) {
|
|
provider.generateImages = async (model, context, options) => {
|
|
const implementation = images[model.api];
|
|
if (!implementation) {
|
|
return imageErrorResult(model, new ModelsError("provider", `Provider ${input.id} has no image generation implementation for "${model.api}"`));
|
|
}
|
|
return implementation.generateImages(model, context, options);
|
|
};
|
|
}
|
|
if (classifiers && classifierImplementations.length > 0) {
|
|
provider.classify = async (model, context, options) => {
|
|
const implementation = classifiers[model.api];
|
|
if (!implementation) {
|
|
return classifierErrorResult(model, new ModelsError("provider", `Provider ${input.id} has no classifier implementation for "${model.api}"`));
|
|
}
|
|
return implementation.classify(model, context, options);
|
|
};
|
|
}
|
|
return provider;
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/utils/json-parse.js
|
|
var import_partial_json = __toESM(require_dist(), 1);
|
|
var VALID_JSON_ESCAPES = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t"]);
|
|
function isControlCharacter(char) {
|
|
const codePoint = char.codePointAt(0);
|
|
return codePoint !== void 0 && codePoint >= 0 && codePoint <= 31;
|
|
}
|
|
function escapeControlCharacter(char) {
|
|
switch (char) {
|
|
case "\b":
|
|
return "\\b";
|
|
case "\f":
|
|
return "\\f";
|
|
case "\n":
|
|
return "\\n";
|
|
case "\r":
|
|
return "\\r";
|
|
case " ":
|
|
return "\\t";
|
|
default:
|
|
return `\\u${char.codePointAt(0)?.toString(16).padStart(4, "0") ?? "0000"}`;
|
|
}
|
|
}
|
|
function repairJson(json) {
|
|
let repaired = "";
|
|
let inString = false;
|
|
for (let index = 0; index < json.length; index++) {
|
|
const char = json[index];
|
|
if (!inString) {
|
|
repaired += char;
|
|
if (char === '"') {
|
|
inString = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (char === '"') {
|
|
repaired += char;
|
|
inString = false;
|
|
continue;
|
|
}
|
|
if (char === "\\") {
|
|
const nextChar = json[index + 1];
|
|
if (nextChar === void 0) {
|
|
repaired += "\\\\";
|
|
continue;
|
|
}
|
|
if (nextChar === "u") {
|
|
const unicodeDigits = json.slice(index + 2, index + 6);
|
|
if (/^[0-9a-fA-F]{4}$/.test(unicodeDigits)) {
|
|
repaired += `\\u${unicodeDigits}`;
|
|
index += 5;
|
|
continue;
|
|
}
|
|
}
|
|
if (VALID_JSON_ESCAPES.has(nextChar)) {
|
|
repaired += `\\${nextChar}`;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
repaired += "\\\\";
|
|
continue;
|
|
}
|
|
repaired += isControlCharacter(char) ? escapeControlCharacter(char) : char;
|
|
}
|
|
return repaired;
|
|
}
|
|
function parseJsonWithRepair(json) {
|
|
try {
|
|
return JSON.parse(json);
|
|
} catch (error) {
|
|
const repairedJson = repairJson(json);
|
|
if (repairedJson !== json) {
|
|
return JSON.parse(repairedJson);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
function parseToolCallArguments(json) {
|
|
if (!json?.trim())
|
|
return { arguments: {} };
|
|
try {
|
|
const parsed = parseJsonWithRepair(json);
|
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
throw new Error("Tool arguments must be a JSON object");
|
|
}
|
|
return { arguments: parsed };
|
|
} catch (error) {
|
|
return {
|
|
arguments: {},
|
|
argumentsError: `${error instanceof Error ? error.message : String(error)}
|
|
|
|
Received arguments:
|
|
${json}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/utils/streaming-json.js
|
|
var MAX_REPORTED_DEPTH = 32;
|
|
var ROOT_SLOT = { kind: "root" };
|
|
var CREDIT_PER_APPEND = 256;
|
|
var CREDIT_PER_CHARACTER = 8;
|
|
var MAX_PENDING_SCALAR_LENGTH = 64;
|
|
var NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
var NUMBER_WITHOUT_EXPONENT_DIGITS = /^(-?(?:0|[1-9]\d*)(?:\.\d+)?)[eE][+-]?$/;
|
|
var LITERALS = [
|
|
["true", true],
|
|
["false", false],
|
|
["null", null]
|
|
];
|
|
var QUOTE = 34;
|
|
var BACKSLASH = 92;
|
|
var StreamingJsonParser = class {
|
|
#root = void 0;
|
|
#done = false;
|
|
#stack = [];
|
|
#string = void 0;
|
|
#scalar = "";
|
|
#scalarSlot = void 0;
|
|
/** Sum of `size + 1` over the open path: what a fresh snapshot copies. */
|
|
#pathCost = 0;
|
|
#credit = 0;
|
|
#dirty = false;
|
|
#snapshot = void 0;
|
|
/** Changes of the piece being read. */
|
|
#changes = [];
|
|
/**
|
|
* Read the next piece of text; returns the changes it made, in order. Costs time linear in `text.length`.
|
|
*/
|
|
append(text) {
|
|
this.#credit += CREDIT_PER_APPEND + CREDIT_PER_CHARACTER * text.length;
|
|
this.#changes = [];
|
|
if (this.#string !== void 0)
|
|
this.#string.change = void 0;
|
|
let index = 0;
|
|
while (index < text.length && !this.#done) {
|
|
const open = this.#string;
|
|
if (open !== void 0)
|
|
index = this.#readString(open, text, index);
|
|
else if (this.#scalar !== "")
|
|
index = this.#readScalar(text, index);
|
|
else
|
|
this.#structural(text[index++]);
|
|
}
|
|
return this.#changes;
|
|
}
|
|
/**
|
|
* The value of the text read so far: `undefined` before a value starts, otherwise a plain JSON value that never
|
|
* changes afterwards. Returns the previous snapshot while the text read since did not change the value, or while a
|
|
* fresh copy of the open path is not yet paid for (see the module comment).
|
|
*/
|
|
snapshot() {
|
|
if (!this.#dirty)
|
|
return this.#snapshot;
|
|
const pending = this.#pendingScalar();
|
|
const cost = this.#pathCost + (pending === void 0 ? 0 : 1);
|
|
if (cost > this.#credit)
|
|
return this.#snapshot;
|
|
this.#credit -= cost;
|
|
this.#dirty = false;
|
|
this.#snapshot = this.#copyOpenPath(pending);
|
|
return this.#snapshot;
|
|
}
|
|
/** Read string characters from `start` until the string ends or the text does; returns the next index. */
|
|
#readString(open, text, start) {
|
|
let runStart = start;
|
|
for (let index = start; index < text.length; index++) {
|
|
const code = text.charCodeAt(index);
|
|
if (open.escape !== "") {
|
|
if (this.#continueEscape(open, text[index])) {
|
|
runStart = index + 1;
|
|
continue;
|
|
}
|
|
runStart = index;
|
|
}
|
|
if (code === QUOTE) {
|
|
if (index > runStart)
|
|
this.#appendString(open, text.slice(runStart, index));
|
|
this.#endString(open);
|
|
return index + 1;
|
|
}
|
|
if (code === BACKSLASH) {
|
|
if (index > runStart)
|
|
this.#appendString(open, text.slice(runStart, index));
|
|
open.escape = "\\";
|
|
runStart = index + 1;
|
|
}
|
|
}
|
|
if (text.length > runStart)
|
|
this.#appendString(open, text.slice(runStart));
|
|
return text.length;
|
|
}
|
|
/** Read number or literal characters from `start` up to the next delimiter; returns the delimiter's index. */
|
|
#readScalar(text, start) {
|
|
let end = start;
|
|
while (end < text.length && !isDelimiter(text[end]))
|
|
end++;
|
|
if (end > start) {
|
|
this.#scalar += text.slice(start, end);
|
|
if (this.#scalarSlot !== void 0)
|
|
this.#dirty = true;
|
|
}
|
|
if (end < text.length)
|
|
this.#endScalar();
|
|
return end;
|
|
}
|
|
/** Handle one character outside strings. */
|
|
#structural(char) {
|
|
switch (char) {
|
|
case " ":
|
|
case " ":
|
|
case "\n":
|
|
case "\r":
|
|
return;
|
|
case '"':
|
|
this.#startString();
|
|
return;
|
|
case "{":
|
|
case "[":
|
|
this.#openContainer(char === "{" ? {} : []);
|
|
return;
|
|
case "}":
|
|
case "]":
|
|
this.#closeContainer();
|
|
return;
|
|
case ":": {
|
|
const frame = this.#stack.at(-1);
|
|
if (frame !== void 0 && !Array.isArray(frame.container) && frame.phase === "colon")
|
|
frame.phase = "value";
|
|
return;
|
|
}
|
|
case ",": {
|
|
const frame = this.#stack.at(-1);
|
|
if (frame !== void 0)
|
|
frame.phase = Array.isArray(frame.container) ? "value" : "key";
|
|
return;
|
|
}
|
|
default:
|
|
this.#scalar = char;
|
|
this.#scalarSlot = this.#valueSlot();
|
|
if (this.#scalarSlot !== void 0)
|
|
this.#dirty = true;
|
|
}
|
|
}
|
|
/** Where a value starting now goes, or `undefined` when this position takes no value. */
|
|
#valueSlot() {
|
|
const frame = this.#stack.at(-1);
|
|
if (frame === void 0)
|
|
return this.#root === void 0 ? ROOT_SLOT : void 0;
|
|
if (Array.isArray(frame.container))
|
|
return { kind: "element", index: frame.container.length };
|
|
if ((frame.phase === "value" || frame.phase === "colon") && frame.key !== void 0) {
|
|
return { kind: "member", key: frame.key };
|
|
}
|
|
return void 0;
|
|
}
|
|
#startString() {
|
|
const frame = this.#stack.at(-1);
|
|
if (frame !== void 0 && !Array.isArray(frame.container) && (frame.phase === "key" || frame.phase === "comma")) {
|
|
this.#string = { target: "key", slot: void 0, text: "", escape: "", path: void 0, change: void 0 };
|
|
return;
|
|
}
|
|
const slot = this.#valueSlot();
|
|
const path = this.#reportedPath(slot);
|
|
this.#string = { target: "value", slot, text: "", escape: "", path, change: void 0 };
|
|
this.#place(slot, "");
|
|
if (path !== void 0)
|
|
this.#string.change = this.#report({ kind: "set", path, value: "" });
|
|
}
|
|
/** The path of a value at `slot` in the innermost open container, when its changes are reported. */
|
|
#reportedPath(slot) {
|
|
if (slot === void 0)
|
|
return void 0;
|
|
if (slot.kind === "root")
|
|
return [];
|
|
const frame = this.#stack.at(-1);
|
|
if (!frame.reportsInside)
|
|
return void 0;
|
|
return [...frame.path, slot.kind === "member" ? slot.key : slot.index];
|
|
}
|
|
#report(change) {
|
|
this.#changes.push(change);
|
|
return change;
|
|
}
|
|
/** Continue an escape with `char`; returns false when `char` is not part of it and must be read normally. */
|
|
#continueEscape(open, char) {
|
|
if (open.escape === "\\") {
|
|
const decoded = SIMPLE_ESCAPES[char];
|
|
if (decoded !== void 0) {
|
|
open.escape = "";
|
|
this.#appendString(open, decoded);
|
|
return true;
|
|
}
|
|
if (char === "u") {
|
|
open.escape = "\\u";
|
|
return true;
|
|
}
|
|
open.escape = "";
|
|
this.#appendString(open, "\\");
|
|
return false;
|
|
}
|
|
if (isHexDigit(char)) {
|
|
open.escape += char;
|
|
if (open.escape.length < 6)
|
|
return true;
|
|
const decoded = String.fromCharCode(Number.parseInt(open.escape.slice(2), 16));
|
|
open.escape = "";
|
|
this.#appendString(open, decoded);
|
|
return true;
|
|
}
|
|
const literal = open.escape;
|
|
open.escape = "";
|
|
this.#appendString(open, literal);
|
|
return false;
|
|
}
|
|
#appendString(open, text) {
|
|
open.text += text;
|
|
if (open.target !== "value")
|
|
return;
|
|
this.#place(open.slot, open.text);
|
|
if (open.path === void 0)
|
|
return;
|
|
const change = open.change;
|
|
if (change !== void 0 && this.#changes.at(-1) === change) {
|
|
if (change.kind === "set")
|
|
change.value = change.value + text;
|
|
else
|
|
change.text += text;
|
|
return;
|
|
}
|
|
open.change = this.#report({ kind: "append", path: open.path, text });
|
|
}
|
|
#endString(open) {
|
|
this.#string = void 0;
|
|
const frame = this.#stack.at(-1);
|
|
if (open.target === "key") {
|
|
if (frame === void 0)
|
|
return;
|
|
frame.key = open.text;
|
|
frame.phase = "colon";
|
|
return;
|
|
}
|
|
this.#valueEnded(frame);
|
|
}
|
|
#endScalar() {
|
|
const token = this.#scalar;
|
|
const slot = this.#scalarSlot;
|
|
this.#scalar = "";
|
|
this.#scalarSlot = void 0;
|
|
const value = scalarValue(token);
|
|
if (value !== void 0) {
|
|
this.#place(slot, value);
|
|
const path = this.#reportedPath(slot);
|
|
if (path !== void 0)
|
|
this.#report({ kind: "set", path, value });
|
|
}
|
|
this.#valueEnded(this.#stack.at(-1));
|
|
}
|
|
/** The value of the unterminated scalar at the end of the text, if it shows. */
|
|
#pendingScalar() {
|
|
const slot = this.#scalarSlot;
|
|
const token = this.#scalar;
|
|
if (slot === void 0 || token === "" || token.length > MAX_PENDING_SCALAR_LENGTH)
|
|
return void 0;
|
|
const value = pendingScalarValue(token);
|
|
return value === void 0 ? void 0 : { slot, value };
|
|
}
|
|
#openContainer(container) {
|
|
const slot = this.#valueSlot();
|
|
const path = this.#reportedPath(slot);
|
|
this.#place(slot, container);
|
|
this.#stack.push({
|
|
container,
|
|
slot,
|
|
path,
|
|
reportsInside: path !== void 0 && path.length < MAX_REPORTED_DEPTH,
|
|
size: 0,
|
|
key: void 0,
|
|
phase: Array.isArray(container) ? "value" : "key"
|
|
});
|
|
this.#pathCost += 1;
|
|
this.#dirty = true;
|
|
if (path !== void 0)
|
|
this.#report({ kind: "set", path, value: Array.isArray(container) ? [] : {} });
|
|
}
|
|
#closeContainer() {
|
|
const frame = this.#stack.pop();
|
|
if (frame === void 0)
|
|
return;
|
|
this.#pathCost -= frame.size + 1;
|
|
this.#dirty = true;
|
|
if (frame.path !== void 0 && !frame.reportsInside) {
|
|
this.#report({ kind: "set", path: frame.path, value: frame.container });
|
|
}
|
|
this.#valueEnded(this.#stack.at(-1));
|
|
}
|
|
/** A value in `frame`, or the root value when there is no frame, has ended. */
|
|
#valueEnded(frame) {
|
|
if (frame === void 0) {
|
|
if (this.#root !== void 0)
|
|
this.#done = true;
|
|
return;
|
|
}
|
|
frame.phase = "comma";
|
|
}
|
|
/** Set the value at `slot` in the open path; nothing for a value in an invalid position. */
|
|
#place(slot, value) {
|
|
if (slot === void 0)
|
|
return;
|
|
this.#dirty = true;
|
|
if (slot.kind === "root") {
|
|
this.#root = value;
|
|
return;
|
|
}
|
|
const frame = this.#stack.at(-1);
|
|
if (slot.kind === "element") {
|
|
const elements = frame.container;
|
|
if (slot.index === elements.length) {
|
|
frame.size++;
|
|
this.#pathCost++;
|
|
}
|
|
elements[slot.index] = value;
|
|
return;
|
|
}
|
|
const members = frame.container;
|
|
if (!Object.hasOwn(members, slot.key)) {
|
|
frame.size++;
|
|
this.#pathCost++;
|
|
}
|
|
setMember(members, slot.key, value);
|
|
}
|
|
/** A fresh snapshot: copies of the open containers, innermost first, each holding the copy of its open child. */
|
|
#copyOpenPath(pending) {
|
|
if (this.#stack.length === 0)
|
|
return pending?.slot.kind === "root" ? pending.value : this.#root;
|
|
let child;
|
|
let childSlot;
|
|
for (let depth = this.#stack.length - 1; depth >= 0; depth--) {
|
|
const frame = this.#stack[depth];
|
|
const copy = Array.isArray(frame.container) ? frame.container.slice() : { ...frame.container };
|
|
if (depth === this.#stack.length - 1 && pending !== void 0)
|
|
setSlot(copy, pending.slot, pending.value);
|
|
if (child !== void 0 && childSlot !== void 0)
|
|
setSlot(copy, childSlot, child);
|
|
child = copy;
|
|
childSlot = frame.slot;
|
|
}
|
|
return childSlot?.kind === "root" ? child : this.#root;
|
|
}
|
|
};
|
|
var ToolCallArgumentsStream = class {
|
|
#text = "";
|
|
#parser = new StreamingJsonParser();
|
|
#snapshot = void 0;
|
|
#arguments = {};
|
|
/** The raw argument text received so far. */
|
|
get text() {
|
|
return this.#text;
|
|
}
|
|
/** Append a piece of argument text; returns the partial arguments, a new object whenever they changed. */
|
|
append(delta) {
|
|
this.#text += delta;
|
|
this.#parser.append(delta);
|
|
return this.#partial();
|
|
}
|
|
/**
|
|
* Take the provider's complete argument text. When it continues the text so far, returns the missing piece as
|
|
* `delta`; otherwise starts over with `text`. Either way returns the partial arguments of `text`.
|
|
*/
|
|
replace(text) {
|
|
if (text.startsWith(this.#text)) {
|
|
const delta = text.slice(this.#text.length);
|
|
return { delta, arguments: delta === "" ? this.#partial() : this.append(delta) };
|
|
}
|
|
this.#text = "";
|
|
this.#parser = new StreamingJsonParser();
|
|
return { delta: void 0, arguments: this.append(text) };
|
|
}
|
|
/** The final arguments: one full parse of the complete text, which reports malformed arguments. */
|
|
finish() {
|
|
return parseToolCallArguments(this.#text);
|
|
}
|
|
#partial() {
|
|
const snapshot = this.#parser.snapshot();
|
|
if (snapshot === this.#snapshot)
|
|
return this.#arguments;
|
|
this.#snapshot = snapshot;
|
|
this.#arguments = typeof snapshot === "object" && snapshot !== null && !Array.isArray(snapshot) ? snapshot : {};
|
|
return this.#arguments;
|
|
}
|
|
};
|
|
var SIMPLE_ESCAPES = {
|
|
'"': '"',
|
|
"\\": "\\",
|
|
"/": "/",
|
|
b: "\b",
|
|
f: "\f",
|
|
n: "\n",
|
|
r: "\r",
|
|
t: " "
|
|
};
|
|
function isDelimiter(char) {
|
|
return char === " " || char === " " || char === "\n" || char === "\r" || char === "," || char === ":" || char === "{" || char === "}" || char === "[" || char === "]" || char === '"';
|
|
}
|
|
function isHexDigit(char) {
|
|
return char >= "0" && char <= "9" || char >= "a" && char <= "f" || char >= "A" && char <= "F";
|
|
}
|
|
function scalarValue(token) {
|
|
for (const [literal, value] of LITERALS)
|
|
if (token === literal)
|
|
return value;
|
|
return NUMBER.test(token) ? Number(token) : void 0;
|
|
}
|
|
function pendingScalarValue(token) {
|
|
for (const [literal, value] of LITERALS)
|
|
if (literal.startsWith(token))
|
|
return value;
|
|
if (NUMBER.test(token))
|
|
return Number(token);
|
|
const mantissa = NUMBER_WITHOUT_EXPONENT_DIGITS.exec(token);
|
|
return mantissa === null ? void 0 : Number(mantissa[1]);
|
|
}
|
|
function setSlot(container, slot, value) {
|
|
if (slot.kind === "element")
|
|
container[slot.index] = value;
|
|
else if (slot.kind === "member")
|
|
setMember(container, slot.key, value);
|
|
}
|
|
function setMember(members, key, value) {
|
|
if (key === "__proto__") {
|
|
Object.defineProperty(members, key, { value, writable: true, enumerable: true, configurable: true });
|
|
} else {
|
|
members[key] = value;
|
|
}
|
|
}
|
|
|
|
// node_modules/@earendil-works/pi-ai/dist/providers/faux.js
|
|
var DEFAULT_API = "faux";
|
|
var DEFAULT_PROVIDER = "faux";
|
|
var DEFAULT_MODEL_ID = "faux-1";
|
|
var DEFAULT_MODEL_NAME = "Faux Model";
|
|
var DEFAULT_BASE_URL = "http://localhost:0";
|
|
var DEFAULT_MIN_TOKEN_SIZE = 3;
|
|
var DEFAULT_MAX_TOKEN_SIZE = 5;
|
|
var DEFAULT_USAGE = {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
};
|
|
function fauxText(text) {
|
|
return { type: "text", text };
|
|
}
|
|
function fauxToolCall(name, arguments_, options = {}) {
|
|
return {
|
|
type: "toolCall",
|
|
id: options.id ?? randomId("tool"),
|
|
name,
|
|
arguments: arguments_
|
|
};
|
|
}
|
|
function normalizeFauxAssistantContent(content) {
|
|
if (typeof content === "string") {
|
|
return [fauxText(content)];
|
|
}
|
|
return Array.isArray(content) ? content : [content];
|
|
}
|
|
function fauxAssistantMessage(content, options = {}) {
|
|
return {
|
|
role: "assistant",
|
|
content: normalizeFauxAssistantContent(content),
|
|
api: DEFAULT_API,
|
|
provider: DEFAULT_PROVIDER,
|
|
model: DEFAULT_MODEL_ID,
|
|
usage: DEFAULT_USAGE,
|
|
stopReason: options.stopReason ?? "stop",
|
|
...options.deferred === void 0 ? {} : { deferred: options.deferred },
|
|
...options.errorMessage === void 0 ? {} : { errorMessage: options.errorMessage },
|
|
...options.responseId === void 0 ? {} : { responseId: options.responseId },
|
|
timestamp: options.timestamp ?? Date.now()
|
|
};
|
|
}
|
|
function estimateTokens(text) {
|
|
return Math.ceil(text.length / 4);
|
|
}
|
|
function randomId(prefix) {
|
|
return `${prefix}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
}
|
|
function contentToText(content) {
|
|
if (typeof content === "string") {
|
|
return content;
|
|
}
|
|
return content.map((block) => {
|
|
if (block.type === "text") {
|
|
return block.text;
|
|
}
|
|
return `[image:${block.mimeType}:${block.data.length}]`;
|
|
}).join("\n");
|
|
}
|
|
function assistantContentToText(content) {
|
|
return content.map((block) => {
|
|
if (block.type === "text") {
|
|
return block.text;
|
|
}
|
|
if (block.type === "thinking") {
|
|
return block.thinking;
|
|
}
|
|
return `${block.name}:${JSON.stringify(block.arguments)}`;
|
|
}).join("\n");
|
|
}
|
|
function toolResultToText(message) {
|
|
return [message.toolName, ...message.content.map((block) => contentToText([block]))].join("\n");
|
|
}
|
|
function messageToText(message) {
|
|
if (message.role === "system") {
|
|
return [
|
|
getSystemMessageText(message),
|
|
...message.toolsRemoved?.map((tool) => `tool-:${JSON.stringify(tool)}`) ?? [],
|
|
...message.toolsAdded?.map((tool) => `tool+:${JSON.stringify(tool)}`) ?? []
|
|
].filter((part) => part.length > 0).join("\n");
|
|
}
|
|
if (message.role === "user") {
|
|
return contentToText(message.content);
|
|
}
|
|
if (message.role === "assistant") {
|
|
return assistantContentToText(message.content);
|
|
}
|
|
return toolResultToText(message);
|
|
}
|
|
function joinedLength(messages, count = messages.length) {
|
|
let length = count > 0 ? (count - 1) * 2 : 0;
|
|
for (let index = 0; index < count; index++)
|
|
length += messages[index].length;
|
|
return length;
|
|
}
|
|
function commonPromptPrefixLength(previous, current) {
|
|
let index = 0;
|
|
while (index < previous.length && index < current.length && previous[index] === current[index])
|
|
index++;
|
|
const rest = (messages) => index === messages.length ? "" : (index > 0 ? "\n\n" : "") + messages.slice(index).join("\n\n");
|
|
return joinedLength(previous, index) + commonPrefixLength(rest(previous), rest(current));
|
|
}
|
|
function commonPrefixLength(a, b) {
|
|
const length = Math.min(a.length, b.length);
|
|
let index = 0;
|
|
while (index < length && a[index] === b[index]) {
|
|
index++;
|
|
}
|
|
return index;
|
|
}
|
|
function withUsageEstimate(message, context, options, promptCache) {
|
|
const prompt = context.messages.map((message2) => `${message2.role}:${messageToText(message2)}`);
|
|
const promptLength = joinedLength(prompt);
|
|
const promptTokens = Math.ceil(promptLength / 4);
|
|
const outputTokens = estimateTokens(assistantContentToText(message.content));
|
|
let input = promptTokens;
|
|
let cacheRead = 0;
|
|
let cacheWrite = 0;
|
|
const sessionId = options?.sessionId;
|
|
if (sessionId && options?.cacheRetention !== "none") {
|
|
const previousPrompt = promptCache.get(sessionId);
|
|
if (previousPrompt) {
|
|
const cachedChars = commonPromptPrefixLength(previousPrompt, prompt);
|
|
cacheRead = Math.ceil(cachedChars / 4);
|
|
cacheWrite = Math.max(0, promptTokens - cacheRead);
|
|
} else {
|
|
cacheWrite = promptTokens;
|
|
}
|
|
input = 0;
|
|
promptCache.set(sessionId, prompt);
|
|
}
|
|
return {
|
|
...message,
|
|
usage: {
|
|
input,
|
|
output: outputTokens,
|
|
cacheRead,
|
|
cacheWrite,
|
|
totalTokens: input + outputTokens + cacheRead + cacheWrite,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
}
|
|
};
|
|
}
|
|
function splitStringByTokenSize(text, minTokenSize, maxTokenSize) {
|
|
const chunks = [];
|
|
let index = 0;
|
|
while (index < text.length) {
|
|
const tokenSize = minTokenSize + Math.floor(Math.random() * (maxTokenSize - minTokenSize + 1));
|
|
const charSize = Math.max(1, tokenSize * 4);
|
|
chunks.push(text.slice(index, index + charSize));
|
|
index += charSize;
|
|
}
|
|
return chunks.length > 0 ? chunks : [""];
|
|
}
|
|
function cloneMessage(message, api, provider, modelId) {
|
|
const cloned = structuredClone(message);
|
|
return {
|
|
...cloned,
|
|
api,
|
|
provider,
|
|
model: modelId,
|
|
timestamp: cloned.timestamp ?? Date.now(),
|
|
usage: cloned.usage ?? DEFAULT_USAGE
|
|
};
|
|
}
|
|
function createDeferredMessage(model, handle) {
|
|
return {
|
|
role: "assistant",
|
|
content: [],
|
|
api: model.api,
|
|
provider: model.provider,
|
|
model: model.id,
|
|
usage: DEFAULT_USAGE,
|
|
stopReason: "deferred",
|
|
deferred: handle,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
function createErrorMessage(error, api, provider, modelId) {
|
|
return {
|
|
role: "assistant",
|
|
content: [],
|
|
api,
|
|
provider,
|
|
model: modelId,
|
|
usage: DEFAULT_USAGE,
|
|
stopReason: "error",
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
function createAbortedMessage(partial) {
|
|
return {
|
|
...partial,
|
|
stopReason: "aborted",
|
|
errorMessage: "Request was aborted",
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
function scheduleChunk(chunk, tokensPerSecond) {
|
|
if (!tokensPerSecond || tokensPerSecond <= 0) {
|
|
return new Promise((resolve) => queueMicrotask(resolve));
|
|
}
|
|
const delayMs = estimateTokens(chunk) / tokensPerSecond * 1e3;
|
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
async function streamWithDeltas(stream, message, minTokenSize, maxTokenSize, tokensPerSecond, signal) {
|
|
const partial = { ...message, content: [], stopReason: "pending" };
|
|
if (signal?.aborted) {
|
|
const aborted = createAbortedMessage(partial);
|
|
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
stream.end(aborted);
|
|
return;
|
|
}
|
|
stream.push({ type: "start", partial: { ...partial } });
|
|
for (let index = 0; index < message.content.length; index++) {
|
|
if (signal?.aborted) {
|
|
const aborted = createAbortedMessage(partial);
|
|
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
stream.end(aborted);
|
|
return;
|
|
}
|
|
const block = message.content[index];
|
|
if (block.type === "thinking") {
|
|
partial.content = [...partial.content, { type: "thinking", thinking: "" }];
|
|
stream.push({ type: "thinking_start", contentIndex: index, partial: { ...partial } });
|
|
for (const chunk of splitStringByTokenSize(block.thinking, minTokenSize, maxTokenSize)) {
|
|
await scheduleChunk(chunk, tokensPerSecond);
|
|
if (signal?.aborted) {
|
|
const aborted = createAbortedMessage(partial);
|
|
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
stream.end(aborted);
|
|
return;
|
|
}
|
|
partial.content[index].thinking += chunk;
|
|
stream.push({ type: "thinking_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
|
|
}
|
|
stream.push({
|
|
type: "thinking_end",
|
|
contentIndex: index,
|
|
content: block.thinking,
|
|
partial: { ...partial }
|
|
});
|
|
continue;
|
|
}
|
|
if (block.type === "text") {
|
|
partial.content = [...partial.content, { type: "text", text: "" }];
|
|
stream.push({ type: "text_start", contentIndex: index, partial: { ...partial } });
|
|
for (const chunk of splitStringByTokenSize(block.text, minTokenSize, maxTokenSize)) {
|
|
await scheduleChunk(chunk, tokensPerSecond);
|
|
if (signal?.aborted) {
|
|
const aborted = createAbortedMessage(partial);
|
|
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
stream.end(aborted);
|
|
return;
|
|
}
|
|
partial.content[index].text += chunk;
|
|
stream.push({ type: "text_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
|
|
}
|
|
stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: { ...partial } });
|
|
continue;
|
|
}
|
|
partial.content = [...partial.content, { type: "toolCall", id: block.id, name: block.name, arguments: {} }];
|
|
stream.push({ type: "toolcall_start", contentIndex: index, partial: { ...partial } });
|
|
const toolArguments = new ToolCallArgumentsStream();
|
|
for (const chunk of splitStringByTokenSize(JSON.stringify(block.arguments), minTokenSize, maxTokenSize)) {
|
|
await scheduleChunk(chunk, tokensPerSecond);
|
|
if (signal?.aborted) {
|
|
const aborted = createAbortedMessage(partial);
|
|
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
stream.end(aborted);
|
|
return;
|
|
}
|
|
partial.content[index].arguments = toolArguments.append(chunk);
|
|
stream.push({ type: "toolcall_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
|
|
}
|
|
partial.content[index].arguments = block.arguments;
|
|
stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: { ...partial } });
|
|
}
|
|
if (message.stopReason === "pending") {
|
|
throw new Error("Faux response ended without a stop reason");
|
|
}
|
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
stream.push({ type: "error", reason: message.stopReason, error: message });
|
|
stream.end(message);
|
|
return;
|
|
}
|
|
stream.push({ type: "done", reason: message.stopReason, message });
|
|
stream.end(message);
|
|
}
|
|
function createFauxCore(options) {
|
|
const api = options.api ?? randomId(DEFAULT_API);
|
|
const provider = options.provider ?? DEFAULT_PROVIDER;
|
|
const minTokenSize = Math.max(1, Math.min(options.tokenSize?.min ?? DEFAULT_MIN_TOKEN_SIZE, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE));
|
|
const maxTokenSize = Math.max(minTokenSize, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE);
|
|
let pendingResponses = [];
|
|
const tokensPerSecond = options.tokensPerSecond;
|
|
const state = { callCount: 0, deferredFetchCount: 0, cancelledDeferred: [] };
|
|
const promptCache = /* @__PURE__ */ new Map();
|
|
const deferredResponses = /* @__PURE__ */ new Map();
|
|
const modelDefinitions = options.models?.length ? options.models : [
|
|
{
|
|
id: DEFAULT_MODEL_ID,
|
|
name: DEFAULT_MODEL_NAME,
|
|
reasoning: false,
|
|
input: ["text", "image"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 128e3,
|
|
maxTokens: 16384
|
|
}
|
|
];
|
|
const models = modelDefinitions.map((definition) => ({
|
|
id: definition.id,
|
|
name: definition.name ?? definition.id,
|
|
api,
|
|
provider,
|
|
baseUrl: DEFAULT_BASE_URL,
|
|
reasoning: definition.reasoning ?? false,
|
|
input: definition.input ?? ["text", "image"],
|
|
inputLimits: definition.inputLimits,
|
|
cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: definition.contextWindow ?? 128e3,
|
|
maxTokens: definition.maxTokens ?? 16384
|
|
}));
|
|
const resolveResponse = async (step, context, streamOptions, requestModel) => {
|
|
const resolved = typeof step === "function" ? await step(context, streamOptions, state, requestModel) : step;
|
|
return withUsageEstimate(cloneMessage(resolved, api, provider, requestModel.id), context, streamOptions, promptCache);
|
|
};
|
|
const stream = (requestModel, context, streamOptions) => {
|
|
const outer = createAssistantMessageEventStream();
|
|
const step = pendingResponses.shift();
|
|
state.callCount++;
|
|
queueMicrotask(async () => {
|
|
try {
|
|
await streamOptions?.onResponse?.({ status: 200, headers: {} }, requestModel);
|
|
if (!step) {
|
|
let message2 = createErrorMessage(new Error("No more faux responses queued"), api, provider, requestModel.id);
|
|
message2 = withUsageEstimate(message2, context, streamOptions, promptCache);
|
|
outer.push({ type: "error", reason: "error", error: message2 });
|
|
outer.end(message2);
|
|
return;
|
|
}
|
|
if (streamOptions?.deferred) {
|
|
const handle = {
|
|
provider: requestModel.provider,
|
|
modelId: requestModel.id,
|
|
api: requestModel.api,
|
|
id: randomId("deferred"),
|
|
...options.deferred?.pollAfterMs !== void 0 ? { pollAfterMs: options.deferred.pollAfterMs } : {}
|
|
};
|
|
deferredResponses.set(handle.id, {
|
|
handle,
|
|
step,
|
|
context,
|
|
options: streamOptions,
|
|
model: requestModel,
|
|
pendingFetches: Math.max(0, Math.floor(options.deferred?.pendingFetches ?? 0)),
|
|
cancelled: false
|
|
});
|
|
await streamWithDeltas(outer, createDeferredMessage(requestModel, handle), minTokenSize, maxTokenSize, tokensPerSecond, streamOptions.signal);
|
|
return;
|
|
}
|
|
const message = await resolveResponse(step, context, streamOptions, requestModel);
|
|
await streamWithDeltas(outer, message, minTokenSize, maxTokenSize, tokensPerSecond, streamOptions?.signal);
|
|
} catch (error) {
|
|
const message = createErrorMessage(error, api, provider, requestModel.id);
|
|
outer.push({ type: "error", reason: "error", error: message });
|
|
outer.end(message);
|
|
}
|
|
});
|
|
return outer;
|
|
};
|
|
const streamSimple = (streamModel, context, streamOptions) => stream(streamModel, context, streamOptions);
|
|
const fetchDeferred = (requestModel, handle, fetchOptions) => {
|
|
const outer = createAssistantMessageEventStream();
|
|
state.deferredFetchCount++;
|
|
queueMicrotask(async () => {
|
|
try {
|
|
await fetchOptions?.onResponse?.({ status: 200, headers: {} }, requestModel);
|
|
const entry = deferredResponses.get(handle.id);
|
|
if (!entry || entry.handle.provider !== handle.provider || entry.handle.modelId !== handle.modelId || entry.handle.api !== handle.api) {
|
|
throw new Error(`Unknown faux deferred response: ${handle.id}`);
|
|
}
|
|
if (entry.cancelled)
|
|
throw new Error(`Faux deferred response was cancelled: ${handle.id}`);
|
|
if (entry.pendingFetches > 0) {
|
|
entry.pendingFetches--;
|
|
await streamWithDeltas(outer, createDeferredMessage(requestModel, entry.handle), minTokenSize, maxTokenSize, tokensPerSecond, fetchOptions?.signal);
|
|
return;
|
|
}
|
|
if (!entry.final) {
|
|
const { deferred: _deferred, signal: _submissionSignal, onResponse: _submissionOnResponse, ...submissionOptions } = entry.options ?? {};
|
|
try {
|
|
entry.final = await resolveResponse(entry.step, entry.context, submissionOptions, entry.model);
|
|
} catch (error) {
|
|
entry.final = createErrorMessage(error, api, provider, entry.model.id);
|
|
}
|
|
}
|
|
await streamWithDeltas(outer, entry.final, minTokenSize, maxTokenSize, tokensPerSecond, fetchOptions?.signal);
|
|
} catch (error) {
|
|
const message = createErrorMessage(error, api, provider, requestModel.id);
|
|
outer.push({ type: "error", reason: "error", error: message });
|
|
outer.end(message);
|
|
}
|
|
});
|
|
return outer;
|
|
};
|
|
const cancelDeferred = async (requestModel, handle, cancelOptions) => {
|
|
state.cancelledDeferred.push(structuredClone(handle));
|
|
const entry = deferredResponses.get(handle.id);
|
|
if (entry)
|
|
entry.cancelled = true;
|
|
await cancelOptions?.onResponse?.({ status: 200, headers: {} }, requestModel);
|
|
};
|
|
function getModel(requestedModelId) {
|
|
if (!requestedModelId) {
|
|
return models[0];
|
|
}
|
|
return models.find((candidate) => candidate.id === requestedModelId);
|
|
}
|
|
return {
|
|
api,
|
|
provider,
|
|
models,
|
|
stream,
|
|
streamSimple,
|
|
fetchDeferred,
|
|
cancelDeferred,
|
|
getModel,
|
|
state,
|
|
setResponses(responses) {
|
|
pendingResponses = [...responses];
|
|
},
|
|
appendResponses(responses) {
|
|
pendingResponses.push(...responses);
|
|
},
|
|
getPendingResponseCount() {
|
|
return pendingResponses.length;
|
|
}
|
|
};
|
|
}
|
|
function fauxProvider(options = {}) {
|
|
const core = createFauxCore(options);
|
|
const provider = createProvider({
|
|
id: core.provider,
|
|
auth: { apiKey: { name: "Faux", resolve: async () => ({ auth: {} }) } },
|
|
models: core.models,
|
|
api: {
|
|
stream: core.stream,
|
|
streamSimple: core.streamSimple,
|
|
fetchDeferred: core.fetchDeferred,
|
|
cancelDeferred: core.cancelDeferred
|
|
}
|
|
});
|
|
return {
|
|
provider,
|
|
api: core.api,
|
|
models: core.models,
|
|
getModel: core.getModel,
|
|
state: core.state,
|
|
setResponses: core.setResponses,
|
|
appendResponses: core.appendResponses,
|
|
getPendingResponseCount: core.getPendingResponseCount
|
|
};
|
|
}
|
|
|
|
// p4-test.ts
|
|
var main = `fn greeting(name: &str) -> String { format!("hello {name}") }
|
|
fn main() {
|
|
let Some(name) = std::env::args().nth(1) else { std::process::exit(7) };
|
|
println!("{}", serde_json::json!({"greeting": greeting(&name)}));
|
|
}
|
|
#[test]
|
|
fn greeting_test() { assert_eq!(greeting("world"), "hello world", "P4_expected_failure"); }
|
|
`;
|
|
var write = (path, text) => `cat > ${path} <<'P4_FILE'
|
|
${text}
|
|
P4_FILE
|
|
`;
|
|
var phases = [
|
|
`set -e
|
|
mkdir -p /src/demo-cli/src /src/demo-node /tmp
|
|
cd /src/demo-cli
|
|
if test -d target; then cargo clean; fi
|
|
${write("Cargo.toml", '[package]\nname="demo-cli"\nversion="0.1.0"\nedition="2024"\n[workspace]\n')}
|
|
${write("src/main.rs", main.replace('"hello world", "P4_expected_failure"', '"hello wrong", "P4_expected_failure"'))}
|
|
cargo add serde_json
|
|
cargo fmt
|
|
set +e
|
|
cargo test -- --nocapture > /tmp/p4-fail.stdout 2> /tmp/p4-fail.stderr
|
|
code=$?
|
|
set -e
|
|
cat /tmp/p4-fail.stdout /tmp/p4-fail.stderr
|
|
test "$code" = 101
|
|
case "$(cat /tmp/p4-fail.stderr)" in *P4_expected_failure*) ;; *) exit 1 ;; esac
|
|
printf 'P4_PHASE_0_OK
|
|
'`,
|
|
`set -e
|
|
cd /src/demo-cli
|
|
${write("src/main.rs", main)}
|
|
cargo fmt
|
|
cargo test -- --nocapture
|
|
cargo build --release
|
|
test -f target/wasm32-wasip2/release/demo-cli.wasm
|
|
sh -c '/src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm world' > /tmp/p4-cli.json
|
|
case "$(cat /tmp/p4-cli.json)" in *'"greeting":"hello world"'*) ;; *) exit 1 ;; esac
|
|
set +e
|
|
sh -c '/src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm'
|
|
code=$?
|
|
set -e
|
|
test "$code" = 7
|
|
printf 'P4_PHASE_1_OK
|
|
'`,
|
|
`set -e
|
|
${write("/src/demo-node/package.json", '{"scripts":{"test":"node test.cjs"}}')}
|
|
${write("/src/demo-node/test.cjs", 'const fs = require("node:fs");\nrequire("node:assert/strict").equal(2 + 2, 4);\nfs.writeFileSync("result.txt", "P4 npm test passed\\n");')}
|
|
id=$(box create --ttl 600)
|
|
printf '%s
|
|
' "$id" > /tmp/p4-box-id
|
|
trap 'box rm "$id"' EXIT
|
|
box put "$id" /src/demo-node /work
|
|
box exec "$id" --timeout 120 -- sh -c 'cd /work && npm test'
|
|
box get "$id" /work/result.txt /src/demo-node/result.txt
|
|
test "$(cat /src/demo-node/result.txt)" = 'P4 npm test passed'
|
|
box rm "$id"
|
|
trap - EXIT
|
|
printf 'P4_PHASE_2_OK
|
|
'`,
|
|
`set -e
|
|
undo list --conversation 1 > /tmp/p4-undo.json
|
|
box ls > /tmp/p4-boxes
|
|
printf 'P4_PHASE_3_OK
|
|
'`
|
|
];
|
|
function p4(host) {
|
|
const faux = fauxProvider({ provider: "openai", models: [{ id: "gpt-6.1-sol" }], tokensPerSecond: 1e6 });
|
|
const respond = (transcript) => {
|
|
faux.appendResponses([respond]);
|
|
const messages = transcript.messages;
|
|
const input = messages.findLast((message) => message.role === "user");
|
|
const text = (message) => typeof message.content === "string" ? message.content : (message.content ?? []).filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
const round = /P4 start (\d+)/.exec(text(input))?.[1];
|
|
if (!round) throw new Error(`Unexpected P4 input: ${JSON.stringify(input)}`);
|
|
const last = messages.findLast((message) => message.role !== "system");
|
|
let phase = 0;
|
|
if (last.role !== "user") {
|
|
const prior = /p4-\d+-(\d+)/.exec(last.toolCallId)?.[1];
|
|
if (last.isError || prior === void 0 || !text(last).includes(`P4_PHASE_${prior}_OK`)) throw new Error(`P4 bash failed: ${JSON.stringify(last)}`);
|
|
phase = Number(prior) + 1;
|
|
}
|
|
if (phase === phases.length) return fauxAssistantMessage(`P4_COMPLETE_${round}`);
|
|
return fauxAssistantMessage([fauxToolCall("bash", { command: phases[phase], timeout: 300 }, { id: `p4-${round}-${phase}` })], { stopReason: "toolUse" });
|
|
};
|
|
faux.setResponses([respond]);
|
|
host.models().setProvider(faux.provider);
|
|
return { name: "p4" };
|
|
}
|
|
export {
|
|
p4 as default
|
|
};
|