@nylorun/runtime 0.3.0-beta → 0.5.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +53 -20
- package/dist/adapters/media.d.ts +2 -18
- package/dist/adapters/media.js +2 -52
- package/dist/adapters/observe.js +1 -1
- package/dist/cli.js +11 -6
- package/dist/config.d.ts +17 -10
- package/dist/contracts.d.ts +3 -166
- package/dist/dev-entry.d.ts +1 -0
- package/dist/dev-entry.js +2 -0
- package/dist/dev.js +6 -1
- package/dist/environment.d.ts +2 -0
- package/dist/environment.js +64 -0
- package/dist/index.d.ts +8 -8
- package/dist/index.js +5 -5
- package/dist/launcher.d.ts +1 -0
- package/dist/launcher.js +33 -0
- package/dist/media.d.ts +29 -0
- package/dist/media.js +53 -0
- package/dist/model/auth-store.d.ts +2 -1
- package/dist/model/auth-store.js +9 -2
- package/dist/model/configure.js +60 -30
- package/dist/model/defaults.d.ts +17 -0
- package/dist/model/defaults.js +21 -0
- package/dist/model/http-model.d.ts +12 -0
- package/dist/model/http-model.js +299 -0
- package/dist/model/models.d.ts +2 -2
- package/dist/model/models.js +24 -3
- package/dist/model/pi-model.d.ts +2 -1
- package/dist/model/pi-model.js +53 -8
- package/dist/model/settings.js +35 -13
- package/dist/node/index.d.ts +5 -0
- package/dist/node/index.js +5 -0
- package/dist/node/local-sessions.d.ts +5 -0
- package/dist/node/local-sessions.js +157 -0
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +14 -0
- package/dist/server/ag-ui.d.ts +1 -1
- package/dist/server/delivery.d.ts +24 -0
- package/dist/server/delivery.js +107 -0
- package/dist/server/host.d.ts +11 -6
- package/dist/server/host.js +236 -305
- package/dist/sessions/host.d.ts +29 -0
- package/dist/sessions/host.js +291 -0
- package/dist/sessions/store.d.ts +40 -0
- package/dist/sessions/store.js +30 -0
- package/package.json +11 -4
- package/dist/adapters/journal.d.ts +0 -35
- package/dist/adapters/journal.js +0 -130
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { installNodeModelFactory, installRuntimeLifecycle, } from "./model/defaults.js";
|
|
5
|
+
import { piModel } from "./model/pi-model.js";
|
|
6
|
+
import { loadProjectEnvironment } from "./environment.js";
|
|
7
|
+
export async function start(entry = "dist/src/index.js", development = false) {
|
|
8
|
+
loadProjectEnvironment();
|
|
9
|
+
if (development)
|
|
10
|
+
process.env.NYLORUN_DEV = "1";
|
|
11
|
+
else
|
|
12
|
+
delete process.env.NYLORUN_DEV;
|
|
13
|
+
const port = Number(process.env.PORT ?? "3000");
|
|
14
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
15
|
+
throw new Error("PORT must be an integer between 1 and 65535.");
|
|
16
|
+
const shutdowns = new Set();
|
|
17
|
+
installRuntimeLifecycle((close) => shutdowns.add(close));
|
|
18
|
+
installNodeModelFactory((options) => piModel({ onPreview: options.onPreview, media: options.media }));
|
|
19
|
+
const { default: app } = await import(pathToFileURL(resolve(entry)).href);
|
|
20
|
+
if (!app || typeof app.fetch !== "function")
|
|
21
|
+
throw new Error(`${entry} must export a Hono application with 'export default app' and a callable fetch.`);
|
|
22
|
+
const server = serve({ fetch: app.fetch.bind(app), port }, (info) => {
|
|
23
|
+
console.log(`Server is running on http://localhost:${info.port}`);
|
|
24
|
+
});
|
|
25
|
+
const stop = () => {
|
|
26
|
+
process.removeListener("SIGINT", stop);
|
|
27
|
+
process.removeListener("SIGTERM", stop);
|
|
28
|
+
void Promise.allSettled([...shutdowns].map((close) => close())).then(() => server.close());
|
|
29
|
+
setTimeout(() => process.exit(0), 5_000).unref();
|
|
30
|
+
};
|
|
31
|
+
process.once("SIGINT", stop);
|
|
32
|
+
process.once("SIGTERM", stop);
|
|
33
|
+
}
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const IMAGE_MEDIA_TYPES: readonly string[];
|
|
2
|
+
export declare const MAX_IMAGE_BYTES: number;
|
|
3
|
+
export interface MediaAsset {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly mediaType: string;
|
|
6
|
+
readonly bytes: number;
|
|
7
|
+
readonly kind: "input" | "generated";
|
|
8
|
+
}
|
|
9
|
+
/** Opaque reference retained by Harness and resolved by the configured media adapter. */
|
|
10
|
+
export interface MediaReference {
|
|
11
|
+
readonly agentId: string;
|
|
12
|
+
readonly assetId: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function decodeImageBase64(mediaType: string, base64: string): Uint8Array;
|
|
15
|
+
/** Verify the asserted media type and lightweight file signature for a supported image. */
|
|
16
|
+
export declare function validateImageBytes(mediaType: string, bytes: Uint8Array): void;
|
|
17
|
+
export interface RuntimeMedia {
|
|
18
|
+
saveInput(agentId: string, sessionId: string, mediaType: string, base64: string): Promise<MediaAsset>;
|
|
19
|
+
saveGenerated(agentId: string, sessionId: string, mediaType: string, bytes: Uint8Array): Promise<MediaAsset>;
|
|
20
|
+
dataUrl(reference: MediaReference, sessionId: string): Promise<{
|
|
21
|
+
readonly asset: MediaAsset;
|
|
22
|
+
readonly url: string;
|
|
23
|
+
} | undefined>;
|
|
24
|
+
latestInput(agentId: string, sessionId: string): Promise<MediaAsset | undefined>;
|
|
25
|
+
read(agentId: string, sessionId: string, assetId: string): Promise<{
|
|
26
|
+
asset: MediaAsset;
|
|
27
|
+
bytes: Uint8Array;
|
|
28
|
+
} | undefined>;
|
|
29
|
+
}
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const IMAGE_MEDIA_TYPES = Object.freeze([
|
|
2
|
+
"image/jpeg",
|
|
3
|
+
"image/png",
|
|
4
|
+
"image/webp",
|
|
5
|
+
]);
|
|
6
|
+
export const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
export function decodeImageBase64(mediaType, base64) {
|
|
8
|
+
if (!IMAGE_MEDIA_TYPES.includes(mediaType))
|
|
9
|
+
throw new Error("Only JPEG, PNG, and WebP images are supported.");
|
|
10
|
+
if (typeof base64 !== "string" || base64 === "")
|
|
11
|
+
throw new Error("Image data is required.");
|
|
12
|
+
if (base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(base64))
|
|
13
|
+
throw new Error("Image data must be canonical base64.");
|
|
14
|
+
const decoded = atob(base64);
|
|
15
|
+
const bytes = Uint8Array.from(decoded, (character) => character.charCodeAt(0));
|
|
16
|
+
if (btoa(decoded) !== base64)
|
|
17
|
+
throw new Error("Image data must be canonical base64.");
|
|
18
|
+
validateImageBytes(mediaType, bytes);
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
/** Verify the asserted media type and lightweight file signature for a supported image. */
|
|
22
|
+
export function validateImageBytes(mediaType, bytes) {
|
|
23
|
+
if (!IMAGE_MEDIA_TYPES.includes(mediaType))
|
|
24
|
+
throw new Error("Only JPEG, PNG, and WebP images are supported.");
|
|
25
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_IMAGE_BYTES)
|
|
26
|
+
throw new Error("Images must be no larger than 8 MiB.");
|
|
27
|
+
const signature = mediaType === "image/jpeg"
|
|
28
|
+
? bytes.byteLength >= 3 &&
|
|
29
|
+
bytes[0] === 0xff &&
|
|
30
|
+
bytes[1] === 0xd8 &&
|
|
31
|
+
bytes[2] === 0xff
|
|
32
|
+
: mediaType === "image/png"
|
|
33
|
+
? bytes.byteLength >= 8 &&
|
|
34
|
+
bytes[0] === 0x89 &&
|
|
35
|
+
bytes[1] === 0x50 &&
|
|
36
|
+
bytes[2] === 0x4e &&
|
|
37
|
+
bytes[3] === 0x47 &&
|
|
38
|
+
bytes[4] === 0x0d &&
|
|
39
|
+
bytes[5] === 0x0a &&
|
|
40
|
+
bytes[6] === 0x1a &&
|
|
41
|
+
bytes[7] === 0x0a
|
|
42
|
+
: bytes.byteLength >= 12 &&
|
|
43
|
+
bytes[0] === 0x52 &&
|
|
44
|
+
bytes[1] === 0x49 &&
|
|
45
|
+
bytes[2] === 0x46 &&
|
|
46
|
+
bytes[3] === 0x46 &&
|
|
47
|
+
bytes[8] === 0x57 &&
|
|
48
|
+
bytes[9] === 0x45 &&
|
|
49
|
+
bytes[10] === 0x42 &&
|
|
50
|
+
bytes[11] === 0x50;
|
|
51
|
+
if (!signature)
|
|
52
|
+
throw new Error(`Image bytes do not match ${mediaType}.`);
|
|
53
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { Credential, CredentialInfo, CredentialStore } from "@earendil-work
|
|
|
2
2
|
export declare class ProjectCredentialStore implements CredentialStore {
|
|
3
3
|
#private;
|
|
4
4
|
private readonly file;
|
|
5
|
-
|
|
5
|
+
private readonly legacyFile?;
|
|
6
|
+
constructor(file?: string, legacyFile?: string | undefined);
|
|
6
7
|
read(providerId: string): Promise<Credential | undefined>;
|
|
7
8
|
list(): Promise<readonly CredentialInfo[]>;
|
|
8
9
|
modify(providerId: string, fn: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
|
package/dist/model/auth-store.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
var _a;
|
|
1
2
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
export class ProjectCredentialStore {
|
|
4
5
|
file;
|
|
6
|
+
legacyFile;
|
|
5
7
|
#chain = Promise.resolve();
|
|
6
|
-
constructor(file = join(process.cwd(), ".
|
|
8
|
+
constructor(file = join(process.cwd(), ".nylorun", "auth.json"), legacyFile) {
|
|
7
9
|
this.file = file;
|
|
10
|
+
this.legacyFile = legacyFile;
|
|
8
11
|
}
|
|
9
12
|
async read(providerId) {
|
|
10
13
|
return (await this.#all())[providerId];
|
|
@@ -54,8 +57,11 @@ export class ProjectCredentialStore {
|
|
|
54
57
|
return JSON.parse(await readFile(this.file, "utf8"));
|
|
55
58
|
}
|
|
56
59
|
catch (error) {
|
|
57
|
-
if (error.code
|
|
60
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) {
|
|
61
|
+
if (this.legacyFile)
|
|
62
|
+
return new _a(this.legacyFile).#all();
|
|
58
63
|
return {};
|
|
64
|
+
}
|
|
59
65
|
throw error;
|
|
60
66
|
}
|
|
61
67
|
}
|
|
@@ -83,3 +89,4 @@ export class ProjectCredentialStore {
|
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
}
|
|
92
|
+
_a = ProjectCredentialStore;
|
package/dist/model/configure.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { mkdir, writeFile, rename, rm, rmdir } from "node:fs/promises";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
1
|
import { join } from "node:path";
|
|
4
2
|
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { saveEnvironment } from "../environment.js";
|
|
5
4
|
import { ProjectCredentialStore } from "./auth-store.js";
|
|
6
5
|
import { modelsFor } from "./models.js";
|
|
7
6
|
export class ConfigurationCancelled extends Error {
|
|
@@ -21,7 +20,25 @@ export async function configureProvider(options = {}) {
|
|
|
21
20
|
const signal = controller.signal;
|
|
22
21
|
const forwardAbort = () => controller.abort(options.signal.reason);
|
|
23
22
|
options.signal?.throwIfAborted();
|
|
24
|
-
|
|
23
|
+
let enteredKey;
|
|
24
|
+
let enteredEnvironment = {};
|
|
25
|
+
const oauthStore = new ProjectCredentialStore(join(root, ".nylorun", "auth.json"), join(root, ".env", "auth.json"));
|
|
26
|
+
const store = {
|
|
27
|
+
read: (id) => oauthStore.read(id),
|
|
28
|
+
list: () => oauthStore.list(),
|
|
29
|
+
delete: (id) => oauthStore.delete(id),
|
|
30
|
+
async modify(id, fn) {
|
|
31
|
+
const next = await fn(await oauthStore.read(id));
|
|
32
|
+
if (next?.type === "api_key") {
|
|
33
|
+
if (next.key === "")
|
|
34
|
+
throw new Error("An API key is required.");
|
|
35
|
+
enteredKey = next.key;
|
|
36
|
+
enteredEnvironment = { ...next.env };
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
return oauthStore.modify(id, async () => next);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
25
42
|
const models = modelsFor({ provider: "", model: "" }, store);
|
|
26
43
|
const providers = models.getProviders();
|
|
27
44
|
const prompt = createInterface({
|
|
@@ -59,7 +76,8 @@ export async function configureProvider(options = {}) {
|
|
|
59
76
|
custom: { baseUrl },
|
|
60
77
|
};
|
|
61
78
|
const customModels = modelsFor(selection, store);
|
|
62
|
-
await customModels.
|
|
79
|
+
if (!(await customModels.checkAuth("custom", { signal })))
|
|
80
|
+
await customModels.login("custom", "api_key", interaction());
|
|
63
81
|
await save(selection);
|
|
64
82
|
}
|
|
65
83
|
else {
|
|
@@ -72,7 +90,17 @@ export async function configureProvider(options = {}) {
|
|
|
72
90
|
if (!model)
|
|
73
91
|
throw new Error("Choose a listed model.");
|
|
74
92
|
if (!(await models.checkAuth(chosen.id, { signal }))) {
|
|
75
|
-
|
|
93
|
+
let method = chosen.auth.apiKey
|
|
94
|
+
? "api_key"
|
|
95
|
+
: "oauth";
|
|
96
|
+
if (chosen.auth.apiKey && chosen.auth.oauth) {
|
|
97
|
+
const answer = (await question("Choose authentication: 1. API key (default), 2. OAuth: ")).trim();
|
|
98
|
+
if (answer && !["1", "2"].includes(answer))
|
|
99
|
+
throw new Error("Choose authentication 1 or 2.");
|
|
100
|
+
if (answer === "2")
|
|
101
|
+
method = "oauth";
|
|
102
|
+
}
|
|
103
|
+
await models.login(chosen.id, method, interaction());
|
|
76
104
|
}
|
|
77
105
|
await save({ provider: chosen.id, model: model.id });
|
|
78
106
|
}
|
|
@@ -92,34 +120,36 @@ export async function configureProvider(options = {}) {
|
|
|
92
120
|
function interaction() {
|
|
93
121
|
return {
|
|
94
122
|
signal,
|
|
95
|
-
prompt: async (item) =>
|
|
96
|
-
|
|
123
|
+
prompt: async (item) => {
|
|
124
|
+
if (item.type !== "select")
|
|
125
|
+
return question(item.message + ": ");
|
|
126
|
+
item.options.forEach((option, index) => output.write(`${index + 1}. ${option.label}\n`));
|
|
127
|
+
const answer = (await question(item.message + " ")).trim();
|
|
128
|
+
const option = item.options.find((option) => option.id === answer) ??
|
|
129
|
+
item.options[Number(answer) - 1];
|
|
130
|
+
if (!option)
|
|
131
|
+
throw new Error("Choose a listed authentication option.");
|
|
132
|
+
return option.id;
|
|
133
|
+
},
|
|
134
|
+
notify: (event) => {
|
|
135
|
+
output.write(("url" in event
|
|
136
|
+
? event.url
|
|
137
|
+
: "verificationUri" in event
|
|
138
|
+
? event.verificationUri
|
|
139
|
+
: event.message) + "\n");
|
|
140
|
+
},
|
|
97
141
|
};
|
|
98
142
|
}
|
|
99
143
|
async function save(selection) {
|
|
100
144
|
signal.throwIfAborted();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
await rm(join(root, "config", "model.json"), { force: true });
|
|
111
|
-
try {
|
|
112
|
-
await rmdir(join(root, "config"));
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
if (!(error instanceof Error) ||
|
|
116
|
-
!("code" in error) ||
|
|
117
|
-
!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(String(error.code)))
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
await rm(temporary, { force: true });
|
|
123
|
-
}
|
|
145
|
+
await saveEnvironment(root, {
|
|
146
|
+
...enteredEnvironment,
|
|
147
|
+
MODEL_PROVIDER: selection.provider,
|
|
148
|
+
MODEL: selection.model,
|
|
149
|
+
MODEL_PROVIDER_BASE_URL: selection.custom?.baseUrl,
|
|
150
|
+
...(enteredKey === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { MODEL_PROVIDER_API_KEY: enteredKey }),
|
|
153
|
+
}, signal);
|
|
124
154
|
}
|
|
125
155
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ModelAdapter } from "@nylorun/harness";
|
|
2
|
+
import { type ModelEnvironment } from "./http-model.js";
|
|
3
|
+
export interface ModelPreview {
|
|
4
|
+
readonly invocationId: string;
|
|
5
|
+
readonly text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ModelFactoryOptions {
|
|
8
|
+
readonly environment?: ModelEnvironment;
|
|
9
|
+
readonly media?: import("../media.js").RuntimeMedia;
|
|
10
|
+
readonly onPreview?: (preview: ModelPreview) => void;
|
|
11
|
+
}
|
|
12
|
+
/** Installed only by the Node launcher before application imports. */
|
|
13
|
+
export declare function installNodeModelFactory(factory: (options: ModelFactoryOptions) => ModelAdapter): void;
|
|
14
|
+
export declare function defaultModel(options: ModelFactoryOptions): ModelAdapter;
|
|
15
|
+
export declare function processEnvironment(): ModelEnvironment;
|
|
16
|
+
export declare function installRuntimeLifecycle(register: (close: () => Promise<void>) => void): void;
|
|
17
|
+
export declare function registerRuntimeLifecycle(close: () => Promise<void>): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { httpModel } from "./http-model.js";
|
|
2
|
+
let nodeFactory;
|
|
3
|
+
/** Installed only by the Node launcher before application imports. */
|
|
4
|
+
export function installNodeModelFactory(factory) {
|
|
5
|
+
nodeFactory = factory;
|
|
6
|
+
}
|
|
7
|
+
export function defaultModel(options) {
|
|
8
|
+
return options.environment === undefined && nodeFactory
|
|
9
|
+
? nodeFactory(options)
|
|
10
|
+
: httpModel(options);
|
|
11
|
+
}
|
|
12
|
+
export function processEnvironment() {
|
|
13
|
+
return typeof process === "undefined" ? {} : process.env;
|
|
14
|
+
}
|
|
15
|
+
let runtimeLifecycle;
|
|
16
|
+
export function installRuntimeLifecycle(register) {
|
|
17
|
+
runtimeLifecycle = register;
|
|
18
|
+
}
|
|
19
|
+
export function registerRuntimeLifecycle(close) {
|
|
20
|
+
runtimeLifecycle?.(close);
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModelAdapter } from "@nylorun/harness";
|
|
2
|
+
import type { ModelFactoryOptions } from "./defaults.js";
|
|
3
|
+
export type ModelEnvironment = Readonly<Record<string, string | undefined>>;
|
|
4
|
+
export interface HttpModelOptions extends ModelFactoryOptions {
|
|
5
|
+
readonly provider?: "openai" | "custom" | "anthropic";
|
|
6
|
+
readonly model?: string;
|
|
7
|
+
readonly apiKey?: string;
|
|
8
|
+
readonly baseUrl?: string;
|
|
9
|
+
readonly fetch?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
/** Fetch-only adapter for OpenAI-compatible and Anthropic HTTP endpoints. */
|
|
12
|
+
export declare function httpModel(options?: HttpModelOptions): ModelAdapter;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/** Fetch-only adapter for OpenAI-compatible and Anthropic HTTP endpoints. */
|
|
2
|
+
export function httpModel(options = {}) {
|
|
3
|
+
return async (call, context) => {
|
|
4
|
+
const env = options.environment ??
|
|
5
|
+
(typeof process === "undefined" ? {} : process.env);
|
|
6
|
+
const provider = options.provider ?? env.MODEL_PROVIDER;
|
|
7
|
+
const model = call.model?.id ?? options.model ?? env.MODEL;
|
|
8
|
+
const baseUrl = options.baseUrl ?? env.MODEL_PROVIDER_BASE_URL;
|
|
9
|
+
if (!provider || !model)
|
|
10
|
+
throw new Error("Set both MODEL_PROVIDER and MODEL, or supply a model adapter.");
|
|
11
|
+
if (!["openai", "custom", "anthropic"].includes(provider))
|
|
12
|
+
throw new Error(`The portable HTTP adapter does not support '${provider}'. Supply onModelCall or use the Node piModel adapter.`);
|
|
13
|
+
if (provider === "custom" && !baseUrl)
|
|
14
|
+
throw new Error("MODEL_PROVIDER=custom requires MODEL_PROVIDER_BASE_URL.");
|
|
15
|
+
const apiKey = options.apiKey ??
|
|
16
|
+
env.MODEL_PROVIDER_API_KEY ??
|
|
17
|
+
(provider === "anthropic" ? env.ANTHROPIC_API_KEY : env.OPENAI_API_KEY);
|
|
18
|
+
if (!apiKey)
|
|
19
|
+
throw new Error("Set MODEL_PROVIDER_API_KEY or the provider-native API key variable.");
|
|
20
|
+
const text = (parts) => parts
|
|
21
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
22
|
+
.join("\n");
|
|
23
|
+
if (call.prompt.some((item) => item.content.some((part) => part.type === "media")))
|
|
24
|
+
throw new Error("Supply a media-aware model adapter for media inputs.");
|
|
25
|
+
context.signal.throwIfAborted();
|
|
26
|
+
let body;
|
|
27
|
+
let url;
|
|
28
|
+
let headers;
|
|
29
|
+
const streaming = provider !== "anthropic" && options.onPreview !== undefined;
|
|
30
|
+
if (provider === "anthropic") {
|
|
31
|
+
const messages = call.prompt
|
|
32
|
+
.filter((item) => item.kind !== "instructions")
|
|
33
|
+
.map((item) => {
|
|
34
|
+
if (item.kind === "tool-result")
|
|
35
|
+
return {
|
|
36
|
+
role: "user",
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "tool_result",
|
|
40
|
+
tool_use_id: item.toolCallId,
|
|
41
|
+
content: text(item.content),
|
|
42
|
+
is_error: item.status !== "completed",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
role: item.role === "assistant" ? "assistant" : "user",
|
|
48
|
+
content: item.content.flatMap((part) => part.type === "tool-call"
|
|
49
|
+
? [
|
|
50
|
+
{
|
|
51
|
+
type: "tool_use",
|
|
52
|
+
id: part.id,
|
|
53
|
+
name: part.name,
|
|
54
|
+
input: part.args,
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
: part.type === "text"
|
|
58
|
+
? [{ type: "text", text: part.text }]
|
|
59
|
+
: []),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
body = {
|
|
63
|
+
model,
|
|
64
|
+
messages,
|
|
65
|
+
max_tokens: call.model?.controls?.maxOutputTokens ?? 4096,
|
|
66
|
+
system: call.prompt
|
|
67
|
+
.filter((item) => item.kind === "instructions")
|
|
68
|
+
.map((item) => text(item.content))
|
|
69
|
+
.join("\n"),
|
|
70
|
+
...(call.tools.length
|
|
71
|
+
? {
|
|
72
|
+
tools: call.tools.map((tool) => ({
|
|
73
|
+
name: tool.name,
|
|
74
|
+
description: tool.description ?? "",
|
|
75
|
+
input_schema: tool.inputSchema,
|
|
76
|
+
})),
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
...(call.outputSchema
|
|
80
|
+
? {
|
|
81
|
+
output_config: {
|
|
82
|
+
format: { type: "json_schema", schema: call.outputSchema },
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
...(call.model?.controls?.temperature === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { temperature: call.model.controls.temperature }),
|
|
89
|
+
};
|
|
90
|
+
url = `${(baseUrl ?? "https://api.anthropic.com").replace(/\/$/, "")}/v1/messages`;
|
|
91
|
+
headers = {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
"x-api-key": apiKey,
|
|
94
|
+
"anthropic-version": "2023-06-01",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const messages = call.prompt.map((item) => {
|
|
99
|
+
if (item.kind === "tool-result")
|
|
100
|
+
return {
|
|
101
|
+
role: "tool",
|
|
102
|
+
tool_call_id: item.toolCallId,
|
|
103
|
+
content: text(item.content),
|
|
104
|
+
};
|
|
105
|
+
const calls = item.content
|
|
106
|
+
.filter((part) => part.type === "tool-call")
|
|
107
|
+
.map((part) => ({
|
|
108
|
+
id: part.id,
|
|
109
|
+
type: "function",
|
|
110
|
+
function: { name: part.name, arguments: JSON.stringify(part.args) },
|
|
111
|
+
}));
|
|
112
|
+
return {
|
|
113
|
+
role: item.kind === "instructions" ? "system" : item.role,
|
|
114
|
+
content: text(item.content),
|
|
115
|
+
...(calls.length ? { tool_calls: calls } : {}),
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
body = {
|
|
119
|
+
model,
|
|
120
|
+
messages,
|
|
121
|
+
stream: streaming,
|
|
122
|
+
...(call.tools.length
|
|
123
|
+
? {
|
|
124
|
+
tools: call.tools.map((tool) => ({
|
|
125
|
+
type: "function",
|
|
126
|
+
function: {
|
|
127
|
+
name: tool.name,
|
|
128
|
+
description: tool.description,
|
|
129
|
+
parameters: tool.inputSchema,
|
|
130
|
+
},
|
|
131
|
+
})),
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
...(call.outputSchema
|
|
135
|
+
? {
|
|
136
|
+
response_format: {
|
|
137
|
+
type: "json_schema",
|
|
138
|
+
json_schema: {
|
|
139
|
+
name: "agent_output",
|
|
140
|
+
strict: true,
|
|
141
|
+
schema: call.outputSchema,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
...(call.model?.controls?.temperature === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { temperature: call.model.controls.temperature }),
|
|
149
|
+
...(call.model?.controls?.maxOutputTokens === undefined
|
|
150
|
+
? {}
|
|
151
|
+
: { max_completion_tokens: call.model.controls.maxOutputTokens }),
|
|
152
|
+
};
|
|
153
|
+
url = `${(baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "")}/chat/completions`;
|
|
154
|
+
headers = {
|
|
155
|
+
"content-type": "application/json",
|
|
156
|
+
authorization: `Bearer ${apiKey}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (!["https:", "http:"].includes(new URL(url).protocol))
|
|
160
|
+
throw new Error("Model endpoint must use HTTP(S).");
|
|
161
|
+
context.reportPreparedCall?.({
|
|
162
|
+
adapter: "runtime.http",
|
|
163
|
+
call: JSON.parse(JSON.stringify(body)),
|
|
164
|
+
});
|
|
165
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers,
|
|
168
|
+
body: JSON.stringify(body),
|
|
169
|
+
signal: context.signal,
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok)
|
|
172
|
+
throw new Error(`Model provider returned HTTP ${response.status}. Check the selected model, credentials, and endpoint.`);
|
|
173
|
+
let result;
|
|
174
|
+
if (streaming)
|
|
175
|
+
result = await readOpenAIStream(response, (value) => {
|
|
176
|
+
try {
|
|
177
|
+
void Promise.resolve(options.onPreview?.({
|
|
178
|
+
invocationId: context.invocationId,
|
|
179
|
+
text: value,
|
|
180
|
+
})).catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* Preview delivery cannot affect generation. */
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
else
|
|
187
|
+
result = await response.json();
|
|
188
|
+
context.signal.throwIfAborted();
|
|
189
|
+
const output = [];
|
|
190
|
+
if (provider === "anthropic") {
|
|
191
|
+
for (const part of result.content ?? []) {
|
|
192
|
+
if (part.type === "text")
|
|
193
|
+
output.push(call.outputSchema &&
|
|
194
|
+
!(result.content ?? []).some((item) => item.type === "tool_use")
|
|
195
|
+
? { type: "json", value: JSON.parse(part.text) }
|
|
196
|
+
: { type: "text", text: part.text });
|
|
197
|
+
if (part.type === "tool_use")
|
|
198
|
+
output.push({
|
|
199
|
+
type: "tool-call",
|
|
200
|
+
id: part.id,
|
|
201
|
+
name: part.name,
|
|
202
|
+
args: part.input,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const message = result.choices?.[0]?.message;
|
|
208
|
+
if (!message)
|
|
209
|
+
throw new Error("Model provider returned no message.");
|
|
210
|
+
if (message.content)
|
|
211
|
+
output.push(call.outputSchema && !message.tool_calls?.length
|
|
212
|
+
? { type: "json", value: JSON.parse(message.content) }
|
|
213
|
+
: { type: "text", text: message.content });
|
|
214
|
+
for (const tool of message.tool_calls ?? [])
|
|
215
|
+
output.push({
|
|
216
|
+
type: "tool-call",
|
|
217
|
+
id: tool.id,
|
|
218
|
+
name: tool.function.name,
|
|
219
|
+
args: JSON.parse(tool.function.arguments),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return { output, evidence: { resolvedModel: result.model ?? model } };
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async function readOpenAIStream(response, preview) {
|
|
226
|
+
if (!response.body)
|
|
227
|
+
throw new Error("Model provider returned no stream.");
|
|
228
|
+
const reader = response.body.getReader();
|
|
229
|
+
const decoder = new TextDecoder();
|
|
230
|
+
let buffer = "", content = "", complete = false;
|
|
231
|
+
const calls = new Map();
|
|
232
|
+
const line = (value) => {
|
|
233
|
+
if (!value.startsWith("data:"))
|
|
234
|
+
return;
|
|
235
|
+
const data = value.slice(5).trim();
|
|
236
|
+
if (data === "[DONE]") {
|
|
237
|
+
complete = true;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!data)
|
|
241
|
+
return;
|
|
242
|
+
const chunk = JSON.parse(data);
|
|
243
|
+
if (chunk.error)
|
|
244
|
+
throw new Error("Model provider reported a streaming error.");
|
|
245
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
246
|
+
if (typeof delta?.content === "string") {
|
|
247
|
+
content += delta.content;
|
|
248
|
+
preview(delta.content);
|
|
249
|
+
}
|
|
250
|
+
for (const tool of delta?.tool_calls ?? []) {
|
|
251
|
+
const target = calls.get(tool.index) ?? {
|
|
252
|
+
id: "",
|
|
253
|
+
type: "function",
|
|
254
|
+
function: { name: "", arguments: "" },
|
|
255
|
+
};
|
|
256
|
+
if (tool.id)
|
|
257
|
+
target.id = tool.id;
|
|
258
|
+
if (tool.function?.name)
|
|
259
|
+
target.function.name += tool.function.name;
|
|
260
|
+
if (tool.function?.arguments)
|
|
261
|
+
target.function.arguments += tool.function.arguments;
|
|
262
|
+
calls.set(tool.index, target);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
try {
|
|
266
|
+
while (!complete) {
|
|
267
|
+
const next = await reader.read();
|
|
268
|
+
buffer += decoder.decode(next.value, { stream: !next.done });
|
|
269
|
+
let end;
|
|
270
|
+
while ((end = buffer.indexOf("\n")) >= 0) {
|
|
271
|
+
line(buffer.slice(0, end).replace(/\r$/, ""));
|
|
272
|
+
buffer = buffer.slice(end + 1);
|
|
273
|
+
}
|
|
274
|
+
if (next.done) {
|
|
275
|
+
if (buffer)
|
|
276
|
+
line(buffer);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!complete)
|
|
281
|
+
throw new Error("Model stream ended before completion.");
|
|
282
|
+
return {
|
|
283
|
+
choices: [
|
|
284
|
+
{
|
|
285
|
+
message: {
|
|
286
|
+
content,
|
|
287
|
+
tool_calls: [...calls]
|
|
288
|
+
.sort(([a], [b]) => a - b)
|
|
289
|
+
.map(([, value]) => value),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
await reader.cancel().catch(() => { });
|
|
297
|
+
reader.releaseLock();
|
|
298
|
+
}
|
|
299
|
+
}
|
package/dist/model/models.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CredentialStore } from "@earendil-works/pi-ai";
|
|
2
2
|
export type Selection = Readonly<{
|
|
3
3
|
provider: string;
|
|
4
4
|
model: string;
|
|
@@ -6,4 +6,4 @@ export type Selection = Readonly<{
|
|
|
6
6
|
baseUrl: string;
|
|
7
7
|
}>;
|
|
8
8
|
}>;
|
|
9
|
-
export declare function modelsFor(selection: Selection, credentials:
|
|
9
|
+
export declare function modelsFor(selection: Selection, credentials: CredentialStore): import("@earendil-works/pi-ai").MutableModels;
|