mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
refactor: extract reusable AI runtime package (#99059)
* refactor: extract reusable AI runtime package * refactor: complete AI provider relocation * refactor: keep llm core internal * refactor(ai): make @openclaw/ai self-contained with host policy ports Move pure transport helpers (tool projections, strict-schema normalization, prompt-cache boundary, stream guards, anthropic/openai compat, request activity) from src into packages/ai; move utf16-slice into normalization-core. Inject host policy (guarded fetch, redaction, strict-tool defaults, diagnostics logging) through AiTransportHost with inert library defaults installed by src/llm/stream.ts. Narrow the public barrel to instance-scoped createApiRegistry/createLlmRuntime; the process-default runtime moves behind internal/ and registerBuiltInApiProviders takes an explicit registry. Delete the src/llm/api-registry re-export facade. * fix(ai): teach node, jiti, and vite resolvers the @openclaw/ai and utf16-slice subpaths The workspace alias tables in root-alias.cjs, plugin-sdk-native-resolver, sdk-alias, the shared vitest config, and the Control UI vite config only knew @openclaw/llm-core; Node-side plugin loading resolved @openclaw/ai through the pnpm symlink to the unbuilt dist (checks-node-compact CI failures), and the Control UI build broke on the new normalization-core/utf16-slice subpath. * chore(ui): drop leftover service-worker debug logging * build(release): ship @openclaw/ai with its own shrinkwrap and honest dependency set packages/ai declares only its six real runtime deps (kysely, chalk, json5, tslog, zod, fs-safe, and proxyline were never imported); orphaned root deps removed. generate-npm-shrinkwrap now treats publishable packages/* like publishable plugins so the AI tarball pins its transitive tree even though workspace deps are omitted from the root shrinkwrap. knip learns the package entry points; the tsdown dts neverBundle option moves to its documented deps.dts home; the README documents the no-semver internal/* contract and host ports. * docs(ai): add minimal external-consumer example app examples/ai-chat consumes only the public @openclaw/ai surface (built dist via the workspace link): isolated runtime, built-in provider registration, one streamed completion. Supports Anthropic/OpenAI via env keys and a keyless local Ollama target; live-verified against Ollama. * docs(ai): document the @openclaw/ai package and workspace shrinkwrap boundary * chore(check): include examples/ in duplicate-scan targets * fix: emit normalization package subpaths * fix: complete AI package boundary artifacts * fix: align AI package boundary contracts * fix(ci): stabilize package release contracts * test: align documentation contract checks * test: keep cron docs guard aligned * test: align restored docs contract guards * test: follow upstream docs contracts * docs: drop superseded talk wording
This commit is contained in:
parent
913311845e
commit
062f88e3e3
230 changed files with 3122 additions and 1248 deletions
|
|
@ -28,7 +28,7 @@ paths:
|
|||
- src/agents/agent-tools.abort.ts
|
||||
- src/agents/agent-tools.before-tool-call*.ts
|
||||
- src/agents/agent-tools.read.ts
|
||||
- src/agents/agent-tools-parameter-schema.ts
|
||||
- packages/ai/src/providers/agent-tools-parameter-schema.ts
|
||||
- src/agents/sessions/tools/**
|
||||
- src/agents/embedded-agent-runner/effective-tool-policy.ts
|
||||
- src/agents/embedded-agent-runner/tool-name-allowlist.ts
|
||||
|
|
|
|||
69
.github/workflows/openclaw-npm-release.yml
vendored
69
.github/workflows/openclaw-npm-release.yml
vendored
|
|
@ -170,6 +170,23 @@ jobs:
|
|||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Pack AI runtime package
|
||||
id: ai_runtime_tarballs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
|
||||
package_version="$(node -p "require('./packages/ai/package.json').version")"
|
||||
if [[ "$package_version" != "$PACKAGE_VERSION" ]]; then
|
||||
echo "packages/ai version ${package_version} does not match openclaw ${PACKAGE_VERSION}." >&2
|
||||
exit 1
|
||||
fi
|
||||
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-ai-runtime-packages"
|
||||
rm -rf "$ARTIFACT_DIR"
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
pnpm --dir packages/ai pack --pack-destination "$ARTIFACT_DIR"
|
||||
(cd "$ARTIFACT_DIR" && sha256sum ./*.tgz > SHA256SUMS)
|
||||
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build Control UI
|
||||
run: pnpm ui:build
|
||||
|
||||
|
|
@ -233,10 +250,11 @@ jobs:
|
|||
RELEASE_REF: ${{ inputs.tag }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
DEPENDENCY_EVIDENCE_DIR: ${{ steps.dependency_evidence.outputs.dir }}
|
||||
AI_RUNTIME_TARBALL_DIR: ${{ steps.ai_runtime_tarballs.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACK_OUTPUT="$RUNNER_TEMP/npm-pack-output.txt"
|
||||
npm pack --json 2>&1 | tee "$PACK_OUTPUT"
|
||||
pnpm pack --json 2>&1 | tee "$PACK_OUTPUT"
|
||||
PACK_NAME="$(node - "$PACK_OUTPUT" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
|
@ -244,19 +262,23 @@ jobs:
|
|||
|
||||
function resolveTarballFileName(value) {
|
||||
const fileName = typeof value === "string" ? value.trim() : "";
|
||||
const resolved = path.resolve(fileName);
|
||||
if (
|
||||
!fileName.endsWith(".tgz") ||
|
||||
fileName.includes("\0") ||
|
||||
fileName !== path.basename(fileName) ||
|
||||
fileName !== path.win32.basename(fileName)
|
||||
fileName !== path.win32.basename(fileName) ||
|
||||
path.dirname(resolved) !== process.cwd()
|
||||
) {
|
||||
console.error(`npm pack reported unsafe tarball filename ${JSON.stringify(fileName)}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
return fileName;
|
||||
return path.basename(resolved);
|
||||
}
|
||||
|
||||
function arrayEndFrom(start) {
|
||||
function jsonEndFrom(start) {
|
||||
const opening = input[start];
|
||||
const closing = opening === "[" ? "]" : "}";
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escape = false;
|
||||
|
|
@ -274,9 +296,9 @@ jobs:
|
|||
}
|
||||
if (char === "\"") {
|
||||
inString = true;
|
||||
} else if (char === "[") {
|
||||
} else if (char === opening) {
|
||||
depth += 1;
|
||||
} else if (char === "]") {
|
||||
} else if (char === closing) {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return i + 1;
|
||||
|
|
@ -286,15 +308,15 @@ jobs:
|
|||
return -1;
|
||||
}
|
||||
|
||||
for (const match of input.matchAll(/\[/g)) {
|
||||
for (const match of input.matchAll(/[\[{]/g)) {
|
||||
const start = match.index;
|
||||
const end = arrayEndFrom(start);
|
||||
const end = jsonEndFrom(start);
|
||||
if (end === -1) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(input.slice(start, end));
|
||||
const first = Array.isArray(parsed) ? parsed[0] : null;
|
||||
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
||||
if (first && Object.prototype.hasOwnProperty.call(first, "filename")) {
|
||||
process.stdout.write(resolveTarballFileName(first.filename));
|
||||
process.exit(0);
|
||||
|
|
@ -326,6 +348,8 @@ jobs:
|
|||
rm -rf "$ARTIFACT_DIR"
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
cp "$PACK_PATH" "$ARTIFACT_DIR/"
|
||||
cp "$AI_RUNTIME_TARBALL_DIR"/*.tgz "$ARTIFACT_DIR/"
|
||||
cp "$AI_RUNTIME_TARBALL_DIR/SHA256SUMS" "$ARTIFACT_DIR/ai-runtime-SHA256SUMS"
|
||||
cp -R "$DEPENDENCY_EVIDENCE_DIR" "$ARTIFACT_DIR/dependency-evidence"
|
||||
printf '%s\n' "$RELEASE_TAG" > "$ARTIFACT_DIR/release-tag.txt"
|
||||
printf '%s\n' "$RELEASE_SHA" > "$ARTIFACT_DIR/release-sha.txt"
|
||||
|
|
@ -358,14 +382,16 @@ jobs:
|
|||
PREFLIGHT_ARTIFACT_DIR: ${{ steps.packed_tarball.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL_PATH="$(find "$PREFLIGHT_ARTIFACT_DIR" -maxdepth 1 -type f -name '*.tgz' -print | sort | tail -n 1)"
|
||||
TARBALL_PATH="$(find "$PREFLIGHT_ARTIFACT_DIR" -maxdepth 1 -type f -name 'openclaw-[0-9]*.tgz' -print | sort | tail -n 1)"
|
||||
if [[ -z "$TARBALL_PATH" ]]; then
|
||||
echo "Prepared preflight tarball not found." >&2
|
||||
ls -la "$PREFLIGHT_ARTIFACT_DIR" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
|
||||
node --import tsx scripts/openclaw-npm-prepublish-verify.ts "$TARBALL_PATH" "$PACKAGE_VERSION"
|
||||
AI_TARBALL="$(find "$PREFLIGHT_ARTIFACT_DIR" -maxdepth 1 -type f -name 'openclaw-ai-*.tgz' -print | head -n 1)"
|
||||
node --import tsx scripts/openclaw-npm-prepublish-verify.ts \
|
||||
"$TARBALL_PATH" "$PACKAGE_VERSION" "$AI_TARBALL"
|
||||
|
||||
- name: Upload dependency release evidence
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
|
|
@ -716,6 +742,11 @@ jobs:
|
|||
echo "Prepared preflight tarball digest mismatch." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f preflight-tarball/ai-runtime-SHA256SUMS ]]; then
|
||||
echo "Prepared AI runtime dependency checksums are missing." >&2
|
||||
exit 1
|
||||
fi
|
||||
(cd preflight-tarball && sha256sum -c ai-runtime-SHA256SUMS)
|
||||
echo "tarball_name=$ARTIFACT_TARBALL_NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "tarball_sha256=$ARTIFACT_TARBALL_SHA256" >> "$GITHUB_OUTPUT"
|
||||
echo "tarball_path=$ARTIFACT_TARBALL_PATH" >> "$GITHUB_OUTPUT"
|
||||
|
|
@ -779,6 +810,22 @@ jobs:
|
|||
if [[ -n "${publish_target}" ]]; then
|
||||
publish_target="./${publish_target}"
|
||||
fi
|
||||
publish_if_missing() {
|
||||
local package_name="$1"
|
||||
local tarball_path="$2"
|
||||
local package_version
|
||||
if [[ -z "$tarball_path" || ! -f "$tarball_path" ]]; then
|
||||
echo "Prepared tarball for ${package_name} was not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
package_version="$(node -p "require('./package.json').version")"
|
||||
if npm view "${package_name}@${package_version}" version >/dev/null 2>&1; then
|
||||
echo "${package_name}@${package_version} is already published; reusing it."
|
||||
return 0
|
||||
fi
|
||||
bash scripts/openclaw-npm-publish.sh --publish "./${tarball_path}"
|
||||
}
|
||||
publish_if_missing "@openclaw/ai" "$(find preflight-tarball -type f -name 'openclaw-ai-*.tgz' -print | head -n 1)"
|
||||
bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"
|
||||
|
||||
- name: Verify extended-stable registry readback
|
||||
|
|
|
|||
|
|
@ -138,9 +138,13 @@ const config = {
|
|||
entry: rootEntries,
|
||||
ignoreDependencies: [
|
||||
"@openclaw/*",
|
||||
// Docker packaging stages @openclaw/ai without nested dependencies after
|
||||
// verifying the root owns its exact runtime dependency versions.
|
||||
"@mistralai/mistralai",
|
||||
"cross-spawn",
|
||||
"file-type",
|
||||
"playwright-core",
|
||||
"partial-json",
|
||||
"sqlite-vec",
|
||||
"tree-sitter-bash",
|
||||
...rootBundledPluginRuntimeDependencies,
|
||||
|
|
@ -164,6 +168,19 @@ const config = {
|
|||
ignoreDependencies: ["three"],
|
||||
project: ["src/**/*.{ts,tsx}!"],
|
||||
},
|
||||
"packages/ai": {
|
||||
// Mirror the published export map so knip sees every dist entry point.
|
||||
entry: [
|
||||
"src/index.ts!",
|
||||
"src/providers.ts!",
|
||||
"src/types.ts!",
|
||||
"src/validation.ts!",
|
||||
"src/utils/diagnostics.ts!",
|
||||
"src/utils/event-stream.ts!",
|
||||
"src/internal/*.ts!",
|
||||
],
|
||||
project: ["src/**/*.ts!"],
|
||||
},
|
||||
"packages/sdk": {
|
||||
entry: ["src/index.ts!"],
|
||||
project: ["src/**/*.ts!"],
|
||||
|
|
|
|||
123
docs/docs.json
123
docs/docs.json
|
|
@ -1015,7 +1015,11 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["index", "start/showcase", "concepts/features"]
|
||||
"pages": [
|
||||
"index",
|
||||
"start/showcase",
|
||||
"concepts/features"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "First steps",
|
||||
|
|
@ -1041,7 +1045,11 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Install overview",
|
||||
"pages": ["install/index", "install/installer", "install/node"]
|
||||
"pages": [
|
||||
"install/index",
|
||||
"install/installer",
|
||||
"install/node"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Maintenance",
|
||||
|
|
@ -1094,7 +1102,10 @@
|
|||
},
|
||||
{
|
||||
"group": "Advanced setup",
|
||||
"pages": ["start/setup", "openclaw-agent-runtime"]
|
||||
"pages": [
|
||||
"start/setup",
|
||||
"openclaw-agent-runtime"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1103,7 +1114,10 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["channels/index", "announcements/bluebubbles-imessage"]
|
||||
"pages": [
|
||||
"channels/index",
|
||||
"announcements/bluebubbles-imessage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Mainstream messaging",
|
||||
|
|
@ -1240,7 +1254,9 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["tools/index"]
|
||||
"pages": [
|
||||
"tools/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Plugins",
|
||||
|
|
@ -1381,7 +1397,11 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["clawhub/index", "clawhub/quickstart", "clawhub/how-it-works"]
|
||||
"pages": [
|
||||
"clawhub/index",
|
||||
"clawhub/quickstart",
|
||||
"clawhub/how-it-works"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Using ClawHub",
|
||||
|
|
@ -1411,11 +1431,18 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["providers/index", "providers/models"]
|
||||
"pages": [
|
||||
"providers/index",
|
||||
"providers/models"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Concepts and configuration",
|
||||
"pages": ["concepts/models", "concepts/model-providers", "concepts/model-failover"]
|
||||
"pages": [
|
||||
"concepts/models",
|
||||
"concepts/model-providers",
|
||||
"concepts/model-failover"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Providers",
|
||||
|
|
@ -1619,13 +1646,22 @@
|
|||
},
|
||||
{
|
||||
"group": "Networking and discovery",
|
||||
"pages": ["network", "gateway/pairing", "gateway/discovery", "gateway/bonjour"]
|
||||
"pages": [
|
||||
"network",
|
||||
"gateway/pairing",
|
||||
"gateway/discovery",
|
||||
"gateway/bonjour"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Remote access",
|
||||
"pages": ["gateway/remote", "gateway/remote-gateway-readme", "gateway/tailscale"]
|
||||
"pages": [
|
||||
"gateway/remote",
|
||||
"gateway/remote-gateway-readme",
|
||||
"gateway/tailscale"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Security",
|
||||
|
|
@ -1653,13 +1689,23 @@
|
|||
},
|
||||
{
|
||||
"group": "Node features",
|
||||
"pages": ["nodes/talk", "nodes/voicewake", "nodes/location-command"]
|
||||
"pages": [
|
||||
"nodes/talk",
|
||||
"nodes/voicewake",
|
||||
"nodes/location-command"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Web interfaces",
|
||||
"pages": ["web/index", "web/control-ui", "web/dashboard", "web/webchat", "web/tui"]
|
||||
"pages": [
|
||||
"web/index",
|
||||
"web/control-ui",
|
||||
"web/dashboard",
|
||||
"web/webchat",
|
||||
"web/tui"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1732,7 +1778,11 @@
|
|||
},
|
||||
{
|
||||
"group": "Configuration",
|
||||
"pages": ["cli/config", "cli/configure", "cli/webhooks"]
|
||||
"pages": [
|
||||
"cli/config",
|
||||
"cli/configure",
|
||||
"cli/webhooks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Plugins and skills",
|
||||
|
|
@ -1746,7 +1796,11 @@
|
|||
},
|
||||
{
|
||||
"group": "Interfaces",
|
||||
"pages": ["cli/attach", "cli/dashboard", "cli/tui"]
|
||||
"pages": [
|
||||
"cli/attach",
|
||||
"cli/dashboard",
|
||||
"cli/tui"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Utility",
|
||||
|
|
@ -1835,6 +1889,7 @@
|
|||
"reference/wizard",
|
||||
"reference/token-use",
|
||||
"reference/secretref-credential-surface",
|
||||
"reference/openclaw-ai",
|
||||
"reference/prompt-caching",
|
||||
"reference/api-usage-costs",
|
||||
"reference/transcript-hygiene",
|
||||
|
|
@ -1856,14 +1911,20 @@
|
|||
},
|
||||
{
|
||||
"group": "Project",
|
||||
"pages": ["reference/application-modernization-plan", "reference/credits"]
|
||||
"pages": [
|
||||
"reference/application-modernization-plan",
|
||||
"reference/credits"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Release and CI",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Release notes",
|
||||
"pages": ["releases/index", "releases/2026.6.11"]
|
||||
"pages": [
|
||||
"releases/index",
|
||||
"releases/2026.6.11"
|
||||
]
|
||||
},
|
||||
"maturity/scorecard",
|
||||
"maturity/taxonomy",
|
||||
|
|
@ -1882,23 +1943,43 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Start here",
|
||||
"pages": ["help/index", "help/troubleshooting", "help/debugging"]
|
||||
"pages": [
|
||||
"help/index",
|
||||
"help/troubleshooting",
|
||||
"help/debugging"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "FAQ",
|
||||
"pages": ["help/faq", "help/faq-first-run", "help/faq-models"]
|
||||
"pages": [
|
||||
"help/faq",
|
||||
"help/faq-first-run",
|
||||
"help/faq-models"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Testing",
|
||||
"pages": ["help/testing", "help/testing-updates-plugins", "help/testing-live"]
|
||||
"pages": [
|
||||
"help/testing",
|
||||
"help/testing-updates-plugins",
|
||||
"help/testing-live"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Diagnostics",
|
||||
"pages": ["help/environment", "diagnostics/flags", "debug/node-issue"]
|
||||
"pages": [
|
||||
"help/environment",
|
||||
"diagnostics/flags",
|
||||
"debug/node-issue"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Community and meta",
|
||||
"pages": ["start/lore", "start/hubs", "start/docs-directory"]
|
||||
"pages": [
|
||||
"start/lore",
|
||||
"start/hubs",
|
||||
"start/docs-directory"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8161,6 +8161,14 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
|||
- H3: Example
|
||||
- H2: Related
|
||||
|
||||
## reference/openclaw-ai.md
|
||||
|
||||
- Route: /reference/openclaw-ai
|
||||
- Headings:
|
||||
- H2: Quick start
|
||||
- H2: Design contract
|
||||
- H2: Subpath exports
|
||||
|
||||
## reference/prompt-caching.md
|
||||
|
||||
- Route: /reference/prompt-caching
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ OpenClaw is a gateway, plugin host, model router, and agent runtime, so a defaul
|
|||
|
||||
## Generating and checking
|
||||
|
||||
The root `openclaw` npm package and OpenClaw-owned npm plugin packages (for example `@openclaw/discord`) include `npm-shrinkwrap.json` when they publish. Suitable plugin packages can also publish with explicit `bundledDependencies`, carrying their runtime dependency files in the plugin tarball instead of relying only on install-time resolution.
|
||||
The root `openclaw` npm package, OpenClaw-owned npm plugin packages (for example `@openclaw/discord`), and publishable workspace packages such as [`@openclaw/ai`](/reference/openclaw-ai) include `npm-shrinkwrap.json` when they publish. Workspace dependencies are omitted from the root shrinkwrap because they publish beside the root package; each publishable workspace package pins its own transitive tree instead. Suitable plugin packages can also publish with explicit `bundledDependencies`, carrying their runtime dependency files in the plugin tarball instead of relying only on install-time resolution.
|
||||
|
||||
```bash
|
||||
# All shrinkwrap-managed packages (root + publishable plugins)
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ A legacy fallback correction tag may reuse base-package evidence only when the c
|
|||
- Run `pnpm check:architecture` before release preflight so the broader import cycle and architecture boundary checks are green outside the faster local gate.
|
||||
- Run `pnpm build && pnpm ui:build` before `pnpm release:check` so the expected `dist/*` release artifacts and Control UI bundle exist for the pack validation step.
|
||||
- Run `pnpm release:prep` after the root version bump and before tagging. It runs every deterministic release generator that commonly drifts after a version/config/API change: plugin versions, npm shrinkwraps, plugin inventory, base config schema, bundled channel config metadata, config docs baseline, plugin SDK exports, and plugin SDK API baseline. `pnpm release:check` re-runs those guards in check mode (plus a plugin SDK surface budget check) and reports every generated drift failure in one pass before running package release checks.
|
||||
- Plugin version sync updates official plugin package versions and existing `openclaw.compat.pluginApi` floors to the OpenClaw release version by default. Treat that field as the plugin SDK/runtime API floor, not just a copy of the package version: for plugin-only releases that intentionally remain compatible with older OpenClaw hosts, keep the floor at the oldest supported host API and document that choice in the plugin release proof.
|
||||
- Plugin version sync updates the publishable `@openclaw/ai` runtime package, official plugin package versions, and existing `openclaw.compat.pluginApi` floors to the OpenClaw release version by default. Treat that field as the plugin SDK/runtime API floor, not just a copy of the package version: for plugin-only releases that intentionally remain compatible with older OpenClaw hosts, keep the floor at the oldest supported host API and document that choice in the plugin release proof.
|
||||
- Run the manual `Full Release Validation` workflow before release approval to kick off all pre-release test boxes from one entrypoint. It accepts a branch, tag, or full commit SHA, dispatches manual `CI`, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full runs always include exhaustive live/E2E and Docker release-path soak; `run_release_soak=true` is retained for an explicit beta soak. Package Acceptance provides the canonical package Telegram E2E during candidate validation, avoiding a second concurrent live poller.
|
||||
|
||||
Provide `release_package_spec` after publishing a beta to reuse the shipped npm package across release checks, Package Acceptance, and package Telegram E2E without rebuilding the release tarball. Provide `npm_telegram_package_spec` only when Telegram should use a different published package from the rest of release validation. Provide `package_acceptance_package_spec` when Package Acceptance should use a different published package from the release package spec. Provide `evidence_package_spec` when the release evidence report should prove that validation matches a published npm package without forcing Telegram E2E.
|
||||
|
|
|
|||
71
docs/reference/openclaw-ai.md
Normal file
71
docs/reference/openclaw-ai.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
summary: "The @openclaw/ai npm package: reusable model transports, isolated runtimes, and host policy ports"
|
||||
title: "@openclaw/ai package"
|
||||
read_when:
|
||||
- You want to reuse OpenClaw's model transports in another application
|
||||
- You are changing packages/ai or the AI transport host ports
|
||||
- You are reviewing what the openclaw release publishes to npm besides the root package
|
||||
---
|
||||
|
||||
`@openclaw/ai` is the publishable library form of OpenClaw's model execution
|
||||
layer: provider-neutral message/tool/stream contracts, validation, diagnostics,
|
||||
event streams, an isolated runtime registry, and lazy adapters for the eight
|
||||
built-in API families (Anthropic Messages, OpenAI Completions, OpenAI
|
||||
Responses, Azure OpenAI Responses, ChatGPT/Codex Responses, Google Generative
|
||||
AI, Google Vertex, Mistral Conversations).
|
||||
|
||||
It publishes alongside the root `openclaw` package on every release, pinned to
|
||||
the same version, with its own `npm-shrinkwrap.json` so its transitive
|
||||
dependency tree is locked at install time. Installing `openclaw` installs the
|
||||
matching `@openclaw/ai` automatically; library consumers can depend on it
|
||||
directly without any OpenClaw application code.
|
||||
|
||||
## Quick start
|
||||
|
||||
```js
|
||||
import { createLlmRuntime } from "@openclaw/ai";
|
||||
import { registerBuiltInApiProviders } from "@openclaw/ai/providers";
|
||||
|
||||
const runtime = createLlmRuntime();
|
||||
registerBuiltInApiProviders(runtime.registry);
|
||||
|
||||
const stream = runtime.streamSimple(model, { messages }, { apiKey });
|
||||
for await (const event of stream) {
|
||||
if (event.type === "text_delta") process.stdout.write(event.delta);
|
||||
}
|
||||
const result = await stream.result();
|
||||
```
|
||||
|
||||
A runnable version lives in the repository at `examples/ai-chat`.
|
||||
|
||||
## Design contract
|
||||
|
||||
- **Instance-scoped by default.** Importing the package registers nothing
|
||||
globally. `createApiRegistry()` / `createLlmRuntime()` return isolated
|
||||
instances; `registerBuiltInApiProviders(registry)` opts one registry into the
|
||||
built-in transports. Provider SDK modules load lazily on first use.
|
||||
- **Host policy is injected, not bundled.** Request fetch guarding (for
|
||||
example SSRF policy), secret redaction of tool-result replay text, OpenAI
|
||||
strict-tool defaults, and diagnostics logging are `AiTransportHost` ports
|
||||
configured with `configureAiTransportHost`. The library defaults are inert;
|
||||
OpenClaw installs its real implementations in its stream facade.
|
||||
- **One event-stream identity.** `@openclaw/ai/event-stream` is the canonical
|
||||
`EventStream` constructor shared by OpenClaw core, agent-core, and external
|
||||
consumers.
|
||||
- **`internal/*` subpaths are not API.** They exist for the OpenClaw
|
||||
application itself and carry no semver guarantee.
|
||||
- Provider ids, credentials, model catalogs, retries, and failover remain
|
||||
application concerns. OpenClaw layers those around this package; a library
|
||||
consumer supplies a `Model` object and options directly.
|
||||
|
||||
## Subpath exports
|
||||
|
||||
| Subpath | Contents |
|
||||
| ---------------- | ------------------------------------------------------------------------------ |
|
||||
| `.` | Contracts, `createApiRegistry`, `createLlmRuntime`, `configureAiTransportHost` |
|
||||
| `./providers` | `registerBuiltInApiProviders`, `resetApiProviders` |
|
||||
| `./types` | Model/message/tool/stream types |
|
||||
| `./validation` | Tool argument validation |
|
||||
| `./diagnostics` | Diagnostics contracts |
|
||||
| `./event-stream` | Shared `EventStream` implementation |
|
||||
| `./internal/*` | OpenClaw-internal, no semver guarantee |
|
||||
29
examples/ai-chat/README.md
Normal file
29
examples/ai-chat/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# @openclaw/ai example: minimal chat completion
|
||||
|
||||
Smallest possible consumer of the published `@openclaw/ai` package: creates an
|
||||
isolated runtime, registers the built-in API providers, and streams one
|
||||
completion. No OpenClaw application code is involved — inside this repo the
|
||||
workspace link resolves to the package's built `dist`, the same artifact npm
|
||||
consumers install, so `pnpm build` must have run first.
|
||||
|
||||
```sh
|
||||
ANTHROPIC_API_KEY=sk-ant-... node index.mjs "Say hello"
|
||||
OPENAI_API_KEY=sk-... node index.mjs --provider openai "Say hello"
|
||||
# keyless, against a local Ollama server (OLLAMA_MODEL overrides the model id)
|
||||
node index.mjs --provider ollama "Say hello"
|
||||
```
|
||||
|
||||
Text deltas stream to stdout; stop reason and token usage go to stderr.
|
||||
|
||||
Notes for library consumers:
|
||||
|
||||
- `createLlmRuntime()` gives you an isolated registry; nothing is registered
|
||||
globally by importing the package.
|
||||
- `registerBuiltInApiProviders(runtime.registry)` opts into the eight built-in
|
||||
transports (Anthropic, OpenAI Completions/Responses, Azure OpenAI, ChatGPT
|
||||
Responses, Google, Vertex, Mistral). Provider SDK modules load lazily on
|
||||
first use.
|
||||
- Host policy (custom fetch, secret redaction, strict-tool defaults, logging)
|
||||
is injectable via `configureAiTransportHost`; the defaults are inert.
|
||||
- Full TypeScript types ship with the package; this example uses plain ESM so
|
||||
it runs with bare `node`.
|
||||
85
examples/ai-chat/index.mjs
Normal file
85
examples/ai-chat/index.mjs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Minimal @openclaw/ai consumer: one isolated runtime, built-in providers,
|
||||
// one streamed completion. Uses only the public package surface — no OpenClaw
|
||||
// application code. Run with:
|
||||
// ANTHROPIC_API_KEY=... node index.mjs "your prompt"
|
||||
// OPENAI_API_KEY=... node index.mjs --provider openai "your prompt"
|
||||
import { createLlmRuntime } from "@openclaw/ai";
|
||||
import { registerBuiltInApiProviders } from "@openclaw/ai/providers";
|
||||
|
||||
const MODELS = {
|
||||
anthropic: {
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
openai: {
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 16_384,
|
||||
},
|
||||
// Local Ollama server; no API key required.
|
||||
ollama: {
|
||||
id: process.env.OLLAMA_MODEL || "llama3.2:latest",
|
||||
name: "Ollama",
|
||||
api: "openai-completions",
|
||||
provider: "ollama",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 32_000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const providerFlag = args.indexOf("--provider");
|
||||
const provider = providerFlag === -1 ? "anthropic" : args[providerFlag + 1];
|
||||
const prompt =
|
||||
args.filter((_, i) => i !== providerFlag && i !== providerFlag + 1).join(" ") ||
|
||||
"Reply with one short sentence: what is @openclaw/ai?";
|
||||
|
||||
const model = MODELS[provider];
|
||||
if (!model) {
|
||||
console.error(`Unknown provider "${provider}". Use one of: ${Object.keys(MODELS).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const runtime = createLlmRuntime();
|
||||
registerBuiltInApiProviders(runtime.registry);
|
||||
|
||||
const stream = runtime.streamSimple(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: prompt, timestamp: Date.now() }] },
|
||||
// Ollama ignores credentials but the OpenAI-compatible transport requires one.
|
||||
provider === "ollama" ? { apiKey: "ollama" } : undefined,
|
||||
);
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === "text_delta") {
|
||||
process.stdout.write(event.delta);
|
||||
}
|
||||
}
|
||||
const result = await stream.result();
|
||||
process.stdout.write("\n");
|
||||
|
||||
if (result.stopReason === "error" || result.stopReason === "aborted") {
|
||||
console.error(`error: ${result.errorMessage ?? result.stopReason}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const { input, output } = result.usage;
|
||||
console.error(`[${model.id}] stop=${result.stopReason} tokens in=${input} out=${output}`);
|
||||
13
examples/ai-chat/package.json
Normal file
13
examples/ai-chat/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "@openclaw/example-ai-chat",
|
||||
"version": "0.0.0-private",
|
||||
"private": true,
|
||||
"description": "Minimal external-consumer example for @openclaw/ai",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openclaw/ai": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +116,27 @@
|
|||
"@openclaw/whatsapp/api.js": [
|
||||
"../dist/plugin-sdk/extensions/whatsapp/api.d.ts"
|
||||
],
|
||||
"@openclaw/ai": [
|
||||
"../dist/plugin-sdk/packages/ai/src/index.d.ts"
|
||||
],
|
||||
"@openclaw/ai/diagnostics": [
|
||||
"../dist/plugin-sdk/packages/ai/src/utils/diagnostics.d.ts"
|
||||
],
|
||||
"@openclaw/ai/event-stream": [
|
||||
"../dist/plugin-sdk/packages/ai/src/utils/event-stream.d.ts"
|
||||
],
|
||||
"@openclaw/ai/providers": [
|
||||
"../dist/plugin-sdk/packages/ai/src/providers.d.ts"
|
||||
],
|
||||
"@openclaw/ai/types": [
|
||||
"../dist/plugin-sdk/packages/ai/src/types.d.ts"
|
||||
],
|
||||
"@openclaw/ai/validation": [
|
||||
"../dist/plugin-sdk/packages/ai/src/validation.d.ts"
|
||||
],
|
||||
"@openclaw/ai/internal/*": [
|
||||
"../dist/plugin-sdk/packages/ai/src/internal/*.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core": [
|
||||
"../dist/plugin-sdk/packages/llm-core/src/index.d.ts"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -102,6 +102,27 @@
|
|||
"@openclaw/qa-channel/api.js": [
|
||||
"../../dist/plugin-sdk/extensions/qa-channel/api.d.ts"
|
||||
],
|
||||
"@openclaw/ai": [
|
||||
"../../dist/plugin-sdk/packages/ai/src/index.d.ts"
|
||||
],
|
||||
"@openclaw/ai/diagnostics": [
|
||||
"../../dist/plugin-sdk/packages/ai/src/utils/diagnostics.d.ts"
|
||||
],
|
||||
"@openclaw/ai/event-stream": [
|
||||
"../../dist/plugin-sdk/packages/ai/src/utils/event-stream.d.ts"
|
||||
],
|
||||
"@openclaw/ai/providers": [
|
||||
"../../dist/plugin-sdk/packages/ai/src/providers.d.ts"
|
||||
],
|
||||
"@openclaw/ai/types": [
|
||||
"../../dist/plugin-sdk/packages/ai/src/types.d.ts"
|
||||
],
|
||||
"@openclaw/ai/validation": [
|
||||
"../../dist/plugin-sdk/packages/ai/src/validation.d.ts"
|
||||
],
|
||||
"@openclaw/ai/internal/*": [
|
||||
"../../dist/plugin-sdk/packages/ai/src/internal/*.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core": [
|
||||
"../../dist/plugin-sdk/packages/llm-core/src/index.d.ts"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1986,6 +1986,7 @@
|
|||
"@mistralai/mistralai": "2.4.0",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@mozilla/readability": "0.6.0",
|
||||
"@openclaw/ai": "workspace:*",
|
||||
"@openclaw/fs-safe": "0.4.1",
|
||||
"@openclaw/proxyline": "0.3.3",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@openclaw/llm-core": "workspace:*",
|
||||
"@openclaw/ai": "workspace:*",
|
||||
"typebox": "1.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Agent Core tests cover agent loop behavior.
|
||||
import { EventStream } from "@openclaw/ai/event-stream";
|
||||
import { Type } from "typebox";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { agentLoop, agentLoopContinue, runAgentLoop } from "./agent-loop.js";
|
||||
|
|
@ -77,6 +78,7 @@ describe("agentLoop EventStream failures", () => {
|
|||
undefined,
|
||||
failingStreamFn,
|
||||
);
|
||||
expect(stream).toBeInstanceOf(EventStream);
|
||||
|
||||
const events = await collectEvents(stream);
|
||||
const result = await stream.result();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Keep the runtime class on the package specifier so built agent-core shares
|
||||
// constructor identity with @openclaw/llm-core; source types keep SDK d.ts bundled.
|
||||
import { EventStream as LlmEventStream } from "@openclaw/llm-core";
|
||||
// Keep the runtime class on the public package specifier so OpenClaw and
|
||||
// external consumers share one constructor identity.
|
||||
import { EventStream as LlmEventStream } from "@openclaw/ai/event-stream";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
AssistantMessageEvent,
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// LLM-core compatibility barrel for agent-core consumers.
|
||||
export * from "@openclaw/llm-core";
|
||||
// AI contract compatibility barrel for agent-core consumers.
|
||||
export * from "@openclaw/ai";
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// Tool validation facade for callers that import validation from agent-core.
|
||||
export { validateToolArguments, validateToolCall } from "@openclaw/llm-core";
|
||||
export { validateToolArguments, validateToolCall } from "@openclaw/ai/validation";
|
||||
|
|
|
|||
21
packages/ai/LICENSE
Normal file
21
packages/ai/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 OpenClaw Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
28
packages/ai/README.md
Normal file
28
packages/ai/README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# `@openclaw/ai`
|
||||
|
||||
Reusable model API contracts, provider adapters, and streaming primitives from
|
||||
OpenClaw. The package supports isolated runtime instances; importing it does not
|
||||
register providers globally.
|
||||
|
||||
```ts
|
||||
import { createLlmRuntime } from "@openclaw/ai";
|
||||
import { registerBuiltInApiProviders } from "@openclaw/ai/providers";
|
||||
|
||||
const runtime = createLlmRuntime();
|
||||
registerBuiltInApiProviders(runtime.registry);
|
||||
```
|
||||
|
||||
Provider-neutral contracts, validation, diagnostics, and event streams are
|
||||
available from the package root and focused subpaths such as
|
||||
`@openclaw/ai/event-stream` and `@openclaw/ai/validation`. No second OpenClaw
|
||||
runtime package is required.
|
||||
|
||||
Provider ids, credentials, model catalogs, retries, and failover remain
|
||||
application concerns. OpenClaw supplies those policies around this package.
|
||||
Host policy (request fetch guarding, secret redaction, strict-tool defaults,
|
||||
diagnostics logging) can be injected with `configureAiTransportHost`; the
|
||||
defaults are inert.
|
||||
|
||||
`@openclaw/ai/internal/*` subpaths exist for the OpenClaw application itself.
|
||||
They carry no semver guarantee and can change or disappear in any release; do
|
||||
not depend on them outside OpenClaw.
|
||||
645
packages/ai/npm-shrinkwrap.json
generated
Normal file
645
packages/ai/npm-shrinkwrap.json
generated
Normal file
|
|
@ -0,0 +1,645 @@
|
|||
{
|
||||
"name": "@openclaw/ai",
|
||||
"version": "2026.6.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/ai",
|
||||
"version": "2026.6.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.109.1",
|
||||
"@google/genai": "2.10.0",
|
||||
"@mistralai/mistralai": "2.4.0",
|
||||
"openai": "6.45.0",
|
||||
"partial-json": "0.1.7",
|
||||
"typebox": "1.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.109.1.tgz",
|
||||
"integrity": "sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"standardwebhooks": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/genai": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.10.0.tgz",
|
||||
"integrity": "sha512-e4cFxj3tiuMtsgOT4G9c1hXyGJhg7/Buj7VVeBacRY3fRtkRZZ59Q3nuVp2xbq8BGQXLXCDB253qMhklMOeUDg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^10.3.0",
|
||||
"p-retry": "^4.6.2",
|
||||
"protobufjs": "^7.5.4",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.25.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@modelcontextprotocol/sdk": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.4.0.tgz",
|
||||
"integrity": "sha512-t6hCx242MTGolB76CI+17jDtPIe/bzLsMdUTMMoMn9Qo1h02N2G5jQYHmKDGU3X//OgR2wvngTD7tO6tPp5poQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-to-json-schema": "^3.25.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/semantic-conventions": {
|
||||
"version": "1.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz",
|
||||
"integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
|
||||
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
|
||||
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
|
||||
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
|
||||
"integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
|
||||
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@stablelib/base64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz",
|
||||
"integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz",
|
||||
"integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz",
|
||||
"integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
||||
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^7.0.0",
|
||||
"google-logging-utils": "^1.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "10.9.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz",
|
||||
"integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"gaxios": "^7.1.4",
|
||||
"gcp-metadata": "8.1.2",
|
||||
"google-logging-utils": "1.1.3",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/google-logging-utils": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
|
||||
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"name": "@nolyfill/domexception",
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/@nolyfill/domexception/-/domexception-1.0.28.tgz",
|
||||
"integrity": "sha512-tlc/FcYIv5i8RYsl2iDil4A0gOihaas1R5jPcIC4Zw3GhjKsVilw90aHcVlhZPTBLGBzd379S+VcnsDjd9ChiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "6.45.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.45.0.tgz",
|
||||
"integrity": "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
|
||||
"@smithy/hash-node": ">=4.3.0 <5",
|
||||
"@smithy/signature-v4": ">=5.4.0 <6",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-provider-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@smithy/hash-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@smithy/signature-v4": {
|
||||
"optional": true
|
||||
},
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/partial-json": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz",
|
||||
"integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz",
|
||||
"integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.5",
|
||||
"@protobufjs/eventemitter": "^1.1.1",
|
||||
"@protobufjs/fetch": "^1.1.1",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.2",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.1",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/base64": "^1.0.0",
|
||||
"fast-sha256": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typebox": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.3.tgz",
|
||||
"integrity": "sha512-URXGUE31PJDQC+PtRMJeLdF4kmmOdFoVPikPCtV2oOIhUpNpppEdIz7W8bH8cFYPYHdDpaRvqwdegMTmHliudg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.25.2",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
|
||||
"integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.28 || ^4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
packages/ai/package.json
Normal file
89
packages/ai/package.json
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"name": "@openclaw/ai",
|
||||
"version": "2026.6.11",
|
||||
"description": "Reusable model provider adapters and streaming runtime from OpenClaw",
|
||||
"keywords": [
|
||||
"ai",
|
||||
"anthropic",
|
||||
"google",
|
||||
"llm",
|
||||
"mistral",
|
||||
"openai",
|
||||
"streaming"
|
||||
],
|
||||
"homepage": "https://github.com/openclaw/openclaw#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/openclaw/openclaw/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/openclaw/openclaw.git",
|
||||
"directory": "packages/ai"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"npm-shrinkwrap.json",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./providers": {
|
||||
"types": "./dist/providers.d.mts",
|
||||
"import": "./dist/providers.mjs",
|
||||
"default": "./dist/providers.mjs"
|
||||
},
|
||||
"./diagnostics": {
|
||||
"types": "./dist/diagnostics.d.mts",
|
||||
"import": "./dist/diagnostics.mjs",
|
||||
"default": "./dist/diagnostics.mjs"
|
||||
},
|
||||
"./event-stream": {
|
||||
"types": "./dist/event-stream.d.mts",
|
||||
"import": "./dist/event-stream.mjs",
|
||||
"default": "./dist/event-stream.mjs"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.mts",
|
||||
"import": "./dist/types.mjs",
|
||||
"default": "./dist/types.mjs"
|
||||
},
|
||||
"./validation": {
|
||||
"types": "./dist/validation.d.mts",
|
||||
"import": "./dist/validation.mjs",
|
||||
"default": "./dist/validation.mjs"
|
||||
},
|
||||
"./internal/*": {
|
||||
"types": "./dist/internal/*.d.mts",
|
||||
"import": "./dist/internal/*.mjs",
|
||||
"default": "./dist/internal/*.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.109.1",
|
||||
"@google/genai": "2.10.0",
|
||||
"@mistralai/mistralai": "2.4.0",
|
||||
"openai": "6.45.0",
|
||||
"partial-json": "0.1.7",
|
||||
"typebox": "1.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"openclaw": {
|
||||
"release": {
|
||||
"publishToNpm": true
|
||||
}
|
||||
}
|
||||
}
|
||||
76
packages/ai/src/api-registry.test.ts
Normal file
76
packages/ai/src/api-registry.test.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// LLM Runtime tests cover api registry behavior.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createApiRegistry,
|
||||
createAssistantMessageEventStream,
|
||||
createLlmRuntime,
|
||||
type Model,
|
||||
} from "./index.js";
|
||||
|
||||
const TEST_SOURCE_ID = "test:llm-runtime-api-registry";
|
||||
const emptyStream = () => createAssistantMessageEventStream();
|
||||
|
||||
const model = {
|
||||
id: "test-model",
|
||||
name: "Test Model",
|
||||
api: "test-api",
|
||||
provider: "test-provider",
|
||||
baseUrl: "https://example.invalid",
|
||||
input: ["text"],
|
||||
reasoning: false,
|
||||
contextWindow: 1000,
|
||||
maxTokens: 100,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
} satisfies Model;
|
||||
|
||||
describe("LLM API registry", () => {
|
||||
it("rejects mismatched model API calls", () => {
|
||||
const registry = createApiRegistry();
|
||||
registry.registerApiProvider(
|
||||
{
|
||||
api: "test-api",
|
||||
stream: emptyStream,
|
||||
streamSimple: emptyStream,
|
||||
},
|
||||
TEST_SOURCE_ID,
|
||||
);
|
||||
|
||||
const provider = registry.getApiProvider("test-api");
|
||||
expect(provider).toBeDefined();
|
||||
expect(() => provider?.streamSimple({ ...model, api: "other-api" }, { messages: [] })).toThrow(
|
||||
"Mismatched api: other-api expected test-api",
|
||||
);
|
||||
});
|
||||
|
||||
it("isolates providers between runtime instances", () => {
|
||||
const first = createLlmRuntime();
|
||||
const second = createLlmRuntime();
|
||||
const streamSimple = vi.fn(() => createAssistantMessageEventStream());
|
||||
first.registry.registerApiProvider({ api: "test-api", stream: streamSimple, streamSimple });
|
||||
|
||||
first.streamSimple(model, { messages: [] });
|
||||
|
||||
expect(streamSimple).toHaveBeenCalledOnce();
|
||||
expect(() => second.streamSimple(model, { messages: [] })).toThrow(
|
||||
"No API provider registered for api: test-api",
|
||||
);
|
||||
});
|
||||
|
||||
it("unregisters every provider owned by one source", () => {
|
||||
const registry = createApiRegistry();
|
||||
for (const api of ["test-api", "test-api-2"] as const) {
|
||||
registry.registerApiProvider(
|
||||
{
|
||||
api,
|
||||
stream: emptyStream,
|
||||
streamSimple: emptyStream,
|
||||
},
|
||||
TEST_SOURCE_ID,
|
||||
);
|
||||
}
|
||||
|
||||
registry.unregisterApiProviders(TEST_SOURCE_ID);
|
||||
|
||||
expect(registry.getApiProviders()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,7 +7,7 @@ import type {
|
|||
SimpleStreamOptions,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
} from "../../llm-core/src/index.js";
|
||||
} from "@openclaw/llm-core";
|
||||
|
||||
/** Runtime stream adapter signature stored in the API provider registry. */
|
||||
export type ApiStreamFunction = (
|
||||
|
|
@ -36,19 +36,18 @@ export interface ApiProvider<
|
|||
streamSimple: StreamFunction<TApi, SimpleStreamOptions>;
|
||||
}
|
||||
|
||||
interface ApiProviderInternal {
|
||||
/** Type-erased provider returned by a registry after API guards are installed. */
|
||||
export interface RegisteredApiProvider {
|
||||
api: Api;
|
||||
stream: ApiStreamFunction;
|
||||
streamSimple: ApiStreamSimpleFunction;
|
||||
}
|
||||
|
||||
type RegisteredApiProvider = {
|
||||
provider: ApiProviderInternal;
|
||||
type RegisteredApiProviderEntry = {
|
||||
provider: RegisteredApiProvider;
|
||||
sourceId?: string;
|
||||
};
|
||||
|
||||
const apiProviderRegistry = new Map<string, RegisteredApiProvider>();
|
||||
|
||||
function wrapStream<TApi extends Api, TOptions extends StreamOptions>(
|
||||
api: TApi,
|
||||
stream: StreamFunction<TApi, TOptions>,
|
||||
|
|
@ -73,13 +72,16 @@ function wrapStreamSimple<TApi extends Api>(
|
|||
};
|
||||
}
|
||||
|
||||
/** Registers or replaces the provider implementation for an API id. */
|
||||
export function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(
|
||||
/** Creates an isolated provider registry for one runtime or tenant. */
|
||||
export function createApiRegistry() {
|
||||
const providers = new Map<string, RegisteredApiProviderEntry>();
|
||||
|
||||
function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(
|
||||
provider: ApiProvider<TApi, TOptions>,
|
||||
/** Optional source id used to unregister all providers owned by one plugin/runtime. */
|
||||
sourceId?: string,
|
||||
): void {
|
||||
apiProviderRegistry.set(provider.api, {
|
||||
): void {
|
||||
providers.set(provider.api, {
|
||||
provider: {
|
||||
api: provider.api,
|
||||
stream: wrapStream(provider.api, provider.stream),
|
||||
|
|
@ -87,28 +89,31 @@ export function registerApiProvider<TApi extends Api, TOptions extends StreamOpt
|
|||
},
|
||||
sourceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Looks up a registered API provider by API id. */
|
||||
export function getApiProvider(api: Api): ApiProviderInternal | undefined {
|
||||
return apiProviderRegistry.get(api)?.provider;
|
||||
}
|
||||
function getApiProvider(api: Api): RegisteredApiProvider | undefined {
|
||||
return providers.get(api)?.provider;
|
||||
}
|
||||
|
||||
/** Lists all currently registered API providers. */
|
||||
export function getApiProviders(): ApiProviderInternal[] {
|
||||
return Array.from(apiProviderRegistry.values(), (entry) => entry.provider);
|
||||
}
|
||||
function getApiProviders(): RegisteredApiProvider[] {
|
||||
return Array.from(providers.values(), (entry) => entry.provider);
|
||||
}
|
||||
|
||||
/** Removes all providers registered by a plugin/source id. */
|
||||
export function unregisterApiProviders(sourceId: string): void {
|
||||
for (const [api, entry] of apiProviderRegistry.entries()) {
|
||||
function unregisterApiProviders(sourceId: string): void {
|
||||
for (const [api, entry] of providers.entries()) {
|
||||
if (entry.sourceId === sourceId) {
|
||||
apiProviderRegistry.delete(api);
|
||||
providers.delete(api);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
registerApiProvider,
|
||||
getApiProvider,
|
||||
getApiProviders,
|
||||
unregisterApiProviders,
|
||||
clearApiProviders: () => providers.clear(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Clears the registry for test teardown and runtime reset flows. */
|
||||
export function clearApiProviders(): void {
|
||||
apiProviderRegistry.clear();
|
||||
}
|
||||
export type ApiRegistry = ReturnType<typeof createApiRegistry>;
|
||||
66
packages/ai/src/host.ts
Normal file
66
packages/ai/src/host.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Host policy ports for the reusable transport package. Fetch guarding,
|
||||
// secret redaction, strict-tool policy, and diagnostics logging are owned by
|
||||
// the embedding application (OpenClaw core installs its implementations via
|
||||
// configureAiTransportHost); the library defaults below are inert so external
|
||||
// consumers get safe, dependency-free behavior without wiring anything.
|
||||
import type { Model } from "@openclaw/llm-core";
|
||||
|
||||
/** Strict-tool policy inputs for OpenAI-compatible routes. */
|
||||
export interface OpenAIStrictToolSettingOptions {
|
||||
transport?: "stream" | "websocket";
|
||||
supportsStrictMode?: boolean;
|
||||
}
|
||||
|
||||
/** Narrow host ports consumed by the built-in provider adapters. */
|
||||
export interface AiTransportHost {
|
||||
/**
|
||||
* Builds a policy-guarded fetch for one model request.
|
||||
* Returning undefined keeps the provider SDK's default fetch.
|
||||
*/
|
||||
buildModelFetch(
|
||||
model: Model,
|
||||
timeoutMs?: number,
|
||||
options?: { sanitizeSse?: boolean },
|
||||
): typeof fetch | undefined;
|
||||
/** Redacts secrets inside structured tool-result payloads. */
|
||||
redactSecrets<T>(value: T): T;
|
||||
/** Redacts secret-bearing text in tool payload strings. */
|
||||
redactToolPayloadText(text: string): string;
|
||||
/**
|
||||
* Resolves the host strict-tool default for OpenAI-compatible routes.
|
||||
* undefined lets the request omit the strict flag entirely.
|
||||
*/
|
||||
resolveOpenAIStrictToolSetting(
|
||||
model: Pick<Model, "provider" | "api" | "baseUrl" | "id"> & { compat?: unknown },
|
||||
options?: OpenAIStrictToolSettingOptions,
|
||||
): boolean | undefined;
|
||||
/**
|
||||
* Emits one transport diagnostic; build runs only when the host logs it and
|
||||
* may return null to suppress the entry (e.g. de-duplication).
|
||||
*/
|
||||
logDebug(
|
||||
subsystem: string,
|
||||
build: () => { message: string; data?: Record<string, unknown> } | null,
|
||||
): void;
|
||||
}
|
||||
|
||||
const inertAiTransportHost: AiTransportHost = {
|
||||
buildModelFetch: () => undefined,
|
||||
redactSecrets: (value) => value,
|
||||
redactToolPayloadText: (text) => text,
|
||||
resolveOpenAIStrictToolSetting: (_model, options) =>
|
||||
options?.supportsStrictMode ? false : undefined,
|
||||
logDebug: () => {},
|
||||
};
|
||||
|
||||
let activeAiTransportHost = inertAiTransportHost;
|
||||
|
||||
/** Installs host implementations for the transport policy ports. */
|
||||
export function configureAiTransportHost(host: Partial<AiTransportHost>): void {
|
||||
activeAiTransportHost = { ...inertAiTransportHost, ...host };
|
||||
}
|
||||
|
||||
/** Returns the active transport host (inert defaults unless configured). */
|
||||
export function getAiTransportHost(): AiTransportHost {
|
||||
return activeAiTransportHost;
|
||||
}
|
||||
5
packages/ai/src/index.ts
Normal file
5
packages/ai/src/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/** Reusable model API contracts, provider adapters, and streaming runtime. */
|
||||
export * from "@openclaw/llm-core";
|
||||
export * from "./api-registry.js";
|
||||
export * from "./host.js";
|
||||
export * from "./stream.js";
|
||||
7
packages/ai/src/internal/anthropic.ts
Normal file
7
packages/ai/src/internal/anthropic.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * from "../providers/anthropic.js";
|
||||
export * from "../providers/anthropic-auth-headers.js";
|
||||
export * from "../providers/anthropic-model-contract.js";
|
||||
export * from "../providers/anthropic-refusal.js";
|
||||
export * from "../providers/anthropic-server-fallback.js";
|
||||
export * from "../providers/anthropic-thinking-replay.js";
|
||||
export * from "../providers/anthropic-tool-projection.js";
|
||||
19
packages/ai/src/internal/default-runtime.ts
Normal file
19
packages/ai/src/internal/default-runtime.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Process-default registry/runtime retained for the OpenClaw compatibility
|
||||
// facade (src/llm). Deliberately not part of the public package API: external
|
||||
// consumers create isolated runtimes via createLlmRuntime(); exporting these
|
||||
// from the root barrel would reintroduce the mutable process-global registry.
|
||||
import { createApiRegistry } from "../api-registry.js";
|
||||
import { createLlmRuntime } from "../stream.js";
|
||||
|
||||
export const defaultApiRegistry = createApiRegistry();
|
||||
export const defaultLlmRuntime = createLlmRuntime(defaultApiRegistry);
|
||||
|
||||
export const {
|
||||
registerApiProvider,
|
||||
getApiProvider,
|
||||
getApiProviders,
|
||||
unregisterApiProviders,
|
||||
clearApiProviders,
|
||||
} = defaultApiRegistry;
|
||||
|
||||
export const { stream, complete, streamSimple, completeSimple } = defaultLlmRuntime;
|
||||
14
packages/ai/src/internal/openai.ts
Normal file
14
packages/ai/src/internal/openai.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export * from "../providers/agent-tools-parameter-schema.js";
|
||||
export * from "../providers/azure-deployment-map.js";
|
||||
export * from "../providers/azure-openai-responses-client-compat.js";
|
||||
export * from "../providers/clean-for-gemini.js";
|
||||
export * from "../providers/openai-completions.js";
|
||||
export * from "../providers/openai-prompt-cache.js";
|
||||
export * from "../providers/openai-reasoning-effort.js";
|
||||
export * from "../providers/openai-responses.js";
|
||||
export * from "../providers/openai-responses-stream-compat.js";
|
||||
export * from "../providers/openai-stop-reason.js";
|
||||
export * from "../providers/openai-tool-projection.js";
|
||||
export * from "../providers/openai-tool-schema.js";
|
||||
export * from "../providers/schema-keyword-strip.js";
|
||||
export * from "../providers/tool-schema-json-projection.js";
|
||||
15
packages/ai/src/internal/runtime.ts
Normal file
15
packages/ai/src/internal/runtime.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export * from "./default-runtime.js";
|
||||
export * from "../env-api-keys.js";
|
||||
export * from "../model-utils.js";
|
||||
export * from "../session-resources.js";
|
||||
export * from "../utils/deferred-event-buffer.js";
|
||||
export * from "../utils/hash.js";
|
||||
export * from "../utils/headers.js";
|
||||
export * from "../utils/json-parse.js";
|
||||
export * from "../utils/llm-request-activity.js";
|
||||
export * from "../utils/oauth/openai-chatgpt-jwt.js";
|
||||
export * from "../utils/overflow.js";
|
||||
export * from "../utils/reasoning-tag-text-partitioner.js";
|
||||
export * from "../utils/sanitize-unicode.js";
|
||||
export * from "../utils/stream-first-event-timeout.js";
|
||||
export * from "../utils/streaming-byte-guard.js";
|
||||
5
packages/ai/src/internal/shared.ts
Normal file
5
packages/ai/src/internal/shared.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from "../providers/simple-options.js";
|
||||
export * from "../providers/tool-result-text.js";
|
||||
export * from "../providers/transform-messages.js";
|
||||
export * from "../utils/prompt-cache-stability.js";
|
||||
export * from "../utils/system-prompt-cache-boundary.js";
|
||||
6
packages/ai/src/providers.ts
Normal file
6
packages/ai/src/providers.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/** Lazy built-in protocol adapter registration. */
|
||||
export {
|
||||
BUILT_IN_API_PROVIDER_SOURCE_ID,
|
||||
registerBuiltInApiProviders,
|
||||
resetApiProviders,
|
||||
} from "./providers/register-builtins.js";
|
||||
|
|
@ -5,20 +5,62 @@
|
|||
*/
|
||||
import { isRecord as isSchemaRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import { uniqueValues } from "@openclaw/normalization-core/string-normalization";
|
||||
import type { TSchema } from "typebox";
|
||||
import type { ModelCompatConfig } from "../config/types.models.js";
|
||||
import {
|
||||
resolveUnsupportedToolSchemaKeywords,
|
||||
shouldOmitEmptyArrayItems,
|
||||
} from "../plugins/provider-model-compat.js";
|
||||
import { stripUnsupportedSchemaKeywords } from "../shared/schema-keyword-strip.js";
|
||||
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
|
||||
normalizeStringEntries,
|
||||
uniqueValues,
|
||||
} from "@openclaw/normalization-core/string-normalization";
|
||||
import type { TSchema } from "typebox";
|
||||
import { cleanSchemaForGemini } from "./clean-for-gemini.js";
|
||||
import { stripUnsupportedSchemaKeywords } from "./schema-keyword-strip.js";
|
||||
|
||||
/**
|
||||
* Narrow structural view of the host's model compat config. packages/ai must stay
|
||||
* config-agnostic, so only tool-schema-relevant fields are modeled here; the host's
|
||||
* ModelCompatConfig remains structurally assignable.
|
||||
*/
|
||||
export type ToolSchemaModelCompat = {
|
||||
toolSchemaProfile?: string;
|
||||
unsupportedToolSchemaKeywords?: string[];
|
||||
omitEmptyArrayItems?: boolean;
|
||||
};
|
||||
|
||||
/** Extracts the compat record whether callers pass a model (`{ compat }`) or the compat itself. */
|
||||
export function extractToolSchemaModelCompat(
|
||||
modelOrCompat: { compat?: unknown } | ToolSchemaModelCompat | undefined,
|
||||
): ToolSchemaModelCompat | undefined {
|
||||
if (!modelOrCompat || typeof modelOrCompat !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
if ("compat" in modelOrCompat) {
|
||||
const compat = (modelOrCompat as { compat?: unknown }).compat;
|
||||
return compat && typeof compat === "object" ? (compat as ToolSchemaModelCompat) : undefined;
|
||||
}
|
||||
return modelOrCompat as ToolSchemaModelCompat;
|
||||
}
|
||||
|
||||
/** JSON Schema keywords this model/provider rejects in tool schemas. */
|
||||
export function resolveUnsupportedToolSchemaKeywords(
|
||||
modelOrCompat: { compat?: unknown } | ToolSchemaModelCompat | undefined,
|
||||
): ReadonlySet<string> {
|
||||
const keywords = extractToolSchemaModelCompat(modelOrCompat)?.unsupportedToolSchemaKeywords ?? [];
|
||||
return new Set(
|
||||
normalizeStringEntries(
|
||||
keywords.filter((keyword): keyword is string => typeof keyword === "string"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether empty `items: {}` on array schemas must be omitted for this model/provider. */
|
||||
export function shouldOmitEmptyArrayItems(
|
||||
modelOrCompat: { compat?: unknown } | ToolSchemaModelCompat | undefined,
|
||||
): boolean {
|
||||
return extractToolSchemaModelCompat(modelOrCompat)?.omitEmptyArrayItems === true;
|
||||
}
|
||||
|
||||
export type ToolParameterSchemaOptions = {
|
||||
modelProvider?: string;
|
||||
modelId?: string;
|
||||
modelCompat?: ModelCompatConfig;
|
||||
modelCompat?: ToolSchemaModelCompat;
|
||||
};
|
||||
|
||||
const MAX_TOOL_PARAMETER_SCHEMA_CACHE_ENTRIES_PER_SCHEMA = 8;
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
// Model-bound thinking cannot be exposed or replayed after a model switch.
|
||||
import {
|
||||
resolveClaudeFable5ModelIdentity,
|
||||
resolveClaudeModelIdentity,
|
||||
} from "@openclaw/llm-core";
|
||||
import { resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity } from "@openclaw/llm-core";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
export {
|
||||
resolveClaudeFable5ModelIdentity,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AssistantMessageDiagnostic } from "../llm/types.js";
|
||||
import type { AssistantMessageDiagnostic } from "../types.js";
|
||||
|
||||
type AnthropicRefusalOutput = {
|
||||
stopReason: string;
|
||||
|
|
@ -1,21 +1,12 @@
|
|||
import type { AssistantMessageDiagnostic } from "../llm/types.js";
|
||||
import type { AssistantMessageDiagnostic } from "../types.js";
|
||||
|
||||
/**
|
||||
* Anthropic server-side refusal fallback (`server-side-fallback-2026-06-01`).
|
||||
* When Claude Fable 5 safety classifiers decline a request, the API re-serves
|
||||
* the same call on a permitted fallback model inside the same stream instead
|
||||
* of returning `stop_reason: "refusal"`.
|
||||
* https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback
|
||||
*/
|
||||
/** Anthropic beta that re-serves safety refusals on an allowed fallback model. */
|
||||
export const ANTHROPIC_SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-06-01";
|
||||
|
||||
// Anthropic contract: claude-opus-4-8 is the only entry in claude-fable-5's
|
||||
// published `allowed_fallback_models`; other targets are rejected up front.
|
||||
// Anthropic documents claude-opus-4-8 as the allowed fallback for claude-fable-5.
|
||||
export const CLAUDE_FABLE_5_FALLBACK_MODEL = "claude-opus-4-8";
|
||||
|
||||
// Fallback-served turns bill at the serving model's rates (top-level usage
|
||||
// covers only that attempt), so cost math must switch off the Fable table.
|
||||
// Claude Opus 4.8 per-MTok pricing, same shape as the bundled Fable table.
|
||||
// Fallback-served turns bill at the serving model's rates.
|
||||
export const CLAUDE_FABLE_5_FALLBACK_MODEL_COST = {
|
||||
input: 5,
|
||||
output: 25,
|
||||
|
|
@ -56,10 +47,8 @@ export function readAnthropicFallbackBoundary(block: unknown): AnthropicFallback
|
|||
}
|
||||
|
||||
/**
|
||||
* Applies a mid-stream fallback boundary to the accumulated assistant output.
|
||||
* Anthropic's replay contract: pre-boundary thinking/tool_use blocks must not
|
||||
* be echoed on later turns (and dropped tool calls must never execute); the
|
||||
* pre-boundary text is the continuation prefix the fallback model built on.
|
||||
* Drops pre-fallback thinking/tool calls while preserving the text prefix that
|
||||
* the serving model continued. Dropped tool calls must never execute or replay.
|
||||
*/
|
||||
export function applyAnthropicFallbackBoundary(params: {
|
||||
output: {
|
||||
|
|
@ -73,9 +62,6 @@ export function applyAnthropicFallbackBoundary(params: {
|
|||
const { output, boundary } = params;
|
||||
const survivors = output.content.filter((block) => block.type === "text");
|
||||
for (const survivor of survivors) {
|
||||
// Commentary phase tags refer to dropped pre-boundary tool calls; the
|
||||
// prefix is now the start of the final answer, and phase-aware display
|
||||
// extraction would otherwise hide it from the visible response.
|
||||
delete (survivor as { textSignature?: string }).textSignature;
|
||||
}
|
||||
output.content.splice(0, output.content.length, ...survivors);
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
// Anthropic provider tests cover stream events, tools, and message mapping.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { configureAiTransportHost } from "../host.js";
|
||||
import type { Context, Model, Tool } from "../types.js";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../utils/system-prompt-cache-boundary.js";
|
||||
|
||||
const anthropicMockState = vi.hoisted(() => ({
|
||||
configs: [] as unknown[],
|
||||
|
|
@ -54,7 +55,14 @@ describe("Anthropic provider", () => {
|
|||
anthropicMockState.configs = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
configureAiTransportHost({});
|
||||
});
|
||||
|
||||
it("keeps Cloudflare AI Gateway upstream provider auth on the Anthropic API key", async () => {
|
||||
// Prove the Cloudflare client receives the host-built model fetch.
|
||||
const hostFetch: typeof fetch = async () => new Response(null, { status: 500 });
|
||||
configureAiTransportHost({ buildModelFetch: () => hostFetch });
|
||||
const model = makeAnthropicModel({
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/account/gateway/anthropic/v1/messages",
|
||||
|
|
@ -82,7 +90,7 @@ describe("Anthropic provider", () => {
|
|||
expect(config.authToken).toBeNull();
|
||||
expect(config.defaultHeaders?.["x-api-key"]).toBeUndefined();
|
||||
expect(config.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer gateway-token");
|
||||
expect(typeof config.fetch).toBe("function");
|
||||
expect(config.fetch).toBe(hostFetch);
|
||||
});
|
||||
|
||||
it("uses bearer auth for Microsoft Foundry Anthropic requests", async () => {
|
||||
|
|
@ -8,42 +8,8 @@ import type {
|
|||
RawMessageStreamEvent,
|
||||
TextBlockParam,
|
||||
} from "@anthropic-ai/sdk/resources/messages.js";
|
||||
import {
|
||||
projectAnthropicTools,
|
||||
reconcileAnthropicToolChoice,
|
||||
resolveOriginalAnthropicToolName,
|
||||
type AnthropicProjectedToolChoice,
|
||||
type AnthropicToolProjection,
|
||||
} from "../../agents/anthropic-tool-projection.js";
|
||||
import { resolveProviderEndpoint } from "../../agents/provider-attribution.js";
|
||||
import { buildGuardedModelFetch } from "../../agents/provider-transport-fetch.js";
|
||||
import {
|
||||
splitSystemPromptCacheBoundary,
|
||||
stripSystemPromptCacheBoundary,
|
||||
} from "../../agents/system-prompt-cache-boundary.js";
|
||||
import {
|
||||
omitFoundryBearerCredentialHeaders,
|
||||
usesFoundryBearerAuth,
|
||||
} from "../../shared/anthropic-auth-headers.js";
|
||||
import {
|
||||
resolveClaudeNativeThinkingLevelMap,
|
||||
requiresClaudeAdaptiveThinking,
|
||||
supportsClaudeAdaptiveThinking,
|
||||
supportsClaudeNativeMaxEffort,
|
||||
supportsClaudeNativeXhighEffort,
|
||||
usesClaudeFable5MessagesContract,
|
||||
} from "../../shared/anthropic-model-contract.js";
|
||||
import { applyAnthropicRefusal } from "../../shared/anthropic-refusal.js";
|
||||
import {
|
||||
ANTHROPIC_SERVER_SIDE_FALLBACK_BETA,
|
||||
CLAUDE_FABLE_5_FALLBACK_MODEL_COST,
|
||||
applyAnthropicFallbackBoundary,
|
||||
buildAnthropicServerSideFallbacks,
|
||||
readAnthropicFallbackBoundary,
|
||||
} from "../../shared/anthropic-server-fallback.js";
|
||||
import { createDeferredEventBuffer } from "../../shared/deferred-event-buffer.js";
|
||||
import { notifyLlmRequestActivity } from "../../shared/llm-request-activity.js";
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { getAiTransportHost } from "../host.js";
|
||||
import { calculateCost, clampThinkingLevel } from "../model-utils.js";
|
||||
import type {
|
||||
AnthropicMessagesCompat,
|
||||
|
|
@ -65,14 +31,47 @@ import type {
|
|||
ToolCall,
|
||||
ToolResultMessage,
|
||||
} from "../types.js";
|
||||
import { createDeferredEventBuffer } from "../utils/deferred-event-buffer.js";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import { headersToRecord } from "../utils/headers.js";
|
||||
import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
|
||||
import { notifyLlmRequestActivity } from "../utils/llm-request-activity.js";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import {
|
||||
splitSystemPromptCacheBoundary,
|
||||
stripSystemPromptCacheBoundary,
|
||||
} from "../utils/system-prompt-cache-boundary.js";
|
||||
import {
|
||||
omitFoundryBearerCredentialHeaders,
|
||||
usesFoundryBearerAuth,
|
||||
} from "./anthropic-auth-headers.js";
|
||||
import {
|
||||
resolveClaudeNativeThinkingLevelMap,
|
||||
requiresClaudeAdaptiveThinking,
|
||||
supportsClaudeAdaptiveThinking,
|
||||
supportsClaudeNativeMaxEffort,
|
||||
supportsClaudeNativeXhighEffort,
|
||||
usesClaudeFable5MessagesContract,
|
||||
} from "./anthropic-model-contract.js";
|
||||
import { applyAnthropicRefusal } from "./anthropic-refusal.js";
|
||||
import {
|
||||
ANTHROPIC_SERVER_SIDE_FALLBACK_BETA,
|
||||
CLAUDE_FABLE_5_FALLBACK_MODEL_COST,
|
||||
applyAnthropicFallbackBoundary,
|
||||
buildAnthropicServerSideFallbacks,
|
||||
readAnthropicFallbackBoundary,
|
||||
} from "./anthropic-server-fallback.js";
|
||||
import {
|
||||
ANTHROPIC_OMITTED_REASONING_TEXT,
|
||||
findActiveAnthropicToolTurnAssistantIndex,
|
||||
} from "./anthropic-thinking-replay.js";
|
||||
import {
|
||||
projectAnthropicTools,
|
||||
reconcileAnthropicToolChoice,
|
||||
resolveOriginalAnthropicToolName,
|
||||
type AnthropicProjectedToolChoice,
|
||||
type AnthropicToolProjection,
|
||||
} from "./anthropic-tool-projection.js";
|
||||
import { resolveCacheRetention } from "./cache-retention.js";
|
||||
import { resolveCloudflareBaseUrl } from "./cloudflare.js";
|
||||
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
||||
|
|
@ -971,6 +970,17 @@ function isOAuthToken(apiKey: string): boolean {
|
|||
return apiKey.includes("sk-ant-oat");
|
||||
}
|
||||
|
||||
function isAnthropicPublicEndpoint(baseUrl: string | undefined): boolean {
|
||||
if (!baseUrl) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return new URL(baseUrl).hostname.toLowerCase() === "api.anthropic.com";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side refusal fallback is a first-party Claude API beta: proxies and
|
||||
* Bedrock/Vertex/Foundry reject the `fallbacks` param, and OAuth (Claude Code
|
||||
|
|
@ -980,8 +990,7 @@ function supportsAnthropicServerSideFallback(model: Model<"anthropic-messages">)
|
|||
if (!usesClaudeFable5MessagesContract(model) || model.provider !== "anthropic") {
|
||||
return false;
|
||||
}
|
||||
const endpointClass = resolveProviderEndpoint(model.baseUrl).endpointClass;
|
||||
return endpointClass === "default" || endpointClass === "anthropic-public";
|
||||
return isAnthropicPublicEndpoint(model.baseUrl);
|
||||
}
|
||||
|
||||
function createClient(
|
||||
|
|
@ -1020,7 +1029,7 @@ function createClient(
|
|||
model.headers,
|
||||
optionsHeaders,
|
||||
),
|
||||
fetch: buildGuardedModelFetch(model),
|
||||
fetch: getAiTransportHost().buildModelFetch(model),
|
||||
});
|
||||
|
||||
return { client, isOAuthToken: false, serverSideFallback: false };
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
// Azure OpenAI Responses provider adapts Azure deployments to Responses API streams.
|
||||
import OpenAI, { AzureOpenAI } from "openai";
|
||||
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
|
||||
import { buildGuardedModelFetch } from "../../agents/provider-transport-fetch.js";
|
||||
import { isOpenAICompatibleAzureResponsesBaseUrl } from "../../shared/azure-openai-responses-client-compat.js";
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { getAiTransportHost } from "../host.js";
|
||||
import type {
|
||||
Context,
|
||||
Model,
|
||||
|
|
@ -13,6 +12,7 @@ import type {
|
|||
} from "../types.js";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import { resolveAzureDeploymentNameFromMap } from "./azure-deployment-map.js";
|
||||
import { isOpenAICompatibleAzureResponsesBaseUrl } from "./azure-openai-responses-client-compat.js";
|
||||
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
||||
import {
|
||||
applyCommonResponsesParams,
|
||||
|
|
@ -200,7 +200,7 @@ function createClient(
|
|||
}
|
||||
|
||||
const { baseUrl, apiVersion } = resolveAzureConfig(model, options);
|
||||
const guardedFetch = buildGuardedModelFetch({ ...model, baseUrl });
|
||||
const guardedFetch = getAiTransportHost().buildModelFetch({ ...model, baseUrl });
|
||||
|
||||
if (isOpenAICompatibleAzureResponsesBaseUrl(baseUrl)) {
|
||||
return new OpenAI({
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// Google shared provider tests cover response conversion and finish reasons.
|
||||
import { FinishReason, type GenerateContentResponse } from "@google/genai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import type { AssistantMessage, Model } from "../types.js";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../utils/system-prompt-cache-boundary.js";
|
||||
import {
|
||||
buildGoogleGenerateContentParams,
|
||||
consumeGoogleGenerateContentStream,
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
type Part,
|
||||
type ThinkingConfig,
|
||||
} from "@google/genai";
|
||||
import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { calculateCost, clampThinkingLevel } from "../model-utils.js";
|
||||
import type {
|
||||
Api,
|
||||
|
|
@ -32,6 +31,7 @@ import type {
|
|||
} from "../types.js";
|
||||
import type { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../utils/system-prompt-cache-boundary.js";
|
||||
import { describeToolResultMediaPlaceholder, extractToolResultText } from "./tool-result-text.js";
|
||||
import { transformMessages } from "./transform-messages.js";
|
||||
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
// Mistral provider tests cover request mapping and stream conversion.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { configureAiTransportHost } from "../host.js";
|
||||
import type { Context, Model } from "../types.js";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../utils/system-prompt-cache-boundary.js";
|
||||
|
||||
const mistralMockState = vi.hoisted(() => ({
|
||||
payloads: [] as unknown[],
|
||||
|
|
@ -72,6 +73,10 @@ describe("Mistral provider", () => {
|
|||
mistralMockState.payloads = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
configureAiTransportHost({});
|
||||
});
|
||||
|
||||
it("forwards simple stop sequences to Mistral stop", async () => {
|
||||
const stream = streamSimpleMistral(makeMistralModel(), context, {
|
||||
apiKey: "sk-mistral-provider",
|
||||
|
|
@ -247,6 +252,10 @@ describe("Mistral provider", () => {
|
|||
});
|
||||
|
||||
it("serializes structured non-image blocks in tool results as JSON text", async () => {
|
||||
// Prove the host redaction port is applied to structured tool-result text.
|
||||
configureAiTransportHost({
|
||||
redactToolPayloadText: (text) => text.replaceAll('"value"', '"***"'),
|
||||
});
|
||||
const testContext = {
|
||||
messages: [
|
||||
{
|
||||
|
|
@ -7,8 +7,6 @@ import type {
|
|||
ContentChunk,
|
||||
FunctionTool,
|
||||
} from "@mistralai/mistralai/models/components";
|
||||
import { createSseByteGuard } from "../../agents/streaming-byte-guard.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { calculateCost, clampThinkingLevel } from "../model-utils.js";
|
||||
import type {
|
||||
|
|
@ -29,6 +27,8 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|||
import { shortHash } from "../utils/hash.js";
|
||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import { createSseByteGuard } from "../utils/streaming-byte-guard.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../utils/system-prompt-cache-boundary.js";
|
||||
import { buildBaseOptions } from "./simple-options.js";
|
||||
import { describeToolResultMediaPlaceholder, extractToolResultText } from "./tool-result-text.js";
|
||||
import { transformMessages } from "./transform-messages.js";
|
||||
|
|
@ -3,8 +3,8 @@ import { zstdDecompressSync } from "node:zlib";
|
|||
// ChatGPT Responses provider tests cover stream handling and timeout behavior.
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import type { Context, Model } from "../types.js";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../utils/system-prompt-cache-boundary.js";
|
||||
import {
|
||||
closeOpenAICodexWebSocketSessions,
|
||||
extractOpenAICodexAccountId,
|
||||
|
|
@ -30,13 +30,6 @@ import {
|
|||
resolveTimerTimeoutMs,
|
||||
clampTimerTimeoutMs,
|
||||
} from "@openclaw/normalization-core/number-coercion";
|
||||
import {
|
||||
createFirstStreamEventAbortController,
|
||||
getFirstStreamEventTimeoutHandler,
|
||||
getFirstStreamEventTimeoutMs,
|
||||
} from "../../agents/stream-first-event-timeout.js";
|
||||
import { createSseByteGuard } from "../../agents/streaming-byte-guard.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { registerSessionResourceCleanup } from "../session-resources.js";
|
||||
import type {
|
||||
|
|
@ -57,6 +50,13 @@ import {
|
|||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import { headersToRecord } from "../utils/headers.js";
|
||||
import { resolveOpenAICodexAccountId } from "../utils/oauth/openai-chatgpt-jwt.js";
|
||||
import {
|
||||
createFirstStreamEventAbortController,
|
||||
getFirstStreamEventTimeoutHandler,
|
||||
getFirstStreamEventTimeoutMs,
|
||||
} from "../utils/stream-first-event-timeout.js";
|
||||
import { createSseByteGuard } from "../utils/streaming-byte-guard.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../utils/system-prompt-cache-boundary.js";
|
||||
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
||||
import {
|
||||
convertResponsesMessages,
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
// OpenAI completions tests cover chat completion stream adaptation.
|
||||
import type { ChatCompletionChunk } from "openai/resources/chat/completions.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { configureAiTransportHost } from "../host.js";
|
||||
import type { Context, Model, SimpleStreamOptions } from "../types.js";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../utils/system-prompt-cache-boundary.js";
|
||||
|
||||
type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> };
|
||||
type OpenAICompatibleDelta = DeepPartial<ChatCompletionChunk["choices"][number]["delta"]> & {
|
||||
|
|
@ -160,10 +161,13 @@ function createNeverYieldingStream(): AsyncIterable<OpenAICompatibleChatCompleti
|
|||
}
|
||||
|
||||
describe("OpenAI-compatible completions params", () => {
|
||||
it("configures the OpenAI SDK client with guarded fetch", async () => {
|
||||
it("configures the OpenAI SDK client with the host-built model fetch", async () => {
|
||||
mockOpenAIOptionsRef.options = [];
|
||||
mockChunksRef.chunks = [makeTextChunk("ok"), makeFinishChunk("stop")];
|
||||
const hostFetch: typeof fetch = async () => new Response(null, { status: 500 });
|
||||
configureAiTransportHost({ buildModelFetch: () => hostFetch });
|
||||
|
||||
try {
|
||||
const stream = streamOpenAICompletions(model, context, {
|
||||
apiKey: "sk-test",
|
||||
});
|
||||
|
|
@ -175,9 +179,10 @@ describe("OpenAI-compatible completions params", () => {
|
|||
baseURL: "https://api.openai.com/v1",
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
expect((mockOpenAIOptionsRef.options[0] as { fetch?: unknown }).fetch).toEqual(
|
||||
expect.any(Function),
|
||||
);
|
||||
expect((mockOpenAIOptionsRef.options[0] as { fetch?: unknown }).fetch).toBe(hostFetch);
|
||||
} finally {
|
||||
configureAiTransportHost({});
|
||||
}
|
||||
});
|
||||
|
||||
it("fails when streaming headers arrive but no first SSE event follows", async () => {
|
||||
|
|
@ -11,25 +11,8 @@ import type {
|
|||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionToolMessageParam,
|
||||
} from "openai/resources/chat/completions.js";
|
||||
import {
|
||||
projectOpenAITools,
|
||||
reconcileOpenAICompletionsToolChoice,
|
||||
type OpenAICompletionsToolChoice,
|
||||
type OpenAIToolProjection,
|
||||
} from "../../agents/openai-tool-projection.js";
|
||||
import { buildGuardedModelFetch } from "../../agents/provider-transport-fetch.js";
|
||||
import {
|
||||
createFirstStreamEventAbortController,
|
||||
getFirstStreamEventTimeoutHandler,
|
||||
getFirstStreamEventTimeoutMs,
|
||||
withFirstStreamEventTimeout,
|
||||
} from "../../agents/stream-first-event-timeout.js";
|
||||
import {
|
||||
splitSystemPromptCacheBoundary,
|
||||
stripSystemPromptCacheBoundary,
|
||||
} from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { createReasoningTagTextPartitioner } from "../../shared/text/reasoning-tag-text-partitioner.js";
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { getAiTransportHost } from "../host.js";
|
||||
import { calculateCost, clampThinkingLevel } from "../model-utils.js";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
|
|
@ -51,12 +34,29 @@ import type {
|
|||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import { headersToRecord } from "../utils/headers.js";
|
||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||
import { createReasoningTagTextPartitioner } from "../utils/reasoning-tag-text-partitioner.js";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import {
|
||||
createFirstStreamEventAbortController,
|
||||
getFirstStreamEventTimeoutHandler,
|
||||
getFirstStreamEventTimeoutMs,
|
||||
withFirstStreamEventTimeout,
|
||||
} from "../utils/stream-first-event-timeout.js";
|
||||
import {
|
||||
splitSystemPromptCacheBoundary,
|
||||
stripSystemPromptCacheBoundary,
|
||||
} from "../utils/system-prompt-cache-boundary.js";
|
||||
import { resolveCacheRetention } from "./cache-retention.js";
|
||||
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
|
||||
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
||||
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
||||
import { mapOpenAIStopReason } from "./openai-stop-reason.js";
|
||||
import {
|
||||
projectOpenAITools,
|
||||
reconcileOpenAICompletionsToolChoice,
|
||||
type OpenAICompletionsToolChoice,
|
||||
type OpenAIToolProjection,
|
||||
} from "./openai-tool-projection.js";
|
||||
import { buildBaseOptions } from "./simple-options.js";
|
||||
import { describeToolResultMediaPlaceholder, extractToolResultText } from "./tool-result-text.js";
|
||||
import { transformMessages } from "./transform-messages.js";
|
||||
|
|
@ -638,7 +638,7 @@ function createClient(
|
|||
baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders,
|
||||
fetch: buildGuardedModelFetch(model),
|
||||
fetch: getAiTransportHost().buildModelFetch(model),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -3,10 +3,11 @@ import type {
|
|||
ResponseStreamEvent,
|
||||
Tool as OpenAIResponsesTool,
|
||||
} from "openai/resources/responses/responses.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { configureAiTransportHost } from "../host.js";
|
||||
import type { AssistantMessage, AssistantMessageEvent, Context, Model, Tool } from "../types.js";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../utils/system-prompt-cache-boundary.js";
|
||||
import {
|
||||
applyCommonResponsesParams,
|
||||
createResponsesAssistantOutput,
|
||||
|
|
@ -117,6 +118,23 @@ async function* responseEvents(events: Array<Record<string, unknown>>) {
|
|||
}
|
||||
|
||||
describe("convertResponsesTools", () => {
|
||||
beforeEach(() => {
|
||||
// Mimic the OpenClaw host strict-tool policy: native OpenAI routes force
|
||||
// strict=true, proxy-like routes leave the flag unset.
|
||||
configureAiTransportHost({
|
||||
resolveOpenAIStrictToolSetting: (model, options) => {
|
||||
if (model.provider === "openai" && model.baseUrl === "https://api.openai.com/v1") {
|
||||
return true;
|
||||
}
|
||||
return options?.supportsStrictMode ? false : undefined;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
configureAiTransportHost({});
|
||||
});
|
||||
|
||||
it("enables native strict OpenAI Responses tools and normalizes schemas", () => {
|
||||
const tools = [
|
||||
{
|
||||
|
|
@ -13,27 +13,6 @@ import type {
|
|||
ResponseReasoningItem,
|
||||
ResponseStreamEvent,
|
||||
} from "openai/resources/responses/responses.js";
|
||||
import {
|
||||
resolveOpenAIReasoningEffortForModel,
|
||||
supportsOpenAIReasoningEffort,
|
||||
} from "../../agents/openai-reasoning-effort.js";
|
||||
import {
|
||||
createFirstStreamEventAbortController,
|
||||
getFirstStreamEventTimeoutHandler,
|
||||
getFirstStreamEventTimeoutMs,
|
||||
type FirstStreamEventInternalOptions,
|
||||
withFirstStreamEventTimeout,
|
||||
} from "../../agents/stream-first-event-timeout.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import {
|
||||
AZURE_RESPONSES_TEXT_CONTENT_PART_TYPE,
|
||||
OPENAI_RESPONSES_OUTPUT_TEXT_CONTENT_PART_TYPE,
|
||||
type AzureResponsesTextContentPart,
|
||||
type AzureResponsesTextDeltaEvent,
|
||||
isAzureResponsesTextDeltaEvent,
|
||||
isResponsesTextContentPartType,
|
||||
resolveResponsesMessageSnapshotCollapse,
|
||||
} from "../../shared/openai-responses-stream-compat.js";
|
||||
import { calculateCost, clampThinkingLevel } from "../model-utils.js";
|
||||
import type {
|
||||
Api,
|
||||
|
|
@ -55,6 +34,27 @@ import { shortHash } from "../utils/hash.js";
|
|||
import { headersToRecord } from "../utils/headers.js";
|
||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import {
|
||||
createFirstStreamEventAbortController,
|
||||
getFirstStreamEventTimeoutHandler,
|
||||
getFirstStreamEventTimeoutMs,
|
||||
type FirstStreamEventInternalOptions,
|
||||
withFirstStreamEventTimeout,
|
||||
} from "../utils/stream-first-event-timeout.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../utils/system-prompt-cache-boundary.js";
|
||||
import {
|
||||
resolveOpenAIReasoningEffortForModel,
|
||||
supportsOpenAIReasoningEffort,
|
||||
} from "./openai-reasoning-effort.js";
|
||||
import {
|
||||
AZURE_RESPONSES_TEXT_CONTENT_PART_TYPE,
|
||||
OPENAI_RESPONSES_OUTPUT_TEXT_CONTENT_PART_TYPE,
|
||||
type AzureResponsesTextContentPart,
|
||||
type AzureResponsesTextDeltaEvent,
|
||||
isAzureResponsesTextDeltaEvent,
|
||||
isResponsesTextContentPartType,
|
||||
resolveResponsesMessageSnapshotCollapse,
|
||||
} from "./openai-responses-stream-compat.js";
|
||||
import { convertResponsesToolPayload, convertResponsesTools } from "./openai-responses-tools.js";
|
||||
import { describeToolResultMediaPlaceholder, extractToolResultText } from "./tool-result-text.js";
|
||||
import { transformMessages } from "./transform-messages.js";
|
||||
|
|
@ -1,18 +1,14 @@
|
|||
// OpenAI Responses tool helpers convert runtime tools to Responses API schemas.
|
||||
import { createHash } from "node:crypto";
|
||||
import type { Tool as OpenAITool } from "openai/resources/responses/responses.js";
|
||||
import { resolveOpenAIStrictToolSetting } from "../../agents/openai-strict-tool-setting.js";
|
||||
import {
|
||||
projectOpenAITools,
|
||||
type OpenAIToolProjection,
|
||||
} from "../../agents/openai-tool-projection.js";
|
||||
import { getAiTransportHost } from "../host.js";
|
||||
import type { Model, Tool } from "../types.js";
|
||||
import { projectOpenAITools, type OpenAIToolProjection } from "./openai-tool-projection.js";
|
||||
import {
|
||||
findOpenAIStrictToolProjectionDiagnostics,
|
||||
normalizeOpenAIStrictToolParameters,
|
||||
resolveOpenAIProjectedToolsStrictToolFlag,
|
||||
} from "../../agents/openai-tool-schema.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import type { Model, Tool } from "../types.js";
|
||||
} from "./openai-tool-schema.js";
|
||||
|
||||
/** Options for converting internal tool schemas to OpenAI Responses function tools. */
|
||||
export interface ConvertResponsesToolsOptions {
|
||||
|
|
@ -36,7 +32,7 @@ export type ConvertedResponsesTools = {
|
|||
};
|
||||
|
||||
// Converts OpenClaw tool schemas to OpenAI Responses tools, including strict-mode compatibility.
|
||||
const log = createSubsystemLogger("llm/openai-responses");
|
||||
const LOG_SUBSYSTEM = "llm/openai-responses";
|
||||
const MAX_STRICT_TOOL_DOWNGRADE_DIAGNOSTIC_KEYS = 64;
|
||||
const loggedStrictToolDowngradeDiagnosticKeys = new Set<string>();
|
||||
|
||||
|
|
@ -83,7 +79,7 @@ function resolveResponsesStrictToolSetting(
|
|||
return options.strict;
|
||||
}
|
||||
if (options?.model) {
|
||||
return resolveOpenAIStrictToolSetting(options.model, {
|
||||
return getAiTransportHost().resolveOpenAIStrictToolSetting(options.model, {
|
||||
transport: "stream",
|
||||
supportsStrictMode: options.supportsStrictMode,
|
||||
});
|
||||
|
|
@ -97,25 +93,29 @@ function resolveResponsesStrictToolFlag(
|
|||
model: Model | undefined,
|
||||
): boolean | undefined {
|
||||
const strict = resolveOpenAIProjectedToolsStrictToolFlag(projection, strictSetting);
|
||||
if (strictSetting === true && strict === false && model && log.isEnabled("debug", "any")) {
|
||||
if (strictSetting === true && strict === false && model) {
|
||||
getAiTransportHost().logDebug(LOG_SUBSYSTEM, () => {
|
||||
const diagnostics = findOpenAIStrictToolProjectionDiagnostics(projection);
|
||||
if (shouldLogStrictToolDowngradeDiagnostic(diagnostics, model)) {
|
||||
if (!shouldLogStrictToolDowngradeDiagnostic(diagnostics, model)) {
|
||||
return null;
|
||||
}
|
||||
const sample = diagnostics.slice(0, 5).map((entry) => ({
|
||||
tool: entry.toolName ?? `tool[${entry.toolIndex}]`,
|
||||
violations: entry.violations.slice(0, 8),
|
||||
}));
|
||||
log.debug(
|
||||
return {
|
||||
message:
|
||||
`OpenAI responses tool schema strict mode downgraded to strict=false for ` +
|
||||
`${model.provider ?? "unknown"}/${model.id ?? "unknown"} because ` +
|
||||
`${diagnostics.length} tool schema(s) are not strict-compatible`,
|
||||
{
|
||||
data: {
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
incompatibleToolCount: diagnostics.length,
|
||||
sample,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
return strict;
|
||||
}
|
||||
|
|
@ -3,9 +3,11 @@
|
|||
*
|
||||
* Caches normalized object inputs by provider compatibility so repeated inventory builds preserve identity.
|
||||
*/
|
||||
import type { ModelCompatConfig } from "../config/types.models.js";
|
||||
import { shouldOmitEmptyArrayItems } from "../plugins/provider-model-compat.js";
|
||||
import { normalizeToolParameterSchema } from "./agent-tools-parameter-schema.js";
|
||||
import {
|
||||
normalizeToolParameterSchema,
|
||||
shouldOmitEmptyArrayItems,
|
||||
type ToolSchemaModelCompat,
|
||||
} from "./agent-tools-parameter-schema.js";
|
||||
import type { OpenAIToolProjection } from "./openai-tool-projection.js";
|
||||
|
||||
/**
|
||||
|
|
@ -24,7 +26,7 @@ let strictOpenAISchemaCache = new WeakMap<object, Array<{ key: string; value: un
|
|||
|
||||
function resolveToolSchemaModelCompat(
|
||||
compat: ToolSchemaCompatInput | null | undefined,
|
||||
): ModelCompatConfig | undefined {
|
||||
): ToolSchemaModelCompat | undefined {
|
||||
if (!compat) {
|
||||
return undefined;
|
||||
}
|
||||
164
packages/ai/src/providers/register-builtins.ts
Normal file
164
packages/ai/src/providers/register-builtins.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
// Built-in provider registration installs lazy protocol adapters.
|
||||
import type { ApiRegistry } from "../api-registry.js";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
AssistantMessageEvent,
|
||||
Model,
|
||||
SimpleStreamOptions,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
} from "../types.js";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
|
||||
type ProviderStreams<TApi extends Api, TOptions extends StreamOptions> = {
|
||||
stream: StreamFunction<TApi, TOptions>;
|
||||
streamSimple: StreamFunction<TApi, SimpleStreamOptions>;
|
||||
};
|
||||
|
||||
type RegisterBuiltIn = (registry: ApiRegistry) => void;
|
||||
|
||||
/** Source id used for built-in API provider registrations. */
|
||||
export const BUILT_IN_API_PROVIDER_SOURCE_ID = "core:built-in";
|
||||
|
||||
function forwardStream(
|
||||
target: AssistantMessageEventStream,
|
||||
source: AsyncIterable<AssistantMessageEvent>,
|
||||
): void {
|
||||
void (async () => {
|
||||
for await (const event of source) {
|
||||
target.push(event);
|
||||
}
|
||||
target.end();
|
||||
})();
|
||||
}
|
||||
|
||||
function createLazyLoadErrorMessage<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
error: unknown,
|
||||
): AssistantMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [],
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "error",
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
// Provider modules load on first use, while callers still receive a stream synchronously.
|
||||
function createLazyStream<TApi extends Api, TOptions extends StreamOptions, TStreams>(
|
||||
load: () => Promise<TStreams>,
|
||||
select: (streams: TStreams) => StreamFunction<TApi, TOptions>,
|
||||
): StreamFunction<TApi, TOptions> {
|
||||
return (model, context, options) => {
|
||||
const outer = new AssistantMessageEventStream();
|
||||
load()
|
||||
.then((streams) => forwardStream(outer, select(streams)(model, context, options)))
|
||||
.catch((error: unknown) => {
|
||||
const message = createLazyLoadErrorMessage(model, error);
|
||||
outer.push({ type: "error", reason: "error", error: message });
|
||||
outer.end(message);
|
||||
});
|
||||
return outer;
|
||||
};
|
||||
}
|
||||
|
||||
function createLazyRegistration<TApi extends Api, TOptions extends StreamOptions, TModule>(
|
||||
api: TApi,
|
||||
importModule: () => Promise<TModule>,
|
||||
select: (module: TModule) => ProviderStreams<TApi, TOptions>,
|
||||
): RegisterBuiltIn {
|
||||
let streamsPromise: Promise<ProviderStreams<TApi, TOptions>> | undefined;
|
||||
const load = () => (streamsPromise ??= importModule().then(select));
|
||||
const stream = createLazyStream(load, (streams) => streams.stream);
|
||||
const streamSimple = createLazyStream<TApi, SimpleStreamOptions, ProviderStreams<TApi, TOptions>>(
|
||||
load,
|
||||
(streams) => streams.streamSimple,
|
||||
);
|
||||
return (registry) => {
|
||||
registry.registerApiProvider({ api, stream, streamSimple }, BUILT_IN_API_PROVIDER_SOURCE_ID);
|
||||
};
|
||||
}
|
||||
|
||||
const registerBuiltIns: RegisterBuiltIn[] = [
|
||||
createLazyRegistration(
|
||||
"anthropic-messages",
|
||||
() => import("./anthropic.js"),
|
||||
(module) => ({ stream: module.streamAnthropic, streamSimple: module.streamSimpleAnthropic }),
|
||||
),
|
||||
createLazyRegistration(
|
||||
"openai-completions",
|
||||
() => import("./openai-completions.js"),
|
||||
(module) => ({
|
||||
stream: module.streamOpenAICompletions,
|
||||
streamSimple: module.streamSimpleOpenAICompletions,
|
||||
}),
|
||||
),
|
||||
createLazyRegistration(
|
||||
"mistral-conversations",
|
||||
() => import("./mistral.js"),
|
||||
(module) => ({ stream: module.streamMistral, streamSimple: module.streamSimpleMistral }),
|
||||
),
|
||||
createLazyRegistration(
|
||||
"openai-responses",
|
||||
() => import("./openai-responses.js"),
|
||||
(module) => ({
|
||||
stream: module.streamOpenAIResponses,
|
||||
streamSimple: module.streamSimpleOpenAIResponses,
|
||||
}),
|
||||
),
|
||||
createLazyRegistration(
|
||||
"azure-openai-responses",
|
||||
() => import("./azure-openai-responses.js"),
|
||||
(module) => ({
|
||||
stream: module.streamAzureOpenAIResponses,
|
||||
streamSimple: module.streamSimpleAzureOpenAIResponses,
|
||||
}),
|
||||
),
|
||||
createLazyRegistration(
|
||||
"openai-chatgpt-responses",
|
||||
() => import("./openai-chatgpt-responses.js"),
|
||||
(module) => ({
|
||||
stream: module.streamOpenAICodexResponses,
|
||||
streamSimple: module.streamSimpleOpenAICodexResponses,
|
||||
}),
|
||||
),
|
||||
createLazyRegistration(
|
||||
"google-generative-ai",
|
||||
() => import("./google.js"),
|
||||
(module) => ({ stream: module.streamGoogle, streamSimple: module.streamSimpleGoogle }),
|
||||
),
|
||||
createLazyRegistration(
|
||||
"google-vertex",
|
||||
() => import("./google-vertex.js"),
|
||||
(module) => ({
|
||||
stream: module.streamGoogleVertex,
|
||||
streamSimple: module.streamSimpleGoogleVertex,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
/** Registers every built-in API provider in one runtime registry. */
|
||||
export function registerBuiltInApiProviders(registry: ApiRegistry): void {
|
||||
for (const register of registerBuiltIns) {
|
||||
register(registry);
|
||||
}
|
||||
}
|
||||
|
||||
/** Restores the built-in provider registry state for tests. */
|
||||
export function resetApiProviders(registry: ApiRegistry): void {
|
||||
registry.unregisterApiProviders(BUILT_IN_API_PROVIDER_SOURCE_ID);
|
||||
registerBuiltInApiProviders(registry);
|
||||
}
|
||||
|
|
@ -2,73 +2,6 @@ import { describe, expect, it } from "vitest";
|
|||
import { describeToolResultMediaPlaceholder, extractToolResultText } from "./tool-result-text.js";
|
||||
|
||||
describe("extractToolResultText", () => {
|
||||
it("redacts structured secret fields with the shared tool-payload contract", () => {
|
||||
const text = extractToolResultText([
|
||||
{
|
||||
type: "json",
|
||||
apiToken: "api-token-value-1234567890",
|
||||
privateKey: "private-key-value-1234567890",
|
||||
private_key: "private-key-snake-1234567890",
|
||||
key: "generic-key-value-1234567890",
|
||||
keyMaterial: "key-material-value-1234567890",
|
||||
bearerToken: "bearer-token-value-1234567890",
|
||||
bearer_token: "bearer-token-snake-value-1234567890",
|
||||
jwt: "jwt-value-1234567890",
|
||||
session: "session-value-1234567890",
|
||||
code: "code-value-1234567890",
|
||||
error: { code: "ERR_VISIBLE_PROVIDER_CODE" },
|
||||
oauth: { code: "OPAQUEPROVIDERCODE1234567890" },
|
||||
providerError: { error: { code: "ERR_VISIBLE_PROVIDER_NESTED_CODE" } },
|
||||
signature: "signature-value-1234567890",
|
||||
cookie: "cookie-value-1234567890",
|
||||
"set-cookie": "set-cookie-value-1234567890",
|
||||
paymentCredential: "payment-credential-value-1234567890",
|
||||
cardNumber: 4111111111111111,
|
||||
cvc: 123,
|
||||
text: '{"apiToken":"api-token-in-text-1234567890","code":"oauth-code-in-text-1234567890","safe":"ok"}',
|
||||
credential: "live-credential-value",
|
||||
appSecret: "app-secret-value",
|
||||
rawSecret: "raw-secret-value",
|
||||
nested: {
|
||||
token: "nested-token-value",
|
||||
visible: "safe-value",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(text).toContain('"credential":"');
|
||||
expect(text).toContain('"appSecret":"');
|
||||
expect(text).toContain('"rawSecret":"');
|
||||
expect(text).toContain('"token":"');
|
||||
expect(text).toContain('"visible":"safe-value"');
|
||||
expect(text).toContain('"code":"ERR_VISIBLE_PROVIDER_CODE"');
|
||||
expect(text).toContain('"code":"ERR_VISIBLE_PROVIDER_NESTED_CODE"');
|
||||
expect(text).not.toContain("api-token-value-1234567890");
|
||||
expect(text).not.toContain("private-key-value-1234567890");
|
||||
expect(text).not.toContain("private-key-snake-1234567890");
|
||||
expect(text).not.toContain("generic-key-value-1234567890");
|
||||
expect(text).not.toContain("key-material-value-1234567890");
|
||||
expect(text).not.toContain("bearer-token-value-1234567890");
|
||||
expect(text).not.toContain("bearer-token-snake-value-1234567890");
|
||||
expect(text).not.toContain("jwt-value-1234567890");
|
||||
expect(text).not.toContain("session-value-1234567890");
|
||||
expect(text).not.toContain("code-value-1234567890");
|
||||
expect(text).not.toContain("OPAQUEPROVIDERCODE1234567890");
|
||||
expect(text).not.toContain("signature-value-1234567890");
|
||||
expect(text).not.toContain("cookie-value-1234567890");
|
||||
expect(text).not.toContain("set-cookie-value-1234567890");
|
||||
expect(text).not.toContain("payment-credential-value-1234567890");
|
||||
expect(text).not.toContain("4111111111111111");
|
||||
expect(text).not.toContain('"cvc":123');
|
||||
expect(text).not.toContain("api-token-in-text-1234567890");
|
||||
expect(text).not.toContain("oauth-code-in-text-1234567890");
|
||||
expect(text).toContain('\\"safe\\":\\"ok\\"');
|
||||
expect(text).not.toContain("live-credential-value");
|
||||
expect(text).not.toContain("app-secret-value");
|
||||
expect(text).not.toContain("raw-secret-value");
|
||||
expect(text).not.toContain("nested-token-value");
|
||||
});
|
||||
|
||||
it("keeps media-only blocks out of provider replay text", () => {
|
||||
const text = extractToolResultText([
|
||||
{ type: "text", text: "summary" },
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { redactSecrets, redactToolPayloadText } from "../../logging/redact.js";
|
||||
import { truncateUtf16Safe } from "../../shared/utf16-slice.js";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { getAiTransportHost } from "../host.js";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
|
||||
const PROVIDER_TOOL_RESULT_MAX_CHARS = 8000;
|
||||
|
|
@ -55,13 +55,14 @@ function redactInlineDataUris(value: string): string {
|
|||
}
|
||||
|
||||
function redactStructuredTextValue(value: string): string {
|
||||
const redacted = redactToolPayloadText(value);
|
||||
const host = getAiTransportHost();
|
||||
const redacted = host.redactToolPayloadText(value);
|
||||
const trimmed = redacted.trim();
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
||||
return redacted;
|
||||
}
|
||||
try {
|
||||
const redactedWrapper = redactSecrets({ structuredTextValue: JSON.parse(redacted) });
|
||||
const redactedWrapper = host.redactSecrets({ structuredTextValue: JSON.parse(redacted) });
|
||||
return JSON.stringify(redactedWrapper.structuredTextValue);
|
||||
} catch {
|
||||
return redacted;
|
||||
|
|
@ -71,7 +72,7 @@ function redactStructuredTextValue(value: string): string {
|
|||
function stringifyStructuredBlock(block: Record<string, unknown>): string | undefined {
|
||||
const seen = new WeakSet<object>();
|
||||
try {
|
||||
const redactedWrapper = redactSecrets({ structuredToolResult: block });
|
||||
const redactedWrapper = getAiTransportHost().redactSecrets({ structuredToolResult: block });
|
||||
const redactedBlock = redactedWrapper.structuredToolResult;
|
||||
const serialized = JSON.stringify(
|
||||
redactedBlock,
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { resolveModelBoundThinkingReplayMode } from "../../shared/anthropic-model-contract.js";
|
||||
// Provider message transform helpers convert runtime messages to provider payloads.
|
||||
import type {
|
||||
Api,
|
||||
|
|
@ -10,6 +9,7 @@ import type {
|
|||
ToolCall,
|
||||
ToolResultMessage,
|
||||
} from "../types.js";
|
||||
import { resolveModelBoundThinkingReplayMode } from "./anthropic-model-contract.js";
|
||||
|
||||
const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
|
||||
const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
|
||||
59
packages/ai/src/stream.ts
Normal file
59
packages/ai/src/stream.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// LLM Runtime module implements stream behavior.
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
AssistantMessageEventStreamContract,
|
||||
Context,
|
||||
Model,
|
||||
ProviderStreamOptions,
|
||||
SimpleStreamOptions,
|
||||
StreamOptions,
|
||||
} from "@openclaw/llm-core";
|
||||
import { createApiRegistry, type ApiRegistry } from "./api-registry.js";
|
||||
|
||||
/** Creates an isolated LLM runtime backed by the supplied provider registry. */
|
||||
export function createLlmRuntime(registry: ApiRegistry = createApiRegistry()) {
|
||||
function resolveApiProvider(api: Api) {
|
||||
const provider = registry.getApiProvider(api);
|
||||
if (!provider) {
|
||||
throw new Error(`No API provider registered for api: ${api}`);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
function stream<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ProviderStreamOptions,
|
||||
): AssistantMessageEventStreamContract {
|
||||
return resolveApiProvider(model.api).stream(model, context, options as StreamOptions);
|
||||
}
|
||||
|
||||
async function complete<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ProviderStreamOptions,
|
||||
): Promise<AssistantMessage> {
|
||||
return stream(model, context, options).result();
|
||||
}
|
||||
|
||||
function streamSimple<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStreamContract {
|
||||
return resolveApiProvider(model.api).streamSimple(model, context, options);
|
||||
}
|
||||
|
||||
async function completeSimple<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): Promise<AssistantMessage> {
|
||||
return streamSimple(model, context, options).result();
|
||||
}
|
||||
|
||||
return { registry, stream, complete, streamSimple, completeSimple };
|
||||
}
|
||||
|
||||
export type LlmRuntime = ReturnType<typeof createLlmRuntime>;
|
||||
2
packages/ai/src/types.ts
Normal file
2
packages/ai/src/types.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/** Shared model, message, tool, and streaming contracts. */
|
||||
export * from "@openclaw/llm-core";
|
||||
2
packages/ai/src/utils/diagnostics.ts
Normal file
2
packages/ai/src/utils/diagnostics.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/** Shared provider diagnostics. */
|
||||
export * from "@openclaw/llm-core/diagnostics";
|
||||
2
packages/ai/src/utils/event-stream.ts
Normal file
2
packages/ai/src/utils/event-stream.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/** Assistant message event stream implementation. */
|
||||
export * from "@openclaw/llm-core/event-stream";
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue