mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-12 23:46:04 +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:
committed by
GitHub
parent
913311845e
commit
062f88e3e3
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
|
||||
|
||||
Reference in New Issue
Block a user