@nylorun/runtime 0.23.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 +252 -0
- package/README.md +87 -28
- package/dist/adapters/execution/restate.js +1 -1
- package/dist/adapters/replication/pgoutput.d.ts +2 -0
- package/dist/adapters/replication/pgoutput.js +6 -3
- package/dist/adapters/sandbox/local.d.ts +9 -1
- package/dist/adapters/sandbox/local.js +16 -4
- package/dist/adapters/sandbox/virtual.js +4 -1
- package/dist/api/a2a/card.d.ts +11 -1
- package/dist/api/a2a/card.js +26 -2
- package/dist/api/a2a/endpoint.js +9 -3
- package/dist/api/a2a/protocol.d.ts +25 -2
- package/dist/api/a2a/protocol.js +20 -3
- package/dist/api/a2a/routes.d.ts +6 -1
- package/dist/api/a2a/routes.js +67 -9
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +8 -3
- package/dist/api/ag-ui/history.d.ts +6 -1
- package/dist/api/ag-ui/history.js +39 -3
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +63 -20
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +5327 -1091
- package/dist/api/components.js +40 -16
- package/dist/api/gate/keys.d.ts +2 -1
- package/dist/api/gate/keys.js +14 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +52 -3
- package/dist/api/http/app.js +17 -6
- package/dist/api/http/body.d.ts +3 -3
- package/dist/api/http/body.js +7 -7
- package/dist/api/http/define.d.ts +6 -0
- package/dist/api/http/define.js +13 -1
- package/dist/api/http/respond.js +1 -1
- package/dist/api/http/routes/artifacts.js +35 -8
- package/dist/api/http/routes/coverage.d.ts +9 -0
- package/dist/api/http/routes/coverage.js +34 -0
- 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.d.ts +11 -0
- package/dist/api/http/routes/environments.js +332 -0
- package/dist/api/http/routes/mcp.d.ts +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.js +1 -1
- package/dist/api/http/routes/reads.d.ts +94 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +24 -9
- package/dist/api/http/routes/tenant.js +65 -15
- package/dist/api/inbound-files.d.ts +65 -0
- package/dist/api/inbound-files.js +115 -0
- package/dist/api/openapi.js +48 -28
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +569 -0
- package/dist/artifacts/export.d.ts +1 -1
- package/dist/artifacts/export.js +2 -3
- package/dist/artifacts/folders.d.ts +6 -3
- package/dist/artifacts/folders.js +17 -12
- 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/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +5 -5
- package/dist/artifacts/workspace.js +7 -19
- package/dist/core/flow-host.d.ts +3 -4
- package/dist/core/flow-host.js +5 -2
- package/dist/core/provider.d.ts +3 -2
- package/dist/core/provider.js +5 -37
- package/dist/core/sandbox-routes.d.ts +2 -21
- package/dist/core/sandbox-routes.js +11 -44
- package/dist/core/turn-manifest.d.ts +3 -2
- package/dist/core/turn-manifest.js +29 -5
- package/dist/execution/types.d.ts +1 -1
- package/dist/gates/contract.d.ts +3 -24
- package/dist/gates/contract.js +1 -36
- package/dist/gates/egress-credentials.d.ts +9 -0
- package/dist/gates/egress-credentials.js +11 -4
- package/dist/gates/egress.d.ts +22 -1
- package/dist/gates/egress.js +116 -10
- package/dist/gates/fixture-model.d.ts +28 -0
- package/dist/gates/fixture-model.js +92 -0
- package/dist/gates/handler.js +4 -4
- package/dist/gates/http-tool.d.ts +9 -6
- package/dist/gates/http-tool.js +11 -17
- package/dist/gates/in-process.d.ts +5 -0
- package/dist/gates/in-process.js +12 -1
- package/dist/gates/journal.d.ts +30 -0
- package/dist/gates/journal.js +51 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +7 -4
- package/dist/gates/meter.d.ts +10 -2
- package/dist/gates/meter.js +20 -5
- package/dist/gates/policy.d.ts +49 -0
- package/dist/gates/policy.js +132 -0
- package/dist/gates/tenant-vaults.d.ts +7 -7
- package/dist/gates/tenant-vaults.js +5 -5
- package/dist/gates/tool-calls.d.ts +10 -5
- package/dist/gates/tool-calls.js +28 -10
- package/dist/gates/tool-client.d.ts +8 -0
- package/dist/gates/tool-client.js +27 -11
- package/dist/gates/tool-contract.d.ts +11 -69
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +14 -7
- package/dist/harness/calls.js +30 -24
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +24 -5
- package/dist/harness/pod.d.ts +39 -0
- package/dist/harness/pod.js +46 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +17 -9
- package/dist/harness/setup.d.ts +29 -0
- package/dist/harness/setup.js +174 -0
- package/dist/harness/ws-client.js +3 -2
- package/dist/harness/ws-port.d.ts +1 -5
- package/dist/harness/ws-port.js +1 -5
- package/dist/harness-api/in-process.js +3 -3
- package/dist/harness-api/record.d.ts +15 -1
- package/dist/harness-api/record.js +180 -50
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +35 -4
- package/dist/harness-api/server.js +130 -17
- package/dist/harness-api/start.d.ts +10 -3
- package/dist/harness-api/start.js +40 -10
- package/dist/harness-api/tool-journal.d.ts +105 -0
- package/dist/harness-api/tool-journal.js +132 -0
- package/dist/harness-api/tool-status.d.ts +13 -0
- package/dist/harness-api/tool-status.js +26 -0
- package/dist/harness-api/workspace.d.ts +12 -7
- package/dist/harness-api/workspace.js +47 -25
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +54 -5
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +64 -0
- package/dist/host/stack-config.js +62 -2
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +13 -6
- package/dist/keys/keys.js +9 -8
- package/dist/management-openapi.json +1264 -2171
- package/dist/mcp/connect.d.ts +26 -22
- package/dist/mcp/connect.js +30 -34
- package/dist/mcp/pool.d.ts +3 -6
- package/dist/mcp/pool.js +14 -28
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -21
- package/dist/mcp/snapshot.d.ts +16 -24
- package/dist/mcp/snapshot.js +33 -45
- 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 +114 -94
- package/dist/openapi.json +12822 -3943
- package/dist/reads/types.d.ts +16 -3
- package/dist/record/append.d.ts +3 -1
- package/dist/record/append.js +1 -0
- package/dist/record/envelope.d.ts +5 -1
- package/dist/record/envelope.js +7 -3
- package/dist/record/environment.d.ts +25 -0
- package/dist/record/environment.js +44 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/sandbox/disk-fs.d.ts +95 -0
- package/dist/sandbox/disk-fs.js +206 -0
- package/dist/sandbox/join.d.ts +17 -18
- package/dist/sandbox/join.js +46 -15
- package/dist/sandbox/manager.d.ts +47 -21
- package/dist/sandbox/manager.js +162 -107
- package/dist/sandbox/placement.d.ts +14 -13
- package/dist/sandbox/placement.js +31 -14
- package/dist/sandbox/pods/client.d.ts +9 -2
- package/dist/sandbox/pods/lifecycle.d.ts +36 -27
- package/dist/sandbox/pods/lifecycle.js +29 -26
- package/dist/sandbox/pods/reconcile.js +18 -12
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +17 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +37 -27
- package/dist/sandbox/resolve.js +73 -41
- 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 +64 -0
- package/dist/sandbox/setup.js +160 -0
- package/dist/sandbox/share.d.ts +21 -26
- package/dist/sandbox/share.js +36 -63
- package/dist/sandbox/skills.d.ts +8 -6
- package/dist/sandbox/skills.js +24 -23
- package/dist/sandbox/tenant-config.d.ts +12 -14
- package/dist/sandbox/tenant-config.js +12 -17
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
- package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
- package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
- 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 +56 -0
- package/dist/store/postgres/reads.js +123 -17
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/record.d.ts +7 -0
- package/dist/store/postgres/record.js +23 -2
- package/dist/store/postgres/schema.d.ts +1126 -736
- package/dist/store/postgres/schema.js +136 -73
- package/dist/store/postgres/store.js +195 -67
- package/dist/store/types.d.ts +165 -50
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +118 -42
- package/dist/tenant/artifact-tool.d.ts +1 -1
- package/dist/tenant/artifact-tool.js +46 -26
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +115 -14
- package/dist/tenant/context.d.ts +56 -24
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +44 -29
- package/dist/tenant/environment-manifests.d.ts +57 -0
- package/dist/tenant/environment-manifests.js +200 -0
- package/dist/tenant/environments.d.ts +174 -0
- package/dist/tenant/environments.js +1044 -0
- package/dist/tenant/harness-tokens.d.ts +21 -0
- package/dist/tenant/harness-tokens.js +61 -0
- package/dist/tenant/host-token.d.ts +1 -1
- package/dist/tenant/host-token.js +2 -4
- package/dist/tenant/identity-file.d.ts +4 -4
- package/dist/tenant/identity-file.js +17 -8
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/model-setting.d.ts +14 -0
- package/dist/tenant/model-setting.js +51 -0
- package/dist/tenant/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +7 -1
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +18 -14
- package/dist/tenant/sessions.js +99 -143
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/skill-tool.d.ts +3 -3
- package/dist/tenant/skill-tool.js +6 -11
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/sweep.js +13 -7
- 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 +245 -36
- package/dist/tenant/transcript.js +64 -12
- package/dist/tenant/turn-timing.d.ts +43 -0
- package/dist/tenant/turn-timing.js +39 -0
- package/dist/tenant/types.d.ts +2 -0
- package/dist/traces/ids.d.ts +41 -0
- package/dist/traces/ids.js +81 -0
- package/dist/traces/key.d.ts +6 -0
- package/dist/traces/key.js +37 -0
- package/dist/traces/map.d.ts +113 -0
- package/dist/traces/map.js +289 -0
- package/dist/traces/options.d.ts +7 -0
- package/dist/traces/options.js +1 -0
- package/dist/traces/otlp.d.ts +42 -0
- package/dist/traces/otlp.js +84 -0
- package/dist/traces/projector.d.ts +58 -0
- package/dist/traces/projector.js +200 -0
- package/dist/traces/read.d.ts +23 -0
- package/dist/traces/read.js +16 -0
- package/dist/vault/coverage.d.ts +26 -0
- package/dist/vault/coverage.js +62 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/host-model.d.ts +4 -1
- package/dist/vault/host-model.js +10 -2
- package/dist/vault/service.d.ts +92 -48
- package/dist/vault/service.js +462 -376
- package/dist/vault/sources.d.ts +5 -16
- package/dist/vault/sources.js +6 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/package.json +13 -13
- package/dist/api/http/routes/sandboxes.d.ts +0 -9
- package/dist/api/http/routes/sandboxes.js +0 -153
- package/dist/api/http/routes/vaults.d.ts +0 -10
- package/dist/api/http/routes/vaults.js +0 -139
- package/dist/record/sandbox.d.ts +0 -25
- package/dist/record/sandbox.js +0 -43
- package/dist/sandbox/capability.d.ts +0 -3
- package/dist/sandbox/capability.js +0 -10
- package/dist/tenant/sandboxes.d.ts +0 -123
- package/dist/tenant/sandboxes.js +0 -566
|
@@ -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.
|
|
@@ -38,10 +39,13 @@ export function piModel(options = {}) {
|
|
|
38
39
|
};
|
|
39
40
|
return async (call, context) => {
|
|
40
41
|
context.signal.throwIfAborted();
|
|
42
|
+
// The first stream event's time, from here (R7.1): the meter keeps it on the usage row.
|
|
43
|
+
const calledAt = Date.now();
|
|
44
|
+
let firstEventAt;
|
|
41
45
|
const root = options.root ?? "";
|
|
42
46
|
const stored = await options.readHostModel?.();
|
|
43
47
|
if (!stored)
|
|
44
|
-
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);
|
|
45
49
|
const secrets = [
|
|
46
50
|
...projectSecrets(root),
|
|
47
51
|
...credentialSecrets(stored.credential),
|
|
@@ -61,18 +65,22 @@ export function piModel(options = {}) {
|
|
|
61
65
|
: {}),
|
|
62
66
|
};
|
|
63
67
|
const registry = modelsFor(selection, hostCredentialStore(stored, options.writeHostCredential), { environment: false, authContext: noAmbientAuth });
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
66
|
-
return failure("invalid_request", "Unknown model. Run
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
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);
|
|
76
84
|
// Publish only portable references, never the materialized provider image bytes.
|
|
77
85
|
context.reportPreparedCall?.({
|
|
78
86
|
adapter: "runtime.pi-ai",
|
|
@@ -90,7 +98,20 @@ export function piModel(options = {}) {
|
|
|
90
98
|
timeoutMs: settings.requestTimeoutMs,
|
|
91
99
|
maxRetries: 2,
|
|
92
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
|
+
: {}),
|
|
93
112
|
};
|
|
113
|
+
// A file the request could not carry fails the call at once: no message, so never retried.
|
|
114
|
+
const nativeFailure = () => failedMessage(selected);
|
|
94
115
|
const attempt = async () => {
|
|
95
116
|
const idle = new AbortController();
|
|
96
117
|
const signal = AbortSignal.any([context.signal, idle.signal]);
|
|
@@ -100,6 +121,8 @@ export function piModel(options = {}) {
|
|
|
100
121
|
timer = setTimeout(() => idle.abort(), settings.idleTimeoutMs);
|
|
101
122
|
};
|
|
102
123
|
arm();
|
|
124
|
+
// Only the answer's own attempt counts: a retried attempt's first event is not its.
|
|
125
|
+
firstEventAt = undefined;
|
|
103
126
|
try {
|
|
104
127
|
const stream = registry.streamSimple(selected, request, {
|
|
105
128
|
...invocationOptions,
|
|
@@ -107,6 +130,7 @@ export function piModel(options = {}) {
|
|
|
107
130
|
});
|
|
108
131
|
for await (const event of stream) {
|
|
109
132
|
arm();
|
|
133
|
+
firstEventAt ??= Date.now();
|
|
110
134
|
if (event.type === "text_delta" && options.onPreview) {
|
|
111
135
|
try {
|
|
112
136
|
void Promise.resolve(options.onPreview({
|
|
@@ -120,6 +144,8 @@ export function piModel(options = {}) {
|
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
const response = await stream.result();
|
|
147
|
+
if (natives.error)
|
|
148
|
+
return nativeFailure();
|
|
123
149
|
if (idle.signal.aborted && !context.signal.aborted)
|
|
124
150
|
return idleTimeout(selected, settings.idleTimeoutMs);
|
|
125
151
|
return response;
|
|
@@ -127,6 +153,8 @@ export function piModel(options = {}) {
|
|
|
127
153
|
catch (error) {
|
|
128
154
|
if (context.signal.aborted)
|
|
129
155
|
throw error;
|
|
156
|
+
if (natives.error)
|
|
157
|
+
return nativeFailure();
|
|
130
158
|
if (idle.signal.aborted)
|
|
131
159
|
return idleTimeout(selected, settings.idleTimeoutMs);
|
|
132
160
|
return {
|
|
@@ -145,6 +173,8 @@ export function piModel(options = {}) {
|
|
|
145
173
|
}, context.signal);
|
|
146
174
|
// An abort is never a failure outcome: the host decides what it means.
|
|
147
175
|
context.signal.throwIfAborted();
|
|
176
|
+
if (natives.error)
|
|
177
|
+
return redacted(failure("invalid_request", natives.error, false));
|
|
148
178
|
if (response.stopReason === "aborted")
|
|
149
179
|
return redacted(failure("transient", "The provider stream was aborted.", true));
|
|
150
180
|
if (response.stopReason === "error") {
|
|
@@ -152,7 +182,7 @@ export function piModel(options = {}) {
|
|
|
152
182
|
return redacted(outcome.code === "auth"
|
|
153
183
|
? {
|
|
154
184
|
...outcome,
|
|
155
|
-
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.`,
|
|
156
186
|
}
|
|
157
187
|
: outcome);
|
|
158
188
|
}
|
|
@@ -218,92 +248,81 @@ export function piModel(options = {}) {
|
|
|
218
248
|
// The engine keeps the next prompt inside this window (Model Calls §7).
|
|
219
249
|
contextWindow: selected.contextWindow,
|
|
220
250
|
maxOutputTokens: selected.maxTokens,
|
|
251
|
+
// From the call to the provider's first stream event (R7.1); absent when none came.
|
|
252
|
+
...(firstEventAt !== undefined ? { firstTokenMs: firstEventAt - calledAt } : {}),
|
|
221
253
|
},
|
|
222
254
|
},
|
|
223
255
|
};
|
|
224
256
|
};
|
|
225
257
|
}
|
|
226
|
-
async function buildContext(call, selected, options) {
|
|
258
|
+
async function buildContext(call, selected, inputs, options, natives) {
|
|
227
259
|
const messages = [];
|
|
228
260
|
const instructions = [];
|
|
229
|
-
|
|
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) => {
|
|
230
320
|
const result = [];
|
|
231
321
|
for (const part of parts) {
|
|
232
322
|
if (part.type === "text")
|
|
233
323
|
result.push({ type: "text", text: part.text });
|
|
234
|
-
else if (part.type === "media"
|
|
235
|
-
|
|
236
|
-
// reads images. Otherwise, or when it cannot be read, a note: the call still goes.
|
|
237
|
-
const note = (why) => ({
|
|
238
|
-
type: "text",
|
|
239
|
-
text: `(The tool returned an image, ${essence(part.mediaType)}, that is not shown: ${why}.)`,
|
|
240
|
-
});
|
|
241
|
-
if (!selected.input.includes("image") || !isModelImage(part.mediaType)) {
|
|
242
|
-
result.push(note(selected.input.includes("image")
|
|
243
|
-
? "the model reads PNG, JPEG, GIF and WebP images only"
|
|
244
|
-
: "this model does not accept images"));
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
if (!options.files) {
|
|
248
|
-
result.push(note("files are unavailable"));
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
const file = await options.files(part.reference);
|
|
253
|
-
if (!isModelImage(file.mediaType))
|
|
254
|
-
result.push(note(`it is ${essence(file.mediaType)}`));
|
|
255
|
-
else
|
|
256
|
-
result.push({
|
|
257
|
-
type: "image",
|
|
258
|
-
data: Buffer.from(file.bytes).toString("base64"),
|
|
259
|
-
mimeType: essence(file.mediaType),
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
if (!(error instanceof FileUnavailableError))
|
|
264
|
-
throw error;
|
|
265
|
-
result.push(note(error.message.replace(/\.$/u, "")));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
else if (part.type === "media") {
|
|
269
|
-
// A file the message named by artifact (protocol 6): its bytes go only into the
|
|
270
|
-
// provider request built here, never into the transcript or the journal.
|
|
271
|
-
if (!options.files)
|
|
272
|
-
throw new InvalidRequest("Files are unavailable; pass a file resolver to piModel({ files }).");
|
|
273
|
-
let file;
|
|
274
|
-
try {
|
|
275
|
-
file = await options.files(part.reference);
|
|
276
|
-
}
|
|
277
|
-
catch (error) {
|
|
278
|
-
if (error instanceof FileUnavailableError)
|
|
279
|
-
throw new InvalidRequest(error.message);
|
|
280
|
-
throw error;
|
|
281
|
-
}
|
|
282
|
-
if (isModelImage(file.mediaType)) {
|
|
283
|
-
if (!selected.input.includes("image"))
|
|
284
|
-
throw new InvalidRequest("The configured model does not accept images.");
|
|
285
|
-
result.push({
|
|
286
|
-
type: "image",
|
|
287
|
-
data: Buffer.from(file.bytes).toString("base64"),
|
|
288
|
-
mimeType: essence(file.mediaType),
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
else if (isText(file.mediaType)) {
|
|
292
|
-
let text;
|
|
293
|
-
try {
|
|
294
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(file.bytes);
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
throw new InvalidRequest(`${file.name} is not UTF-8 text.`);
|
|
298
|
-
}
|
|
299
|
-
result.push({
|
|
300
|
-
type: "text",
|
|
301
|
-
text: `<file name=${JSON.stringify(file.name)} type=${JSON.stringify(essence(file.mediaType))}>\n${text}\n</file>`,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
else
|
|
305
|
-
throw new InvalidRequest(`The model reads images and text files; ${file.name} is ${essence(file.mediaType)}.`);
|
|
306
|
-
}
|
|
324
|
+
else if (part.type === "media")
|
|
325
|
+
result.push(await materialize(part));
|
|
307
326
|
}
|
|
308
327
|
return result;
|
|
309
328
|
};
|
|
@@ -318,7 +337,7 @@ async function buildContext(call, selected, options) {
|
|
|
318
337
|
toolName: item.toolName,
|
|
319
338
|
isError: item.status !== "completed",
|
|
320
339
|
timestamp: Date.now(),
|
|
321
|
-
content: await content(item.content
|
|
340
|
+
content: await content(item.content),
|
|
322
341
|
});
|
|
323
342
|
}
|
|
324
343
|
else if (item.kind === "message" && item.role === "assistant") {
|
|
@@ -349,6 +368,7 @@ async function buildContext(call, selected, options) {
|
|
|
349
368
|
tools,
|
|
350
369
|
};
|
|
351
370
|
}
|
|
371
|
+
const isNativeKind = (input) => input === "pdf" || input === "office" || input === "audio" || input === "video";
|
|
352
372
|
/**
|
|
353
373
|
* A replayed assistant message keeps the model that produced it, so pi-ai converts
|
|
354
374
|
* history correctly after a model switch (tool-call ids, foreign reasoning). Messages
|
|
@@ -493,7 +513,7 @@ function hostCredentialStore(stored, write) {
|
|
|
493
513
|
return next;
|
|
494
514
|
},
|
|
495
515
|
async delete() {
|
|
496
|
-
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.");
|
|
497
517
|
},
|
|
498
518
|
};
|
|
499
519
|
}
|