@nylorun/runtime 0.24.0-beta → 0.25.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 +104 -0
- package/README.md +10 -2
- package/dist/api/a2a/card.d.ts +11 -1
- package/dist/api/a2a/card.js +26 -2
- package/dist/api/a2a/endpoint.js +4 -2
- package/dist/api/a2a/protocol.d.ts +25 -2
- package/dist/api/a2a/protocol.js +20 -3
- package/dist/api/a2a/routes.d.ts +4 -1
- package/dist/api/a2a/routes.js +65 -7
- package/dist/api/ag-ui/endpoint.js +5 -2
- package/dist/api/ag-ui/history.d.ts +6 -1
- package/dist/api/ag-ui/history.js +35 -2
- package/dist/api/ag-ui/routes.js +53 -11
- package/dist/api/components.d.ts +1165 -678
- package/dist/api/components.js +16 -1
- package/dist/api/gate/keys.d.ts +2 -1
- package/dist/api/gate/keys.js +7 -0
- package/dist/api/gate/routes.js +13 -0
- package/dist/api/http/app.js +13 -2
- package/dist/api/http/body.d.ts +3 -3
- package/dist/api/http/body.js +7 -7
- package/dist/api/http/routes/environment-manifests.d.ts +9 -0
- package/dist/api/http/routes/environment-manifests.js +118 -0
- package/dist/api/http/routes/environments.js +61 -12
- package/dist/api/http/routes/reads.d.ts +52 -3
- package/dist/api/http/routes/sessions.js +13 -3
- package/dist/api/http/routes/tenant.js +35 -7
- package/dist/api/inbound-files.d.ts +65 -0
- package/dist/api/inbound-files.js +115 -0
- package/dist/api/openapi.js +18 -3
- package/dist/artifacts/disk.js +20 -14
- package/dist/artifacts/media-types.d.ts +15 -0
- package/dist/artifacts/media-types.js +56 -4
- package/dist/artifacts/parts.d.ts +12 -3
- package/dist/artifacts/parts.js +15 -4
- package/dist/artifacts/workspace.d.ts +4 -4
- package/dist/artifacts/workspace.js +6 -18
- package/dist/core/flow-host.d.ts +1 -1
- package/dist/core/flow-host.js +2 -2
- package/dist/core/sandbox-routes.js +2 -2
- package/dist/core/turn-manifest.d.ts +3 -2
- package/dist/core/turn-manifest.js +29 -5
- package/dist/gates/egress-credentials.js +1 -1
- package/dist/gates/egress.d.ts +13 -19
- package/dist/gates/egress.js +30 -29
- package/dist/gates/http-tool.d.ts +3 -4
- package/dist/gates/http-tool.js +8 -12
- package/dist/gates/journal.js +0 -1
- package/dist/gates/mcp-handler.js +2 -3
- package/dist/gates/policy.js +1 -3
- package/dist/gates/tool-contract.d.ts +1 -1
- package/dist/harness/calls.d.ts +2 -2
- package/dist/harness/calls.js +9 -12
- package/dist/harness/main.js +7 -1
- package/dist/harness/pod.d.ts +6 -1
- package/dist/harness/pod.js +5 -0
- package/dist/harness/service.js +5 -5
- package/dist/harness/setup.d.ts +2 -0
- package/dist/harness/setup.js +25 -1
- package/dist/harness-api/record.d.ts +1 -1
- package/dist/harness-api/record.js +13 -13
- package/dist/harness-api/server.d.ts +27 -4
- package/dist/harness-api/server.js +41 -8
- package/dist/harness-api/tool-journal.d.ts +17 -8
- package/dist/harness-api/tool-journal.js +41 -11
- package/dist/harness-api/workspace.d.ts +7 -5
- package/dist/harness-api/workspace.js +25 -19
- package/dist/host/gates.js +2 -2
- package/dist/host/stack-config.js +1 -1
- package/dist/keys/keys.d.ts +9 -2
- package/dist/keys/keys.js +4 -1
- package/dist/management-openapi.json +366 -61
- package/dist/mcp/connect.d.ts +1 -2
- package/dist/mcp/connect.js +1 -4
- package/dist/mcp/pool.d.ts +1 -4
- package/dist/mcp/pool.js +13 -27
- package/dist/mcp/preview.js +0 -1
- package/dist/mcp/snapshot.d.ts +11 -13
- package/dist/mcp/snapshot.js +27 -38
- package/dist/model/capabilities.d.ts +26 -0
- package/dist/model/capabilities.js +44 -0
- package/dist/model/catalog.d.ts +34 -3
- package/dist/model/catalog.js +38 -2
- package/dist/model/models.d.ts +2 -0
- package/dist/model/models.js +1 -1
- package/dist/model/native-blocks.d.ts +35 -0
- package/dist/model/native-blocks.js +170 -0
- package/dist/model/pi-model.js +106 -94
- package/dist/openapi.json +9554 -5104
- package/dist/record/environment.js +1 -1
- package/dist/sandbox/join.js +4 -0
- package/dist/sandbox/manager.d.ts +32 -14
- package/dist/sandbox/manager.js +132 -93
- package/dist/sandbox/placement.d.ts +1 -1
- package/dist/sandbox/placement.js +2 -2
- package/dist/sandbox/pods/client.d.ts +3 -1
- package/dist/sandbox/pods/lifecycle.d.ts +8 -0
- package/dist/sandbox/pods/lifecycle.js +15 -0
- package/dist/sandbox/pods/spec.js +2 -1
- package/dist/sandbox/resolve.d.ts +5 -3
- package/dist/sandbox/resolve.js +9 -2
- package/dist/sandbox/runtime-tools.d.ts +9 -0
- package/dist/sandbox/runtime-tools.js +16 -0
- package/dist/sandbox/session-sandbox.d.ts +4 -6
- package/dist/sandbox/session-sandbox.js +47 -94
- package/dist/sandbox/setup.d.ts +4 -1
- package/dist/sandbox/setup.js +46 -9
- package/dist/sandbox/share.d.ts +12 -7
- package/dist/sandbox/share.js +27 -25
- package/dist/sandbox/skills.d.ts +8 -6
- package/dist/sandbox/skills.js +24 -23
- package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
- package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
- package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
- package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/reads.js +8 -2
- package/dist/store/postgres/schema.d.ts +217 -0
- package/dist/store/postgres/schema.js +36 -2
- package/dist/store/postgres/store.js +79 -4
- package/dist/store/types.d.ts +68 -14
- package/dist/tenant/advance.js +3 -0
- package/dist/tenant/artifact-tool.d.ts +1 -1
- package/dist/tenant/artifact-tool.js +43 -22
- package/dist/tenant/commands.js +24 -4
- package/dist/tenant/context.d.ts +10 -4
- package/dist/tenant/effects.js +7 -9
- package/dist/tenant/environment-manifests.d.ts +57 -0
- package/dist/tenant/environment-manifests.js +200 -0
- package/dist/tenant/environments.d.ts +54 -47
- package/dist/tenant/environments.js +304 -83
- package/dist/tenant/model-setting.d.ts +14 -0
- package/dist/tenant/model-setting.js +51 -0
- package/dist/tenant/sessions.d.ts +5 -1
- package/dist/tenant/sessions.js +45 -12
- package/dist/tenant/skill-tool.d.ts +3 -3
- package/dist/tenant/skill-tool.js +6 -11
- package/dist/tenant/sweep.js +1 -1
- package/dist/tenant/tool-results.js +1 -1
- package/dist/tenant/tool-search.d.ts +7 -11
- package/dist/tenant/tool-search.js +8 -5
- package/dist/tenant/transcript.d.ts +51 -32
- package/dist/tenant/transcript.js +2 -1
- package/dist/traces/ids.d.ts +0 -1
- package/dist/traces/ids.js +0 -4
- package/dist/traces/map.d.ts +2 -2
- package/dist/traces/map.js +1 -29
- package/dist/vault/coverage.js +11 -13
- package/dist/vault/host-model.d.ts +4 -1
- package/dist/vault/host-model.js +10 -2
- package/dist/vault/service.d.ts +49 -4
- package/dist/vault/service.js +153 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/sandbox/capability.d.ts +0 -3
- package/dist/sandbox/capability.js +0 -10
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TextContent } from "@earendil-works/pi-ai";
|
|
2
|
+
/** The kinds of file a native block carries; images are pi-ai's own. */
|
|
3
|
+
export type NativeFileKind = "pdf" | "office" | "audio" | "video";
|
|
4
|
+
/** A file for one model call: its kind, media type, name and base64 bytes. */
|
|
5
|
+
export interface NativeFile {
|
|
6
|
+
readonly kind: NativeFileKind;
|
|
7
|
+
readonly mediaType: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/** The bytes, base64-encoded. */
|
|
10
|
+
readonly data: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The kinds each pi-ai API has a native block for here: the model inputs an operator may add
|
|
14
|
+
* (`NATIVE_INPUTS`), images aside, so the two tables never disagree.
|
|
15
|
+
*/
|
|
16
|
+
export declare const NATIVE_FILE_KINDS: Readonly<Record<string, readonly NativeFileKind[]>>;
|
|
17
|
+
/** The kinds of file `api` carries in a native block; none for an API without a rewriter. */
|
|
18
|
+
export declare function nativeKindsOf(api: string): readonly NativeFileKind[];
|
|
19
|
+
export declare class NativeBlocks {
|
|
20
|
+
private readonly files;
|
|
21
|
+
/** Why the last `rewrite` could not carry every file; undefined when it did. */
|
|
22
|
+
error?: string;
|
|
23
|
+
/** How many files this call carries. */
|
|
24
|
+
get size(): number;
|
|
25
|
+
/** A text part standing for `file` in pi-ai's context, replaced by `rewrite`. */
|
|
26
|
+
add(file: NativeFile): TextContent;
|
|
27
|
+
/**
|
|
28
|
+
* `payload` (an `onPayload` request body for `api`) with each marker block replaced by its
|
|
29
|
+
* native block. Rewrites in place and returns the same payload; sets `error` when a file
|
|
30
|
+
* could not be carried.
|
|
31
|
+
*/
|
|
32
|
+
rewrite(payload: unknown, api: string): unknown;
|
|
33
|
+
/** The native block for a marker block of ours; undefined for any other block. */
|
|
34
|
+
private replace;
|
|
35
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native file blocks (multimodal messages, M4): a PDF, an office document, audio or video the
|
|
3
|
+
* model reads, carried to the provider in its own block. pi-ai's messages hold only text and
|
|
4
|
+
* images, so a file goes in as a marker text part (`add`), which pi-ai never merges or escapes,
|
|
5
|
+
* and the provider payload pi-ai builds is rewritten before it is sent (`rewrite`, from the
|
|
6
|
+
* call's `onPayload`): each marker block becomes the API's native block. Images never come
|
|
7
|
+
* here: pi-ai carries them itself.
|
|
8
|
+
*
|
|
9
|
+
* One instance serves one model call. pi-ai builds the payload again on every attempt, so
|
|
10
|
+
* `rewrite` can run more than once; each run sets `error` afresh. A file whose kind or format
|
|
11
|
+
* the API has no block for, an API with no rewriter, or a marker left in the payload (pi-ai
|
|
12
|
+
* folded it into a longer string, or dropped it) is `error`: the call fails with it rather than
|
|
13
|
+
* sending a marker to the model.
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { essence } from "../artifacts/media-types.js";
|
|
17
|
+
import { NATIVE_INPUTS } from "./capabilities.js";
|
|
18
|
+
/**
|
|
19
|
+
* The kinds each pi-ai API has a native block for here: the model inputs an operator may add
|
|
20
|
+
* (`NATIVE_INPUTS`), images aside, so the two tables never disagree.
|
|
21
|
+
*/
|
|
22
|
+
export const NATIVE_FILE_KINDS = Object.freeze(Object.fromEntries(Object.entries(NATIVE_INPUTS).map(([api, inputs]) => [
|
|
23
|
+
api,
|
|
24
|
+
Object.freeze(inputs.filter((input) => input !== "text" && input !== "image")),
|
|
25
|
+
])));
|
|
26
|
+
/** The kinds of file `api` carries in a native block; none for an API without a rewriter. */
|
|
27
|
+
export function nativeKindsOf(api) {
|
|
28
|
+
return Object.hasOwn(NATIVE_FILE_KINDS, api) ? NATIVE_FILE_KINDS[api] : [];
|
|
29
|
+
}
|
|
30
|
+
const MARKER = /^\[\[nylorun-file:([0-9a-f-]{36})\]\]$/;
|
|
31
|
+
const MARKER_ANYWHERE = /\[\[nylorun-file:[0-9a-f-]{36}\]\]/;
|
|
32
|
+
const dataUrl = (file) => `data:${essence(file.mediaType)};base64,${file.data}`;
|
|
33
|
+
/** `cache_control` the marker block carried (pi-ai marks the last block of a request). */
|
|
34
|
+
const cacheOf = (block) => block.cache_control === undefined ? {} : { cache_control: block.cache_control };
|
|
35
|
+
const textOfType = (type) => (block) => block.type === type ? block.text : undefined;
|
|
36
|
+
/** `input_audio.format` for a media type: OpenAI Chat Completions takes WAV and MP3 only. */
|
|
37
|
+
function audioFormat(mediaType) {
|
|
38
|
+
switch (essence(mediaType)) {
|
|
39
|
+
case "audio/wav":
|
|
40
|
+
case "audio/x-wav":
|
|
41
|
+
case "audio/wave":
|
|
42
|
+
return "wav";
|
|
43
|
+
case "audio/mpeg":
|
|
44
|
+
case "audio/mp3":
|
|
45
|
+
return "mp3";
|
|
46
|
+
default:
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const anthropic = (file, block) => ({
|
|
51
|
+
type: "document",
|
|
52
|
+
source: { type: "base64", media_type: essence(file.mediaType), data: file.data },
|
|
53
|
+
title: file.name,
|
|
54
|
+
...cacheOf(block),
|
|
55
|
+
});
|
|
56
|
+
const responses = (file) => ({
|
|
57
|
+
type: "input_file",
|
|
58
|
+
filename: file.name,
|
|
59
|
+
file_data: dataUrl(file),
|
|
60
|
+
});
|
|
61
|
+
const completions = (file, block) => {
|
|
62
|
+
switch (file.kind) {
|
|
63
|
+
case "pdf":
|
|
64
|
+
return { type: "file", file: { filename: file.name, file_data: dataUrl(file) }, ...cacheOf(block) };
|
|
65
|
+
case "audio": {
|
|
66
|
+
const format = audioFormat(file.mediaType);
|
|
67
|
+
if (!format)
|
|
68
|
+
return "this API reads WAV and MP3 audio only";
|
|
69
|
+
return { type: "input_audio", input_audio: { data: file.data, format }, ...cacheOf(block) };
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
return { type: "video_url", video_url: { url: dataUrl(file) }, ...cacheOf(block) };
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const google = (file) => ({
|
|
76
|
+
inlineData: { mimeType: essence(file.mediaType), data: file.data },
|
|
77
|
+
});
|
|
78
|
+
const REWRITERS = {
|
|
79
|
+
"anthropic-messages": { marker: textOfType("text"), native: anthropic },
|
|
80
|
+
"openai-responses": { marker: textOfType("input_text"), native: responses },
|
|
81
|
+
"azure-openai-responses": { marker: textOfType("input_text"), native: responses },
|
|
82
|
+
"openai-completions": { marker: textOfType("text"), native: completions },
|
|
83
|
+
"google-generative-ai": { marker: (block) => (block.type === undefined ? block.text : undefined), native: google },
|
|
84
|
+
"google-vertex": { marker: (block) => (block.type === undefined ? block.text : undefined), native: google },
|
|
85
|
+
};
|
|
86
|
+
const isBlock = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
export class NativeBlocks {
|
|
88
|
+
files = new Map();
|
|
89
|
+
/** Why the last `rewrite` could not carry every file; undefined when it did. */
|
|
90
|
+
error;
|
|
91
|
+
/** How many files this call carries. */
|
|
92
|
+
get size() {
|
|
93
|
+
return this.files.size;
|
|
94
|
+
}
|
|
95
|
+
/** A text part standing for `file` in pi-ai's context, replaced by `rewrite`. */
|
|
96
|
+
add(file) {
|
|
97
|
+
const id = randomUUID();
|
|
98
|
+
this.files.set(id, file);
|
|
99
|
+
return { type: "text", text: `[[nylorun-file:${id}]]` };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `payload` (an `onPayload` request body for `api`) with each marker block replaced by its
|
|
103
|
+
* native block. Rewrites in place and returns the same payload; sets `error` when a file
|
|
104
|
+
* could not be carried.
|
|
105
|
+
*/
|
|
106
|
+
rewrite(payload, api) {
|
|
107
|
+
this.error = undefined;
|
|
108
|
+
if (this.files.size === 0)
|
|
109
|
+
return payload;
|
|
110
|
+
const rewriter = Object.hasOwn(REWRITERS, api) ? REWRITERS[api] : undefined;
|
|
111
|
+
if (!rewriter) {
|
|
112
|
+
this.error = `The model's API (${api}) cannot carry files other than images`;
|
|
113
|
+
return payload;
|
|
114
|
+
}
|
|
115
|
+
const kinds = nativeKindsOf(api);
|
|
116
|
+
const placed = new Set();
|
|
117
|
+
let leftover = false;
|
|
118
|
+
const visit = (value) => {
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
for (let index = 0; index < value.length; index++) {
|
|
121
|
+
const item = value[index];
|
|
122
|
+
const replaced = isBlock(item) ? this.replace(item, rewriter, kinds, api) : undefined;
|
|
123
|
+
if (replaced) {
|
|
124
|
+
value[index] = replaced.block;
|
|
125
|
+
placed.add(replaced.id);
|
|
126
|
+
}
|
|
127
|
+
else
|
|
128
|
+
visit(item);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (isBlock(value)) {
|
|
132
|
+
for (const item of Object.values(value))
|
|
133
|
+
visit(item);
|
|
134
|
+
}
|
|
135
|
+
else if (typeof value === "string" && MARKER_ANYWHERE.test(value)) {
|
|
136
|
+
leftover = true;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
visit(payload);
|
|
140
|
+
if (this.error)
|
|
141
|
+
return payload;
|
|
142
|
+
if (leftover || placed.size < this.files.size) {
|
|
143
|
+
const missing = [...this.files].find(([id]) => !placed.has(id))?.[1];
|
|
144
|
+
this.error = missing
|
|
145
|
+
? `File "${missing.name}" did not reach the ${api} request as its own block`
|
|
146
|
+
: `A file marker was left in the ${api} request`;
|
|
147
|
+
}
|
|
148
|
+
return payload;
|
|
149
|
+
}
|
|
150
|
+
/** The native block for a marker block of ours; undefined for any other block. */
|
|
151
|
+
replace(block, rewriter, kinds, api) {
|
|
152
|
+
const text = rewriter.marker(block);
|
|
153
|
+
if (typeof text !== "string")
|
|
154
|
+
return undefined;
|
|
155
|
+
const id = MARKER.exec(text)?.[1];
|
|
156
|
+
const file = id === undefined ? undefined : this.files.get(id);
|
|
157
|
+
if (!file)
|
|
158
|
+
return undefined;
|
|
159
|
+
const refuse = (reason) => {
|
|
160
|
+
this.error ??= `File "${file.name}" (${essence(file.mediaType)}) cannot be sent to ${api}: ${reason}`;
|
|
161
|
+
return undefined;
|
|
162
|
+
};
|
|
163
|
+
if (!kinds.includes(file.kind))
|
|
164
|
+
return refuse(`this API has no block for ${file.kind} files`);
|
|
165
|
+
const native = rewriter.native(file, block);
|
|
166
|
+
if (typeof native === "string")
|
|
167
|
+
return refuse(native);
|
|
168
|
+
return { id: id, block: native };
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/model/pi-model.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { parseJsonWithRepair, retryAssistantCall, } from "@earendil-works/pi-ai";
|
|
2
|
+
import { mediaReferenceOf } from "@nylorun/core/define";
|
|
2
3
|
import { FileUnavailableError, } from "../artifacts/files.js";
|
|
3
|
-
import { essence, isModelImage, isText } from "../artifacts/media-types.js";
|
|
4
|
+
import { essence, inputLabel, isModelImage, isText, modelInputOf, unreadableReason, } from "../artifacts/media-types.js";
|
|
4
5
|
import { scrub } from "../redact.js";
|
|
6
|
+
import { capabilitiesOf } from "./capabilities.js";
|
|
5
7
|
import { classifyAssistantError, failure } from "./classify.js";
|
|
6
8
|
import { modelsFor } from "./models.js";
|
|
9
|
+
import { NativeBlocks, nativeKindsOf } from "./native-blocks.js";
|
|
7
10
|
import { projectSecrets } from "./settings.js";
|
|
8
11
|
const OPENAI_COMPATIBLE_REASONING = /^reasoning(_content|_text)?$/;
|
|
9
12
|
const emptyUsage = () => ({
|
|
@@ -23,8 +26,6 @@ const noAmbientAuth = {
|
|
|
23
26
|
return false;
|
|
24
27
|
},
|
|
25
28
|
};
|
|
26
|
-
class InvalidRequest extends Error {
|
|
27
|
-
}
|
|
28
29
|
/**
|
|
29
30
|
* Node model adapter. Local provider configuration is read only when invoked.
|
|
30
31
|
* A provider failure is returned as a failure outcome; only an abort is thrown.
|
|
@@ -44,7 +45,7 @@ export function piModel(options = {}) {
|
|
|
44
45
|
const root = options.root ?? "";
|
|
45
46
|
const stored = await options.readHostModel?.();
|
|
46
47
|
if (!stored)
|
|
47
|
-
return failure("auth", "Model provider is not configured. Set it in Studio, or run
|
|
48
|
+
return failure("auth", "Model provider is not configured. Set it in Studio, or run nylorun tenant models set.", false);
|
|
48
49
|
const secrets = [
|
|
49
50
|
...projectSecrets(root),
|
|
50
51
|
...credentialSecrets(stored.credential),
|
|
@@ -64,18 +65,22 @@ export function piModel(options = {}) {
|
|
|
64
65
|
: {}),
|
|
65
66
|
};
|
|
66
67
|
const registry = modelsFor(selection, hostCredentialStore(stored, options.writeHostCredential), { environment: false, authContext: noAmbientAuth });
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
69
|
-
return failure("invalid_request", "Unknown model. Run
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
68
|
+
const found = registry.getModel(selection.provider, selection.model);
|
|
69
|
+
if (!found)
|
|
70
|
+
return failure("invalid_request", "Unknown model. Run nylorun tenant models set.", false);
|
|
71
|
+
// What the model reads (protocol 12): its catalog's inputs and the operator's additions.
|
|
72
|
+
// pi-ai drops an image the model's `input` lacks, so the call's model carries an added one.
|
|
73
|
+
const { inputs } = capabilitiesOf(found, selection.custom
|
|
74
|
+
? stored.settings?.inputs
|
|
75
|
+
: stored.addedInputs?.[`${selection.provider}/${selection.model}`]);
|
|
76
|
+
const selected = {
|
|
77
|
+
...found,
|
|
78
|
+
input: inputs.includes("image") ? ["text", "image"] : ["text"],
|
|
79
|
+
};
|
|
80
|
+
// PDFs, Office documents, audio and video go in the API's own blocks: pi-ai builds the
|
|
81
|
+
// request with a marker in each one's place and `onPayload` swaps it for the bytes.
|
|
82
|
+
const natives = new NativeBlocks();
|
|
83
|
+
const request = await buildContext(call, selected, inputs, options, natives);
|
|
79
84
|
// Publish only portable references, never the materialized provider image bytes.
|
|
80
85
|
context.reportPreparedCall?.({
|
|
81
86
|
adapter: "runtime.pi-ai",
|
|
@@ -93,7 +98,20 @@ export function piModel(options = {}) {
|
|
|
93
98
|
timeoutMs: settings.requestTimeoutMs,
|
|
94
99
|
maxRetries: 2,
|
|
95
100
|
maxRetryDelayMs: 60_000,
|
|
101
|
+
...(natives.size > 0
|
|
102
|
+
? {
|
|
103
|
+
// A file that cannot be carried stops the request before it is sent.
|
|
104
|
+
onPayload: (payload) => {
|
|
105
|
+
const rewritten = natives.rewrite(payload, selected.api);
|
|
106
|
+
if (natives.error)
|
|
107
|
+
throw new Error(natives.error);
|
|
108
|
+
return rewritten;
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
96
112
|
};
|
|
113
|
+
// A file the request could not carry fails the call at once: no message, so never retried.
|
|
114
|
+
const nativeFailure = () => failedMessage(selected);
|
|
97
115
|
const attempt = async () => {
|
|
98
116
|
const idle = new AbortController();
|
|
99
117
|
const signal = AbortSignal.any([context.signal, idle.signal]);
|
|
@@ -126,6 +144,8 @@ export function piModel(options = {}) {
|
|
|
126
144
|
}
|
|
127
145
|
}
|
|
128
146
|
const response = await stream.result();
|
|
147
|
+
if (natives.error)
|
|
148
|
+
return nativeFailure();
|
|
129
149
|
if (idle.signal.aborted && !context.signal.aborted)
|
|
130
150
|
return idleTimeout(selected, settings.idleTimeoutMs);
|
|
131
151
|
return response;
|
|
@@ -133,6 +153,8 @@ export function piModel(options = {}) {
|
|
|
133
153
|
catch (error) {
|
|
134
154
|
if (context.signal.aborted)
|
|
135
155
|
throw error;
|
|
156
|
+
if (natives.error)
|
|
157
|
+
return nativeFailure();
|
|
136
158
|
if (idle.signal.aborted)
|
|
137
159
|
return idleTimeout(selected, settings.idleTimeoutMs);
|
|
138
160
|
return {
|
|
@@ -151,6 +173,8 @@ export function piModel(options = {}) {
|
|
|
151
173
|
}, context.signal);
|
|
152
174
|
// An abort is never a failure outcome: the host decides what it means.
|
|
153
175
|
context.signal.throwIfAborted();
|
|
176
|
+
if (natives.error)
|
|
177
|
+
return redacted(failure("invalid_request", natives.error, false));
|
|
154
178
|
if (response.stopReason === "aborted")
|
|
155
179
|
return redacted(failure("transient", "The provider stream was aborted.", true));
|
|
156
180
|
if (response.stopReason === "error") {
|
|
@@ -158,7 +182,7 @@ export function piModel(options = {}) {
|
|
|
158
182
|
return redacted(outcome.code === "auth"
|
|
159
183
|
? {
|
|
160
184
|
...outcome,
|
|
161
|
-
message: `${outcome.message} Update the provider credential in Studio's Model Settings, or run
|
|
185
|
+
message: `${outcome.message} Update the provider credential in Studio's Model Settings, or run nylorun tenant models set.`,
|
|
162
186
|
}
|
|
163
187
|
: outcome);
|
|
164
188
|
}
|
|
@@ -231,87 +255,74 @@ export function piModel(options = {}) {
|
|
|
231
255
|
};
|
|
232
256
|
};
|
|
233
257
|
}
|
|
234
|
-
async function buildContext(call, selected, options) {
|
|
258
|
+
async function buildContext(call, selected, inputs, options, natives) {
|
|
235
259
|
const messages = [];
|
|
236
260
|
const instructions = [];
|
|
237
|
-
|
|
261
|
+
/**
|
|
262
|
+
* A file a media part names (protocol 6, 12), as the model gets it: a text file as a `<file>`
|
|
263
|
+
* block, an image the model reads as an image, a PDF, Office document, audio or video the model
|
|
264
|
+
* reads as a marker for its native block (`natives`), and anything else, or a file that cannot
|
|
265
|
+
* be read, as a note. Its bytes go only into the provider request built here, never into the
|
|
266
|
+
* transcript or the journal, and no file fails the call here.
|
|
267
|
+
*/
|
|
268
|
+
const materialize = async (part) => {
|
|
269
|
+
let name = mediaReferenceOf(part.reference)?.name ?? "file";
|
|
270
|
+
let type = essence(part.mediaType);
|
|
271
|
+
const note = (why) => ({
|
|
272
|
+
type: "text",
|
|
273
|
+
text: `(File ${JSON.stringify(name)}, ${type}, is not shown: ${why}.)`,
|
|
274
|
+
});
|
|
275
|
+
const refused = unreadableReason(part.mediaType, inputs);
|
|
276
|
+
if (refused)
|
|
277
|
+
return note(refused);
|
|
278
|
+
if (!options.files)
|
|
279
|
+
return note("files are unavailable");
|
|
280
|
+
let file;
|
|
281
|
+
try {
|
|
282
|
+
file = await options.files(part.reference);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (!(error instanceof FileUnavailableError))
|
|
286
|
+
throw error;
|
|
287
|
+
return note(error.message.replace(/\.$/u, ""));
|
|
288
|
+
}
|
|
289
|
+
name = file.name;
|
|
290
|
+
type = essence(file.mediaType);
|
|
291
|
+
const stored = unreadableReason(file.mediaType, inputs);
|
|
292
|
+
if (stored)
|
|
293
|
+
return note(stored);
|
|
294
|
+
if (isModelImage(file.mediaType))
|
|
295
|
+
return { type: "image", data: Buffer.from(file.bytes).toString("base64"), mimeType: type };
|
|
296
|
+
if (isText(file.mediaType)) {
|
|
297
|
+
let text;
|
|
298
|
+
try {
|
|
299
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(file.bytes);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return note("it is not UTF-8 text");
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
type: "text",
|
|
306
|
+
text: `<file name=${JSON.stringify(file.name)} type=${JSON.stringify(type)}>\n${text}\n</file>`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const kind = modelInputOf(file.mediaType);
|
|
310
|
+
if (isNativeKind(kind) && nativeKindsOf(selected.api).includes(kind))
|
|
311
|
+
return natives.add({
|
|
312
|
+
kind,
|
|
313
|
+
mediaType: type,
|
|
314
|
+
name: file.name,
|
|
315
|
+
data: Buffer.from(file.bytes).toString("base64"),
|
|
316
|
+
});
|
|
317
|
+
return note(`the Runtime does not send ${inputLabel(kind)} to this model's API (${selected.api})`);
|
|
318
|
+
};
|
|
319
|
+
const content = async (parts) => {
|
|
238
320
|
const result = [];
|
|
239
321
|
for (const part of parts) {
|
|
240
322
|
if (part.type === "text")
|
|
241
323
|
result.push({ type: "text", text: part.text });
|
|
242
|
-
else if (part.type === "media"
|
|
243
|
-
|
|
244
|
-
// reads images. Otherwise, or when it cannot be read, a note: the call still goes.
|
|
245
|
-
const note = (why) => ({
|
|
246
|
-
type: "text",
|
|
247
|
-
text: `(The tool returned an image, ${essence(part.mediaType)}, that is not shown: ${why}.)`,
|
|
248
|
-
});
|
|
249
|
-
if (!selected.input.includes("image") || !isModelImage(part.mediaType)) {
|
|
250
|
-
result.push(note(selected.input.includes("image")
|
|
251
|
-
? "the model reads PNG, JPEG, GIF and WebP images only"
|
|
252
|
-
: "this model does not accept images"));
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (!options.files) {
|
|
256
|
-
result.push(note("files are unavailable"));
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
const file = await options.files(part.reference);
|
|
261
|
-
if (!isModelImage(file.mediaType))
|
|
262
|
-
result.push(note(`it is ${essence(file.mediaType)}`));
|
|
263
|
-
else
|
|
264
|
-
result.push({
|
|
265
|
-
type: "image",
|
|
266
|
-
data: Buffer.from(file.bytes).toString("base64"),
|
|
267
|
-
mimeType: essence(file.mediaType),
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
if (!(error instanceof FileUnavailableError))
|
|
272
|
-
throw error;
|
|
273
|
-
result.push(note(error.message.replace(/\.$/u, "")));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
else if (part.type === "media") {
|
|
277
|
-
// A file the message named by artifact (protocol 6): its bytes go only into the
|
|
278
|
-
// provider request built here, never into the transcript or the journal.
|
|
279
|
-
if (!options.files)
|
|
280
|
-
throw new InvalidRequest("Files are unavailable; pass a file resolver to piModel({ files }).");
|
|
281
|
-
let file;
|
|
282
|
-
try {
|
|
283
|
-
file = await options.files(part.reference);
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
if (error instanceof FileUnavailableError)
|
|
287
|
-
throw new InvalidRequest(error.message);
|
|
288
|
-
throw error;
|
|
289
|
-
}
|
|
290
|
-
if (isModelImage(file.mediaType)) {
|
|
291
|
-
if (!selected.input.includes("image"))
|
|
292
|
-
throw new InvalidRequest("The configured model does not accept images.");
|
|
293
|
-
result.push({
|
|
294
|
-
type: "image",
|
|
295
|
-
data: Buffer.from(file.bytes).toString("base64"),
|
|
296
|
-
mimeType: essence(file.mediaType),
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
else if (isText(file.mediaType)) {
|
|
300
|
-
let text;
|
|
301
|
-
try {
|
|
302
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(file.bytes);
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
throw new InvalidRequest(`${file.name} is not UTF-8 text.`);
|
|
306
|
-
}
|
|
307
|
-
result.push({
|
|
308
|
-
type: "text",
|
|
309
|
-
text: `<file name=${JSON.stringify(file.name)} type=${JSON.stringify(essence(file.mediaType))}>\n${text}\n</file>`,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
else
|
|
313
|
-
throw new InvalidRequest(`The model reads images and text files; ${file.name} is ${essence(file.mediaType)}.`);
|
|
314
|
-
}
|
|
324
|
+
else if (part.type === "media")
|
|
325
|
+
result.push(await materialize(part));
|
|
315
326
|
}
|
|
316
327
|
return result;
|
|
317
328
|
};
|
|
@@ -326,7 +337,7 @@ async function buildContext(call, selected, options) {
|
|
|
326
337
|
toolName: item.toolName,
|
|
327
338
|
isError: item.status !== "completed",
|
|
328
339
|
timestamp: Date.now(),
|
|
329
|
-
content: await content(item.content
|
|
340
|
+
content: await content(item.content),
|
|
330
341
|
});
|
|
331
342
|
}
|
|
332
343
|
else if (item.kind === "message" && item.role === "assistant") {
|
|
@@ -357,6 +368,7 @@ async function buildContext(call, selected, options) {
|
|
|
357
368
|
tools,
|
|
358
369
|
};
|
|
359
370
|
}
|
|
371
|
+
const isNativeKind = (input) => input === "pdf" || input === "office" || input === "audio" || input === "video";
|
|
360
372
|
/**
|
|
361
373
|
* A replayed assistant message keeps the model that produced it, so pi-ai converts
|
|
362
374
|
* history correctly after a model switch (tool-call ids, foreign reasoning). Messages
|
|
@@ -501,7 +513,7 @@ function hostCredentialStore(stored, write) {
|
|
|
501
513
|
return next;
|
|
502
514
|
},
|
|
503
515
|
async delete() {
|
|
504
|
-
throw new Error("Replace the model provider in Studio, or with
|
|
516
|
+
throw new Error("Replace the model provider in Studio, or with nylorun tenant models set.");
|
|
505
517
|
},
|
|
506
518
|
};
|
|
507
519
|
}
|