@axflow/models
Version:
Zero-dependency, modular SDK for building robust natural language applications
252 lines (248 loc) • 7.98 kB
JavaScript
// src/shared/http.ts
var HttpError = class extends Error {
code;
response;
constructor(message, response) {
super(message);
this.code = response.status;
this.response = response;
}
};
function isHttpError(e) {
return e instanceof HttpError;
}
async function POST(url, options) {
const _fetch = options?.fetch ?? fetch;
if (typeof _fetch === "undefined") {
throw new Error(
"Environment does not support fetch (https://developer.mozilla.org/en-US/docs/Web/API/fetch)"
);
}
const init = { ...options, method: "POST" };
delete init.fetch;
const response = await _fetch(url, init);
if (!response.ok) {
throw new HttpError(`Request failed with status code ${response.status}`, response);
}
return response;
}
// src/shared/stream.ts
function IterableToStream(iterable) {
if (typeof ReadableStream.from === "function") {
return ReadableStream.from(iterable);
}
const iterator = iterable[Symbol.asyncIterator]();
return new ReadableStream({
async pull(controller) {
const { done, value } = await iterator.next();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
}
});
}
function StreamToIterable(stream) {
return stream[Symbol.asyncIterator] ? stream[Symbol.asyncIterator]() : createIterable(stream);
}
async function* createIterable(stream) {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
yield value;
}
} finally {
reader.releaseLock();
}
}
var NdJsonStream = class {
/**
* These are the proper headers for newline-delimited JSON streams.
*
* @see http://ndjson.org
*/
static headers = Object.freeze({ "content-type": "application/x-ndjson; charset=utf-8" });
/**
* Transforms a stream of JSON-serializable objects to stream of newline-delimited JSON.
*
* Each object is wrapped with an object that specifies the `type` and references
* the `value`. The `type` is one of `chunk` or `data`. A type of `chunk` means that
* the `value` corresponds to chunks from the input stream. A type of `data` means
* that the `value` corresponds to the additional data provided as the second argument
* to this function.
*
*
* Example WITHOUT additional data:
*
* const chunk = { key: 'value' };
* const stream = new ReadableStream({start(con) { con.enqueue(chunk); con.close() }});
* const ndJsonStream = NdJsonStream.encode(stream);
* const entries = [];
* for await (const chunk of stream) {
* entry.push(new TextDecoder().decode(chunk));
* }
* console.log(entries); // [ "{\"type\":\"chunk\",\"value\":{\"key\":\"value\"}}\n" ]
*
*
* Example WITH additional data:
*
* const chunk = { key: 'value' };
* const stream = new ReadableStream({start(con) { con.enqueue(chunk); con.close() }});
* const ndJsonStream = NdJsonStream.encode(stream, { data: [{ extra: 'data' }] });
* const entries = [];
* for await (const chunk of stream) {
* entry.push(new TextDecoder().decode(chunk));
* }
* console.log(entries); // [ "{\"type\":\"data\",\"value\":{\"extra\":\"data\"}}\n", "{\"type\":\"chunk\",\"value\":{\"key\":\"value\"}}\n" ]
*
* @see http://ndjson.org
*
* @param stream A readable stream of chunks to encode as newline-delimited JSON.
* @param options
* @param options.data Additional data to enqueue to the output stream. If data is a `Promise`, the stream will wait for it to resolve and enqueue its resolved values before closing.
* @returns A readable stream of newline-delimited JSON.
*/
static encode(stream, options) {
const data = options?.data ?? [];
const dataIsPromise = data instanceof Promise;
const encoder = new TextEncoder();
function serialize(obj) {
const serialized = JSON.stringify(obj);
return encoder.encode(`${serialized}
`);
}
const ndJsonEncode = new TransformStream({
start(controller) {
if (dataIsPromise) {
return;
}
if (!Array.isArray(data)) {
throw new Error(
`Expected options.data to be an array of JSON-serializable objects but it was ${typeof data}`
);
}
for (const value of data) {
controller.enqueue(serialize({ type: "data", value }));
}
},
async transform(value, controller) {
controller.enqueue(serialize({ type: "chunk", value }));
},
async flush(controller) {
if (!dataIsPromise) {
return;
}
const result = await Promise.resolve(data);
if (!Array.isArray(result)) {
throw new Error(
`Expected options.data to resolve to an array of JSON-serializable objects but it was ${typeof result}`
);
}
for (const value of result) {
controller.enqueue(serialize({ type: "data", value }));
}
}
});
return stream.pipeThrough(ndJsonEncode);
}
/**
* Transforms a stream of newline-delimited JSON to a stream of objects.
*
* @see http://ndjson.org
*
* @param stream A readable stream of newline-delimited JSON objects.
* @returns A readable stream of objects.
*/
static decode(stream) {
let buffer = [];
const decoder = new TextDecoder();
const parser = new TransformStream({
transform(bytes, controller) {
const chunk = decoder.decode(bytes);
for (let i = 0, len = chunk.length; i < len; ++i) {
const isChunkSeparator = chunk[i] === "\n";
if (!isChunkSeparator) {
buffer.push(chunk[i]);
continue;
}
const line = buffer.join("").trimEnd();
controller.enqueue(JSON.parse(line));
buffer = [];
}
}
});
return stream.pipeThrough(parser);
}
};
var StreamingJsonResponse = class extends Response {
/**
* Create a `Response` object that streams newline-delimited JSON objects.
*
* Example
*
* export async function POST(request: Request) {
* const req = await request.json();
* const stream = await OpenAIChat.stream(req, { apiKey: OPENAI_API_KEY });
* return new StreamingJsonResponse(stream, {
* map: (chunk) => chunk.choices[0].delta.content ?? ''
* data: [{ stream: "additional" }, { data: "here" }]
* });
* }
*
* @see http://ndjson.org
*
* @param stream A readable stream of chunks to encode as newline-delimited JSON.
* @param options
* @param options.status HTTP response status.
* @param options.statusText HTTP response status text.
* @param options.headers HTTP response headers.
* @param options.data Additional data to enqueue to the output stream. If data is a `Promise`, the stream will wait for it to resolve and enqueue its resolved values before closing.
*/
constructor(stream, options) {
options ??= {};
const ndjson = NdJsonStream.encode(stream, {
data: options.data
});
super(ndjson, {
status: options.status,
statusText: options.statusText,
headers: { ...options.headers, ...NdJsonStream.headers }
});
}
};
// src/shared/types.ts
var toolCallWithDefaults = (toolCall) => ({
index: toolCall.index ?? 0,
id: toolCall.id ?? "",
type: "function",
function: {
name: toolCall.function?.name ?? "",
arguments: toolCall.function?.arguments ?? ""
}
});
// src/shared/message.ts
var createMessage = (message) => {
const defaults = {
id: crypto.randomUUID(),
role: "user",
created: Date.now(),
content: ""
};
return Object.assign(defaults, message);
};
export {
HttpError,
IterableToStream,
NdJsonStream,
POST,
StreamToIterable,
StreamingJsonResponse,
createMessage,
isHttpError,
toolCallWithDefaults
};